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]
2801async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2802 init_test(cx, |settings| {
2803 settings.defaults.tab_size = NonZeroU32::new(4)
2804 });
2805
2806 let language = Arc::new(Language::new(
2807 LanguageConfig {
2808 documentation_block: Some(vec!["/**".into(), "*/".into()]),
2809 documentation_comment_prefix: Some("*".into()),
2810 ..LanguageConfig::default()
2811 },
2812 None,
2813 ));
2814 {
2815 let mut cx = EditorTestContext::new(cx).await;
2816 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2817 cx.set_state(indoc! {"
2818 /**ˇ
2819 "});
2820
2821 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2822 cx.assert_editor_state(indoc! {"
2823 /**
2824 *ˇ
2825 "});
2826 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2827 cx.set_state(indoc! {"
2828 ˇ/**
2829 "});
2830 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2831 cx.assert_editor_state(indoc! {"
2832
2833 ˇ/**
2834 "});
2835 // Ensure that if cursor is between it doesn't add comment prefix.
2836 cx.set_state(indoc! {"
2837 /*ˇ*
2838 "});
2839 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2840 cx.assert_editor_state(indoc! {"
2841 /*
2842 ˇ*
2843 "});
2844 // Ensure that if suffix exists on same line after cursor it adds new line.
2845 cx.set_state(indoc! {"
2846 /**ˇ*/
2847 "});
2848 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2849 cx.assert_editor_state(indoc! {"
2850 /**
2851 *ˇ
2852 */
2853 "});
2854 // Ensure that it detects suffix after existing prefix.
2855 cx.set_state(indoc! {"
2856 /**ˇ/
2857 "});
2858 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2859 cx.assert_editor_state(indoc! {"
2860 /**
2861 ˇ/
2862 "});
2863 // Ensure that if suffix exists on same line before cursor it does not add comment prefix.
2864 cx.set_state(indoc! {"
2865 /** */ˇ
2866 "});
2867 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2868 cx.assert_editor_state(indoc! {"
2869 /** */
2870 ˇ
2871 "});
2872 // Ensure that if suffix exists on same line before cursor it does not add comment prefix.
2873 cx.set_state(indoc! {"
2874 /**
2875 *
2876 */ˇ
2877 "});
2878 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2879 cx.assert_editor_state(indoc! {"
2880 /**
2881 *
2882 */
2883 ˇ
2884 "});
2885 }
2886 // Ensure that comment continuations can be disabled.
2887 update_test_language_settings(cx, |settings| {
2888 settings.defaults.extend_comment_on_newline = Some(false);
2889 });
2890 let mut cx = EditorTestContext::new(cx).await;
2891 cx.set_state(indoc! {"
2892 /**ˇ
2893 "});
2894 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2895 cx.assert_editor_state(indoc! {"
2896 /**
2897 ˇ
2898 "});
2899}
2900
2901#[gpui::test]
2902fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2903 init_test(cx, |_| {});
2904
2905 let editor = cx.add_window(|window, cx| {
2906 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2907 let mut editor = build_editor(buffer.clone(), window, cx);
2908 editor.change_selections(None, window, cx, |s| {
2909 s.select_ranges([3..4, 11..12, 19..20])
2910 });
2911 editor
2912 });
2913
2914 _ = editor.update(cx, |editor, window, cx| {
2915 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2916 editor.buffer.update(cx, |buffer, cx| {
2917 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2918 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2919 });
2920 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2921
2922 editor.insert("Z", window, cx);
2923 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2924
2925 // The selections are moved after the inserted characters
2926 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2927 });
2928}
2929
2930#[gpui::test]
2931async fn test_tab(cx: &mut TestAppContext) {
2932 init_test(cx, |settings| {
2933 settings.defaults.tab_size = NonZeroU32::new(3)
2934 });
2935
2936 let mut cx = EditorTestContext::new(cx).await;
2937 cx.set_state(indoc! {"
2938 ˇabˇc
2939 ˇ🏀ˇ🏀ˇefg
2940 dˇ
2941 "});
2942 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2943 cx.assert_editor_state(indoc! {"
2944 ˇab ˇc
2945 ˇ🏀 ˇ🏀 ˇefg
2946 d ˇ
2947 "});
2948
2949 cx.set_state(indoc! {"
2950 a
2951 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2952 "});
2953 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2954 cx.assert_editor_state(indoc! {"
2955 a
2956 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2957 "});
2958}
2959
2960#[gpui::test]
2961async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2962 init_test(cx, |_| {});
2963
2964 let mut cx = EditorTestContext::new(cx).await;
2965 let language = Arc::new(
2966 Language::new(
2967 LanguageConfig::default(),
2968 Some(tree_sitter_rust::LANGUAGE.into()),
2969 )
2970 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2971 .unwrap(),
2972 );
2973 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2974
2975 // test when all cursors are not at suggested indent
2976 // then simply move to their suggested indent location
2977 cx.set_state(indoc! {"
2978 const a: B = (
2979 c(
2980 ˇ
2981 ˇ )
2982 );
2983 "});
2984 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2985 cx.assert_editor_state(indoc! {"
2986 const a: B = (
2987 c(
2988 ˇ
2989 ˇ)
2990 );
2991 "});
2992
2993 // test cursor already at suggested indent not moving when
2994 // other cursors are yet to reach their suggested indents
2995 cx.set_state(indoc! {"
2996 ˇ
2997 const a: B = (
2998 c(
2999 d(
3000 ˇ
3001 )
3002 ˇ
3003 ˇ )
3004 );
3005 "});
3006 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3007 cx.assert_editor_state(indoc! {"
3008 ˇ
3009 const a: B = (
3010 c(
3011 d(
3012 ˇ
3013 )
3014 ˇ
3015 ˇ)
3016 );
3017 "});
3018 // test when all cursors are at suggested indent then tab is inserted
3019 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3020 cx.assert_editor_state(indoc! {"
3021 ˇ
3022 const a: B = (
3023 c(
3024 d(
3025 ˇ
3026 )
3027 ˇ
3028 ˇ)
3029 );
3030 "});
3031
3032 // test when current indent is less than suggested indent,
3033 // we adjust line to match suggested indent and move cursor to it
3034 //
3035 // when no other cursor is at word boundary, all of them should move
3036 cx.set_state(indoc! {"
3037 const a: B = (
3038 c(
3039 d(
3040 ˇ
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 d(
3050 ˇ
3051 ˇ)
3052 ˇ)
3053 );
3054 "});
3055
3056 // test when current indent is less than suggested indent,
3057 // we adjust line to match suggested indent and move cursor to it
3058 //
3059 // when some other cursor is at word boundary, it should not move
3060 cx.set_state(indoc! {"
3061 const a: B = (
3062 c(
3063 d(
3064 ˇ
3065 ˇ )
3066 ˇ)
3067 );
3068 "});
3069 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3070 cx.assert_editor_state(indoc! {"
3071 const a: B = (
3072 c(
3073 d(
3074 ˇ
3075 ˇ)
3076 ˇ)
3077 );
3078 "});
3079
3080 // test when current indent is more than suggested indent,
3081 // we just move cursor to current indent instead of suggested indent
3082 //
3083 // when no other cursor is at word boundary, all of them should move
3084 cx.set_state(indoc! {"
3085 const a: B = (
3086 c(
3087 d(
3088 ˇ
3089 ˇ )
3090 ˇ )
3091 );
3092 "});
3093 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3094 cx.assert_editor_state(indoc! {"
3095 const a: B = (
3096 c(
3097 d(
3098 ˇ
3099 ˇ)
3100 ˇ)
3101 );
3102 "});
3103 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3104 cx.assert_editor_state(indoc! {"
3105 const a: B = (
3106 c(
3107 d(
3108 ˇ
3109 ˇ)
3110 ˇ)
3111 );
3112 "});
3113
3114 // test when current indent is more than suggested indent,
3115 // we just move cursor to current indent instead of suggested indent
3116 //
3117 // when some other cursor is at word boundary, it doesn't move
3118 cx.set_state(indoc! {"
3119 const a: B = (
3120 c(
3121 d(
3122 ˇ
3123 ˇ )
3124 ˇ)
3125 );
3126 "});
3127 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 const a: B = (
3130 c(
3131 d(
3132 ˇ
3133 ˇ)
3134 ˇ)
3135 );
3136 "});
3137
3138 // handle auto-indent when there are multiple cursors on the same line
3139 cx.set_state(indoc! {"
3140 const a: B = (
3141 c(
3142 ˇ ˇ
3143 ˇ )
3144 );
3145 "});
3146 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3147 cx.assert_editor_state(indoc! {"
3148 const a: B = (
3149 c(
3150 ˇ
3151 ˇ)
3152 );
3153 "});
3154}
3155
3156#[gpui::test]
3157async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3158 init_test(cx, |settings| {
3159 settings.defaults.tab_size = NonZeroU32::new(3)
3160 });
3161
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.set_state(indoc! {"
3164 ˇ
3165 \t ˇ
3166 \t ˇ
3167 \t ˇ
3168 \t \t\t \t \t\t \t\t \t \t ˇ
3169 "});
3170
3171 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3172 cx.assert_editor_state(indoc! {"
3173 ˇ
3174 \t ˇ
3175 \t ˇ
3176 \t ˇ
3177 \t \t\t \t \t\t \t\t \t \t ˇ
3178 "});
3179}
3180
3181#[gpui::test]
3182async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3183 init_test(cx, |settings| {
3184 settings.defaults.tab_size = NonZeroU32::new(4)
3185 });
3186
3187 let language = Arc::new(
3188 Language::new(
3189 LanguageConfig::default(),
3190 Some(tree_sitter_rust::LANGUAGE.into()),
3191 )
3192 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3193 .unwrap(),
3194 );
3195
3196 let mut cx = EditorTestContext::new(cx).await;
3197 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3198 cx.set_state(indoc! {"
3199 fn a() {
3200 if b {
3201 \t ˇc
3202 }
3203 }
3204 "});
3205
3206 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3207 cx.assert_editor_state(indoc! {"
3208 fn a() {
3209 if b {
3210 ˇc
3211 }
3212 }
3213 "});
3214}
3215
3216#[gpui::test]
3217async fn test_indent_outdent(cx: &mut TestAppContext) {
3218 init_test(cx, |settings| {
3219 settings.defaults.tab_size = NonZeroU32::new(4);
3220 });
3221
3222 let mut cx = EditorTestContext::new(cx).await;
3223
3224 cx.set_state(indoc! {"
3225 «oneˇ» «twoˇ»
3226 three
3227 four
3228 "});
3229 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3230 cx.assert_editor_state(indoc! {"
3231 «oneˇ» «twoˇ»
3232 three
3233 four
3234 "});
3235
3236 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3237 cx.assert_editor_state(indoc! {"
3238 «oneˇ» «twoˇ»
3239 three
3240 four
3241 "});
3242
3243 // select across line ending
3244 cx.set_state(indoc! {"
3245 one two
3246 t«hree
3247 ˇ» four
3248 "});
3249 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3250 cx.assert_editor_state(indoc! {"
3251 one two
3252 t«hree
3253 ˇ» four
3254 "});
3255
3256 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3257 cx.assert_editor_state(indoc! {"
3258 one two
3259 t«hree
3260 ˇ» four
3261 "});
3262
3263 // Ensure that indenting/outdenting works when the cursor is at column 0.
3264 cx.set_state(indoc! {"
3265 one two
3266 ˇthree
3267 four
3268 "});
3269 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3270 cx.assert_editor_state(indoc! {"
3271 one two
3272 ˇthree
3273 four
3274 "});
3275
3276 cx.set_state(indoc! {"
3277 one two
3278 ˇ three
3279 four
3280 "});
3281 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3282 cx.assert_editor_state(indoc! {"
3283 one two
3284 ˇthree
3285 four
3286 "});
3287}
3288
3289#[gpui::test]
3290async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3291 init_test(cx, |settings| {
3292 settings.defaults.hard_tabs = Some(true);
3293 });
3294
3295 let mut cx = EditorTestContext::new(cx).await;
3296
3297 // select two ranges on one line
3298 cx.set_state(indoc! {"
3299 «oneˇ» «twoˇ»
3300 three
3301 four
3302 "});
3303 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3304 cx.assert_editor_state(indoc! {"
3305 \t«oneˇ» «twoˇ»
3306 three
3307 four
3308 "});
3309 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 \t\t«oneˇ» «twoˇ»
3312 three
3313 four
3314 "});
3315 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3316 cx.assert_editor_state(indoc! {"
3317 \t«oneˇ» «twoˇ»
3318 three
3319 four
3320 "});
3321 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3322 cx.assert_editor_state(indoc! {"
3323 «oneˇ» «twoˇ»
3324 three
3325 four
3326 "});
3327
3328 // select across a line ending
3329 cx.set_state(indoc! {"
3330 one two
3331 t«hree
3332 ˇ»four
3333 "});
3334 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3335 cx.assert_editor_state(indoc! {"
3336 one two
3337 \tt«hree
3338 ˇ»four
3339 "});
3340 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3341 cx.assert_editor_state(indoc! {"
3342 one two
3343 \t\tt«hree
3344 ˇ»four
3345 "});
3346 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3347 cx.assert_editor_state(indoc! {"
3348 one two
3349 \tt«hree
3350 ˇ»four
3351 "});
3352 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3353 cx.assert_editor_state(indoc! {"
3354 one two
3355 t«hree
3356 ˇ»four
3357 "});
3358
3359 // Ensure that indenting/outdenting works when the cursor is at column 0.
3360 cx.set_state(indoc! {"
3361 one two
3362 ˇthree
3363 four
3364 "});
3365 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3366 cx.assert_editor_state(indoc! {"
3367 one two
3368 ˇthree
3369 four
3370 "});
3371 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3372 cx.assert_editor_state(indoc! {"
3373 one two
3374 \tˇthree
3375 four
3376 "});
3377 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3378 cx.assert_editor_state(indoc! {"
3379 one two
3380 ˇthree
3381 four
3382 "});
3383}
3384
3385#[gpui::test]
3386fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3387 init_test(cx, |settings| {
3388 settings.languages.extend([
3389 (
3390 "TOML".into(),
3391 LanguageSettingsContent {
3392 tab_size: NonZeroU32::new(2),
3393 ..Default::default()
3394 },
3395 ),
3396 (
3397 "Rust".into(),
3398 LanguageSettingsContent {
3399 tab_size: NonZeroU32::new(4),
3400 ..Default::default()
3401 },
3402 ),
3403 ]);
3404 });
3405
3406 let toml_language = Arc::new(Language::new(
3407 LanguageConfig {
3408 name: "TOML".into(),
3409 ..Default::default()
3410 },
3411 None,
3412 ));
3413 let rust_language = Arc::new(Language::new(
3414 LanguageConfig {
3415 name: "Rust".into(),
3416 ..Default::default()
3417 },
3418 None,
3419 ));
3420
3421 let toml_buffer =
3422 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3423 let rust_buffer =
3424 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3425 let multibuffer = cx.new(|cx| {
3426 let mut multibuffer = MultiBuffer::new(ReadWrite);
3427 multibuffer.push_excerpts(
3428 toml_buffer.clone(),
3429 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3430 cx,
3431 );
3432 multibuffer.push_excerpts(
3433 rust_buffer.clone(),
3434 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3435 cx,
3436 );
3437 multibuffer
3438 });
3439
3440 cx.add_window(|window, cx| {
3441 let mut editor = build_editor(multibuffer, window, cx);
3442
3443 assert_eq!(
3444 editor.text(cx),
3445 indoc! {"
3446 a = 1
3447 b = 2
3448
3449 const c: usize = 3;
3450 "}
3451 );
3452
3453 select_ranges(
3454 &mut editor,
3455 indoc! {"
3456 «aˇ» = 1
3457 b = 2
3458
3459 «const c:ˇ» usize = 3;
3460 "},
3461 window,
3462 cx,
3463 );
3464
3465 editor.tab(&Tab, window, cx);
3466 assert_text_with_selections(
3467 &mut editor,
3468 indoc! {"
3469 «aˇ» = 1
3470 b = 2
3471
3472 «const c:ˇ» usize = 3;
3473 "},
3474 cx,
3475 );
3476 editor.backtab(&Backtab, window, cx);
3477 assert_text_with_selections(
3478 &mut editor,
3479 indoc! {"
3480 «aˇ» = 1
3481 b = 2
3482
3483 «const c:ˇ» usize = 3;
3484 "},
3485 cx,
3486 );
3487
3488 editor
3489 });
3490}
3491
3492#[gpui::test]
3493async fn test_backspace(cx: &mut TestAppContext) {
3494 init_test(cx, |_| {});
3495
3496 let mut cx = EditorTestContext::new(cx).await;
3497
3498 // Basic backspace
3499 cx.set_state(indoc! {"
3500 onˇe two three
3501 fou«rˇ» five six
3502 seven «ˇeight nine
3503 »ten
3504 "});
3505 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3506 cx.assert_editor_state(indoc! {"
3507 oˇe two three
3508 fouˇ five six
3509 seven ˇten
3510 "});
3511
3512 // Test backspace inside and around indents
3513 cx.set_state(indoc! {"
3514 zero
3515 ˇone
3516 ˇtwo
3517 ˇ ˇ ˇ three
3518 ˇ ˇ four
3519 "});
3520 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 zero
3523 ˇone
3524 ˇtwo
3525 ˇ threeˇ four
3526 "});
3527}
3528
3529#[gpui::test]
3530async fn test_delete(cx: &mut TestAppContext) {
3531 init_test(cx, |_| {});
3532
3533 let mut cx = EditorTestContext::new(cx).await;
3534 cx.set_state(indoc! {"
3535 onˇe two three
3536 fou«rˇ» five six
3537 seven «ˇeight nine
3538 »ten
3539 "});
3540 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3541 cx.assert_editor_state(indoc! {"
3542 onˇ two three
3543 fouˇ five six
3544 seven ˇten
3545 "});
3546}
3547
3548#[gpui::test]
3549fn test_delete_line(cx: &mut TestAppContext) {
3550 init_test(cx, |_| {});
3551
3552 let editor = cx.add_window(|window, cx| {
3553 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3554 build_editor(buffer, window, cx)
3555 });
3556 _ = editor.update(cx, |editor, window, cx| {
3557 editor.change_selections(None, window, cx, |s| {
3558 s.select_display_ranges([
3559 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3560 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3561 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3562 ])
3563 });
3564 editor.delete_line(&DeleteLine, window, cx);
3565 assert_eq!(editor.display_text(cx), "ghi");
3566 assert_eq!(
3567 editor.selections.display_ranges(cx),
3568 vec![
3569 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3570 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3571 ]
3572 );
3573 });
3574
3575 let editor = cx.add_window(|window, cx| {
3576 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3577 build_editor(buffer, window, cx)
3578 });
3579 _ = editor.update(cx, |editor, window, cx| {
3580 editor.change_selections(None, window, cx, |s| {
3581 s.select_display_ranges([
3582 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3583 ])
3584 });
3585 editor.delete_line(&DeleteLine, window, cx);
3586 assert_eq!(editor.display_text(cx), "ghi\n");
3587 assert_eq!(
3588 editor.selections.display_ranges(cx),
3589 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3590 );
3591 });
3592}
3593
3594#[gpui::test]
3595fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3596 init_test(cx, |_| {});
3597
3598 cx.add_window(|window, cx| {
3599 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3600 let mut editor = build_editor(buffer.clone(), window, cx);
3601 let buffer = buffer.read(cx).as_singleton().unwrap();
3602
3603 assert_eq!(
3604 editor.selections.ranges::<Point>(cx),
3605 &[Point::new(0, 0)..Point::new(0, 0)]
3606 );
3607
3608 // When on single line, replace newline at end by space
3609 editor.join_lines(&JoinLines, window, cx);
3610 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3611 assert_eq!(
3612 editor.selections.ranges::<Point>(cx),
3613 &[Point::new(0, 3)..Point::new(0, 3)]
3614 );
3615
3616 // When multiple lines are selected, remove newlines that are spanned by the selection
3617 editor.change_selections(None, window, cx, |s| {
3618 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3619 });
3620 editor.join_lines(&JoinLines, window, cx);
3621 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3622 assert_eq!(
3623 editor.selections.ranges::<Point>(cx),
3624 &[Point::new(0, 11)..Point::new(0, 11)]
3625 );
3626
3627 // Undo should be transactional
3628 editor.undo(&Undo, window, cx);
3629 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3630 assert_eq!(
3631 editor.selections.ranges::<Point>(cx),
3632 &[Point::new(0, 5)..Point::new(2, 2)]
3633 );
3634
3635 // When joining an empty line don't insert a space
3636 editor.change_selections(None, window, cx, |s| {
3637 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3638 });
3639 editor.join_lines(&JoinLines, window, cx);
3640 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3641 assert_eq!(
3642 editor.selections.ranges::<Point>(cx),
3643 [Point::new(2, 3)..Point::new(2, 3)]
3644 );
3645
3646 // We can remove trailing newlines
3647 editor.join_lines(&JoinLines, window, cx);
3648 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3649 assert_eq!(
3650 editor.selections.ranges::<Point>(cx),
3651 [Point::new(2, 3)..Point::new(2, 3)]
3652 );
3653
3654 // We don't blow up on the last line
3655 editor.join_lines(&JoinLines, window, cx);
3656 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3657 assert_eq!(
3658 editor.selections.ranges::<Point>(cx),
3659 [Point::new(2, 3)..Point::new(2, 3)]
3660 );
3661
3662 // reset to test indentation
3663 editor.buffer.update(cx, |buffer, cx| {
3664 buffer.edit(
3665 [
3666 (Point::new(1, 0)..Point::new(1, 2), " "),
3667 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3668 ],
3669 None,
3670 cx,
3671 )
3672 });
3673
3674 // We remove any leading spaces
3675 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3676 editor.change_selections(None, window, cx, |s| {
3677 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3678 });
3679 editor.join_lines(&JoinLines, window, cx);
3680 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3681
3682 // We don't insert a space for a line containing only spaces
3683 editor.join_lines(&JoinLines, window, cx);
3684 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3685
3686 // We ignore any leading tabs
3687 editor.join_lines(&JoinLines, window, cx);
3688 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3689
3690 editor
3691 });
3692}
3693
3694#[gpui::test]
3695fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3696 init_test(cx, |_| {});
3697
3698 cx.add_window(|window, cx| {
3699 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3700 let mut editor = build_editor(buffer.clone(), window, cx);
3701 let buffer = buffer.read(cx).as_singleton().unwrap();
3702
3703 editor.change_selections(None, window, cx, |s| {
3704 s.select_ranges([
3705 Point::new(0, 2)..Point::new(1, 1),
3706 Point::new(1, 2)..Point::new(1, 2),
3707 Point::new(3, 1)..Point::new(3, 2),
3708 ])
3709 });
3710
3711 editor.join_lines(&JoinLines, window, cx);
3712 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3713
3714 assert_eq!(
3715 editor.selections.ranges::<Point>(cx),
3716 [
3717 Point::new(0, 7)..Point::new(0, 7),
3718 Point::new(1, 3)..Point::new(1, 3)
3719 ]
3720 );
3721 editor
3722 });
3723}
3724
3725#[gpui::test]
3726async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3727 init_test(cx, |_| {});
3728
3729 let mut cx = EditorTestContext::new(cx).await;
3730
3731 let diff_base = r#"
3732 Line 0
3733 Line 1
3734 Line 2
3735 Line 3
3736 "#
3737 .unindent();
3738
3739 cx.set_state(
3740 &r#"
3741 ˇLine 0
3742 Line 1
3743 Line 2
3744 Line 3
3745 "#
3746 .unindent(),
3747 );
3748
3749 cx.set_head_text(&diff_base);
3750 executor.run_until_parked();
3751
3752 // Join lines
3753 cx.update_editor(|editor, window, cx| {
3754 editor.join_lines(&JoinLines, window, cx);
3755 });
3756 executor.run_until_parked();
3757
3758 cx.assert_editor_state(
3759 &r#"
3760 Line 0ˇ Line 1
3761 Line 2
3762 Line 3
3763 "#
3764 .unindent(),
3765 );
3766 // Join again
3767 cx.update_editor(|editor, window, cx| {
3768 editor.join_lines(&JoinLines, window, cx);
3769 });
3770 executor.run_until_parked();
3771
3772 cx.assert_editor_state(
3773 &r#"
3774 Line 0 Line 1ˇ Line 2
3775 Line 3
3776 "#
3777 .unindent(),
3778 );
3779}
3780
3781#[gpui::test]
3782async fn test_custom_newlines_cause_no_false_positive_diffs(
3783 executor: BackgroundExecutor,
3784 cx: &mut TestAppContext,
3785) {
3786 init_test(cx, |_| {});
3787 let mut cx = EditorTestContext::new(cx).await;
3788 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3789 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3790 executor.run_until_parked();
3791
3792 cx.update_editor(|editor, window, cx| {
3793 let snapshot = editor.snapshot(window, cx);
3794 assert_eq!(
3795 snapshot
3796 .buffer_snapshot
3797 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3798 .collect::<Vec<_>>(),
3799 Vec::new(),
3800 "Should not have any diffs for files with custom newlines"
3801 );
3802 });
3803}
3804
3805#[gpui::test]
3806async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3807 init_test(cx, |_| {});
3808
3809 let mut cx = EditorTestContext::new(cx).await;
3810
3811 // Test sort_lines_case_insensitive()
3812 cx.set_state(indoc! {"
3813 «z
3814 y
3815 x
3816 Z
3817 Y
3818 Xˇ»
3819 "});
3820 cx.update_editor(|e, window, cx| {
3821 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3822 });
3823 cx.assert_editor_state(indoc! {"
3824 «x
3825 X
3826 y
3827 Y
3828 z
3829 Zˇ»
3830 "});
3831
3832 // Test reverse_lines()
3833 cx.set_state(indoc! {"
3834 «5
3835 4
3836 3
3837 2
3838 1ˇ»
3839 "});
3840 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3841 cx.assert_editor_state(indoc! {"
3842 «1
3843 2
3844 3
3845 4
3846 5ˇ»
3847 "});
3848
3849 // Skip testing shuffle_line()
3850
3851 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3852 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3853
3854 // Don't manipulate when cursor is on single line, but expand the selection
3855 cx.set_state(indoc! {"
3856 ddˇdd
3857 ccc
3858 bb
3859 a
3860 "});
3861 cx.update_editor(|e, window, cx| {
3862 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3863 });
3864 cx.assert_editor_state(indoc! {"
3865 «ddddˇ»
3866 ccc
3867 bb
3868 a
3869 "});
3870
3871 // Basic manipulate case
3872 // Start selection moves to column 0
3873 // End of selection shrinks to fit shorter line
3874 cx.set_state(indoc! {"
3875 dd«d
3876 ccc
3877 bb
3878 aaaaaˇ»
3879 "});
3880 cx.update_editor(|e, window, cx| {
3881 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3882 });
3883 cx.assert_editor_state(indoc! {"
3884 «aaaaa
3885 bb
3886 ccc
3887 dddˇ»
3888 "});
3889
3890 // Manipulate case with newlines
3891 cx.set_state(indoc! {"
3892 dd«d
3893 ccc
3894
3895 bb
3896 aaaaa
3897
3898 ˇ»
3899 "});
3900 cx.update_editor(|e, window, cx| {
3901 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3902 });
3903 cx.assert_editor_state(indoc! {"
3904 «
3905
3906 aaaaa
3907 bb
3908 ccc
3909 dddˇ»
3910
3911 "});
3912
3913 // Adding new line
3914 cx.set_state(indoc! {"
3915 aa«a
3916 bbˇ»b
3917 "});
3918 cx.update_editor(|e, window, cx| {
3919 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3920 });
3921 cx.assert_editor_state(indoc! {"
3922 «aaa
3923 bbb
3924 added_lineˇ»
3925 "});
3926
3927 // Removing line
3928 cx.set_state(indoc! {"
3929 aa«a
3930 bbbˇ»
3931 "});
3932 cx.update_editor(|e, window, cx| {
3933 e.manipulate_lines(window, cx, |lines| {
3934 lines.pop();
3935 })
3936 });
3937 cx.assert_editor_state(indoc! {"
3938 «aaaˇ»
3939 "});
3940
3941 // Removing all lines
3942 cx.set_state(indoc! {"
3943 aa«a
3944 bbbˇ»
3945 "});
3946 cx.update_editor(|e, window, cx| {
3947 e.manipulate_lines(window, cx, |lines| {
3948 lines.drain(..);
3949 })
3950 });
3951 cx.assert_editor_state(indoc! {"
3952 ˇ
3953 "});
3954}
3955
3956#[gpui::test]
3957async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3958 init_test(cx, |_| {});
3959
3960 let mut cx = EditorTestContext::new(cx).await;
3961
3962 // Consider continuous selection as single selection
3963 cx.set_state(indoc! {"
3964 Aaa«aa
3965 cˇ»c«c
3966 bb
3967 aaaˇ»aa
3968 "});
3969 cx.update_editor(|e, window, cx| {
3970 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3971 });
3972 cx.assert_editor_state(indoc! {"
3973 «Aaaaa
3974 ccc
3975 bb
3976 aaaaaˇ»
3977 "});
3978
3979 cx.set_state(indoc! {"
3980 Aaa«aa
3981 cˇ»c«c
3982 bb
3983 aaaˇ»aa
3984 "});
3985 cx.update_editor(|e, window, cx| {
3986 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3987 });
3988 cx.assert_editor_state(indoc! {"
3989 «Aaaaa
3990 ccc
3991 bbˇ»
3992 "});
3993
3994 // Consider non continuous selection as distinct dedup operations
3995 cx.set_state(indoc! {"
3996 «aaaaa
3997 bb
3998 aaaaa
3999 aaaaaˇ»
4000
4001 aaa«aaˇ»
4002 "});
4003 cx.update_editor(|e, window, cx| {
4004 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4005 });
4006 cx.assert_editor_state(indoc! {"
4007 «aaaaa
4008 bbˇ»
4009
4010 «aaaaaˇ»
4011 "});
4012}
4013
4014#[gpui::test]
4015async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4016 init_test(cx, |_| {});
4017
4018 let mut cx = EditorTestContext::new(cx).await;
4019
4020 cx.set_state(indoc! {"
4021 «Aaa
4022 aAa
4023 Aaaˇ»
4024 "});
4025 cx.update_editor(|e, window, cx| {
4026 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4027 });
4028 cx.assert_editor_state(indoc! {"
4029 «Aaa
4030 aAaˇ»
4031 "});
4032
4033 cx.set_state(indoc! {"
4034 «Aaa
4035 aAa
4036 aaAˇ»
4037 "});
4038 cx.update_editor(|e, window, cx| {
4039 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4040 });
4041 cx.assert_editor_state(indoc! {"
4042 «Aaaˇ»
4043 "});
4044}
4045
4046#[gpui::test]
4047async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4048 init_test(cx, |_| {});
4049
4050 let mut cx = EditorTestContext::new(cx).await;
4051
4052 // Manipulate with multiple selections on a single line
4053 cx.set_state(indoc! {"
4054 dd«dd
4055 cˇ»c«c
4056 bb
4057 aaaˇ»aa
4058 "});
4059 cx.update_editor(|e, window, cx| {
4060 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4061 });
4062 cx.assert_editor_state(indoc! {"
4063 «aaaaa
4064 bb
4065 ccc
4066 ddddˇ»
4067 "});
4068
4069 // Manipulate with multiple disjoin selections
4070 cx.set_state(indoc! {"
4071 5«
4072 4
4073 3
4074 2
4075 1ˇ»
4076
4077 dd«dd
4078 ccc
4079 bb
4080 aaaˇ»aa
4081 "});
4082 cx.update_editor(|e, window, cx| {
4083 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4084 });
4085 cx.assert_editor_state(indoc! {"
4086 «1
4087 2
4088 3
4089 4
4090 5ˇ»
4091
4092 «aaaaa
4093 bb
4094 ccc
4095 ddddˇ»
4096 "});
4097
4098 // Adding lines on each selection
4099 cx.set_state(indoc! {"
4100 2«
4101 1ˇ»
4102
4103 bb«bb
4104 aaaˇ»aa
4105 "});
4106 cx.update_editor(|e, window, cx| {
4107 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4108 });
4109 cx.assert_editor_state(indoc! {"
4110 «2
4111 1
4112 added lineˇ»
4113
4114 «bbbb
4115 aaaaa
4116 added lineˇ»
4117 "});
4118
4119 // Removing lines on each selection
4120 cx.set_state(indoc! {"
4121 2«
4122 1ˇ»
4123
4124 bb«bb
4125 aaaˇ»aa
4126 "});
4127 cx.update_editor(|e, window, cx| {
4128 e.manipulate_lines(window, cx, |lines| {
4129 lines.pop();
4130 })
4131 });
4132 cx.assert_editor_state(indoc! {"
4133 «2ˇ»
4134
4135 «bbbbˇ»
4136 "});
4137}
4138
4139#[gpui::test]
4140async fn test_toggle_case(cx: &mut TestAppContext) {
4141 init_test(cx, |_| {});
4142
4143 let mut cx = EditorTestContext::new(cx).await;
4144
4145 // If all lower case -> upper case
4146 cx.set_state(indoc! {"
4147 «hello worldˇ»
4148 "});
4149 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4150 cx.assert_editor_state(indoc! {"
4151 «HELLO WORLDˇ»
4152 "});
4153
4154 // If all upper case -> lower case
4155 cx.set_state(indoc! {"
4156 «HELLO WORLDˇ»
4157 "});
4158 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4159 cx.assert_editor_state(indoc! {"
4160 «hello worldˇ»
4161 "});
4162
4163 // If any upper case characters are identified -> lower case
4164 // This matches JetBrains IDEs
4165 cx.set_state(indoc! {"
4166 «hEllo worldˇ»
4167 "});
4168 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4169 cx.assert_editor_state(indoc! {"
4170 «hello worldˇ»
4171 "});
4172}
4173
4174#[gpui::test]
4175async fn test_manipulate_text(cx: &mut TestAppContext) {
4176 init_test(cx, |_| {});
4177
4178 let mut cx = EditorTestContext::new(cx).await;
4179
4180 // Test convert_to_upper_case()
4181 cx.set_state(indoc! {"
4182 «hello worldˇ»
4183 "});
4184 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4185 cx.assert_editor_state(indoc! {"
4186 «HELLO WORLDˇ»
4187 "});
4188
4189 // Test convert_to_lower_case()
4190 cx.set_state(indoc! {"
4191 «HELLO WORLDˇ»
4192 "});
4193 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4194 cx.assert_editor_state(indoc! {"
4195 «hello worldˇ»
4196 "});
4197
4198 // Test multiple line, single selection case
4199 cx.set_state(indoc! {"
4200 «The quick brown
4201 fox jumps over
4202 the lazy dogˇ»
4203 "});
4204 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4205 cx.assert_editor_state(indoc! {"
4206 «The Quick Brown
4207 Fox Jumps Over
4208 The Lazy Dogˇ»
4209 "});
4210
4211 // Test multiple line, single selection case
4212 cx.set_state(indoc! {"
4213 «The quick brown
4214 fox jumps over
4215 the lazy dogˇ»
4216 "});
4217 cx.update_editor(|e, window, cx| {
4218 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4219 });
4220 cx.assert_editor_state(indoc! {"
4221 «TheQuickBrown
4222 FoxJumpsOver
4223 TheLazyDogˇ»
4224 "});
4225
4226 // From here on out, test more complex cases of manipulate_text()
4227
4228 // Test no selection case - should affect words cursors are in
4229 // Cursor at beginning, middle, and end of word
4230 cx.set_state(indoc! {"
4231 ˇhello big beauˇtiful worldˇ
4232 "});
4233 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4234 cx.assert_editor_state(indoc! {"
4235 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4236 "});
4237
4238 // Test multiple selections on a single line and across multiple lines
4239 cx.set_state(indoc! {"
4240 «Theˇ» quick «brown
4241 foxˇ» jumps «overˇ»
4242 the «lazyˇ» dog
4243 "});
4244 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4245 cx.assert_editor_state(indoc! {"
4246 «THEˇ» quick «BROWN
4247 FOXˇ» jumps «OVERˇ»
4248 the «LAZYˇ» dog
4249 "});
4250
4251 // Test case where text length grows
4252 cx.set_state(indoc! {"
4253 «tschüߡ»
4254 "});
4255 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4256 cx.assert_editor_state(indoc! {"
4257 «TSCHÜSSˇ»
4258 "});
4259
4260 // Test to make sure we don't crash when text shrinks
4261 cx.set_state(indoc! {"
4262 aaa_bbbˇ
4263 "});
4264 cx.update_editor(|e, window, cx| {
4265 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4266 });
4267 cx.assert_editor_state(indoc! {"
4268 «aaaBbbˇ»
4269 "});
4270
4271 // Test to make sure we all aware of the fact that each word can grow and shrink
4272 // Final selections should be aware of this fact
4273 cx.set_state(indoc! {"
4274 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4275 "});
4276 cx.update_editor(|e, window, cx| {
4277 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4278 });
4279 cx.assert_editor_state(indoc! {"
4280 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4281 "});
4282
4283 cx.set_state(indoc! {"
4284 «hElLo, WoRld!ˇ»
4285 "});
4286 cx.update_editor(|e, window, cx| {
4287 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4288 });
4289 cx.assert_editor_state(indoc! {"
4290 «HeLlO, wOrLD!ˇ»
4291 "});
4292}
4293
4294#[gpui::test]
4295fn test_duplicate_line(cx: &mut TestAppContext) {
4296 init_test(cx, |_| {});
4297
4298 let editor = cx.add_window(|window, cx| {
4299 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4300 build_editor(buffer, window, cx)
4301 });
4302 _ = editor.update(cx, |editor, window, cx| {
4303 editor.change_selections(None, window, cx, |s| {
4304 s.select_display_ranges([
4305 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4306 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4307 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4308 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4309 ])
4310 });
4311 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4312 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4313 assert_eq!(
4314 editor.selections.display_ranges(cx),
4315 vec![
4316 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4317 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4318 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4319 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4320 ]
4321 );
4322 });
4323
4324 let editor = cx.add_window(|window, cx| {
4325 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4326 build_editor(buffer, window, cx)
4327 });
4328 _ = editor.update(cx, |editor, window, cx| {
4329 editor.change_selections(None, window, cx, |s| {
4330 s.select_display_ranges([
4331 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4332 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4333 ])
4334 });
4335 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4336 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4337 assert_eq!(
4338 editor.selections.display_ranges(cx),
4339 vec![
4340 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4341 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4342 ]
4343 );
4344 });
4345
4346 // With `move_upwards` the selections stay in place, except for
4347 // the lines inserted above them
4348 let editor = cx.add_window(|window, cx| {
4349 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4350 build_editor(buffer, window, cx)
4351 });
4352 _ = editor.update(cx, |editor, window, cx| {
4353 editor.change_selections(None, window, cx, |s| {
4354 s.select_display_ranges([
4355 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4356 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4357 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4358 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4359 ])
4360 });
4361 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4362 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4363 assert_eq!(
4364 editor.selections.display_ranges(cx),
4365 vec![
4366 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4367 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4368 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4369 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4370 ]
4371 );
4372 });
4373
4374 let editor = cx.add_window(|window, cx| {
4375 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4376 build_editor(buffer, window, cx)
4377 });
4378 _ = editor.update(cx, |editor, window, cx| {
4379 editor.change_selections(None, window, cx, |s| {
4380 s.select_display_ranges([
4381 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4382 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4383 ])
4384 });
4385 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4386 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4387 assert_eq!(
4388 editor.selections.display_ranges(cx),
4389 vec![
4390 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4391 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4392 ]
4393 );
4394 });
4395
4396 let editor = cx.add_window(|window, cx| {
4397 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4398 build_editor(buffer, window, cx)
4399 });
4400 _ = editor.update(cx, |editor, window, cx| {
4401 editor.change_selections(None, window, cx, |s| {
4402 s.select_display_ranges([
4403 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4404 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4405 ])
4406 });
4407 editor.duplicate_selection(&DuplicateSelection, window, cx);
4408 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4409 assert_eq!(
4410 editor.selections.display_ranges(cx),
4411 vec![
4412 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4413 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4414 ]
4415 );
4416 });
4417}
4418
4419#[gpui::test]
4420fn test_move_line_up_down(cx: &mut TestAppContext) {
4421 init_test(cx, |_| {});
4422
4423 let editor = cx.add_window(|window, cx| {
4424 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4425 build_editor(buffer, window, cx)
4426 });
4427 _ = editor.update(cx, |editor, window, cx| {
4428 editor.fold_creases(
4429 vec![
4430 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4431 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4432 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4433 ],
4434 true,
4435 window,
4436 cx,
4437 );
4438 editor.change_selections(None, window, cx, |s| {
4439 s.select_display_ranges([
4440 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4441 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4442 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4443 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4444 ])
4445 });
4446 assert_eq!(
4447 editor.display_text(cx),
4448 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4449 );
4450
4451 editor.move_line_up(&MoveLineUp, window, cx);
4452 assert_eq!(
4453 editor.display_text(cx),
4454 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4455 );
4456 assert_eq!(
4457 editor.selections.display_ranges(cx),
4458 vec![
4459 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4460 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4461 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4462 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4463 ]
4464 );
4465 });
4466
4467 _ = editor.update(cx, |editor, window, cx| {
4468 editor.move_line_down(&MoveLineDown, window, cx);
4469 assert_eq!(
4470 editor.display_text(cx),
4471 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4472 );
4473 assert_eq!(
4474 editor.selections.display_ranges(cx),
4475 vec![
4476 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4477 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4478 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4479 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4480 ]
4481 );
4482 });
4483
4484 _ = editor.update(cx, |editor, window, cx| {
4485 editor.move_line_down(&MoveLineDown, window, cx);
4486 assert_eq!(
4487 editor.display_text(cx),
4488 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4489 );
4490 assert_eq!(
4491 editor.selections.display_ranges(cx),
4492 vec![
4493 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4494 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4495 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4496 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4497 ]
4498 );
4499 });
4500
4501 _ = editor.update(cx, |editor, window, cx| {
4502 editor.move_line_up(&MoveLineUp, window, cx);
4503 assert_eq!(
4504 editor.display_text(cx),
4505 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4506 );
4507 assert_eq!(
4508 editor.selections.display_ranges(cx),
4509 vec![
4510 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4511 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4512 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4513 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4514 ]
4515 );
4516 });
4517}
4518
4519#[gpui::test]
4520fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4521 init_test(cx, |_| {});
4522
4523 let editor = cx.add_window(|window, cx| {
4524 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4525 build_editor(buffer, window, cx)
4526 });
4527 _ = editor.update(cx, |editor, window, cx| {
4528 let snapshot = editor.buffer.read(cx).snapshot(cx);
4529 editor.insert_blocks(
4530 [BlockProperties {
4531 style: BlockStyle::Fixed,
4532 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4533 height: Some(1),
4534 render: Arc::new(|_| div().into_any()),
4535 priority: 0,
4536 render_in_minimap: true,
4537 }],
4538 Some(Autoscroll::fit()),
4539 cx,
4540 );
4541 editor.change_selections(None, window, cx, |s| {
4542 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4543 });
4544 editor.move_line_down(&MoveLineDown, window, cx);
4545 });
4546}
4547
4548#[gpui::test]
4549async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4550 init_test(cx, |_| {});
4551
4552 let mut cx = EditorTestContext::new(cx).await;
4553 cx.set_state(
4554 &"
4555 ˇzero
4556 one
4557 two
4558 three
4559 four
4560 five
4561 "
4562 .unindent(),
4563 );
4564
4565 // Create a four-line block that replaces three lines of text.
4566 cx.update_editor(|editor, window, cx| {
4567 let snapshot = editor.snapshot(window, cx);
4568 let snapshot = &snapshot.buffer_snapshot;
4569 let placement = BlockPlacement::Replace(
4570 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4571 );
4572 editor.insert_blocks(
4573 [BlockProperties {
4574 placement,
4575 height: Some(4),
4576 style: BlockStyle::Sticky,
4577 render: Arc::new(|_| gpui::div().into_any_element()),
4578 priority: 0,
4579 render_in_minimap: true,
4580 }],
4581 None,
4582 cx,
4583 );
4584 });
4585
4586 // Move down so that the cursor touches the block.
4587 cx.update_editor(|editor, window, cx| {
4588 editor.move_down(&Default::default(), window, cx);
4589 });
4590 cx.assert_editor_state(
4591 &"
4592 zero
4593 «one
4594 two
4595 threeˇ»
4596 four
4597 five
4598 "
4599 .unindent(),
4600 );
4601
4602 // Move down past the block.
4603 cx.update_editor(|editor, window, cx| {
4604 editor.move_down(&Default::default(), window, cx);
4605 });
4606 cx.assert_editor_state(
4607 &"
4608 zero
4609 one
4610 two
4611 three
4612 ˇfour
4613 five
4614 "
4615 .unindent(),
4616 );
4617}
4618
4619#[gpui::test]
4620fn test_transpose(cx: &mut TestAppContext) {
4621 init_test(cx, |_| {});
4622
4623 _ = cx.add_window(|window, cx| {
4624 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4625 editor.set_style(EditorStyle::default(), window, cx);
4626 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4627 editor.transpose(&Default::default(), window, cx);
4628 assert_eq!(editor.text(cx), "bac");
4629 assert_eq!(editor.selections.ranges(cx), [2..2]);
4630
4631 editor.transpose(&Default::default(), window, cx);
4632 assert_eq!(editor.text(cx), "bca");
4633 assert_eq!(editor.selections.ranges(cx), [3..3]);
4634
4635 editor.transpose(&Default::default(), window, cx);
4636 assert_eq!(editor.text(cx), "bac");
4637 assert_eq!(editor.selections.ranges(cx), [3..3]);
4638
4639 editor
4640 });
4641
4642 _ = cx.add_window(|window, cx| {
4643 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4644 editor.set_style(EditorStyle::default(), window, cx);
4645 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4646 editor.transpose(&Default::default(), window, cx);
4647 assert_eq!(editor.text(cx), "acb\nde");
4648 assert_eq!(editor.selections.ranges(cx), [3..3]);
4649
4650 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4651 editor.transpose(&Default::default(), window, cx);
4652 assert_eq!(editor.text(cx), "acbd\ne");
4653 assert_eq!(editor.selections.ranges(cx), [5..5]);
4654
4655 editor.transpose(&Default::default(), window, cx);
4656 assert_eq!(editor.text(cx), "acbde\n");
4657 assert_eq!(editor.selections.ranges(cx), [6..6]);
4658
4659 editor.transpose(&Default::default(), window, cx);
4660 assert_eq!(editor.text(cx), "acbd\ne");
4661 assert_eq!(editor.selections.ranges(cx), [6..6]);
4662
4663 editor
4664 });
4665
4666 _ = cx.add_window(|window, cx| {
4667 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4668 editor.set_style(EditorStyle::default(), window, cx);
4669 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4670 editor.transpose(&Default::default(), window, cx);
4671 assert_eq!(editor.text(cx), "bacd\ne");
4672 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4673
4674 editor.transpose(&Default::default(), window, cx);
4675 assert_eq!(editor.text(cx), "bcade\n");
4676 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4677
4678 editor.transpose(&Default::default(), window, cx);
4679 assert_eq!(editor.text(cx), "bcda\ne");
4680 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4681
4682 editor.transpose(&Default::default(), window, cx);
4683 assert_eq!(editor.text(cx), "bcade\n");
4684 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4685
4686 editor.transpose(&Default::default(), window, cx);
4687 assert_eq!(editor.text(cx), "bcaed\n");
4688 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4689
4690 editor
4691 });
4692
4693 _ = cx.add_window(|window, cx| {
4694 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4695 editor.set_style(EditorStyle::default(), window, cx);
4696 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4697 editor.transpose(&Default::default(), window, cx);
4698 assert_eq!(editor.text(cx), "🏀🍐✋");
4699 assert_eq!(editor.selections.ranges(cx), [8..8]);
4700
4701 editor.transpose(&Default::default(), window, cx);
4702 assert_eq!(editor.text(cx), "🏀✋🍐");
4703 assert_eq!(editor.selections.ranges(cx), [11..11]);
4704
4705 editor.transpose(&Default::default(), window, cx);
4706 assert_eq!(editor.text(cx), "🏀🍐✋");
4707 assert_eq!(editor.selections.ranges(cx), [11..11]);
4708
4709 editor
4710 });
4711}
4712
4713#[gpui::test]
4714async fn test_rewrap(cx: &mut TestAppContext) {
4715 init_test(cx, |settings| {
4716 settings.languages.extend([
4717 (
4718 "Markdown".into(),
4719 LanguageSettingsContent {
4720 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4721 ..Default::default()
4722 },
4723 ),
4724 (
4725 "Plain Text".into(),
4726 LanguageSettingsContent {
4727 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4728 ..Default::default()
4729 },
4730 ),
4731 ])
4732 });
4733
4734 let mut cx = EditorTestContext::new(cx).await;
4735
4736 let language_with_c_comments = Arc::new(Language::new(
4737 LanguageConfig {
4738 line_comments: vec!["// ".into()],
4739 ..LanguageConfig::default()
4740 },
4741 None,
4742 ));
4743 let language_with_pound_comments = Arc::new(Language::new(
4744 LanguageConfig {
4745 line_comments: vec!["# ".into()],
4746 ..LanguageConfig::default()
4747 },
4748 None,
4749 ));
4750 let markdown_language = Arc::new(Language::new(
4751 LanguageConfig {
4752 name: "Markdown".into(),
4753 ..LanguageConfig::default()
4754 },
4755 None,
4756 ));
4757 let language_with_doc_comments = Arc::new(Language::new(
4758 LanguageConfig {
4759 line_comments: vec!["// ".into(), "/// ".into()],
4760 ..LanguageConfig::default()
4761 },
4762 Some(tree_sitter_rust::LANGUAGE.into()),
4763 ));
4764
4765 let plaintext_language = Arc::new(Language::new(
4766 LanguageConfig {
4767 name: "Plain Text".into(),
4768 ..LanguageConfig::default()
4769 },
4770 None,
4771 ));
4772
4773 assert_rewrap(
4774 indoc! {"
4775 // ˇ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.
4776 "},
4777 indoc! {"
4778 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4779 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4780 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4781 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4782 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4783 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4784 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4785 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4786 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4787 // porttitor id. Aliquam id accumsan eros.
4788 "},
4789 language_with_c_comments.clone(),
4790 &mut cx,
4791 );
4792
4793 // Test that rewrapping works inside of a selection
4794 assert_rewrap(
4795 indoc! {"
4796 «// 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.ˇ»
4797 "},
4798 indoc! {"
4799 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4800 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4801 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4802 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4803 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4804 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4805 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4806 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4807 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4808 // porttitor id. Aliquam id accumsan eros.ˇ»
4809 "},
4810 language_with_c_comments.clone(),
4811 &mut cx,
4812 );
4813
4814 // Test that cursors that expand to the same region are collapsed.
4815 assert_rewrap(
4816 indoc! {"
4817 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4818 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4819 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4820 // ˇ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.
4821 "},
4822 indoc! {"
4823 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4824 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4825 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4826 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4827 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4828 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4829 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4830 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4831 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4832 // porttitor id. Aliquam id accumsan eros.
4833 "},
4834 language_with_c_comments.clone(),
4835 &mut cx,
4836 );
4837
4838 // Test that non-contiguous selections are treated separately.
4839 assert_rewrap(
4840 indoc! {"
4841 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4842 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4843 //
4844 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4845 // ˇ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.
4846 "},
4847 indoc! {"
4848 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4849 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4850 // auctor, eu lacinia sapien scelerisque.
4851 //
4852 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4853 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4854 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4855 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4856 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4857 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4858 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4859 "},
4860 language_with_c_comments.clone(),
4861 &mut cx,
4862 );
4863
4864 // Test that different comment prefixes are supported.
4865 assert_rewrap(
4866 indoc! {"
4867 # ˇ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.
4868 "},
4869 indoc! {"
4870 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4871 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4872 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4873 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4874 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4875 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4876 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4877 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4878 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4879 # accumsan eros.
4880 "},
4881 language_with_pound_comments.clone(),
4882 &mut cx,
4883 );
4884
4885 // Test that rewrapping is ignored outside of comments in most languages.
4886 assert_rewrap(
4887 indoc! {"
4888 /// Adds two numbers.
4889 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4890 fn add(a: u32, b: u32) -> u32 {
4891 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ˇ
4892 }
4893 "},
4894 indoc! {"
4895 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4896 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4897 fn add(a: u32, b: u32) -> u32 {
4898 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ˇ
4899 }
4900 "},
4901 language_with_doc_comments.clone(),
4902 &mut cx,
4903 );
4904
4905 // Test that rewrapping works in Markdown and Plain Text languages.
4906 assert_rewrap(
4907 indoc! {"
4908 # Hello
4909
4910 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.
4911 "},
4912 indoc! {"
4913 # Hello
4914
4915 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4916 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4917 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4918 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4919 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4920 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4921 Integer sit amet scelerisque nisi.
4922 "},
4923 markdown_language,
4924 &mut cx,
4925 );
4926
4927 assert_rewrap(
4928 indoc! {"
4929 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.
4930 "},
4931 indoc! {"
4932 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4933 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4934 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4935 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4936 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4937 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4938 Integer sit amet scelerisque nisi.
4939 "},
4940 plaintext_language,
4941 &mut cx,
4942 );
4943
4944 // Test rewrapping unaligned comments in a selection.
4945 assert_rewrap(
4946 indoc! {"
4947 fn foo() {
4948 if true {
4949 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4950 // Praesent semper egestas tellus id dignissim.ˇ»
4951 do_something();
4952 } else {
4953 //
4954 }
4955 }
4956 "},
4957 indoc! {"
4958 fn foo() {
4959 if true {
4960 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4961 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4962 // egestas tellus id dignissim.ˇ»
4963 do_something();
4964 } else {
4965 //
4966 }
4967 }
4968 "},
4969 language_with_doc_comments.clone(),
4970 &mut cx,
4971 );
4972
4973 assert_rewrap(
4974 indoc! {"
4975 fn foo() {
4976 if true {
4977 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4978 // Praesent semper egestas tellus id dignissim.»
4979 do_something();
4980 } else {
4981 //
4982 }
4983
4984 }
4985 "},
4986 indoc! {"
4987 fn foo() {
4988 if true {
4989 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4990 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4991 // egestas tellus id dignissim.»
4992 do_something();
4993 } else {
4994 //
4995 }
4996
4997 }
4998 "},
4999 language_with_doc_comments.clone(),
5000 &mut cx,
5001 );
5002
5003 #[track_caller]
5004 fn assert_rewrap(
5005 unwrapped_text: &str,
5006 wrapped_text: &str,
5007 language: Arc<Language>,
5008 cx: &mut EditorTestContext,
5009 ) {
5010 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5011 cx.set_state(unwrapped_text);
5012 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5013 cx.assert_editor_state(wrapped_text);
5014 }
5015}
5016
5017#[gpui::test]
5018async fn test_hard_wrap(cx: &mut TestAppContext) {
5019 init_test(cx, |_| {});
5020 let mut cx = EditorTestContext::new(cx).await;
5021
5022 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5023 cx.update_editor(|editor, _, cx| {
5024 editor.set_hard_wrap(Some(14), cx);
5025 });
5026
5027 cx.set_state(indoc!(
5028 "
5029 one two three ˇ
5030 "
5031 ));
5032 cx.simulate_input("four");
5033 cx.run_until_parked();
5034
5035 cx.assert_editor_state(indoc!(
5036 "
5037 one two three
5038 fourˇ
5039 "
5040 ));
5041
5042 cx.update_editor(|editor, window, cx| {
5043 editor.newline(&Default::default(), window, cx);
5044 });
5045 cx.run_until_parked();
5046 cx.assert_editor_state(indoc!(
5047 "
5048 one two three
5049 four
5050 ˇ
5051 "
5052 ));
5053
5054 cx.simulate_input("five");
5055 cx.run_until_parked();
5056 cx.assert_editor_state(indoc!(
5057 "
5058 one two three
5059 four
5060 fiveˇ
5061 "
5062 ));
5063
5064 cx.update_editor(|editor, window, cx| {
5065 editor.newline(&Default::default(), window, cx);
5066 });
5067 cx.run_until_parked();
5068 cx.simulate_input("# ");
5069 cx.run_until_parked();
5070 cx.assert_editor_state(indoc!(
5071 "
5072 one two three
5073 four
5074 five
5075 # ˇ
5076 "
5077 ));
5078
5079 cx.update_editor(|editor, window, cx| {
5080 editor.newline(&Default::default(), window, cx);
5081 });
5082 cx.run_until_parked();
5083 cx.assert_editor_state(indoc!(
5084 "
5085 one two three
5086 four
5087 five
5088 #\x20
5089 #ˇ
5090 "
5091 ));
5092
5093 cx.simulate_input(" 6");
5094 cx.run_until_parked();
5095 cx.assert_editor_state(indoc!(
5096 "
5097 one two three
5098 four
5099 five
5100 #
5101 # 6ˇ
5102 "
5103 ));
5104}
5105
5106#[gpui::test]
5107async fn test_clipboard(cx: &mut TestAppContext) {
5108 init_test(cx, |_| {});
5109
5110 let mut cx = EditorTestContext::new(cx).await;
5111
5112 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5113 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5114 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5115
5116 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5117 cx.set_state("two ˇfour ˇsix ˇ");
5118 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5119 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5120
5121 // Paste again but with only two cursors. Since the number of cursors doesn't
5122 // match the number of slices in the clipboard, the entire clipboard text
5123 // is pasted at each cursor.
5124 cx.set_state("ˇtwo one✅ four three six five ˇ");
5125 cx.update_editor(|e, window, cx| {
5126 e.handle_input("( ", window, cx);
5127 e.paste(&Paste, window, cx);
5128 e.handle_input(") ", window, cx);
5129 });
5130 cx.assert_editor_state(
5131 &([
5132 "( one✅ ",
5133 "three ",
5134 "five ) ˇtwo one✅ four three six five ( one✅ ",
5135 "three ",
5136 "five ) ˇ",
5137 ]
5138 .join("\n")),
5139 );
5140
5141 // Cut with three selections, one of which is full-line.
5142 cx.set_state(indoc! {"
5143 1«2ˇ»3
5144 4ˇ567
5145 «8ˇ»9"});
5146 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5147 cx.assert_editor_state(indoc! {"
5148 1ˇ3
5149 ˇ9"});
5150
5151 // Paste with three selections, noticing how the copied selection that was full-line
5152 // gets inserted before the second cursor.
5153 cx.set_state(indoc! {"
5154 1ˇ3
5155 9ˇ
5156 «oˇ»ne"});
5157 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5158 cx.assert_editor_state(indoc! {"
5159 12ˇ3
5160 4567
5161 9ˇ
5162 8ˇne"});
5163
5164 // Copy with a single cursor only, which writes the whole line into the clipboard.
5165 cx.set_state(indoc! {"
5166 The quick brown
5167 fox juˇmps over
5168 the lazy dog"});
5169 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5170 assert_eq!(
5171 cx.read_from_clipboard()
5172 .and_then(|item| item.text().as_deref().map(str::to_string)),
5173 Some("fox jumps over\n".to_string())
5174 );
5175
5176 // Paste with three selections, noticing how the copied full-line selection is inserted
5177 // before the empty selections but replaces the selection that is non-empty.
5178 cx.set_state(indoc! {"
5179 Tˇhe quick brown
5180 «foˇ»x jumps over
5181 tˇhe lazy dog"});
5182 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5183 cx.assert_editor_state(indoc! {"
5184 fox jumps over
5185 Tˇhe quick brown
5186 fox jumps over
5187 ˇx jumps over
5188 fox jumps over
5189 tˇhe lazy dog"});
5190}
5191
5192#[gpui::test]
5193async fn test_copy_trim(cx: &mut TestAppContext) {
5194 init_test(cx, |_| {});
5195
5196 let mut cx = EditorTestContext::new(cx).await;
5197 cx.set_state(
5198 r#" «for selection in selections.iter() {
5199 let mut start = selection.start;
5200 let mut end = selection.end;
5201 let is_entire_line = selection.is_empty();
5202 if is_entire_line {
5203 start = Point::new(start.row, 0);ˇ»
5204 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5205 }
5206 "#,
5207 );
5208 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5209 assert_eq!(
5210 cx.read_from_clipboard()
5211 .and_then(|item| item.text().as_deref().map(str::to_string)),
5212 Some(
5213 "for selection in selections.iter() {
5214 let mut start = selection.start;
5215 let mut end = selection.end;
5216 let is_entire_line = selection.is_empty();
5217 if is_entire_line {
5218 start = Point::new(start.row, 0);"
5219 .to_string()
5220 ),
5221 "Regular copying preserves all indentation selected",
5222 );
5223 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5224 assert_eq!(
5225 cx.read_from_clipboard()
5226 .and_then(|item| item.text().as_deref().map(str::to_string)),
5227 Some(
5228 "for selection in selections.iter() {
5229let mut start = selection.start;
5230let mut end = selection.end;
5231let is_entire_line = selection.is_empty();
5232if is_entire_line {
5233 start = Point::new(start.row, 0);"
5234 .to_string()
5235 ),
5236 "Copying with stripping should strip all leading whitespaces"
5237 );
5238
5239 cx.set_state(
5240 r#" « for selection in selections.iter() {
5241 let mut start = selection.start;
5242 let mut end = selection.end;
5243 let is_entire_line = selection.is_empty();
5244 if is_entire_line {
5245 start = Point::new(start.row, 0);ˇ»
5246 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5247 }
5248 "#,
5249 );
5250 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5251 assert_eq!(
5252 cx.read_from_clipboard()
5253 .and_then(|item| item.text().as_deref().map(str::to_string)),
5254 Some(
5255 " for selection in selections.iter() {
5256 let mut start = selection.start;
5257 let mut end = selection.end;
5258 let is_entire_line = selection.is_empty();
5259 if is_entire_line {
5260 start = Point::new(start.row, 0);"
5261 .to_string()
5262 ),
5263 "Regular copying preserves all indentation selected",
5264 );
5265 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5266 assert_eq!(
5267 cx.read_from_clipboard()
5268 .and_then(|item| item.text().as_deref().map(str::to_string)),
5269 Some(
5270 "for selection in selections.iter() {
5271let mut start = selection.start;
5272let mut end = selection.end;
5273let is_entire_line = selection.is_empty();
5274if is_entire_line {
5275 start = Point::new(start.row, 0);"
5276 .to_string()
5277 ),
5278 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5279 );
5280
5281 cx.set_state(
5282 r#" «ˇ for selection in selections.iter() {
5283 let mut start = selection.start;
5284 let mut end = selection.end;
5285 let is_entire_line = selection.is_empty();
5286 if is_entire_line {
5287 start = Point::new(start.row, 0);»
5288 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5289 }
5290 "#,
5291 );
5292 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5293 assert_eq!(
5294 cx.read_from_clipboard()
5295 .and_then(|item| item.text().as_deref().map(str::to_string)),
5296 Some(
5297 " for selection in selections.iter() {
5298 let mut start = selection.start;
5299 let mut end = selection.end;
5300 let is_entire_line = selection.is_empty();
5301 if is_entire_line {
5302 start = Point::new(start.row, 0);"
5303 .to_string()
5304 ),
5305 "Regular copying for reverse selection works the same",
5306 );
5307 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5308 assert_eq!(
5309 cx.read_from_clipboard()
5310 .and_then(|item| item.text().as_deref().map(str::to_string)),
5311 Some(
5312 "for selection in selections.iter() {
5313let mut start = selection.start;
5314let mut end = selection.end;
5315let is_entire_line = selection.is_empty();
5316if is_entire_line {
5317 start = Point::new(start.row, 0);"
5318 .to_string()
5319 ),
5320 "Copying with stripping for reverse selection works the same"
5321 );
5322
5323 cx.set_state(
5324 r#" for selection «in selections.iter() {
5325 let mut start = selection.start;
5326 let mut end = selection.end;
5327 let is_entire_line = selection.is_empty();
5328 if is_entire_line {
5329 start = Point::new(start.row, 0);ˇ»
5330 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5331 }
5332 "#,
5333 );
5334 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5335 assert_eq!(
5336 cx.read_from_clipboard()
5337 .and_then(|item| item.text().as_deref().map(str::to_string)),
5338 Some(
5339 "in selections.iter() {
5340 let mut start = selection.start;
5341 let mut end = selection.end;
5342 let is_entire_line = selection.is_empty();
5343 if is_entire_line {
5344 start = Point::new(start.row, 0);"
5345 .to_string()
5346 ),
5347 "When selecting past the indent, the copying works as usual",
5348 );
5349 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5350 assert_eq!(
5351 cx.read_from_clipboard()
5352 .and_then(|item| item.text().as_deref().map(str::to_string)),
5353 Some(
5354 "in selections.iter() {
5355 let mut start = selection.start;
5356 let mut end = selection.end;
5357 let is_entire_line = selection.is_empty();
5358 if is_entire_line {
5359 start = Point::new(start.row, 0);"
5360 .to_string()
5361 ),
5362 "When selecting past the indent, nothing is trimmed"
5363 );
5364
5365 cx.set_state(
5366 r#" «for selection in selections.iter() {
5367 let mut start = selection.start;
5368
5369 let mut end = selection.end;
5370 let is_entire_line = selection.is_empty();
5371 if is_entire_line {
5372 start = Point::new(start.row, 0);
5373ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5374 }
5375 "#,
5376 );
5377 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5378 assert_eq!(
5379 cx.read_from_clipboard()
5380 .and_then(|item| item.text().as_deref().map(str::to_string)),
5381 Some(
5382 "for selection in selections.iter() {
5383let mut start = selection.start;
5384
5385let mut end = selection.end;
5386let is_entire_line = selection.is_empty();
5387if is_entire_line {
5388 start = Point::new(start.row, 0);
5389"
5390 .to_string()
5391 ),
5392 "Copying with stripping should ignore empty lines"
5393 );
5394}
5395
5396#[gpui::test]
5397async fn test_paste_multiline(cx: &mut TestAppContext) {
5398 init_test(cx, |_| {});
5399
5400 let mut cx = EditorTestContext::new(cx).await;
5401 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5402
5403 // Cut an indented block, without the leading whitespace.
5404 cx.set_state(indoc! {"
5405 const a: B = (
5406 c(),
5407 «d(
5408 e,
5409 f
5410 )ˇ»
5411 );
5412 "});
5413 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5414 cx.assert_editor_state(indoc! {"
5415 const a: B = (
5416 c(),
5417 ˇ
5418 );
5419 "});
5420
5421 // Paste it at the same position.
5422 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5423 cx.assert_editor_state(indoc! {"
5424 const a: B = (
5425 c(),
5426 d(
5427 e,
5428 f
5429 )ˇ
5430 );
5431 "});
5432
5433 // Paste it at a line with a lower indent level.
5434 cx.set_state(indoc! {"
5435 ˇ
5436 const a: B = (
5437 c(),
5438 );
5439 "});
5440 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5441 cx.assert_editor_state(indoc! {"
5442 d(
5443 e,
5444 f
5445 )ˇ
5446 const a: B = (
5447 c(),
5448 );
5449 "});
5450
5451 // Cut an indented block, with the leading whitespace.
5452 cx.set_state(indoc! {"
5453 const a: B = (
5454 c(),
5455 « d(
5456 e,
5457 f
5458 )
5459 ˇ»);
5460 "});
5461 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5462 cx.assert_editor_state(indoc! {"
5463 const a: B = (
5464 c(),
5465 ˇ);
5466 "});
5467
5468 // Paste it at the same position.
5469 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5470 cx.assert_editor_state(indoc! {"
5471 const a: B = (
5472 c(),
5473 d(
5474 e,
5475 f
5476 )
5477 ˇ);
5478 "});
5479
5480 // Paste it at a line with a higher indent level.
5481 cx.set_state(indoc! {"
5482 const a: B = (
5483 c(),
5484 d(
5485 e,
5486 fˇ
5487 )
5488 );
5489 "});
5490 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5491 cx.assert_editor_state(indoc! {"
5492 const a: B = (
5493 c(),
5494 d(
5495 e,
5496 f d(
5497 e,
5498 f
5499 )
5500 ˇ
5501 )
5502 );
5503 "});
5504
5505 // Copy an indented block, starting mid-line
5506 cx.set_state(indoc! {"
5507 const a: B = (
5508 c(),
5509 somethin«g(
5510 e,
5511 f
5512 )ˇ»
5513 );
5514 "});
5515 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5516
5517 // Paste it on a line with a lower indent level
5518 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5519 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5520 cx.assert_editor_state(indoc! {"
5521 const a: B = (
5522 c(),
5523 something(
5524 e,
5525 f
5526 )
5527 );
5528 g(
5529 e,
5530 f
5531 )ˇ"});
5532}
5533
5534#[gpui::test]
5535async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5536 init_test(cx, |_| {});
5537
5538 cx.write_to_clipboard(ClipboardItem::new_string(
5539 " d(\n e\n );\n".into(),
5540 ));
5541
5542 let mut cx = EditorTestContext::new(cx).await;
5543 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5544
5545 cx.set_state(indoc! {"
5546 fn a() {
5547 b();
5548 if c() {
5549 ˇ
5550 }
5551 }
5552 "});
5553
5554 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5555 cx.assert_editor_state(indoc! {"
5556 fn a() {
5557 b();
5558 if c() {
5559 d(
5560 e
5561 );
5562 ˇ
5563 }
5564 }
5565 "});
5566
5567 cx.set_state(indoc! {"
5568 fn a() {
5569 b();
5570 ˇ
5571 }
5572 "});
5573
5574 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5575 cx.assert_editor_state(indoc! {"
5576 fn a() {
5577 b();
5578 d(
5579 e
5580 );
5581 ˇ
5582 }
5583 "});
5584}
5585
5586#[gpui::test]
5587fn test_select_all(cx: &mut TestAppContext) {
5588 init_test(cx, |_| {});
5589
5590 let editor = cx.add_window(|window, cx| {
5591 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5592 build_editor(buffer, window, cx)
5593 });
5594 _ = editor.update(cx, |editor, window, cx| {
5595 editor.select_all(&SelectAll, window, cx);
5596 assert_eq!(
5597 editor.selections.display_ranges(cx),
5598 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5599 );
5600 });
5601}
5602
5603#[gpui::test]
5604fn test_select_line(cx: &mut TestAppContext) {
5605 init_test(cx, |_| {});
5606
5607 let editor = cx.add_window(|window, cx| {
5608 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5609 build_editor(buffer, window, cx)
5610 });
5611 _ = editor.update(cx, |editor, window, cx| {
5612 editor.change_selections(None, window, cx, |s| {
5613 s.select_display_ranges([
5614 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5615 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5616 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5617 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5618 ])
5619 });
5620 editor.select_line(&SelectLine, window, cx);
5621 assert_eq!(
5622 editor.selections.display_ranges(cx),
5623 vec![
5624 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5625 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5626 ]
5627 );
5628 });
5629
5630 _ = editor.update(cx, |editor, window, cx| {
5631 editor.select_line(&SelectLine, window, cx);
5632 assert_eq!(
5633 editor.selections.display_ranges(cx),
5634 vec![
5635 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5636 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5637 ]
5638 );
5639 });
5640
5641 _ = editor.update(cx, |editor, window, cx| {
5642 editor.select_line(&SelectLine, window, cx);
5643 assert_eq!(
5644 editor.selections.display_ranges(cx),
5645 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5646 );
5647 });
5648}
5649
5650#[gpui::test]
5651async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5652 init_test(cx, |_| {});
5653 let mut cx = EditorTestContext::new(cx).await;
5654
5655 #[track_caller]
5656 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5657 cx.set_state(initial_state);
5658 cx.update_editor(|e, window, cx| {
5659 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5660 });
5661 cx.assert_editor_state(expected_state);
5662 }
5663
5664 // Selection starts and ends at the middle of lines, left-to-right
5665 test(
5666 &mut cx,
5667 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5668 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5669 );
5670 // Same thing, right-to-left
5671 test(
5672 &mut cx,
5673 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5674 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5675 );
5676
5677 // Whole buffer, left-to-right, last line *doesn't* end with newline
5678 test(
5679 &mut cx,
5680 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5681 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5682 );
5683 // Same thing, right-to-left
5684 test(
5685 &mut cx,
5686 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5687 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5688 );
5689
5690 // Whole buffer, left-to-right, last line ends with newline
5691 test(
5692 &mut cx,
5693 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5694 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5695 );
5696 // Same thing, right-to-left
5697 test(
5698 &mut cx,
5699 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5700 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5701 );
5702
5703 // Starts at the end of a line, ends at the start of another
5704 test(
5705 &mut cx,
5706 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5707 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5708 );
5709}
5710
5711#[gpui::test]
5712async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5713 init_test(cx, |_| {});
5714
5715 let editor = cx.add_window(|window, cx| {
5716 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5717 build_editor(buffer, window, cx)
5718 });
5719
5720 // setup
5721 _ = editor.update(cx, |editor, window, cx| {
5722 editor.fold_creases(
5723 vec![
5724 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5725 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5726 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5727 ],
5728 true,
5729 window,
5730 cx,
5731 );
5732 assert_eq!(
5733 editor.display_text(cx),
5734 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5735 );
5736 });
5737
5738 _ = editor.update(cx, |editor, window, cx| {
5739 editor.change_selections(None, window, cx, |s| {
5740 s.select_display_ranges([
5741 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5742 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5743 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5744 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5745 ])
5746 });
5747 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5748 assert_eq!(
5749 editor.display_text(cx),
5750 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5751 );
5752 });
5753 EditorTestContext::for_editor(editor, cx)
5754 .await
5755 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5756
5757 _ = editor.update(cx, |editor, window, cx| {
5758 editor.change_selections(None, window, cx, |s| {
5759 s.select_display_ranges([
5760 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5761 ])
5762 });
5763 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5764 assert_eq!(
5765 editor.display_text(cx),
5766 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5767 );
5768 assert_eq!(
5769 editor.selections.display_ranges(cx),
5770 [
5771 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5772 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5773 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5774 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5775 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5776 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5777 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5778 ]
5779 );
5780 });
5781 EditorTestContext::for_editor(editor, cx)
5782 .await
5783 .assert_editor_state(
5784 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5785 );
5786}
5787
5788#[gpui::test]
5789async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5790 init_test(cx, |_| {});
5791
5792 let mut cx = EditorTestContext::new(cx).await;
5793
5794 cx.set_state(indoc!(
5795 r#"abc
5796 defˇghi
5797
5798 jk
5799 nlmo
5800 "#
5801 ));
5802
5803 cx.update_editor(|editor, window, cx| {
5804 editor.add_selection_above(&Default::default(), window, cx);
5805 });
5806
5807 cx.assert_editor_state(indoc!(
5808 r#"abcˇ
5809 defˇghi
5810
5811 jk
5812 nlmo
5813 "#
5814 ));
5815
5816 cx.update_editor(|editor, window, cx| {
5817 editor.add_selection_above(&Default::default(), window, cx);
5818 });
5819
5820 cx.assert_editor_state(indoc!(
5821 r#"abcˇ
5822 defˇghi
5823
5824 jk
5825 nlmo
5826 "#
5827 ));
5828
5829 cx.update_editor(|editor, window, cx| {
5830 editor.add_selection_below(&Default::default(), window, cx);
5831 });
5832
5833 cx.assert_editor_state(indoc!(
5834 r#"abc
5835 defˇghi
5836
5837 jk
5838 nlmo
5839 "#
5840 ));
5841
5842 cx.update_editor(|editor, window, cx| {
5843 editor.undo_selection(&Default::default(), window, cx);
5844 });
5845
5846 cx.assert_editor_state(indoc!(
5847 r#"abcˇ
5848 defˇghi
5849
5850 jk
5851 nlmo
5852 "#
5853 ));
5854
5855 cx.update_editor(|editor, window, cx| {
5856 editor.redo_selection(&Default::default(), window, cx);
5857 });
5858
5859 cx.assert_editor_state(indoc!(
5860 r#"abc
5861 defˇghi
5862
5863 jk
5864 nlmo
5865 "#
5866 ));
5867
5868 cx.update_editor(|editor, window, cx| {
5869 editor.add_selection_below(&Default::default(), window, cx);
5870 });
5871
5872 cx.assert_editor_state(indoc!(
5873 r#"abc
5874 defˇghi
5875
5876 jk
5877 nlmˇo
5878 "#
5879 ));
5880
5881 cx.update_editor(|editor, window, cx| {
5882 editor.add_selection_below(&Default::default(), window, cx);
5883 });
5884
5885 cx.assert_editor_state(indoc!(
5886 r#"abc
5887 defˇghi
5888
5889 jk
5890 nlmˇo
5891 "#
5892 ));
5893
5894 // change selections
5895 cx.set_state(indoc!(
5896 r#"abc
5897 def«ˇg»hi
5898
5899 jk
5900 nlmo
5901 "#
5902 ));
5903
5904 cx.update_editor(|editor, window, cx| {
5905 editor.add_selection_below(&Default::default(), window, cx);
5906 });
5907
5908 cx.assert_editor_state(indoc!(
5909 r#"abc
5910 def«ˇg»hi
5911
5912 jk
5913 nlm«ˇo»
5914 "#
5915 ));
5916
5917 cx.update_editor(|editor, window, cx| {
5918 editor.add_selection_below(&Default::default(), window, cx);
5919 });
5920
5921 cx.assert_editor_state(indoc!(
5922 r#"abc
5923 def«ˇg»hi
5924
5925 jk
5926 nlm«ˇo»
5927 "#
5928 ));
5929
5930 cx.update_editor(|editor, window, cx| {
5931 editor.add_selection_above(&Default::default(), window, cx);
5932 });
5933
5934 cx.assert_editor_state(indoc!(
5935 r#"abc
5936 def«ˇg»hi
5937
5938 jk
5939 nlmo
5940 "#
5941 ));
5942
5943 cx.update_editor(|editor, window, cx| {
5944 editor.add_selection_above(&Default::default(), window, cx);
5945 });
5946
5947 cx.assert_editor_state(indoc!(
5948 r#"abc
5949 def«ˇg»hi
5950
5951 jk
5952 nlmo
5953 "#
5954 ));
5955
5956 // Change selections again
5957 cx.set_state(indoc!(
5958 r#"a«bc
5959 defgˇ»hi
5960
5961 jk
5962 nlmo
5963 "#
5964 ));
5965
5966 cx.update_editor(|editor, window, cx| {
5967 editor.add_selection_below(&Default::default(), window, cx);
5968 });
5969
5970 cx.assert_editor_state(indoc!(
5971 r#"a«bcˇ»
5972 d«efgˇ»hi
5973
5974 j«kˇ»
5975 nlmo
5976 "#
5977 ));
5978
5979 cx.update_editor(|editor, window, cx| {
5980 editor.add_selection_below(&Default::default(), window, cx);
5981 });
5982 cx.assert_editor_state(indoc!(
5983 r#"a«bcˇ»
5984 d«efgˇ»hi
5985
5986 j«kˇ»
5987 n«lmoˇ»
5988 "#
5989 ));
5990 cx.update_editor(|editor, window, cx| {
5991 editor.add_selection_above(&Default::default(), window, cx);
5992 });
5993
5994 cx.assert_editor_state(indoc!(
5995 r#"a«bcˇ»
5996 d«efgˇ»hi
5997
5998 j«kˇ»
5999 nlmo
6000 "#
6001 ));
6002
6003 // Change selections again
6004 cx.set_state(indoc!(
6005 r#"abc
6006 d«ˇefghi
6007
6008 jk
6009 nlm»o
6010 "#
6011 ));
6012
6013 cx.update_editor(|editor, window, cx| {
6014 editor.add_selection_above(&Default::default(), window, cx);
6015 });
6016
6017 cx.assert_editor_state(indoc!(
6018 r#"a«ˇbc»
6019 d«ˇef»ghi
6020
6021 j«ˇk»
6022 n«ˇlm»o
6023 "#
6024 ));
6025
6026 cx.update_editor(|editor, window, cx| {
6027 editor.add_selection_below(&Default::default(), window, cx);
6028 });
6029
6030 cx.assert_editor_state(indoc!(
6031 r#"abc
6032 d«ˇef»ghi
6033
6034 j«ˇk»
6035 n«ˇlm»o
6036 "#
6037 ));
6038}
6039
6040#[gpui::test]
6041async fn test_select_next(cx: &mut TestAppContext) {
6042 init_test(cx, |_| {});
6043
6044 let mut cx = EditorTestContext::new(cx).await;
6045 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6046
6047 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6048 .unwrap();
6049 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6050
6051 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6052 .unwrap();
6053 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6054
6055 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6056 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6057
6058 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6059 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6060
6061 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6062 .unwrap();
6063 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6064
6065 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6066 .unwrap();
6067 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6068
6069 // Test selection direction should be preserved
6070 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6071
6072 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6073 .unwrap();
6074 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6075}
6076
6077#[gpui::test]
6078async fn test_select_all_matches(cx: &mut TestAppContext) {
6079 init_test(cx, |_| {});
6080
6081 let mut cx = EditorTestContext::new(cx).await;
6082
6083 // Test caret-only selections
6084 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6085 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6086 .unwrap();
6087 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6088
6089 // Test left-to-right selections
6090 cx.set_state("abc\n«abcˇ»\nabc");
6091 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6092 .unwrap();
6093 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6094
6095 // Test right-to-left selections
6096 cx.set_state("abc\n«ˇabc»\nabc");
6097 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6098 .unwrap();
6099 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6100
6101 // Test selecting whitespace with caret selection
6102 cx.set_state("abc\nˇ abc\nabc");
6103 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6104 .unwrap();
6105 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6106
6107 // Test selecting whitespace with left-to-right selection
6108 cx.set_state("abc\n«ˇ »abc\nabc");
6109 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6110 .unwrap();
6111 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6112
6113 // Test no matches with right-to-left selection
6114 cx.set_state("abc\n« ˇ»abc\nabc");
6115 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6116 .unwrap();
6117 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6118}
6119
6120#[gpui::test]
6121async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6122 init_test(cx, |_| {});
6123
6124 let mut cx = EditorTestContext::new(cx).await;
6125
6126 let large_body_1 = "\nd".repeat(200);
6127 let large_body_2 = "\ne".repeat(200);
6128
6129 cx.set_state(&format!(
6130 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6131 ));
6132 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6133 let scroll_position = editor.scroll_position(cx);
6134 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6135 scroll_position
6136 });
6137
6138 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6139 .unwrap();
6140 cx.assert_editor_state(&format!(
6141 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6142 ));
6143 let scroll_position_after_selection =
6144 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6145 assert_eq!(
6146 initial_scroll_position, scroll_position_after_selection,
6147 "Scroll position should not change after selecting all matches"
6148 );
6149}
6150
6151#[gpui::test]
6152async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6153 init_test(cx, |_| {});
6154
6155 let mut cx = EditorLspTestContext::new_rust(
6156 lsp::ServerCapabilities {
6157 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6158 ..Default::default()
6159 },
6160 cx,
6161 )
6162 .await;
6163
6164 cx.set_state(indoc! {"
6165 line 1
6166 line 2
6167 linˇe 3
6168 line 4
6169 line 5
6170 "});
6171
6172 // Make an edit
6173 cx.update_editor(|editor, window, cx| {
6174 editor.handle_input("X", window, cx);
6175 });
6176
6177 // Move cursor to a different position
6178 cx.update_editor(|editor, window, cx| {
6179 editor.change_selections(None, window, cx, |s| {
6180 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6181 });
6182 });
6183
6184 cx.assert_editor_state(indoc! {"
6185 line 1
6186 line 2
6187 linXe 3
6188 line 4
6189 liˇne 5
6190 "});
6191
6192 cx.lsp
6193 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6194 Ok(Some(vec![lsp::TextEdit::new(
6195 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6196 "PREFIX ".to_string(),
6197 )]))
6198 });
6199
6200 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6201 .unwrap()
6202 .await
6203 .unwrap();
6204
6205 cx.assert_editor_state(indoc! {"
6206 PREFIX line 1
6207 line 2
6208 linXe 3
6209 line 4
6210 liˇne 5
6211 "});
6212
6213 // Undo formatting
6214 cx.update_editor(|editor, window, cx| {
6215 editor.undo(&Default::default(), window, cx);
6216 });
6217
6218 // Verify cursor moved back to position after edit
6219 cx.assert_editor_state(indoc! {"
6220 line 1
6221 line 2
6222 linXˇe 3
6223 line 4
6224 line 5
6225 "});
6226}
6227
6228#[gpui::test]
6229async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6230 init_test(cx, |_| {});
6231
6232 let mut cx = EditorTestContext::new(cx).await;
6233 cx.set_state(
6234 r#"let foo = 2;
6235lˇet foo = 2;
6236let fooˇ = 2;
6237let foo = 2;
6238let foo = ˇ2;"#,
6239 );
6240
6241 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6242 .unwrap();
6243 cx.assert_editor_state(
6244 r#"let foo = 2;
6245«letˇ» foo = 2;
6246let «fooˇ» = 2;
6247let foo = 2;
6248let foo = «2ˇ»;"#,
6249 );
6250
6251 // noop for multiple selections with different contents
6252 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6253 .unwrap();
6254 cx.assert_editor_state(
6255 r#"let foo = 2;
6256«letˇ» foo = 2;
6257let «fooˇ» = 2;
6258let foo = 2;
6259let foo = «2ˇ»;"#,
6260 );
6261
6262 // Test last selection direction should be preserved
6263 cx.set_state(
6264 r#"let foo = 2;
6265let foo = 2;
6266let «fooˇ» = 2;
6267let «ˇfoo» = 2;
6268let foo = 2;"#,
6269 );
6270
6271 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6272 .unwrap();
6273 cx.assert_editor_state(
6274 r#"let foo = 2;
6275let foo = 2;
6276let «fooˇ» = 2;
6277let «ˇfoo» = 2;
6278let «ˇfoo» = 2;"#,
6279 );
6280}
6281
6282#[gpui::test]
6283async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6284 init_test(cx, |_| {});
6285
6286 let mut cx =
6287 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6288
6289 cx.assert_editor_state(indoc! {"
6290 ˇbbb
6291 ccc
6292
6293 bbb
6294 ccc
6295 "});
6296 cx.dispatch_action(SelectPrevious::default());
6297 cx.assert_editor_state(indoc! {"
6298 «bbbˇ»
6299 ccc
6300
6301 bbb
6302 ccc
6303 "});
6304 cx.dispatch_action(SelectPrevious::default());
6305 cx.assert_editor_state(indoc! {"
6306 «bbbˇ»
6307 ccc
6308
6309 «bbbˇ»
6310 ccc
6311 "});
6312}
6313
6314#[gpui::test]
6315async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6316 init_test(cx, |_| {});
6317
6318 let mut cx = EditorTestContext::new(cx).await;
6319 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6320
6321 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6322 .unwrap();
6323 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6324
6325 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6326 .unwrap();
6327 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6328
6329 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6330 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6331
6332 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6333 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6334
6335 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6336 .unwrap();
6337 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6338
6339 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6340 .unwrap();
6341 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6342}
6343
6344#[gpui::test]
6345async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6346 init_test(cx, |_| {});
6347
6348 let mut cx = EditorTestContext::new(cx).await;
6349 cx.set_state("aˇ");
6350
6351 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6352 .unwrap();
6353 cx.assert_editor_state("«aˇ»");
6354 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6355 .unwrap();
6356 cx.assert_editor_state("«aˇ»");
6357}
6358
6359#[gpui::test]
6360async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6361 init_test(cx, |_| {});
6362
6363 let mut cx = EditorTestContext::new(cx).await;
6364 cx.set_state(
6365 r#"let foo = 2;
6366lˇet foo = 2;
6367let fooˇ = 2;
6368let foo = 2;
6369let foo = ˇ2;"#,
6370 );
6371
6372 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6373 .unwrap();
6374 cx.assert_editor_state(
6375 r#"let foo = 2;
6376«letˇ» foo = 2;
6377let «fooˇ» = 2;
6378let foo = 2;
6379let foo = «2ˇ»;"#,
6380 );
6381
6382 // noop for multiple selections with different contents
6383 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6384 .unwrap();
6385 cx.assert_editor_state(
6386 r#"let foo = 2;
6387«letˇ» foo = 2;
6388let «fooˇ» = 2;
6389let foo = 2;
6390let foo = «2ˇ»;"#,
6391 );
6392}
6393
6394#[gpui::test]
6395async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6396 init_test(cx, |_| {});
6397
6398 let mut cx = EditorTestContext::new(cx).await;
6399 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6400
6401 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6402 .unwrap();
6403 // selection direction is preserved
6404 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6405
6406 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6407 .unwrap();
6408 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6409
6410 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6411 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6412
6413 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6414 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6415
6416 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6417 .unwrap();
6418 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6419
6420 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6421 .unwrap();
6422 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6423}
6424
6425#[gpui::test]
6426async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6427 init_test(cx, |_| {});
6428
6429 let language = Arc::new(Language::new(
6430 LanguageConfig::default(),
6431 Some(tree_sitter_rust::LANGUAGE.into()),
6432 ));
6433
6434 let text = r#"
6435 use mod1::mod2::{mod3, mod4};
6436
6437 fn fn_1(param1: bool, param2: &str) {
6438 let var1 = "text";
6439 }
6440 "#
6441 .unindent();
6442
6443 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6444 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6445 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6446
6447 editor
6448 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6449 .await;
6450
6451 editor.update_in(cx, |editor, window, cx| {
6452 editor.change_selections(None, window, cx, |s| {
6453 s.select_display_ranges([
6454 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6455 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6456 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6457 ]);
6458 });
6459 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6460 });
6461 editor.update(cx, |editor, cx| {
6462 assert_text_with_selections(
6463 editor,
6464 indoc! {r#"
6465 use mod1::mod2::{mod3, «mod4ˇ»};
6466
6467 fn fn_1«ˇ(param1: bool, param2: &str)» {
6468 let var1 = "«ˇtext»";
6469 }
6470 "#},
6471 cx,
6472 );
6473 });
6474
6475 editor.update_in(cx, |editor, window, cx| {
6476 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6477 });
6478 editor.update(cx, |editor, cx| {
6479 assert_text_with_selections(
6480 editor,
6481 indoc! {r#"
6482 use mod1::mod2::«{mod3, mod4}ˇ»;
6483
6484 «ˇfn fn_1(param1: bool, param2: &str) {
6485 let var1 = "text";
6486 }»
6487 "#},
6488 cx,
6489 );
6490 });
6491
6492 editor.update_in(cx, |editor, window, cx| {
6493 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6494 });
6495 assert_eq!(
6496 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6497 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6498 );
6499
6500 // Trying to expand the selected syntax node one more time has no effect.
6501 editor.update_in(cx, |editor, window, cx| {
6502 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6503 });
6504 assert_eq!(
6505 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6506 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6507 );
6508
6509 editor.update_in(cx, |editor, window, cx| {
6510 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6511 });
6512 editor.update(cx, |editor, cx| {
6513 assert_text_with_selections(
6514 editor,
6515 indoc! {r#"
6516 use mod1::mod2::«{mod3, mod4}ˇ»;
6517
6518 «ˇfn fn_1(param1: bool, param2: &str) {
6519 let var1 = "text";
6520 }»
6521 "#},
6522 cx,
6523 );
6524 });
6525
6526 editor.update_in(cx, |editor, window, cx| {
6527 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6528 });
6529 editor.update(cx, |editor, cx| {
6530 assert_text_with_selections(
6531 editor,
6532 indoc! {r#"
6533 use mod1::mod2::{mod3, «mod4ˇ»};
6534
6535 fn fn_1«ˇ(param1: bool, param2: &str)» {
6536 let var1 = "«ˇtext»";
6537 }
6538 "#},
6539 cx,
6540 );
6541 });
6542
6543 editor.update_in(cx, |editor, window, cx| {
6544 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6545 });
6546 editor.update(cx, |editor, cx| {
6547 assert_text_with_selections(
6548 editor,
6549 indoc! {r#"
6550 use mod1::mod2::{mod3, mo«ˇ»d4};
6551
6552 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6553 let var1 = "te«ˇ»xt";
6554 }
6555 "#},
6556 cx,
6557 );
6558 });
6559
6560 // Trying to shrink the selected syntax node one more time has no effect.
6561 editor.update_in(cx, |editor, window, cx| {
6562 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6563 });
6564 editor.update_in(cx, |editor, _, cx| {
6565 assert_text_with_selections(
6566 editor,
6567 indoc! {r#"
6568 use mod1::mod2::{mod3, mo«ˇ»d4};
6569
6570 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6571 let var1 = "te«ˇ»xt";
6572 }
6573 "#},
6574 cx,
6575 );
6576 });
6577
6578 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6579 // a fold.
6580 editor.update_in(cx, |editor, window, cx| {
6581 editor.fold_creases(
6582 vec![
6583 Crease::simple(
6584 Point::new(0, 21)..Point::new(0, 24),
6585 FoldPlaceholder::test(),
6586 ),
6587 Crease::simple(
6588 Point::new(3, 20)..Point::new(3, 22),
6589 FoldPlaceholder::test(),
6590 ),
6591 ],
6592 true,
6593 window,
6594 cx,
6595 );
6596 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6597 });
6598 editor.update(cx, |editor, cx| {
6599 assert_text_with_selections(
6600 editor,
6601 indoc! {r#"
6602 use mod1::mod2::«{mod3, mod4}ˇ»;
6603
6604 fn fn_1«ˇ(param1: bool, param2: &str)» {
6605 let var1 = "«ˇtext»";
6606 }
6607 "#},
6608 cx,
6609 );
6610 });
6611}
6612
6613#[gpui::test]
6614async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6615 init_test(cx, |_| {});
6616
6617 let language = Arc::new(Language::new(
6618 LanguageConfig::default(),
6619 Some(tree_sitter_rust::LANGUAGE.into()),
6620 ));
6621
6622 let text = "let a = 2;";
6623
6624 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6625 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6626 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6627
6628 editor
6629 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6630 .await;
6631
6632 // Test case 1: Cursor at end of word
6633 editor.update_in(cx, |editor, window, cx| {
6634 editor.change_selections(None, window, cx, |s| {
6635 s.select_display_ranges([
6636 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6637 ]);
6638 });
6639 });
6640 editor.update(cx, |editor, cx| {
6641 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6642 });
6643 editor.update_in(cx, |editor, window, cx| {
6644 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6645 });
6646 editor.update(cx, |editor, cx| {
6647 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6648 });
6649 editor.update_in(cx, |editor, window, cx| {
6650 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6651 });
6652 editor.update(cx, |editor, cx| {
6653 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6654 });
6655
6656 // Test case 2: Cursor at end of statement
6657 editor.update_in(cx, |editor, window, cx| {
6658 editor.change_selections(None, window, cx, |s| {
6659 s.select_display_ranges([
6660 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6661 ]);
6662 });
6663 });
6664 editor.update(cx, |editor, cx| {
6665 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6666 });
6667 editor.update_in(cx, |editor, window, cx| {
6668 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6669 });
6670 editor.update(cx, |editor, cx| {
6671 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6672 });
6673}
6674
6675#[gpui::test]
6676async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6677 init_test(cx, |_| {});
6678
6679 let language = Arc::new(Language::new(
6680 LanguageConfig::default(),
6681 Some(tree_sitter_rust::LANGUAGE.into()),
6682 ));
6683
6684 let text = r#"
6685 use mod1::mod2::{mod3, mod4};
6686
6687 fn fn_1(param1: bool, param2: &str) {
6688 let var1 = "hello world";
6689 }
6690 "#
6691 .unindent();
6692
6693 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6694 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6695 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6696
6697 editor
6698 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6699 .await;
6700
6701 // Test 1: Cursor on a letter of a string word
6702 editor.update_in(cx, |editor, window, cx| {
6703 editor.change_selections(None, window, cx, |s| {
6704 s.select_display_ranges([
6705 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6706 ]);
6707 });
6708 });
6709 editor.update_in(cx, |editor, window, cx| {
6710 assert_text_with_selections(
6711 editor,
6712 indoc! {r#"
6713 use mod1::mod2::{mod3, mod4};
6714
6715 fn fn_1(param1: bool, param2: &str) {
6716 let var1 = "hˇello world";
6717 }
6718 "#},
6719 cx,
6720 );
6721 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6722 assert_text_with_selections(
6723 editor,
6724 indoc! {r#"
6725 use mod1::mod2::{mod3, mod4};
6726
6727 fn fn_1(param1: bool, param2: &str) {
6728 let var1 = "«ˇhello» world";
6729 }
6730 "#},
6731 cx,
6732 );
6733 });
6734
6735 // Test 2: Partial selection within a word
6736 editor.update_in(cx, |editor, window, cx| {
6737 editor.change_selections(None, window, cx, |s| {
6738 s.select_display_ranges([
6739 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6740 ]);
6741 });
6742 });
6743 editor.update_in(cx, |editor, window, cx| {
6744 assert_text_with_selections(
6745 editor,
6746 indoc! {r#"
6747 use mod1::mod2::{mod3, mod4};
6748
6749 fn fn_1(param1: bool, param2: &str) {
6750 let var1 = "h«elˇ»lo world";
6751 }
6752 "#},
6753 cx,
6754 );
6755 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6756 assert_text_with_selections(
6757 editor,
6758 indoc! {r#"
6759 use mod1::mod2::{mod3, mod4};
6760
6761 fn fn_1(param1: bool, param2: &str) {
6762 let var1 = "«ˇhello» world";
6763 }
6764 "#},
6765 cx,
6766 );
6767 });
6768
6769 // Test 3: Complete word already selected
6770 editor.update_in(cx, |editor, window, cx| {
6771 editor.change_selections(None, window, cx, |s| {
6772 s.select_display_ranges([
6773 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6774 ]);
6775 });
6776 });
6777 editor.update_in(cx, |editor, window, cx| {
6778 assert_text_with_selections(
6779 editor,
6780 indoc! {r#"
6781 use mod1::mod2::{mod3, mod4};
6782
6783 fn fn_1(param1: bool, param2: &str) {
6784 let var1 = "«helloˇ» world";
6785 }
6786 "#},
6787 cx,
6788 );
6789 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6790 assert_text_with_selections(
6791 editor,
6792 indoc! {r#"
6793 use mod1::mod2::{mod3, mod4};
6794
6795 fn fn_1(param1: bool, param2: &str) {
6796 let var1 = "«hello worldˇ»";
6797 }
6798 "#},
6799 cx,
6800 );
6801 });
6802
6803 // Test 4: Selection spanning across words
6804 editor.update_in(cx, |editor, window, cx| {
6805 editor.change_selections(None, window, cx, |s| {
6806 s.select_display_ranges([
6807 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6808 ]);
6809 });
6810 });
6811 editor.update_in(cx, |editor, window, cx| {
6812 assert_text_with_selections(
6813 editor,
6814 indoc! {r#"
6815 use mod1::mod2::{mod3, mod4};
6816
6817 fn fn_1(param1: bool, param2: &str) {
6818 let var1 = "hel«lo woˇ»rld";
6819 }
6820 "#},
6821 cx,
6822 );
6823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6824 assert_text_with_selections(
6825 editor,
6826 indoc! {r#"
6827 use mod1::mod2::{mod3, mod4};
6828
6829 fn fn_1(param1: bool, param2: &str) {
6830 let var1 = "«ˇhello world»";
6831 }
6832 "#},
6833 cx,
6834 );
6835 });
6836
6837 // Test 5: Expansion beyond string
6838 editor.update_in(cx, |editor, window, cx| {
6839 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6840 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6841 assert_text_with_selections(
6842 editor,
6843 indoc! {r#"
6844 use mod1::mod2::{mod3, mod4};
6845
6846 fn fn_1(param1: bool, param2: &str) {
6847 «ˇlet var1 = "hello world";»
6848 }
6849 "#},
6850 cx,
6851 );
6852 });
6853}
6854
6855#[gpui::test]
6856async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6857 init_test(cx, |_| {});
6858
6859 let base_text = r#"
6860 impl A {
6861 // this is an uncommitted comment
6862
6863 fn b() {
6864 c();
6865 }
6866
6867 // this is another uncommitted comment
6868
6869 fn d() {
6870 // e
6871 // f
6872 }
6873 }
6874
6875 fn g() {
6876 // h
6877 }
6878 "#
6879 .unindent();
6880
6881 let text = r#"
6882 ˇimpl A {
6883
6884 fn b() {
6885 c();
6886 }
6887
6888 fn d() {
6889 // e
6890 // f
6891 }
6892 }
6893
6894 fn g() {
6895 // h
6896 }
6897 "#
6898 .unindent();
6899
6900 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6901 cx.set_state(&text);
6902 cx.set_head_text(&base_text);
6903 cx.update_editor(|editor, window, cx| {
6904 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6905 });
6906
6907 cx.assert_state_with_diff(
6908 "
6909 ˇimpl A {
6910 - // this is an uncommitted comment
6911
6912 fn b() {
6913 c();
6914 }
6915
6916 - // this is another uncommitted comment
6917 -
6918 fn d() {
6919 // e
6920 // f
6921 }
6922 }
6923
6924 fn g() {
6925 // h
6926 }
6927 "
6928 .unindent(),
6929 );
6930
6931 let expected_display_text = "
6932 impl A {
6933 // this is an uncommitted comment
6934
6935 fn b() {
6936 ⋯
6937 }
6938
6939 // this is another uncommitted comment
6940
6941 fn d() {
6942 ⋯
6943 }
6944 }
6945
6946 fn g() {
6947 ⋯
6948 }
6949 "
6950 .unindent();
6951
6952 cx.update_editor(|editor, window, cx| {
6953 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6954 assert_eq!(editor.display_text(cx), expected_display_text);
6955 });
6956}
6957
6958#[gpui::test]
6959async fn test_autoindent(cx: &mut TestAppContext) {
6960 init_test(cx, |_| {});
6961
6962 let language = Arc::new(
6963 Language::new(
6964 LanguageConfig {
6965 brackets: BracketPairConfig {
6966 pairs: vec![
6967 BracketPair {
6968 start: "{".to_string(),
6969 end: "}".to_string(),
6970 close: false,
6971 surround: false,
6972 newline: true,
6973 },
6974 BracketPair {
6975 start: "(".to_string(),
6976 end: ")".to_string(),
6977 close: false,
6978 surround: false,
6979 newline: true,
6980 },
6981 ],
6982 ..Default::default()
6983 },
6984 ..Default::default()
6985 },
6986 Some(tree_sitter_rust::LANGUAGE.into()),
6987 )
6988 .with_indents_query(
6989 r#"
6990 (_ "(" ")" @end) @indent
6991 (_ "{" "}" @end) @indent
6992 "#,
6993 )
6994 .unwrap(),
6995 );
6996
6997 let text = "fn a() {}";
6998
6999 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7000 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7001 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7002 editor
7003 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7004 .await;
7005
7006 editor.update_in(cx, |editor, window, cx| {
7007 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7008 editor.newline(&Newline, window, cx);
7009 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7010 assert_eq!(
7011 editor.selections.ranges(cx),
7012 &[
7013 Point::new(1, 4)..Point::new(1, 4),
7014 Point::new(3, 4)..Point::new(3, 4),
7015 Point::new(5, 0)..Point::new(5, 0)
7016 ]
7017 );
7018 });
7019}
7020
7021#[gpui::test]
7022async fn test_autoindent_selections(cx: &mut TestAppContext) {
7023 init_test(cx, |_| {});
7024
7025 {
7026 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7027 cx.set_state(indoc! {"
7028 impl A {
7029
7030 fn b() {}
7031
7032 «fn c() {
7033
7034 }ˇ»
7035 }
7036 "});
7037
7038 cx.update_editor(|editor, window, cx| {
7039 editor.autoindent(&Default::default(), window, cx);
7040 });
7041
7042 cx.assert_editor_state(indoc! {"
7043 impl A {
7044
7045 fn b() {}
7046
7047 «fn c() {
7048
7049 }ˇ»
7050 }
7051 "});
7052 }
7053
7054 {
7055 let mut cx = EditorTestContext::new_multibuffer(
7056 cx,
7057 [indoc! { "
7058 impl A {
7059 «
7060 // a
7061 fn b(){}
7062 »
7063 «
7064 }
7065 fn c(){}
7066 »
7067 "}],
7068 );
7069
7070 let buffer = cx.update_editor(|editor, _, cx| {
7071 let buffer = editor.buffer().update(cx, |buffer, _| {
7072 buffer.all_buffers().iter().next().unwrap().clone()
7073 });
7074 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7075 buffer
7076 });
7077
7078 cx.run_until_parked();
7079 cx.update_editor(|editor, window, cx| {
7080 editor.select_all(&Default::default(), window, cx);
7081 editor.autoindent(&Default::default(), window, cx)
7082 });
7083 cx.run_until_parked();
7084
7085 cx.update(|_, cx| {
7086 assert_eq!(
7087 buffer.read(cx).text(),
7088 indoc! { "
7089 impl A {
7090
7091 // a
7092 fn b(){}
7093
7094
7095 }
7096 fn c(){}
7097
7098 " }
7099 )
7100 });
7101 }
7102}
7103
7104#[gpui::test]
7105async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7106 init_test(cx, |_| {});
7107
7108 let mut cx = EditorTestContext::new(cx).await;
7109
7110 let language = Arc::new(Language::new(
7111 LanguageConfig {
7112 brackets: BracketPairConfig {
7113 pairs: vec![
7114 BracketPair {
7115 start: "{".to_string(),
7116 end: "}".to_string(),
7117 close: true,
7118 surround: true,
7119 newline: true,
7120 },
7121 BracketPair {
7122 start: "(".to_string(),
7123 end: ")".to_string(),
7124 close: true,
7125 surround: true,
7126 newline: true,
7127 },
7128 BracketPair {
7129 start: "/*".to_string(),
7130 end: " */".to_string(),
7131 close: true,
7132 surround: true,
7133 newline: true,
7134 },
7135 BracketPair {
7136 start: "[".to_string(),
7137 end: "]".to_string(),
7138 close: false,
7139 surround: false,
7140 newline: true,
7141 },
7142 BracketPair {
7143 start: "\"".to_string(),
7144 end: "\"".to_string(),
7145 close: true,
7146 surround: true,
7147 newline: false,
7148 },
7149 BracketPair {
7150 start: "<".to_string(),
7151 end: ">".to_string(),
7152 close: false,
7153 surround: true,
7154 newline: true,
7155 },
7156 ],
7157 ..Default::default()
7158 },
7159 autoclose_before: "})]".to_string(),
7160 ..Default::default()
7161 },
7162 Some(tree_sitter_rust::LANGUAGE.into()),
7163 ));
7164
7165 cx.language_registry().add(language.clone());
7166 cx.update_buffer(|buffer, cx| {
7167 buffer.set_language(Some(language), cx);
7168 });
7169
7170 cx.set_state(
7171 &r#"
7172 🏀ˇ
7173 εˇ
7174 ❤️ˇ
7175 "#
7176 .unindent(),
7177 );
7178
7179 // autoclose multiple nested brackets at multiple cursors
7180 cx.update_editor(|editor, window, cx| {
7181 editor.handle_input("{", window, cx);
7182 editor.handle_input("{", window, cx);
7183 editor.handle_input("{", window, cx);
7184 });
7185 cx.assert_editor_state(
7186 &"
7187 🏀{{{ˇ}}}
7188 ε{{{ˇ}}}
7189 ❤️{{{ˇ}}}
7190 "
7191 .unindent(),
7192 );
7193
7194 // insert a different closing bracket
7195 cx.update_editor(|editor, window, cx| {
7196 editor.handle_input(")", window, cx);
7197 });
7198 cx.assert_editor_state(
7199 &"
7200 🏀{{{)ˇ}}}
7201 ε{{{)ˇ}}}
7202 ❤️{{{)ˇ}}}
7203 "
7204 .unindent(),
7205 );
7206
7207 // skip over the auto-closed brackets when typing a closing bracket
7208 cx.update_editor(|editor, window, cx| {
7209 editor.move_right(&MoveRight, window, cx);
7210 editor.handle_input("}", window, cx);
7211 editor.handle_input("}", window, cx);
7212 editor.handle_input("}", window, cx);
7213 });
7214 cx.assert_editor_state(
7215 &"
7216 🏀{{{)}}}}ˇ
7217 ε{{{)}}}}ˇ
7218 ❤️{{{)}}}}ˇ
7219 "
7220 .unindent(),
7221 );
7222
7223 // autoclose multi-character pairs
7224 cx.set_state(
7225 &"
7226 ˇ
7227 ˇ
7228 "
7229 .unindent(),
7230 );
7231 cx.update_editor(|editor, window, cx| {
7232 editor.handle_input("/", window, cx);
7233 editor.handle_input("*", window, cx);
7234 });
7235 cx.assert_editor_state(
7236 &"
7237 /*ˇ */
7238 /*ˇ */
7239 "
7240 .unindent(),
7241 );
7242
7243 // one cursor autocloses a multi-character pair, one cursor
7244 // does not autoclose.
7245 cx.set_state(
7246 &"
7247 /ˇ
7248 ˇ
7249 "
7250 .unindent(),
7251 );
7252 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7253 cx.assert_editor_state(
7254 &"
7255 /*ˇ */
7256 *ˇ
7257 "
7258 .unindent(),
7259 );
7260
7261 // Don't autoclose if the next character isn't whitespace and isn't
7262 // listed in the language's "autoclose_before" section.
7263 cx.set_state("ˇa b");
7264 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7265 cx.assert_editor_state("{ˇa b");
7266
7267 // Don't autoclose if `close` is false for the bracket pair
7268 cx.set_state("ˇ");
7269 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7270 cx.assert_editor_state("[ˇ");
7271
7272 // Surround with brackets if text is selected
7273 cx.set_state("«aˇ» b");
7274 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7275 cx.assert_editor_state("{«aˇ»} b");
7276
7277 // Autoclose when not immediately after a word character
7278 cx.set_state("a ˇ");
7279 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7280 cx.assert_editor_state("a \"ˇ\"");
7281
7282 // Autoclose pair where the start and end characters are the same
7283 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7284 cx.assert_editor_state("a \"\"ˇ");
7285
7286 // Don't autoclose when immediately after a word character
7287 cx.set_state("aˇ");
7288 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7289 cx.assert_editor_state("a\"ˇ");
7290
7291 // Do autoclose when after a non-word character
7292 cx.set_state("{ˇ");
7293 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7294 cx.assert_editor_state("{\"ˇ\"");
7295
7296 // Non identical pairs autoclose regardless of preceding character
7297 cx.set_state("aˇ");
7298 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7299 cx.assert_editor_state("a{ˇ}");
7300
7301 // Don't autoclose pair if autoclose is disabled
7302 cx.set_state("ˇ");
7303 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7304 cx.assert_editor_state("<ˇ");
7305
7306 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7307 cx.set_state("«aˇ» b");
7308 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7309 cx.assert_editor_state("<«aˇ»> b");
7310}
7311
7312#[gpui::test]
7313async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7314 init_test(cx, |settings| {
7315 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7316 });
7317
7318 let mut cx = EditorTestContext::new(cx).await;
7319
7320 let language = Arc::new(Language::new(
7321 LanguageConfig {
7322 brackets: BracketPairConfig {
7323 pairs: vec![
7324 BracketPair {
7325 start: "{".to_string(),
7326 end: "}".to_string(),
7327 close: true,
7328 surround: true,
7329 newline: true,
7330 },
7331 BracketPair {
7332 start: "(".to_string(),
7333 end: ")".to_string(),
7334 close: true,
7335 surround: true,
7336 newline: true,
7337 },
7338 BracketPair {
7339 start: "[".to_string(),
7340 end: "]".to_string(),
7341 close: false,
7342 surround: false,
7343 newline: true,
7344 },
7345 ],
7346 ..Default::default()
7347 },
7348 autoclose_before: "})]".to_string(),
7349 ..Default::default()
7350 },
7351 Some(tree_sitter_rust::LANGUAGE.into()),
7352 ));
7353
7354 cx.language_registry().add(language.clone());
7355 cx.update_buffer(|buffer, cx| {
7356 buffer.set_language(Some(language), cx);
7357 });
7358
7359 cx.set_state(
7360 &"
7361 ˇ
7362 ˇ
7363 ˇ
7364 "
7365 .unindent(),
7366 );
7367
7368 // ensure only matching closing brackets are skipped over
7369 cx.update_editor(|editor, window, cx| {
7370 editor.handle_input("}", window, cx);
7371 editor.move_left(&MoveLeft, window, cx);
7372 editor.handle_input(")", window, cx);
7373 editor.move_left(&MoveLeft, window, cx);
7374 });
7375 cx.assert_editor_state(
7376 &"
7377 ˇ)}
7378 ˇ)}
7379 ˇ)}
7380 "
7381 .unindent(),
7382 );
7383
7384 // skip-over closing brackets at multiple cursors
7385 cx.update_editor(|editor, window, cx| {
7386 editor.handle_input(")", window, cx);
7387 editor.handle_input("}", window, cx);
7388 });
7389 cx.assert_editor_state(
7390 &"
7391 )}ˇ
7392 )}ˇ
7393 )}ˇ
7394 "
7395 .unindent(),
7396 );
7397
7398 // ignore non-close brackets
7399 cx.update_editor(|editor, window, cx| {
7400 editor.handle_input("]", window, cx);
7401 editor.move_left(&MoveLeft, window, cx);
7402 editor.handle_input("]", window, cx);
7403 });
7404 cx.assert_editor_state(
7405 &"
7406 )}]ˇ]
7407 )}]ˇ]
7408 )}]ˇ]
7409 "
7410 .unindent(),
7411 );
7412}
7413
7414#[gpui::test]
7415async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7416 init_test(cx, |_| {});
7417
7418 let mut cx = EditorTestContext::new(cx).await;
7419
7420 let html_language = Arc::new(
7421 Language::new(
7422 LanguageConfig {
7423 name: "HTML".into(),
7424 brackets: BracketPairConfig {
7425 pairs: vec![
7426 BracketPair {
7427 start: "<".into(),
7428 end: ">".into(),
7429 close: true,
7430 ..Default::default()
7431 },
7432 BracketPair {
7433 start: "{".into(),
7434 end: "}".into(),
7435 close: true,
7436 ..Default::default()
7437 },
7438 BracketPair {
7439 start: "(".into(),
7440 end: ")".into(),
7441 close: true,
7442 ..Default::default()
7443 },
7444 ],
7445 ..Default::default()
7446 },
7447 autoclose_before: "})]>".into(),
7448 ..Default::default()
7449 },
7450 Some(tree_sitter_html::LANGUAGE.into()),
7451 )
7452 .with_injection_query(
7453 r#"
7454 (script_element
7455 (raw_text) @injection.content
7456 (#set! injection.language "javascript"))
7457 "#,
7458 )
7459 .unwrap(),
7460 );
7461
7462 let javascript_language = Arc::new(Language::new(
7463 LanguageConfig {
7464 name: "JavaScript".into(),
7465 brackets: BracketPairConfig {
7466 pairs: vec![
7467 BracketPair {
7468 start: "/*".into(),
7469 end: " */".into(),
7470 close: true,
7471 ..Default::default()
7472 },
7473 BracketPair {
7474 start: "{".into(),
7475 end: "}".into(),
7476 close: true,
7477 ..Default::default()
7478 },
7479 BracketPair {
7480 start: "(".into(),
7481 end: ")".into(),
7482 close: true,
7483 ..Default::default()
7484 },
7485 ],
7486 ..Default::default()
7487 },
7488 autoclose_before: "})]>".into(),
7489 ..Default::default()
7490 },
7491 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7492 ));
7493
7494 cx.language_registry().add(html_language.clone());
7495 cx.language_registry().add(javascript_language.clone());
7496
7497 cx.update_buffer(|buffer, cx| {
7498 buffer.set_language(Some(html_language), cx);
7499 });
7500
7501 cx.set_state(
7502 &r#"
7503 <body>ˇ
7504 <script>
7505 var x = 1;ˇ
7506 </script>
7507 </body>ˇ
7508 "#
7509 .unindent(),
7510 );
7511
7512 // Precondition: different languages are active at different locations.
7513 cx.update_editor(|editor, window, cx| {
7514 let snapshot = editor.snapshot(window, cx);
7515 let cursors = editor.selections.ranges::<usize>(cx);
7516 let languages = cursors
7517 .iter()
7518 .map(|c| snapshot.language_at(c.start).unwrap().name())
7519 .collect::<Vec<_>>();
7520 assert_eq!(
7521 languages,
7522 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7523 );
7524 });
7525
7526 // Angle brackets autoclose in HTML, but not JavaScript.
7527 cx.update_editor(|editor, window, cx| {
7528 editor.handle_input("<", window, cx);
7529 editor.handle_input("a", window, cx);
7530 });
7531 cx.assert_editor_state(
7532 &r#"
7533 <body><aˇ>
7534 <script>
7535 var x = 1;<aˇ
7536 </script>
7537 </body><aˇ>
7538 "#
7539 .unindent(),
7540 );
7541
7542 // Curly braces and parens autoclose in both HTML and JavaScript.
7543 cx.update_editor(|editor, window, cx| {
7544 editor.handle_input(" b=", window, cx);
7545 editor.handle_input("{", window, cx);
7546 editor.handle_input("c", window, cx);
7547 editor.handle_input("(", window, cx);
7548 });
7549 cx.assert_editor_state(
7550 &r#"
7551 <body><a b={c(ˇ)}>
7552 <script>
7553 var x = 1;<a b={c(ˇ)}
7554 </script>
7555 </body><a b={c(ˇ)}>
7556 "#
7557 .unindent(),
7558 );
7559
7560 // Brackets that were already autoclosed are skipped.
7561 cx.update_editor(|editor, window, cx| {
7562 editor.handle_input(")", window, cx);
7563 editor.handle_input("d", window, cx);
7564 editor.handle_input("}", window, cx);
7565 });
7566 cx.assert_editor_state(
7567 &r#"
7568 <body><a b={c()d}ˇ>
7569 <script>
7570 var x = 1;<a b={c()d}ˇ
7571 </script>
7572 </body><a b={c()d}ˇ>
7573 "#
7574 .unindent(),
7575 );
7576 cx.update_editor(|editor, window, cx| {
7577 editor.handle_input(">", window, cx);
7578 });
7579 cx.assert_editor_state(
7580 &r#"
7581 <body><a b={c()d}>ˇ
7582 <script>
7583 var x = 1;<a b={c()d}>ˇ
7584 </script>
7585 </body><a b={c()d}>ˇ
7586 "#
7587 .unindent(),
7588 );
7589
7590 // Reset
7591 cx.set_state(
7592 &r#"
7593 <body>ˇ
7594 <script>
7595 var x = 1;ˇ
7596 </script>
7597 </body>ˇ
7598 "#
7599 .unindent(),
7600 );
7601
7602 cx.update_editor(|editor, window, cx| {
7603 editor.handle_input("<", window, cx);
7604 });
7605 cx.assert_editor_state(
7606 &r#"
7607 <body><ˇ>
7608 <script>
7609 var x = 1;<ˇ
7610 </script>
7611 </body><ˇ>
7612 "#
7613 .unindent(),
7614 );
7615
7616 // When backspacing, the closing angle brackets are removed.
7617 cx.update_editor(|editor, window, cx| {
7618 editor.backspace(&Backspace, window, cx);
7619 });
7620 cx.assert_editor_state(
7621 &r#"
7622 <body>ˇ
7623 <script>
7624 var x = 1;ˇ
7625 </script>
7626 </body>ˇ
7627 "#
7628 .unindent(),
7629 );
7630
7631 // Block comments autoclose in JavaScript, but not HTML.
7632 cx.update_editor(|editor, window, cx| {
7633 editor.handle_input("/", window, cx);
7634 editor.handle_input("*", window, cx);
7635 });
7636 cx.assert_editor_state(
7637 &r#"
7638 <body>/*ˇ
7639 <script>
7640 var x = 1;/*ˇ */
7641 </script>
7642 </body>/*ˇ
7643 "#
7644 .unindent(),
7645 );
7646}
7647
7648#[gpui::test]
7649async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7650 init_test(cx, |_| {});
7651
7652 let mut cx = EditorTestContext::new(cx).await;
7653
7654 let rust_language = Arc::new(
7655 Language::new(
7656 LanguageConfig {
7657 name: "Rust".into(),
7658 brackets: serde_json::from_value(json!([
7659 { "start": "{", "end": "}", "close": true, "newline": true },
7660 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7661 ]))
7662 .unwrap(),
7663 autoclose_before: "})]>".into(),
7664 ..Default::default()
7665 },
7666 Some(tree_sitter_rust::LANGUAGE.into()),
7667 )
7668 .with_override_query("(string_literal) @string")
7669 .unwrap(),
7670 );
7671
7672 cx.language_registry().add(rust_language.clone());
7673 cx.update_buffer(|buffer, cx| {
7674 buffer.set_language(Some(rust_language), cx);
7675 });
7676
7677 cx.set_state(
7678 &r#"
7679 let x = ˇ
7680 "#
7681 .unindent(),
7682 );
7683
7684 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7685 cx.update_editor(|editor, window, cx| {
7686 editor.handle_input("\"", window, cx);
7687 });
7688 cx.assert_editor_state(
7689 &r#"
7690 let x = "ˇ"
7691 "#
7692 .unindent(),
7693 );
7694
7695 // Inserting another quotation mark. The cursor moves across the existing
7696 // automatically-inserted quotation mark.
7697 cx.update_editor(|editor, window, cx| {
7698 editor.handle_input("\"", window, cx);
7699 });
7700 cx.assert_editor_state(
7701 &r#"
7702 let x = ""ˇ
7703 "#
7704 .unindent(),
7705 );
7706
7707 // Reset
7708 cx.set_state(
7709 &r#"
7710 let x = ˇ
7711 "#
7712 .unindent(),
7713 );
7714
7715 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7716 cx.update_editor(|editor, window, cx| {
7717 editor.handle_input("\"", window, cx);
7718 editor.handle_input(" ", window, cx);
7719 editor.move_left(&Default::default(), window, cx);
7720 editor.handle_input("\\", window, cx);
7721 editor.handle_input("\"", window, cx);
7722 });
7723 cx.assert_editor_state(
7724 &r#"
7725 let x = "\"ˇ "
7726 "#
7727 .unindent(),
7728 );
7729
7730 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7731 // mark. Nothing is inserted.
7732 cx.update_editor(|editor, window, cx| {
7733 editor.move_right(&Default::default(), window, cx);
7734 editor.handle_input("\"", window, cx);
7735 });
7736 cx.assert_editor_state(
7737 &r#"
7738 let x = "\" "ˇ
7739 "#
7740 .unindent(),
7741 );
7742}
7743
7744#[gpui::test]
7745async fn test_surround_with_pair(cx: &mut TestAppContext) {
7746 init_test(cx, |_| {});
7747
7748 let language = Arc::new(Language::new(
7749 LanguageConfig {
7750 brackets: BracketPairConfig {
7751 pairs: vec![
7752 BracketPair {
7753 start: "{".to_string(),
7754 end: "}".to_string(),
7755 close: true,
7756 surround: true,
7757 newline: true,
7758 },
7759 BracketPair {
7760 start: "/* ".to_string(),
7761 end: "*/".to_string(),
7762 close: true,
7763 surround: true,
7764 ..Default::default()
7765 },
7766 ],
7767 ..Default::default()
7768 },
7769 ..Default::default()
7770 },
7771 Some(tree_sitter_rust::LANGUAGE.into()),
7772 ));
7773
7774 let text = r#"
7775 a
7776 b
7777 c
7778 "#
7779 .unindent();
7780
7781 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7782 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7783 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7784 editor
7785 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7786 .await;
7787
7788 editor.update_in(cx, |editor, window, cx| {
7789 editor.change_selections(None, window, cx, |s| {
7790 s.select_display_ranges([
7791 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7792 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7793 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7794 ])
7795 });
7796
7797 editor.handle_input("{", window, cx);
7798 editor.handle_input("{", window, cx);
7799 editor.handle_input("{", window, cx);
7800 assert_eq!(
7801 editor.text(cx),
7802 "
7803 {{{a}}}
7804 {{{b}}}
7805 {{{c}}}
7806 "
7807 .unindent()
7808 );
7809 assert_eq!(
7810 editor.selections.display_ranges(cx),
7811 [
7812 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7813 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7814 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7815 ]
7816 );
7817
7818 editor.undo(&Undo, window, cx);
7819 editor.undo(&Undo, window, cx);
7820 editor.undo(&Undo, window, cx);
7821 assert_eq!(
7822 editor.text(cx),
7823 "
7824 a
7825 b
7826 c
7827 "
7828 .unindent()
7829 );
7830 assert_eq!(
7831 editor.selections.display_ranges(cx),
7832 [
7833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7834 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7835 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7836 ]
7837 );
7838
7839 // Ensure inserting the first character of a multi-byte bracket pair
7840 // doesn't surround the selections with the bracket.
7841 editor.handle_input("/", window, cx);
7842 assert_eq!(
7843 editor.text(cx),
7844 "
7845 /
7846 /
7847 /
7848 "
7849 .unindent()
7850 );
7851 assert_eq!(
7852 editor.selections.display_ranges(cx),
7853 [
7854 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7855 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7856 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7857 ]
7858 );
7859
7860 editor.undo(&Undo, window, cx);
7861 assert_eq!(
7862 editor.text(cx),
7863 "
7864 a
7865 b
7866 c
7867 "
7868 .unindent()
7869 );
7870 assert_eq!(
7871 editor.selections.display_ranges(cx),
7872 [
7873 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7874 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7875 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7876 ]
7877 );
7878
7879 // Ensure inserting the last character of a multi-byte bracket pair
7880 // doesn't surround the selections with the bracket.
7881 editor.handle_input("*", window, cx);
7882 assert_eq!(
7883 editor.text(cx),
7884 "
7885 *
7886 *
7887 *
7888 "
7889 .unindent()
7890 );
7891 assert_eq!(
7892 editor.selections.display_ranges(cx),
7893 [
7894 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7895 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7896 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7897 ]
7898 );
7899 });
7900}
7901
7902#[gpui::test]
7903async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7904 init_test(cx, |_| {});
7905
7906 let language = Arc::new(Language::new(
7907 LanguageConfig {
7908 brackets: BracketPairConfig {
7909 pairs: vec![BracketPair {
7910 start: "{".to_string(),
7911 end: "}".to_string(),
7912 close: true,
7913 surround: true,
7914 newline: true,
7915 }],
7916 ..Default::default()
7917 },
7918 autoclose_before: "}".to_string(),
7919 ..Default::default()
7920 },
7921 Some(tree_sitter_rust::LANGUAGE.into()),
7922 ));
7923
7924 let text = r#"
7925 a
7926 b
7927 c
7928 "#
7929 .unindent();
7930
7931 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7932 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7933 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7934 editor
7935 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7936 .await;
7937
7938 editor.update_in(cx, |editor, window, cx| {
7939 editor.change_selections(None, window, cx, |s| {
7940 s.select_ranges([
7941 Point::new(0, 1)..Point::new(0, 1),
7942 Point::new(1, 1)..Point::new(1, 1),
7943 Point::new(2, 1)..Point::new(2, 1),
7944 ])
7945 });
7946
7947 editor.handle_input("{", window, cx);
7948 editor.handle_input("{", window, cx);
7949 editor.handle_input("_", window, cx);
7950 assert_eq!(
7951 editor.text(cx),
7952 "
7953 a{{_}}
7954 b{{_}}
7955 c{{_}}
7956 "
7957 .unindent()
7958 );
7959 assert_eq!(
7960 editor.selections.ranges::<Point>(cx),
7961 [
7962 Point::new(0, 4)..Point::new(0, 4),
7963 Point::new(1, 4)..Point::new(1, 4),
7964 Point::new(2, 4)..Point::new(2, 4)
7965 ]
7966 );
7967
7968 editor.backspace(&Default::default(), window, cx);
7969 editor.backspace(&Default::default(), window, cx);
7970 assert_eq!(
7971 editor.text(cx),
7972 "
7973 a{}
7974 b{}
7975 c{}
7976 "
7977 .unindent()
7978 );
7979 assert_eq!(
7980 editor.selections.ranges::<Point>(cx),
7981 [
7982 Point::new(0, 2)..Point::new(0, 2),
7983 Point::new(1, 2)..Point::new(1, 2),
7984 Point::new(2, 2)..Point::new(2, 2)
7985 ]
7986 );
7987
7988 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7989 assert_eq!(
7990 editor.text(cx),
7991 "
7992 a
7993 b
7994 c
7995 "
7996 .unindent()
7997 );
7998 assert_eq!(
7999 editor.selections.ranges::<Point>(cx),
8000 [
8001 Point::new(0, 1)..Point::new(0, 1),
8002 Point::new(1, 1)..Point::new(1, 1),
8003 Point::new(2, 1)..Point::new(2, 1)
8004 ]
8005 );
8006 });
8007}
8008
8009#[gpui::test]
8010async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8011 init_test(cx, |settings| {
8012 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8013 });
8014
8015 let mut cx = EditorTestContext::new(cx).await;
8016
8017 let language = Arc::new(Language::new(
8018 LanguageConfig {
8019 brackets: BracketPairConfig {
8020 pairs: vec![
8021 BracketPair {
8022 start: "{".to_string(),
8023 end: "}".to_string(),
8024 close: true,
8025 surround: true,
8026 newline: true,
8027 },
8028 BracketPair {
8029 start: "(".to_string(),
8030 end: ")".to_string(),
8031 close: true,
8032 surround: true,
8033 newline: true,
8034 },
8035 BracketPair {
8036 start: "[".to_string(),
8037 end: "]".to_string(),
8038 close: false,
8039 surround: true,
8040 newline: true,
8041 },
8042 ],
8043 ..Default::default()
8044 },
8045 autoclose_before: "})]".to_string(),
8046 ..Default::default()
8047 },
8048 Some(tree_sitter_rust::LANGUAGE.into()),
8049 ));
8050
8051 cx.language_registry().add(language.clone());
8052 cx.update_buffer(|buffer, cx| {
8053 buffer.set_language(Some(language), cx);
8054 });
8055
8056 cx.set_state(
8057 &"
8058 {(ˇ)}
8059 [[ˇ]]
8060 {(ˇ)}
8061 "
8062 .unindent(),
8063 );
8064
8065 cx.update_editor(|editor, window, cx| {
8066 editor.backspace(&Default::default(), window, cx);
8067 editor.backspace(&Default::default(), window, cx);
8068 });
8069
8070 cx.assert_editor_state(
8071 &"
8072 ˇ
8073 ˇ]]
8074 ˇ
8075 "
8076 .unindent(),
8077 );
8078
8079 cx.update_editor(|editor, window, cx| {
8080 editor.handle_input("{", window, cx);
8081 editor.handle_input("{", window, cx);
8082 editor.move_right(&MoveRight, window, cx);
8083 editor.move_right(&MoveRight, window, cx);
8084 editor.move_left(&MoveLeft, window, cx);
8085 editor.move_left(&MoveLeft, window, cx);
8086 editor.backspace(&Default::default(), window, cx);
8087 });
8088
8089 cx.assert_editor_state(
8090 &"
8091 {ˇ}
8092 {ˇ}]]
8093 {ˇ}
8094 "
8095 .unindent(),
8096 );
8097
8098 cx.update_editor(|editor, window, cx| {
8099 editor.backspace(&Default::default(), window, cx);
8100 });
8101
8102 cx.assert_editor_state(
8103 &"
8104 ˇ
8105 ˇ]]
8106 ˇ
8107 "
8108 .unindent(),
8109 );
8110}
8111
8112#[gpui::test]
8113async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8114 init_test(cx, |_| {});
8115
8116 let language = Arc::new(Language::new(
8117 LanguageConfig::default(),
8118 Some(tree_sitter_rust::LANGUAGE.into()),
8119 ));
8120
8121 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8122 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8123 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8124 editor
8125 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8126 .await;
8127
8128 editor.update_in(cx, |editor, window, cx| {
8129 editor.set_auto_replace_emoji_shortcode(true);
8130
8131 editor.handle_input("Hello ", window, cx);
8132 editor.handle_input(":wave", window, cx);
8133 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8134
8135 editor.handle_input(":", window, cx);
8136 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8137
8138 editor.handle_input(" :smile", window, cx);
8139 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8140
8141 editor.handle_input(":", window, cx);
8142 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8143
8144 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8145 editor.handle_input(":wave", window, cx);
8146 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8147
8148 editor.handle_input(":", window, cx);
8149 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8150
8151 editor.handle_input(":1", window, cx);
8152 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8153
8154 editor.handle_input(":", window, cx);
8155 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8156
8157 // Ensure shortcode does not get replaced when it is part of a word
8158 editor.handle_input(" Test:wave", window, cx);
8159 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8160
8161 editor.handle_input(":", window, cx);
8162 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8163
8164 editor.set_auto_replace_emoji_shortcode(false);
8165
8166 // Ensure shortcode does not get replaced when auto replace is off
8167 editor.handle_input(" :wave", window, cx);
8168 assert_eq!(
8169 editor.text(cx),
8170 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8171 );
8172
8173 editor.handle_input(":", window, cx);
8174 assert_eq!(
8175 editor.text(cx),
8176 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8177 );
8178 });
8179}
8180
8181#[gpui::test]
8182async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8183 init_test(cx, |_| {});
8184
8185 let (text, insertion_ranges) = marked_text_ranges(
8186 indoc! {"
8187 ˇ
8188 "},
8189 false,
8190 );
8191
8192 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8193 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8194
8195 _ = editor.update_in(cx, |editor, window, cx| {
8196 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8197
8198 editor
8199 .insert_snippet(&insertion_ranges, snippet, window, cx)
8200 .unwrap();
8201
8202 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8203 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8204 assert_eq!(editor.text(cx), expected_text);
8205 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8206 }
8207
8208 assert(
8209 editor,
8210 cx,
8211 indoc! {"
8212 type «» =•
8213 "},
8214 );
8215
8216 assert!(editor.context_menu_visible(), "There should be a matches");
8217 });
8218}
8219
8220#[gpui::test]
8221async fn test_snippets(cx: &mut TestAppContext) {
8222 init_test(cx, |_| {});
8223
8224 let (text, insertion_ranges) = marked_text_ranges(
8225 indoc! {"
8226 a.ˇ b
8227 a.ˇ b
8228 a.ˇ b
8229 "},
8230 false,
8231 );
8232
8233 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8234 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8235
8236 editor.update_in(cx, |editor, window, cx| {
8237 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8238
8239 editor
8240 .insert_snippet(&insertion_ranges, snippet, window, cx)
8241 .unwrap();
8242
8243 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8244 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8245 assert_eq!(editor.text(cx), expected_text);
8246 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8247 }
8248
8249 assert(
8250 editor,
8251 cx,
8252 indoc! {"
8253 a.f(«one», two, «three») b
8254 a.f(«one», two, «three») b
8255 a.f(«one», two, «three») b
8256 "},
8257 );
8258
8259 // Can't move earlier than the first tab stop
8260 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8261 assert(
8262 editor,
8263 cx,
8264 indoc! {"
8265 a.f(«one», two, «three») b
8266 a.f(«one», two, «three») b
8267 a.f(«one», two, «three») b
8268 "},
8269 );
8270
8271 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8272 assert(
8273 editor,
8274 cx,
8275 indoc! {"
8276 a.f(one, «two», three) b
8277 a.f(one, «two», three) b
8278 a.f(one, «two», three) b
8279 "},
8280 );
8281
8282 editor.move_to_prev_snippet_tabstop(window, cx);
8283 assert(
8284 editor,
8285 cx,
8286 indoc! {"
8287 a.f(«one», two, «three») b
8288 a.f(«one», two, «three») b
8289 a.f(«one», two, «three») b
8290 "},
8291 );
8292
8293 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8294 assert(
8295 editor,
8296 cx,
8297 indoc! {"
8298 a.f(one, «two», three) b
8299 a.f(one, «two», three) b
8300 a.f(one, «two», three) b
8301 "},
8302 );
8303 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8304 assert(
8305 editor,
8306 cx,
8307 indoc! {"
8308 a.f(one, two, three)ˇ b
8309 a.f(one, two, three)ˇ b
8310 a.f(one, two, three)ˇ b
8311 "},
8312 );
8313
8314 // As soon as the last tab stop is reached, snippet state is gone
8315 editor.move_to_prev_snippet_tabstop(window, cx);
8316 assert(
8317 editor,
8318 cx,
8319 indoc! {"
8320 a.f(one, two, three)ˇ b
8321 a.f(one, two, three)ˇ b
8322 a.f(one, two, three)ˇ b
8323 "},
8324 );
8325 });
8326}
8327
8328#[gpui::test]
8329async fn test_document_format_during_save(cx: &mut TestAppContext) {
8330 init_test(cx, |_| {});
8331
8332 let fs = FakeFs::new(cx.executor());
8333 fs.insert_file(path!("/file.rs"), Default::default()).await;
8334
8335 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8336
8337 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8338 language_registry.add(rust_lang());
8339 let mut fake_servers = language_registry.register_fake_lsp(
8340 "Rust",
8341 FakeLspAdapter {
8342 capabilities: lsp::ServerCapabilities {
8343 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8344 ..Default::default()
8345 },
8346 ..Default::default()
8347 },
8348 );
8349
8350 let buffer = project
8351 .update(cx, |project, cx| {
8352 project.open_local_buffer(path!("/file.rs"), cx)
8353 })
8354 .await
8355 .unwrap();
8356
8357 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8358 let (editor, cx) = cx.add_window_view(|window, cx| {
8359 build_editor_with_project(project.clone(), buffer, window, cx)
8360 });
8361 editor.update_in(cx, |editor, window, cx| {
8362 editor.set_text("one\ntwo\nthree\n", window, cx)
8363 });
8364 assert!(cx.read(|cx| editor.is_dirty(cx)));
8365
8366 cx.executor().start_waiting();
8367 let fake_server = fake_servers.next().await.unwrap();
8368
8369 {
8370 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8371 move |params, _| async move {
8372 assert_eq!(
8373 params.text_document.uri,
8374 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8375 );
8376 assert_eq!(params.options.tab_size, 4);
8377 Ok(Some(vec![lsp::TextEdit::new(
8378 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8379 ", ".to_string(),
8380 )]))
8381 },
8382 );
8383 let save = editor
8384 .update_in(cx, |editor, window, cx| {
8385 editor.save(true, project.clone(), window, cx)
8386 })
8387 .unwrap();
8388 cx.executor().start_waiting();
8389 save.await;
8390
8391 assert_eq!(
8392 editor.update(cx, |editor, cx| editor.text(cx)),
8393 "one, two\nthree\n"
8394 );
8395 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8396 }
8397
8398 {
8399 editor.update_in(cx, |editor, window, cx| {
8400 editor.set_text("one\ntwo\nthree\n", window, cx)
8401 });
8402 assert!(cx.read(|cx| editor.is_dirty(cx)));
8403
8404 // Ensure we can still save even if formatting hangs.
8405 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8406 move |params, _| async move {
8407 assert_eq!(
8408 params.text_document.uri,
8409 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8410 );
8411 futures::future::pending::<()>().await;
8412 unreachable!()
8413 },
8414 );
8415 let save = editor
8416 .update_in(cx, |editor, window, cx| {
8417 editor.save(true, project.clone(), window, cx)
8418 })
8419 .unwrap();
8420 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8421 cx.executor().start_waiting();
8422 save.await;
8423 assert_eq!(
8424 editor.update(cx, |editor, cx| editor.text(cx)),
8425 "one\ntwo\nthree\n"
8426 );
8427 }
8428
8429 // For non-dirty buffer, no formatting request should be sent
8430 {
8431 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8432
8433 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8434 panic!("Should not be invoked on non-dirty buffer");
8435 });
8436 let save = editor
8437 .update_in(cx, |editor, window, cx| {
8438 editor.save(true, project.clone(), window, cx)
8439 })
8440 .unwrap();
8441 cx.executor().start_waiting();
8442 save.await;
8443 }
8444
8445 // Set rust language override and assert overridden tabsize is sent to language server
8446 update_test_language_settings(cx, |settings| {
8447 settings.languages.insert(
8448 "Rust".into(),
8449 LanguageSettingsContent {
8450 tab_size: NonZeroU32::new(8),
8451 ..Default::default()
8452 },
8453 );
8454 });
8455
8456 {
8457 editor.update_in(cx, |editor, window, cx| {
8458 editor.set_text("somehting_new\n", window, cx)
8459 });
8460 assert!(cx.read(|cx| editor.is_dirty(cx)));
8461 let _formatting_request_signal = fake_server
8462 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8463 assert_eq!(
8464 params.text_document.uri,
8465 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8466 );
8467 assert_eq!(params.options.tab_size, 8);
8468 Ok(Some(vec![]))
8469 });
8470 let save = editor
8471 .update_in(cx, |editor, window, cx| {
8472 editor.save(true, project.clone(), window, cx)
8473 })
8474 .unwrap();
8475 cx.executor().start_waiting();
8476 save.await;
8477 }
8478}
8479
8480#[gpui::test]
8481async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8482 init_test(cx, |_| {});
8483
8484 let cols = 4;
8485 let rows = 10;
8486 let sample_text_1 = sample_text(rows, cols, 'a');
8487 assert_eq!(
8488 sample_text_1,
8489 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8490 );
8491 let sample_text_2 = sample_text(rows, cols, 'l');
8492 assert_eq!(
8493 sample_text_2,
8494 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8495 );
8496 let sample_text_3 = sample_text(rows, cols, 'v');
8497 assert_eq!(
8498 sample_text_3,
8499 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8500 );
8501
8502 let fs = FakeFs::new(cx.executor());
8503 fs.insert_tree(
8504 path!("/a"),
8505 json!({
8506 "main.rs": sample_text_1,
8507 "other.rs": sample_text_2,
8508 "lib.rs": sample_text_3,
8509 }),
8510 )
8511 .await;
8512
8513 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8514 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8515 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8516
8517 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8518 language_registry.add(rust_lang());
8519 let mut fake_servers = language_registry.register_fake_lsp(
8520 "Rust",
8521 FakeLspAdapter {
8522 capabilities: lsp::ServerCapabilities {
8523 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8524 ..Default::default()
8525 },
8526 ..Default::default()
8527 },
8528 );
8529
8530 let worktree = project.update(cx, |project, cx| {
8531 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8532 assert_eq!(worktrees.len(), 1);
8533 worktrees.pop().unwrap()
8534 });
8535 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8536
8537 let buffer_1 = project
8538 .update(cx, |project, cx| {
8539 project.open_buffer((worktree_id, "main.rs"), cx)
8540 })
8541 .await
8542 .unwrap();
8543 let buffer_2 = project
8544 .update(cx, |project, cx| {
8545 project.open_buffer((worktree_id, "other.rs"), cx)
8546 })
8547 .await
8548 .unwrap();
8549 let buffer_3 = project
8550 .update(cx, |project, cx| {
8551 project.open_buffer((worktree_id, "lib.rs"), cx)
8552 })
8553 .await
8554 .unwrap();
8555
8556 let multi_buffer = cx.new(|cx| {
8557 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8558 multi_buffer.push_excerpts(
8559 buffer_1.clone(),
8560 [
8561 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8562 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8563 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8564 ],
8565 cx,
8566 );
8567 multi_buffer.push_excerpts(
8568 buffer_2.clone(),
8569 [
8570 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8571 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8572 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8573 ],
8574 cx,
8575 );
8576 multi_buffer.push_excerpts(
8577 buffer_3.clone(),
8578 [
8579 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8580 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8581 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8582 ],
8583 cx,
8584 );
8585 multi_buffer
8586 });
8587 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8588 Editor::new(
8589 EditorMode::full(),
8590 multi_buffer,
8591 Some(project.clone()),
8592 window,
8593 cx,
8594 )
8595 });
8596
8597 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8598 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8599 s.select_ranges(Some(1..2))
8600 });
8601 editor.insert("|one|two|three|", window, cx);
8602 });
8603 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8604 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8605 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8606 s.select_ranges(Some(60..70))
8607 });
8608 editor.insert("|four|five|six|", window, cx);
8609 });
8610 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8611
8612 // First two buffers should be edited, but not the third one.
8613 assert_eq!(
8614 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8615 "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}",
8616 );
8617 buffer_1.update(cx, |buffer, _| {
8618 assert!(buffer.is_dirty());
8619 assert_eq!(
8620 buffer.text(),
8621 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8622 )
8623 });
8624 buffer_2.update(cx, |buffer, _| {
8625 assert!(buffer.is_dirty());
8626 assert_eq!(
8627 buffer.text(),
8628 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8629 )
8630 });
8631 buffer_3.update(cx, |buffer, _| {
8632 assert!(!buffer.is_dirty());
8633 assert_eq!(buffer.text(), sample_text_3,)
8634 });
8635 cx.executor().run_until_parked();
8636
8637 cx.executor().start_waiting();
8638 let save = multi_buffer_editor
8639 .update_in(cx, |editor, window, cx| {
8640 editor.save(true, project.clone(), window, cx)
8641 })
8642 .unwrap();
8643
8644 let fake_server = fake_servers.next().await.unwrap();
8645 fake_server
8646 .server
8647 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8648 Ok(Some(vec![lsp::TextEdit::new(
8649 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8650 format!("[{} formatted]", params.text_document.uri),
8651 )]))
8652 })
8653 .detach();
8654 save.await;
8655
8656 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8657 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8658 assert_eq!(
8659 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8660 uri!(
8661 "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}"
8662 ),
8663 );
8664 buffer_1.update(cx, |buffer, _| {
8665 assert!(!buffer.is_dirty());
8666 assert_eq!(
8667 buffer.text(),
8668 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8669 )
8670 });
8671 buffer_2.update(cx, |buffer, _| {
8672 assert!(!buffer.is_dirty());
8673 assert_eq!(
8674 buffer.text(),
8675 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8676 )
8677 });
8678 buffer_3.update(cx, |buffer, _| {
8679 assert!(!buffer.is_dirty());
8680 assert_eq!(buffer.text(), sample_text_3,)
8681 });
8682}
8683
8684#[gpui::test]
8685async fn test_range_format_during_save(cx: &mut TestAppContext) {
8686 init_test(cx, |_| {});
8687
8688 let fs = FakeFs::new(cx.executor());
8689 fs.insert_file(path!("/file.rs"), Default::default()).await;
8690
8691 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8692
8693 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8694 language_registry.add(rust_lang());
8695 let mut fake_servers = language_registry.register_fake_lsp(
8696 "Rust",
8697 FakeLspAdapter {
8698 capabilities: lsp::ServerCapabilities {
8699 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8700 ..Default::default()
8701 },
8702 ..Default::default()
8703 },
8704 );
8705
8706 let buffer = project
8707 .update(cx, |project, cx| {
8708 project.open_local_buffer(path!("/file.rs"), cx)
8709 })
8710 .await
8711 .unwrap();
8712
8713 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8714 let (editor, cx) = cx.add_window_view(|window, cx| {
8715 build_editor_with_project(project.clone(), buffer, window, cx)
8716 });
8717 editor.update_in(cx, |editor, window, cx| {
8718 editor.set_text("one\ntwo\nthree\n", window, cx)
8719 });
8720 assert!(cx.read(|cx| editor.is_dirty(cx)));
8721
8722 cx.executor().start_waiting();
8723 let fake_server = fake_servers.next().await.unwrap();
8724
8725 let save = editor
8726 .update_in(cx, |editor, window, cx| {
8727 editor.save(true, project.clone(), window, cx)
8728 })
8729 .unwrap();
8730 fake_server
8731 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8732 assert_eq!(
8733 params.text_document.uri,
8734 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8735 );
8736 assert_eq!(params.options.tab_size, 4);
8737 Ok(Some(vec![lsp::TextEdit::new(
8738 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8739 ", ".to_string(),
8740 )]))
8741 })
8742 .next()
8743 .await;
8744 cx.executor().start_waiting();
8745 save.await;
8746 assert_eq!(
8747 editor.update(cx, |editor, cx| editor.text(cx)),
8748 "one, two\nthree\n"
8749 );
8750 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8751
8752 editor.update_in(cx, |editor, window, cx| {
8753 editor.set_text("one\ntwo\nthree\n", window, cx)
8754 });
8755 assert!(cx.read(|cx| editor.is_dirty(cx)));
8756
8757 // Ensure we can still save even if formatting hangs.
8758 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8759 move |params, _| async move {
8760 assert_eq!(
8761 params.text_document.uri,
8762 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8763 );
8764 futures::future::pending::<()>().await;
8765 unreachable!()
8766 },
8767 );
8768 let save = editor
8769 .update_in(cx, |editor, window, cx| {
8770 editor.save(true, project.clone(), window, cx)
8771 })
8772 .unwrap();
8773 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8774 cx.executor().start_waiting();
8775 save.await;
8776 assert_eq!(
8777 editor.update(cx, |editor, cx| editor.text(cx)),
8778 "one\ntwo\nthree\n"
8779 );
8780 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8781
8782 // For non-dirty buffer, no formatting request should be sent
8783 let save = editor
8784 .update_in(cx, |editor, window, cx| {
8785 editor.save(true, project.clone(), window, cx)
8786 })
8787 .unwrap();
8788 let _pending_format_request = fake_server
8789 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8790 panic!("Should not be invoked on non-dirty buffer");
8791 })
8792 .next();
8793 cx.executor().start_waiting();
8794 save.await;
8795
8796 // Set Rust language override and assert overridden tabsize is sent to language server
8797 update_test_language_settings(cx, |settings| {
8798 settings.languages.insert(
8799 "Rust".into(),
8800 LanguageSettingsContent {
8801 tab_size: NonZeroU32::new(8),
8802 ..Default::default()
8803 },
8804 );
8805 });
8806
8807 editor.update_in(cx, |editor, window, cx| {
8808 editor.set_text("somehting_new\n", window, cx)
8809 });
8810 assert!(cx.read(|cx| editor.is_dirty(cx)));
8811 let save = editor
8812 .update_in(cx, |editor, window, cx| {
8813 editor.save(true, project.clone(), window, cx)
8814 })
8815 .unwrap();
8816 fake_server
8817 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8818 assert_eq!(
8819 params.text_document.uri,
8820 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8821 );
8822 assert_eq!(params.options.tab_size, 8);
8823 Ok(Some(vec![]))
8824 })
8825 .next()
8826 .await;
8827 cx.executor().start_waiting();
8828 save.await;
8829}
8830
8831#[gpui::test]
8832async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8833 init_test(cx, |settings| {
8834 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8835 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8836 ))
8837 });
8838
8839 let fs = FakeFs::new(cx.executor());
8840 fs.insert_file(path!("/file.rs"), Default::default()).await;
8841
8842 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8843
8844 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8845 language_registry.add(Arc::new(Language::new(
8846 LanguageConfig {
8847 name: "Rust".into(),
8848 matcher: LanguageMatcher {
8849 path_suffixes: vec!["rs".to_string()],
8850 ..Default::default()
8851 },
8852 ..LanguageConfig::default()
8853 },
8854 Some(tree_sitter_rust::LANGUAGE.into()),
8855 )));
8856 update_test_language_settings(cx, |settings| {
8857 // Enable Prettier formatting for the same buffer, and ensure
8858 // LSP is called instead of Prettier.
8859 settings.defaults.prettier = Some(PrettierSettings {
8860 allowed: true,
8861 ..PrettierSettings::default()
8862 });
8863 });
8864 let mut fake_servers = language_registry.register_fake_lsp(
8865 "Rust",
8866 FakeLspAdapter {
8867 capabilities: lsp::ServerCapabilities {
8868 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8869 ..Default::default()
8870 },
8871 ..Default::default()
8872 },
8873 );
8874
8875 let buffer = project
8876 .update(cx, |project, cx| {
8877 project.open_local_buffer(path!("/file.rs"), cx)
8878 })
8879 .await
8880 .unwrap();
8881
8882 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8883 let (editor, cx) = cx.add_window_view(|window, cx| {
8884 build_editor_with_project(project.clone(), buffer, window, cx)
8885 });
8886 editor.update_in(cx, |editor, window, cx| {
8887 editor.set_text("one\ntwo\nthree\n", window, cx)
8888 });
8889
8890 cx.executor().start_waiting();
8891 let fake_server = fake_servers.next().await.unwrap();
8892
8893 let format = editor
8894 .update_in(cx, |editor, window, cx| {
8895 editor.perform_format(
8896 project.clone(),
8897 FormatTrigger::Manual,
8898 FormatTarget::Buffers,
8899 window,
8900 cx,
8901 )
8902 })
8903 .unwrap();
8904 fake_server
8905 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8906 assert_eq!(
8907 params.text_document.uri,
8908 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8909 );
8910 assert_eq!(params.options.tab_size, 4);
8911 Ok(Some(vec![lsp::TextEdit::new(
8912 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8913 ", ".to_string(),
8914 )]))
8915 })
8916 .next()
8917 .await;
8918 cx.executor().start_waiting();
8919 format.await;
8920 assert_eq!(
8921 editor.update(cx, |editor, cx| editor.text(cx)),
8922 "one, two\nthree\n"
8923 );
8924
8925 editor.update_in(cx, |editor, window, cx| {
8926 editor.set_text("one\ntwo\nthree\n", window, cx)
8927 });
8928 // Ensure we don't lock if formatting hangs.
8929 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8930 move |params, _| async move {
8931 assert_eq!(
8932 params.text_document.uri,
8933 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8934 );
8935 futures::future::pending::<()>().await;
8936 unreachable!()
8937 },
8938 );
8939 let format = editor
8940 .update_in(cx, |editor, window, cx| {
8941 editor.perform_format(
8942 project,
8943 FormatTrigger::Manual,
8944 FormatTarget::Buffers,
8945 window,
8946 cx,
8947 )
8948 })
8949 .unwrap();
8950 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8951 cx.executor().start_waiting();
8952 format.await;
8953 assert_eq!(
8954 editor.update(cx, |editor, cx| editor.text(cx)),
8955 "one\ntwo\nthree\n"
8956 );
8957}
8958
8959#[gpui::test]
8960async fn test_multiple_formatters(cx: &mut TestAppContext) {
8961 init_test(cx, |settings| {
8962 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8963 settings.defaults.formatter =
8964 Some(language_settings::SelectedFormatter::List(FormatterList(
8965 vec![
8966 Formatter::LanguageServer { name: None },
8967 Formatter::CodeActions(
8968 [
8969 ("code-action-1".into(), true),
8970 ("code-action-2".into(), true),
8971 ]
8972 .into_iter()
8973 .collect(),
8974 ),
8975 ]
8976 .into(),
8977 )))
8978 });
8979
8980 let fs = FakeFs::new(cx.executor());
8981 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8982 .await;
8983
8984 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8985 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8986 language_registry.add(rust_lang());
8987
8988 let mut fake_servers = language_registry.register_fake_lsp(
8989 "Rust",
8990 FakeLspAdapter {
8991 capabilities: lsp::ServerCapabilities {
8992 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8993 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8994 commands: vec!["the-command-for-code-action-1".into()],
8995 ..Default::default()
8996 }),
8997 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8998 ..Default::default()
8999 },
9000 ..Default::default()
9001 },
9002 );
9003
9004 let buffer = project
9005 .update(cx, |project, cx| {
9006 project.open_local_buffer(path!("/file.rs"), cx)
9007 })
9008 .await
9009 .unwrap();
9010
9011 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9012 let (editor, cx) = cx.add_window_view(|window, cx| {
9013 build_editor_with_project(project.clone(), buffer, window, cx)
9014 });
9015
9016 cx.executor().start_waiting();
9017
9018 let fake_server = fake_servers.next().await.unwrap();
9019 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9020 move |_params, _| async move {
9021 Ok(Some(vec![lsp::TextEdit::new(
9022 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9023 "applied-formatting\n".to_string(),
9024 )]))
9025 },
9026 );
9027 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9028 move |params, _| async move {
9029 assert_eq!(
9030 params.context.only,
9031 Some(vec!["code-action-1".into(), "code-action-2".into()])
9032 );
9033 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9034 Ok(Some(vec![
9035 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9036 kind: Some("code-action-1".into()),
9037 edit: Some(lsp::WorkspaceEdit::new(
9038 [(
9039 uri.clone(),
9040 vec![lsp::TextEdit::new(
9041 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9042 "applied-code-action-1-edit\n".to_string(),
9043 )],
9044 )]
9045 .into_iter()
9046 .collect(),
9047 )),
9048 command: Some(lsp::Command {
9049 command: "the-command-for-code-action-1".into(),
9050 ..Default::default()
9051 }),
9052 ..Default::default()
9053 }),
9054 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9055 kind: Some("code-action-2".into()),
9056 edit: Some(lsp::WorkspaceEdit::new(
9057 [(
9058 uri.clone(),
9059 vec![lsp::TextEdit::new(
9060 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9061 "applied-code-action-2-edit\n".to_string(),
9062 )],
9063 )]
9064 .into_iter()
9065 .collect(),
9066 )),
9067 ..Default::default()
9068 }),
9069 ]))
9070 },
9071 );
9072
9073 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9074 move |params, _| async move { Ok(params) }
9075 });
9076
9077 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9078 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9079 let fake = fake_server.clone();
9080 let lock = command_lock.clone();
9081 move |params, _| {
9082 assert_eq!(params.command, "the-command-for-code-action-1");
9083 let fake = fake.clone();
9084 let lock = lock.clone();
9085 async move {
9086 lock.lock().await;
9087 fake.server
9088 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9089 label: None,
9090 edit: lsp::WorkspaceEdit {
9091 changes: Some(
9092 [(
9093 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9094 vec![lsp::TextEdit {
9095 range: lsp::Range::new(
9096 lsp::Position::new(0, 0),
9097 lsp::Position::new(0, 0),
9098 ),
9099 new_text: "applied-code-action-1-command\n".into(),
9100 }],
9101 )]
9102 .into_iter()
9103 .collect(),
9104 ),
9105 ..Default::default()
9106 },
9107 })
9108 .await
9109 .into_response()
9110 .unwrap();
9111 Ok(Some(json!(null)))
9112 }
9113 }
9114 });
9115
9116 cx.executor().start_waiting();
9117 editor
9118 .update_in(cx, |editor, window, cx| {
9119 editor.perform_format(
9120 project.clone(),
9121 FormatTrigger::Manual,
9122 FormatTarget::Buffers,
9123 window,
9124 cx,
9125 )
9126 })
9127 .unwrap()
9128 .await;
9129 editor.update(cx, |editor, cx| {
9130 assert_eq!(
9131 editor.text(cx),
9132 r#"
9133 applied-code-action-2-edit
9134 applied-code-action-1-command
9135 applied-code-action-1-edit
9136 applied-formatting
9137 one
9138 two
9139 three
9140 "#
9141 .unindent()
9142 );
9143 });
9144
9145 editor.update_in(cx, |editor, window, cx| {
9146 editor.undo(&Default::default(), window, cx);
9147 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9148 });
9149
9150 // Perform a manual edit while waiting for an LSP command
9151 // that's being run as part of a formatting code action.
9152 let lock_guard = command_lock.lock().await;
9153 let format = editor
9154 .update_in(cx, |editor, window, cx| {
9155 editor.perform_format(
9156 project.clone(),
9157 FormatTrigger::Manual,
9158 FormatTarget::Buffers,
9159 window,
9160 cx,
9161 )
9162 })
9163 .unwrap();
9164 cx.run_until_parked();
9165 editor.update(cx, |editor, cx| {
9166 assert_eq!(
9167 editor.text(cx),
9168 r#"
9169 applied-code-action-1-edit
9170 applied-formatting
9171 one
9172 two
9173 three
9174 "#
9175 .unindent()
9176 );
9177
9178 editor.buffer.update(cx, |buffer, cx| {
9179 let ix = buffer.len(cx);
9180 buffer.edit([(ix..ix, "edited\n")], None, cx);
9181 });
9182 });
9183
9184 // Allow the LSP command to proceed. Because the buffer was edited,
9185 // the second code action will not be run.
9186 drop(lock_guard);
9187 format.await;
9188 editor.update_in(cx, |editor, window, cx| {
9189 assert_eq!(
9190 editor.text(cx),
9191 r#"
9192 applied-code-action-1-command
9193 applied-code-action-1-edit
9194 applied-formatting
9195 one
9196 two
9197 three
9198 edited
9199 "#
9200 .unindent()
9201 );
9202
9203 // The manual edit is undone first, because it is the last thing the user did
9204 // (even though the command completed afterwards).
9205 editor.undo(&Default::default(), window, cx);
9206 assert_eq!(
9207 editor.text(cx),
9208 r#"
9209 applied-code-action-1-command
9210 applied-code-action-1-edit
9211 applied-formatting
9212 one
9213 two
9214 three
9215 "#
9216 .unindent()
9217 );
9218
9219 // All the formatting (including the command, which completed after the manual edit)
9220 // is undone together.
9221 editor.undo(&Default::default(), window, cx);
9222 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9223 });
9224}
9225
9226#[gpui::test]
9227async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9228 init_test(cx, |settings| {
9229 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9230 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9231 ))
9232 });
9233
9234 let fs = FakeFs::new(cx.executor());
9235 fs.insert_file(path!("/file.ts"), Default::default()).await;
9236
9237 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9238
9239 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9240 language_registry.add(Arc::new(Language::new(
9241 LanguageConfig {
9242 name: "TypeScript".into(),
9243 matcher: LanguageMatcher {
9244 path_suffixes: vec!["ts".to_string()],
9245 ..Default::default()
9246 },
9247 ..LanguageConfig::default()
9248 },
9249 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9250 )));
9251 update_test_language_settings(cx, |settings| {
9252 settings.defaults.prettier = Some(PrettierSettings {
9253 allowed: true,
9254 ..PrettierSettings::default()
9255 });
9256 });
9257 let mut fake_servers = language_registry.register_fake_lsp(
9258 "TypeScript",
9259 FakeLspAdapter {
9260 capabilities: lsp::ServerCapabilities {
9261 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9262 ..Default::default()
9263 },
9264 ..Default::default()
9265 },
9266 );
9267
9268 let buffer = project
9269 .update(cx, |project, cx| {
9270 project.open_local_buffer(path!("/file.ts"), cx)
9271 })
9272 .await
9273 .unwrap();
9274
9275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9276 let (editor, cx) = cx.add_window_view(|window, cx| {
9277 build_editor_with_project(project.clone(), buffer, window, cx)
9278 });
9279 editor.update_in(cx, |editor, window, cx| {
9280 editor.set_text(
9281 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9282 window,
9283 cx,
9284 )
9285 });
9286
9287 cx.executor().start_waiting();
9288 let fake_server = fake_servers.next().await.unwrap();
9289
9290 let format = editor
9291 .update_in(cx, |editor, window, cx| {
9292 editor.perform_code_action_kind(
9293 project.clone(),
9294 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9295 window,
9296 cx,
9297 )
9298 })
9299 .unwrap();
9300 fake_server
9301 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9302 assert_eq!(
9303 params.text_document.uri,
9304 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9305 );
9306 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9307 lsp::CodeAction {
9308 title: "Organize Imports".to_string(),
9309 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9310 edit: Some(lsp::WorkspaceEdit {
9311 changes: Some(
9312 [(
9313 params.text_document.uri.clone(),
9314 vec![lsp::TextEdit::new(
9315 lsp::Range::new(
9316 lsp::Position::new(1, 0),
9317 lsp::Position::new(2, 0),
9318 ),
9319 "".to_string(),
9320 )],
9321 )]
9322 .into_iter()
9323 .collect(),
9324 ),
9325 ..Default::default()
9326 }),
9327 ..Default::default()
9328 },
9329 )]))
9330 })
9331 .next()
9332 .await;
9333 cx.executor().start_waiting();
9334 format.await;
9335 assert_eq!(
9336 editor.update(cx, |editor, cx| editor.text(cx)),
9337 "import { a } from 'module';\n\nconst x = a;\n"
9338 );
9339
9340 editor.update_in(cx, |editor, window, cx| {
9341 editor.set_text(
9342 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9343 window,
9344 cx,
9345 )
9346 });
9347 // Ensure we don't lock if code action hangs.
9348 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9349 move |params, _| async move {
9350 assert_eq!(
9351 params.text_document.uri,
9352 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9353 );
9354 futures::future::pending::<()>().await;
9355 unreachable!()
9356 },
9357 );
9358 let format = editor
9359 .update_in(cx, |editor, window, cx| {
9360 editor.perform_code_action_kind(
9361 project,
9362 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9363 window,
9364 cx,
9365 )
9366 })
9367 .unwrap();
9368 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9369 cx.executor().start_waiting();
9370 format.await;
9371 assert_eq!(
9372 editor.update(cx, |editor, cx| editor.text(cx)),
9373 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9374 );
9375}
9376
9377#[gpui::test]
9378async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9379 init_test(cx, |_| {});
9380
9381 let mut cx = EditorLspTestContext::new_rust(
9382 lsp::ServerCapabilities {
9383 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9384 ..Default::default()
9385 },
9386 cx,
9387 )
9388 .await;
9389
9390 cx.set_state(indoc! {"
9391 one.twoˇ
9392 "});
9393
9394 // The format request takes a long time. When it completes, it inserts
9395 // a newline and an indent before the `.`
9396 cx.lsp
9397 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9398 let executor = cx.background_executor().clone();
9399 async move {
9400 executor.timer(Duration::from_millis(100)).await;
9401 Ok(Some(vec![lsp::TextEdit {
9402 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9403 new_text: "\n ".into(),
9404 }]))
9405 }
9406 });
9407
9408 // Submit a format request.
9409 let format_1 = cx
9410 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9411 .unwrap();
9412 cx.executor().run_until_parked();
9413
9414 // Submit a second format request.
9415 let format_2 = cx
9416 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9417 .unwrap();
9418 cx.executor().run_until_parked();
9419
9420 // Wait for both format requests to complete
9421 cx.executor().advance_clock(Duration::from_millis(200));
9422 cx.executor().start_waiting();
9423 format_1.await.unwrap();
9424 cx.executor().start_waiting();
9425 format_2.await.unwrap();
9426
9427 // The formatting edits only happens once.
9428 cx.assert_editor_state(indoc! {"
9429 one
9430 .twoˇ
9431 "});
9432}
9433
9434#[gpui::test]
9435async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9436 init_test(cx, |settings| {
9437 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9438 });
9439
9440 let mut cx = EditorLspTestContext::new_rust(
9441 lsp::ServerCapabilities {
9442 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9443 ..Default::default()
9444 },
9445 cx,
9446 )
9447 .await;
9448
9449 // Set up a buffer white some trailing whitespace and no trailing newline.
9450 cx.set_state(
9451 &[
9452 "one ", //
9453 "twoˇ", //
9454 "three ", //
9455 "four", //
9456 ]
9457 .join("\n"),
9458 );
9459
9460 // Submit a format request.
9461 let format = cx
9462 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9463 .unwrap();
9464
9465 // Record which buffer changes have been sent to the language server
9466 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9467 cx.lsp
9468 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9469 let buffer_changes = buffer_changes.clone();
9470 move |params, _| {
9471 buffer_changes.lock().extend(
9472 params
9473 .content_changes
9474 .into_iter()
9475 .map(|e| (e.range.unwrap(), e.text)),
9476 );
9477 }
9478 });
9479
9480 // Handle formatting requests to the language server.
9481 cx.lsp
9482 .set_request_handler::<lsp::request::Formatting, _, _>({
9483 let buffer_changes = buffer_changes.clone();
9484 move |_, _| {
9485 // When formatting is requested, trailing whitespace has already been stripped,
9486 // and the trailing newline has already been added.
9487 assert_eq!(
9488 &buffer_changes.lock()[1..],
9489 &[
9490 (
9491 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9492 "".into()
9493 ),
9494 (
9495 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9496 "".into()
9497 ),
9498 (
9499 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9500 "\n".into()
9501 ),
9502 ]
9503 );
9504
9505 // Insert blank lines between each line of the buffer.
9506 async move {
9507 Ok(Some(vec![
9508 lsp::TextEdit {
9509 range: lsp::Range::new(
9510 lsp::Position::new(1, 0),
9511 lsp::Position::new(1, 0),
9512 ),
9513 new_text: "\n".into(),
9514 },
9515 lsp::TextEdit {
9516 range: lsp::Range::new(
9517 lsp::Position::new(2, 0),
9518 lsp::Position::new(2, 0),
9519 ),
9520 new_text: "\n".into(),
9521 },
9522 ]))
9523 }
9524 }
9525 });
9526
9527 // After formatting the buffer, the trailing whitespace is stripped,
9528 // a newline is appended, and the edits provided by the language server
9529 // have been applied.
9530 format.await.unwrap();
9531 cx.assert_editor_state(
9532 &[
9533 "one", //
9534 "", //
9535 "twoˇ", //
9536 "", //
9537 "three", //
9538 "four", //
9539 "", //
9540 ]
9541 .join("\n"),
9542 );
9543
9544 // Undoing the formatting undoes the trailing whitespace removal, the
9545 // trailing newline, and the LSP edits.
9546 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9547 cx.assert_editor_state(
9548 &[
9549 "one ", //
9550 "twoˇ", //
9551 "three ", //
9552 "four", //
9553 ]
9554 .join("\n"),
9555 );
9556}
9557
9558#[gpui::test]
9559async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9560 cx: &mut TestAppContext,
9561) {
9562 init_test(cx, |_| {});
9563
9564 cx.update(|cx| {
9565 cx.update_global::<SettingsStore, _>(|settings, cx| {
9566 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9567 settings.auto_signature_help = Some(true);
9568 });
9569 });
9570 });
9571
9572 let mut cx = EditorLspTestContext::new_rust(
9573 lsp::ServerCapabilities {
9574 signature_help_provider: Some(lsp::SignatureHelpOptions {
9575 ..Default::default()
9576 }),
9577 ..Default::default()
9578 },
9579 cx,
9580 )
9581 .await;
9582
9583 let language = Language::new(
9584 LanguageConfig {
9585 name: "Rust".into(),
9586 brackets: BracketPairConfig {
9587 pairs: vec![
9588 BracketPair {
9589 start: "{".to_string(),
9590 end: "}".to_string(),
9591 close: true,
9592 surround: true,
9593 newline: true,
9594 },
9595 BracketPair {
9596 start: "(".to_string(),
9597 end: ")".to_string(),
9598 close: true,
9599 surround: true,
9600 newline: true,
9601 },
9602 BracketPair {
9603 start: "/*".to_string(),
9604 end: " */".to_string(),
9605 close: true,
9606 surround: true,
9607 newline: true,
9608 },
9609 BracketPair {
9610 start: "[".to_string(),
9611 end: "]".to_string(),
9612 close: false,
9613 surround: false,
9614 newline: true,
9615 },
9616 BracketPair {
9617 start: "\"".to_string(),
9618 end: "\"".to_string(),
9619 close: true,
9620 surround: true,
9621 newline: false,
9622 },
9623 BracketPair {
9624 start: "<".to_string(),
9625 end: ">".to_string(),
9626 close: false,
9627 surround: true,
9628 newline: true,
9629 },
9630 ],
9631 ..Default::default()
9632 },
9633 autoclose_before: "})]".to_string(),
9634 ..Default::default()
9635 },
9636 Some(tree_sitter_rust::LANGUAGE.into()),
9637 );
9638 let language = Arc::new(language);
9639
9640 cx.language_registry().add(language.clone());
9641 cx.update_buffer(|buffer, cx| {
9642 buffer.set_language(Some(language), cx);
9643 });
9644
9645 cx.set_state(
9646 &r#"
9647 fn main() {
9648 sampleˇ
9649 }
9650 "#
9651 .unindent(),
9652 );
9653
9654 cx.update_editor(|editor, window, cx| {
9655 editor.handle_input("(", window, cx);
9656 });
9657 cx.assert_editor_state(
9658 &"
9659 fn main() {
9660 sample(ˇ)
9661 }
9662 "
9663 .unindent(),
9664 );
9665
9666 let mocked_response = lsp::SignatureHelp {
9667 signatures: vec![lsp::SignatureInformation {
9668 label: "fn sample(param1: u8, param2: u8)".to_string(),
9669 documentation: None,
9670 parameters: Some(vec![
9671 lsp::ParameterInformation {
9672 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9673 documentation: None,
9674 },
9675 lsp::ParameterInformation {
9676 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9677 documentation: None,
9678 },
9679 ]),
9680 active_parameter: None,
9681 }],
9682 active_signature: Some(0),
9683 active_parameter: Some(0),
9684 };
9685 handle_signature_help_request(&mut cx, mocked_response).await;
9686
9687 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9688 .await;
9689
9690 cx.editor(|editor, _, _| {
9691 let signature_help_state = editor.signature_help_state.popover().cloned();
9692 assert_eq!(
9693 signature_help_state.unwrap().label,
9694 "param1: u8, param2: u8"
9695 );
9696 });
9697}
9698
9699#[gpui::test]
9700async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9701 init_test(cx, |_| {});
9702
9703 cx.update(|cx| {
9704 cx.update_global::<SettingsStore, _>(|settings, cx| {
9705 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9706 settings.auto_signature_help = Some(false);
9707 settings.show_signature_help_after_edits = Some(false);
9708 });
9709 });
9710 });
9711
9712 let mut cx = EditorLspTestContext::new_rust(
9713 lsp::ServerCapabilities {
9714 signature_help_provider: Some(lsp::SignatureHelpOptions {
9715 ..Default::default()
9716 }),
9717 ..Default::default()
9718 },
9719 cx,
9720 )
9721 .await;
9722
9723 let language = Language::new(
9724 LanguageConfig {
9725 name: "Rust".into(),
9726 brackets: BracketPairConfig {
9727 pairs: vec![
9728 BracketPair {
9729 start: "{".to_string(),
9730 end: "}".to_string(),
9731 close: true,
9732 surround: true,
9733 newline: true,
9734 },
9735 BracketPair {
9736 start: "(".to_string(),
9737 end: ")".to_string(),
9738 close: true,
9739 surround: true,
9740 newline: true,
9741 },
9742 BracketPair {
9743 start: "/*".to_string(),
9744 end: " */".to_string(),
9745 close: true,
9746 surround: true,
9747 newline: true,
9748 },
9749 BracketPair {
9750 start: "[".to_string(),
9751 end: "]".to_string(),
9752 close: false,
9753 surround: false,
9754 newline: true,
9755 },
9756 BracketPair {
9757 start: "\"".to_string(),
9758 end: "\"".to_string(),
9759 close: true,
9760 surround: true,
9761 newline: false,
9762 },
9763 BracketPair {
9764 start: "<".to_string(),
9765 end: ">".to_string(),
9766 close: false,
9767 surround: true,
9768 newline: true,
9769 },
9770 ],
9771 ..Default::default()
9772 },
9773 autoclose_before: "})]".to_string(),
9774 ..Default::default()
9775 },
9776 Some(tree_sitter_rust::LANGUAGE.into()),
9777 );
9778 let language = Arc::new(language);
9779
9780 cx.language_registry().add(language.clone());
9781 cx.update_buffer(|buffer, cx| {
9782 buffer.set_language(Some(language), cx);
9783 });
9784
9785 // Ensure that signature_help is not called when no signature help is enabled.
9786 cx.set_state(
9787 &r#"
9788 fn main() {
9789 sampleˇ
9790 }
9791 "#
9792 .unindent(),
9793 );
9794 cx.update_editor(|editor, window, cx| {
9795 editor.handle_input("(", window, cx);
9796 });
9797 cx.assert_editor_state(
9798 &"
9799 fn main() {
9800 sample(ˇ)
9801 }
9802 "
9803 .unindent(),
9804 );
9805 cx.editor(|editor, _, _| {
9806 assert!(editor.signature_help_state.task().is_none());
9807 });
9808
9809 let mocked_response = lsp::SignatureHelp {
9810 signatures: vec![lsp::SignatureInformation {
9811 label: "fn sample(param1: u8, param2: u8)".to_string(),
9812 documentation: None,
9813 parameters: Some(vec![
9814 lsp::ParameterInformation {
9815 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9816 documentation: None,
9817 },
9818 lsp::ParameterInformation {
9819 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9820 documentation: None,
9821 },
9822 ]),
9823 active_parameter: None,
9824 }],
9825 active_signature: Some(0),
9826 active_parameter: Some(0),
9827 };
9828
9829 // Ensure that signature_help is called when enabled afte edits
9830 cx.update(|_, cx| {
9831 cx.update_global::<SettingsStore, _>(|settings, cx| {
9832 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9833 settings.auto_signature_help = Some(false);
9834 settings.show_signature_help_after_edits = Some(true);
9835 });
9836 });
9837 });
9838 cx.set_state(
9839 &r#"
9840 fn main() {
9841 sampleˇ
9842 }
9843 "#
9844 .unindent(),
9845 );
9846 cx.update_editor(|editor, window, cx| {
9847 editor.handle_input("(", window, cx);
9848 });
9849 cx.assert_editor_state(
9850 &"
9851 fn main() {
9852 sample(ˇ)
9853 }
9854 "
9855 .unindent(),
9856 );
9857 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9858 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9859 .await;
9860 cx.update_editor(|editor, _, _| {
9861 let signature_help_state = editor.signature_help_state.popover().cloned();
9862 assert!(signature_help_state.is_some());
9863 assert_eq!(
9864 signature_help_state.unwrap().label,
9865 "param1: u8, param2: u8"
9866 );
9867 editor.signature_help_state = SignatureHelpState::default();
9868 });
9869
9870 // Ensure that signature_help is called when auto signature help override is enabled
9871 cx.update(|_, cx| {
9872 cx.update_global::<SettingsStore, _>(|settings, cx| {
9873 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9874 settings.auto_signature_help = Some(true);
9875 settings.show_signature_help_after_edits = Some(false);
9876 });
9877 });
9878 });
9879 cx.set_state(
9880 &r#"
9881 fn main() {
9882 sampleˇ
9883 }
9884 "#
9885 .unindent(),
9886 );
9887 cx.update_editor(|editor, window, cx| {
9888 editor.handle_input("(", window, cx);
9889 });
9890 cx.assert_editor_state(
9891 &"
9892 fn main() {
9893 sample(ˇ)
9894 }
9895 "
9896 .unindent(),
9897 );
9898 handle_signature_help_request(&mut cx, mocked_response).await;
9899 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9900 .await;
9901 cx.editor(|editor, _, _| {
9902 let signature_help_state = editor.signature_help_state.popover().cloned();
9903 assert!(signature_help_state.is_some());
9904 assert_eq!(
9905 signature_help_state.unwrap().label,
9906 "param1: u8, param2: u8"
9907 );
9908 });
9909}
9910
9911#[gpui::test]
9912async fn test_signature_help(cx: &mut TestAppContext) {
9913 init_test(cx, |_| {});
9914 cx.update(|cx| {
9915 cx.update_global::<SettingsStore, _>(|settings, cx| {
9916 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9917 settings.auto_signature_help = Some(true);
9918 });
9919 });
9920 });
9921
9922 let mut cx = EditorLspTestContext::new_rust(
9923 lsp::ServerCapabilities {
9924 signature_help_provider: Some(lsp::SignatureHelpOptions {
9925 ..Default::default()
9926 }),
9927 ..Default::default()
9928 },
9929 cx,
9930 )
9931 .await;
9932
9933 // A test that directly calls `show_signature_help`
9934 cx.update_editor(|editor, window, cx| {
9935 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9936 });
9937
9938 let mocked_response = lsp::SignatureHelp {
9939 signatures: vec![lsp::SignatureInformation {
9940 label: "fn sample(param1: u8, param2: u8)".to_string(),
9941 documentation: None,
9942 parameters: Some(vec![
9943 lsp::ParameterInformation {
9944 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9945 documentation: None,
9946 },
9947 lsp::ParameterInformation {
9948 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9949 documentation: None,
9950 },
9951 ]),
9952 active_parameter: None,
9953 }],
9954 active_signature: Some(0),
9955 active_parameter: Some(0),
9956 };
9957 handle_signature_help_request(&mut cx, mocked_response).await;
9958
9959 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9960 .await;
9961
9962 cx.editor(|editor, _, _| {
9963 let signature_help_state = editor.signature_help_state.popover().cloned();
9964 assert!(signature_help_state.is_some());
9965 assert_eq!(
9966 signature_help_state.unwrap().label,
9967 "param1: u8, param2: u8"
9968 );
9969 });
9970
9971 // When exiting outside from inside the brackets, `signature_help` is closed.
9972 cx.set_state(indoc! {"
9973 fn main() {
9974 sample(ˇ);
9975 }
9976
9977 fn sample(param1: u8, param2: u8) {}
9978 "});
9979
9980 cx.update_editor(|editor, window, cx| {
9981 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9982 });
9983
9984 let mocked_response = lsp::SignatureHelp {
9985 signatures: Vec::new(),
9986 active_signature: None,
9987 active_parameter: None,
9988 };
9989 handle_signature_help_request(&mut cx, mocked_response).await;
9990
9991 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9992 .await;
9993
9994 cx.editor(|editor, _, _| {
9995 assert!(!editor.signature_help_state.is_shown());
9996 });
9997
9998 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9999 cx.set_state(indoc! {"
10000 fn main() {
10001 sample(ˇ);
10002 }
10003
10004 fn sample(param1: u8, param2: u8) {}
10005 "});
10006
10007 let mocked_response = lsp::SignatureHelp {
10008 signatures: vec![lsp::SignatureInformation {
10009 label: "fn sample(param1: u8, param2: u8)".to_string(),
10010 documentation: None,
10011 parameters: Some(vec![
10012 lsp::ParameterInformation {
10013 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10014 documentation: None,
10015 },
10016 lsp::ParameterInformation {
10017 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10018 documentation: None,
10019 },
10020 ]),
10021 active_parameter: None,
10022 }],
10023 active_signature: Some(0),
10024 active_parameter: Some(0),
10025 };
10026 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10027 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10028 .await;
10029 cx.editor(|editor, _, _| {
10030 assert!(editor.signature_help_state.is_shown());
10031 });
10032
10033 // Restore the popover with more parameter input
10034 cx.set_state(indoc! {"
10035 fn main() {
10036 sample(param1, param2ˇ);
10037 }
10038
10039 fn sample(param1: u8, param2: u8) {}
10040 "});
10041
10042 let mocked_response = lsp::SignatureHelp {
10043 signatures: vec![lsp::SignatureInformation {
10044 label: "fn sample(param1: u8, param2: u8)".to_string(),
10045 documentation: None,
10046 parameters: Some(vec![
10047 lsp::ParameterInformation {
10048 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10049 documentation: None,
10050 },
10051 lsp::ParameterInformation {
10052 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10053 documentation: None,
10054 },
10055 ]),
10056 active_parameter: None,
10057 }],
10058 active_signature: Some(0),
10059 active_parameter: Some(1),
10060 };
10061 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10062 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10063 .await;
10064
10065 // When selecting a range, the popover is gone.
10066 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10067 cx.update_editor(|editor, window, cx| {
10068 editor.change_selections(None, window, cx, |s| {
10069 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10070 })
10071 });
10072 cx.assert_editor_state(indoc! {"
10073 fn main() {
10074 sample(param1, «ˇparam2»);
10075 }
10076
10077 fn sample(param1: u8, param2: u8) {}
10078 "});
10079 cx.editor(|editor, _, _| {
10080 assert!(!editor.signature_help_state.is_shown());
10081 });
10082
10083 // When unselecting again, the popover is back if within the brackets.
10084 cx.update_editor(|editor, window, cx| {
10085 editor.change_selections(None, window, cx, |s| {
10086 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10087 })
10088 });
10089 cx.assert_editor_state(indoc! {"
10090 fn main() {
10091 sample(param1, ˇparam2);
10092 }
10093
10094 fn sample(param1: u8, param2: u8) {}
10095 "});
10096 handle_signature_help_request(&mut cx, mocked_response).await;
10097 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10098 .await;
10099 cx.editor(|editor, _, _| {
10100 assert!(editor.signature_help_state.is_shown());
10101 });
10102
10103 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10104 cx.update_editor(|editor, window, cx| {
10105 editor.change_selections(None, window, cx, |s| {
10106 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10107 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10108 })
10109 });
10110 cx.assert_editor_state(indoc! {"
10111 fn main() {
10112 sample(param1, ˇparam2);
10113 }
10114
10115 fn sample(param1: u8, param2: u8) {}
10116 "});
10117
10118 let mocked_response = lsp::SignatureHelp {
10119 signatures: vec![lsp::SignatureInformation {
10120 label: "fn sample(param1: u8, param2: u8)".to_string(),
10121 documentation: None,
10122 parameters: Some(vec![
10123 lsp::ParameterInformation {
10124 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10125 documentation: None,
10126 },
10127 lsp::ParameterInformation {
10128 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10129 documentation: None,
10130 },
10131 ]),
10132 active_parameter: None,
10133 }],
10134 active_signature: Some(0),
10135 active_parameter: Some(1),
10136 };
10137 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10138 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10139 .await;
10140 cx.update_editor(|editor, _, cx| {
10141 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10142 });
10143 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10144 .await;
10145 cx.update_editor(|editor, window, cx| {
10146 editor.change_selections(None, window, cx, |s| {
10147 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10148 })
10149 });
10150 cx.assert_editor_state(indoc! {"
10151 fn main() {
10152 sample(param1, «ˇparam2»);
10153 }
10154
10155 fn sample(param1: u8, param2: u8) {}
10156 "});
10157 cx.update_editor(|editor, window, cx| {
10158 editor.change_selections(None, window, cx, |s| {
10159 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10160 })
10161 });
10162 cx.assert_editor_state(indoc! {"
10163 fn main() {
10164 sample(param1, ˇparam2);
10165 }
10166
10167 fn sample(param1: u8, param2: u8) {}
10168 "});
10169 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10170 .await;
10171}
10172
10173#[gpui::test]
10174async fn test_completion_mode(cx: &mut TestAppContext) {
10175 init_test(cx, |_| {});
10176 let mut cx = EditorLspTestContext::new_rust(
10177 lsp::ServerCapabilities {
10178 completion_provider: Some(lsp::CompletionOptions {
10179 resolve_provider: Some(true),
10180 ..Default::default()
10181 }),
10182 ..Default::default()
10183 },
10184 cx,
10185 )
10186 .await;
10187
10188 struct Run {
10189 run_description: &'static str,
10190 initial_state: String,
10191 buffer_marked_text: String,
10192 completion_text: &'static str,
10193 expected_with_insert_mode: String,
10194 expected_with_replace_mode: String,
10195 expected_with_replace_subsequence_mode: String,
10196 expected_with_replace_suffix_mode: String,
10197 }
10198
10199 let runs = [
10200 Run {
10201 run_description: "Start of word matches completion text",
10202 initial_state: "before ediˇ after".into(),
10203 buffer_marked_text: "before <edi|> after".into(),
10204 completion_text: "editor",
10205 expected_with_insert_mode: "before editorˇ after".into(),
10206 expected_with_replace_mode: "before editorˇ after".into(),
10207 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10208 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10209 },
10210 Run {
10211 run_description: "Accept same text at the middle of the word",
10212 initial_state: "before ediˇtor after".into(),
10213 buffer_marked_text: "before <edi|tor> after".into(),
10214 completion_text: "editor",
10215 expected_with_insert_mode: "before editorˇtor after".into(),
10216 expected_with_replace_mode: "before editorˇ after".into(),
10217 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10218 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10219 },
10220 Run {
10221 run_description: "End of word matches completion text -- cursor at end",
10222 initial_state: "before torˇ after".into(),
10223 buffer_marked_text: "before <tor|> after".into(),
10224 completion_text: "editor",
10225 expected_with_insert_mode: "before editorˇ after".into(),
10226 expected_with_replace_mode: "before editorˇ after".into(),
10227 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10228 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10229 },
10230 Run {
10231 run_description: "End of word matches completion text -- cursor at start",
10232 initial_state: "before ˇtor after".into(),
10233 buffer_marked_text: "before <|tor> after".into(),
10234 completion_text: "editor",
10235 expected_with_insert_mode: "before editorˇtor after".into(),
10236 expected_with_replace_mode: "before editorˇ after".into(),
10237 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10238 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10239 },
10240 Run {
10241 run_description: "Prepend text containing whitespace",
10242 initial_state: "pˇfield: bool".into(),
10243 buffer_marked_text: "<p|field>: bool".into(),
10244 completion_text: "pub ",
10245 expected_with_insert_mode: "pub ˇfield: bool".into(),
10246 expected_with_replace_mode: "pub ˇ: bool".into(),
10247 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10248 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10249 },
10250 Run {
10251 run_description: "Add element to start of list",
10252 initial_state: "[element_ˇelement_2]".into(),
10253 buffer_marked_text: "[<element_|element_2>]".into(),
10254 completion_text: "element_1",
10255 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10256 expected_with_replace_mode: "[element_1ˇ]".into(),
10257 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10258 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10259 },
10260 Run {
10261 run_description: "Add element to start of list -- first and second elements are equal",
10262 initial_state: "[elˇelement]".into(),
10263 buffer_marked_text: "[<el|element>]".into(),
10264 completion_text: "element",
10265 expected_with_insert_mode: "[elementˇelement]".into(),
10266 expected_with_replace_mode: "[elementˇ]".into(),
10267 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10268 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10269 },
10270 Run {
10271 run_description: "Ends with matching suffix",
10272 initial_state: "SubˇError".into(),
10273 buffer_marked_text: "<Sub|Error>".into(),
10274 completion_text: "SubscriptionError",
10275 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10276 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10277 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10278 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10279 },
10280 Run {
10281 run_description: "Suffix is a subsequence -- contiguous",
10282 initial_state: "SubˇErr".into(),
10283 buffer_marked_text: "<Sub|Err>".into(),
10284 completion_text: "SubscriptionError",
10285 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10286 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10287 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10288 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10289 },
10290 Run {
10291 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10292 initial_state: "Suˇscrirr".into(),
10293 buffer_marked_text: "<Su|scrirr>".into(),
10294 completion_text: "SubscriptionError",
10295 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10296 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10297 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10298 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10299 },
10300 Run {
10301 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10302 initial_state: "foo(indˇix)".into(),
10303 buffer_marked_text: "foo(<ind|ix>)".into(),
10304 completion_text: "node_index",
10305 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10306 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10307 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10308 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10309 },
10310 ];
10311
10312 for run in runs {
10313 let run_variations = [
10314 (LspInsertMode::Insert, run.expected_with_insert_mode),
10315 (LspInsertMode::Replace, run.expected_with_replace_mode),
10316 (
10317 LspInsertMode::ReplaceSubsequence,
10318 run.expected_with_replace_subsequence_mode,
10319 ),
10320 (
10321 LspInsertMode::ReplaceSuffix,
10322 run.expected_with_replace_suffix_mode,
10323 ),
10324 ];
10325
10326 for (lsp_insert_mode, expected_text) in run_variations {
10327 eprintln!(
10328 "run = {:?}, mode = {lsp_insert_mode:.?}",
10329 run.run_description,
10330 );
10331
10332 update_test_language_settings(&mut cx, |settings| {
10333 settings.defaults.completions = Some(CompletionSettings {
10334 lsp_insert_mode,
10335 words: WordsCompletionMode::Disabled,
10336 lsp: true,
10337 lsp_fetch_timeout_ms: 0,
10338 });
10339 });
10340
10341 cx.set_state(&run.initial_state);
10342 cx.update_editor(|editor, window, cx| {
10343 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10344 });
10345
10346 let counter = Arc::new(AtomicUsize::new(0));
10347 handle_completion_request_with_insert_and_replace(
10348 &mut cx,
10349 &run.buffer_marked_text,
10350 vec![run.completion_text],
10351 counter.clone(),
10352 )
10353 .await;
10354 cx.condition(|editor, _| editor.context_menu_visible())
10355 .await;
10356 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10357
10358 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10359 editor
10360 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10361 .unwrap()
10362 });
10363 cx.assert_editor_state(&expected_text);
10364 handle_resolve_completion_request(&mut cx, None).await;
10365 apply_additional_edits.await.unwrap();
10366 }
10367 }
10368}
10369
10370#[gpui::test]
10371async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10372 init_test(cx, |_| {});
10373 let mut cx = EditorLspTestContext::new_rust(
10374 lsp::ServerCapabilities {
10375 completion_provider: Some(lsp::CompletionOptions {
10376 resolve_provider: Some(true),
10377 ..Default::default()
10378 }),
10379 ..Default::default()
10380 },
10381 cx,
10382 )
10383 .await;
10384
10385 let initial_state = "SubˇError";
10386 let buffer_marked_text = "<Sub|Error>";
10387 let completion_text = "SubscriptionError";
10388 let expected_with_insert_mode = "SubscriptionErrorˇError";
10389 let expected_with_replace_mode = "SubscriptionErrorˇ";
10390
10391 update_test_language_settings(&mut cx, |settings| {
10392 settings.defaults.completions = Some(CompletionSettings {
10393 words: WordsCompletionMode::Disabled,
10394 // set the opposite here to ensure that the action is overriding the default behavior
10395 lsp_insert_mode: LspInsertMode::Insert,
10396 lsp: true,
10397 lsp_fetch_timeout_ms: 0,
10398 });
10399 });
10400
10401 cx.set_state(initial_state);
10402 cx.update_editor(|editor, window, cx| {
10403 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10404 });
10405
10406 let counter = Arc::new(AtomicUsize::new(0));
10407 handle_completion_request_with_insert_and_replace(
10408 &mut cx,
10409 &buffer_marked_text,
10410 vec![completion_text],
10411 counter.clone(),
10412 )
10413 .await;
10414 cx.condition(|editor, _| editor.context_menu_visible())
10415 .await;
10416 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10417
10418 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10419 editor
10420 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10421 .unwrap()
10422 });
10423 cx.assert_editor_state(&expected_with_replace_mode);
10424 handle_resolve_completion_request(&mut cx, None).await;
10425 apply_additional_edits.await.unwrap();
10426
10427 update_test_language_settings(&mut cx, |settings| {
10428 settings.defaults.completions = Some(CompletionSettings {
10429 words: WordsCompletionMode::Disabled,
10430 // set the opposite here to ensure that the action is overriding the default behavior
10431 lsp_insert_mode: LspInsertMode::Replace,
10432 lsp: true,
10433 lsp_fetch_timeout_ms: 0,
10434 });
10435 });
10436
10437 cx.set_state(initial_state);
10438 cx.update_editor(|editor, window, cx| {
10439 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10440 });
10441 handle_completion_request_with_insert_and_replace(
10442 &mut cx,
10443 &buffer_marked_text,
10444 vec![completion_text],
10445 counter.clone(),
10446 )
10447 .await;
10448 cx.condition(|editor, _| editor.context_menu_visible())
10449 .await;
10450 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10451
10452 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10453 editor
10454 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10455 .unwrap()
10456 });
10457 cx.assert_editor_state(&expected_with_insert_mode);
10458 handle_resolve_completion_request(&mut cx, None).await;
10459 apply_additional_edits.await.unwrap();
10460}
10461
10462#[gpui::test]
10463async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10464 init_test(cx, |_| {});
10465 let mut cx = EditorLspTestContext::new_rust(
10466 lsp::ServerCapabilities {
10467 completion_provider: Some(lsp::CompletionOptions {
10468 resolve_provider: Some(true),
10469 ..Default::default()
10470 }),
10471 ..Default::default()
10472 },
10473 cx,
10474 )
10475 .await;
10476
10477 // scenario: surrounding text matches completion text
10478 let completion_text = "to_offset";
10479 let initial_state = indoc! {"
10480 1. buf.to_offˇsuffix
10481 2. buf.to_offˇsuf
10482 3. buf.to_offˇfix
10483 4. buf.to_offˇ
10484 5. into_offˇensive
10485 6. ˇsuffix
10486 7. let ˇ //
10487 8. aaˇzz
10488 9. buf.to_off«zzzzzˇ»suffix
10489 10. buf.«ˇzzzzz»suffix
10490 11. to_off«ˇzzzzz»
10491
10492 buf.to_offˇsuffix // newest cursor
10493 "};
10494 let completion_marked_buffer = indoc! {"
10495 1. buf.to_offsuffix
10496 2. buf.to_offsuf
10497 3. buf.to_offfix
10498 4. buf.to_off
10499 5. into_offensive
10500 6. suffix
10501 7. let //
10502 8. aazz
10503 9. buf.to_offzzzzzsuffix
10504 10. buf.zzzzzsuffix
10505 11. to_offzzzzz
10506
10507 buf.<to_off|suffix> // newest cursor
10508 "};
10509 let expected = indoc! {"
10510 1. buf.to_offsetˇ
10511 2. buf.to_offsetˇsuf
10512 3. buf.to_offsetˇfix
10513 4. buf.to_offsetˇ
10514 5. into_offsetˇensive
10515 6. to_offsetˇsuffix
10516 7. let to_offsetˇ //
10517 8. aato_offsetˇzz
10518 9. buf.to_offsetˇ
10519 10. buf.to_offsetˇsuffix
10520 11. to_offsetˇ
10521
10522 buf.to_offsetˇ // newest cursor
10523 "};
10524 cx.set_state(initial_state);
10525 cx.update_editor(|editor, window, cx| {
10526 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10527 });
10528 handle_completion_request_with_insert_and_replace(
10529 &mut cx,
10530 completion_marked_buffer,
10531 vec![completion_text],
10532 Arc::new(AtomicUsize::new(0)),
10533 )
10534 .await;
10535 cx.condition(|editor, _| editor.context_menu_visible())
10536 .await;
10537 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10538 editor
10539 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10540 .unwrap()
10541 });
10542 cx.assert_editor_state(expected);
10543 handle_resolve_completion_request(&mut cx, None).await;
10544 apply_additional_edits.await.unwrap();
10545
10546 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10547 let completion_text = "foo_and_bar";
10548 let initial_state = indoc! {"
10549 1. ooanbˇ
10550 2. zooanbˇ
10551 3. ooanbˇz
10552 4. zooanbˇz
10553 5. ooanˇ
10554 6. oanbˇ
10555
10556 ooanbˇ
10557 "};
10558 let completion_marked_buffer = indoc! {"
10559 1. ooanb
10560 2. zooanb
10561 3. ooanbz
10562 4. zooanbz
10563 5. ooan
10564 6. oanb
10565
10566 <ooanb|>
10567 "};
10568 let expected = indoc! {"
10569 1. foo_and_barˇ
10570 2. zfoo_and_barˇ
10571 3. foo_and_barˇz
10572 4. zfoo_and_barˇz
10573 5. ooanfoo_and_barˇ
10574 6. oanbfoo_and_barˇ
10575
10576 foo_and_barˇ
10577 "};
10578 cx.set_state(initial_state);
10579 cx.update_editor(|editor, window, cx| {
10580 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10581 });
10582 handle_completion_request_with_insert_and_replace(
10583 &mut cx,
10584 completion_marked_buffer,
10585 vec![completion_text],
10586 Arc::new(AtomicUsize::new(0)),
10587 )
10588 .await;
10589 cx.condition(|editor, _| editor.context_menu_visible())
10590 .await;
10591 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10592 editor
10593 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10594 .unwrap()
10595 });
10596 cx.assert_editor_state(expected);
10597 handle_resolve_completion_request(&mut cx, None).await;
10598 apply_additional_edits.await.unwrap();
10599
10600 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10601 // (expects the same as if it was inserted at the end)
10602 let completion_text = "foo_and_bar";
10603 let initial_state = indoc! {"
10604 1. ooˇanb
10605 2. zooˇanb
10606 3. ooˇanbz
10607 4. zooˇanbz
10608
10609 ooˇanb
10610 "};
10611 let completion_marked_buffer = indoc! {"
10612 1. ooanb
10613 2. zooanb
10614 3. ooanbz
10615 4. zooanbz
10616
10617 <oo|anb>
10618 "};
10619 let expected = indoc! {"
10620 1. foo_and_barˇ
10621 2. zfoo_and_barˇ
10622 3. foo_and_barˇz
10623 4. zfoo_and_barˇz
10624
10625 foo_and_barˇ
10626 "};
10627 cx.set_state(initial_state);
10628 cx.update_editor(|editor, window, cx| {
10629 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10630 });
10631 handle_completion_request_with_insert_and_replace(
10632 &mut cx,
10633 completion_marked_buffer,
10634 vec![completion_text],
10635 Arc::new(AtomicUsize::new(0)),
10636 )
10637 .await;
10638 cx.condition(|editor, _| editor.context_menu_visible())
10639 .await;
10640 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10641 editor
10642 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10643 .unwrap()
10644 });
10645 cx.assert_editor_state(expected);
10646 handle_resolve_completion_request(&mut cx, None).await;
10647 apply_additional_edits.await.unwrap();
10648}
10649
10650// This used to crash
10651#[gpui::test]
10652async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10653 init_test(cx, |_| {});
10654
10655 let buffer_text = indoc! {"
10656 fn main() {
10657 10.satu;
10658
10659 //
10660 // separate cursors so they open in different excerpts (manually reproducible)
10661 //
10662
10663 10.satu20;
10664 }
10665 "};
10666 let multibuffer_text_with_selections = indoc! {"
10667 fn main() {
10668 10.satuˇ;
10669
10670 //
10671
10672 //
10673
10674 10.satuˇ20;
10675 }
10676 "};
10677 let expected_multibuffer = indoc! {"
10678 fn main() {
10679 10.saturating_sub()ˇ;
10680
10681 //
10682
10683 //
10684
10685 10.saturating_sub()ˇ;
10686 }
10687 "};
10688
10689 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10690 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10691
10692 let fs = FakeFs::new(cx.executor());
10693 fs.insert_tree(
10694 path!("/a"),
10695 json!({
10696 "main.rs": buffer_text,
10697 }),
10698 )
10699 .await;
10700
10701 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10702 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10703 language_registry.add(rust_lang());
10704 let mut fake_servers = language_registry.register_fake_lsp(
10705 "Rust",
10706 FakeLspAdapter {
10707 capabilities: lsp::ServerCapabilities {
10708 completion_provider: Some(lsp::CompletionOptions {
10709 resolve_provider: None,
10710 ..lsp::CompletionOptions::default()
10711 }),
10712 ..lsp::ServerCapabilities::default()
10713 },
10714 ..FakeLspAdapter::default()
10715 },
10716 );
10717 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10718 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10719 let buffer = project
10720 .update(cx, |project, cx| {
10721 project.open_local_buffer(path!("/a/main.rs"), cx)
10722 })
10723 .await
10724 .unwrap();
10725
10726 let multi_buffer = cx.new(|cx| {
10727 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10728 multi_buffer.push_excerpts(
10729 buffer.clone(),
10730 [ExcerptRange::new(0..first_excerpt_end)],
10731 cx,
10732 );
10733 multi_buffer.push_excerpts(
10734 buffer.clone(),
10735 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10736 cx,
10737 );
10738 multi_buffer
10739 });
10740
10741 let editor = workspace
10742 .update(cx, |_, window, cx| {
10743 cx.new(|cx| {
10744 Editor::new(
10745 EditorMode::Full {
10746 scale_ui_elements_with_buffer_font_size: false,
10747 show_active_line_background: false,
10748 sized_by_content: false,
10749 },
10750 multi_buffer.clone(),
10751 Some(project.clone()),
10752 window,
10753 cx,
10754 )
10755 })
10756 })
10757 .unwrap();
10758
10759 let pane = workspace
10760 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10761 .unwrap();
10762 pane.update_in(cx, |pane, window, cx| {
10763 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10764 });
10765
10766 let fake_server = fake_servers.next().await.unwrap();
10767
10768 editor.update_in(cx, |editor, window, cx| {
10769 editor.change_selections(None, window, cx, |s| {
10770 s.select_ranges([
10771 Point::new(1, 11)..Point::new(1, 11),
10772 Point::new(7, 11)..Point::new(7, 11),
10773 ])
10774 });
10775
10776 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10777 });
10778
10779 editor.update_in(cx, |editor, window, cx| {
10780 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10781 });
10782
10783 fake_server
10784 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10785 let completion_item = lsp::CompletionItem {
10786 label: "saturating_sub()".into(),
10787 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10788 lsp::InsertReplaceEdit {
10789 new_text: "saturating_sub()".to_owned(),
10790 insert: lsp::Range::new(
10791 lsp::Position::new(7, 7),
10792 lsp::Position::new(7, 11),
10793 ),
10794 replace: lsp::Range::new(
10795 lsp::Position::new(7, 7),
10796 lsp::Position::new(7, 13),
10797 ),
10798 },
10799 )),
10800 ..lsp::CompletionItem::default()
10801 };
10802
10803 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10804 })
10805 .next()
10806 .await
10807 .unwrap();
10808
10809 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10810 .await;
10811
10812 editor
10813 .update_in(cx, |editor, window, cx| {
10814 editor
10815 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10816 .unwrap()
10817 })
10818 .await
10819 .unwrap();
10820
10821 editor.update(cx, |editor, cx| {
10822 assert_text_with_selections(editor, expected_multibuffer, cx);
10823 })
10824}
10825
10826#[gpui::test]
10827async fn test_completion(cx: &mut TestAppContext) {
10828 init_test(cx, |_| {});
10829
10830 let mut cx = EditorLspTestContext::new_rust(
10831 lsp::ServerCapabilities {
10832 completion_provider: Some(lsp::CompletionOptions {
10833 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10834 resolve_provider: Some(true),
10835 ..Default::default()
10836 }),
10837 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10838 ..Default::default()
10839 },
10840 cx,
10841 )
10842 .await;
10843 let counter = Arc::new(AtomicUsize::new(0));
10844
10845 cx.set_state(indoc! {"
10846 oneˇ
10847 two
10848 three
10849 "});
10850 cx.simulate_keystroke(".");
10851 handle_completion_request(
10852 &mut cx,
10853 indoc! {"
10854 one.|<>
10855 two
10856 three
10857 "},
10858 vec!["first_completion", "second_completion"],
10859 counter.clone(),
10860 )
10861 .await;
10862 cx.condition(|editor, _| editor.context_menu_visible())
10863 .await;
10864 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10865
10866 let _handler = handle_signature_help_request(
10867 &mut cx,
10868 lsp::SignatureHelp {
10869 signatures: vec![lsp::SignatureInformation {
10870 label: "test signature".to_string(),
10871 documentation: None,
10872 parameters: Some(vec![lsp::ParameterInformation {
10873 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10874 documentation: None,
10875 }]),
10876 active_parameter: None,
10877 }],
10878 active_signature: None,
10879 active_parameter: None,
10880 },
10881 );
10882 cx.update_editor(|editor, window, cx| {
10883 assert!(
10884 !editor.signature_help_state.is_shown(),
10885 "No signature help was called for"
10886 );
10887 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10888 });
10889 cx.run_until_parked();
10890 cx.update_editor(|editor, _, _| {
10891 assert!(
10892 !editor.signature_help_state.is_shown(),
10893 "No signature help should be shown when completions menu is open"
10894 );
10895 });
10896
10897 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10898 editor.context_menu_next(&Default::default(), window, cx);
10899 editor
10900 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10901 .unwrap()
10902 });
10903 cx.assert_editor_state(indoc! {"
10904 one.second_completionˇ
10905 two
10906 three
10907 "});
10908
10909 handle_resolve_completion_request(
10910 &mut cx,
10911 Some(vec![
10912 (
10913 //This overlaps with the primary completion edit which is
10914 //misbehavior from the LSP spec, test that we filter it out
10915 indoc! {"
10916 one.second_ˇcompletion
10917 two
10918 threeˇ
10919 "},
10920 "overlapping additional edit",
10921 ),
10922 (
10923 indoc! {"
10924 one.second_completion
10925 two
10926 threeˇ
10927 "},
10928 "\nadditional edit",
10929 ),
10930 ]),
10931 )
10932 .await;
10933 apply_additional_edits.await.unwrap();
10934 cx.assert_editor_state(indoc! {"
10935 one.second_completionˇ
10936 two
10937 three
10938 additional edit
10939 "});
10940
10941 cx.set_state(indoc! {"
10942 one.second_completion
10943 twoˇ
10944 threeˇ
10945 additional edit
10946 "});
10947 cx.simulate_keystroke(" ");
10948 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10949 cx.simulate_keystroke("s");
10950 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10951
10952 cx.assert_editor_state(indoc! {"
10953 one.second_completion
10954 two sˇ
10955 three sˇ
10956 additional edit
10957 "});
10958 handle_completion_request(
10959 &mut cx,
10960 indoc! {"
10961 one.second_completion
10962 two s
10963 three <s|>
10964 additional edit
10965 "},
10966 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10967 counter.clone(),
10968 )
10969 .await;
10970 cx.condition(|editor, _| editor.context_menu_visible())
10971 .await;
10972 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10973
10974 cx.simulate_keystroke("i");
10975
10976 handle_completion_request(
10977 &mut cx,
10978 indoc! {"
10979 one.second_completion
10980 two si
10981 three <si|>
10982 additional edit
10983 "},
10984 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10985 counter.clone(),
10986 )
10987 .await;
10988 cx.condition(|editor, _| editor.context_menu_visible())
10989 .await;
10990 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10991
10992 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10993 editor
10994 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10995 .unwrap()
10996 });
10997 cx.assert_editor_state(indoc! {"
10998 one.second_completion
10999 two sixth_completionˇ
11000 three sixth_completionˇ
11001 additional edit
11002 "});
11003
11004 apply_additional_edits.await.unwrap();
11005
11006 update_test_language_settings(&mut cx, |settings| {
11007 settings.defaults.show_completions_on_input = Some(false);
11008 });
11009 cx.set_state("editorˇ");
11010 cx.simulate_keystroke(".");
11011 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11012 cx.simulate_keystrokes("c l o");
11013 cx.assert_editor_state("editor.cloˇ");
11014 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11015 cx.update_editor(|editor, window, cx| {
11016 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11017 });
11018 handle_completion_request(
11019 &mut cx,
11020 "editor.<clo|>",
11021 vec!["close", "clobber"],
11022 counter.clone(),
11023 )
11024 .await;
11025 cx.condition(|editor, _| editor.context_menu_visible())
11026 .await;
11027 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11028
11029 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11030 editor
11031 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11032 .unwrap()
11033 });
11034 cx.assert_editor_state("editor.closeˇ");
11035 handle_resolve_completion_request(&mut cx, None).await;
11036 apply_additional_edits.await.unwrap();
11037}
11038
11039#[gpui::test]
11040async fn test_word_completion(cx: &mut TestAppContext) {
11041 let lsp_fetch_timeout_ms = 10;
11042 init_test(cx, |language_settings| {
11043 language_settings.defaults.completions = Some(CompletionSettings {
11044 words: WordsCompletionMode::Fallback,
11045 lsp: true,
11046 lsp_fetch_timeout_ms: 10,
11047 lsp_insert_mode: LspInsertMode::Insert,
11048 });
11049 });
11050
11051 let mut cx = EditorLspTestContext::new_rust(
11052 lsp::ServerCapabilities {
11053 completion_provider: Some(lsp::CompletionOptions {
11054 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11055 ..lsp::CompletionOptions::default()
11056 }),
11057 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11058 ..lsp::ServerCapabilities::default()
11059 },
11060 cx,
11061 )
11062 .await;
11063
11064 let throttle_completions = Arc::new(AtomicBool::new(false));
11065
11066 let lsp_throttle_completions = throttle_completions.clone();
11067 let _completion_requests_handler =
11068 cx.lsp
11069 .server
11070 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11071 let lsp_throttle_completions = lsp_throttle_completions.clone();
11072 let cx = cx.clone();
11073 async move {
11074 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11075 cx.background_executor()
11076 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11077 .await;
11078 }
11079 Ok(Some(lsp::CompletionResponse::Array(vec![
11080 lsp::CompletionItem {
11081 label: "first".into(),
11082 ..lsp::CompletionItem::default()
11083 },
11084 lsp::CompletionItem {
11085 label: "last".into(),
11086 ..lsp::CompletionItem::default()
11087 },
11088 ])))
11089 }
11090 });
11091
11092 cx.set_state(indoc! {"
11093 oneˇ
11094 two
11095 three
11096 "});
11097 cx.simulate_keystroke(".");
11098 cx.executor().run_until_parked();
11099 cx.condition(|editor, _| editor.context_menu_visible())
11100 .await;
11101 cx.update_editor(|editor, window, cx| {
11102 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11103 {
11104 assert_eq!(
11105 completion_menu_entries(&menu),
11106 &["first", "last"],
11107 "When LSP server is fast to reply, no fallback word completions are used"
11108 );
11109 } else {
11110 panic!("expected completion menu to be open");
11111 }
11112 editor.cancel(&Cancel, window, cx);
11113 });
11114 cx.executor().run_until_parked();
11115 cx.condition(|editor, _| !editor.context_menu_visible())
11116 .await;
11117
11118 throttle_completions.store(true, atomic::Ordering::Release);
11119 cx.simulate_keystroke(".");
11120 cx.executor()
11121 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11122 cx.executor().run_until_parked();
11123 cx.condition(|editor, _| editor.context_menu_visible())
11124 .await;
11125 cx.update_editor(|editor, _, _| {
11126 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11127 {
11128 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11129 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11130 } else {
11131 panic!("expected completion menu to be open");
11132 }
11133 });
11134}
11135
11136#[gpui::test]
11137async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11138 init_test(cx, |language_settings| {
11139 language_settings.defaults.completions = Some(CompletionSettings {
11140 words: WordsCompletionMode::Enabled,
11141 lsp: true,
11142 lsp_fetch_timeout_ms: 0,
11143 lsp_insert_mode: LspInsertMode::Insert,
11144 });
11145 });
11146
11147 let mut cx = EditorLspTestContext::new_rust(
11148 lsp::ServerCapabilities {
11149 completion_provider: Some(lsp::CompletionOptions {
11150 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11151 ..lsp::CompletionOptions::default()
11152 }),
11153 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11154 ..lsp::ServerCapabilities::default()
11155 },
11156 cx,
11157 )
11158 .await;
11159
11160 let _completion_requests_handler =
11161 cx.lsp
11162 .server
11163 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11164 Ok(Some(lsp::CompletionResponse::Array(vec![
11165 lsp::CompletionItem {
11166 label: "first".into(),
11167 ..lsp::CompletionItem::default()
11168 },
11169 lsp::CompletionItem {
11170 label: "last".into(),
11171 ..lsp::CompletionItem::default()
11172 },
11173 ])))
11174 });
11175
11176 cx.set_state(indoc! {"ˇ
11177 first
11178 last
11179 second
11180 "});
11181 cx.simulate_keystroke(".");
11182 cx.executor().run_until_parked();
11183 cx.condition(|editor, _| editor.context_menu_visible())
11184 .await;
11185 cx.update_editor(|editor, _, _| {
11186 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11187 {
11188 assert_eq!(
11189 completion_menu_entries(&menu),
11190 &["first", "last", "second"],
11191 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11192 );
11193 } else {
11194 panic!("expected completion menu to be open");
11195 }
11196 });
11197}
11198
11199#[gpui::test]
11200async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11201 init_test(cx, |language_settings| {
11202 language_settings.defaults.completions = Some(CompletionSettings {
11203 words: WordsCompletionMode::Disabled,
11204 lsp: true,
11205 lsp_fetch_timeout_ms: 0,
11206 lsp_insert_mode: LspInsertMode::Insert,
11207 });
11208 });
11209
11210 let mut cx = EditorLspTestContext::new_rust(
11211 lsp::ServerCapabilities {
11212 completion_provider: Some(lsp::CompletionOptions {
11213 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11214 ..lsp::CompletionOptions::default()
11215 }),
11216 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11217 ..lsp::ServerCapabilities::default()
11218 },
11219 cx,
11220 )
11221 .await;
11222
11223 let _completion_requests_handler =
11224 cx.lsp
11225 .server
11226 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11227 panic!("LSP completions should not be queried when dealing with word completions")
11228 });
11229
11230 cx.set_state(indoc! {"ˇ
11231 first
11232 last
11233 second
11234 "});
11235 cx.update_editor(|editor, window, cx| {
11236 editor.show_word_completions(&ShowWordCompletions, window, cx);
11237 });
11238 cx.executor().run_until_parked();
11239 cx.condition(|editor, _| editor.context_menu_visible())
11240 .await;
11241 cx.update_editor(|editor, _, _| {
11242 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11243 {
11244 assert_eq!(
11245 completion_menu_entries(&menu),
11246 &["first", "last", "second"],
11247 "`ShowWordCompletions` action should show word completions"
11248 );
11249 } else {
11250 panic!("expected completion menu to be open");
11251 }
11252 });
11253
11254 cx.simulate_keystroke("l");
11255 cx.executor().run_until_parked();
11256 cx.condition(|editor, _| editor.context_menu_visible())
11257 .await;
11258 cx.update_editor(|editor, _, _| {
11259 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11260 {
11261 assert_eq!(
11262 completion_menu_entries(&menu),
11263 &["last"],
11264 "After showing word completions, further editing should filter them and not query the LSP"
11265 );
11266 } else {
11267 panic!("expected completion menu to be open");
11268 }
11269 });
11270}
11271
11272#[gpui::test]
11273async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11274 init_test(cx, |language_settings| {
11275 language_settings.defaults.completions = Some(CompletionSettings {
11276 words: WordsCompletionMode::Fallback,
11277 lsp: false,
11278 lsp_fetch_timeout_ms: 0,
11279 lsp_insert_mode: LspInsertMode::Insert,
11280 });
11281 });
11282
11283 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11284
11285 cx.set_state(indoc! {"ˇ
11286 0_usize
11287 let
11288 33
11289 4.5f32
11290 "});
11291 cx.update_editor(|editor, window, cx| {
11292 editor.show_completions(&ShowCompletions::default(), window, cx);
11293 });
11294 cx.executor().run_until_parked();
11295 cx.condition(|editor, _| editor.context_menu_visible())
11296 .await;
11297 cx.update_editor(|editor, window, cx| {
11298 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11299 {
11300 assert_eq!(
11301 completion_menu_entries(&menu),
11302 &["let"],
11303 "With no digits in the completion query, no digits should be in the word completions"
11304 );
11305 } else {
11306 panic!("expected completion menu to be open");
11307 }
11308 editor.cancel(&Cancel, window, cx);
11309 });
11310
11311 cx.set_state(indoc! {"3ˇ
11312 0_usize
11313 let
11314 3
11315 33.35f32
11316 "});
11317 cx.update_editor(|editor, window, cx| {
11318 editor.show_completions(&ShowCompletions::default(), window, cx);
11319 });
11320 cx.executor().run_until_parked();
11321 cx.condition(|editor, _| editor.context_menu_visible())
11322 .await;
11323 cx.update_editor(|editor, _, _| {
11324 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11325 {
11326 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11327 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11328 } else {
11329 panic!("expected completion menu to be open");
11330 }
11331 });
11332}
11333
11334fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11335 let position = || lsp::Position {
11336 line: params.text_document_position.position.line,
11337 character: params.text_document_position.position.character,
11338 };
11339 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11340 range: lsp::Range {
11341 start: position(),
11342 end: position(),
11343 },
11344 new_text: text.to_string(),
11345 }))
11346}
11347
11348#[gpui::test]
11349async fn test_multiline_completion(cx: &mut TestAppContext) {
11350 init_test(cx, |_| {});
11351
11352 let fs = FakeFs::new(cx.executor());
11353 fs.insert_tree(
11354 path!("/a"),
11355 json!({
11356 "main.ts": "a",
11357 }),
11358 )
11359 .await;
11360
11361 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11362 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11363 let typescript_language = Arc::new(Language::new(
11364 LanguageConfig {
11365 name: "TypeScript".into(),
11366 matcher: LanguageMatcher {
11367 path_suffixes: vec!["ts".to_string()],
11368 ..LanguageMatcher::default()
11369 },
11370 line_comments: vec!["// ".into()],
11371 ..LanguageConfig::default()
11372 },
11373 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11374 ));
11375 language_registry.add(typescript_language.clone());
11376 let mut fake_servers = language_registry.register_fake_lsp(
11377 "TypeScript",
11378 FakeLspAdapter {
11379 capabilities: lsp::ServerCapabilities {
11380 completion_provider: Some(lsp::CompletionOptions {
11381 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11382 ..lsp::CompletionOptions::default()
11383 }),
11384 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11385 ..lsp::ServerCapabilities::default()
11386 },
11387 // Emulate vtsls label generation
11388 label_for_completion: Some(Box::new(|item, _| {
11389 let text = if let Some(description) = item
11390 .label_details
11391 .as_ref()
11392 .and_then(|label_details| label_details.description.as_ref())
11393 {
11394 format!("{} {}", item.label, description)
11395 } else if let Some(detail) = &item.detail {
11396 format!("{} {}", item.label, detail)
11397 } else {
11398 item.label.clone()
11399 };
11400 let len = text.len();
11401 Some(language::CodeLabel {
11402 text,
11403 runs: Vec::new(),
11404 filter_range: 0..len,
11405 })
11406 })),
11407 ..FakeLspAdapter::default()
11408 },
11409 );
11410 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11411 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11412 let worktree_id = workspace
11413 .update(cx, |workspace, _window, cx| {
11414 workspace.project().update(cx, |project, cx| {
11415 project.worktrees(cx).next().unwrap().read(cx).id()
11416 })
11417 })
11418 .unwrap();
11419 let _buffer = project
11420 .update(cx, |project, cx| {
11421 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11422 })
11423 .await
11424 .unwrap();
11425 let editor = workspace
11426 .update(cx, |workspace, window, cx| {
11427 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11428 })
11429 .unwrap()
11430 .await
11431 .unwrap()
11432 .downcast::<Editor>()
11433 .unwrap();
11434 let fake_server = fake_servers.next().await.unwrap();
11435
11436 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11437 let multiline_label_2 = "a\nb\nc\n";
11438 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11439 let multiline_description = "d\ne\nf\n";
11440 let multiline_detail_2 = "g\nh\ni\n";
11441
11442 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11443 move |params, _| async move {
11444 Ok(Some(lsp::CompletionResponse::Array(vec![
11445 lsp::CompletionItem {
11446 label: multiline_label.to_string(),
11447 text_edit: gen_text_edit(¶ms, "new_text_1"),
11448 ..lsp::CompletionItem::default()
11449 },
11450 lsp::CompletionItem {
11451 label: "single line label 1".to_string(),
11452 detail: Some(multiline_detail.to_string()),
11453 text_edit: gen_text_edit(¶ms, "new_text_2"),
11454 ..lsp::CompletionItem::default()
11455 },
11456 lsp::CompletionItem {
11457 label: "single line label 2".to_string(),
11458 label_details: Some(lsp::CompletionItemLabelDetails {
11459 description: Some(multiline_description.to_string()),
11460 detail: None,
11461 }),
11462 text_edit: gen_text_edit(¶ms, "new_text_2"),
11463 ..lsp::CompletionItem::default()
11464 },
11465 lsp::CompletionItem {
11466 label: multiline_label_2.to_string(),
11467 detail: Some(multiline_detail_2.to_string()),
11468 text_edit: gen_text_edit(¶ms, "new_text_3"),
11469 ..lsp::CompletionItem::default()
11470 },
11471 lsp::CompletionItem {
11472 label: "Label with many spaces and \t but without newlines".to_string(),
11473 detail: Some(
11474 "Details with many spaces and \t but without newlines".to_string(),
11475 ),
11476 text_edit: gen_text_edit(¶ms, "new_text_4"),
11477 ..lsp::CompletionItem::default()
11478 },
11479 ])))
11480 },
11481 );
11482
11483 editor.update_in(cx, |editor, window, cx| {
11484 cx.focus_self(window);
11485 editor.move_to_end(&MoveToEnd, window, cx);
11486 editor.handle_input(".", window, cx);
11487 });
11488 cx.run_until_parked();
11489 completion_handle.next().await.unwrap();
11490
11491 editor.update(cx, |editor, _| {
11492 assert!(editor.context_menu_visible());
11493 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11494 {
11495 let completion_labels = menu
11496 .completions
11497 .borrow()
11498 .iter()
11499 .map(|c| c.label.text.clone())
11500 .collect::<Vec<_>>();
11501 assert_eq!(
11502 completion_labels,
11503 &[
11504 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11505 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11506 "single line label 2 d e f ",
11507 "a b c g h i ",
11508 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11509 ],
11510 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11511 );
11512
11513 for completion in menu
11514 .completions
11515 .borrow()
11516 .iter() {
11517 assert_eq!(
11518 completion.label.filter_range,
11519 0..completion.label.text.len(),
11520 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11521 );
11522 }
11523 } else {
11524 panic!("expected completion menu to be open");
11525 }
11526 });
11527}
11528
11529#[gpui::test]
11530async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11531 init_test(cx, |_| {});
11532 let mut cx = EditorLspTestContext::new_rust(
11533 lsp::ServerCapabilities {
11534 completion_provider: Some(lsp::CompletionOptions {
11535 trigger_characters: Some(vec![".".to_string()]),
11536 ..Default::default()
11537 }),
11538 ..Default::default()
11539 },
11540 cx,
11541 )
11542 .await;
11543 cx.lsp
11544 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11545 Ok(Some(lsp::CompletionResponse::Array(vec![
11546 lsp::CompletionItem {
11547 label: "first".into(),
11548 ..Default::default()
11549 },
11550 lsp::CompletionItem {
11551 label: "last".into(),
11552 ..Default::default()
11553 },
11554 ])))
11555 });
11556 cx.set_state("variableˇ");
11557 cx.simulate_keystroke(".");
11558 cx.executor().run_until_parked();
11559
11560 cx.update_editor(|editor, _, _| {
11561 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11562 {
11563 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11564 } else {
11565 panic!("expected completion menu to be open");
11566 }
11567 });
11568
11569 cx.update_editor(|editor, window, cx| {
11570 editor.move_page_down(&MovePageDown::default(), window, cx);
11571 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11572 {
11573 assert!(
11574 menu.selected_item == 1,
11575 "expected PageDown to select the last item from the context menu"
11576 );
11577 } else {
11578 panic!("expected completion menu to stay open after PageDown");
11579 }
11580 });
11581
11582 cx.update_editor(|editor, window, cx| {
11583 editor.move_page_up(&MovePageUp::default(), window, cx);
11584 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11585 {
11586 assert!(
11587 menu.selected_item == 0,
11588 "expected PageUp to select the first item from the context menu"
11589 );
11590 } else {
11591 panic!("expected completion menu to stay open after PageUp");
11592 }
11593 });
11594}
11595
11596#[gpui::test]
11597async fn test_as_is_completions(cx: &mut TestAppContext) {
11598 init_test(cx, |_| {});
11599 let mut cx = EditorLspTestContext::new_rust(
11600 lsp::ServerCapabilities {
11601 completion_provider: Some(lsp::CompletionOptions {
11602 ..Default::default()
11603 }),
11604 ..Default::default()
11605 },
11606 cx,
11607 )
11608 .await;
11609 cx.lsp
11610 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11611 Ok(Some(lsp::CompletionResponse::Array(vec![
11612 lsp::CompletionItem {
11613 label: "unsafe".into(),
11614 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11615 range: lsp::Range {
11616 start: lsp::Position {
11617 line: 1,
11618 character: 2,
11619 },
11620 end: lsp::Position {
11621 line: 1,
11622 character: 3,
11623 },
11624 },
11625 new_text: "unsafe".to_string(),
11626 })),
11627 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11628 ..Default::default()
11629 },
11630 ])))
11631 });
11632 cx.set_state("fn a() {}\n nˇ");
11633 cx.executor().run_until_parked();
11634 cx.update_editor(|editor, window, cx| {
11635 editor.show_completions(
11636 &ShowCompletions {
11637 trigger: Some("\n".into()),
11638 },
11639 window,
11640 cx,
11641 );
11642 });
11643 cx.executor().run_until_parked();
11644
11645 cx.update_editor(|editor, window, cx| {
11646 editor.confirm_completion(&Default::default(), window, cx)
11647 });
11648 cx.executor().run_until_parked();
11649 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11650}
11651
11652#[gpui::test]
11653async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11654 init_test(cx, |_| {});
11655
11656 let mut cx = EditorLspTestContext::new_rust(
11657 lsp::ServerCapabilities {
11658 completion_provider: Some(lsp::CompletionOptions {
11659 trigger_characters: Some(vec![".".to_string()]),
11660 resolve_provider: Some(true),
11661 ..Default::default()
11662 }),
11663 ..Default::default()
11664 },
11665 cx,
11666 )
11667 .await;
11668
11669 cx.set_state("fn main() { let a = 2ˇ; }");
11670 cx.simulate_keystroke(".");
11671 let completion_item = lsp::CompletionItem {
11672 label: "Some".into(),
11673 kind: Some(lsp::CompletionItemKind::SNIPPET),
11674 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11675 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11676 kind: lsp::MarkupKind::Markdown,
11677 value: "```rust\nSome(2)\n```".to_string(),
11678 })),
11679 deprecated: Some(false),
11680 sort_text: Some("Some".to_string()),
11681 filter_text: Some("Some".to_string()),
11682 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11683 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11684 range: lsp::Range {
11685 start: lsp::Position {
11686 line: 0,
11687 character: 22,
11688 },
11689 end: lsp::Position {
11690 line: 0,
11691 character: 22,
11692 },
11693 },
11694 new_text: "Some(2)".to_string(),
11695 })),
11696 additional_text_edits: Some(vec![lsp::TextEdit {
11697 range: lsp::Range {
11698 start: lsp::Position {
11699 line: 0,
11700 character: 20,
11701 },
11702 end: lsp::Position {
11703 line: 0,
11704 character: 22,
11705 },
11706 },
11707 new_text: "".to_string(),
11708 }]),
11709 ..Default::default()
11710 };
11711
11712 let closure_completion_item = completion_item.clone();
11713 let counter = Arc::new(AtomicUsize::new(0));
11714 let counter_clone = counter.clone();
11715 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11716 let task_completion_item = closure_completion_item.clone();
11717 counter_clone.fetch_add(1, atomic::Ordering::Release);
11718 async move {
11719 Ok(Some(lsp::CompletionResponse::Array(vec![
11720 task_completion_item,
11721 ])))
11722 }
11723 });
11724
11725 cx.condition(|editor, _| editor.context_menu_visible())
11726 .await;
11727 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11728 assert!(request.next().await.is_some());
11729 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11730
11731 cx.simulate_keystrokes("S o m");
11732 cx.condition(|editor, _| editor.context_menu_visible())
11733 .await;
11734 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11735 assert!(request.next().await.is_some());
11736 assert!(request.next().await.is_some());
11737 assert!(request.next().await.is_some());
11738 request.close();
11739 assert!(request.next().await.is_none());
11740 assert_eq!(
11741 counter.load(atomic::Ordering::Acquire),
11742 4,
11743 "With the completions menu open, only one LSP request should happen per input"
11744 );
11745}
11746
11747#[gpui::test]
11748async fn test_toggle_comment(cx: &mut TestAppContext) {
11749 init_test(cx, |_| {});
11750 let mut cx = EditorTestContext::new(cx).await;
11751 let language = Arc::new(Language::new(
11752 LanguageConfig {
11753 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11754 ..Default::default()
11755 },
11756 Some(tree_sitter_rust::LANGUAGE.into()),
11757 ));
11758 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11759
11760 // If multiple selections intersect a line, the line is only toggled once.
11761 cx.set_state(indoc! {"
11762 fn a() {
11763 «//b();
11764 ˇ»// «c();
11765 //ˇ» d();
11766 }
11767 "});
11768
11769 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11770
11771 cx.assert_editor_state(indoc! {"
11772 fn a() {
11773 «b();
11774 c();
11775 ˇ» d();
11776 }
11777 "});
11778
11779 // The comment prefix is inserted at the same column for every line in a
11780 // selection.
11781 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11782
11783 cx.assert_editor_state(indoc! {"
11784 fn a() {
11785 // «b();
11786 // c();
11787 ˇ»// d();
11788 }
11789 "});
11790
11791 // If a selection ends at the beginning of a line, that line is not toggled.
11792 cx.set_selections_state(indoc! {"
11793 fn a() {
11794 // b();
11795 «// c();
11796 ˇ» // d();
11797 }
11798 "});
11799
11800 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11801
11802 cx.assert_editor_state(indoc! {"
11803 fn a() {
11804 // b();
11805 «c();
11806 ˇ» // d();
11807 }
11808 "});
11809
11810 // If a selection span a single line and is empty, the line is toggled.
11811 cx.set_state(indoc! {"
11812 fn a() {
11813 a();
11814 b();
11815 ˇ
11816 }
11817 "});
11818
11819 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11820
11821 cx.assert_editor_state(indoc! {"
11822 fn a() {
11823 a();
11824 b();
11825 //•ˇ
11826 }
11827 "});
11828
11829 // If a selection span multiple lines, empty lines are not toggled.
11830 cx.set_state(indoc! {"
11831 fn a() {
11832 «a();
11833
11834 c();ˇ»
11835 }
11836 "});
11837
11838 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11839
11840 cx.assert_editor_state(indoc! {"
11841 fn a() {
11842 // «a();
11843
11844 // c();ˇ»
11845 }
11846 "});
11847
11848 // If a selection includes multiple comment prefixes, all lines are uncommented.
11849 cx.set_state(indoc! {"
11850 fn a() {
11851 «// a();
11852 /// b();
11853 //! c();ˇ»
11854 }
11855 "});
11856
11857 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11858
11859 cx.assert_editor_state(indoc! {"
11860 fn a() {
11861 «a();
11862 b();
11863 c();ˇ»
11864 }
11865 "});
11866}
11867
11868#[gpui::test]
11869async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11870 init_test(cx, |_| {});
11871 let mut cx = EditorTestContext::new(cx).await;
11872 let language = Arc::new(Language::new(
11873 LanguageConfig {
11874 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11875 ..Default::default()
11876 },
11877 Some(tree_sitter_rust::LANGUAGE.into()),
11878 ));
11879 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11880
11881 let toggle_comments = &ToggleComments {
11882 advance_downwards: false,
11883 ignore_indent: true,
11884 };
11885
11886 // If multiple selections intersect a line, the line is only toggled once.
11887 cx.set_state(indoc! {"
11888 fn a() {
11889 // «b();
11890 // c();
11891 // ˇ» d();
11892 }
11893 "});
11894
11895 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11896
11897 cx.assert_editor_state(indoc! {"
11898 fn a() {
11899 «b();
11900 c();
11901 ˇ» d();
11902 }
11903 "});
11904
11905 // The comment prefix is inserted at the beginning of each line
11906 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11907
11908 cx.assert_editor_state(indoc! {"
11909 fn a() {
11910 // «b();
11911 // c();
11912 // ˇ» d();
11913 }
11914 "});
11915
11916 // If a selection ends at the beginning of a line, that line is not toggled.
11917 cx.set_selections_state(indoc! {"
11918 fn a() {
11919 // b();
11920 // «c();
11921 ˇ»// d();
11922 }
11923 "});
11924
11925 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11926
11927 cx.assert_editor_state(indoc! {"
11928 fn a() {
11929 // b();
11930 «c();
11931 ˇ»// d();
11932 }
11933 "});
11934
11935 // If a selection span a single line and is empty, the line is toggled.
11936 cx.set_state(indoc! {"
11937 fn a() {
11938 a();
11939 b();
11940 ˇ
11941 }
11942 "});
11943
11944 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11945
11946 cx.assert_editor_state(indoc! {"
11947 fn a() {
11948 a();
11949 b();
11950 //ˇ
11951 }
11952 "});
11953
11954 // If a selection span multiple lines, empty lines are not toggled.
11955 cx.set_state(indoc! {"
11956 fn a() {
11957 «a();
11958
11959 c();ˇ»
11960 }
11961 "});
11962
11963 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11964
11965 cx.assert_editor_state(indoc! {"
11966 fn a() {
11967 // «a();
11968
11969 // c();ˇ»
11970 }
11971 "});
11972
11973 // If a selection includes multiple comment prefixes, all lines are uncommented.
11974 cx.set_state(indoc! {"
11975 fn a() {
11976 // «a();
11977 /// b();
11978 //! c();ˇ»
11979 }
11980 "});
11981
11982 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11983
11984 cx.assert_editor_state(indoc! {"
11985 fn a() {
11986 «a();
11987 b();
11988 c();ˇ»
11989 }
11990 "});
11991}
11992
11993#[gpui::test]
11994async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11995 init_test(cx, |_| {});
11996
11997 let language = Arc::new(Language::new(
11998 LanguageConfig {
11999 line_comments: vec!["// ".into()],
12000 ..Default::default()
12001 },
12002 Some(tree_sitter_rust::LANGUAGE.into()),
12003 ));
12004
12005 let mut cx = EditorTestContext::new(cx).await;
12006
12007 cx.language_registry().add(language.clone());
12008 cx.update_buffer(|buffer, cx| {
12009 buffer.set_language(Some(language), cx);
12010 });
12011
12012 let toggle_comments = &ToggleComments {
12013 advance_downwards: true,
12014 ignore_indent: false,
12015 };
12016
12017 // Single cursor on one line -> advance
12018 // Cursor moves horizontally 3 characters as well on non-blank line
12019 cx.set_state(indoc!(
12020 "fn a() {
12021 ˇdog();
12022 cat();
12023 }"
12024 ));
12025 cx.update_editor(|editor, window, cx| {
12026 editor.toggle_comments(toggle_comments, window, cx);
12027 });
12028 cx.assert_editor_state(indoc!(
12029 "fn a() {
12030 // dog();
12031 catˇ();
12032 }"
12033 ));
12034
12035 // Single selection on one line -> don't advance
12036 cx.set_state(indoc!(
12037 "fn a() {
12038 «dog()ˇ»;
12039 cat();
12040 }"
12041 ));
12042 cx.update_editor(|editor, window, cx| {
12043 editor.toggle_comments(toggle_comments, window, cx);
12044 });
12045 cx.assert_editor_state(indoc!(
12046 "fn a() {
12047 // «dog()ˇ»;
12048 cat();
12049 }"
12050 ));
12051
12052 // Multiple cursors on one line -> advance
12053 cx.set_state(indoc!(
12054 "fn a() {
12055 ˇdˇog();
12056 cat();
12057 }"
12058 ));
12059 cx.update_editor(|editor, window, cx| {
12060 editor.toggle_comments(toggle_comments, window, cx);
12061 });
12062 cx.assert_editor_state(indoc!(
12063 "fn a() {
12064 // dog();
12065 catˇ(ˇ);
12066 }"
12067 ));
12068
12069 // Multiple cursors on one line, with selection -> don't advance
12070 cx.set_state(indoc!(
12071 "fn a() {
12072 ˇdˇog«()ˇ»;
12073 cat();
12074 }"
12075 ));
12076 cx.update_editor(|editor, window, cx| {
12077 editor.toggle_comments(toggle_comments, window, cx);
12078 });
12079 cx.assert_editor_state(indoc!(
12080 "fn a() {
12081 // ˇdˇog«()ˇ»;
12082 cat();
12083 }"
12084 ));
12085
12086 // Single cursor on one line -> advance
12087 // Cursor moves to column 0 on blank line
12088 cx.set_state(indoc!(
12089 "fn a() {
12090 ˇdog();
12091
12092 cat();
12093 }"
12094 ));
12095 cx.update_editor(|editor, window, cx| {
12096 editor.toggle_comments(toggle_comments, window, cx);
12097 });
12098 cx.assert_editor_state(indoc!(
12099 "fn a() {
12100 // dog();
12101 ˇ
12102 cat();
12103 }"
12104 ));
12105
12106 // Single cursor on one line -> advance
12107 // Cursor starts and ends at column 0
12108 cx.set_state(indoc!(
12109 "fn a() {
12110 ˇ dog();
12111 cat();
12112 }"
12113 ));
12114 cx.update_editor(|editor, window, cx| {
12115 editor.toggle_comments(toggle_comments, window, cx);
12116 });
12117 cx.assert_editor_state(indoc!(
12118 "fn a() {
12119 // dog();
12120 ˇ cat();
12121 }"
12122 ));
12123}
12124
12125#[gpui::test]
12126async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12127 init_test(cx, |_| {});
12128
12129 let mut cx = EditorTestContext::new(cx).await;
12130
12131 let html_language = Arc::new(
12132 Language::new(
12133 LanguageConfig {
12134 name: "HTML".into(),
12135 block_comment: Some(("<!-- ".into(), " -->".into())),
12136 ..Default::default()
12137 },
12138 Some(tree_sitter_html::LANGUAGE.into()),
12139 )
12140 .with_injection_query(
12141 r#"
12142 (script_element
12143 (raw_text) @injection.content
12144 (#set! injection.language "javascript"))
12145 "#,
12146 )
12147 .unwrap(),
12148 );
12149
12150 let javascript_language = Arc::new(Language::new(
12151 LanguageConfig {
12152 name: "JavaScript".into(),
12153 line_comments: vec!["// ".into()],
12154 ..Default::default()
12155 },
12156 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12157 ));
12158
12159 cx.language_registry().add(html_language.clone());
12160 cx.language_registry().add(javascript_language.clone());
12161 cx.update_buffer(|buffer, cx| {
12162 buffer.set_language(Some(html_language), cx);
12163 });
12164
12165 // Toggle comments for empty selections
12166 cx.set_state(
12167 &r#"
12168 <p>A</p>ˇ
12169 <p>B</p>ˇ
12170 <p>C</p>ˇ
12171 "#
12172 .unindent(),
12173 );
12174 cx.update_editor(|editor, window, cx| {
12175 editor.toggle_comments(&ToggleComments::default(), window, cx)
12176 });
12177 cx.assert_editor_state(
12178 &r#"
12179 <!-- <p>A</p>ˇ -->
12180 <!-- <p>B</p>ˇ -->
12181 <!-- <p>C</p>ˇ -->
12182 "#
12183 .unindent(),
12184 );
12185 cx.update_editor(|editor, window, cx| {
12186 editor.toggle_comments(&ToggleComments::default(), window, cx)
12187 });
12188 cx.assert_editor_state(
12189 &r#"
12190 <p>A</p>ˇ
12191 <p>B</p>ˇ
12192 <p>C</p>ˇ
12193 "#
12194 .unindent(),
12195 );
12196
12197 // Toggle comments for mixture of empty and non-empty selections, where
12198 // multiple selections occupy a given line.
12199 cx.set_state(
12200 &r#"
12201 <p>A«</p>
12202 <p>ˇ»B</p>ˇ
12203 <p>C«</p>
12204 <p>ˇ»D</p>ˇ
12205 "#
12206 .unindent(),
12207 );
12208
12209 cx.update_editor(|editor, window, cx| {
12210 editor.toggle_comments(&ToggleComments::default(), window, cx)
12211 });
12212 cx.assert_editor_state(
12213 &r#"
12214 <!-- <p>A«</p>
12215 <p>ˇ»B</p>ˇ -->
12216 <!-- <p>C«</p>
12217 <p>ˇ»D</p>ˇ -->
12218 "#
12219 .unindent(),
12220 );
12221 cx.update_editor(|editor, window, cx| {
12222 editor.toggle_comments(&ToggleComments::default(), window, cx)
12223 });
12224 cx.assert_editor_state(
12225 &r#"
12226 <p>A«</p>
12227 <p>ˇ»B</p>ˇ
12228 <p>C«</p>
12229 <p>ˇ»D</p>ˇ
12230 "#
12231 .unindent(),
12232 );
12233
12234 // Toggle comments when different languages are active for different
12235 // selections.
12236 cx.set_state(
12237 &r#"
12238 ˇ<script>
12239 ˇvar x = new Y();
12240 ˇ</script>
12241 "#
12242 .unindent(),
12243 );
12244 cx.executor().run_until_parked();
12245 cx.update_editor(|editor, window, cx| {
12246 editor.toggle_comments(&ToggleComments::default(), window, cx)
12247 });
12248 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12249 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12250 cx.assert_editor_state(
12251 &r#"
12252 <!-- ˇ<script> -->
12253 // ˇvar x = new Y();
12254 <!-- ˇ</script> -->
12255 "#
12256 .unindent(),
12257 );
12258}
12259
12260#[gpui::test]
12261fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12262 init_test(cx, |_| {});
12263
12264 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12265 let multibuffer = cx.new(|cx| {
12266 let mut multibuffer = MultiBuffer::new(ReadWrite);
12267 multibuffer.push_excerpts(
12268 buffer.clone(),
12269 [
12270 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12271 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12272 ],
12273 cx,
12274 );
12275 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12276 multibuffer
12277 });
12278
12279 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12280 editor.update_in(cx, |editor, window, cx| {
12281 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12282 editor.change_selections(None, window, cx, |s| {
12283 s.select_ranges([
12284 Point::new(0, 0)..Point::new(0, 0),
12285 Point::new(1, 0)..Point::new(1, 0),
12286 ])
12287 });
12288
12289 editor.handle_input("X", window, cx);
12290 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12291 assert_eq!(
12292 editor.selections.ranges(cx),
12293 [
12294 Point::new(0, 1)..Point::new(0, 1),
12295 Point::new(1, 1)..Point::new(1, 1),
12296 ]
12297 );
12298
12299 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12300 editor.change_selections(None, window, cx, |s| {
12301 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12302 });
12303 editor.backspace(&Default::default(), window, cx);
12304 assert_eq!(editor.text(cx), "Xa\nbbb");
12305 assert_eq!(
12306 editor.selections.ranges(cx),
12307 [Point::new(1, 0)..Point::new(1, 0)]
12308 );
12309
12310 editor.change_selections(None, window, cx, |s| {
12311 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12312 });
12313 editor.backspace(&Default::default(), window, cx);
12314 assert_eq!(editor.text(cx), "X\nbb");
12315 assert_eq!(
12316 editor.selections.ranges(cx),
12317 [Point::new(0, 1)..Point::new(0, 1)]
12318 );
12319 });
12320}
12321
12322#[gpui::test]
12323fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12324 init_test(cx, |_| {});
12325
12326 let markers = vec![('[', ']').into(), ('(', ')').into()];
12327 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12328 indoc! {"
12329 [aaaa
12330 (bbbb]
12331 cccc)",
12332 },
12333 markers.clone(),
12334 );
12335 let excerpt_ranges = markers.into_iter().map(|marker| {
12336 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12337 ExcerptRange::new(context.clone())
12338 });
12339 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12340 let multibuffer = cx.new(|cx| {
12341 let mut multibuffer = MultiBuffer::new(ReadWrite);
12342 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12343 multibuffer
12344 });
12345
12346 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12347 editor.update_in(cx, |editor, window, cx| {
12348 let (expected_text, selection_ranges) = marked_text_ranges(
12349 indoc! {"
12350 aaaa
12351 bˇbbb
12352 bˇbbˇb
12353 cccc"
12354 },
12355 true,
12356 );
12357 assert_eq!(editor.text(cx), expected_text);
12358 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12359
12360 editor.handle_input("X", window, cx);
12361
12362 let (expected_text, expected_selections) = marked_text_ranges(
12363 indoc! {"
12364 aaaa
12365 bXˇbbXb
12366 bXˇbbXˇb
12367 cccc"
12368 },
12369 false,
12370 );
12371 assert_eq!(editor.text(cx), expected_text);
12372 assert_eq!(editor.selections.ranges(cx), expected_selections);
12373
12374 editor.newline(&Newline, window, cx);
12375 let (expected_text, expected_selections) = marked_text_ranges(
12376 indoc! {"
12377 aaaa
12378 bX
12379 ˇbbX
12380 b
12381 bX
12382 ˇbbX
12383 ˇb
12384 cccc"
12385 },
12386 false,
12387 );
12388 assert_eq!(editor.text(cx), expected_text);
12389 assert_eq!(editor.selections.ranges(cx), expected_selections);
12390 });
12391}
12392
12393#[gpui::test]
12394fn test_refresh_selections(cx: &mut TestAppContext) {
12395 init_test(cx, |_| {});
12396
12397 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12398 let mut excerpt1_id = None;
12399 let multibuffer = cx.new(|cx| {
12400 let mut multibuffer = MultiBuffer::new(ReadWrite);
12401 excerpt1_id = multibuffer
12402 .push_excerpts(
12403 buffer.clone(),
12404 [
12405 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12406 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12407 ],
12408 cx,
12409 )
12410 .into_iter()
12411 .next();
12412 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12413 multibuffer
12414 });
12415
12416 let editor = cx.add_window(|window, cx| {
12417 let mut editor = build_editor(multibuffer.clone(), window, cx);
12418 let snapshot = editor.snapshot(window, cx);
12419 editor.change_selections(None, window, cx, |s| {
12420 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12421 });
12422 editor.begin_selection(
12423 Point::new(2, 1).to_display_point(&snapshot),
12424 true,
12425 1,
12426 window,
12427 cx,
12428 );
12429 assert_eq!(
12430 editor.selections.ranges(cx),
12431 [
12432 Point::new(1, 3)..Point::new(1, 3),
12433 Point::new(2, 1)..Point::new(2, 1),
12434 ]
12435 );
12436 editor
12437 });
12438
12439 // Refreshing selections is a no-op when excerpts haven't changed.
12440 _ = editor.update(cx, |editor, window, cx| {
12441 editor.change_selections(None, window, cx, |s| s.refresh());
12442 assert_eq!(
12443 editor.selections.ranges(cx),
12444 [
12445 Point::new(1, 3)..Point::new(1, 3),
12446 Point::new(2, 1)..Point::new(2, 1),
12447 ]
12448 );
12449 });
12450
12451 multibuffer.update(cx, |multibuffer, cx| {
12452 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12453 });
12454 _ = editor.update(cx, |editor, window, cx| {
12455 // Removing an excerpt causes the first selection to become degenerate.
12456 assert_eq!(
12457 editor.selections.ranges(cx),
12458 [
12459 Point::new(0, 0)..Point::new(0, 0),
12460 Point::new(0, 1)..Point::new(0, 1)
12461 ]
12462 );
12463
12464 // Refreshing selections will relocate the first selection to the original buffer
12465 // location.
12466 editor.change_selections(None, window, cx, |s| s.refresh());
12467 assert_eq!(
12468 editor.selections.ranges(cx),
12469 [
12470 Point::new(0, 1)..Point::new(0, 1),
12471 Point::new(0, 3)..Point::new(0, 3)
12472 ]
12473 );
12474 assert!(editor.selections.pending_anchor().is_some());
12475 });
12476}
12477
12478#[gpui::test]
12479fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12480 init_test(cx, |_| {});
12481
12482 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12483 let mut excerpt1_id = None;
12484 let multibuffer = cx.new(|cx| {
12485 let mut multibuffer = MultiBuffer::new(ReadWrite);
12486 excerpt1_id = multibuffer
12487 .push_excerpts(
12488 buffer.clone(),
12489 [
12490 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12491 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12492 ],
12493 cx,
12494 )
12495 .into_iter()
12496 .next();
12497 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12498 multibuffer
12499 });
12500
12501 let editor = cx.add_window(|window, cx| {
12502 let mut editor = build_editor(multibuffer.clone(), window, cx);
12503 let snapshot = editor.snapshot(window, cx);
12504 editor.begin_selection(
12505 Point::new(1, 3).to_display_point(&snapshot),
12506 false,
12507 1,
12508 window,
12509 cx,
12510 );
12511 assert_eq!(
12512 editor.selections.ranges(cx),
12513 [Point::new(1, 3)..Point::new(1, 3)]
12514 );
12515 editor
12516 });
12517
12518 multibuffer.update(cx, |multibuffer, cx| {
12519 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12520 });
12521 _ = editor.update(cx, |editor, window, cx| {
12522 assert_eq!(
12523 editor.selections.ranges(cx),
12524 [Point::new(0, 0)..Point::new(0, 0)]
12525 );
12526
12527 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12528 editor.change_selections(None, window, cx, |s| s.refresh());
12529 assert_eq!(
12530 editor.selections.ranges(cx),
12531 [Point::new(0, 3)..Point::new(0, 3)]
12532 );
12533 assert!(editor.selections.pending_anchor().is_some());
12534 });
12535}
12536
12537#[gpui::test]
12538async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12539 init_test(cx, |_| {});
12540
12541 let language = Arc::new(
12542 Language::new(
12543 LanguageConfig {
12544 brackets: BracketPairConfig {
12545 pairs: vec![
12546 BracketPair {
12547 start: "{".to_string(),
12548 end: "}".to_string(),
12549 close: true,
12550 surround: true,
12551 newline: true,
12552 },
12553 BracketPair {
12554 start: "/* ".to_string(),
12555 end: " */".to_string(),
12556 close: true,
12557 surround: true,
12558 newline: true,
12559 },
12560 ],
12561 ..Default::default()
12562 },
12563 ..Default::default()
12564 },
12565 Some(tree_sitter_rust::LANGUAGE.into()),
12566 )
12567 .with_indents_query("")
12568 .unwrap(),
12569 );
12570
12571 let text = concat!(
12572 "{ }\n", //
12573 " x\n", //
12574 " /* */\n", //
12575 "x\n", //
12576 "{{} }\n", //
12577 );
12578
12579 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12580 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12581 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12582 editor
12583 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12584 .await;
12585
12586 editor.update_in(cx, |editor, window, cx| {
12587 editor.change_selections(None, window, cx, |s| {
12588 s.select_display_ranges([
12589 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12590 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12591 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12592 ])
12593 });
12594 editor.newline(&Newline, window, cx);
12595
12596 assert_eq!(
12597 editor.buffer().read(cx).read(cx).text(),
12598 concat!(
12599 "{ \n", // Suppress rustfmt
12600 "\n", //
12601 "}\n", //
12602 " x\n", //
12603 " /* \n", //
12604 " \n", //
12605 " */\n", //
12606 "x\n", //
12607 "{{} \n", //
12608 "}\n", //
12609 )
12610 );
12611 });
12612}
12613
12614#[gpui::test]
12615fn test_highlighted_ranges(cx: &mut TestAppContext) {
12616 init_test(cx, |_| {});
12617
12618 let editor = cx.add_window(|window, cx| {
12619 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12620 build_editor(buffer.clone(), window, cx)
12621 });
12622
12623 _ = editor.update(cx, |editor, window, cx| {
12624 struct Type1;
12625 struct Type2;
12626
12627 let buffer = editor.buffer.read(cx).snapshot(cx);
12628
12629 let anchor_range =
12630 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12631
12632 editor.highlight_background::<Type1>(
12633 &[
12634 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12635 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12636 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12637 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12638 ],
12639 |_| Hsla::red(),
12640 cx,
12641 );
12642 editor.highlight_background::<Type2>(
12643 &[
12644 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12645 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12646 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12647 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12648 ],
12649 |_| Hsla::green(),
12650 cx,
12651 );
12652
12653 let snapshot = editor.snapshot(window, cx);
12654 let mut highlighted_ranges = editor.background_highlights_in_range(
12655 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12656 &snapshot,
12657 cx.theme().colors(),
12658 );
12659 // Enforce a consistent ordering based on color without relying on the ordering of the
12660 // highlight's `TypeId` which is non-executor.
12661 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12662 assert_eq!(
12663 highlighted_ranges,
12664 &[
12665 (
12666 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12667 Hsla::red(),
12668 ),
12669 (
12670 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12671 Hsla::red(),
12672 ),
12673 (
12674 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12675 Hsla::green(),
12676 ),
12677 (
12678 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12679 Hsla::green(),
12680 ),
12681 ]
12682 );
12683 assert_eq!(
12684 editor.background_highlights_in_range(
12685 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12686 &snapshot,
12687 cx.theme().colors(),
12688 ),
12689 &[(
12690 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12691 Hsla::red(),
12692 )]
12693 );
12694 });
12695}
12696
12697#[gpui::test]
12698async fn test_following(cx: &mut TestAppContext) {
12699 init_test(cx, |_| {});
12700
12701 let fs = FakeFs::new(cx.executor());
12702 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12703
12704 let buffer = project.update(cx, |project, cx| {
12705 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12706 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12707 });
12708 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12709 let follower = cx.update(|cx| {
12710 cx.open_window(
12711 WindowOptions {
12712 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12713 gpui::Point::new(px(0.), px(0.)),
12714 gpui::Point::new(px(10.), px(80.)),
12715 ))),
12716 ..Default::default()
12717 },
12718 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12719 )
12720 .unwrap()
12721 });
12722
12723 let is_still_following = Rc::new(RefCell::new(true));
12724 let follower_edit_event_count = Rc::new(RefCell::new(0));
12725 let pending_update = Rc::new(RefCell::new(None));
12726 let leader_entity = leader.root(cx).unwrap();
12727 let follower_entity = follower.root(cx).unwrap();
12728 _ = follower.update(cx, {
12729 let update = pending_update.clone();
12730 let is_still_following = is_still_following.clone();
12731 let follower_edit_event_count = follower_edit_event_count.clone();
12732 |_, window, cx| {
12733 cx.subscribe_in(
12734 &leader_entity,
12735 window,
12736 move |_, leader, event, window, cx| {
12737 leader.read(cx).add_event_to_update_proto(
12738 event,
12739 &mut update.borrow_mut(),
12740 window,
12741 cx,
12742 );
12743 },
12744 )
12745 .detach();
12746
12747 cx.subscribe_in(
12748 &follower_entity,
12749 window,
12750 move |_, _, event: &EditorEvent, _window, _cx| {
12751 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12752 *is_still_following.borrow_mut() = false;
12753 }
12754
12755 if let EditorEvent::BufferEdited = event {
12756 *follower_edit_event_count.borrow_mut() += 1;
12757 }
12758 },
12759 )
12760 .detach();
12761 }
12762 });
12763
12764 // Update the selections only
12765 _ = leader.update(cx, |leader, window, cx| {
12766 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12767 });
12768 follower
12769 .update(cx, |follower, window, cx| {
12770 follower.apply_update_proto(
12771 &project,
12772 pending_update.borrow_mut().take().unwrap(),
12773 window,
12774 cx,
12775 )
12776 })
12777 .unwrap()
12778 .await
12779 .unwrap();
12780 _ = follower.update(cx, |follower, _, cx| {
12781 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12782 });
12783 assert!(*is_still_following.borrow());
12784 assert_eq!(*follower_edit_event_count.borrow(), 0);
12785
12786 // Update the scroll position only
12787 _ = leader.update(cx, |leader, window, cx| {
12788 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12789 });
12790 follower
12791 .update(cx, |follower, window, cx| {
12792 follower.apply_update_proto(
12793 &project,
12794 pending_update.borrow_mut().take().unwrap(),
12795 window,
12796 cx,
12797 )
12798 })
12799 .unwrap()
12800 .await
12801 .unwrap();
12802 assert_eq!(
12803 follower
12804 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12805 .unwrap(),
12806 gpui::Point::new(1.5, 3.5)
12807 );
12808 assert!(*is_still_following.borrow());
12809 assert_eq!(*follower_edit_event_count.borrow(), 0);
12810
12811 // Update the selections and scroll position. The follower's scroll position is updated
12812 // via autoscroll, not via the leader's exact scroll position.
12813 _ = leader.update(cx, |leader, window, cx| {
12814 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12815 leader.request_autoscroll(Autoscroll::newest(), cx);
12816 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12817 });
12818 follower
12819 .update(cx, |follower, window, cx| {
12820 follower.apply_update_proto(
12821 &project,
12822 pending_update.borrow_mut().take().unwrap(),
12823 window,
12824 cx,
12825 )
12826 })
12827 .unwrap()
12828 .await
12829 .unwrap();
12830 _ = follower.update(cx, |follower, _, cx| {
12831 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12832 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12833 });
12834 assert!(*is_still_following.borrow());
12835
12836 // Creating a pending selection that precedes another selection
12837 _ = leader.update(cx, |leader, window, cx| {
12838 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12839 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12840 });
12841 follower
12842 .update(cx, |follower, window, cx| {
12843 follower.apply_update_proto(
12844 &project,
12845 pending_update.borrow_mut().take().unwrap(),
12846 window,
12847 cx,
12848 )
12849 })
12850 .unwrap()
12851 .await
12852 .unwrap();
12853 _ = follower.update(cx, |follower, _, cx| {
12854 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12855 });
12856 assert!(*is_still_following.borrow());
12857
12858 // Extend the pending selection so that it surrounds another selection
12859 _ = leader.update(cx, |leader, window, cx| {
12860 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12861 });
12862 follower
12863 .update(cx, |follower, window, cx| {
12864 follower.apply_update_proto(
12865 &project,
12866 pending_update.borrow_mut().take().unwrap(),
12867 window,
12868 cx,
12869 )
12870 })
12871 .unwrap()
12872 .await
12873 .unwrap();
12874 _ = follower.update(cx, |follower, _, cx| {
12875 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12876 });
12877
12878 // Scrolling locally breaks the follow
12879 _ = follower.update(cx, |follower, window, cx| {
12880 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12881 follower.set_scroll_anchor(
12882 ScrollAnchor {
12883 anchor: top_anchor,
12884 offset: gpui::Point::new(0.0, 0.5),
12885 },
12886 window,
12887 cx,
12888 );
12889 });
12890 assert!(!(*is_still_following.borrow()));
12891}
12892
12893#[gpui::test]
12894async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12895 init_test(cx, |_| {});
12896
12897 let fs = FakeFs::new(cx.executor());
12898 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12899 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12900 let pane = workspace
12901 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12902 .unwrap();
12903
12904 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12905
12906 let leader = pane.update_in(cx, |_, window, cx| {
12907 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12908 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12909 });
12910
12911 // Start following the editor when it has no excerpts.
12912 let mut state_message =
12913 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12914 let workspace_entity = workspace.root(cx).unwrap();
12915 let follower_1 = cx
12916 .update_window(*workspace.deref(), |_, window, cx| {
12917 Editor::from_state_proto(
12918 workspace_entity,
12919 ViewId {
12920 creator: CollaboratorId::PeerId(PeerId::default()),
12921 id: 0,
12922 },
12923 &mut state_message,
12924 window,
12925 cx,
12926 )
12927 })
12928 .unwrap()
12929 .unwrap()
12930 .await
12931 .unwrap();
12932
12933 let update_message = Rc::new(RefCell::new(None));
12934 follower_1.update_in(cx, {
12935 let update = update_message.clone();
12936 |_, window, cx| {
12937 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12938 leader.read(cx).add_event_to_update_proto(
12939 event,
12940 &mut update.borrow_mut(),
12941 window,
12942 cx,
12943 );
12944 })
12945 .detach();
12946 }
12947 });
12948
12949 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12950 (
12951 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12952 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12953 )
12954 });
12955
12956 // Insert some excerpts.
12957 leader.update(cx, |leader, cx| {
12958 leader.buffer.update(cx, |multibuffer, cx| {
12959 multibuffer.set_excerpts_for_path(
12960 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
12961 buffer_1.clone(),
12962 vec![
12963 Point::row_range(0..3),
12964 Point::row_range(1..6),
12965 Point::row_range(12..15),
12966 ],
12967 0,
12968 cx,
12969 );
12970 multibuffer.set_excerpts_for_path(
12971 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
12972 buffer_2.clone(),
12973 vec![Point::row_range(0..6), Point::row_range(8..12)],
12974 0,
12975 cx,
12976 );
12977 });
12978 });
12979
12980 // Apply the update of adding the excerpts.
12981 follower_1
12982 .update_in(cx, |follower, window, cx| {
12983 follower.apply_update_proto(
12984 &project,
12985 update_message.borrow().clone().unwrap(),
12986 window,
12987 cx,
12988 )
12989 })
12990 .await
12991 .unwrap();
12992 assert_eq!(
12993 follower_1.update(cx, |editor, cx| editor.text(cx)),
12994 leader.update(cx, |editor, cx| editor.text(cx))
12995 );
12996 update_message.borrow_mut().take();
12997
12998 // Start following separately after it already has excerpts.
12999 let mut state_message =
13000 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13001 let workspace_entity = workspace.root(cx).unwrap();
13002 let follower_2 = cx
13003 .update_window(*workspace.deref(), |_, window, cx| {
13004 Editor::from_state_proto(
13005 workspace_entity,
13006 ViewId {
13007 creator: CollaboratorId::PeerId(PeerId::default()),
13008 id: 0,
13009 },
13010 &mut state_message,
13011 window,
13012 cx,
13013 )
13014 })
13015 .unwrap()
13016 .unwrap()
13017 .await
13018 .unwrap();
13019 assert_eq!(
13020 follower_2.update(cx, |editor, cx| editor.text(cx)),
13021 leader.update(cx, |editor, cx| editor.text(cx))
13022 );
13023
13024 // Remove some excerpts.
13025 leader.update(cx, |leader, cx| {
13026 leader.buffer.update(cx, |multibuffer, cx| {
13027 let excerpt_ids = multibuffer.excerpt_ids();
13028 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13029 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13030 });
13031 });
13032
13033 // Apply the update of removing the excerpts.
13034 follower_1
13035 .update_in(cx, |follower, window, cx| {
13036 follower.apply_update_proto(
13037 &project,
13038 update_message.borrow().clone().unwrap(),
13039 window,
13040 cx,
13041 )
13042 })
13043 .await
13044 .unwrap();
13045 follower_2
13046 .update_in(cx, |follower, window, cx| {
13047 follower.apply_update_proto(
13048 &project,
13049 update_message.borrow().clone().unwrap(),
13050 window,
13051 cx,
13052 )
13053 })
13054 .await
13055 .unwrap();
13056 update_message.borrow_mut().take();
13057 assert_eq!(
13058 follower_1.update(cx, |editor, cx| editor.text(cx)),
13059 leader.update(cx, |editor, cx| editor.text(cx))
13060 );
13061}
13062
13063#[gpui::test]
13064async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13065 init_test(cx, |_| {});
13066
13067 let mut cx = EditorTestContext::new(cx).await;
13068 let lsp_store =
13069 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13070
13071 cx.set_state(indoc! {"
13072 ˇfn func(abc def: i32) -> u32 {
13073 }
13074 "});
13075
13076 cx.update(|_, cx| {
13077 lsp_store.update(cx, |lsp_store, cx| {
13078 lsp_store
13079 .update_diagnostics(
13080 LanguageServerId(0),
13081 lsp::PublishDiagnosticsParams {
13082 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13083 version: None,
13084 diagnostics: vec![
13085 lsp::Diagnostic {
13086 range: lsp::Range::new(
13087 lsp::Position::new(0, 11),
13088 lsp::Position::new(0, 12),
13089 ),
13090 severity: Some(lsp::DiagnosticSeverity::ERROR),
13091 ..Default::default()
13092 },
13093 lsp::Diagnostic {
13094 range: lsp::Range::new(
13095 lsp::Position::new(0, 12),
13096 lsp::Position::new(0, 15),
13097 ),
13098 severity: Some(lsp::DiagnosticSeverity::ERROR),
13099 ..Default::default()
13100 },
13101 lsp::Diagnostic {
13102 range: lsp::Range::new(
13103 lsp::Position::new(0, 25),
13104 lsp::Position::new(0, 28),
13105 ),
13106 severity: Some(lsp::DiagnosticSeverity::ERROR),
13107 ..Default::default()
13108 },
13109 ],
13110 },
13111 &[],
13112 cx,
13113 )
13114 .unwrap()
13115 });
13116 });
13117
13118 executor.run_until_parked();
13119
13120 cx.update_editor(|editor, window, cx| {
13121 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13122 });
13123
13124 cx.assert_editor_state(indoc! {"
13125 fn func(abc def: i32) -> ˇu32 {
13126 }
13127 "});
13128
13129 cx.update_editor(|editor, window, cx| {
13130 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13131 });
13132
13133 cx.assert_editor_state(indoc! {"
13134 fn func(abc ˇdef: i32) -> u32 {
13135 }
13136 "});
13137
13138 cx.update_editor(|editor, window, cx| {
13139 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13140 });
13141
13142 cx.assert_editor_state(indoc! {"
13143 fn func(abcˇ def: i32) -> u32 {
13144 }
13145 "});
13146
13147 cx.update_editor(|editor, window, cx| {
13148 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13149 });
13150
13151 cx.assert_editor_state(indoc! {"
13152 fn func(abc def: i32) -> ˇu32 {
13153 }
13154 "});
13155}
13156
13157#[gpui::test]
13158async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13159 init_test(cx, |_| {});
13160
13161 let mut cx = EditorTestContext::new(cx).await;
13162
13163 let diff_base = r#"
13164 use some::mod;
13165
13166 const A: u32 = 42;
13167
13168 fn main() {
13169 println!("hello");
13170
13171 println!("world");
13172 }
13173 "#
13174 .unindent();
13175
13176 // Edits are modified, removed, modified, added
13177 cx.set_state(
13178 &r#"
13179 use some::modified;
13180
13181 ˇ
13182 fn main() {
13183 println!("hello there");
13184
13185 println!("around the");
13186 println!("world");
13187 }
13188 "#
13189 .unindent(),
13190 );
13191
13192 cx.set_head_text(&diff_base);
13193 executor.run_until_parked();
13194
13195 cx.update_editor(|editor, window, cx| {
13196 //Wrap around the bottom of the buffer
13197 for _ in 0..3 {
13198 editor.go_to_next_hunk(&GoToHunk, window, cx);
13199 }
13200 });
13201
13202 cx.assert_editor_state(
13203 &r#"
13204 ˇuse some::modified;
13205
13206
13207 fn main() {
13208 println!("hello there");
13209
13210 println!("around the");
13211 println!("world");
13212 }
13213 "#
13214 .unindent(),
13215 );
13216
13217 cx.update_editor(|editor, window, cx| {
13218 //Wrap around the top of the buffer
13219 for _ in 0..2 {
13220 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13221 }
13222 });
13223
13224 cx.assert_editor_state(
13225 &r#"
13226 use some::modified;
13227
13228
13229 fn main() {
13230 ˇ println!("hello there");
13231
13232 println!("around the");
13233 println!("world");
13234 }
13235 "#
13236 .unindent(),
13237 );
13238
13239 cx.update_editor(|editor, window, cx| {
13240 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13241 });
13242
13243 cx.assert_editor_state(
13244 &r#"
13245 use some::modified;
13246
13247 ˇ
13248 fn main() {
13249 println!("hello there");
13250
13251 println!("around the");
13252 println!("world");
13253 }
13254 "#
13255 .unindent(),
13256 );
13257
13258 cx.update_editor(|editor, window, cx| {
13259 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13260 });
13261
13262 cx.assert_editor_state(
13263 &r#"
13264 ˇuse some::modified;
13265
13266
13267 fn main() {
13268 println!("hello there");
13269
13270 println!("around the");
13271 println!("world");
13272 }
13273 "#
13274 .unindent(),
13275 );
13276
13277 cx.update_editor(|editor, window, cx| {
13278 for _ in 0..2 {
13279 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13280 }
13281 });
13282
13283 cx.assert_editor_state(
13284 &r#"
13285 use some::modified;
13286
13287
13288 fn main() {
13289 ˇ println!("hello there");
13290
13291 println!("around the");
13292 println!("world");
13293 }
13294 "#
13295 .unindent(),
13296 );
13297
13298 cx.update_editor(|editor, window, cx| {
13299 editor.fold(&Fold, window, cx);
13300 });
13301
13302 cx.update_editor(|editor, window, cx| {
13303 editor.go_to_next_hunk(&GoToHunk, window, cx);
13304 });
13305
13306 cx.assert_editor_state(
13307 &r#"
13308 ˇuse some::modified;
13309
13310
13311 fn main() {
13312 println!("hello there");
13313
13314 println!("around the");
13315 println!("world");
13316 }
13317 "#
13318 .unindent(),
13319 );
13320}
13321
13322#[test]
13323fn test_split_words() {
13324 fn split(text: &str) -> Vec<&str> {
13325 split_words(text).collect()
13326 }
13327
13328 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13329 assert_eq!(split("hello_world"), &["hello_", "world"]);
13330 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13331 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13332 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13333 assert_eq!(split("helloworld"), &["helloworld"]);
13334
13335 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13336}
13337
13338#[gpui::test]
13339async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13340 init_test(cx, |_| {});
13341
13342 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13343 let mut assert = |before, after| {
13344 let _state_context = cx.set_state(before);
13345 cx.run_until_parked();
13346 cx.update_editor(|editor, window, cx| {
13347 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13348 });
13349 cx.run_until_parked();
13350 cx.assert_editor_state(after);
13351 };
13352
13353 // Outside bracket jumps to outside of matching bracket
13354 assert("console.logˇ(var);", "console.log(var)ˇ;");
13355 assert("console.log(var)ˇ;", "console.logˇ(var);");
13356
13357 // Inside bracket jumps to inside of matching bracket
13358 assert("console.log(ˇvar);", "console.log(varˇ);");
13359 assert("console.log(varˇ);", "console.log(ˇvar);");
13360
13361 // When outside a bracket and inside, favor jumping to the inside bracket
13362 assert(
13363 "console.log('foo', [1, 2, 3]ˇ);",
13364 "console.log(ˇ'foo', [1, 2, 3]);",
13365 );
13366 assert(
13367 "console.log(ˇ'foo', [1, 2, 3]);",
13368 "console.log('foo', [1, 2, 3]ˇ);",
13369 );
13370
13371 // Bias forward if two options are equally likely
13372 assert(
13373 "let result = curried_fun()ˇ();",
13374 "let result = curried_fun()()ˇ;",
13375 );
13376
13377 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13378 assert(
13379 indoc! {"
13380 function test() {
13381 console.log('test')ˇ
13382 }"},
13383 indoc! {"
13384 function test() {
13385 console.logˇ('test')
13386 }"},
13387 );
13388}
13389
13390#[gpui::test]
13391async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13392 init_test(cx, |_| {});
13393
13394 let fs = FakeFs::new(cx.executor());
13395 fs.insert_tree(
13396 path!("/a"),
13397 json!({
13398 "main.rs": "fn main() { let a = 5; }",
13399 "other.rs": "// Test file",
13400 }),
13401 )
13402 .await;
13403 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13404
13405 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13406 language_registry.add(Arc::new(Language::new(
13407 LanguageConfig {
13408 name: "Rust".into(),
13409 matcher: LanguageMatcher {
13410 path_suffixes: vec!["rs".to_string()],
13411 ..Default::default()
13412 },
13413 brackets: BracketPairConfig {
13414 pairs: vec![BracketPair {
13415 start: "{".to_string(),
13416 end: "}".to_string(),
13417 close: true,
13418 surround: true,
13419 newline: true,
13420 }],
13421 disabled_scopes_by_bracket_ix: Vec::new(),
13422 },
13423 ..Default::default()
13424 },
13425 Some(tree_sitter_rust::LANGUAGE.into()),
13426 )));
13427 let mut fake_servers = language_registry.register_fake_lsp(
13428 "Rust",
13429 FakeLspAdapter {
13430 capabilities: lsp::ServerCapabilities {
13431 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13432 first_trigger_character: "{".to_string(),
13433 more_trigger_character: None,
13434 }),
13435 ..Default::default()
13436 },
13437 ..Default::default()
13438 },
13439 );
13440
13441 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13442
13443 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13444
13445 let worktree_id = workspace
13446 .update(cx, |workspace, _, cx| {
13447 workspace.project().update(cx, |project, cx| {
13448 project.worktrees(cx).next().unwrap().read(cx).id()
13449 })
13450 })
13451 .unwrap();
13452
13453 let buffer = project
13454 .update(cx, |project, cx| {
13455 project.open_local_buffer(path!("/a/main.rs"), cx)
13456 })
13457 .await
13458 .unwrap();
13459 let editor_handle = workspace
13460 .update(cx, |workspace, window, cx| {
13461 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13462 })
13463 .unwrap()
13464 .await
13465 .unwrap()
13466 .downcast::<Editor>()
13467 .unwrap();
13468
13469 cx.executor().start_waiting();
13470 let fake_server = fake_servers.next().await.unwrap();
13471
13472 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13473 |params, _| async move {
13474 assert_eq!(
13475 params.text_document_position.text_document.uri,
13476 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13477 );
13478 assert_eq!(
13479 params.text_document_position.position,
13480 lsp::Position::new(0, 21),
13481 );
13482
13483 Ok(Some(vec![lsp::TextEdit {
13484 new_text: "]".to_string(),
13485 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13486 }]))
13487 },
13488 );
13489
13490 editor_handle.update_in(cx, |editor, window, cx| {
13491 window.focus(&editor.focus_handle(cx));
13492 editor.change_selections(None, window, cx, |s| {
13493 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13494 });
13495 editor.handle_input("{", window, cx);
13496 });
13497
13498 cx.executor().run_until_parked();
13499
13500 buffer.update(cx, |buffer, _| {
13501 assert_eq!(
13502 buffer.text(),
13503 "fn main() { let a = {5}; }",
13504 "No extra braces from on type formatting should appear in the buffer"
13505 )
13506 });
13507}
13508
13509#[gpui::test]
13510async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13511 init_test(cx, |_| {});
13512
13513 let fs = FakeFs::new(cx.executor());
13514 fs.insert_tree(
13515 path!("/a"),
13516 json!({
13517 "main.rs": "fn main() { let a = 5; }",
13518 "other.rs": "// Test file",
13519 }),
13520 )
13521 .await;
13522
13523 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13524
13525 let server_restarts = Arc::new(AtomicUsize::new(0));
13526 let closure_restarts = Arc::clone(&server_restarts);
13527 let language_server_name = "test language server";
13528 let language_name: LanguageName = "Rust".into();
13529
13530 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13531 language_registry.add(Arc::new(Language::new(
13532 LanguageConfig {
13533 name: language_name.clone(),
13534 matcher: LanguageMatcher {
13535 path_suffixes: vec!["rs".to_string()],
13536 ..Default::default()
13537 },
13538 ..Default::default()
13539 },
13540 Some(tree_sitter_rust::LANGUAGE.into()),
13541 )));
13542 let mut fake_servers = language_registry.register_fake_lsp(
13543 "Rust",
13544 FakeLspAdapter {
13545 name: language_server_name,
13546 initialization_options: Some(json!({
13547 "testOptionValue": true
13548 })),
13549 initializer: Some(Box::new(move |fake_server| {
13550 let task_restarts = Arc::clone(&closure_restarts);
13551 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13552 task_restarts.fetch_add(1, atomic::Ordering::Release);
13553 futures::future::ready(Ok(()))
13554 });
13555 })),
13556 ..Default::default()
13557 },
13558 );
13559
13560 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13561 let _buffer = project
13562 .update(cx, |project, cx| {
13563 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13564 })
13565 .await
13566 .unwrap();
13567 let _fake_server = fake_servers.next().await.unwrap();
13568 update_test_language_settings(cx, |language_settings| {
13569 language_settings.languages.insert(
13570 language_name.clone(),
13571 LanguageSettingsContent {
13572 tab_size: NonZeroU32::new(8),
13573 ..Default::default()
13574 },
13575 );
13576 });
13577 cx.executor().run_until_parked();
13578 assert_eq!(
13579 server_restarts.load(atomic::Ordering::Acquire),
13580 0,
13581 "Should not restart LSP server on an unrelated change"
13582 );
13583
13584 update_test_project_settings(cx, |project_settings| {
13585 project_settings.lsp.insert(
13586 "Some other server name".into(),
13587 LspSettings {
13588 binary: None,
13589 settings: None,
13590 initialization_options: Some(json!({
13591 "some other init value": false
13592 })),
13593 enable_lsp_tasks: false,
13594 },
13595 );
13596 });
13597 cx.executor().run_until_parked();
13598 assert_eq!(
13599 server_restarts.load(atomic::Ordering::Acquire),
13600 0,
13601 "Should not restart LSP server on an unrelated LSP settings change"
13602 );
13603
13604 update_test_project_settings(cx, |project_settings| {
13605 project_settings.lsp.insert(
13606 language_server_name.into(),
13607 LspSettings {
13608 binary: None,
13609 settings: None,
13610 initialization_options: Some(json!({
13611 "anotherInitValue": false
13612 })),
13613 enable_lsp_tasks: false,
13614 },
13615 );
13616 });
13617 cx.executor().run_until_parked();
13618 assert_eq!(
13619 server_restarts.load(atomic::Ordering::Acquire),
13620 1,
13621 "Should restart LSP server on a related LSP settings change"
13622 );
13623
13624 update_test_project_settings(cx, |project_settings| {
13625 project_settings.lsp.insert(
13626 language_server_name.into(),
13627 LspSettings {
13628 binary: None,
13629 settings: None,
13630 initialization_options: Some(json!({
13631 "anotherInitValue": false
13632 })),
13633 enable_lsp_tasks: false,
13634 },
13635 );
13636 });
13637 cx.executor().run_until_parked();
13638 assert_eq!(
13639 server_restarts.load(atomic::Ordering::Acquire),
13640 1,
13641 "Should not restart LSP server on a related LSP settings change that is the same"
13642 );
13643
13644 update_test_project_settings(cx, |project_settings| {
13645 project_settings.lsp.insert(
13646 language_server_name.into(),
13647 LspSettings {
13648 binary: None,
13649 settings: None,
13650 initialization_options: None,
13651 enable_lsp_tasks: false,
13652 },
13653 );
13654 });
13655 cx.executor().run_until_parked();
13656 assert_eq!(
13657 server_restarts.load(atomic::Ordering::Acquire),
13658 2,
13659 "Should restart LSP server on another related LSP settings change"
13660 );
13661}
13662
13663#[gpui::test]
13664async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13665 init_test(cx, |_| {});
13666
13667 let mut cx = EditorLspTestContext::new_rust(
13668 lsp::ServerCapabilities {
13669 completion_provider: Some(lsp::CompletionOptions {
13670 trigger_characters: Some(vec![".".to_string()]),
13671 resolve_provider: Some(true),
13672 ..Default::default()
13673 }),
13674 ..Default::default()
13675 },
13676 cx,
13677 )
13678 .await;
13679
13680 cx.set_state("fn main() { let a = 2ˇ; }");
13681 cx.simulate_keystroke(".");
13682 let completion_item = lsp::CompletionItem {
13683 label: "some".into(),
13684 kind: Some(lsp::CompletionItemKind::SNIPPET),
13685 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13686 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13687 kind: lsp::MarkupKind::Markdown,
13688 value: "```rust\nSome(2)\n```".to_string(),
13689 })),
13690 deprecated: Some(false),
13691 sort_text: Some("fffffff2".to_string()),
13692 filter_text: Some("some".to_string()),
13693 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13694 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13695 range: lsp::Range {
13696 start: lsp::Position {
13697 line: 0,
13698 character: 22,
13699 },
13700 end: lsp::Position {
13701 line: 0,
13702 character: 22,
13703 },
13704 },
13705 new_text: "Some(2)".to_string(),
13706 })),
13707 additional_text_edits: Some(vec![lsp::TextEdit {
13708 range: lsp::Range {
13709 start: lsp::Position {
13710 line: 0,
13711 character: 20,
13712 },
13713 end: lsp::Position {
13714 line: 0,
13715 character: 22,
13716 },
13717 },
13718 new_text: "".to_string(),
13719 }]),
13720 ..Default::default()
13721 };
13722
13723 let closure_completion_item = completion_item.clone();
13724 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13725 let task_completion_item = closure_completion_item.clone();
13726 async move {
13727 Ok(Some(lsp::CompletionResponse::Array(vec![
13728 task_completion_item,
13729 ])))
13730 }
13731 });
13732
13733 request.next().await;
13734
13735 cx.condition(|editor, _| editor.context_menu_visible())
13736 .await;
13737 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13738 editor
13739 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13740 .unwrap()
13741 });
13742 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13743
13744 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13745 let task_completion_item = completion_item.clone();
13746 async move { Ok(task_completion_item) }
13747 })
13748 .next()
13749 .await
13750 .unwrap();
13751 apply_additional_edits.await.unwrap();
13752 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13753}
13754
13755#[gpui::test]
13756async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13757 init_test(cx, |_| {});
13758
13759 let mut cx = EditorLspTestContext::new_rust(
13760 lsp::ServerCapabilities {
13761 completion_provider: Some(lsp::CompletionOptions {
13762 trigger_characters: Some(vec![".".to_string()]),
13763 resolve_provider: Some(true),
13764 ..Default::default()
13765 }),
13766 ..Default::default()
13767 },
13768 cx,
13769 )
13770 .await;
13771
13772 cx.set_state("fn main() { let a = 2ˇ; }");
13773 cx.simulate_keystroke(".");
13774
13775 let item1 = lsp::CompletionItem {
13776 label: "method id()".to_string(),
13777 filter_text: Some("id".to_string()),
13778 detail: None,
13779 documentation: None,
13780 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13781 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13782 new_text: ".id".to_string(),
13783 })),
13784 ..lsp::CompletionItem::default()
13785 };
13786
13787 let item2 = lsp::CompletionItem {
13788 label: "other".to_string(),
13789 filter_text: Some("other".to_string()),
13790 detail: None,
13791 documentation: None,
13792 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13793 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13794 new_text: ".other".to_string(),
13795 })),
13796 ..lsp::CompletionItem::default()
13797 };
13798
13799 let item1 = item1.clone();
13800 cx.set_request_handler::<lsp::request::Completion, _, _>({
13801 let item1 = item1.clone();
13802 move |_, _, _| {
13803 let item1 = item1.clone();
13804 let item2 = item2.clone();
13805 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13806 }
13807 })
13808 .next()
13809 .await;
13810
13811 cx.condition(|editor, _| editor.context_menu_visible())
13812 .await;
13813 cx.update_editor(|editor, _, _| {
13814 let context_menu = editor.context_menu.borrow_mut();
13815 let context_menu = context_menu
13816 .as_ref()
13817 .expect("Should have the context menu deployed");
13818 match context_menu {
13819 CodeContextMenu::Completions(completions_menu) => {
13820 let completions = completions_menu.completions.borrow_mut();
13821 assert_eq!(
13822 completions
13823 .iter()
13824 .map(|completion| &completion.label.text)
13825 .collect::<Vec<_>>(),
13826 vec!["method id()", "other"]
13827 )
13828 }
13829 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13830 }
13831 });
13832
13833 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13834 let item1 = item1.clone();
13835 move |_, item_to_resolve, _| {
13836 let item1 = item1.clone();
13837 async move {
13838 if item1 == item_to_resolve {
13839 Ok(lsp::CompletionItem {
13840 label: "method id()".to_string(),
13841 filter_text: Some("id".to_string()),
13842 detail: Some("Now resolved!".to_string()),
13843 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13844 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13845 range: lsp::Range::new(
13846 lsp::Position::new(0, 22),
13847 lsp::Position::new(0, 22),
13848 ),
13849 new_text: ".id".to_string(),
13850 })),
13851 ..lsp::CompletionItem::default()
13852 })
13853 } else {
13854 Ok(item_to_resolve)
13855 }
13856 }
13857 }
13858 })
13859 .next()
13860 .await
13861 .unwrap();
13862 cx.run_until_parked();
13863
13864 cx.update_editor(|editor, window, cx| {
13865 editor.context_menu_next(&Default::default(), window, cx);
13866 });
13867
13868 cx.update_editor(|editor, _, _| {
13869 let context_menu = editor.context_menu.borrow_mut();
13870 let context_menu = context_menu
13871 .as_ref()
13872 .expect("Should have the context menu deployed");
13873 match context_menu {
13874 CodeContextMenu::Completions(completions_menu) => {
13875 let completions = completions_menu.completions.borrow_mut();
13876 assert_eq!(
13877 completions
13878 .iter()
13879 .map(|completion| &completion.label.text)
13880 .collect::<Vec<_>>(),
13881 vec!["method id() Now resolved!", "other"],
13882 "Should update first completion label, but not second as the filter text did not match."
13883 );
13884 }
13885 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13886 }
13887 });
13888}
13889
13890#[gpui::test]
13891async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13892 init_test(cx, |_| {});
13893
13894 let mut cx = EditorLspTestContext::new_rust(
13895 lsp::ServerCapabilities {
13896 completion_provider: Some(lsp::CompletionOptions {
13897 trigger_characters: Some(vec![".".to_string()]),
13898 resolve_provider: Some(true),
13899 ..Default::default()
13900 }),
13901 ..Default::default()
13902 },
13903 cx,
13904 )
13905 .await;
13906
13907 cx.set_state("fn main() { let a = 2ˇ; }");
13908 cx.simulate_keystroke(".");
13909
13910 let unresolved_item_1 = lsp::CompletionItem {
13911 label: "id".to_string(),
13912 filter_text: Some("id".to_string()),
13913 detail: None,
13914 documentation: None,
13915 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13916 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13917 new_text: ".id".to_string(),
13918 })),
13919 ..lsp::CompletionItem::default()
13920 };
13921 let resolved_item_1 = lsp::CompletionItem {
13922 additional_text_edits: Some(vec![lsp::TextEdit {
13923 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13924 new_text: "!!".to_string(),
13925 }]),
13926 ..unresolved_item_1.clone()
13927 };
13928 let unresolved_item_2 = lsp::CompletionItem {
13929 label: "other".to_string(),
13930 filter_text: Some("other".to_string()),
13931 detail: None,
13932 documentation: None,
13933 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13934 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13935 new_text: ".other".to_string(),
13936 })),
13937 ..lsp::CompletionItem::default()
13938 };
13939 let resolved_item_2 = lsp::CompletionItem {
13940 additional_text_edits: Some(vec![lsp::TextEdit {
13941 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13942 new_text: "??".to_string(),
13943 }]),
13944 ..unresolved_item_2.clone()
13945 };
13946
13947 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13948 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13949 cx.lsp
13950 .server
13951 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13952 let unresolved_item_1 = unresolved_item_1.clone();
13953 let resolved_item_1 = resolved_item_1.clone();
13954 let unresolved_item_2 = unresolved_item_2.clone();
13955 let resolved_item_2 = resolved_item_2.clone();
13956 let resolve_requests_1 = resolve_requests_1.clone();
13957 let resolve_requests_2 = resolve_requests_2.clone();
13958 move |unresolved_request, _| {
13959 let unresolved_item_1 = unresolved_item_1.clone();
13960 let resolved_item_1 = resolved_item_1.clone();
13961 let unresolved_item_2 = unresolved_item_2.clone();
13962 let resolved_item_2 = resolved_item_2.clone();
13963 let resolve_requests_1 = resolve_requests_1.clone();
13964 let resolve_requests_2 = resolve_requests_2.clone();
13965 async move {
13966 if unresolved_request == unresolved_item_1 {
13967 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13968 Ok(resolved_item_1.clone())
13969 } else if unresolved_request == unresolved_item_2 {
13970 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13971 Ok(resolved_item_2.clone())
13972 } else {
13973 panic!("Unexpected completion item {unresolved_request:?}")
13974 }
13975 }
13976 }
13977 })
13978 .detach();
13979
13980 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13981 let unresolved_item_1 = unresolved_item_1.clone();
13982 let unresolved_item_2 = unresolved_item_2.clone();
13983 async move {
13984 Ok(Some(lsp::CompletionResponse::Array(vec![
13985 unresolved_item_1,
13986 unresolved_item_2,
13987 ])))
13988 }
13989 })
13990 .next()
13991 .await;
13992
13993 cx.condition(|editor, _| editor.context_menu_visible())
13994 .await;
13995 cx.update_editor(|editor, _, _| {
13996 let context_menu = editor.context_menu.borrow_mut();
13997 let context_menu = context_menu
13998 .as_ref()
13999 .expect("Should have the context menu deployed");
14000 match context_menu {
14001 CodeContextMenu::Completions(completions_menu) => {
14002 let completions = completions_menu.completions.borrow_mut();
14003 assert_eq!(
14004 completions
14005 .iter()
14006 .map(|completion| &completion.label.text)
14007 .collect::<Vec<_>>(),
14008 vec!["id", "other"]
14009 )
14010 }
14011 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14012 }
14013 });
14014 cx.run_until_parked();
14015
14016 cx.update_editor(|editor, window, cx| {
14017 editor.context_menu_next(&ContextMenuNext, window, cx);
14018 });
14019 cx.run_until_parked();
14020 cx.update_editor(|editor, window, cx| {
14021 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14022 });
14023 cx.run_until_parked();
14024 cx.update_editor(|editor, window, cx| {
14025 editor.context_menu_next(&ContextMenuNext, window, cx);
14026 });
14027 cx.run_until_parked();
14028 cx.update_editor(|editor, window, cx| {
14029 editor
14030 .compose_completion(&ComposeCompletion::default(), window, cx)
14031 .expect("No task returned")
14032 })
14033 .await
14034 .expect("Completion failed");
14035 cx.run_until_parked();
14036
14037 cx.update_editor(|editor, _, cx| {
14038 assert_eq!(
14039 resolve_requests_1.load(atomic::Ordering::Acquire),
14040 1,
14041 "Should always resolve once despite multiple selections"
14042 );
14043 assert_eq!(
14044 resolve_requests_2.load(atomic::Ordering::Acquire),
14045 1,
14046 "Should always resolve once after multiple selections and applying the completion"
14047 );
14048 assert_eq!(
14049 editor.text(cx),
14050 "fn main() { let a = ??.other; }",
14051 "Should use resolved data when applying the completion"
14052 );
14053 });
14054}
14055
14056#[gpui::test]
14057async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14058 init_test(cx, |_| {});
14059
14060 let item_0 = lsp::CompletionItem {
14061 label: "abs".into(),
14062 insert_text: Some("abs".into()),
14063 data: Some(json!({ "very": "special"})),
14064 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14065 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14066 lsp::InsertReplaceEdit {
14067 new_text: "abs".to_string(),
14068 insert: lsp::Range::default(),
14069 replace: lsp::Range::default(),
14070 },
14071 )),
14072 ..lsp::CompletionItem::default()
14073 };
14074 let items = iter::once(item_0.clone())
14075 .chain((11..51).map(|i| lsp::CompletionItem {
14076 label: format!("item_{}", i),
14077 insert_text: Some(format!("item_{}", i)),
14078 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14079 ..lsp::CompletionItem::default()
14080 }))
14081 .collect::<Vec<_>>();
14082
14083 let default_commit_characters = vec!["?".to_string()];
14084 let default_data = json!({ "default": "data"});
14085 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14086 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14087 let default_edit_range = lsp::Range {
14088 start: lsp::Position {
14089 line: 0,
14090 character: 5,
14091 },
14092 end: lsp::Position {
14093 line: 0,
14094 character: 5,
14095 },
14096 };
14097
14098 let mut cx = EditorLspTestContext::new_rust(
14099 lsp::ServerCapabilities {
14100 completion_provider: Some(lsp::CompletionOptions {
14101 trigger_characters: Some(vec![".".to_string()]),
14102 resolve_provider: Some(true),
14103 ..Default::default()
14104 }),
14105 ..Default::default()
14106 },
14107 cx,
14108 )
14109 .await;
14110
14111 cx.set_state("fn main() { let a = 2ˇ; }");
14112 cx.simulate_keystroke(".");
14113
14114 let completion_data = default_data.clone();
14115 let completion_characters = default_commit_characters.clone();
14116 let completion_items = items.clone();
14117 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14118 let default_data = completion_data.clone();
14119 let default_commit_characters = completion_characters.clone();
14120 let items = completion_items.clone();
14121 async move {
14122 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14123 items,
14124 item_defaults: Some(lsp::CompletionListItemDefaults {
14125 data: Some(default_data.clone()),
14126 commit_characters: Some(default_commit_characters.clone()),
14127 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14128 default_edit_range,
14129 )),
14130 insert_text_format: Some(default_insert_text_format),
14131 insert_text_mode: Some(default_insert_text_mode),
14132 }),
14133 ..lsp::CompletionList::default()
14134 })))
14135 }
14136 })
14137 .next()
14138 .await;
14139
14140 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14141 cx.lsp
14142 .server
14143 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14144 let closure_resolved_items = resolved_items.clone();
14145 move |item_to_resolve, _| {
14146 let closure_resolved_items = closure_resolved_items.clone();
14147 async move {
14148 closure_resolved_items.lock().push(item_to_resolve.clone());
14149 Ok(item_to_resolve)
14150 }
14151 }
14152 })
14153 .detach();
14154
14155 cx.condition(|editor, _| editor.context_menu_visible())
14156 .await;
14157 cx.run_until_parked();
14158 cx.update_editor(|editor, _, _| {
14159 let menu = editor.context_menu.borrow_mut();
14160 match menu.as_ref().expect("should have the completions menu") {
14161 CodeContextMenu::Completions(completions_menu) => {
14162 assert_eq!(
14163 completions_menu
14164 .entries
14165 .borrow()
14166 .iter()
14167 .map(|mat| mat.string.clone())
14168 .collect::<Vec<String>>(),
14169 items
14170 .iter()
14171 .map(|completion| completion.label.clone())
14172 .collect::<Vec<String>>()
14173 );
14174 }
14175 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14176 }
14177 });
14178 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14179 // with 4 from the end.
14180 assert_eq!(
14181 *resolved_items.lock(),
14182 [&items[0..16], &items[items.len() - 4..items.len()]]
14183 .concat()
14184 .iter()
14185 .cloned()
14186 .map(|mut item| {
14187 if item.data.is_none() {
14188 item.data = Some(default_data.clone());
14189 }
14190 item
14191 })
14192 .collect::<Vec<lsp::CompletionItem>>(),
14193 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14194 );
14195 resolved_items.lock().clear();
14196
14197 cx.update_editor(|editor, window, cx| {
14198 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14199 });
14200 cx.run_until_parked();
14201 // Completions that have already been resolved are skipped.
14202 assert_eq!(
14203 *resolved_items.lock(),
14204 items[items.len() - 16..items.len() - 4]
14205 .iter()
14206 .cloned()
14207 .map(|mut item| {
14208 if item.data.is_none() {
14209 item.data = Some(default_data.clone());
14210 }
14211 item
14212 })
14213 .collect::<Vec<lsp::CompletionItem>>()
14214 );
14215 resolved_items.lock().clear();
14216}
14217
14218#[gpui::test]
14219async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14220 init_test(cx, |_| {});
14221
14222 let mut cx = EditorLspTestContext::new(
14223 Language::new(
14224 LanguageConfig {
14225 matcher: LanguageMatcher {
14226 path_suffixes: vec!["jsx".into()],
14227 ..Default::default()
14228 },
14229 overrides: [(
14230 "element".into(),
14231 LanguageConfigOverride {
14232 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14233 ..Default::default()
14234 },
14235 )]
14236 .into_iter()
14237 .collect(),
14238 ..Default::default()
14239 },
14240 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14241 )
14242 .with_override_query("(jsx_self_closing_element) @element")
14243 .unwrap(),
14244 lsp::ServerCapabilities {
14245 completion_provider: Some(lsp::CompletionOptions {
14246 trigger_characters: Some(vec![":".to_string()]),
14247 ..Default::default()
14248 }),
14249 ..Default::default()
14250 },
14251 cx,
14252 )
14253 .await;
14254
14255 cx.lsp
14256 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14257 Ok(Some(lsp::CompletionResponse::Array(vec![
14258 lsp::CompletionItem {
14259 label: "bg-blue".into(),
14260 ..Default::default()
14261 },
14262 lsp::CompletionItem {
14263 label: "bg-red".into(),
14264 ..Default::default()
14265 },
14266 lsp::CompletionItem {
14267 label: "bg-yellow".into(),
14268 ..Default::default()
14269 },
14270 ])))
14271 });
14272
14273 cx.set_state(r#"<p class="bgˇ" />"#);
14274
14275 // Trigger completion when typing a dash, because the dash is an extra
14276 // word character in the 'element' scope, which contains the cursor.
14277 cx.simulate_keystroke("-");
14278 cx.executor().run_until_parked();
14279 cx.update_editor(|editor, _, _| {
14280 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14281 {
14282 assert_eq!(
14283 completion_menu_entries(&menu),
14284 &["bg-red", "bg-blue", "bg-yellow"]
14285 );
14286 } else {
14287 panic!("expected completion menu to be open");
14288 }
14289 });
14290
14291 cx.simulate_keystroke("l");
14292 cx.executor().run_until_parked();
14293 cx.update_editor(|editor, _, _| {
14294 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14295 {
14296 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14297 } else {
14298 panic!("expected completion menu to be open");
14299 }
14300 });
14301
14302 // When filtering completions, consider the character after the '-' to
14303 // be the start of a subword.
14304 cx.set_state(r#"<p class="yelˇ" />"#);
14305 cx.simulate_keystroke("l");
14306 cx.executor().run_until_parked();
14307 cx.update_editor(|editor, _, _| {
14308 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14309 {
14310 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14311 } else {
14312 panic!("expected completion menu to be open");
14313 }
14314 });
14315}
14316
14317fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14318 let entries = menu.entries.borrow();
14319 entries.iter().map(|mat| mat.string.clone()).collect()
14320}
14321
14322#[gpui::test]
14323async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14324 init_test(cx, |settings| {
14325 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14326 FormatterList(vec![Formatter::Prettier].into()),
14327 ))
14328 });
14329
14330 let fs = FakeFs::new(cx.executor());
14331 fs.insert_file(path!("/file.ts"), Default::default()).await;
14332
14333 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14334 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14335
14336 language_registry.add(Arc::new(Language::new(
14337 LanguageConfig {
14338 name: "TypeScript".into(),
14339 matcher: LanguageMatcher {
14340 path_suffixes: vec!["ts".to_string()],
14341 ..Default::default()
14342 },
14343 ..Default::default()
14344 },
14345 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14346 )));
14347 update_test_language_settings(cx, |settings| {
14348 settings.defaults.prettier = Some(PrettierSettings {
14349 allowed: true,
14350 ..PrettierSettings::default()
14351 });
14352 });
14353
14354 let test_plugin = "test_plugin";
14355 let _ = language_registry.register_fake_lsp(
14356 "TypeScript",
14357 FakeLspAdapter {
14358 prettier_plugins: vec![test_plugin],
14359 ..Default::default()
14360 },
14361 );
14362
14363 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14364 let buffer = project
14365 .update(cx, |project, cx| {
14366 project.open_local_buffer(path!("/file.ts"), cx)
14367 })
14368 .await
14369 .unwrap();
14370
14371 let buffer_text = "one\ntwo\nthree\n";
14372 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14373 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14374 editor.update_in(cx, |editor, window, cx| {
14375 editor.set_text(buffer_text, window, cx)
14376 });
14377
14378 editor
14379 .update_in(cx, |editor, window, cx| {
14380 editor.perform_format(
14381 project.clone(),
14382 FormatTrigger::Manual,
14383 FormatTarget::Buffers,
14384 window,
14385 cx,
14386 )
14387 })
14388 .unwrap()
14389 .await;
14390 assert_eq!(
14391 editor.update(cx, |editor, cx| editor.text(cx)),
14392 buffer_text.to_string() + prettier_format_suffix,
14393 "Test prettier formatting was not applied to the original buffer text",
14394 );
14395
14396 update_test_language_settings(cx, |settings| {
14397 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14398 });
14399 let format = editor.update_in(cx, |editor, window, cx| {
14400 editor.perform_format(
14401 project.clone(),
14402 FormatTrigger::Manual,
14403 FormatTarget::Buffers,
14404 window,
14405 cx,
14406 )
14407 });
14408 format.await.unwrap();
14409 assert_eq!(
14410 editor.update(cx, |editor, cx| editor.text(cx)),
14411 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14412 "Autoformatting (via test prettier) was not applied to the original buffer text",
14413 );
14414}
14415
14416#[gpui::test]
14417async fn test_addition_reverts(cx: &mut TestAppContext) {
14418 init_test(cx, |_| {});
14419 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14420 let base_text = indoc! {r#"
14421 struct Row;
14422 struct Row1;
14423 struct Row2;
14424
14425 struct Row4;
14426 struct Row5;
14427 struct Row6;
14428
14429 struct Row8;
14430 struct Row9;
14431 struct Row10;"#};
14432
14433 // When addition hunks are not adjacent to carets, no hunk revert is performed
14434 assert_hunk_revert(
14435 indoc! {r#"struct Row;
14436 struct Row1;
14437 struct Row1.1;
14438 struct Row1.2;
14439 struct Row2;ˇ
14440
14441 struct Row4;
14442 struct Row5;
14443 struct Row6;
14444
14445 struct Row8;
14446 ˇstruct Row9;
14447 struct Row9.1;
14448 struct Row9.2;
14449 struct Row9.3;
14450 struct Row10;"#},
14451 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14452 indoc! {r#"struct Row;
14453 struct Row1;
14454 struct Row1.1;
14455 struct Row1.2;
14456 struct Row2;ˇ
14457
14458 struct Row4;
14459 struct Row5;
14460 struct Row6;
14461
14462 struct Row8;
14463 ˇstruct Row9;
14464 struct Row9.1;
14465 struct Row9.2;
14466 struct Row9.3;
14467 struct Row10;"#},
14468 base_text,
14469 &mut cx,
14470 );
14471 // Same for selections
14472 assert_hunk_revert(
14473 indoc! {r#"struct Row;
14474 struct Row1;
14475 struct Row2;
14476 struct Row2.1;
14477 struct Row2.2;
14478 «ˇ
14479 struct Row4;
14480 struct» Row5;
14481 «struct Row6;
14482 ˇ»
14483 struct Row9.1;
14484 struct Row9.2;
14485 struct Row9.3;
14486 struct Row8;
14487 struct Row9;
14488 struct Row10;"#},
14489 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14490 indoc! {r#"struct Row;
14491 struct Row1;
14492 struct Row2;
14493 struct Row2.1;
14494 struct Row2.2;
14495 «ˇ
14496 struct Row4;
14497 struct» Row5;
14498 «struct Row6;
14499 ˇ»
14500 struct Row9.1;
14501 struct Row9.2;
14502 struct Row9.3;
14503 struct Row8;
14504 struct Row9;
14505 struct Row10;"#},
14506 base_text,
14507 &mut cx,
14508 );
14509
14510 // When carets and selections intersect the addition hunks, those are reverted.
14511 // Adjacent carets got merged.
14512 assert_hunk_revert(
14513 indoc! {r#"struct Row;
14514 ˇ// something on the top
14515 struct Row1;
14516 struct Row2;
14517 struct Roˇw3.1;
14518 struct Row2.2;
14519 struct Row2.3;ˇ
14520
14521 struct Row4;
14522 struct ˇRow5.1;
14523 struct Row5.2;
14524 struct «Rowˇ»5.3;
14525 struct Row5;
14526 struct Row6;
14527 ˇ
14528 struct Row9.1;
14529 struct «Rowˇ»9.2;
14530 struct «ˇRow»9.3;
14531 struct Row8;
14532 struct Row9;
14533 «ˇ// something on bottom»
14534 struct Row10;"#},
14535 vec![
14536 DiffHunkStatusKind::Added,
14537 DiffHunkStatusKind::Added,
14538 DiffHunkStatusKind::Added,
14539 DiffHunkStatusKind::Added,
14540 DiffHunkStatusKind::Added,
14541 ],
14542 indoc! {r#"struct Row;
14543 ˇstruct Row1;
14544 struct Row2;
14545 ˇ
14546 struct Row4;
14547 ˇstruct Row5;
14548 struct Row6;
14549 ˇ
14550 ˇstruct Row8;
14551 struct Row9;
14552 ˇstruct Row10;"#},
14553 base_text,
14554 &mut cx,
14555 );
14556}
14557
14558#[gpui::test]
14559async fn test_modification_reverts(cx: &mut TestAppContext) {
14560 init_test(cx, |_| {});
14561 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14562 let base_text = indoc! {r#"
14563 struct Row;
14564 struct Row1;
14565 struct Row2;
14566
14567 struct Row4;
14568 struct Row5;
14569 struct Row6;
14570
14571 struct Row8;
14572 struct Row9;
14573 struct Row10;"#};
14574
14575 // Modification hunks behave the same as the addition ones.
14576 assert_hunk_revert(
14577 indoc! {r#"struct Row;
14578 struct Row1;
14579 struct Row33;
14580 ˇ
14581 struct Row4;
14582 struct Row5;
14583 struct Row6;
14584 ˇ
14585 struct Row99;
14586 struct Row9;
14587 struct Row10;"#},
14588 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14589 indoc! {r#"struct Row;
14590 struct Row1;
14591 struct Row33;
14592 ˇ
14593 struct Row4;
14594 struct Row5;
14595 struct Row6;
14596 ˇ
14597 struct Row99;
14598 struct Row9;
14599 struct Row10;"#},
14600 base_text,
14601 &mut cx,
14602 );
14603 assert_hunk_revert(
14604 indoc! {r#"struct Row;
14605 struct Row1;
14606 struct Row33;
14607 «ˇ
14608 struct Row4;
14609 struct» Row5;
14610 «struct Row6;
14611 ˇ»
14612 struct Row99;
14613 struct Row9;
14614 struct Row10;"#},
14615 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14616 indoc! {r#"struct Row;
14617 struct Row1;
14618 struct Row33;
14619 «ˇ
14620 struct Row4;
14621 struct» Row5;
14622 «struct Row6;
14623 ˇ»
14624 struct Row99;
14625 struct Row9;
14626 struct Row10;"#},
14627 base_text,
14628 &mut cx,
14629 );
14630
14631 assert_hunk_revert(
14632 indoc! {r#"ˇstruct Row1.1;
14633 struct Row1;
14634 «ˇstr»uct Row22;
14635
14636 struct ˇRow44;
14637 struct Row5;
14638 struct «Rˇ»ow66;ˇ
14639
14640 «struˇ»ct Row88;
14641 struct Row9;
14642 struct Row1011;ˇ"#},
14643 vec![
14644 DiffHunkStatusKind::Modified,
14645 DiffHunkStatusKind::Modified,
14646 DiffHunkStatusKind::Modified,
14647 DiffHunkStatusKind::Modified,
14648 DiffHunkStatusKind::Modified,
14649 DiffHunkStatusKind::Modified,
14650 ],
14651 indoc! {r#"struct Row;
14652 ˇstruct Row1;
14653 struct Row2;
14654 ˇ
14655 struct Row4;
14656 ˇstruct Row5;
14657 struct Row6;
14658 ˇ
14659 struct Row8;
14660 ˇstruct Row9;
14661 struct Row10;ˇ"#},
14662 base_text,
14663 &mut cx,
14664 );
14665}
14666
14667#[gpui::test]
14668async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14669 init_test(cx, |_| {});
14670 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14671 let base_text = indoc! {r#"
14672 one
14673
14674 two
14675 three
14676 "#};
14677
14678 cx.set_head_text(base_text);
14679 cx.set_state("\nˇ\n");
14680 cx.executor().run_until_parked();
14681 cx.update_editor(|editor, _window, cx| {
14682 editor.expand_selected_diff_hunks(cx);
14683 });
14684 cx.executor().run_until_parked();
14685 cx.update_editor(|editor, window, cx| {
14686 editor.backspace(&Default::default(), window, cx);
14687 });
14688 cx.run_until_parked();
14689 cx.assert_state_with_diff(
14690 indoc! {r#"
14691
14692 - two
14693 - threeˇ
14694 +
14695 "#}
14696 .to_string(),
14697 );
14698}
14699
14700#[gpui::test]
14701async fn test_deletion_reverts(cx: &mut TestAppContext) {
14702 init_test(cx, |_| {});
14703 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14704 let base_text = indoc! {r#"struct Row;
14705struct Row1;
14706struct Row2;
14707
14708struct Row4;
14709struct Row5;
14710struct Row6;
14711
14712struct Row8;
14713struct Row9;
14714struct Row10;"#};
14715
14716 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14717 assert_hunk_revert(
14718 indoc! {r#"struct Row;
14719 struct Row2;
14720
14721 ˇstruct Row4;
14722 struct Row5;
14723 struct Row6;
14724 ˇ
14725 struct Row8;
14726 struct Row10;"#},
14727 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14728 indoc! {r#"struct Row;
14729 struct Row2;
14730
14731 ˇstruct Row4;
14732 struct Row5;
14733 struct Row6;
14734 ˇ
14735 struct Row8;
14736 struct Row10;"#},
14737 base_text,
14738 &mut cx,
14739 );
14740 assert_hunk_revert(
14741 indoc! {r#"struct Row;
14742 struct Row2;
14743
14744 «ˇstruct Row4;
14745 struct» Row5;
14746 «struct Row6;
14747 ˇ»
14748 struct Row8;
14749 struct Row10;"#},
14750 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14751 indoc! {r#"struct Row;
14752 struct Row2;
14753
14754 «ˇstruct Row4;
14755 struct» Row5;
14756 «struct Row6;
14757 ˇ»
14758 struct Row8;
14759 struct Row10;"#},
14760 base_text,
14761 &mut cx,
14762 );
14763
14764 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14765 assert_hunk_revert(
14766 indoc! {r#"struct Row;
14767 ˇstruct Row2;
14768
14769 struct Row4;
14770 struct Row5;
14771 struct Row6;
14772
14773 struct Row8;ˇ
14774 struct Row10;"#},
14775 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14776 indoc! {r#"struct Row;
14777 struct Row1;
14778 ˇstruct Row2;
14779
14780 struct Row4;
14781 struct Row5;
14782 struct Row6;
14783
14784 struct Row8;ˇ
14785 struct Row9;
14786 struct Row10;"#},
14787 base_text,
14788 &mut cx,
14789 );
14790 assert_hunk_revert(
14791 indoc! {r#"struct Row;
14792 struct Row2«ˇ;
14793 struct Row4;
14794 struct» Row5;
14795 «struct Row6;
14796
14797 struct Row8;ˇ»
14798 struct Row10;"#},
14799 vec![
14800 DiffHunkStatusKind::Deleted,
14801 DiffHunkStatusKind::Deleted,
14802 DiffHunkStatusKind::Deleted,
14803 ],
14804 indoc! {r#"struct Row;
14805 struct Row1;
14806 struct Row2«ˇ;
14807
14808 struct Row4;
14809 struct» Row5;
14810 «struct Row6;
14811
14812 struct Row8;ˇ»
14813 struct Row9;
14814 struct Row10;"#},
14815 base_text,
14816 &mut cx,
14817 );
14818}
14819
14820#[gpui::test]
14821async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14822 init_test(cx, |_| {});
14823
14824 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14825 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14826 let base_text_3 =
14827 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14828
14829 let text_1 = edit_first_char_of_every_line(base_text_1);
14830 let text_2 = edit_first_char_of_every_line(base_text_2);
14831 let text_3 = edit_first_char_of_every_line(base_text_3);
14832
14833 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14834 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14835 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14836
14837 let multibuffer = cx.new(|cx| {
14838 let mut multibuffer = MultiBuffer::new(ReadWrite);
14839 multibuffer.push_excerpts(
14840 buffer_1.clone(),
14841 [
14842 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14843 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14844 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14845 ],
14846 cx,
14847 );
14848 multibuffer.push_excerpts(
14849 buffer_2.clone(),
14850 [
14851 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14852 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14853 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14854 ],
14855 cx,
14856 );
14857 multibuffer.push_excerpts(
14858 buffer_3.clone(),
14859 [
14860 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14861 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14862 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14863 ],
14864 cx,
14865 );
14866 multibuffer
14867 });
14868
14869 let fs = FakeFs::new(cx.executor());
14870 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14871 let (editor, cx) = cx
14872 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14873 editor.update_in(cx, |editor, _window, cx| {
14874 for (buffer, diff_base) in [
14875 (buffer_1.clone(), base_text_1),
14876 (buffer_2.clone(), base_text_2),
14877 (buffer_3.clone(), base_text_3),
14878 ] {
14879 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14880 editor
14881 .buffer
14882 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14883 }
14884 });
14885 cx.executor().run_until_parked();
14886
14887 editor.update_in(cx, |editor, window, cx| {
14888 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}");
14889 editor.select_all(&SelectAll, window, cx);
14890 editor.git_restore(&Default::default(), window, cx);
14891 });
14892 cx.executor().run_until_parked();
14893
14894 // When all ranges are selected, all buffer hunks are reverted.
14895 editor.update(cx, |editor, cx| {
14896 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");
14897 });
14898 buffer_1.update(cx, |buffer, _| {
14899 assert_eq!(buffer.text(), base_text_1);
14900 });
14901 buffer_2.update(cx, |buffer, _| {
14902 assert_eq!(buffer.text(), base_text_2);
14903 });
14904 buffer_3.update(cx, |buffer, _| {
14905 assert_eq!(buffer.text(), base_text_3);
14906 });
14907
14908 editor.update_in(cx, |editor, window, cx| {
14909 editor.undo(&Default::default(), window, cx);
14910 });
14911
14912 editor.update_in(cx, |editor, window, cx| {
14913 editor.change_selections(None, window, cx, |s| {
14914 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14915 });
14916 editor.git_restore(&Default::default(), window, cx);
14917 });
14918
14919 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14920 // but not affect buffer_2 and its related excerpts.
14921 editor.update(cx, |editor, cx| {
14922 assert_eq!(
14923 editor.text(cx),
14924 "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}"
14925 );
14926 });
14927 buffer_1.update(cx, |buffer, _| {
14928 assert_eq!(buffer.text(), base_text_1);
14929 });
14930 buffer_2.update(cx, |buffer, _| {
14931 assert_eq!(
14932 buffer.text(),
14933 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14934 );
14935 });
14936 buffer_3.update(cx, |buffer, _| {
14937 assert_eq!(
14938 buffer.text(),
14939 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14940 );
14941 });
14942
14943 fn edit_first_char_of_every_line(text: &str) -> String {
14944 text.split('\n')
14945 .map(|line| format!("X{}", &line[1..]))
14946 .collect::<Vec<_>>()
14947 .join("\n")
14948 }
14949}
14950
14951#[gpui::test]
14952async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14953 init_test(cx, |_| {});
14954
14955 let cols = 4;
14956 let rows = 10;
14957 let sample_text_1 = sample_text(rows, cols, 'a');
14958 assert_eq!(
14959 sample_text_1,
14960 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14961 );
14962 let sample_text_2 = sample_text(rows, cols, 'l');
14963 assert_eq!(
14964 sample_text_2,
14965 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14966 );
14967 let sample_text_3 = sample_text(rows, cols, 'v');
14968 assert_eq!(
14969 sample_text_3,
14970 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14971 );
14972
14973 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14974 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14975 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14976
14977 let multi_buffer = cx.new(|cx| {
14978 let mut multibuffer = MultiBuffer::new(ReadWrite);
14979 multibuffer.push_excerpts(
14980 buffer_1.clone(),
14981 [
14982 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14983 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14984 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14985 ],
14986 cx,
14987 );
14988 multibuffer.push_excerpts(
14989 buffer_2.clone(),
14990 [
14991 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14992 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14993 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14994 ],
14995 cx,
14996 );
14997 multibuffer.push_excerpts(
14998 buffer_3.clone(),
14999 [
15000 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15001 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15002 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15003 ],
15004 cx,
15005 );
15006 multibuffer
15007 });
15008
15009 let fs = FakeFs::new(cx.executor());
15010 fs.insert_tree(
15011 "/a",
15012 json!({
15013 "main.rs": sample_text_1,
15014 "other.rs": sample_text_2,
15015 "lib.rs": sample_text_3,
15016 }),
15017 )
15018 .await;
15019 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15020 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15021 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15022 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15023 Editor::new(
15024 EditorMode::full(),
15025 multi_buffer,
15026 Some(project.clone()),
15027 window,
15028 cx,
15029 )
15030 });
15031 let multibuffer_item_id = workspace
15032 .update(cx, |workspace, window, cx| {
15033 assert!(
15034 workspace.active_item(cx).is_none(),
15035 "active item should be None before the first item is added"
15036 );
15037 workspace.add_item_to_active_pane(
15038 Box::new(multi_buffer_editor.clone()),
15039 None,
15040 true,
15041 window,
15042 cx,
15043 );
15044 let active_item = workspace
15045 .active_item(cx)
15046 .expect("should have an active item after adding the multi buffer");
15047 assert!(
15048 !active_item.is_singleton(cx),
15049 "A multi buffer was expected to active after adding"
15050 );
15051 active_item.item_id()
15052 })
15053 .unwrap();
15054 cx.executor().run_until_parked();
15055
15056 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15057 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15058 s.select_ranges(Some(1..2))
15059 });
15060 editor.open_excerpts(&OpenExcerpts, window, cx);
15061 });
15062 cx.executor().run_until_parked();
15063 let first_item_id = workspace
15064 .update(cx, |workspace, window, cx| {
15065 let active_item = workspace
15066 .active_item(cx)
15067 .expect("should have an active item after navigating into the 1st buffer");
15068 let first_item_id = active_item.item_id();
15069 assert_ne!(
15070 first_item_id, multibuffer_item_id,
15071 "Should navigate into the 1st buffer and activate it"
15072 );
15073 assert!(
15074 active_item.is_singleton(cx),
15075 "New active item should be a singleton buffer"
15076 );
15077 assert_eq!(
15078 active_item
15079 .act_as::<Editor>(cx)
15080 .expect("should have navigated into an editor for the 1st buffer")
15081 .read(cx)
15082 .text(cx),
15083 sample_text_1
15084 );
15085
15086 workspace
15087 .go_back(workspace.active_pane().downgrade(), window, cx)
15088 .detach_and_log_err(cx);
15089
15090 first_item_id
15091 })
15092 .unwrap();
15093 cx.executor().run_until_parked();
15094 workspace
15095 .update(cx, |workspace, _, cx| {
15096 let active_item = workspace
15097 .active_item(cx)
15098 .expect("should have an active item after navigating back");
15099 assert_eq!(
15100 active_item.item_id(),
15101 multibuffer_item_id,
15102 "Should navigate back to the multi buffer"
15103 );
15104 assert!(!active_item.is_singleton(cx));
15105 })
15106 .unwrap();
15107
15108 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15109 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15110 s.select_ranges(Some(39..40))
15111 });
15112 editor.open_excerpts(&OpenExcerpts, window, cx);
15113 });
15114 cx.executor().run_until_parked();
15115 let second_item_id = workspace
15116 .update(cx, |workspace, window, cx| {
15117 let active_item = workspace
15118 .active_item(cx)
15119 .expect("should have an active item after navigating into the 2nd buffer");
15120 let second_item_id = active_item.item_id();
15121 assert_ne!(
15122 second_item_id, multibuffer_item_id,
15123 "Should navigate away from the multibuffer"
15124 );
15125 assert_ne!(
15126 second_item_id, first_item_id,
15127 "Should navigate into the 2nd buffer and activate it"
15128 );
15129 assert!(
15130 active_item.is_singleton(cx),
15131 "New active item should be a singleton buffer"
15132 );
15133 assert_eq!(
15134 active_item
15135 .act_as::<Editor>(cx)
15136 .expect("should have navigated into an editor")
15137 .read(cx)
15138 .text(cx),
15139 sample_text_2
15140 );
15141
15142 workspace
15143 .go_back(workspace.active_pane().downgrade(), window, cx)
15144 .detach_and_log_err(cx);
15145
15146 second_item_id
15147 })
15148 .unwrap();
15149 cx.executor().run_until_parked();
15150 workspace
15151 .update(cx, |workspace, _, cx| {
15152 let active_item = workspace
15153 .active_item(cx)
15154 .expect("should have an active item after navigating back from the 2nd buffer");
15155 assert_eq!(
15156 active_item.item_id(),
15157 multibuffer_item_id,
15158 "Should navigate back from the 2nd buffer to the multi buffer"
15159 );
15160 assert!(!active_item.is_singleton(cx));
15161 })
15162 .unwrap();
15163
15164 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15165 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15166 s.select_ranges(Some(70..70))
15167 });
15168 editor.open_excerpts(&OpenExcerpts, window, cx);
15169 });
15170 cx.executor().run_until_parked();
15171 workspace
15172 .update(cx, |workspace, window, cx| {
15173 let active_item = workspace
15174 .active_item(cx)
15175 .expect("should have an active item after navigating into the 3rd buffer");
15176 let third_item_id = active_item.item_id();
15177 assert_ne!(
15178 third_item_id, multibuffer_item_id,
15179 "Should navigate into the 3rd buffer and activate it"
15180 );
15181 assert_ne!(third_item_id, first_item_id);
15182 assert_ne!(third_item_id, second_item_id);
15183 assert!(
15184 active_item.is_singleton(cx),
15185 "New active item should be a singleton buffer"
15186 );
15187 assert_eq!(
15188 active_item
15189 .act_as::<Editor>(cx)
15190 .expect("should have navigated into an editor")
15191 .read(cx)
15192 .text(cx),
15193 sample_text_3
15194 );
15195
15196 workspace
15197 .go_back(workspace.active_pane().downgrade(), window, cx)
15198 .detach_and_log_err(cx);
15199 })
15200 .unwrap();
15201 cx.executor().run_until_parked();
15202 workspace
15203 .update(cx, |workspace, _, cx| {
15204 let active_item = workspace
15205 .active_item(cx)
15206 .expect("should have an active item after navigating back from the 3rd buffer");
15207 assert_eq!(
15208 active_item.item_id(),
15209 multibuffer_item_id,
15210 "Should navigate back from the 3rd buffer to the multi buffer"
15211 );
15212 assert!(!active_item.is_singleton(cx));
15213 })
15214 .unwrap();
15215}
15216
15217#[gpui::test]
15218async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15219 init_test(cx, |_| {});
15220
15221 let mut cx = EditorTestContext::new(cx).await;
15222
15223 let diff_base = r#"
15224 use some::mod;
15225
15226 const A: u32 = 42;
15227
15228 fn main() {
15229 println!("hello");
15230
15231 println!("world");
15232 }
15233 "#
15234 .unindent();
15235
15236 cx.set_state(
15237 &r#"
15238 use some::modified;
15239
15240 ˇ
15241 fn main() {
15242 println!("hello there");
15243
15244 println!("around the");
15245 println!("world");
15246 }
15247 "#
15248 .unindent(),
15249 );
15250
15251 cx.set_head_text(&diff_base);
15252 executor.run_until_parked();
15253
15254 cx.update_editor(|editor, window, cx| {
15255 editor.go_to_next_hunk(&GoToHunk, window, cx);
15256 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15257 });
15258 executor.run_until_parked();
15259 cx.assert_state_with_diff(
15260 r#"
15261 use some::modified;
15262
15263
15264 fn main() {
15265 - println!("hello");
15266 + ˇ println!("hello there");
15267
15268 println!("around the");
15269 println!("world");
15270 }
15271 "#
15272 .unindent(),
15273 );
15274
15275 cx.update_editor(|editor, window, cx| {
15276 for _ in 0..2 {
15277 editor.go_to_next_hunk(&GoToHunk, window, cx);
15278 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15279 }
15280 });
15281 executor.run_until_parked();
15282 cx.assert_state_with_diff(
15283 r#"
15284 - use some::mod;
15285 + ˇuse some::modified;
15286
15287
15288 fn main() {
15289 - println!("hello");
15290 + println!("hello there");
15291
15292 + println!("around the");
15293 println!("world");
15294 }
15295 "#
15296 .unindent(),
15297 );
15298
15299 cx.update_editor(|editor, window, cx| {
15300 editor.go_to_next_hunk(&GoToHunk, window, cx);
15301 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15302 });
15303 executor.run_until_parked();
15304 cx.assert_state_with_diff(
15305 r#"
15306 - use some::mod;
15307 + use some::modified;
15308
15309 - const A: u32 = 42;
15310 ˇ
15311 fn main() {
15312 - println!("hello");
15313 + println!("hello there");
15314
15315 + println!("around the");
15316 println!("world");
15317 }
15318 "#
15319 .unindent(),
15320 );
15321
15322 cx.update_editor(|editor, window, cx| {
15323 editor.cancel(&Cancel, window, cx);
15324 });
15325
15326 cx.assert_state_with_diff(
15327 r#"
15328 use some::modified;
15329
15330 ˇ
15331 fn main() {
15332 println!("hello there");
15333
15334 println!("around the");
15335 println!("world");
15336 }
15337 "#
15338 .unindent(),
15339 );
15340}
15341
15342#[gpui::test]
15343async fn test_diff_base_change_with_expanded_diff_hunks(
15344 executor: BackgroundExecutor,
15345 cx: &mut TestAppContext,
15346) {
15347 init_test(cx, |_| {});
15348
15349 let mut cx = EditorTestContext::new(cx).await;
15350
15351 let diff_base = r#"
15352 use some::mod1;
15353 use some::mod2;
15354
15355 const A: u32 = 42;
15356 const B: u32 = 42;
15357 const C: u32 = 42;
15358
15359 fn main() {
15360 println!("hello");
15361
15362 println!("world");
15363 }
15364 "#
15365 .unindent();
15366
15367 cx.set_state(
15368 &r#"
15369 use some::mod2;
15370
15371 const A: u32 = 42;
15372 const C: u32 = 42;
15373
15374 fn main(ˇ) {
15375 //println!("hello");
15376
15377 println!("world");
15378 //
15379 //
15380 }
15381 "#
15382 .unindent(),
15383 );
15384
15385 cx.set_head_text(&diff_base);
15386 executor.run_until_parked();
15387
15388 cx.update_editor(|editor, window, cx| {
15389 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15390 });
15391 executor.run_until_parked();
15392 cx.assert_state_with_diff(
15393 r#"
15394 - use some::mod1;
15395 use some::mod2;
15396
15397 const A: u32 = 42;
15398 - const B: u32 = 42;
15399 const C: u32 = 42;
15400
15401 fn main(ˇ) {
15402 - println!("hello");
15403 + //println!("hello");
15404
15405 println!("world");
15406 + //
15407 + //
15408 }
15409 "#
15410 .unindent(),
15411 );
15412
15413 cx.set_head_text("new diff base!");
15414 executor.run_until_parked();
15415 cx.assert_state_with_diff(
15416 r#"
15417 - new diff base!
15418 + use some::mod2;
15419 +
15420 + const A: u32 = 42;
15421 + const C: u32 = 42;
15422 +
15423 + fn main(ˇ) {
15424 + //println!("hello");
15425 +
15426 + println!("world");
15427 + //
15428 + //
15429 + }
15430 "#
15431 .unindent(),
15432 );
15433}
15434
15435#[gpui::test]
15436async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15437 init_test(cx, |_| {});
15438
15439 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15440 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15441 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15442 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15443 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15444 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15445
15446 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15447 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15448 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15449
15450 let multi_buffer = cx.new(|cx| {
15451 let mut multibuffer = MultiBuffer::new(ReadWrite);
15452 multibuffer.push_excerpts(
15453 buffer_1.clone(),
15454 [
15455 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15456 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15457 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15458 ],
15459 cx,
15460 );
15461 multibuffer.push_excerpts(
15462 buffer_2.clone(),
15463 [
15464 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15465 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15466 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15467 ],
15468 cx,
15469 );
15470 multibuffer.push_excerpts(
15471 buffer_3.clone(),
15472 [
15473 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15474 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15475 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15476 ],
15477 cx,
15478 );
15479 multibuffer
15480 });
15481
15482 let editor =
15483 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15484 editor
15485 .update(cx, |editor, _window, cx| {
15486 for (buffer, diff_base) in [
15487 (buffer_1.clone(), file_1_old),
15488 (buffer_2.clone(), file_2_old),
15489 (buffer_3.clone(), file_3_old),
15490 ] {
15491 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15492 editor
15493 .buffer
15494 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15495 }
15496 })
15497 .unwrap();
15498
15499 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15500 cx.run_until_parked();
15501
15502 cx.assert_editor_state(
15503 &"
15504 ˇaaa
15505 ccc
15506 ddd
15507
15508 ggg
15509 hhh
15510
15511
15512 lll
15513 mmm
15514 NNN
15515
15516 qqq
15517 rrr
15518
15519 uuu
15520 111
15521 222
15522 333
15523
15524 666
15525 777
15526
15527 000
15528 !!!"
15529 .unindent(),
15530 );
15531
15532 cx.update_editor(|editor, window, cx| {
15533 editor.select_all(&SelectAll, window, cx);
15534 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15535 });
15536 cx.executor().run_until_parked();
15537
15538 cx.assert_state_with_diff(
15539 "
15540 «aaa
15541 - bbb
15542 ccc
15543 ddd
15544
15545 ggg
15546 hhh
15547
15548
15549 lll
15550 mmm
15551 - nnn
15552 + NNN
15553
15554 qqq
15555 rrr
15556
15557 uuu
15558 111
15559 222
15560 333
15561
15562 + 666
15563 777
15564
15565 000
15566 !!!ˇ»"
15567 .unindent(),
15568 );
15569}
15570
15571#[gpui::test]
15572async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15573 init_test(cx, |_| {});
15574
15575 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15576 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15577
15578 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15579 let multi_buffer = cx.new(|cx| {
15580 let mut multibuffer = MultiBuffer::new(ReadWrite);
15581 multibuffer.push_excerpts(
15582 buffer.clone(),
15583 [
15584 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15585 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15586 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15587 ],
15588 cx,
15589 );
15590 multibuffer
15591 });
15592
15593 let editor =
15594 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15595 editor
15596 .update(cx, |editor, _window, cx| {
15597 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15598 editor
15599 .buffer
15600 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15601 })
15602 .unwrap();
15603
15604 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15605 cx.run_until_parked();
15606
15607 cx.update_editor(|editor, window, cx| {
15608 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15609 });
15610 cx.executor().run_until_parked();
15611
15612 // When the start of a hunk coincides with the start of its excerpt,
15613 // the hunk is expanded. When the start of a a hunk is earlier than
15614 // the start of its excerpt, the hunk is not expanded.
15615 cx.assert_state_with_diff(
15616 "
15617 ˇaaa
15618 - bbb
15619 + BBB
15620
15621 - ddd
15622 - eee
15623 + DDD
15624 + EEE
15625 fff
15626
15627 iii
15628 "
15629 .unindent(),
15630 );
15631}
15632
15633#[gpui::test]
15634async fn test_edits_around_expanded_insertion_hunks(
15635 executor: BackgroundExecutor,
15636 cx: &mut TestAppContext,
15637) {
15638 init_test(cx, |_| {});
15639
15640 let mut cx = EditorTestContext::new(cx).await;
15641
15642 let diff_base = r#"
15643 use some::mod1;
15644 use some::mod2;
15645
15646 const A: u32 = 42;
15647
15648 fn main() {
15649 println!("hello");
15650
15651 println!("world");
15652 }
15653 "#
15654 .unindent();
15655 executor.run_until_parked();
15656 cx.set_state(
15657 &r#"
15658 use some::mod1;
15659 use some::mod2;
15660
15661 const A: u32 = 42;
15662 const B: u32 = 42;
15663 const C: u32 = 42;
15664 ˇ
15665
15666 fn main() {
15667 println!("hello");
15668
15669 println!("world");
15670 }
15671 "#
15672 .unindent(),
15673 );
15674
15675 cx.set_head_text(&diff_base);
15676 executor.run_until_parked();
15677
15678 cx.update_editor(|editor, window, cx| {
15679 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15680 });
15681 executor.run_until_parked();
15682
15683 cx.assert_state_with_diff(
15684 r#"
15685 use some::mod1;
15686 use some::mod2;
15687
15688 const A: u32 = 42;
15689 + const B: u32 = 42;
15690 + const C: u32 = 42;
15691 + ˇ
15692
15693 fn main() {
15694 println!("hello");
15695
15696 println!("world");
15697 }
15698 "#
15699 .unindent(),
15700 );
15701
15702 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15703 executor.run_until_parked();
15704
15705 cx.assert_state_with_diff(
15706 r#"
15707 use some::mod1;
15708 use some::mod2;
15709
15710 const A: u32 = 42;
15711 + const B: u32 = 42;
15712 + const C: u32 = 42;
15713 + const D: u32 = 42;
15714 + ˇ
15715
15716 fn main() {
15717 println!("hello");
15718
15719 println!("world");
15720 }
15721 "#
15722 .unindent(),
15723 );
15724
15725 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15726 executor.run_until_parked();
15727
15728 cx.assert_state_with_diff(
15729 r#"
15730 use some::mod1;
15731 use some::mod2;
15732
15733 const A: u32 = 42;
15734 + const B: u32 = 42;
15735 + const C: u32 = 42;
15736 + const D: u32 = 42;
15737 + const E: u32 = 42;
15738 + ˇ
15739
15740 fn main() {
15741 println!("hello");
15742
15743 println!("world");
15744 }
15745 "#
15746 .unindent(),
15747 );
15748
15749 cx.update_editor(|editor, window, cx| {
15750 editor.delete_line(&DeleteLine, window, cx);
15751 });
15752 executor.run_until_parked();
15753
15754 cx.assert_state_with_diff(
15755 r#"
15756 use some::mod1;
15757 use some::mod2;
15758
15759 const A: u32 = 42;
15760 + const B: u32 = 42;
15761 + const C: u32 = 42;
15762 + const D: u32 = 42;
15763 + const E: u32 = 42;
15764 ˇ
15765 fn main() {
15766 println!("hello");
15767
15768 println!("world");
15769 }
15770 "#
15771 .unindent(),
15772 );
15773
15774 cx.update_editor(|editor, window, cx| {
15775 editor.move_up(&MoveUp, window, cx);
15776 editor.delete_line(&DeleteLine, window, cx);
15777 editor.move_up(&MoveUp, window, cx);
15778 editor.delete_line(&DeleteLine, window, cx);
15779 editor.move_up(&MoveUp, window, cx);
15780 editor.delete_line(&DeleteLine, window, cx);
15781 });
15782 executor.run_until_parked();
15783 cx.assert_state_with_diff(
15784 r#"
15785 use some::mod1;
15786 use some::mod2;
15787
15788 const A: u32 = 42;
15789 + const B: u32 = 42;
15790 ˇ
15791 fn main() {
15792 println!("hello");
15793
15794 println!("world");
15795 }
15796 "#
15797 .unindent(),
15798 );
15799
15800 cx.update_editor(|editor, window, cx| {
15801 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15802 editor.delete_line(&DeleteLine, window, cx);
15803 });
15804 executor.run_until_parked();
15805 cx.assert_state_with_diff(
15806 r#"
15807 ˇ
15808 fn main() {
15809 println!("hello");
15810
15811 println!("world");
15812 }
15813 "#
15814 .unindent(),
15815 );
15816}
15817
15818#[gpui::test]
15819async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15820 init_test(cx, |_| {});
15821
15822 let mut cx = EditorTestContext::new(cx).await;
15823 cx.set_head_text(indoc! { "
15824 one
15825 two
15826 three
15827 four
15828 five
15829 "
15830 });
15831 cx.set_state(indoc! { "
15832 one
15833 ˇthree
15834 five
15835 "});
15836 cx.run_until_parked();
15837 cx.update_editor(|editor, window, cx| {
15838 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15839 });
15840 cx.assert_state_with_diff(
15841 indoc! { "
15842 one
15843 - two
15844 ˇthree
15845 - four
15846 five
15847 "}
15848 .to_string(),
15849 );
15850 cx.update_editor(|editor, window, cx| {
15851 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15852 });
15853
15854 cx.assert_state_with_diff(
15855 indoc! { "
15856 one
15857 ˇthree
15858 five
15859 "}
15860 .to_string(),
15861 );
15862
15863 cx.set_state(indoc! { "
15864 one
15865 ˇTWO
15866 three
15867 four
15868 five
15869 "});
15870 cx.run_until_parked();
15871 cx.update_editor(|editor, window, cx| {
15872 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15873 });
15874
15875 cx.assert_state_with_diff(
15876 indoc! { "
15877 one
15878 - two
15879 + ˇTWO
15880 three
15881 four
15882 five
15883 "}
15884 .to_string(),
15885 );
15886 cx.update_editor(|editor, window, cx| {
15887 editor.move_up(&Default::default(), window, cx);
15888 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15889 });
15890 cx.assert_state_with_diff(
15891 indoc! { "
15892 one
15893 ˇTWO
15894 three
15895 four
15896 five
15897 "}
15898 .to_string(),
15899 );
15900}
15901
15902#[gpui::test]
15903async fn test_edits_around_expanded_deletion_hunks(
15904 executor: BackgroundExecutor,
15905 cx: &mut TestAppContext,
15906) {
15907 init_test(cx, |_| {});
15908
15909 let mut cx = EditorTestContext::new(cx).await;
15910
15911 let diff_base = r#"
15912 use some::mod1;
15913 use some::mod2;
15914
15915 const A: u32 = 42;
15916 const B: u32 = 42;
15917 const C: u32 = 42;
15918
15919
15920 fn main() {
15921 println!("hello");
15922
15923 println!("world");
15924 }
15925 "#
15926 .unindent();
15927 executor.run_until_parked();
15928 cx.set_state(
15929 &r#"
15930 use some::mod1;
15931 use some::mod2;
15932
15933 ˇconst B: u32 = 42;
15934 const C: u32 = 42;
15935
15936
15937 fn main() {
15938 println!("hello");
15939
15940 println!("world");
15941 }
15942 "#
15943 .unindent(),
15944 );
15945
15946 cx.set_head_text(&diff_base);
15947 executor.run_until_parked();
15948
15949 cx.update_editor(|editor, window, cx| {
15950 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15951 });
15952 executor.run_until_parked();
15953
15954 cx.assert_state_with_diff(
15955 r#"
15956 use some::mod1;
15957 use some::mod2;
15958
15959 - const A: u32 = 42;
15960 ˇconst B: u32 = 42;
15961 const C: u32 = 42;
15962
15963
15964 fn main() {
15965 println!("hello");
15966
15967 println!("world");
15968 }
15969 "#
15970 .unindent(),
15971 );
15972
15973 cx.update_editor(|editor, window, cx| {
15974 editor.delete_line(&DeleteLine, window, cx);
15975 });
15976 executor.run_until_parked();
15977 cx.assert_state_with_diff(
15978 r#"
15979 use some::mod1;
15980 use some::mod2;
15981
15982 - const A: u32 = 42;
15983 - const B: u32 = 42;
15984 ˇconst C: u32 = 42;
15985
15986
15987 fn main() {
15988 println!("hello");
15989
15990 println!("world");
15991 }
15992 "#
15993 .unindent(),
15994 );
15995
15996 cx.update_editor(|editor, window, cx| {
15997 editor.delete_line(&DeleteLine, window, cx);
15998 });
15999 executor.run_until_parked();
16000 cx.assert_state_with_diff(
16001 r#"
16002 use some::mod1;
16003 use some::mod2;
16004
16005 - const A: u32 = 42;
16006 - const B: u32 = 42;
16007 - const C: u32 = 42;
16008 ˇ
16009
16010 fn main() {
16011 println!("hello");
16012
16013 println!("world");
16014 }
16015 "#
16016 .unindent(),
16017 );
16018
16019 cx.update_editor(|editor, window, cx| {
16020 editor.handle_input("replacement", window, cx);
16021 });
16022 executor.run_until_parked();
16023 cx.assert_state_with_diff(
16024 r#"
16025 use some::mod1;
16026 use some::mod2;
16027
16028 - const A: u32 = 42;
16029 - const B: u32 = 42;
16030 - const C: u32 = 42;
16031 -
16032 + replacementˇ
16033
16034 fn main() {
16035 println!("hello");
16036
16037 println!("world");
16038 }
16039 "#
16040 .unindent(),
16041 );
16042}
16043
16044#[gpui::test]
16045async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16046 init_test(cx, |_| {});
16047
16048 let mut cx = EditorTestContext::new(cx).await;
16049
16050 let base_text = r#"
16051 one
16052 two
16053 three
16054 four
16055 five
16056 "#
16057 .unindent();
16058 executor.run_until_parked();
16059 cx.set_state(
16060 &r#"
16061 one
16062 two
16063 fˇour
16064 five
16065 "#
16066 .unindent(),
16067 );
16068
16069 cx.set_head_text(&base_text);
16070 executor.run_until_parked();
16071
16072 cx.update_editor(|editor, window, cx| {
16073 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16074 });
16075 executor.run_until_parked();
16076
16077 cx.assert_state_with_diff(
16078 r#"
16079 one
16080 two
16081 - three
16082 fˇour
16083 five
16084 "#
16085 .unindent(),
16086 );
16087
16088 cx.update_editor(|editor, window, cx| {
16089 editor.backspace(&Backspace, window, cx);
16090 editor.backspace(&Backspace, window, cx);
16091 });
16092 executor.run_until_parked();
16093 cx.assert_state_with_diff(
16094 r#"
16095 one
16096 two
16097 - threeˇ
16098 - four
16099 + our
16100 five
16101 "#
16102 .unindent(),
16103 );
16104}
16105
16106#[gpui::test]
16107async fn test_edit_after_expanded_modification_hunk(
16108 executor: BackgroundExecutor,
16109 cx: &mut TestAppContext,
16110) {
16111 init_test(cx, |_| {});
16112
16113 let mut cx = EditorTestContext::new(cx).await;
16114
16115 let diff_base = r#"
16116 use some::mod1;
16117 use some::mod2;
16118
16119 const A: u32 = 42;
16120 const B: u32 = 42;
16121 const C: u32 = 42;
16122 const D: u32 = 42;
16123
16124
16125 fn main() {
16126 println!("hello");
16127
16128 println!("world");
16129 }"#
16130 .unindent();
16131
16132 cx.set_state(
16133 &r#"
16134 use some::mod1;
16135 use some::mod2;
16136
16137 const A: u32 = 42;
16138 const B: u32 = 42;
16139 const C: u32 = 43ˇ
16140 const D: u32 = 42;
16141
16142
16143 fn main() {
16144 println!("hello");
16145
16146 println!("world");
16147 }"#
16148 .unindent(),
16149 );
16150
16151 cx.set_head_text(&diff_base);
16152 executor.run_until_parked();
16153 cx.update_editor(|editor, window, cx| {
16154 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16155 });
16156 executor.run_until_parked();
16157
16158 cx.assert_state_with_diff(
16159 r#"
16160 use some::mod1;
16161 use some::mod2;
16162
16163 const A: u32 = 42;
16164 const B: u32 = 42;
16165 - const C: u32 = 42;
16166 + const C: u32 = 43ˇ
16167 const D: u32 = 42;
16168
16169
16170 fn main() {
16171 println!("hello");
16172
16173 println!("world");
16174 }"#
16175 .unindent(),
16176 );
16177
16178 cx.update_editor(|editor, window, cx| {
16179 editor.handle_input("\nnew_line\n", window, cx);
16180 });
16181 executor.run_until_parked();
16182
16183 cx.assert_state_with_diff(
16184 r#"
16185 use some::mod1;
16186 use some::mod2;
16187
16188 const A: u32 = 42;
16189 const B: u32 = 42;
16190 - const C: u32 = 42;
16191 + const C: u32 = 43
16192 + new_line
16193 + ˇ
16194 const D: u32 = 42;
16195
16196
16197 fn main() {
16198 println!("hello");
16199
16200 println!("world");
16201 }"#
16202 .unindent(),
16203 );
16204}
16205
16206#[gpui::test]
16207async fn test_stage_and_unstage_added_file_hunk(
16208 executor: BackgroundExecutor,
16209 cx: &mut TestAppContext,
16210) {
16211 init_test(cx, |_| {});
16212
16213 let mut cx = EditorTestContext::new(cx).await;
16214 cx.update_editor(|editor, _, cx| {
16215 editor.set_expand_all_diff_hunks(cx);
16216 });
16217
16218 let working_copy = r#"
16219 ˇfn main() {
16220 println!("hello, world!");
16221 }
16222 "#
16223 .unindent();
16224
16225 cx.set_state(&working_copy);
16226 executor.run_until_parked();
16227
16228 cx.assert_state_with_diff(
16229 r#"
16230 + ˇfn main() {
16231 + println!("hello, world!");
16232 + }
16233 "#
16234 .unindent(),
16235 );
16236 cx.assert_index_text(None);
16237
16238 cx.update_editor(|editor, window, cx| {
16239 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16240 });
16241 executor.run_until_parked();
16242 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16243 cx.assert_state_with_diff(
16244 r#"
16245 + ˇfn main() {
16246 + println!("hello, world!");
16247 + }
16248 "#
16249 .unindent(),
16250 );
16251
16252 cx.update_editor(|editor, window, cx| {
16253 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16254 });
16255 executor.run_until_parked();
16256 cx.assert_index_text(None);
16257}
16258
16259async fn setup_indent_guides_editor(
16260 text: &str,
16261 cx: &mut TestAppContext,
16262) -> (BufferId, EditorTestContext) {
16263 init_test(cx, |_| {});
16264
16265 let mut cx = EditorTestContext::new(cx).await;
16266
16267 let buffer_id = cx.update_editor(|editor, window, cx| {
16268 editor.set_text(text, window, cx);
16269 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16270
16271 buffer_ids[0]
16272 });
16273
16274 (buffer_id, cx)
16275}
16276
16277fn assert_indent_guides(
16278 range: Range<u32>,
16279 expected: Vec<IndentGuide>,
16280 active_indices: Option<Vec<usize>>,
16281 cx: &mut EditorTestContext,
16282) {
16283 let indent_guides = cx.update_editor(|editor, window, cx| {
16284 let snapshot = editor.snapshot(window, cx).display_snapshot;
16285 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16286 editor,
16287 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16288 true,
16289 &snapshot,
16290 cx,
16291 );
16292
16293 indent_guides.sort_by(|a, b| {
16294 a.depth.cmp(&b.depth).then(
16295 a.start_row
16296 .cmp(&b.start_row)
16297 .then(a.end_row.cmp(&b.end_row)),
16298 )
16299 });
16300 indent_guides
16301 });
16302
16303 if let Some(expected) = active_indices {
16304 let active_indices = cx.update_editor(|editor, window, cx| {
16305 let snapshot = editor.snapshot(window, cx).display_snapshot;
16306 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16307 });
16308
16309 assert_eq!(
16310 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16311 expected,
16312 "Active indent guide indices do not match"
16313 );
16314 }
16315
16316 assert_eq!(indent_guides, expected, "Indent guides do not match");
16317}
16318
16319fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16320 IndentGuide {
16321 buffer_id,
16322 start_row: MultiBufferRow(start_row),
16323 end_row: MultiBufferRow(end_row),
16324 depth,
16325 tab_size: 4,
16326 settings: IndentGuideSettings {
16327 enabled: true,
16328 line_width: 1,
16329 active_line_width: 1,
16330 ..Default::default()
16331 },
16332 }
16333}
16334
16335#[gpui::test]
16336async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16337 let (buffer_id, mut cx) = setup_indent_guides_editor(
16338 &"
16339 fn main() {
16340 let a = 1;
16341 }"
16342 .unindent(),
16343 cx,
16344 )
16345 .await;
16346
16347 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16348}
16349
16350#[gpui::test]
16351async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16352 let (buffer_id, mut cx) = setup_indent_guides_editor(
16353 &"
16354 fn main() {
16355 let a = 1;
16356 let b = 2;
16357 }"
16358 .unindent(),
16359 cx,
16360 )
16361 .await;
16362
16363 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16364}
16365
16366#[gpui::test]
16367async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16368 let (buffer_id, mut cx) = setup_indent_guides_editor(
16369 &"
16370 fn main() {
16371 let a = 1;
16372 if a == 3 {
16373 let b = 2;
16374 } else {
16375 let c = 3;
16376 }
16377 }"
16378 .unindent(),
16379 cx,
16380 )
16381 .await;
16382
16383 assert_indent_guides(
16384 0..8,
16385 vec![
16386 indent_guide(buffer_id, 1, 6, 0),
16387 indent_guide(buffer_id, 3, 3, 1),
16388 indent_guide(buffer_id, 5, 5, 1),
16389 ],
16390 None,
16391 &mut cx,
16392 );
16393}
16394
16395#[gpui::test]
16396async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16397 let (buffer_id, mut cx) = setup_indent_guides_editor(
16398 &"
16399 fn main() {
16400 let a = 1;
16401 let b = 2;
16402 let c = 3;
16403 }"
16404 .unindent(),
16405 cx,
16406 )
16407 .await;
16408
16409 assert_indent_guides(
16410 0..5,
16411 vec![
16412 indent_guide(buffer_id, 1, 3, 0),
16413 indent_guide(buffer_id, 2, 2, 1),
16414 ],
16415 None,
16416 &mut cx,
16417 );
16418}
16419
16420#[gpui::test]
16421async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16422 let (buffer_id, mut cx) = setup_indent_guides_editor(
16423 &"
16424 fn main() {
16425 let a = 1;
16426
16427 let c = 3;
16428 }"
16429 .unindent(),
16430 cx,
16431 )
16432 .await;
16433
16434 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16435}
16436
16437#[gpui::test]
16438async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16439 let (buffer_id, mut cx) = setup_indent_guides_editor(
16440 &"
16441 fn main() {
16442 let a = 1;
16443
16444 let c = 3;
16445
16446 if a == 3 {
16447 let b = 2;
16448 } else {
16449 let c = 3;
16450 }
16451 }"
16452 .unindent(),
16453 cx,
16454 )
16455 .await;
16456
16457 assert_indent_guides(
16458 0..11,
16459 vec![
16460 indent_guide(buffer_id, 1, 9, 0),
16461 indent_guide(buffer_id, 6, 6, 1),
16462 indent_guide(buffer_id, 8, 8, 1),
16463 ],
16464 None,
16465 &mut cx,
16466 );
16467}
16468
16469#[gpui::test]
16470async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16471 let (buffer_id, mut cx) = setup_indent_guides_editor(
16472 &"
16473 fn main() {
16474 let a = 1;
16475
16476 let c = 3;
16477
16478 if a == 3 {
16479 let b = 2;
16480 } else {
16481 let c = 3;
16482 }
16483 }"
16484 .unindent(),
16485 cx,
16486 )
16487 .await;
16488
16489 assert_indent_guides(
16490 1..11,
16491 vec![
16492 indent_guide(buffer_id, 1, 9, 0),
16493 indent_guide(buffer_id, 6, 6, 1),
16494 indent_guide(buffer_id, 8, 8, 1),
16495 ],
16496 None,
16497 &mut cx,
16498 );
16499}
16500
16501#[gpui::test]
16502async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16503 let (buffer_id, mut cx) = setup_indent_guides_editor(
16504 &"
16505 fn main() {
16506 let a = 1;
16507
16508 let c = 3;
16509
16510 if a == 3 {
16511 let b = 2;
16512 } else {
16513 let c = 3;
16514 }
16515 }"
16516 .unindent(),
16517 cx,
16518 )
16519 .await;
16520
16521 assert_indent_guides(
16522 1..10,
16523 vec![
16524 indent_guide(buffer_id, 1, 9, 0),
16525 indent_guide(buffer_id, 6, 6, 1),
16526 indent_guide(buffer_id, 8, 8, 1),
16527 ],
16528 None,
16529 &mut cx,
16530 );
16531}
16532
16533#[gpui::test]
16534async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16535 let (buffer_id, mut cx) = setup_indent_guides_editor(
16536 &"
16537 block1
16538 block2
16539 block3
16540 block4
16541 block2
16542 block1
16543 block1"
16544 .unindent(),
16545 cx,
16546 )
16547 .await;
16548
16549 assert_indent_guides(
16550 1..10,
16551 vec![
16552 indent_guide(buffer_id, 1, 4, 0),
16553 indent_guide(buffer_id, 2, 3, 1),
16554 indent_guide(buffer_id, 3, 3, 2),
16555 ],
16556 None,
16557 &mut cx,
16558 );
16559}
16560
16561#[gpui::test]
16562async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16563 let (buffer_id, mut cx) = setup_indent_guides_editor(
16564 &"
16565 block1
16566 block2
16567 block3
16568
16569 block1
16570 block1"
16571 .unindent(),
16572 cx,
16573 )
16574 .await;
16575
16576 assert_indent_guides(
16577 0..6,
16578 vec![
16579 indent_guide(buffer_id, 1, 2, 0),
16580 indent_guide(buffer_id, 2, 2, 1),
16581 ],
16582 None,
16583 &mut cx,
16584 );
16585}
16586
16587#[gpui::test]
16588async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16589 let (buffer_id, mut cx) = setup_indent_guides_editor(
16590 &"
16591 block1
16592
16593
16594
16595 block2
16596 "
16597 .unindent(),
16598 cx,
16599 )
16600 .await;
16601
16602 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16603}
16604
16605#[gpui::test]
16606async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16607 let (buffer_id, mut cx) = setup_indent_guides_editor(
16608 &"
16609 def a:
16610 \tb = 3
16611 \tif True:
16612 \t\tc = 4
16613 \t\td = 5
16614 \tprint(b)
16615 "
16616 .unindent(),
16617 cx,
16618 )
16619 .await;
16620
16621 assert_indent_guides(
16622 0..6,
16623 vec![
16624 indent_guide(buffer_id, 1, 5, 0),
16625 indent_guide(buffer_id, 3, 4, 1),
16626 ],
16627 None,
16628 &mut cx,
16629 );
16630}
16631
16632#[gpui::test]
16633async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16634 let (buffer_id, mut cx) = setup_indent_guides_editor(
16635 &"
16636 fn main() {
16637 let a = 1;
16638 }"
16639 .unindent(),
16640 cx,
16641 )
16642 .await;
16643
16644 cx.update_editor(|editor, window, cx| {
16645 editor.change_selections(None, window, cx, |s| {
16646 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16647 });
16648 });
16649
16650 assert_indent_guides(
16651 0..3,
16652 vec![indent_guide(buffer_id, 1, 1, 0)],
16653 Some(vec![0]),
16654 &mut cx,
16655 );
16656}
16657
16658#[gpui::test]
16659async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16660 let (buffer_id, mut cx) = setup_indent_guides_editor(
16661 &"
16662 fn main() {
16663 if 1 == 2 {
16664 let a = 1;
16665 }
16666 }"
16667 .unindent(),
16668 cx,
16669 )
16670 .await;
16671
16672 cx.update_editor(|editor, window, cx| {
16673 editor.change_selections(None, window, cx, |s| {
16674 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16675 });
16676 });
16677
16678 assert_indent_guides(
16679 0..4,
16680 vec![
16681 indent_guide(buffer_id, 1, 3, 0),
16682 indent_guide(buffer_id, 2, 2, 1),
16683 ],
16684 Some(vec![1]),
16685 &mut cx,
16686 );
16687
16688 cx.update_editor(|editor, window, cx| {
16689 editor.change_selections(None, window, cx, |s| {
16690 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16691 });
16692 });
16693
16694 assert_indent_guides(
16695 0..4,
16696 vec![
16697 indent_guide(buffer_id, 1, 3, 0),
16698 indent_guide(buffer_id, 2, 2, 1),
16699 ],
16700 Some(vec![1]),
16701 &mut cx,
16702 );
16703
16704 cx.update_editor(|editor, window, cx| {
16705 editor.change_selections(None, window, cx, |s| {
16706 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16707 });
16708 });
16709
16710 assert_indent_guides(
16711 0..4,
16712 vec![
16713 indent_guide(buffer_id, 1, 3, 0),
16714 indent_guide(buffer_id, 2, 2, 1),
16715 ],
16716 Some(vec![0]),
16717 &mut cx,
16718 );
16719}
16720
16721#[gpui::test]
16722async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16723 let (buffer_id, mut cx) = setup_indent_guides_editor(
16724 &"
16725 fn main() {
16726 let a = 1;
16727
16728 let b = 2;
16729 }"
16730 .unindent(),
16731 cx,
16732 )
16733 .await;
16734
16735 cx.update_editor(|editor, window, cx| {
16736 editor.change_selections(None, window, cx, |s| {
16737 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16738 });
16739 });
16740
16741 assert_indent_guides(
16742 0..5,
16743 vec![indent_guide(buffer_id, 1, 3, 0)],
16744 Some(vec![0]),
16745 &mut cx,
16746 );
16747}
16748
16749#[gpui::test]
16750async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16751 let (buffer_id, mut cx) = setup_indent_guides_editor(
16752 &"
16753 def m:
16754 a = 1
16755 pass"
16756 .unindent(),
16757 cx,
16758 )
16759 .await;
16760
16761 cx.update_editor(|editor, window, cx| {
16762 editor.change_selections(None, window, cx, |s| {
16763 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16764 });
16765 });
16766
16767 assert_indent_guides(
16768 0..3,
16769 vec![indent_guide(buffer_id, 1, 2, 0)],
16770 Some(vec![0]),
16771 &mut cx,
16772 );
16773}
16774
16775#[gpui::test]
16776async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16777 init_test(cx, |_| {});
16778 let mut cx = EditorTestContext::new(cx).await;
16779 let text = indoc! {
16780 "
16781 impl A {
16782 fn b() {
16783 0;
16784 3;
16785 5;
16786 6;
16787 7;
16788 }
16789 }
16790 "
16791 };
16792 let base_text = indoc! {
16793 "
16794 impl A {
16795 fn b() {
16796 0;
16797 1;
16798 2;
16799 3;
16800 4;
16801 }
16802 fn c() {
16803 5;
16804 6;
16805 7;
16806 }
16807 }
16808 "
16809 };
16810
16811 cx.update_editor(|editor, window, cx| {
16812 editor.set_text(text, window, cx);
16813
16814 editor.buffer().update(cx, |multibuffer, cx| {
16815 let buffer = multibuffer.as_singleton().unwrap();
16816 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16817
16818 multibuffer.set_all_diff_hunks_expanded(cx);
16819 multibuffer.add_diff(diff, cx);
16820
16821 buffer.read(cx).remote_id()
16822 })
16823 });
16824 cx.run_until_parked();
16825
16826 cx.assert_state_with_diff(
16827 indoc! { "
16828 impl A {
16829 fn b() {
16830 0;
16831 - 1;
16832 - 2;
16833 3;
16834 - 4;
16835 - }
16836 - fn c() {
16837 5;
16838 6;
16839 7;
16840 }
16841 }
16842 ˇ"
16843 }
16844 .to_string(),
16845 );
16846
16847 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16848 editor
16849 .snapshot(window, cx)
16850 .buffer_snapshot
16851 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16852 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16853 .collect::<Vec<_>>()
16854 });
16855 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16856 assert_eq!(
16857 actual_guides,
16858 vec![
16859 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16860 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16861 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16862 ]
16863 );
16864}
16865
16866#[gpui::test]
16867async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16868 init_test(cx, |_| {});
16869 let mut cx = EditorTestContext::new(cx).await;
16870
16871 let diff_base = r#"
16872 a
16873 b
16874 c
16875 "#
16876 .unindent();
16877
16878 cx.set_state(
16879 &r#"
16880 ˇA
16881 b
16882 C
16883 "#
16884 .unindent(),
16885 );
16886 cx.set_head_text(&diff_base);
16887 cx.update_editor(|editor, window, cx| {
16888 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16889 });
16890 executor.run_until_parked();
16891
16892 let both_hunks_expanded = r#"
16893 - a
16894 + ˇA
16895 b
16896 - c
16897 + C
16898 "#
16899 .unindent();
16900
16901 cx.assert_state_with_diff(both_hunks_expanded.clone());
16902
16903 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16904 let snapshot = editor.snapshot(window, cx);
16905 let hunks = editor
16906 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16907 .collect::<Vec<_>>();
16908 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16909 let buffer_id = hunks[0].buffer_id;
16910 hunks
16911 .into_iter()
16912 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16913 .collect::<Vec<_>>()
16914 });
16915 assert_eq!(hunk_ranges.len(), 2);
16916
16917 cx.update_editor(|editor, _, cx| {
16918 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16919 });
16920 executor.run_until_parked();
16921
16922 let second_hunk_expanded = r#"
16923 ˇA
16924 b
16925 - c
16926 + C
16927 "#
16928 .unindent();
16929
16930 cx.assert_state_with_diff(second_hunk_expanded);
16931
16932 cx.update_editor(|editor, _, cx| {
16933 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16934 });
16935 executor.run_until_parked();
16936
16937 cx.assert_state_with_diff(both_hunks_expanded.clone());
16938
16939 cx.update_editor(|editor, _, cx| {
16940 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16941 });
16942 executor.run_until_parked();
16943
16944 let first_hunk_expanded = r#"
16945 - a
16946 + ˇA
16947 b
16948 C
16949 "#
16950 .unindent();
16951
16952 cx.assert_state_with_diff(first_hunk_expanded);
16953
16954 cx.update_editor(|editor, _, cx| {
16955 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16956 });
16957 executor.run_until_parked();
16958
16959 cx.assert_state_with_diff(both_hunks_expanded);
16960
16961 cx.set_state(
16962 &r#"
16963 ˇA
16964 b
16965 "#
16966 .unindent(),
16967 );
16968 cx.run_until_parked();
16969
16970 // TODO this cursor position seems bad
16971 cx.assert_state_with_diff(
16972 r#"
16973 - ˇa
16974 + A
16975 b
16976 "#
16977 .unindent(),
16978 );
16979
16980 cx.update_editor(|editor, window, cx| {
16981 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16982 });
16983
16984 cx.assert_state_with_diff(
16985 r#"
16986 - ˇa
16987 + A
16988 b
16989 - c
16990 "#
16991 .unindent(),
16992 );
16993
16994 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16995 let snapshot = editor.snapshot(window, cx);
16996 let hunks = editor
16997 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16998 .collect::<Vec<_>>();
16999 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17000 let buffer_id = hunks[0].buffer_id;
17001 hunks
17002 .into_iter()
17003 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17004 .collect::<Vec<_>>()
17005 });
17006 assert_eq!(hunk_ranges.len(), 2);
17007
17008 cx.update_editor(|editor, _, cx| {
17009 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17010 });
17011 executor.run_until_parked();
17012
17013 cx.assert_state_with_diff(
17014 r#"
17015 - ˇa
17016 + A
17017 b
17018 "#
17019 .unindent(),
17020 );
17021}
17022
17023#[gpui::test]
17024async fn test_toggle_deletion_hunk_at_start_of_file(
17025 executor: BackgroundExecutor,
17026 cx: &mut TestAppContext,
17027) {
17028 init_test(cx, |_| {});
17029 let mut cx = EditorTestContext::new(cx).await;
17030
17031 let diff_base = r#"
17032 a
17033 b
17034 c
17035 "#
17036 .unindent();
17037
17038 cx.set_state(
17039 &r#"
17040 ˇb
17041 c
17042 "#
17043 .unindent(),
17044 );
17045 cx.set_head_text(&diff_base);
17046 cx.update_editor(|editor, window, cx| {
17047 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17048 });
17049 executor.run_until_parked();
17050
17051 let hunk_expanded = r#"
17052 - a
17053 ˇb
17054 c
17055 "#
17056 .unindent();
17057
17058 cx.assert_state_with_diff(hunk_expanded.clone());
17059
17060 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17061 let snapshot = editor.snapshot(window, cx);
17062 let hunks = editor
17063 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17064 .collect::<Vec<_>>();
17065 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17066 let buffer_id = hunks[0].buffer_id;
17067 hunks
17068 .into_iter()
17069 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17070 .collect::<Vec<_>>()
17071 });
17072 assert_eq!(hunk_ranges.len(), 1);
17073
17074 cx.update_editor(|editor, _, cx| {
17075 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17076 });
17077 executor.run_until_parked();
17078
17079 let hunk_collapsed = r#"
17080 ˇb
17081 c
17082 "#
17083 .unindent();
17084
17085 cx.assert_state_with_diff(hunk_collapsed);
17086
17087 cx.update_editor(|editor, _, cx| {
17088 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17089 });
17090 executor.run_until_parked();
17091
17092 cx.assert_state_with_diff(hunk_expanded.clone());
17093}
17094
17095#[gpui::test]
17096async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17097 init_test(cx, |_| {});
17098
17099 let fs = FakeFs::new(cx.executor());
17100 fs.insert_tree(
17101 path!("/test"),
17102 json!({
17103 ".git": {},
17104 "file-1": "ONE\n",
17105 "file-2": "TWO\n",
17106 "file-3": "THREE\n",
17107 }),
17108 )
17109 .await;
17110
17111 fs.set_head_for_repo(
17112 path!("/test/.git").as_ref(),
17113 &[
17114 ("file-1".into(), "one\n".into()),
17115 ("file-2".into(), "two\n".into()),
17116 ("file-3".into(), "three\n".into()),
17117 ],
17118 );
17119
17120 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17121 let mut buffers = vec![];
17122 for i in 1..=3 {
17123 let buffer = project
17124 .update(cx, |project, cx| {
17125 let path = format!(path!("/test/file-{}"), i);
17126 project.open_local_buffer(path, cx)
17127 })
17128 .await
17129 .unwrap();
17130 buffers.push(buffer);
17131 }
17132
17133 let multibuffer = cx.new(|cx| {
17134 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17135 multibuffer.set_all_diff_hunks_expanded(cx);
17136 for buffer in &buffers {
17137 let snapshot = buffer.read(cx).snapshot();
17138 multibuffer.set_excerpts_for_path(
17139 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17140 buffer.clone(),
17141 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17142 DEFAULT_MULTIBUFFER_CONTEXT,
17143 cx,
17144 );
17145 }
17146 multibuffer
17147 });
17148
17149 let editor = cx.add_window(|window, cx| {
17150 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17151 });
17152 cx.run_until_parked();
17153
17154 let snapshot = editor
17155 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17156 .unwrap();
17157 let hunks = snapshot
17158 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17159 .map(|hunk| match hunk {
17160 DisplayDiffHunk::Unfolded {
17161 display_row_range, ..
17162 } => display_row_range,
17163 DisplayDiffHunk::Folded { .. } => unreachable!(),
17164 })
17165 .collect::<Vec<_>>();
17166 assert_eq!(
17167 hunks,
17168 [
17169 DisplayRow(2)..DisplayRow(4),
17170 DisplayRow(7)..DisplayRow(9),
17171 DisplayRow(12)..DisplayRow(14),
17172 ]
17173 );
17174}
17175
17176#[gpui::test]
17177async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17178 init_test(cx, |_| {});
17179
17180 let mut cx = EditorTestContext::new(cx).await;
17181 cx.set_head_text(indoc! { "
17182 one
17183 two
17184 three
17185 four
17186 five
17187 "
17188 });
17189 cx.set_index_text(indoc! { "
17190 one
17191 two
17192 three
17193 four
17194 five
17195 "
17196 });
17197 cx.set_state(indoc! {"
17198 one
17199 TWO
17200 ˇTHREE
17201 FOUR
17202 five
17203 "});
17204 cx.run_until_parked();
17205 cx.update_editor(|editor, window, cx| {
17206 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17207 });
17208 cx.run_until_parked();
17209 cx.assert_index_text(Some(indoc! {"
17210 one
17211 TWO
17212 THREE
17213 FOUR
17214 five
17215 "}));
17216 cx.set_state(indoc! { "
17217 one
17218 TWO
17219 ˇTHREE-HUNDRED
17220 FOUR
17221 five
17222 "});
17223 cx.run_until_parked();
17224 cx.update_editor(|editor, window, cx| {
17225 let snapshot = editor.snapshot(window, cx);
17226 let hunks = editor
17227 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17228 .collect::<Vec<_>>();
17229 assert_eq!(hunks.len(), 1);
17230 assert_eq!(
17231 hunks[0].status(),
17232 DiffHunkStatus {
17233 kind: DiffHunkStatusKind::Modified,
17234 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17235 }
17236 );
17237
17238 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17239 });
17240 cx.run_until_parked();
17241 cx.assert_index_text(Some(indoc! {"
17242 one
17243 TWO
17244 THREE-HUNDRED
17245 FOUR
17246 five
17247 "}));
17248}
17249
17250#[gpui::test]
17251fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17252 init_test(cx, |_| {});
17253
17254 let editor = cx.add_window(|window, cx| {
17255 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17256 build_editor(buffer, window, cx)
17257 });
17258
17259 let render_args = Arc::new(Mutex::new(None));
17260 let snapshot = editor
17261 .update(cx, |editor, window, cx| {
17262 let snapshot = editor.buffer().read(cx).snapshot(cx);
17263 let range =
17264 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17265
17266 struct RenderArgs {
17267 row: MultiBufferRow,
17268 folded: bool,
17269 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17270 }
17271
17272 let crease = Crease::inline(
17273 range,
17274 FoldPlaceholder::test(),
17275 {
17276 let toggle_callback = render_args.clone();
17277 move |row, folded, callback, _window, _cx| {
17278 *toggle_callback.lock() = Some(RenderArgs {
17279 row,
17280 folded,
17281 callback,
17282 });
17283 div()
17284 }
17285 },
17286 |_row, _folded, _window, _cx| div(),
17287 );
17288
17289 editor.insert_creases(Some(crease), cx);
17290 let snapshot = editor.snapshot(window, cx);
17291 let _div = snapshot.render_crease_toggle(
17292 MultiBufferRow(1),
17293 false,
17294 cx.entity().clone(),
17295 window,
17296 cx,
17297 );
17298 snapshot
17299 })
17300 .unwrap();
17301
17302 let render_args = render_args.lock().take().unwrap();
17303 assert_eq!(render_args.row, MultiBufferRow(1));
17304 assert!(!render_args.folded);
17305 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17306
17307 cx.update_window(*editor, |_, window, cx| {
17308 (render_args.callback)(true, window, cx)
17309 })
17310 .unwrap();
17311 let snapshot = editor
17312 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17313 .unwrap();
17314 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17315
17316 cx.update_window(*editor, |_, window, cx| {
17317 (render_args.callback)(false, window, cx)
17318 })
17319 .unwrap();
17320 let snapshot = editor
17321 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17322 .unwrap();
17323 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17324}
17325
17326#[gpui::test]
17327async fn test_input_text(cx: &mut TestAppContext) {
17328 init_test(cx, |_| {});
17329 let mut cx = EditorTestContext::new(cx).await;
17330
17331 cx.set_state(
17332 &r#"ˇone
17333 two
17334
17335 three
17336 fourˇ
17337 five
17338
17339 siˇx"#
17340 .unindent(),
17341 );
17342
17343 cx.dispatch_action(HandleInput(String::new()));
17344 cx.assert_editor_state(
17345 &r#"ˇone
17346 two
17347
17348 three
17349 fourˇ
17350 five
17351
17352 siˇx"#
17353 .unindent(),
17354 );
17355
17356 cx.dispatch_action(HandleInput("AAAA".to_string()));
17357 cx.assert_editor_state(
17358 &r#"AAAAˇone
17359 two
17360
17361 three
17362 fourAAAAˇ
17363 five
17364
17365 siAAAAˇx"#
17366 .unindent(),
17367 );
17368}
17369
17370#[gpui::test]
17371async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17372 init_test(cx, |_| {});
17373
17374 let mut cx = EditorTestContext::new(cx).await;
17375 cx.set_state(
17376 r#"let foo = 1;
17377let foo = 2;
17378let foo = 3;
17379let fooˇ = 4;
17380let foo = 5;
17381let foo = 6;
17382let foo = 7;
17383let foo = 8;
17384let foo = 9;
17385let foo = 10;
17386let foo = 11;
17387let foo = 12;
17388let foo = 13;
17389let foo = 14;
17390let foo = 15;"#,
17391 );
17392
17393 cx.update_editor(|e, window, cx| {
17394 assert_eq!(
17395 e.next_scroll_position,
17396 NextScrollCursorCenterTopBottom::Center,
17397 "Default next scroll direction is center",
17398 );
17399
17400 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17401 assert_eq!(
17402 e.next_scroll_position,
17403 NextScrollCursorCenterTopBottom::Top,
17404 "After center, next scroll direction should be top",
17405 );
17406
17407 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17408 assert_eq!(
17409 e.next_scroll_position,
17410 NextScrollCursorCenterTopBottom::Bottom,
17411 "After top, next scroll direction should be bottom",
17412 );
17413
17414 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17415 assert_eq!(
17416 e.next_scroll_position,
17417 NextScrollCursorCenterTopBottom::Center,
17418 "After bottom, scrolling should start over",
17419 );
17420
17421 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17422 assert_eq!(
17423 e.next_scroll_position,
17424 NextScrollCursorCenterTopBottom::Top,
17425 "Scrolling continues if retriggered fast enough"
17426 );
17427 });
17428
17429 cx.executor()
17430 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17431 cx.executor().run_until_parked();
17432 cx.update_editor(|e, _, _| {
17433 assert_eq!(
17434 e.next_scroll_position,
17435 NextScrollCursorCenterTopBottom::Center,
17436 "If scrolling is not triggered fast enough, it should reset"
17437 );
17438 });
17439}
17440
17441#[gpui::test]
17442async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17443 init_test(cx, |_| {});
17444 let mut cx = EditorLspTestContext::new_rust(
17445 lsp::ServerCapabilities {
17446 definition_provider: Some(lsp::OneOf::Left(true)),
17447 references_provider: Some(lsp::OneOf::Left(true)),
17448 ..lsp::ServerCapabilities::default()
17449 },
17450 cx,
17451 )
17452 .await;
17453
17454 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17455 let go_to_definition = cx
17456 .lsp
17457 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17458 move |params, _| async move {
17459 if empty_go_to_definition {
17460 Ok(None)
17461 } else {
17462 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17463 uri: params.text_document_position_params.text_document.uri,
17464 range: lsp::Range::new(
17465 lsp::Position::new(4, 3),
17466 lsp::Position::new(4, 6),
17467 ),
17468 })))
17469 }
17470 },
17471 );
17472 let references = cx
17473 .lsp
17474 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17475 Ok(Some(vec![lsp::Location {
17476 uri: params.text_document_position.text_document.uri,
17477 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17478 }]))
17479 });
17480 (go_to_definition, references)
17481 };
17482
17483 cx.set_state(
17484 &r#"fn one() {
17485 let mut a = ˇtwo();
17486 }
17487
17488 fn two() {}"#
17489 .unindent(),
17490 );
17491 set_up_lsp_handlers(false, &mut cx);
17492 let navigated = cx
17493 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17494 .await
17495 .expect("Failed to navigate to definition");
17496 assert_eq!(
17497 navigated,
17498 Navigated::Yes,
17499 "Should have navigated to definition from the GetDefinition response"
17500 );
17501 cx.assert_editor_state(
17502 &r#"fn one() {
17503 let mut a = two();
17504 }
17505
17506 fn «twoˇ»() {}"#
17507 .unindent(),
17508 );
17509
17510 let editors = cx.update_workspace(|workspace, _, cx| {
17511 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17512 });
17513 cx.update_editor(|_, _, test_editor_cx| {
17514 assert_eq!(
17515 editors.len(),
17516 1,
17517 "Initially, only one, test, editor should be open in the workspace"
17518 );
17519 assert_eq!(
17520 test_editor_cx.entity(),
17521 editors.last().expect("Asserted len is 1").clone()
17522 );
17523 });
17524
17525 set_up_lsp_handlers(true, &mut cx);
17526 let navigated = cx
17527 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17528 .await
17529 .expect("Failed to navigate to lookup references");
17530 assert_eq!(
17531 navigated,
17532 Navigated::Yes,
17533 "Should have navigated to references as a fallback after empty GoToDefinition response"
17534 );
17535 // We should not change the selections in the existing file,
17536 // if opening another milti buffer with the references
17537 cx.assert_editor_state(
17538 &r#"fn one() {
17539 let mut a = two();
17540 }
17541
17542 fn «twoˇ»() {}"#
17543 .unindent(),
17544 );
17545 let editors = cx.update_workspace(|workspace, _, cx| {
17546 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17547 });
17548 cx.update_editor(|_, _, test_editor_cx| {
17549 assert_eq!(
17550 editors.len(),
17551 2,
17552 "After falling back to references search, we open a new editor with the results"
17553 );
17554 let references_fallback_text = editors
17555 .into_iter()
17556 .find(|new_editor| *new_editor != test_editor_cx.entity())
17557 .expect("Should have one non-test editor now")
17558 .read(test_editor_cx)
17559 .text(test_editor_cx);
17560 assert_eq!(
17561 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17562 "Should use the range from the references response and not the GoToDefinition one"
17563 );
17564 });
17565}
17566
17567#[gpui::test]
17568async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17569 init_test(cx, |_| {});
17570 cx.update(|cx| {
17571 let mut editor_settings = EditorSettings::get_global(cx).clone();
17572 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17573 EditorSettings::override_global(editor_settings, cx);
17574 });
17575 let mut cx = EditorLspTestContext::new_rust(
17576 lsp::ServerCapabilities {
17577 definition_provider: Some(lsp::OneOf::Left(true)),
17578 references_provider: Some(lsp::OneOf::Left(true)),
17579 ..lsp::ServerCapabilities::default()
17580 },
17581 cx,
17582 )
17583 .await;
17584 let original_state = r#"fn one() {
17585 let mut a = ˇtwo();
17586 }
17587
17588 fn two() {}"#
17589 .unindent();
17590 cx.set_state(&original_state);
17591
17592 let mut go_to_definition = cx
17593 .lsp
17594 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17595 move |_, _| async move { Ok(None) },
17596 );
17597 let _references = cx
17598 .lsp
17599 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17600 panic!("Should not call for references with no go to definition fallback")
17601 });
17602
17603 let navigated = cx
17604 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17605 .await
17606 .expect("Failed to navigate to lookup references");
17607 go_to_definition
17608 .next()
17609 .await
17610 .expect("Should have called the go_to_definition handler");
17611
17612 assert_eq!(
17613 navigated,
17614 Navigated::No,
17615 "Should have navigated to references as a fallback after empty GoToDefinition response"
17616 );
17617 cx.assert_editor_state(&original_state);
17618 let editors = cx.update_workspace(|workspace, _, cx| {
17619 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17620 });
17621 cx.update_editor(|_, _, _| {
17622 assert_eq!(
17623 editors.len(),
17624 1,
17625 "After unsuccessful fallback, no other editor should have been opened"
17626 );
17627 });
17628}
17629
17630#[gpui::test]
17631async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17632 init_test(cx, |_| {});
17633
17634 let language = Arc::new(Language::new(
17635 LanguageConfig::default(),
17636 Some(tree_sitter_rust::LANGUAGE.into()),
17637 ));
17638
17639 let text = r#"
17640 #[cfg(test)]
17641 mod tests() {
17642 #[test]
17643 fn runnable_1() {
17644 let a = 1;
17645 }
17646
17647 #[test]
17648 fn runnable_2() {
17649 let a = 1;
17650 let b = 2;
17651 }
17652 }
17653 "#
17654 .unindent();
17655
17656 let fs = FakeFs::new(cx.executor());
17657 fs.insert_file("/file.rs", Default::default()).await;
17658
17659 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17660 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17661 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17662 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17663 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17664
17665 let editor = cx.new_window_entity(|window, cx| {
17666 Editor::new(
17667 EditorMode::full(),
17668 multi_buffer,
17669 Some(project.clone()),
17670 window,
17671 cx,
17672 )
17673 });
17674
17675 editor.update_in(cx, |editor, window, cx| {
17676 let snapshot = editor.buffer().read(cx).snapshot(cx);
17677 editor.tasks.insert(
17678 (buffer.read(cx).remote_id(), 3),
17679 RunnableTasks {
17680 templates: vec![],
17681 offset: snapshot.anchor_before(43),
17682 column: 0,
17683 extra_variables: HashMap::default(),
17684 context_range: BufferOffset(43)..BufferOffset(85),
17685 },
17686 );
17687 editor.tasks.insert(
17688 (buffer.read(cx).remote_id(), 8),
17689 RunnableTasks {
17690 templates: vec![],
17691 offset: snapshot.anchor_before(86),
17692 column: 0,
17693 extra_variables: HashMap::default(),
17694 context_range: BufferOffset(86)..BufferOffset(191),
17695 },
17696 );
17697
17698 // Test finding task when cursor is inside function body
17699 editor.change_selections(None, window, cx, |s| {
17700 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17701 });
17702 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17703 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17704
17705 // Test finding task when cursor is on function name
17706 editor.change_selections(None, window, cx, |s| {
17707 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17708 });
17709 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17710 assert_eq!(row, 8, "Should find task when cursor is on function name");
17711 });
17712}
17713
17714#[gpui::test]
17715async fn test_folding_buffers(cx: &mut TestAppContext) {
17716 init_test(cx, |_| {});
17717
17718 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17719 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17720 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17721
17722 let fs = FakeFs::new(cx.executor());
17723 fs.insert_tree(
17724 path!("/a"),
17725 json!({
17726 "first.rs": sample_text_1,
17727 "second.rs": sample_text_2,
17728 "third.rs": sample_text_3,
17729 }),
17730 )
17731 .await;
17732 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17733 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17734 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17735 let worktree = project.update(cx, |project, cx| {
17736 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17737 assert_eq!(worktrees.len(), 1);
17738 worktrees.pop().unwrap()
17739 });
17740 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17741
17742 let buffer_1 = project
17743 .update(cx, |project, cx| {
17744 project.open_buffer((worktree_id, "first.rs"), cx)
17745 })
17746 .await
17747 .unwrap();
17748 let buffer_2 = project
17749 .update(cx, |project, cx| {
17750 project.open_buffer((worktree_id, "second.rs"), cx)
17751 })
17752 .await
17753 .unwrap();
17754 let buffer_3 = project
17755 .update(cx, |project, cx| {
17756 project.open_buffer((worktree_id, "third.rs"), cx)
17757 })
17758 .await
17759 .unwrap();
17760
17761 let multi_buffer = cx.new(|cx| {
17762 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17763 multi_buffer.push_excerpts(
17764 buffer_1.clone(),
17765 [
17766 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17767 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17768 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17769 ],
17770 cx,
17771 );
17772 multi_buffer.push_excerpts(
17773 buffer_2.clone(),
17774 [
17775 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17776 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17777 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17778 ],
17779 cx,
17780 );
17781 multi_buffer.push_excerpts(
17782 buffer_3.clone(),
17783 [
17784 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17785 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17786 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17787 ],
17788 cx,
17789 );
17790 multi_buffer
17791 });
17792 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17793 Editor::new(
17794 EditorMode::full(),
17795 multi_buffer.clone(),
17796 Some(project.clone()),
17797 window,
17798 cx,
17799 )
17800 });
17801
17802 assert_eq!(
17803 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17804 "\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",
17805 );
17806
17807 multi_buffer_editor.update(cx, |editor, cx| {
17808 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17809 });
17810 assert_eq!(
17811 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17812 "\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",
17813 "After folding the first buffer, its text should not be displayed"
17814 );
17815
17816 multi_buffer_editor.update(cx, |editor, cx| {
17817 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17818 });
17819 assert_eq!(
17820 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17821 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17822 "After folding the second buffer, its text should not be displayed"
17823 );
17824
17825 multi_buffer_editor.update(cx, |editor, cx| {
17826 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17827 });
17828 assert_eq!(
17829 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17830 "\n\n\n\n\n",
17831 "After folding the third buffer, its text should not be displayed"
17832 );
17833
17834 // Emulate selection inside the fold logic, that should work
17835 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17836 editor
17837 .snapshot(window, cx)
17838 .next_line_boundary(Point::new(0, 4));
17839 });
17840
17841 multi_buffer_editor.update(cx, |editor, cx| {
17842 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17843 });
17844 assert_eq!(
17845 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17846 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17847 "After unfolding the second buffer, its text should be displayed"
17848 );
17849
17850 // Typing inside of buffer 1 causes that buffer to be unfolded.
17851 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17852 assert_eq!(
17853 multi_buffer
17854 .read(cx)
17855 .snapshot(cx)
17856 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17857 .collect::<String>(),
17858 "bbbb"
17859 );
17860 editor.change_selections(None, window, cx, |selections| {
17861 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17862 });
17863 editor.handle_input("B", window, cx);
17864 });
17865
17866 assert_eq!(
17867 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17868 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17869 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17870 );
17871
17872 multi_buffer_editor.update(cx, |editor, cx| {
17873 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17874 });
17875 assert_eq!(
17876 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17877 "\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",
17878 "After unfolding the all buffers, all original text should be displayed"
17879 );
17880}
17881
17882#[gpui::test]
17883async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17884 init_test(cx, |_| {});
17885
17886 let sample_text_1 = "1111\n2222\n3333".to_string();
17887 let sample_text_2 = "4444\n5555\n6666".to_string();
17888 let sample_text_3 = "7777\n8888\n9999".to_string();
17889
17890 let fs = FakeFs::new(cx.executor());
17891 fs.insert_tree(
17892 path!("/a"),
17893 json!({
17894 "first.rs": sample_text_1,
17895 "second.rs": sample_text_2,
17896 "third.rs": sample_text_3,
17897 }),
17898 )
17899 .await;
17900 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17901 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17902 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17903 let worktree = project.update(cx, |project, cx| {
17904 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17905 assert_eq!(worktrees.len(), 1);
17906 worktrees.pop().unwrap()
17907 });
17908 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17909
17910 let buffer_1 = project
17911 .update(cx, |project, cx| {
17912 project.open_buffer((worktree_id, "first.rs"), cx)
17913 })
17914 .await
17915 .unwrap();
17916 let buffer_2 = project
17917 .update(cx, |project, cx| {
17918 project.open_buffer((worktree_id, "second.rs"), cx)
17919 })
17920 .await
17921 .unwrap();
17922 let buffer_3 = project
17923 .update(cx, |project, cx| {
17924 project.open_buffer((worktree_id, "third.rs"), cx)
17925 })
17926 .await
17927 .unwrap();
17928
17929 let multi_buffer = cx.new(|cx| {
17930 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17931 multi_buffer.push_excerpts(
17932 buffer_1.clone(),
17933 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17934 cx,
17935 );
17936 multi_buffer.push_excerpts(
17937 buffer_2.clone(),
17938 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17939 cx,
17940 );
17941 multi_buffer.push_excerpts(
17942 buffer_3.clone(),
17943 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17944 cx,
17945 );
17946 multi_buffer
17947 });
17948
17949 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17950 Editor::new(
17951 EditorMode::full(),
17952 multi_buffer,
17953 Some(project.clone()),
17954 window,
17955 cx,
17956 )
17957 });
17958
17959 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17960 assert_eq!(
17961 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17962 full_text,
17963 );
17964
17965 multi_buffer_editor.update(cx, |editor, cx| {
17966 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17967 });
17968 assert_eq!(
17969 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17970 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17971 "After folding the first buffer, its text should not be displayed"
17972 );
17973
17974 multi_buffer_editor.update(cx, |editor, cx| {
17975 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17976 });
17977
17978 assert_eq!(
17979 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17980 "\n\n\n\n\n\n7777\n8888\n9999",
17981 "After folding the second buffer, its text should not be displayed"
17982 );
17983
17984 multi_buffer_editor.update(cx, |editor, cx| {
17985 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17986 });
17987 assert_eq!(
17988 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17989 "\n\n\n\n\n",
17990 "After folding the third buffer, its text should not be displayed"
17991 );
17992
17993 multi_buffer_editor.update(cx, |editor, cx| {
17994 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17995 });
17996 assert_eq!(
17997 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17998 "\n\n\n\n4444\n5555\n6666\n\n",
17999 "After unfolding the second buffer, its text should be displayed"
18000 );
18001
18002 multi_buffer_editor.update(cx, |editor, cx| {
18003 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18004 });
18005 assert_eq!(
18006 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18007 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18008 "After unfolding the first buffer, its text should be displayed"
18009 );
18010
18011 multi_buffer_editor.update(cx, |editor, cx| {
18012 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18013 });
18014 assert_eq!(
18015 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18016 full_text,
18017 "After unfolding all buffers, all original text should be displayed"
18018 );
18019}
18020
18021#[gpui::test]
18022async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18023 init_test(cx, |_| {});
18024
18025 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18026
18027 let fs = FakeFs::new(cx.executor());
18028 fs.insert_tree(
18029 path!("/a"),
18030 json!({
18031 "main.rs": sample_text,
18032 }),
18033 )
18034 .await;
18035 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18036 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18037 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18038 let worktree = project.update(cx, |project, cx| {
18039 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18040 assert_eq!(worktrees.len(), 1);
18041 worktrees.pop().unwrap()
18042 });
18043 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18044
18045 let buffer_1 = project
18046 .update(cx, |project, cx| {
18047 project.open_buffer((worktree_id, "main.rs"), cx)
18048 })
18049 .await
18050 .unwrap();
18051
18052 let multi_buffer = cx.new(|cx| {
18053 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18054 multi_buffer.push_excerpts(
18055 buffer_1.clone(),
18056 [ExcerptRange::new(
18057 Point::new(0, 0)
18058 ..Point::new(
18059 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18060 0,
18061 ),
18062 )],
18063 cx,
18064 );
18065 multi_buffer
18066 });
18067 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18068 Editor::new(
18069 EditorMode::full(),
18070 multi_buffer,
18071 Some(project.clone()),
18072 window,
18073 cx,
18074 )
18075 });
18076
18077 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18078 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18079 enum TestHighlight {}
18080 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18081 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18082 editor.highlight_text::<TestHighlight>(
18083 vec![highlight_range.clone()],
18084 HighlightStyle::color(Hsla::green()),
18085 cx,
18086 );
18087 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18088 });
18089
18090 let full_text = format!("\n\n{sample_text}");
18091 assert_eq!(
18092 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18093 full_text,
18094 );
18095}
18096
18097#[gpui::test]
18098async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18099 init_test(cx, |_| {});
18100 cx.update(|cx| {
18101 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18102 "keymaps/default-linux.json",
18103 cx,
18104 )
18105 .unwrap();
18106 cx.bind_keys(default_key_bindings);
18107 });
18108
18109 let (editor, cx) = cx.add_window_view(|window, cx| {
18110 let multi_buffer = MultiBuffer::build_multi(
18111 [
18112 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18113 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18114 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18115 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18116 ],
18117 cx,
18118 );
18119 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18120
18121 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18122 // fold all but the second buffer, so that we test navigating between two
18123 // adjacent folded buffers, as well as folded buffers at the start and
18124 // end the multibuffer
18125 editor.fold_buffer(buffer_ids[0], cx);
18126 editor.fold_buffer(buffer_ids[2], cx);
18127 editor.fold_buffer(buffer_ids[3], cx);
18128
18129 editor
18130 });
18131 cx.simulate_resize(size(px(1000.), px(1000.)));
18132
18133 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18134 cx.assert_excerpts_with_selections(indoc! {"
18135 [EXCERPT]
18136 ˇ[FOLDED]
18137 [EXCERPT]
18138 a1
18139 b1
18140 [EXCERPT]
18141 [FOLDED]
18142 [EXCERPT]
18143 [FOLDED]
18144 "
18145 });
18146 cx.simulate_keystroke("down");
18147 cx.assert_excerpts_with_selections(indoc! {"
18148 [EXCERPT]
18149 [FOLDED]
18150 [EXCERPT]
18151 ˇa1
18152 b1
18153 [EXCERPT]
18154 [FOLDED]
18155 [EXCERPT]
18156 [FOLDED]
18157 "
18158 });
18159 cx.simulate_keystroke("down");
18160 cx.assert_excerpts_with_selections(indoc! {"
18161 [EXCERPT]
18162 [FOLDED]
18163 [EXCERPT]
18164 a1
18165 ˇb1
18166 [EXCERPT]
18167 [FOLDED]
18168 [EXCERPT]
18169 [FOLDED]
18170 "
18171 });
18172 cx.simulate_keystroke("down");
18173 cx.assert_excerpts_with_selections(indoc! {"
18174 [EXCERPT]
18175 [FOLDED]
18176 [EXCERPT]
18177 a1
18178 b1
18179 ˇ[EXCERPT]
18180 [FOLDED]
18181 [EXCERPT]
18182 [FOLDED]
18183 "
18184 });
18185 cx.simulate_keystroke("down");
18186 cx.assert_excerpts_with_selections(indoc! {"
18187 [EXCERPT]
18188 [FOLDED]
18189 [EXCERPT]
18190 a1
18191 b1
18192 [EXCERPT]
18193 ˇ[FOLDED]
18194 [EXCERPT]
18195 [FOLDED]
18196 "
18197 });
18198 for _ in 0..5 {
18199 cx.simulate_keystroke("down");
18200 cx.assert_excerpts_with_selections(indoc! {"
18201 [EXCERPT]
18202 [FOLDED]
18203 [EXCERPT]
18204 a1
18205 b1
18206 [EXCERPT]
18207 [FOLDED]
18208 [EXCERPT]
18209 ˇ[FOLDED]
18210 "
18211 });
18212 }
18213
18214 cx.simulate_keystroke("up");
18215 cx.assert_excerpts_with_selections(indoc! {"
18216 [EXCERPT]
18217 [FOLDED]
18218 [EXCERPT]
18219 a1
18220 b1
18221 [EXCERPT]
18222 ˇ[FOLDED]
18223 [EXCERPT]
18224 [FOLDED]
18225 "
18226 });
18227 cx.simulate_keystroke("up");
18228 cx.assert_excerpts_with_selections(indoc! {"
18229 [EXCERPT]
18230 [FOLDED]
18231 [EXCERPT]
18232 a1
18233 b1
18234 ˇ[EXCERPT]
18235 [FOLDED]
18236 [EXCERPT]
18237 [FOLDED]
18238 "
18239 });
18240 cx.simulate_keystroke("up");
18241 cx.assert_excerpts_with_selections(indoc! {"
18242 [EXCERPT]
18243 [FOLDED]
18244 [EXCERPT]
18245 a1
18246 ˇb1
18247 [EXCERPT]
18248 [FOLDED]
18249 [EXCERPT]
18250 [FOLDED]
18251 "
18252 });
18253 cx.simulate_keystroke("up");
18254 cx.assert_excerpts_with_selections(indoc! {"
18255 [EXCERPT]
18256 [FOLDED]
18257 [EXCERPT]
18258 ˇa1
18259 b1
18260 [EXCERPT]
18261 [FOLDED]
18262 [EXCERPT]
18263 [FOLDED]
18264 "
18265 });
18266 for _ in 0..5 {
18267 cx.simulate_keystroke("up");
18268 cx.assert_excerpts_with_selections(indoc! {"
18269 [EXCERPT]
18270 ˇ[FOLDED]
18271 [EXCERPT]
18272 a1
18273 b1
18274 [EXCERPT]
18275 [FOLDED]
18276 [EXCERPT]
18277 [FOLDED]
18278 "
18279 });
18280 }
18281}
18282
18283#[gpui::test]
18284async fn test_inline_completion_text(cx: &mut TestAppContext) {
18285 init_test(cx, |_| {});
18286
18287 // Simple insertion
18288 assert_highlighted_edits(
18289 "Hello, world!",
18290 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18291 true,
18292 cx,
18293 |highlighted_edits, cx| {
18294 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18295 assert_eq!(highlighted_edits.highlights.len(), 1);
18296 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18297 assert_eq!(
18298 highlighted_edits.highlights[0].1.background_color,
18299 Some(cx.theme().status().created_background)
18300 );
18301 },
18302 )
18303 .await;
18304
18305 // Replacement
18306 assert_highlighted_edits(
18307 "This is a test.",
18308 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18309 false,
18310 cx,
18311 |highlighted_edits, cx| {
18312 assert_eq!(highlighted_edits.text, "That is a test.");
18313 assert_eq!(highlighted_edits.highlights.len(), 1);
18314 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18315 assert_eq!(
18316 highlighted_edits.highlights[0].1.background_color,
18317 Some(cx.theme().status().created_background)
18318 );
18319 },
18320 )
18321 .await;
18322
18323 // Multiple edits
18324 assert_highlighted_edits(
18325 "Hello, world!",
18326 vec![
18327 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18328 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18329 ],
18330 false,
18331 cx,
18332 |highlighted_edits, cx| {
18333 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18334 assert_eq!(highlighted_edits.highlights.len(), 2);
18335 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18336 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18337 assert_eq!(
18338 highlighted_edits.highlights[0].1.background_color,
18339 Some(cx.theme().status().created_background)
18340 );
18341 assert_eq!(
18342 highlighted_edits.highlights[1].1.background_color,
18343 Some(cx.theme().status().created_background)
18344 );
18345 },
18346 )
18347 .await;
18348
18349 // Multiple lines with edits
18350 assert_highlighted_edits(
18351 "First line\nSecond line\nThird line\nFourth line",
18352 vec![
18353 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18354 (
18355 Point::new(2, 0)..Point::new(2, 10),
18356 "New third line".to_string(),
18357 ),
18358 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18359 ],
18360 false,
18361 cx,
18362 |highlighted_edits, cx| {
18363 assert_eq!(
18364 highlighted_edits.text,
18365 "Second modified\nNew third line\nFourth updated line"
18366 );
18367 assert_eq!(highlighted_edits.highlights.len(), 3);
18368 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18369 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18370 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18371 for highlight in &highlighted_edits.highlights {
18372 assert_eq!(
18373 highlight.1.background_color,
18374 Some(cx.theme().status().created_background)
18375 );
18376 }
18377 },
18378 )
18379 .await;
18380}
18381
18382#[gpui::test]
18383async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18384 init_test(cx, |_| {});
18385
18386 // Deletion
18387 assert_highlighted_edits(
18388 "Hello, world!",
18389 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18390 true,
18391 cx,
18392 |highlighted_edits, cx| {
18393 assert_eq!(highlighted_edits.text, "Hello, world!");
18394 assert_eq!(highlighted_edits.highlights.len(), 1);
18395 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18396 assert_eq!(
18397 highlighted_edits.highlights[0].1.background_color,
18398 Some(cx.theme().status().deleted_background)
18399 );
18400 },
18401 )
18402 .await;
18403
18404 // Insertion
18405 assert_highlighted_edits(
18406 "Hello, world!",
18407 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18408 true,
18409 cx,
18410 |highlighted_edits, cx| {
18411 assert_eq!(highlighted_edits.highlights.len(), 1);
18412 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18413 assert_eq!(
18414 highlighted_edits.highlights[0].1.background_color,
18415 Some(cx.theme().status().created_background)
18416 );
18417 },
18418 )
18419 .await;
18420}
18421
18422async fn assert_highlighted_edits(
18423 text: &str,
18424 edits: Vec<(Range<Point>, String)>,
18425 include_deletions: bool,
18426 cx: &mut TestAppContext,
18427 assertion_fn: impl Fn(HighlightedText, &App),
18428) {
18429 let window = cx.add_window(|window, cx| {
18430 let buffer = MultiBuffer::build_simple(text, cx);
18431 Editor::new(EditorMode::full(), buffer, None, window, cx)
18432 });
18433 let cx = &mut VisualTestContext::from_window(*window, cx);
18434
18435 let (buffer, snapshot) = window
18436 .update(cx, |editor, _window, cx| {
18437 (
18438 editor.buffer().clone(),
18439 editor.buffer().read(cx).snapshot(cx),
18440 )
18441 })
18442 .unwrap();
18443
18444 let edits = edits
18445 .into_iter()
18446 .map(|(range, edit)| {
18447 (
18448 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18449 edit,
18450 )
18451 })
18452 .collect::<Vec<_>>();
18453
18454 let text_anchor_edits = edits
18455 .clone()
18456 .into_iter()
18457 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18458 .collect::<Vec<_>>();
18459
18460 let edit_preview = window
18461 .update(cx, |_, _window, cx| {
18462 buffer
18463 .read(cx)
18464 .as_singleton()
18465 .unwrap()
18466 .read(cx)
18467 .preview_edits(text_anchor_edits.into(), cx)
18468 })
18469 .unwrap()
18470 .await;
18471
18472 cx.update(|_window, cx| {
18473 let highlighted_edits = inline_completion_edit_text(
18474 &snapshot.as_singleton().unwrap().2,
18475 &edits,
18476 &edit_preview,
18477 include_deletions,
18478 cx,
18479 );
18480 assertion_fn(highlighted_edits, cx)
18481 });
18482}
18483
18484#[track_caller]
18485fn assert_breakpoint(
18486 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18487 path: &Arc<Path>,
18488 expected: Vec<(u32, Breakpoint)>,
18489) {
18490 if expected.len() == 0usize {
18491 assert!(!breakpoints.contains_key(path), "{}", path.display());
18492 } else {
18493 let mut breakpoint = breakpoints
18494 .get(path)
18495 .unwrap()
18496 .into_iter()
18497 .map(|breakpoint| {
18498 (
18499 breakpoint.row,
18500 Breakpoint {
18501 message: breakpoint.message.clone(),
18502 state: breakpoint.state,
18503 condition: breakpoint.condition.clone(),
18504 hit_condition: breakpoint.hit_condition.clone(),
18505 },
18506 )
18507 })
18508 .collect::<Vec<_>>();
18509
18510 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18511
18512 assert_eq!(expected, breakpoint);
18513 }
18514}
18515
18516fn add_log_breakpoint_at_cursor(
18517 editor: &mut Editor,
18518 log_message: &str,
18519 window: &mut Window,
18520 cx: &mut Context<Editor>,
18521) {
18522 let (anchor, bp) = editor
18523 .breakpoints_at_cursors(window, cx)
18524 .first()
18525 .and_then(|(anchor, bp)| {
18526 if let Some(bp) = bp {
18527 Some((*anchor, bp.clone()))
18528 } else {
18529 None
18530 }
18531 })
18532 .unwrap_or_else(|| {
18533 let cursor_position: Point = editor.selections.newest(cx).head();
18534
18535 let breakpoint_position = editor
18536 .snapshot(window, cx)
18537 .display_snapshot
18538 .buffer_snapshot
18539 .anchor_before(Point::new(cursor_position.row, 0));
18540
18541 (breakpoint_position, Breakpoint::new_log(&log_message))
18542 });
18543
18544 editor.edit_breakpoint_at_anchor(
18545 anchor,
18546 bp,
18547 BreakpointEditAction::EditLogMessage(log_message.into()),
18548 cx,
18549 );
18550}
18551
18552#[gpui::test]
18553async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18554 init_test(cx, |_| {});
18555
18556 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18557 let fs = FakeFs::new(cx.executor());
18558 fs.insert_tree(
18559 path!("/a"),
18560 json!({
18561 "main.rs": sample_text,
18562 }),
18563 )
18564 .await;
18565 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18566 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18567 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18568
18569 let fs = FakeFs::new(cx.executor());
18570 fs.insert_tree(
18571 path!("/a"),
18572 json!({
18573 "main.rs": sample_text,
18574 }),
18575 )
18576 .await;
18577 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18578 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18579 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18580 let worktree_id = workspace
18581 .update(cx, |workspace, _window, cx| {
18582 workspace.project().update(cx, |project, cx| {
18583 project.worktrees(cx).next().unwrap().read(cx).id()
18584 })
18585 })
18586 .unwrap();
18587
18588 let buffer = project
18589 .update(cx, |project, cx| {
18590 project.open_buffer((worktree_id, "main.rs"), cx)
18591 })
18592 .await
18593 .unwrap();
18594
18595 let (editor, cx) = cx.add_window_view(|window, cx| {
18596 Editor::new(
18597 EditorMode::full(),
18598 MultiBuffer::build_from_buffer(buffer, cx),
18599 Some(project.clone()),
18600 window,
18601 cx,
18602 )
18603 });
18604
18605 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18606 let abs_path = project.read_with(cx, |project, cx| {
18607 project
18608 .absolute_path(&project_path, cx)
18609 .map(|path_buf| Arc::from(path_buf.to_owned()))
18610 .unwrap()
18611 });
18612
18613 // assert we can add breakpoint on the first line
18614 editor.update_in(cx, |editor, window, cx| {
18615 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18616 editor.move_to_end(&MoveToEnd, window, cx);
18617 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18618 });
18619
18620 let breakpoints = editor.update(cx, |editor, cx| {
18621 editor
18622 .breakpoint_store()
18623 .as_ref()
18624 .unwrap()
18625 .read(cx)
18626 .all_breakpoints(cx)
18627 .clone()
18628 });
18629
18630 assert_eq!(1, breakpoints.len());
18631 assert_breakpoint(
18632 &breakpoints,
18633 &abs_path,
18634 vec![
18635 (0, Breakpoint::new_standard()),
18636 (3, Breakpoint::new_standard()),
18637 ],
18638 );
18639
18640 editor.update_in(cx, |editor, window, cx| {
18641 editor.move_to_beginning(&MoveToBeginning, window, cx);
18642 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18643 });
18644
18645 let breakpoints = editor.update(cx, |editor, cx| {
18646 editor
18647 .breakpoint_store()
18648 .as_ref()
18649 .unwrap()
18650 .read(cx)
18651 .all_breakpoints(cx)
18652 .clone()
18653 });
18654
18655 assert_eq!(1, breakpoints.len());
18656 assert_breakpoint(
18657 &breakpoints,
18658 &abs_path,
18659 vec![(3, Breakpoint::new_standard())],
18660 );
18661
18662 editor.update_in(cx, |editor, window, cx| {
18663 editor.move_to_end(&MoveToEnd, window, cx);
18664 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18665 });
18666
18667 let breakpoints = editor.update(cx, |editor, cx| {
18668 editor
18669 .breakpoint_store()
18670 .as_ref()
18671 .unwrap()
18672 .read(cx)
18673 .all_breakpoints(cx)
18674 .clone()
18675 });
18676
18677 assert_eq!(0, breakpoints.len());
18678 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18679}
18680
18681#[gpui::test]
18682async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18683 init_test(cx, |_| {});
18684
18685 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18686
18687 let fs = FakeFs::new(cx.executor());
18688 fs.insert_tree(
18689 path!("/a"),
18690 json!({
18691 "main.rs": sample_text,
18692 }),
18693 )
18694 .await;
18695 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18696 let (workspace, cx) =
18697 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18698
18699 let worktree_id = workspace.update(cx, |workspace, cx| {
18700 workspace.project().update(cx, |project, cx| {
18701 project.worktrees(cx).next().unwrap().read(cx).id()
18702 })
18703 });
18704
18705 let buffer = project
18706 .update(cx, |project, cx| {
18707 project.open_buffer((worktree_id, "main.rs"), cx)
18708 })
18709 .await
18710 .unwrap();
18711
18712 let (editor, cx) = cx.add_window_view(|window, cx| {
18713 Editor::new(
18714 EditorMode::full(),
18715 MultiBuffer::build_from_buffer(buffer, cx),
18716 Some(project.clone()),
18717 window,
18718 cx,
18719 )
18720 });
18721
18722 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18723 let abs_path = project.read_with(cx, |project, cx| {
18724 project
18725 .absolute_path(&project_path, cx)
18726 .map(|path_buf| Arc::from(path_buf.to_owned()))
18727 .unwrap()
18728 });
18729
18730 editor.update_in(cx, |editor, window, cx| {
18731 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18732 });
18733
18734 let breakpoints = editor.update(cx, |editor, cx| {
18735 editor
18736 .breakpoint_store()
18737 .as_ref()
18738 .unwrap()
18739 .read(cx)
18740 .all_breakpoints(cx)
18741 .clone()
18742 });
18743
18744 assert_breakpoint(
18745 &breakpoints,
18746 &abs_path,
18747 vec![(0, Breakpoint::new_log("hello world"))],
18748 );
18749
18750 // Removing a log message from a log breakpoint should remove it
18751 editor.update_in(cx, |editor, window, cx| {
18752 add_log_breakpoint_at_cursor(editor, "", window, cx);
18753 });
18754
18755 let breakpoints = editor.update(cx, |editor, cx| {
18756 editor
18757 .breakpoint_store()
18758 .as_ref()
18759 .unwrap()
18760 .read(cx)
18761 .all_breakpoints(cx)
18762 .clone()
18763 });
18764
18765 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18766
18767 editor.update_in(cx, |editor, window, cx| {
18768 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18769 editor.move_to_end(&MoveToEnd, window, cx);
18770 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18771 // Not adding a log message to a standard breakpoint shouldn't remove it
18772 add_log_breakpoint_at_cursor(editor, "", window, cx);
18773 });
18774
18775 let breakpoints = editor.update(cx, |editor, cx| {
18776 editor
18777 .breakpoint_store()
18778 .as_ref()
18779 .unwrap()
18780 .read(cx)
18781 .all_breakpoints(cx)
18782 .clone()
18783 });
18784
18785 assert_breakpoint(
18786 &breakpoints,
18787 &abs_path,
18788 vec![
18789 (0, Breakpoint::new_standard()),
18790 (3, Breakpoint::new_standard()),
18791 ],
18792 );
18793
18794 editor.update_in(cx, |editor, window, cx| {
18795 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18796 });
18797
18798 let breakpoints = editor.update(cx, |editor, cx| {
18799 editor
18800 .breakpoint_store()
18801 .as_ref()
18802 .unwrap()
18803 .read(cx)
18804 .all_breakpoints(cx)
18805 .clone()
18806 });
18807
18808 assert_breakpoint(
18809 &breakpoints,
18810 &abs_path,
18811 vec![
18812 (0, Breakpoint::new_standard()),
18813 (3, Breakpoint::new_log("hello world")),
18814 ],
18815 );
18816
18817 editor.update_in(cx, |editor, window, cx| {
18818 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18819 });
18820
18821 let breakpoints = editor.update(cx, |editor, cx| {
18822 editor
18823 .breakpoint_store()
18824 .as_ref()
18825 .unwrap()
18826 .read(cx)
18827 .all_breakpoints(cx)
18828 .clone()
18829 });
18830
18831 assert_breakpoint(
18832 &breakpoints,
18833 &abs_path,
18834 vec![
18835 (0, Breakpoint::new_standard()),
18836 (3, Breakpoint::new_log("hello Earth!!")),
18837 ],
18838 );
18839}
18840
18841/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18842/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18843/// or when breakpoints were placed out of order. This tests for a regression too
18844#[gpui::test]
18845async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18846 init_test(cx, |_| {});
18847
18848 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18849 let fs = FakeFs::new(cx.executor());
18850 fs.insert_tree(
18851 path!("/a"),
18852 json!({
18853 "main.rs": sample_text,
18854 }),
18855 )
18856 .await;
18857 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18858 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18859 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18860
18861 let fs = FakeFs::new(cx.executor());
18862 fs.insert_tree(
18863 path!("/a"),
18864 json!({
18865 "main.rs": sample_text,
18866 }),
18867 )
18868 .await;
18869 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18870 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18871 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18872 let worktree_id = workspace
18873 .update(cx, |workspace, _window, cx| {
18874 workspace.project().update(cx, |project, cx| {
18875 project.worktrees(cx).next().unwrap().read(cx).id()
18876 })
18877 })
18878 .unwrap();
18879
18880 let buffer = project
18881 .update(cx, |project, cx| {
18882 project.open_buffer((worktree_id, "main.rs"), cx)
18883 })
18884 .await
18885 .unwrap();
18886
18887 let (editor, cx) = cx.add_window_view(|window, cx| {
18888 Editor::new(
18889 EditorMode::full(),
18890 MultiBuffer::build_from_buffer(buffer, cx),
18891 Some(project.clone()),
18892 window,
18893 cx,
18894 )
18895 });
18896
18897 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18898 let abs_path = project.read_with(cx, |project, cx| {
18899 project
18900 .absolute_path(&project_path, cx)
18901 .map(|path_buf| Arc::from(path_buf.to_owned()))
18902 .unwrap()
18903 });
18904
18905 // assert we can add breakpoint on the first line
18906 editor.update_in(cx, |editor, window, cx| {
18907 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18908 editor.move_to_end(&MoveToEnd, window, cx);
18909 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18910 editor.move_up(&MoveUp, window, cx);
18911 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18912 });
18913
18914 let breakpoints = editor.update(cx, |editor, cx| {
18915 editor
18916 .breakpoint_store()
18917 .as_ref()
18918 .unwrap()
18919 .read(cx)
18920 .all_breakpoints(cx)
18921 .clone()
18922 });
18923
18924 assert_eq!(1, breakpoints.len());
18925 assert_breakpoint(
18926 &breakpoints,
18927 &abs_path,
18928 vec![
18929 (0, Breakpoint::new_standard()),
18930 (2, Breakpoint::new_standard()),
18931 (3, Breakpoint::new_standard()),
18932 ],
18933 );
18934
18935 editor.update_in(cx, |editor, window, cx| {
18936 editor.move_to_beginning(&MoveToBeginning, window, cx);
18937 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18938 editor.move_to_end(&MoveToEnd, window, cx);
18939 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18940 // Disabling a breakpoint that doesn't exist should do nothing
18941 editor.move_up(&MoveUp, window, cx);
18942 editor.move_up(&MoveUp, window, cx);
18943 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18944 });
18945
18946 let breakpoints = editor.update(cx, |editor, cx| {
18947 editor
18948 .breakpoint_store()
18949 .as_ref()
18950 .unwrap()
18951 .read(cx)
18952 .all_breakpoints(cx)
18953 .clone()
18954 });
18955
18956 let disable_breakpoint = {
18957 let mut bp = Breakpoint::new_standard();
18958 bp.state = BreakpointState::Disabled;
18959 bp
18960 };
18961
18962 assert_eq!(1, breakpoints.len());
18963 assert_breakpoint(
18964 &breakpoints,
18965 &abs_path,
18966 vec![
18967 (0, disable_breakpoint.clone()),
18968 (2, Breakpoint::new_standard()),
18969 (3, disable_breakpoint.clone()),
18970 ],
18971 );
18972
18973 editor.update_in(cx, |editor, window, cx| {
18974 editor.move_to_beginning(&MoveToBeginning, window, cx);
18975 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18976 editor.move_to_end(&MoveToEnd, window, cx);
18977 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18978 editor.move_up(&MoveUp, window, cx);
18979 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18980 });
18981
18982 let breakpoints = editor.update(cx, |editor, cx| {
18983 editor
18984 .breakpoint_store()
18985 .as_ref()
18986 .unwrap()
18987 .read(cx)
18988 .all_breakpoints(cx)
18989 .clone()
18990 });
18991
18992 assert_eq!(1, breakpoints.len());
18993 assert_breakpoint(
18994 &breakpoints,
18995 &abs_path,
18996 vec![
18997 (0, Breakpoint::new_standard()),
18998 (2, disable_breakpoint),
18999 (3, Breakpoint::new_standard()),
19000 ],
19001 );
19002}
19003
19004#[gpui::test]
19005async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19006 init_test(cx, |_| {});
19007 let capabilities = lsp::ServerCapabilities {
19008 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19009 prepare_provider: Some(true),
19010 work_done_progress_options: Default::default(),
19011 })),
19012 ..Default::default()
19013 };
19014 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19015
19016 cx.set_state(indoc! {"
19017 struct Fˇoo {}
19018 "});
19019
19020 cx.update_editor(|editor, _, cx| {
19021 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19022 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19023 editor.highlight_background::<DocumentHighlightRead>(
19024 &[highlight_range],
19025 |c| c.editor_document_highlight_read_background,
19026 cx,
19027 );
19028 });
19029
19030 let mut prepare_rename_handler = cx
19031 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19032 move |_, _, _| async move {
19033 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19034 start: lsp::Position {
19035 line: 0,
19036 character: 7,
19037 },
19038 end: lsp::Position {
19039 line: 0,
19040 character: 10,
19041 },
19042 })))
19043 },
19044 );
19045 let prepare_rename_task = cx
19046 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19047 .expect("Prepare rename was not started");
19048 prepare_rename_handler.next().await.unwrap();
19049 prepare_rename_task.await.expect("Prepare rename failed");
19050
19051 let mut rename_handler =
19052 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19053 let edit = lsp::TextEdit {
19054 range: lsp::Range {
19055 start: lsp::Position {
19056 line: 0,
19057 character: 7,
19058 },
19059 end: lsp::Position {
19060 line: 0,
19061 character: 10,
19062 },
19063 },
19064 new_text: "FooRenamed".to_string(),
19065 };
19066 Ok(Some(lsp::WorkspaceEdit::new(
19067 // Specify the same edit twice
19068 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19069 )))
19070 });
19071 let rename_task = cx
19072 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19073 .expect("Confirm rename was not started");
19074 rename_handler.next().await.unwrap();
19075 rename_task.await.expect("Confirm rename failed");
19076 cx.run_until_parked();
19077
19078 // Despite two edits, only one is actually applied as those are identical
19079 cx.assert_editor_state(indoc! {"
19080 struct FooRenamedˇ {}
19081 "});
19082}
19083
19084#[gpui::test]
19085async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19086 init_test(cx, |_| {});
19087 // These capabilities indicate that the server does not support prepare rename.
19088 let capabilities = lsp::ServerCapabilities {
19089 rename_provider: Some(lsp::OneOf::Left(true)),
19090 ..Default::default()
19091 };
19092 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19093
19094 cx.set_state(indoc! {"
19095 struct Fˇoo {}
19096 "});
19097
19098 cx.update_editor(|editor, _window, cx| {
19099 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19100 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19101 editor.highlight_background::<DocumentHighlightRead>(
19102 &[highlight_range],
19103 |c| c.editor_document_highlight_read_background,
19104 cx,
19105 );
19106 });
19107
19108 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19109 .expect("Prepare rename was not started")
19110 .await
19111 .expect("Prepare rename failed");
19112
19113 let mut rename_handler =
19114 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19115 let edit = lsp::TextEdit {
19116 range: lsp::Range {
19117 start: lsp::Position {
19118 line: 0,
19119 character: 7,
19120 },
19121 end: lsp::Position {
19122 line: 0,
19123 character: 10,
19124 },
19125 },
19126 new_text: "FooRenamed".to_string(),
19127 };
19128 Ok(Some(lsp::WorkspaceEdit::new(
19129 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19130 )))
19131 });
19132 let rename_task = cx
19133 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19134 .expect("Confirm rename was not started");
19135 rename_handler.next().await.unwrap();
19136 rename_task.await.expect("Confirm rename failed");
19137 cx.run_until_parked();
19138
19139 // Correct range is renamed, as `surrounding_word` is used to find it.
19140 cx.assert_editor_state(indoc! {"
19141 struct FooRenamedˇ {}
19142 "});
19143}
19144
19145#[gpui::test]
19146async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19147 init_test(cx, |_| {});
19148 let mut cx = EditorTestContext::new(cx).await;
19149
19150 let language = Arc::new(
19151 Language::new(
19152 LanguageConfig::default(),
19153 Some(tree_sitter_html::LANGUAGE.into()),
19154 )
19155 .with_brackets_query(
19156 r#"
19157 ("<" @open "/>" @close)
19158 ("</" @open ">" @close)
19159 ("<" @open ">" @close)
19160 ("\"" @open "\"" @close)
19161 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19162 "#,
19163 )
19164 .unwrap(),
19165 );
19166 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19167
19168 cx.set_state(indoc! {"
19169 <span>ˇ</span>
19170 "});
19171 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19172 cx.assert_editor_state(indoc! {"
19173 <span>
19174 ˇ
19175 </span>
19176 "});
19177
19178 cx.set_state(indoc! {"
19179 <span><span></span>ˇ</span>
19180 "});
19181 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19182 cx.assert_editor_state(indoc! {"
19183 <span><span></span>
19184 ˇ</span>
19185 "});
19186
19187 cx.set_state(indoc! {"
19188 <span>ˇ
19189 </span>
19190 "});
19191 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19192 cx.assert_editor_state(indoc! {"
19193 <span>
19194 ˇ
19195 </span>
19196 "});
19197}
19198
19199#[gpui::test(iterations = 10)]
19200async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19201 init_test(cx, |_| {});
19202
19203 let fs = FakeFs::new(cx.executor());
19204 fs.insert_tree(
19205 path!("/dir"),
19206 json!({
19207 "a.ts": "a",
19208 }),
19209 )
19210 .await;
19211
19212 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19213 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19214 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19215
19216 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19217 language_registry.add(Arc::new(Language::new(
19218 LanguageConfig {
19219 name: "TypeScript".into(),
19220 matcher: LanguageMatcher {
19221 path_suffixes: vec!["ts".to_string()],
19222 ..Default::default()
19223 },
19224 ..Default::default()
19225 },
19226 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19227 )));
19228 let mut fake_language_servers = language_registry.register_fake_lsp(
19229 "TypeScript",
19230 FakeLspAdapter {
19231 capabilities: lsp::ServerCapabilities {
19232 code_lens_provider: Some(lsp::CodeLensOptions {
19233 resolve_provider: Some(true),
19234 }),
19235 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19236 commands: vec!["_the/command".to_string()],
19237 ..lsp::ExecuteCommandOptions::default()
19238 }),
19239 ..lsp::ServerCapabilities::default()
19240 },
19241 ..FakeLspAdapter::default()
19242 },
19243 );
19244
19245 let (buffer, _handle) = project
19246 .update(cx, |p, cx| {
19247 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19248 })
19249 .await
19250 .unwrap();
19251 cx.executor().run_until_parked();
19252
19253 let fake_server = fake_language_servers.next().await.unwrap();
19254
19255 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19256 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19257 drop(buffer_snapshot);
19258 let actions = cx
19259 .update_window(*workspace, |_, window, cx| {
19260 project.code_actions(&buffer, anchor..anchor, window, cx)
19261 })
19262 .unwrap();
19263
19264 fake_server
19265 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19266 Ok(Some(vec![
19267 lsp::CodeLens {
19268 range: lsp::Range::default(),
19269 command: Some(lsp::Command {
19270 title: "Code lens command".to_owned(),
19271 command: "_the/command".to_owned(),
19272 arguments: None,
19273 }),
19274 data: None,
19275 },
19276 lsp::CodeLens {
19277 range: lsp::Range::default(),
19278 command: Some(lsp::Command {
19279 title: "Command not in capabilities".to_owned(),
19280 command: "not in capabilities".to_owned(),
19281 arguments: None,
19282 }),
19283 data: None,
19284 },
19285 lsp::CodeLens {
19286 range: lsp::Range {
19287 start: lsp::Position {
19288 line: 1,
19289 character: 1,
19290 },
19291 end: lsp::Position {
19292 line: 1,
19293 character: 1,
19294 },
19295 },
19296 command: Some(lsp::Command {
19297 title: "Command not in range".to_owned(),
19298 command: "_the/command".to_owned(),
19299 arguments: None,
19300 }),
19301 data: None,
19302 },
19303 ]))
19304 })
19305 .next()
19306 .await;
19307
19308 let actions = actions.await.unwrap();
19309 assert_eq!(
19310 actions.len(),
19311 1,
19312 "Should have only one valid action for the 0..0 range"
19313 );
19314 let action = actions[0].clone();
19315 let apply = project.update(cx, |project, cx| {
19316 project.apply_code_action(buffer.clone(), action, true, cx)
19317 });
19318
19319 // Resolving the code action does not populate its edits. In absence of
19320 // edits, we must execute the given command.
19321 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19322 |mut lens, _| async move {
19323 let lens_command = lens.command.as_mut().expect("should have a command");
19324 assert_eq!(lens_command.title, "Code lens command");
19325 lens_command.arguments = Some(vec![json!("the-argument")]);
19326 Ok(lens)
19327 },
19328 );
19329
19330 // While executing the command, the language server sends the editor
19331 // a `workspaceEdit` request.
19332 fake_server
19333 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19334 let fake = fake_server.clone();
19335 move |params, _| {
19336 assert_eq!(params.command, "_the/command");
19337 let fake = fake.clone();
19338 async move {
19339 fake.server
19340 .request::<lsp::request::ApplyWorkspaceEdit>(
19341 lsp::ApplyWorkspaceEditParams {
19342 label: None,
19343 edit: lsp::WorkspaceEdit {
19344 changes: Some(
19345 [(
19346 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19347 vec![lsp::TextEdit {
19348 range: lsp::Range::new(
19349 lsp::Position::new(0, 0),
19350 lsp::Position::new(0, 0),
19351 ),
19352 new_text: "X".into(),
19353 }],
19354 )]
19355 .into_iter()
19356 .collect(),
19357 ),
19358 ..Default::default()
19359 },
19360 },
19361 )
19362 .await
19363 .into_response()
19364 .unwrap();
19365 Ok(Some(json!(null)))
19366 }
19367 }
19368 })
19369 .next()
19370 .await;
19371
19372 // Applying the code lens command returns a project transaction containing the edits
19373 // sent by the language server in its `workspaceEdit` request.
19374 let transaction = apply.await.unwrap();
19375 assert!(transaction.0.contains_key(&buffer));
19376 buffer.update(cx, |buffer, cx| {
19377 assert_eq!(buffer.text(), "Xa");
19378 buffer.undo(cx);
19379 assert_eq!(buffer.text(), "a");
19380 });
19381}
19382
19383#[gpui::test]
19384async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19385 init_test(cx, |_| {});
19386
19387 let fs = FakeFs::new(cx.executor());
19388 let main_text = r#"fn main() {
19389println!("1");
19390println!("2");
19391println!("3");
19392println!("4");
19393println!("5");
19394}"#;
19395 let lib_text = "mod foo {}";
19396 fs.insert_tree(
19397 path!("/a"),
19398 json!({
19399 "lib.rs": lib_text,
19400 "main.rs": main_text,
19401 }),
19402 )
19403 .await;
19404
19405 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19406 let (workspace, cx) =
19407 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19408 let worktree_id = workspace.update(cx, |workspace, cx| {
19409 workspace.project().update(cx, |project, cx| {
19410 project.worktrees(cx).next().unwrap().read(cx).id()
19411 })
19412 });
19413
19414 let expected_ranges = vec![
19415 Point::new(0, 0)..Point::new(0, 0),
19416 Point::new(1, 0)..Point::new(1, 1),
19417 Point::new(2, 0)..Point::new(2, 2),
19418 Point::new(3, 0)..Point::new(3, 3),
19419 ];
19420
19421 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19422 let editor_1 = workspace
19423 .update_in(cx, |workspace, window, cx| {
19424 workspace.open_path(
19425 (worktree_id, "main.rs"),
19426 Some(pane_1.downgrade()),
19427 true,
19428 window,
19429 cx,
19430 )
19431 })
19432 .unwrap()
19433 .await
19434 .downcast::<Editor>()
19435 .unwrap();
19436 pane_1.update(cx, |pane, cx| {
19437 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19438 open_editor.update(cx, |editor, cx| {
19439 assert_eq!(
19440 editor.display_text(cx),
19441 main_text,
19442 "Original main.rs text on initial open",
19443 );
19444 assert_eq!(
19445 editor
19446 .selections
19447 .all::<Point>(cx)
19448 .into_iter()
19449 .map(|s| s.range())
19450 .collect::<Vec<_>>(),
19451 vec![Point::zero()..Point::zero()],
19452 "Default selections on initial open",
19453 );
19454 })
19455 });
19456 editor_1.update_in(cx, |editor, window, cx| {
19457 editor.change_selections(None, window, cx, |s| {
19458 s.select_ranges(expected_ranges.clone());
19459 });
19460 });
19461
19462 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19463 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19464 });
19465 let editor_2 = workspace
19466 .update_in(cx, |workspace, window, cx| {
19467 workspace.open_path(
19468 (worktree_id, "main.rs"),
19469 Some(pane_2.downgrade()),
19470 true,
19471 window,
19472 cx,
19473 )
19474 })
19475 .unwrap()
19476 .await
19477 .downcast::<Editor>()
19478 .unwrap();
19479 pane_2.update(cx, |pane, cx| {
19480 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19481 open_editor.update(cx, |editor, cx| {
19482 assert_eq!(
19483 editor.display_text(cx),
19484 main_text,
19485 "Original main.rs text on initial open in another panel",
19486 );
19487 assert_eq!(
19488 editor
19489 .selections
19490 .all::<Point>(cx)
19491 .into_iter()
19492 .map(|s| s.range())
19493 .collect::<Vec<_>>(),
19494 vec![Point::zero()..Point::zero()],
19495 "Default selections on initial open in another panel",
19496 );
19497 })
19498 });
19499
19500 editor_2.update_in(cx, |editor, window, cx| {
19501 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19502 });
19503
19504 let _other_editor_1 = workspace
19505 .update_in(cx, |workspace, window, cx| {
19506 workspace.open_path(
19507 (worktree_id, "lib.rs"),
19508 Some(pane_1.downgrade()),
19509 true,
19510 window,
19511 cx,
19512 )
19513 })
19514 .unwrap()
19515 .await
19516 .downcast::<Editor>()
19517 .unwrap();
19518 pane_1
19519 .update_in(cx, |pane, window, cx| {
19520 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19521 .unwrap()
19522 })
19523 .await
19524 .unwrap();
19525 drop(editor_1);
19526 pane_1.update(cx, |pane, cx| {
19527 pane.active_item()
19528 .unwrap()
19529 .downcast::<Editor>()
19530 .unwrap()
19531 .update(cx, |editor, cx| {
19532 assert_eq!(
19533 editor.display_text(cx),
19534 lib_text,
19535 "Other file should be open and active",
19536 );
19537 });
19538 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19539 });
19540
19541 let _other_editor_2 = workspace
19542 .update_in(cx, |workspace, window, cx| {
19543 workspace.open_path(
19544 (worktree_id, "lib.rs"),
19545 Some(pane_2.downgrade()),
19546 true,
19547 window,
19548 cx,
19549 )
19550 })
19551 .unwrap()
19552 .await
19553 .downcast::<Editor>()
19554 .unwrap();
19555 pane_2
19556 .update_in(cx, |pane, window, cx| {
19557 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19558 .unwrap()
19559 })
19560 .await
19561 .unwrap();
19562 drop(editor_2);
19563 pane_2.update(cx, |pane, cx| {
19564 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19565 open_editor.update(cx, |editor, cx| {
19566 assert_eq!(
19567 editor.display_text(cx),
19568 lib_text,
19569 "Other file should be open and active in another panel too",
19570 );
19571 });
19572 assert_eq!(
19573 pane.items().count(),
19574 1,
19575 "No other editors should be open in another pane",
19576 );
19577 });
19578
19579 let _editor_1_reopened = workspace
19580 .update_in(cx, |workspace, window, cx| {
19581 workspace.open_path(
19582 (worktree_id, "main.rs"),
19583 Some(pane_1.downgrade()),
19584 true,
19585 window,
19586 cx,
19587 )
19588 })
19589 .unwrap()
19590 .await
19591 .downcast::<Editor>()
19592 .unwrap();
19593 let _editor_2_reopened = workspace
19594 .update_in(cx, |workspace, window, cx| {
19595 workspace.open_path(
19596 (worktree_id, "main.rs"),
19597 Some(pane_2.downgrade()),
19598 true,
19599 window,
19600 cx,
19601 )
19602 })
19603 .unwrap()
19604 .await
19605 .downcast::<Editor>()
19606 .unwrap();
19607 pane_1.update(cx, |pane, cx| {
19608 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19609 open_editor.update(cx, |editor, cx| {
19610 assert_eq!(
19611 editor.display_text(cx),
19612 main_text,
19613 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19614 );
19615 assert_eq!(
19616 editor
19617 .selections
19618 .all::<Point>(cx)
19619 .into_iter()
19620 .map(|s| s.range())
19621 .collect::<Vec<_>>(),
19622 expected_ranges,
19623 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19624 );
19625 })
19626 });
19627 pane_2.update(cx, |pane, cx| {
19628 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19629 open_editor.update(cx, |editor, cx| {
19630 assert_eq!(
19631 editor.display_text(cx),
19632 r#"fn main() {
19633⋯rintln!("1");
19634⋯intln!("2");
19635⋯ntln!("3");
19636println!("4");
19637println!("5");
19638}"#,
19639 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19640 );
19641 assert_eq!(
19642 editor
19643 .selections
19644 .all::<Point>(cx)
19645 .into_iter()
19646 .map(|s| s.range())
19647 .collect::<Vec<_>>(),
19648 vec![Point::zero()..Point::zero()],
19649 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19650 );
19651 })
19652 });
19653}
19654
19655#[gpui::test]
19656async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19657 init_test(cx, |_| {});
19658
19659 let fs = FakeFs::new(cx.executor());
19660 let main_text = r#"fn main() {
19661println!("1");
19662println!("2");
19663println!("3");
19664println!("4");
19665println!("5");
19666}"#;
19667 let lib_text = "mod foo {}";
19668 fs.insert_tree(
19669 path!("/a"),
19670 json!({
19671 "lib.rs": lib_text,
19672 "main.rs": main_text,
19673 }),
19674 )
19675 .await;
19676
19677 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19678 let (workspace, cx) =
19679 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19680 let worktree_id = workspace.update(cx, |workspace, cx| {
19681 workspace.project().update(cx, |project, cx| {
19682 project.worktrees(cx).next().unwrap().read(cx).id()
19683 })
19684 });
19685
19686 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19687 let editor = workspace
19688 .update_in(cx, |workspace, window, cx| {
19689 workspace.open_path(
19690 (worktree_id, "main.rs"),
19691 Some(pane.downgrade()),
19692 true,
19693 window,
19694 cx,
19695 )
19696 })
19697 .unwrap()
19698 .await
19699 .downcast::<Editor>()
19700 .unwrap();
19701 pane.update(cx, |pane, cx| {
19702 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19703 open_editor.update(cx, |editor, cx| {
19704 assert_eq!(
19705 editor.display_text(cx),
19706 main_text,
19707 "Original main.rs text on initial open",
19708 );
19709 })
19710 });
19711 editor.update_in(cx, |editor, window, cx| {
19712 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19713 });
19714
19715 cx.update_global(|store: &mut SettingsStore, cx| {
19716 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19717 s.restore_on_file_reopen = Some(false);
19718 });
19719 });
19720 editor.update_in(cx, |editor, window, cx| {
19721 editor.fold_ranges(
19722 vec![
19723 Point::new(1, 0)..Point::new(1, 1),
19724 Point::new(2, 0)..Point::new(2, 2),
19725 Point::new(3, 0)..Point::new(3, 3),
19726 ],
19727 false,
19728 window,
19729 cx,
19730 );
19731 });
19732 pane.update_in(cx, |pane, window, cx| {
19733 pane.close_all_items(&CloseAllItems::default(), window, cx)
19734 .unwrap()
19735 })
19736 .await
19737 .unwrap();
19738 pane.update(cx, |pane, _| {
19739 assert!(pane.active_item().is_none());
19740 });
19741 cx.update_global(|store: &mut SettingsStore, cx| {
19742 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19743 s.restore_on_file_reopen = Some(true);
19744 });
19745 });
19746
19747 let _editor_reopened = workspace
19748 .update_in(cx, |workspace, window, cx| {
19749 workspace.open_path(
19750 (worktree_id, "main.rs"),
19751 Some(pane.downgrade()),
19752 true,
19753 window,
19754 cx,
19755 )
19756 })
19757 .unwrap()
19758 .await
19759 .downcast::<Editor>()
19760 .unwrap();
19761 pane.update(cx, |pane, cx| {
19762 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19763 open_editor.update(cx, |editor, cx| {
19764 assert_eq!(
19765 editor.display_text(cx),
19766 main_text,
19767 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19768 );
19769 })
19770 });
19771}
19772
19773#[gpui::test]
19774async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19775 struct EmptyModalView {
19776 focus_handle: gpui::FocusHandle,
19777 }
19778 impl EventEmitter<DismissEvent> for EmptyModalView {}
19779 impl Render for EmptyModalView {
19780 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19781 div()
19782 }
19783 }
19784 impl Focusable for EmptyModalView {
19785 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19786 self.focus_handle.clone()
19787 }
19788 }
19789 impl workspace::ModalView for EmptyModalView {}
19790 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19791 EmptyModalView {
19792 focus_handle: cx.focus_handle(),
19793 }
19794 }
19795
19796 init_test(cx, |_| {});
19797
19798 let fs = FakeFs::new(cx.executor());
19799 let project = Project::test(fs, [], cx).await;
19800 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19801 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19802 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19803 let editor = cx.new_window_entity(|window, cx| {
19804 Editor::new(
19805 EditorMode::full(),
19806 buffer,
19807 Some(project.clone()),
19808 window,
19809 cx,
19810 )
19811 });
19812 workspace
19813 .update(cx, |workspace, window, cx| {
19814 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19815 })
19816 .unwrap();
19817 editor.update_in(cx, |editor, window, cx| {
19818 editor.open_context_menu(&OpenContextMenu, window, cx);
19819 assert!(editor.mouse_context_menu.is_some());
19820 });
19821 workspace
19822 .update(cx, |workspace, window, cx| {
19823 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19824 })
19825 .unwrap();
19826 cx.read(|cx| {
19827 assert!(editor.read(cx).mouse_context_menu.is_none());
19828 });
19829}
19830
19831#[gpui::test]
19832async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19833 init_test(cx, |_| {});
19834
19835 let fs = FakeFs::new(cx.executor());
19836 fs.insert_file(path!("/file.html"), Default::default())
19837 .await;
19838
19839 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19840
19841 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19842 let html_language = Arc::new(Language::new(
19843 LanguageConfig {
19844 name: "HTML".into(),
19845 matcher: LanguageMatcher {
19846 path_suffixes: vec!["html".to_string()],
19847 ..LanguageMatcher::default()
19848 },
19849 brackets: BracketPairConfig {
19850 pairs: vec![BracketPair {
19851 start: "<".into(),
19852 end: ">".into(),
19853 close: true,
19854 ..Default::default()
19855 }],
19856 ..Default::default()
19857 },
19858 ..Default::default()
19859 },
19860 Some(tree_sitter_html::LANGUAGE.into()),
19861 ));
19862 language_registry.add(html_language);
19863 let mut fake_servers = language_registry.register_fake_lsp(
19864 "HTML",
19865 FakeLspAdapter {
19866 capabilities: lsp::ServerCapabilities {
19867 completion_provider: Some(lsp::CompletionOptions {
19868 resolve_provider: Some(true),
19869 ..Default::default()
19870 }),
19871 ..Default::default()
19872 },
19873 ..Default::default()
19874 },
19875 );
19876
19877 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19878 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19879
19880 let worktree_id = workspace
19881 .update(cx, |workspace, _window, cx| {
19882 workspace.project().update(cx, |project, cx| {
19883 project.worktrees(cx).next().unwrap().read(cx).id()
19884 })
19885 })
19886 .unwrap();
19887 project
19888 .update(cx, |project, cx| {
19889 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19890 })
19891 .await
19892 .unwrap();
19893 let editor = workspace
19894 .update(cx, |workspace, window, cx| {
19895 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19896 })
19897 .unwrap()
19898 .await
19899 .unwrap()
19900 .downcast::<Editor>()
19901 .unwrap();
19902
19903 let fake_server = fake_servers.next().await.unwrap();
19904 editor.update_in(cx, |editor, window, cx| {
19905 editor.set_text("<ad></ad>", window, cx);
19906 editor.change_selections(None, window, cx, |selections| {
19907 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
19908 });
19909 let Some((buffer, _)) = editor
19910 .buffer
19911 .read(cx)
19912 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
19913 else {
19914 panic!("Failed to get buffer for selection position");
19915 };
19916 let buffer = buffer.read(cx);
19917 let buffer_id = buffer.remote_id();
19918 let opening_range =
19919 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
19920 let closing_range =
19921 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
19922 let mut linked_ranges = HashMap::default();
19923 linked_ranges.insert(
19924 buffer_id,
19925 vec![(opening_range.clone(), vec![closing_range.clone()])],
19926 );
19927 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
19928 });
19929 let mut completion_handle =
19930 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19931 Ok(Some(lsp::CompletionResponse::Array(vec![
19932 lsp::CompletionItem {
19933 label: "head".to_string(),
19934 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19935 lsp::InsertReplaceEdit {
19936 new_text: "head".to_string(),
19937 insert: lsp::Range::new(
19938 lsp::Position::new(0, 1),
19939 lsp::Position::new(0, 3),
19940 ),
19941 replace: lsp::Range::new(
19942 lsp::Position::new(0, 1),
19943 lsp::Position::new(0, 3),
19944 ),
19945 },
19946 )),
19947 ..Default::default()
19948 },
19949 ])))
19950 });
19951 editor.update_in(cx, |editor, window, cx| {
19952 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
19953 });
19954 cx.run_until_parked();
19955 completion_handle.next().await.unwrap();
19956 editor.update(cx, |editor, _| {
19957 assert!(
19958 editor.context_menu_visible(),
19959 "Completion menu should be visible"
19960 );
19961 });
19962 editor.update_in(cx, |editor, window, cx| {
19963 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
19964 });
19965 cx.executor().run_until_parked();
19966 editor.update(cx, |editor, cx| {
19967 assert_eq!(editor.text(cx), "<head></head>");
19968 });
19969}
19970
19971#[gpui::test]
19972async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
19973 init_test(cx, |_| {});
19974
19975 let fs = FakeFs::new(cx.executor());
19976 fs.insert_tree(
19977 path!("/root"),
19978 json!({
19979 "a": {
19980 "main.rs": "fn main() {}",
19981 },
19982 "foo": {
19983 "bar": {
19984 "external_file.rs": "pub mod external {}",
19985 }
19986 }
19987 }),
19988 )
19989 .await;
19990
19991 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
19992 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19993 language_registry.add(rust_lang());
19994 let _fake_servers = language_registry.register_fake_lsp(
19995 "Rust",
19996 FakeLspAdapter {
19997 ..FakeLspAdapter::default()
19998 },
19999 );
20000 let (workspace, cx) =
20001 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20002 let worktree_id = workspace.update(cx, |workspace, cx| {
20003 workspace.project().update(cx, |project, cx| {
20004 project.worktrees(cx).next().unwrap().read(cx).id()
20005 })
20006 });
20007
20008 let assert_language_servers_count =
20009 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20010 project.update(cx, |project, cx| {
20011 let current = project
20012 .lsp_store()
20013 .read(cx)
20014 .as_local()
20015 .unwrap()
20016 .language_servers
20017 .len();
20018 assert_eq!(expected, current, "{context}");
20019 });
20020 };
20021
20022 assert_language_servers_count(
20023 0,
20024 "No servers should be running before any file is open",
20025 cx,
20026 );
20027 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20028 let main_editor = workspace
20029 .update_in(cx, |workspace, window, cx| {
20030 workspace.open_path(
20031 (worktree_id, "main.rs"),
20032 Some(pane.downgrade()),
20033 true,
20034 window,
20035 cx,
20036 )
20037 })
20038 .unwrap()
20039 .await
20040 .downcast::<Editor>()
20041 .unwrap();
20042 pane.update(cx, |pane, cx| {
20043 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20044 open_editor.update(cx, |editor, cx| {
20045 assert_eq!(
20046 editor.display_text(cx),
20047 "fn main() {}",
20048 "Original main.rs text on initial open",
20049 );
20050 });
20051 assert_eq!(open_editor, main_editor);
20052 });
20053 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20054
20055 let external_editor = workspace
20056 .update_in(cx, |workspace, window, cx| {
20057 workspace.open_abs_path(
20058 PathBuf::from("/root/foo/bar/external_file.rs"),
20059 OpenOptions::default(),
20060 window,
20061 cx,
20062 )
20063 })
20064 .await
20065 .expect("opening external file")
20066 .downcast::<Editor>()
20067 .expect("downcasted external file's open element to editor");
20068 pane.update(cx, |pane, cx| {
20069 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20070 open_editor.update(cx, |editor, cx| {
20071 assert_eq!(
20072 editor.display_text(cx),
20073 "pub mod external {}",
20074 "External file is open now",
20075 );
20076 });
20077 assert_eq!(open_editor, external_editor);
20078 });
20079 assert_language_servers_count(
20080 1,
20081 "Second, external, *.rs file should join the existing server",
20082 cx,
20083 );
20084
20085 pane.update_in(cx, |pane, window, cx| {
20086 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20087 })
20088 .unwrap()
20089 .await
20090 .unwrap();
20091 pane.update_in(cx, |pane, window, cx| {
20092 pane.navigate_backward(window, cx);
20093 });
20094 cx.run_until_parked();
20095 pane.update(cx, |pane, cx| {
20096 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20097 open_editor.update(cx, |editor, cx| {
20098 assert_eq!(
20099 editor.display_text(cx),
20100 "pub mod external {}",
20101 "External file is open now",
20102 );
20103 });
20104 });
20105 assert_language_servers_count(
20106 1,
20107 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20108 cx,
20109 );
20110
20111 cx.update(|_, cx| {
20112 workspace::reload(&workspace::Reload::default(), cx);
20113 });
20114 assert_language_servers_count(
20115 1,
20116 "After reloading the worktree with local and external files opened, only one project should be started",
20117 cx,
20118 );
20119}
20120
20121fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20122 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20123 point..point
20124}
20125
20126fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20127 let (text, ranges) = marked_text_ranges(marked_text, true);
20128 assert_eq!(editor.text(cx), text);
20129 assert_eq!(
20130 editor.selections.ranges(cx),
20131 ranges,
20132 "Assert selections are {}",
20133 marked_text
20134 );
20135}
20136
20137pub fn handle_signature_help_request(
20138 cx: &mut EditorLspTestContext,
20139 mocked_response: lsp::SignatureHelp,
20140) -> impl Future<Output = ()> + use<> {
20141 let mut request =
20142 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20143 let mocked_response = mocked_response.clone();
20144 async move { Ok(Some(mocked_response)) }
20145 });
20146
20147 async move {
20148 request.next().await;
20149 }
20150}
20151
20152/// Handle completion request passing a marked string specifying where the completion
20153/// should be triggered from using '|' character, what range should be replaced, and what completions
20154/// should be returned using '<' and '>' to delimit the range.
20155///
20156/// Also see `handle_completion_request_with_insert_and_replace`.
20157#[track_caller]
20158pub fn handle_completion_request(
20159 cx: &mut EditorLspTestContext,
20160 marked_string: &str,
20161 completions: Vec<&'static str>,
20162 counter: Arc<AtomicUsize>,
20163) -> impl Future<Output = ()> {
20164 let complete_from_marker: TextRangeMarker = '|'.into();
20165 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20166 let (_, mut marked_ranges) = marked_text_ranges_by(
20167 marked_string,
20168 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20169 );
20170
20171 let complete_from_position =
20172 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20173 let replace_range =
20174 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20175
20176 let mut request =
20177 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20178 let completions = completions.clone();
20179 counter.fetch_add(1, atomic::Ordering::Release);
20180 async move {
20181 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20182 assert_eq!(
20183 params.text_document_position.position,
20184 complete_from_position
20185 );
20186 Ok(Some(lsp::CompletionResponse::Array(
20187 completions
20188 .iter()
20189 .map(|completion_text| lsp::CompletionItem {
20190 label: completion_text.to_string(),
20191 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20192 range: replace_range,
20193 new_text: completion_text.to_string(),
20194 })),
20195 ..Default::default()
20196 })
20197 .collect(),
20198 )))
20199 }
20200 });
20201
20202 async move {
20203 request.next().await;
20204 }
20205}
20206
20207/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20208/// given instead, which also contains an `insert` range.
20209///
20210/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20211/// that is, `replace_range.start..cursor_pos`.
20212pub fn handle_completion_request_with_insert_and_replace(
20213 cx: &mut EditorLspTestContext,
20214 marked_string: &str,
20215 completions: Vec<&'static str>,
20216 counter: Arc<AtomicUsize>,
20217) -> impl Future<Output = ()> {
20218 let complete_from_marker: TextRangeMarker = '|'.into();
20219 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20220 let (_, mut marked_ranges) = marked_text_ranges_by(
20221 marked_string,
20222 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20223 );
20224
20225 let complete_from_position =
20226 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20227 let replace_range =
20228 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20229
20230 let mut request =
20231 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20232 let completions = completions.clone();
20233 counter.fetch_add(1, atomic::Ordering::Release);
20234 async move {
20235 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20236 assert_eq!(
20237 params.text_document_position.position, complete_from_position,
20238 "marker `|` position doesn't match",
20239 );
20240 Ok(Some(lsp::CompletionResponse::Array(
20241 completions
20242 .iter()
20243 .map(|completion_text| lsp::CompletionItem {
20244 label: completion_text.to_string(),
20245 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20246 lsp::InsertReplaceEdit {
20247 insert: lsp::Range {
20248 start: replace_range.start,
20249 end: complete_from_position,
20250 },
20251 replace: replace_range,
20252 new_text: completion_text.to_string(),
20253 },
20254 )),
20255 ..Default::default()
20256 })
20257 .collect(),
20258 )))
20259 }
20260 });
20261
20262 async move {
20263 request.next().await;
20264 }
20265}
20266
20267fn handle_resolve_completion_request(
20268 cx: &mut EditorLspTestContext,
20269 edits: Option<Vec<(&'static str, &'static str)>>,
20270) -> impl Future<Output = ()> {
20271 let edits = edits.map(|edits| {
20272 edits
20273 .iter()
20274 .map(|(marked_string, new_text)| {
20275 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20276 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20277 lsp::TextEdit::new(replace_range, new_text.to_string())
20278 })
20279 .collect::<Vec<_>>()
20280 });
20281
20282 let mut request =
20283 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20284 let edits = edits.clone();
20285 async move {
20286 Ok(lsp::CompletionItem {
20287 additional_text_edits: edits,
20288 ..Default::default()
20289 })
20290 }
20291 });
20292
20293 async move {
20294 request.next().await;
20295 }
20296}
20297
20298pub(crate) fn update_test_language_settings(
20299 cx: &mut TestAppContext,
20300 f: impl Fn(&mut AllLanguageSettingsContent),
20301) {
20302 cx.update(|cx| {
20303 SettingsStore::update_global(cx, |store, cx| {
20304 store.update_user_settings::<AllLanguageSettings>(cx, f);
20305 });
20306 });
20307}
20308
20309pub(crate) fn update_test_project_settings(
20310 cx: &mut TestAppContext,
20311 f: impl Fn(&mut ProjectSettings),
20312) {
20313 cx.update(|cx| {
20314 SettingsStore::update_global(cx, |store, cx| {
20315 store.update_user_settings::<ProjectSettings>(cx, f);
20316 });
20317 });
20318}
20319
20320pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20321 cx.update(|cx| {
20322 assets::Assets.load_test_fonts(cx);
20323 let store = SettingsStore::test(cx);
20324 cx.set_global(store);
20325 theme::init(theme::LoadThemes::JustBase, cx);
20326 release_channel::init(SemanticVersion::default(), cx);
20327 client::init_settings(cx);
20328 language::init(cx);
20329 Project::init_settings(cx);
20330 workspace::init_settings(cx);
20331 crate::init(cx);
20332 });
20333
20334 update_test_language_settings(cx, f);
20335}
20336
20337#[track_caller]
20338fn assert_hunk_revert(
20339 not_reverted_text_with_selections: &str,
20340 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
20341 expected_reverted_text_with_selections: &str,
20342 base_text: &str,
20343 cx: &mut EditorLspTestContext,
20344) {
20345 cx.set_state(not_reverted_text_with_selections);
20346 cx.set_head_text(base_text);
20347 cx.executor().run_until_parked();
20348
20349 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
20350 let snapshot = editor.snapshot(window, cx);
20351 let reverted_hunk_statuses = snapshot
20352 .buffer_snapshot
20353 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
20354 .map(|hunk| hunk.status().kind)
20355 .collect::<Vec<_>>();
20356
20357 editor.git_restore(&Default::default(), window, cx);
20358 reverted_hunk_statuses
20359 });
20360 cx.executor().run_until_parked();
20361 cx.assert_editor_state(expected_reverted_text_with_selections);
20362 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20363}