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: Some(language::DocumentationConfig {
2809 start: "/**".into(),
2810 end: "*/".into(),
2811 prefix: "* ".into(),
2812 tab_size: NonZeroU32::new(1).unwrap(),
2813 }),
2814 ..LanguageConfig::default()
2815 },
2816 None,
2817 ));
2818 {
2819 let mut cx = EditorTestContext::new(cx).await;
2820 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2821 cx.set_state(indoc! {"
2822 /**ˇ
2823 "});
2824
2825 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2826 cx.assert_editor_state(indoc! {"
2827 /**
2828 * ˇ
2829 "});
2830 // Ensure that if cursor is before the comment start,
2831 // we do not actually insert a comment prefix.
2832 cx.set_state(indoc! {"
2833 ˇ/**
2834 "});
2835 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2836 cx.assert_editor_state(indoc! {"
2837
2838 ˇ/**
2839 "});
2840 // Ensure that if cursor is between it doesn't add comment prefix.
2841 cx.set_state(indoc! {"
2842 /*ˇ*
2843 "});
2844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2845 cx.assert_editor_state(indoc! {"
2846 /*
2847 ˇ*
2848 "});
2849 // Ensure that if suffix exists on same line after cursor it adds new line.
2850 cx.set_state(indoc! {"
2851 /**ˇ*/
2852 "});
2853 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 /**
2856 * ˇ
2857 */
2858 "});
2859 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2860 cx.set_state(indoc! {"
2861 /**ˇ */
2862 "});
2863 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2864 cx.assert_editor_state(indoc! {"
2865 /**
2866 * ˇ
2867 */
2868 "});
2869 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2870 cx.set_state(indoc! {"
2871 /** ˇ*/
2872 "});
2873 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2874 cx.assert_editor_state(
2875 indoc! {"
2876 /**s
2877 * ˇ
2878 */
2879 "}
2880 .replace("s", " ") // s is used as space placeholder to prevent format on save
2881 .as_str(),
2882 );
2883 // Ensure that delimiter space is preserved when newline on already
2884 // spaced delimiter.
2885 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2886 cx.assert_editor_state(
2887 indoc! {"
2888 /**s
2889 *s
2890 * ˇ
2891 */
2892 "}
2893 .replace("s", " ") // s is used as space placeholder to prevent format on save
2894 .as_str(),
2895 );
2896 // Ensure that delimiter space is preserved when space is not
2897 // on existing delimiter.
2898 cx.set_state(indoc! {"
2899 /**
2900 *ˇ
2901 */
2902 "});
2903 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2904 cx.assert_editor_state(indoc! {"
2905 /**
2906 *
2907 * ˇ
2908 */
2909 "});
2910 // Ensure that if suffix exists on same line after cursor it
2911 // doesn't add extra new line if prefix is not on same line.
2912 cx.set_state(indoc! {"
2913 /**
2914 ˇ*/
2915 "});
2916 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2917 cx.assert_editor_state(indoc! {"
2918 /**
2919
2920 ˇ*/
2921 "});
2922 // Ensure that it detects suffix after existing prefix.
2923 cx.set_state(indoc! {"
2924 /**ˇ/
2925 "});
2926 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2927 cx.assert_editor_state(indoc! {"
2928 /**
2929 ˇ/
2930 "});
2931 // Ensure that if suffix exists on same line before
2932 // cursor it does not add comment prefix.
2933 cx.set_state(indoc! {"
2934 /** */ˇ
2935 "});
2936 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2937 cx.assert_editor_state(indoc! {"
2938 /** */
2939 ˇ
2940 "});
2941 // Ensure that if suffix exists on same line before
2942 // cursor it does not add comment prefix.
2943 cx.set_state(indoc! {"
2944 /**
2945 *
2946 */ˇ
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(indoc! {"
2950 /**
2951 *
2952 */
2953 ˇ
2954 "});
2955 }
2956 // Ensure that comment continuations can be disabled.
2957 update_test_language_settings(cx, |settings| {
2958 settings.defaults.extend_comment_on_newline = Some(false);
2959 });
2960 let mut cx = EditorTestContext::new(cx).await;
2961 cx.set_state(indoc! {"
2962 /**ˇ
2963 "});
2964 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2965 cx.assert_editor_state(indoc! {"
2966 /**
2967 ˇ
2968 "});
2969}
2970
2971#[gpui::test]
2972fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2973 init_test(cx, |_| {});
2974
2975 let editor = cx.add_window(|window, cx| {
2976 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2977 let mut editor = build_editor(buffer.clone(), window, cx);
2978 editor.change_selections(None, window, cx, |s| {
2979 s.select_ranges([3..4, 11..12, 19..20])
2980 });
2981 editor
2982 });
2983
2984 _ = editor.update(cx, |editor, window, cx| {
2985 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2986 editor.buffer.update(cx, |buffer, cx| {
2987 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2988 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2989 });
2990 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2991
2992 editor.insert("Z", window, cx);
2993 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2994
2995 // The selections are moved after the inserted characters
2996 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2997 });
2998}
2999
3000#[gpui::test]
3001async fn test_tab(cx: &mut TestAppContext) {
3002 init_test(cx, |settings| {
3003 settings.defaults.tab_size = NonZeroU32::new(3)
3004 });
3005
3006 let mut cx = EditorTestContext::new(cx).await;
3007 cx.set_state(indoc! {"
3008 ˇabˇc
3009 ˇ🏀ˇ🏀ˇefg
3010 dˇ
3011 "});
3012 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3013 cx.assert_editor_state(indoc! {"
3014 ˇab ˇc
3015 ˇ🏀 ˇ🏀 ˇefg
3016 d ˇ
3017 "});
3018
3019 cx.set_state(indoc! {"
3020 a
3021 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3022 "});
3023 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 a
3026 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3027 "});
3028}
3029
3030#[gpui::test]
3031async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3032 init_test(cx, |_| {});
3033
3034 let mut cx = EditorTestContext::new(cx).await;
3035 let language = Arc::new(
3036 Language::new(
3037 LanguageConfig::default(),
3038 Some(tree_sitter_rust::LANGUAGE.into()),
3039 )
3040 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3041 .unwrap(),
3042 );
3043 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3044
3045 // test when all cursors are not at suggested indent
3046 // then simply move to their suggested indent location
3047 cx.set_state(indoc! {"
3048 const a: B = (
3049 c(
3050 ˇ
3051 ˇ )
3052 );
3053 "});
3054 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 const a: B = (
3057 c(
3058 ˇ
3059 ˇ)
3060 );
3061 "});
3062
3063 // test cursor already at suggested indent not moving when
3064 // other cursors are yet to reach their suggested indents
3065 cx.set_state(indoc! {"
3066 ˇ
3067 const a: B = (
3068 c(
3069 d(
3070 ˇ
3071 )
3072 ˇ
3073 ˇ )
3074 );
3075 "});
3076 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 ˇ
3079 const a: B = (
3080 c(
3081 d(
3082 ˇ
3083 )
3084 ˇ
3085 ˇ)
3086 );
3087 "});
3088 // test when all cursors are at suggested indent then tab is inserted
3089 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3090 cx.assert_editor_state(indoc! {"
3091 ˇ
3092 const a: B = (
3093 c(
3094 d(
3095 ˇ
3096 )
3097 ˇ
3098 ˇ)
3099 );
3100 "});
3101
3102 // test when current indent is less than suggested indent,
3103 // we adjust line to match suggested indent and move cursor to it
3104 //
3105 // when no other cursor is at word boundary, all of them should move
3106 cx.set_state(indoc! {"
3107 const a: B = (
3108 c(
3109 d(
3110 ˇ
3111 ˇ )
3112 ˇ )
3113 );
3114 "});
3115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 const a: B = (
3118 c(
3119 d(
3120 ˇ
3121 ˇ)
3122 ˇ)
3123 );
3124 "});
3125
3126 // test when current indent is less than suggested indent,
3127 // we adjust line to match suggested indent and move cursor to it
3128 //
3129 // when some other cursor is at word boundary, it should not move
3130 cx.set_state(indoc! {"
3131 const a: B = (
3132 c(
3133 d(
3134 ˇ
3135 ˇ )
3136 ˇ)
3137 );
3138 "});
3139 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3140 cx.assert_editor_state(indoc! {"
3141 const a: B = (
3142 c(
3143 d(
3144 ˇ
3145 ˇ)
3146 ˇ)
3147 );
3148 "});
3149
3150 // test when current indent is more than suggested indent,
3151 // we just move cursor to current indent instead of suggested indent
3152 //
3153 // when no other cursor is at word boundary, all of them should move
3154 cx.set_state(indoc! {"
3155 const a: B = (
3156 c(
3157 d(
3158 ˇ
3159 ˇ )
3160 ˇ )
3161 );
3162 "});
3163 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3164 cx.assert_editor_state(indoc! {"
3165 const a: B = (
3166 c(
3167 d(
3168 ˇ
3169 ˇ)
3170 ˇ)
3171 );
3172 "});
3173 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3174 cx.assert_editor_state(indoc! {"
3175 const a: B = (
3176 c(
3177 d(
3178 ˇ
3179 ˇ)
3180 ˇ)
3181 );
3182 "});
3183
3184 // test when current indent is more than suggested indent,
3185 // we just move cursor to current indent instead of suggested indent
3186 //
3187 // when some other cursor is at word boundary, it doesn't move
3188 cx.set_state(indoc! {"
3189 const a: B = (
3190 c(
3191 d(
3192 ˇ
3193 ˇ )
3194 ˇ)
3195 );
3196 "});
3197 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3198 cx.assert_editor_state(indoc! {"
3199 const a: B = (
3200 c(
3201 d(
3202 ˇ
3203 ˇ)
3204 ˇ)
3205 );
3206 "});
3207
3208 // handle auto-indent when there are multiple cursors on the same line
3209 cx.set_state(indoc! {"
3210 const a: B = (
3211 c(
3212 ˇ ˇ
3213 ˇ )
3214 );
3215 "});
3216 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3217 cx.assert_editor_state(indoc! {"
3218 const a: B = (
3219 c(
3220 ˇ
3221 ˇ)
3222 );
3223 "});
3224}
3225
3226#[gpui::test]
3227async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3228 init_test(cx, |settings| {
3229 settings.defaults.tab_size = NonZeroU32::new(3)
3230 });
3231
3232 let mut cx = EditorTestContext::new(cx).await;
3233 cx.set_state(indoc! {"
3234 ˇ
3235 \t ˇ
3236 \t ˇ
3237 \t ˇ
3238 \t \t\t \t \t\t \t\t \t \t ˇ
3239 "});
3240
3241 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3242 cx.assert_editor_state(indoc! {"
3243 ˇ
3244 \t ˇ
3245 \t ˇ
3246 \t ˇ
3247 \t \t\t \t \t\t \t\t \t \t ˇ
3248 "});
3249}
3250
3251#[gpui::test]
3252async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3253 init_test(cx, |settings| {
3254 settings.defaults.tab_size = NonZeroU32::new(4)
3255 });
3256
3257 let language = Arc::new(
3258 Language::new(
3259 LanguageConfig::default(),
3260 Some(tree_sitter_rust::LANGUAGE.into()),
3261 )
3262 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3263 .unwrap(),
3264 );
3265
3266 let mut cx = EditorTestContext::new(cx).await;
3267 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3268 cx.set_state(indoc! {"
3269 fn a() {
3270 if b {
3271 \t ˇc
3272 }
3273 }
3274 "});
3275
3276 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3277 cx.assert_editor_state(indoc! {"
3278 fn a() {
3279 if b {
3280 ˇc
3281 }
3282 }
3283 "});
3284}
3285
3286#[gpui::test]
3287async fn test_indent_outdent(cx: &mut TestAppContext) {
3288 init_test(cx, |settings| {
3289 settings.defaults.tab_size = NonZeroU32::new(4);
3290 });
3291
3292 let mut cx = EditorTestContext::new(cx).await;
3293
3294 cx.set_state(indoc! {"
3295 «oneˇ» «twoˇ»
3296 three
3297 four
3298 "});
3299 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3300 cx.assert_editor_state(indoc! {"
3301 «oneˇ» «twoˇ»
3302 three
3303 four
3304 "});
3305
3306 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3307 cx.assert_editor_state(indoc! {"
3308 «oneˇ» «twoˇ»
3309 three
3310 four
3311 "});
3312
3313 // select across line ending
3314 cx.set_state(indoc! {"
3315 one two
3316 t«hree
3317 ˇ» four
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 one two
3322 t«hree
3323 ˇ» four
3324 "});
3325
3326 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3327 cx.assert_editor_state(indoc! {"
3328 one two
3329 t«hree
3330 ˇ» four
3331 "});
3332
3333 // Ensure that indenting/outdenting works when the cursor is at column 0.
3334 cx.set_state(indoc! {"
3335 one two
3336 ˇthree
3337 four
3338 "});
3339 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3340 cx.assert_editor_state(indoc! {"
3341 one two
3342 ˇthree
3343 four
3344 "});
3345
3346 cx.set_state(indoc! {"
3347 one two
3348 ˇ three
3349 four
3350 "});
3351 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3352 cx.assert_editor_state(indoc! {"
3353 one two
3354 ˇthree
3355 four
3356 "});
3357}
3358
3359#[gpui::test]
3360async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3361 init_test(cx, |settings| {
3362 settings.defaults.hard_tabs = Some(true);
3363 });
3364
3365 let mut cx = EditorTestContext::new(cx).await;
3366
3367 // select two ranges on one line
3368 cx.set_state(indoc! {"
3369 «oneˇ» «twoˇ»
3370 three
3371 four
3372 "});
3373 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3374 cx.assert_editor_state(indoc! {"
3375 \t«oneˇ» «twoˇ»
3376 three
3377 four
3378 "});
3379 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381 \t\t«oneˇ» «twoˇ»
3382 three
3383 four
3384 "});
3385 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3386 cx.assert_editor_state(indoc! {"
3387 \t«oneˇ» «twoˇ»
3388 three
3389 four
3390 "});
3391 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3392 cx.assert_editor_state(indoc! {"
3393 «oneˇ» «twoˇ»
3394 three
3395 four
3396 "});
3397
3398 // select across a line ending
3399 cx.set_state(indoc! {"
3400 one two
3401 t«hree
3402 ˇ»four
3403 "});
3404 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406 one two
3407 \tt«hree
3408 ˇ»four
3409 "});
3410 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 one two
3413 \t\tt«hree
3414 ˇ»four
3415 "});
3416 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3417 cx.assert_editor_state(indoc! {"
3418 one two
3419 \tt«hree
3420 ˇ»four
3421 "});
3422 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 one two
3425 t«hree
3426 ˇ»four
3427 "});
3428
3429 // Ensure that indenting/outdenting works when the cursor is at column 0.
3430 cx.set_state(indoc! {"
3431 one two
3432 ˇthree
3433 four
3434 "});
3435 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3436 cx.assert_editor_state(indoc! {"
3437 one two
3438 ˇthree
3439 four
3440 "});
3441 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3442 cx.assert_editor_state(indoc! {"
3443 one two
3444 \tˇthree
3445 four
3446 "});
3447 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3448 cx.assert_editor_state(indoc! {"
3449 one two
3450 ˇthree
3451 four
3452 "});
3453}
3454
3455#[gpui::test]
3456fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3457 init_test(cx, |settings| {
3458 settings.languages.extend([
3459 (
3460 "TOML".into(),
3461 LanguageSettingsContent {
3462 tab_size: NonZeroU32::new(2),
3463 ..Default::default()
3464 },
3465 ),
3466 (
3467 "Rust".into(),
3468 LanguageSettingsContent {
3469 tab_size: NonZeroU32::new(4),
3470 ..Default::default()
3471 },
3472 ),
3473 ]);
3474 });
3475
3476 let toml_language = Arc::new(Language::new(
3477 LanguageConfig {
3478 name: "TOML".into(),
3479 ..Default::default()
3480 },
3481 None,
3482 ));
3483 let rust_language = Arc::new(Language::new(
3484 LanguageConfig {
3485 name: "Rust".into(),
3486 ..Default::default()
3487 },
3488 None,
3489 ));
3490
3491 let toml_buffer =
3492 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3493 let rust_buffer =
3494 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3495 let multibuffer = cx.new(|cx| {
3496 let mut multibuffer = MultiBuffer::new(ReadWrite);
3497 multibuffer.push_excerpts(
3498 toml_buffer.clone(),
3499 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3500 cx,
3501 );
3502 multibuffer.push_excerpts(
3503 rust_buffer.clone(),
3504 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3505 cx,
3506 );
3507 multibuffer
3508 });
3509
3510 cx.add_window(|window, cx| {
3511 let mut editor = build_editor(multibuffer, window, cx);
3512
3513 assert_eq!(
3514 editor.text(cx),
3515 indoc! {"
3516 a = 1
3517 b = 2
3518
3519 const c: usize = 3;
3520 "}
3521 );
3522
3523 select_ranges(
3524 &mut editor,
3525 indoc! {"
3526 «aˇ» = 1
3527 b = 2
3528
3529 «const c:ˇ» usize = 3;
3530 "},
3531 window,
3532 cx,
3533 );
3534
3535 editor.tab(&Tab, window, cx);
3536 assert_text_with_selections(
3537 &mut editor,
3538 indoc! {"
3539 «aˇ» = 1
3540 b = 2
3541
3542 «const c:ˇ» usize = 3;
3543 "},
3544 cx,
3545 );
3546 editor.backtab(&Backtab, window, cx);
3547 assert_text_with_selections(
3548 &mut editor,
3549 indoc! {"
3550 «aˇ» = 1
3551 b = 2
3552
3553 «const c:ˇ» usize = 3;
3554 "},
3555 cx,
3556 );
3557
3558 editor
3559 });
3560}
3561
3562#[gpui::test]
3563async fn test_backspace(cx: &mut TestAppContext) {
3564 init_test(cx, |_| {});
3565
3566 let mut cx = EditorTestContext::new(cx).await;
3567
3568 // Basic backspace
3569 cx.set_state(indoc! {"
3570 onˇe two three
3571 fou«rˇ» five six
3572 seven «ˇeight nine
3573 »ten
3574 "});
3575 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3576 cx.assert_editor_state(indoc! {"
3577 oˇe two three
3578 fouˇ five six
3579 seven ˇten
3580 "});
3581
3582 // Test backspace inside and around indents
3583 cx.set_state(indoc! {"
3584 zero
3585 ˇone
3586 ˇtwo
3587 ˇ ˇ ˇ three
3588 ˇ ˇ four
3589 "});
3590 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3591 cx.assert_editor_state(indoc! {"
3592 zero
3593 ˇone
3594 ˇtwo
3595 ˇ threeˇ four
3596 "});
3597}
3598
3599#[gpui::test]
3600async fn test_delete(cx: &mut TestAppContext) {
3601 init_test(cx, |_| {});
3602
3603 let mut cx = EditorTestContext::new(cx).await;
3604 cx.set_state(indoc! {"
3605 onˇe two three
3606 fou«rˇ» five six
3607 seven «ˇeight nine
3608 »ten
3609 "});
3610 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3611 cx.assert_editor_state(indoc! {"
3612 onˇ two three
3613 fouˇ five six
3614 seven ˇten
3615 "});
3616}
3617
3618#[gpui::test]
3619fn test_delete_line(cx: &mut TestAppContext) {
3620 init_test(cx, |_| {});
3621
3622 let editor = cx.add_window(|window, cx| {
3623 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3624 build_editor(buffer, window, cx)
3625 });
3626 _ = editor.update(cx, |editor, window, cx| {
3627 editor.change_selections(None, window, cx, |s| {
3628 s.select_display_ranges([
3629 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3631 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3632 ])
3633 });
3634 editor.delete_line(&DeleteLine, window, cx);
3635 assert_eq!(editor.display_text(cx), "ghi");
3636 assert_eq!(
3637 editor.selections.display_ranges(cx),
3638 vec![
3639 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3640 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3641 ]
3642 );
3643 });
3644
3645 let editor = cx.add_window(|window, cx| {
3646 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3647 build_editor(buffer, window, cx)
3648 });
3649 _ = editor.update(cx, |editor, window, cx| {
3650 editor.change_selections(None, window, cx, |s| {
3651 s.select_display_ranges([
3652 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3653 ])
3654 });
3655 editor.delete_line(&DeleteLine, window, cx);
3656 assert_eq!(editor.display_text(cx), "ghi\n");
3657 assert_eq!(
3658 editor.selections.display_ranges(cx),
3659 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3660 );
3661 });
3662}
3663
3664#[gpui::test]
3665fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3666 init_test(cx, |_| {});
3667
3668 cx.add_window(|window, cx| {
3669 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3670 let mut editor = build_editor(buffer.clone(), window, cx);
3671 let buffer = buffer.read(cx).as_singleton().unwrap();
3672
3673 assert_eq!(
3674 editor.selections.ranges::<Point>(cx),
3675 &[Point::new(0, 0)..Point::new(0, 0)]
3676 );
3677
3678 // When on single line, replace newline at end by space
3679 editor.join_lines(&JoinLines, window, cx);
3680 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3681 assert_eq!(
3682 editor.selections.ranges::<Point>(cx),
3683 &[Point::new(0, 3)..Point::new(0, 3)]
3684 );
3685
3686 // When multiple lines are selected, remove newlines that are spanned by the selection
3687 editor.change_selections(None, window, cx, |s| {
3688 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3689 });
3690 editor.join_lines(&JoinLines, window, cx);
3691 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3692 assert_eq!(
3693 editor.selections.ranges::<Point>(cx),
3694 &[Point::new(0, 11)..Point::new(0, 11)]
3695 );
3696
3697 // Undo should be transactional
3698 editor.undo(&Undo, window, cx);
3699 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3700 assert_eq!(
3701 editor.selections.ranges::<Point>(cx),
3702 &[Point::new(0, 5)..Point::new(2, 2)]
3703 );
3704
3705 // When joining an empty line don't insert a space
3706 editor.change_selections(None, window, cx, |s| {
3707 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3708 });
3709 editor.join_lines(&JoinLines, window, cx);
3710 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3711 assert_eq!(
3712 editor.selections.ranges::<Point>(cx),
3713 [Point::new(2, 3)..Point::new(2, 3)]
3714 );
3715
3716 // We can remove trailing newlines
3717 editor.join_lines(&JoinLines, window, cx);
3718 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3719 assert_eq!(
3720 editor.selections.ranges::<Point>(cx),
3721 [Point::new(2, 3)..Point::new(2, 3)]
3722 );
3723
3724 // We don't blow up on the last line
3725 editor.join_lines(&JoinLines, window, cx);
3726 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3727 assert_eq!(
3728 editor.selections.ranges::<Point>(cx),
3729 [Point::new(2, 3)..Point::new(2, 3)]
3730 );
3731
3732 // reset to test indentation
3733 editor.buffer.update(cx, |buffer, cx| {
3734 buffer.edit(
3735 [
3736 (Point::new(1, 0)..Point::new(1, 2), " "),
3737 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3738 ],
3739 None,
3740 cx,
3741 )
3742 });
3743
3744 // We remove any leading spaces
3745 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3746 editor.change_selections(None, window, cx, |s| {
3747 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3748 });
3749 editor.join_lines(&JoinLines, window, cx);
3750 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3751
3752 // We don't insert a space for a line containing only spaces
3753 editor.join_lines(&JoinLines, window, cx);
3754 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3755
3756 // We ignore any leading tabs
3757 editor.join_lines(&JoinLines, window, cx);
3758 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3759
3760 editor
3761 });
3762}
3763
3764#[gpui::test]
3765fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3766 init_test(cx, |_| {});
3767
3768 cx.add_window(|window, cx| {
3769 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3770 let mut editor = build_editor(buffer.clone(), window, cx);
3771 let buffer = buffer.read(cx).as_singleton().unwrap();
3772
3773 editor.change_selections(None, window, cx, |s| {
3774 s.select_ranges([
3775 Point::new(0, 2)..Point::new(1, 1),
3776 Point::new(1, 2)..Point::new(1, 2),
3777 Point::new(3, 1)..Point::new(3, 2),
3778 ])
3779 });
3780
3781 editor.join_lines(&JoinLines, window, cx);
3782 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3783
3784 assert_eq!(
3785 editor.selections.ranges::<Point>(cx),
3786 [
3787 Point::new(0, 7)..Point::new(0, 7),
3788 Point::new(1, 3)..Point::new(1, 3)
3789 ]
3790 );
3791 editor
3792 });
3793}
3794
3795#[gpui::test]
3796async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3797 init_test(cx, |_| {});
3798
3799 let mut cx = EditorTestContext::new(cx).await;
3800
3801 let diff_base = r#"
3802 Line 0
3803 Line 1
3804 Line 2
3805 Line 3
3806 "#
3807 .unindent();
3808
3809 cx.set_state(
3810 &r#"
3811 ˇLine 0
3812 Line 1
3813 Line 2
3814 Line 3
3815 "#
3816 .unindent(),
3817 );
3818
3819 cx.set_head_text(&diff_base);
3820 executor.run_until_parked();
3821
3822 // Join lines
3823 cx.update_editor(|editor, window, cx| {
3824 editor.join_lines(&JoinLines, window, cx);
3825 });
3826 executor.run_until_parked();
3827
3828 cx.assert_editor_state(
3829 &r#"
3830 Line 0ˇ Line 1
3831 Line 2
3832 Line 3
3833 "#
3834 .unindent(),
3835 );
3836 // Join again
3837 cx.update_editor(|editor, window, cx| {
3838 editor.join_lines(&JoinLines, window, cx);
3839 });
3840 executor.run_until_parked();
3841
3842 cx.assert_editor_state(
3843 &r#"
3844 Line 0 Line 1ˇ Line 2
3845 Line 3
3846 "#
3847 .unindent(),
3848 );
3849}
3850
3851#[gpui::test]
3852async fn test_custom_newlines_cause_no_false_positive_diffs(
3853 executor: BackgroundExecutor,
3854 cx: &mut TestAppContext,
3855) {
3856 init_test(cx, |_| {});
3857 let mut cx = EditorTestContext::new(cx).await;
3858 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3859 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3860 executor.run_until_parked();
3861
3862 cx.update_editor(|editor, window, cx| {
3863 let snapshot = editor.snapshot(window, cx);
3864 assert_eq!(
3865 snapshot
3866 .buffer_snapshot
3867 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3868 .collect::<Vec<_>>(),
3869 Vec::new(),
3870 "Should not have any diffs for files with custom newlines"
3871 );
3872 });
3873}
3874
3875#[gpui::test]
3876async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3877 init_test(cx, |_| {});
3878
3879 let mut cx = EditorTestContext::new(cx).await;
3880
3881 // Test sort_lines_case_insensitive()
3882 cx.set_state(indoc! {"
3883 «z
3884 y
3885 x
3886 Z
3887 Y
3888 Xˇ»
3889 "});
3890 cx.update_editor(|e, window, cx| {
3891 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3892 });
3893 cx.assert_editor_state(indoc! {"
3894 «x
3895 X
3896 y
3897 Y
3898 z
3899 Zˇ»
3900 "});
3901
3902 // Test reverse_lines()
3903 cx.set_state(indoc! {"
3904 «5
3905 4
3906 3
3907 2
3908 1ˇ»
3909 "});
3910 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3911 cx.assert_editor_state(indoc! {"
3912 «1
3913 2
3914 3
3915 4
3916 5ˇ»
3917 "});
3918
3919 // Skip testing shuffle_line()
3920
3921 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3922 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3923
3924 // Don't manipulate when cursor is on single line, but expand the selection
3925 cx.set_state(indoc! {"
3926 ddˇdd
3927 ccc
3928 bb
3929 a
3930 "});
3931 cx.update_editor(|e, window, cx| {
3932 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3933 });
3934 cx.assert_editor_state(indoc! {"
3935 «ddddˇ»
3936 ccc
3937 bb
3938 a
3939 "});
3940
3941 // Basic manipulate case
3942 // Start selection moves to column 0
3943 // End of selection shrinks to fit shorter line
3944 cx.set_state(indoc! {"
3945 dd«d
3946 ccc
3947 bb
3948 aaaaaˇ»
3949 "});
3950 cx.update_editor(|e, window, cx| {
3951 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3952 });
3953 cx.assert_editor_state(indoc! {"
3954 «aaaaa
3955 bb
3956 ccc
3957 dddˇ»
3958 "});
3959
3960 // Manipulate case with newlines
3961 cx.set_state(indoc! {"
3962 dd«d
3963 ccc
3964
3965 bb
3966 aaaaa
3967
3968 ˇ»
3969 "});
3970 cx.update_editor(|e, window, cx| {
3971 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3972 });
3973 cx.assert_editor_state(indoc! {"
3974 «
3975
3976 aaaaa
3977 bb
3978 ccc
3979 dddˇ»
3980
3981 "});
3982
3983 // Adding new line
3984 cx.set_state(indoc! {"
3985 aa«a
3986 bbˇ»b
3987 "});
3988 cx.update_editor(|e, window, cx| {
3989 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3990 });
3991 cx.assert_editor_state(indoc! {"
3992 «aaa
3993 bbb
3994 added_lineˇ»
3995 "});
3996
3997 // Removing line
3998 cx.set_state(indoc! {"
3999 aa«a
4000 bbbˇ»
4001 "});
4002 cx.update_editor(|e, window, cx| {
4003 e.manipulate_lines(window, cx, |lines| {
4004 lines.pop();
4005 })
4006 });
4007 cx.assert_editor_state(indoc! {"
4008 «aaaˇ»
4009 "});
4010
4011 // Removing all lines
4012 cx.set_state(indoc! {"
4013 aa«a
4014 bbbˇ»
4015 "});
4016 cx.update_editor(|e, window, cx| {
4017 e.manipulate_lines(window, cx, |lines| {
4018 lines.drain(..);
4019 })
4020 });
4021 cx.assert_editor_state(indoc! {"
4022 ˇ
4023 "});
4024}
4025
4026#[gpui::test]
4027async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4028 init_test(cx, |_| {});
4029
4030 let mut cx = EditorTestContext::new(cx).await;
4031
4032 // Consider continuous selection as single selection
4033 cx.set_state(indoc! {"
4034 Aaa«aa
4035 cˇ»c«c
4036 bb
4037 aaaˇ»aa
4038 "});
4039 cx.update_editor(|e, window, cx| {
4040 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4041 });
4042 cx.assert_editor_state(indoc! {"
4043 «Aaaaa
4044 ccc
4045 bb
4046 aaaaaˇ»
4047 "});
4048
4049 cx.set_state(indoc! {"
4050 Aaa«aa
4051 cˇ»c«c
4052 bb
4053 aaaˇ»aa
4054 "});
4055 cx.update_editor(|e, window, cx| {
4056 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4057 });
4058 cx.assert_editor_state(indoc! {"
4059 «Aaaaa
4060 ccc
4061 bbˇ»
4062 "});
4063
4064 // Consider non continuous selection as distinct dedup operations
4065 cx.set_state(indoc! {"
4066 «aaaaa
4067 bb
4068 aaaaa
4069 aaaaaˇ»
4070
4071 aaa«aaˇ»
4072 "});
4073 cx.update_editor(|e, window, cx| {
4074 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4075 });
4076 cx.assert_editor_state(indoc! {"
4077 «aaaaa
4078 bbˇ»
4079
4080 «aaaaaˇ»
4081 "});
4082}
4083
4084#[gpui::test]
4085async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4086 init_test(cx, |_| {});
4087
4088 let mut cx = EditorTestContext::new(cx).await;
4089
4090 cx.set_state(indoc! {"
4091 «Aaa
4092 aAa
4093 Aaaˇ»
4094 "});
4095 cx.update_editor(|e, window, cx| {
4096 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4097 });
4098 cx.assert_editor_state(indoc! {"
4099 «Aaa
4100 aAaˇ»
4101 "});
4102
4103 cx.set_state(indoc! {"
4104 «Aaa
4105 aAa
4106 aaAˇ»
4107 "});
4108 cx.update_editor(|e, window, cx| {
4109 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4110 });
4111 cx.assert_editor_state(indoc! {"
4112 «Aaaˇ»
4113 "});
4114}
4115
4116#[gpui::test]
4117async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4118 init_test(cx, |_| {});
4119
4120 let mut cx = EditorTestContext::new(cx).await;
4121
4122 // Manipulate with multiple selections on a single line
4123 cx.set_state(indoc! {"
4124 dd«dd
4125 cˇ»c«c
4126 bb
4127 aaaˇ»aa
4128 "});
4129 cx.update_editor(|e, window, cx| {
4130 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4131 });
4132 cx.assert_editor_state(indoc! {"
4133 «aaaaa
4134 bb
4135 ccc
4136 ddddˇ»
4137 "});
4138
4139 // Manipulate with multiple disjoin selections
4140 cx.set_state(indoc! {"
4141 5«
4142 4
4143 3
4144 2
4145 1ˇ»
4146
4147 dd«dd
4148 ccc
4149 bb
4150 aaaˇ»aa
4151 "});
4152 cx.update_editor(|e, window, cx| {
4153 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4154 });
4155 cx.assert_editor_state(indoc! {"
4156 «1
4157 2
4158 3
4159 4
4160 5ˇ»
4161
4162 «aaaaa
4163 bb
4164 ccc
4165 ddddˇ»
4166 "});
4167
4168 // Adding lines on each selection
4169 cx.set_state(indoc! {"
4170 2«
4171 1ˇ»
4172
4173 bb«bb
4174 aaaˇ»aa
4175 "});
4176 cx.update_editor(|e, window, cx| {
4177 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4178 });
4179 cx.assert_editor_state(indoc! {"
4180 «2
4181 1
4182 added lineˇ»
4183
4184 «bbbb
4185 aaaaa
4186 added lineˇ»
4187 "});
4188
4189 // Removing lines on each selection
4190 cx.set_state(indoc! {"
4191 2«
4192 1ˇ»
4193
4194 bb«bb
4195 aaaˇ»aa
4196 "});
4197 cx.update_editor(|e, window, cx| {
4198 e.manipulate_lines(window, cx, |lines| {
4199 lines.pop();
4200 })
4201 });
4202 cx.assert_editor_state(indoc! {"
4203 «2ˇ»
4204
4205 «bbbbˇ»
4206 "});
4207}
4208
4209#[gpui::test]
4210async fn test_toggle_case(cx: &mut TestAppContext) {
4211 init_test(cx, |_| {});
4212
4213 let mut cx = EditorTestContext::new(cx).await;
4214
4215 // If all lower case -> upper case
4216 cx.set_state(indoc! {"
4217 «hello worldˇ»
4218 "});
4219 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4220 cx.assert_editor_state(indoc! {"
4221 «HELLO WORLDˇ»
4222 "});
4223
4224 // If all upper case -> lower case
4225 cx.set_state(indoc! {"
4226 «HELLO WORLDˇ»
4227 "});
4228 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4229 cx.assert_editor_state(indoc! {"
4230 «hello worldˇ»
4231 "});
4232
4233 // If any upper case characters are identified -> lower case
4234 // This matches JetBrains IDEs
4235 cx.set_state(indoc! {"
4236 «hEllo worldˇ»
4237 "});
4238 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4239 cx.assert_editor_state(indoc! {"
4240 «hello worldˇ»
4241 "});
4242}
4243
4244#[gpui::test]
4245async fn test_manipulate_text(cx: &mut TestAppContext) {
4246 init_test(cx, |_| {});
4247
4248 let mut cx = EditorTestContext::new(cx).await;
4249
4250 // Test convert_to_upper_case()
4251 cx.set_state(indoc! {"
4252 «hello worldˇ»
4253 "});
4254 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4255 cx.assert_editor_state(indoc! {"
4256 «HELLO WORLDˇ»
4257 "});
4258
4259 // Test convert_to_lower_case()
4260 cx.set_state(indoc! {"
4261 «HELLO WORLDˇ»
4262 "});
4263 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4264 cx.assert_editor_state(indoc! {"
4265 «hello worldˇ»
4266 "});
4267
4268 // Test multiple line, single selection case
4269 cx.set_state(indoc! {"
4270 «The quick brown
4271 fox jumps over
4272 the lazy dogˇ»
4273 "});
4274 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4275 cx.assert_editor_state(indoc! {"
4276 «The Quick Brown
4277 Fox Jumps Over
4278 The Lazy Dogˇ»
4279 "});
4280
4281 // Test multiple line, single selection case
4282 cx.set_state(indoc! {"
4283 «The quick brown
4284 fox jumps over
4285 the lazy dogˇ»
4286 "});
4287 cx.update_editor(|e, window, cx| {
4288 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4289 });
4290 cx.assert_editor_state(indoc! {"
4291 «TheQuickBrown
4292 FoxJumpsOver
4293 TheLazyDogˇ»
4294 "});
4295
4296 // From here on out, test more complex cases of manipulate_text()
4297
4298 // Test no selection case - should affect words cursors are in
4299 // Cursor at beginning, middle, and end of word
4300 cx.set_state(indoc! {"
4301 ˇhello big beauˇtiful worldˇ
4302 "});
4303 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4304 cx.assert_editor_state(indoc! {"
4305 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4306 "});
4307
4308 // Test multiple selections on a single line and across multiple lines
4309 cx.set_state(indoc! {"
4310 «Theˇ» quick «brown
4311 foxˇ» jumps «overˇ»
4312 the «lazyˇ» dog
4313 "});
4314 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4315 cx.assert_editor_state(indoc! {"
4316 «THEˇ» quick «BROWN
4317 FOXˇ» jumps «OVERˇ»
4318 the «LAZYˇ» dog
4319 "});
4320
4321 // Test case where text length grows
4322 cx.set_state(indoc! {"
4323 «tschüߡ»
4324 "});
4325 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4326 cx.assert_editor_state(indoc! {"
4327 «TSCHÜSSˇ»
4328 "});
4329
4330 // Test to make sure we don't crash when text shrinks
4331 cx.set_state(indoc! {"
4332 aaa_bbbˇ
4333 "});
4334 cx.update_editor(|e, window, cx| {
4335 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4336 });
4337 cx.assert_editor_state(indoc! {"
4338 «aaaBbbˇ»
4339 "});
4340
4341 // Test to make sure we all aware of the fact that each word can grow and shrink
4342 // Final selections should be aware of this fact
4343 cx.set_state(indoc! {"
4344 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4345 "});
4346 cx.update_editor(|e, window, cx| {
4347 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4348 });
4349 cx.assert_editor_state(indoc! {"
4350 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4351 "});
4352
4353 cx.set_state(indoc! {"
4354 «hElLo, WoRld!ˇ»
4355 "});
4356 cx.update_editor(|e, window, cx| {
4357 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4358 });
4359 cx.assert_editor_state(indoc! {"
4360 «HeLlO, wOrLD!ˇ»
4361 "});
4362}
4363
4364#[gpui::test]
4365fn test_duplicate_line(cx: &mut TestAppContext) {
4366 init_test(cx, |_| {});
4367
4368 let editor = cx.add_window(|window, cx| {
4369 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4370 build_editor(buffer, window, cx)
4371 });
4372 _ = editor.update(cx, |editor, window, cx| {
4373 editor.change_selections(None, window, cx, |s| {
4374 s.select_display_ranges([
4375 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4376 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4377 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4378 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4379 ])
4380 });
4381 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4382 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4383 assert_eq!(
4384 editor.selections.display_ranges(cx),
4385 vec![
4386 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4387 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4388 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4389 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4390 ]
4391 );
4392 });
4393
4394 let editor = cx.add_window(|window, cx| {
4395 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4396 build_editor(buffer, window, cx)
4397 });
4398 _ = editor.update(cx, |editor, window, cx| {
4399 editor.change_selections(None, window, cx, |s| {
4400 s.select_display_ranges([
4401 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4402 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4403 ])
4404 });
4405 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4406 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4407 assert_eq!(
4408 editor.selections.display_ranges(cx),
4409 vec![
4410 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4411 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4412 ]
4413 );
4414 });
4415
4416 // With `move_upwards` the selections stay in place, except for
4417 // the lines inserted above them
4418 let editor = cx.add_window(|window, cx| {
4419 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4420 build_editor(buffer, window, cx)
4421 });
4422 _ = editor.update(cx, |editor, window, cx| {
4423 editor.change_selections(None, window, cx, |s| {
4424 s.select_display_ranges([
4425 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4426 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4427 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4428 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4429 ])
4430 });
4431 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4432 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4433 assert_eq!(
4434 editor.selections.display_ranges(cx),
4435 vec![
4436 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4437 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4438 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4439 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4440 ]
4441 );
4442 });
4443
4444 let editor = cx.add_window(|window, cx| {
4445 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4446 build_editor(buffer, window, cx)
4447 });
4448 _ = editor.update(cx, |editor, window, cx| {
4449 editor.change_selections(None, window, cx, |s| {
4450 s.select_display_ranges([
4451 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4452 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4453 ])
4454 });
4455 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4456 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4457 assert_eq!(
4458 editor.selections.display_ranges(cx),
4459 vec![
4460 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4461 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4462 ]
4463 );
4464 });
4465
4466 let editor = cx.add_window(|window, cx| {
4467 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4468 build_editor(buffer, window, cx)
4469 });
4470 _ = editor.update(cx, |editor, window, cx| {
4471 editor.change_selections(None, window, cx, |s| {
4472 s.select_display_ranges([
4473 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4474 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4475 ])
4476 });
4477 editor.duplicate_selection(&DuplicateSelection, window, cx);
4478 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4479 assert_eq!(
4480 editor.selections.display_ranges(cx),
4481 vec![
4482 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4483 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4484 ]
4485 );
4486 });
4487}
4488
4489#[gpui::test]
4490fn test_move_line_up_down(cx: &mut TestAppContext) {
4491 init_test(cx, |_| {});
4492
4493 let editor = cx.add_window(|window, cx| {
4494 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4495 build_editor(buffer, window, cx)
4496 });
4497 _ = editor.update(cx, |editor, window, cx| {
4498 editor.fold_creases(
4499 vec![
4500 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4501 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4502 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4503 ],
4504 true,
4505 window,
4506 cx,
4507 );
4508 editor.change_selections(None, window, cx, |s| {
4509 s.select_display_ranges([
4510 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4511 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4512 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4513 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4514 ])
4515 });
4516 assert_eq!(
4517 editor.display_text(cx),
4518 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4519 );
4520
4521 editor.move_line_up(&MoveLineUp, window, cx);
4522 assert_eq!(
4523 editor.display_text(cx),
4524 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4525 );
4526 assert_eq!(
4527 editor.selections.display_ranges(cx),
4528 vec![
4529 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4530 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4531 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4532 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4533 ]
4534 );
4535 });
4536
4537 _ = editor.update(cx, |editor, window, cx| {
4538 editor.move_line_down(&MoveLineDown, window, cx);
4539 assert_eq!(
4540 editor.display_text(cx),
4541 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4542 );
4543 assert_eq!(
4544 editor.selections.display_ranges(cx),
4545 vec![
4546 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4547 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4548 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4549 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4550 ]
4551 );
4552 });
4553
4554 _ = editor.update(cx, |editor, window, cx| {
4555 editor.move_line_down(&MoveLineDown, window, cx);
4556 assert_eq!(
4557 editor.display_text(cx),
4558 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4559 );
4560 assert_eq!(
4561 editor.selections.display_ranges(cx),
4562 vec![
4563 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4564 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4565 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4566 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4567 ]
4568 );
4569 });
4570
4571 _ = editor.update(cx, |editor, window, cx| {
4572 editor.move_line_up(&MoveLineUp, window, cx);
4573 assert_eq!(
4574 editor.display_text(cx),
4575 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4576 );
4577 assert_eq!(
4578 editor.selections.display_ranges(cx),
4579 vec![
4580 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4581 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4582 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4584 ]
4585 );
4586 });
4587}
4588
4589#[gpui::test]
4590fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4591 init_test(cx, |_| {});
4592
4593 let editor = cx.add_window(|window, cx| {
4594 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4595 build_editor(buffer, window, cx)
4596 });
4597 _ = editor.update(cx, |editor, window, cx| {
4598 let snapshot = editor.buffer.read(cx).snapshot(cx);
4599 editor.insert_blocks(
4600 [BlockProperties {
4601 style: BlockStyle::Fixed,
4602 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4603 height: Some(1),
4604 render: Arc::new(|_| div().into_any()),
4605 priority: 0,
4606 render_in_minimap: true,
4607 }],
4608 Some(Autoscroll::fit()),
4609 cx,
4610 );
4611 editor.change_selections(None, window, cx, |s| {
4612 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4613 });
4614 editor.move_line_down(&MoveLineDown, window, cx);
4615 });
4616}
4617
4618#[gpui::test]
4619async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4620 init_test(cx, |_| {});
4621
4622 let mut cx = EditorTestContext::new(cx).await;
4623 cx.set_state(
4624 &"
4625 ˇzero
4626 one
4627 two
4628 three
4629 four
4630 five
4631 "
4632 .unindent(),
4633 );
4634
4635 // Create a four-line block that replaces three lines of text.
4636 cx.update_editor(|editor, window, cx| {
4637 let snapshot = editor.snapshot(window, cx);
4638 let snapshot = &snapshot.buffer_snapshot;
4639 let placement = BlockPlacement::Replace(
4640 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4641 );
4642 editor.insert_blocks(
4643 [BlockProperties {
4644 placement,
4645 height: Some(4),
4646 style: BlockStyle::Sticky,
4647 render: Arc::new(|_| gpui::div().into_any_element()),
4648 priority: 0,
4649 render_in_minimap: true,
4650 }],
4651 None,
4652 cx,
4653 );
4654 });
4655
4656 // Move down so that the cursor touches the block.
4657 cx.update_editor(|editor, window, cx| {
4658 editor.move_down(&Default::default(), window, cx);
4659 });
4660 cx.assert_editor_state(
4661 &"
4662 zero
4663 «one
4664 two
4665 threeˇ»
4666 four
4667 five
4668 "
4669 .unindent(),
4670 );
4671
4672 // Move down past the block.
4673 cx.update_editor(|editor, window, cx| {
4674 editor.move_down(&Default::default(), window, cx);
4675 });
4676 cx.assert_editor_state(
4677 &"
4678 zero
4679 one
4680 two
4681 three
4682 ˇfour
4683 five
4684 "
4685 .unindent(),
4686 );
4687}
4688
4689#[gpui::test]
4690fn test_transpose(cx: &mut TestAppContext) {
4691 init_test(cx, |_| {});
4692
4693 _ = cx.add_window(|window, cx| {
4694 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4695 editor.set_style(EditorStyle::default(), window, cx);
4696 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4697 editor.transpose(&Default::default(), window, cx);
4698 assert_eq!(editor.text(cx), "bac");
4699 assert_eq!(editor.selections.ranges(cx), [2..2]);
4700
4701 editor.transpose(&Default::default(), window, cx);
4702 assert_eq!(editor.text(cx), "bca");
4703 assert_eq!(editor.selections.ranges(cx), [3..3]);
4704
4705 editor.transpose(&Default::default(), window, cx);
4706 assert_eq!(editor.text(cx), "bac");
4707 assert_eq!(editor.selections.ranges(cx), [3..3]);
4708
4709 editor
4710 });
4711
4712 _ = cx.add_window(|window, cx| {
4713 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4714 editor.set_style(EditorStyle::default(), window, cx);
4715 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4716 editor.transpose(&Default::default(), window, cx);
4717 assert_eq!(editor.text(cx), "acb\nde");
4718 assert_eq!(editor.selections.ranges(cx), [3..3]);
4719
4720 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4721 editor.transpose(&Default::default(), window, cx);
4722 assert_eq!(editor.text(cx), "acbd\ne");
4723 assert_eq!(editor.selections.ranges(cx), [5..5]);
4724
4725 editor.transpose(&Default::default(), window, cx);
4726 assert_eq!(editor.text(cx), "acbde\n");
4727 assert_eq!(editor.selections.ranges(cx), [6..6]);
4728
4729 editor.transpose(&Default::default(), window, cx);
4730 assert_eq!(editor.text(cx), "acbd\ne");
4731 assert_eq!(editor.selections.ranges(cx), [6..6]);
4732
4733 editor
4734 });
4735
4736 _ = cx.add_window(|window, cx| {
4737 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4738 editor.set_style(EditorStyle::default(), window, cx);
4739 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4740 editor.transpose(&Default::default(), window, cx);
4741 assert_eq!(editor.text(cx), "bacd\ne");
4742 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4743
4744 editor.transpose(&Default::default(), window, cx);
4745 assert_eq!(editor.text(cx), "bcade\n");
4746 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4747
4748 editor.transpose(&Default::default(), window, cx);
4749 assert_eq!(editor.text(cx), "bcda\ne");
4750 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4751
4752 editor.transpose(&Default::default(), window, cx);
4753 assert_eq!(editor.text(cx), "bcade\n");
4754 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4755
4756 editor.transpose(&Default::default(), window, cx);
4757 assert_eq!(editor.text(cx), "bcaed\n");
4758 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4759
4760 editor
4761 });
4762
4763 _ = cx.add_window(|window, cx| {
4764 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4765 editor.set_style(EditorStyle::default(), window, cx);
4766 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4767 editor.transpose(&Default::default(), window, cx);
4768 assert_eq!(editor.text(cx), "🏀🍐✋");
4769 assert_eq!(editor.selections.ranges(cx), [8..8]);
4770
4771 editor.transpose(&Default::default(), window, cx);
4772 assert_eq!(editor.text(cx), "🏀✋🍐");
4773 assert_eq!(editor.selections.ranges(cx), [11..11]);
4774
4775 editor.transpose(&Default::default(), window, cx);
4776 assert_eq!(editor.text(cx), "🏀🍐✋");
4777 assert_eq!(editor.selections.ranges(cx), [11..11]);
4778
4779 editor
4780 });
4781}
4782
4783#[gpui::test]
4784async fn test_rewrap(cx: &mut TestAppContext) {
4785 init_test(cx, |settings| {
4786 settings.languages.extend([
4787 (
4788 "Markdown".into(),
4789 LanguageSettingsContent {
4790 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4791 ..Default::default()
4792 },
4793 ),
4794 (
4795 "Plain Text".into(),
4796 LanguageSettingsContent {
4797 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4798 ..Default::default()
4799 },
4800 ),
4801 ])
4802 });
4803
4804 let mut cx = EditorTestContext::new(cx).await;
4805
4806 let language_with_c_comments = Arc::new(Language::new(
4807 LanguageConfig {
4808 line_comments: vec!["// ".into()],
4809 ..LanguageConfig::default()
4810 },
4811 None,
4812 ));
4813 let language_with_pound_comments = Arc::new(Language::new(
4814 LanguageConfig {
4815 line_comments: vec!["# ".into()],
4816 ..LanguageConfig::default()
4817 },
4818 None,
4819 ));
4820 let markdown_language = Arc::new(Language::new(
4821 LanguageConfig {
4822 name: "Markdown".into(),
4823 ..LanguageConfig::default()
4824 },
4825 None,
4826 ));
4827 let language_with_doc_comments = Arc::new(Language::new(
4828 LanguageConfig {
4829 line_comments: vec!["// ".into(), "/// ".into()],
4830 ..LanguageConfig::default()
4831 },
4832 Some(tree_sitter_rust::LANGUAGE.into()),
4833 ));
4834
4835 let plaintext_language = Arc::new(Language::new(
4836 LanguageConfig {
4837 name: "Plain Text".into(),
4838 ..LanguageConfig::default()
4839 },
4840 None,
4841 ));
4842
4843 assert_rewrap(
4844 indoc! {"
4845 // ˇ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.
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. Vivamus sit amet neque et quam
4851 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4852 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4853 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4854 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4855 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4856 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4857 // porttitor id. Aliquam id accumsan eros.
4858 "},
4859 language_with_c_comments.clone(),
4860 &mut cx,
4861 );
4862
4863 // Test that rewrapping works inside of a selection
4864 assert_rewrap(
4865 indoc! {"
4866 «// 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.ˇ»
4867 "},
4868 indoc! {"
4869 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4870 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4871 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4872 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4873 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4874 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4875 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4876 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4877 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4878 // porttitor id. Aliquam id accumsan eros.ˇ»
4879 "},
4880 language_with_c_comments.clone(),
4881 &mut cx,
4882 );
4883
4884 // Test that cursors that expand to the same region are collapsed.
4885 assert_rewrap(
4886 indoc! {"
4887 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4888 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4889 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4890 // ˇ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.
4891 "},
4892 indoc! {"
4893 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4894 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4895 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4896 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4897 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4898 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4899 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4900 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4901 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4902 // porttitor id. Aliquam id accumsan eros.
4903 "},
4904 language_with_c_comments.clone(),
4905 &mut cx,
4906 );
4907
4908 // Test that non-contiguous selections are treated separately.
4909 assert_rewrap(
4910 indoc! {"
4911 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4912 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4913 //
4914 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4915 // ˇ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.
4916 "},
4917 indoc! {"
4918 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4919 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4920 // auctor, eu lacinia sapien scelerisque.
4921 //
4922 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4923 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4924 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4925 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4926 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4927 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4928 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4929 "},
4930 language_with_c_comments.clone(),
4931 &mut cx,
4932 );
4933
4934 // Test that different comment prefixes are supported.
4935 assert_rewrap(
4936 indoc! {"
4937 # ˇ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.
4938 "},
4939 indoc! {"
4940 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4941 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4942 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4943 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4944 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4945 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4946 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4947 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4948 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4949 # accumsan eros.
4950 "},
4951 language_with_pound_comments.clone(),
4952 &mut cx,
4953 );
4954
4955 // Test that rewrapping is ignored outside of comments in most languages.
4956 assert_rewrap(
4957 indoc! {"
4958 /// Adds two numbers.
4959 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4960 fn add(a: u32, b: u32) -> u32 {
4961 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ˇ
4962 }
4963 "},
4964 indoc! {"
4965 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4966 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4967 fn add(a: u32, b: u32) -> u32 {
4968 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ˇ
4969 }
4970 "},
4971 language_with_doc_comments.clone(),
4972 &mut cx,
4973 );
4974
4975 // Test that rewrapping works in Markdown and Plain Text languages.
4976 assert_rewrap(
4977 indoc! {"
4978 # Hello
4979
4980 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.
4981 "},
4982 indoc! {"
4983 # Hello
4984
4985 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4986 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4987 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4988 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4989 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4990 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4991 Integer sit amet scelerisque nisi.
4992 "},
4993 markdown_language,
4994 &mut cx,
4995 );
4996
4997 assert_rewrap(
4998 indoc! {"
4999 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.
5000 "},
5001 indoc! {"
5002 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5003 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5004 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5005 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5006 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5007 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5008 Integer sit amet scelerisque nisi.
5009 "},
5010 plaintext_language,
5011 &mut cx,
5012 );
5013
5014 // Test rewrapping unaligned comments in a selection.
5015 assert_rewrap(
5016 indoc! {"
5017 fn foo() {
5018 if true {
5019 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5020 // Praesent semper egestas tellus id dignissim.ˇ»
5021 do_something();
5022 } else {
5023 //
5024 }
5025 }
5026 "},
5027 indoc! {"
5028 fn foo() {
5029 if true {
5030 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5031 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5032 // egestas tellus id dignissim.ˇ»
5033 do_something();
5034 } else {
5035 //
5036 }
5037 }
5038 "},
5039 language_with_doc_comments.clone(),
5040 &mut cx,
5041 );
5042
5043 assert_rewrap(
5044 indoc! {"
5045 fn foo() {
5046 if true {
5047 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5048 // Praesent semper egestas tellus id dignissim.»
5049 do_something();
5050 } else {
5051 //
5052 }
5053
5054 }
5055 "},
5056 indoc! {"
5057 fn foo() {
5058 if true {
5059 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5060 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5061 // egestas tellus id dignissim.»
5062 do_something();
5063 } else {
5064 //
5065 }
5066
5067 }
5068 "},
5069 language_with_doc_comments.clone(),
5070 &mut cx,
5071 );
5072
5073 #[track_caller]
5074 fn assert_rewrap(
5075 unwrapped_text: &str,
5076 wrapped_text: &str,
5077 language: Arc<Language>,
5078 cx: &mut EditorTestContext,
5079 ) {
5080 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5081 cx.set_state(unwrapped_text);
5082 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5083 cx.assert_editor_state(wrapped_text);
5084 }
5085}
5086
5087#[gpui::test]
5088async fn test_hard_wrap(cx: &mut TestAppContext) {
5089 init_test(cx, |_| {});
5090 let mut cx = EditorTestContext::new(cx).await;
5091
5092 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5093 cx.update_editor(|editor, _, cx| {
5094 editor.set_hard_wrap(Some(14), cx);
5095 });
5096
5097 cx.set_state(indoc!(
5098 "
5099 one two three ˇ
5100 "
5101 ));
5102 cx.simulate_input("four");
5103 cx.run_until_parked();
5104
5105 cx.assert_editor_state(indoc!(
5106 "
5107 one two three
5108 fourˇ
5109 "
5110 ));
5111
5112 cx.update_editor(|editor, window, cx| {
5113 editor.newline(&Default::default(), window, cx);
5114 });
5115 cx.run_until_parked();
5116 cx.assert_editor_state(indoc!(
5117 "
5118 one two three
5119 four
5120 ˇ
5121 "
5122 ));
5123
5124 cx.simulate_input("five");
5125 cx.run_until_parked();
5126 cx.assert_editor_state(indoc!(
5127 "
5128 one two three
5129 four
5130 fiveˇ
5131 "
5132 ));
5133
5134 cx.update_editor(|editor, window, cx| {
5135 editor.newline(&Default::default(), window, cx);
5136 });
5137 cx.run_until_parked();
5138 cx.simulate_input("# ");
5139 cx.run_until_parked();
5140 cx.assert_editor_state(indoc!(
5141 "
5142 one two three
5143 four
5144 five
5145 # ˇ
5146 "
5147 ));
5148
5149 cx.update_editor(|editor, window, cx| {
5150 editor.newline(&Default::default(), window, cx);
5151 });
5152 cx.run_until_parked();
5153 cx.assert_editor_state(indoc!(
5154 "
5155 one two three
5156 four
5157 five
5158 #\x20
5159 #ˇ
5160 "
5161 ));
5162
5163 cx.simulate_input(" 6");
5164 cx.run_until_parked();
5165 cx.assert_editor_state(indoc!(
5166 "
5167 one two three
5168 four
5169 five
5170 #
5171 # 6ˇ
5172 "
5173 ));
5174}
5175
5176#[gpui::test]
5177async fn test_clipboard(cx: &mut TestAppContext) {
5178 init_test(cx, |_| {});
5179
5180 let mut cx = EditorTestContext::new(cx).await;
5181
5182 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5183 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5184 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5185
5186 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5187 cx.set_state("two ˇfour ˇsix ˇ");
5188 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5189 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5190
5191 // Paste again but with only two cursors. Since the number of cursors doesn't
5192 // match the number of slices in the clipboard, the entire clipboard text
5193 // is pasted at each cursor.
5194 cx.set_state("ˇtwo one✅ four three six five ˇ");
5195 cx.update_editor(|e, window, cx| {
5196 e.handle_input("( ", window, cx);
5197 e.paste(&Paste, window, cx);
5198 e.handle_input(") ", window, cx);
5199 });
5200 cx.assert_editor_state(
5201 &([
5202 "( one✅ ",
5203 "three ",
5204 "five ) ˇtwo one✅ four three six five ( one✅ ",
5205 "three ",
5206 "five ) ˇ",
5207 ]
5208 .join("\n")),
5209 );
5210
5211 // Cut with three selections, one of which is full-line.
5212 cx.set_state(indoc! {"
5213 1«2ˇ»3
5214 4ˇ567
5215 «8ˇ»9"});
5216 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5217 cx.assert_editor_state(indoc! {"
5218 1ˇ3
5219 ˇ9"});
5220
5221 // Paste with three selections, noticing how the copied selection that was full-line
5222 // gets inserted before the second cursor.
5223 cx.set_state(indoc! {"
5224 1ˇ3
5225 9ˇ
5226 «oˇ»ne"});
5227 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5228 cx.assert_editor_state(indoc! {"
5229 12ˇ3
5230 4567
5231 9ˇ
5232 8ˇne"});
5233
5234 // Copy with a single cursor only, which writes the whole line into the clipboard.
5235 cx.set_state(indoc! {"
5236 The quick brown
5237 fox juˇmps over
5238 the lazy dog"});
5239 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5240 assert_eq!(
5241 cx.read_from_clipboard()
5242 .and_then(|item| item.text().as_deref().map(str::to_string)),
5243 Some("fox jumps over\n".to_string())
5244 );
5245
5246 // Paste with three selections, noticing how the copied full-line selection is inserted
5247 // before the empty selections but replaces the selection that is non-empty.
5248 cx.set_state(indoc! {"
5249 Tˇhe quick brown
5250 «foˇ»x jumps over
5251 tˇhe lazy dog"});
5252 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5253 cx.assert_editor_state(indoc! {"
5254 fox jumps over
5255 Tˇhe quick brown
5256 fox jumps over
5257 ˇx jumps over
5258 fox jumps over
5259 tˇhe lazy dog"});
5260}
5261
5262#[gpui::test]
5263async fn test_copy_trim(cx: &mut TestAppContext) {
5264 init_test(cx, |_| {});
5265
5266 let mut cx = EditorTestContext::new(cx).await;
5267 cx.set_state(
5268 r#" «for selection in selections.iter() {
5269 let mut start = selection.start;
5270 let mut end = selection.end;
5271 let is_entire_line = selection.is_empty();
5272 if is_entire_line {
5273 start = Point::new(start.row, 0);ˇ»
5274 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5275 }
5276 "#,
5277 );
5278 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5279 assert_eq!(
5280 cx.read_from_clipboard()
5281 .and_then(|item| item.text().as_deref().map(str::to_string)),
5282 Some(
5283 "for selection in selections.iter() {
5284 let mut start = selection.start;
5285 let mut end = selection.end;
5286 let is_entire_line = selection.is_empty();
5287 if is_entire_line {
5288 start = Point::new(start.row, 0);"
5289 .to_string()
5290 ),
5291 "Regular copying preserves all indentation selected",
5292 );
5293 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5294 assert_eq!(
5295 cx.read_from_clipboard()
5296 .and_then(|item| item.text().as_deref().map(str::to_string)),
5297 Some(
5298 "for selection in selections.iter() {
5299let mut start = selection.start;
5300let mut end = selection.end;
5301let is_entire_line = selection.is_empty();
5302if is_entire_line {
5303 start = Point::new(start.row, 0);"
5304 .to_string()
5305 ),
5306 "Copying with stripping should strip all leading whitespaces"
5307 );
5308
5309 cx.set_state(
5310 r#" « for selection in selections.iter() {
5311 let mut start = selection.start;
5312 let mut end = selection.end;
5313 let is_entire_line = selection.is_empty();
5314 if is_entire_line {
5315 start = Point::new(start.row, 0);ˇ»
5316 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5317 }
5318 "#,
5319 );
5320 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5321 assert_eq!(
5322 cx.read_from_clipboard()
5323 .and_then(|item| item.text().as_deref().map(str::to_string)),
5324 Some(
5325 " for selection in selections.iter() {
5326 let mut start = selection.start;
5327 let mut end = selection.end;
5328 let is_entire_line = selection.is_empty();
5329 if is_entire_line {
5330 start = Point::new(start.row, 0);"
5331 .to_string()
5332 ),
5333 "Regular copying preserves all indentation selected",
5334 );
5335 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5336 assert_eq!(
5337 cx.read_from_clipboard()
5338 .and_then(|item| item.text().as_deref().map(str::to_string)),
5339 Some(
5340 "for selection in selections.iter() {
5341let mut start = selection.start;
5342let mut end = selection.end;
5343let is_entire_line = selection.is_empty();
5344if is_entire_line {
5345 start = Point::new(start.row, 0);"
5346 .to_string()
5347 ),
5348 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5349 );
5350
5351 cx.set_state(
5352 r#" «ˇ for selection in selections.iter() {
5353 let mut start = selection.start;
5354 let mut end = selection.end;
5355 let is_entire_line = selection.is_empty();
5356 if is_entire_line {
5357 start = Point::new(start.row, 0);»
5358 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5359 }
5360 "#,
5361 );
5362 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5363 assert_eq!(
5364 cx.read_from_clipboard()
5365 .and_then(|item| item.text().as_deref().map(str::to_string)),
5366 Some(
5367 " for selection in selections.iter() {
5368 let mut start = selection.start;
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 .to_string()
5374 ),
5375 "Regular copying for reverse selection works the same",
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;
5384let mut end = selection.end;
5385let is_entire_line = selection.is_empty();
5386if is_entire_line {
5387 start = Point::new(start.row, 0);"
5388 .to_string()
5389 ),
5390 "Copying with stripping for reverse selection works the same"
5391 );
5392
5393 cx.set_state(
5394 r#" for selection «in selections.iter() {
5395 let mut start = selection.start;
5396 let mut end = selection.end;
5397 let is_entire_line = selection.is_empty();
5398 if is_entire_line {
5399 start = Point::new(start.row, 0);ˇ»
5400 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5401 }
5402 "#,
5403 );
5404 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5405 assert_eq!(
5406 cx.read_from_clipboard()
5407 .and_then(|item| item.text().as_deref().map(str::to_string)),
5408 Some(
5409 "in selections.iter() {
5410 let mut start = selection.start;
5411 let mut end = selection.end;
5412 let is_entire_line = selection.is_empty();
5413 if is_entire_line {
5414 start = Point::new(start.row, 0);"
5415 .to_string()
5416 ),
5417 "When selecting past the indent, the copying works as usual",
5418 );
5419 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5420 assert_eq!(
5421 cx.read_from_clipboard()
5422 .and_then(|item| item.text().as_deref().map(str::to_string)),
5423 Some(
5424 "in selections.iter() {
5425 let mut start = selection.start;
5426 let mut end = selection.end;
5427 let is_entire_line = selection.is_empty();
5428 if is_entire_line {
5429 start = Point::new(start.row, 0);"
5430 .to_string()
5431 ),
5432 "When selecting past the indent, nothing is trimmed"
5433 );
5434
5435 cx.set_state(
5436 r#" «for selection in selections.iter() {
5437 let mut start = selection.start;
5438
5439 let mut end = selection.end;
5440 let is_entire_line = selection.is_empty();
5441 if is_entire_line {
5442 start = Point::new(start.row, 0);
5443ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5444 }
5445 "#,
5446 );
5447 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5448 assert_eq!(
5449 cx.read_from_clipboard()
5450 .and_then(|item| item.text().as_deref().map(str::to_string)),
5451 Some(
5452 "for selection in selections.iter() {
5453let mut start = selection.start;
5454
5455let mut end = selection.end;
5456let is_entire_line = selection.is_empty();
5457if is_entire_line {
5458 start = Point::new(start.row, 0);
5459"
5460 .to_string()
5461 ),
5462 "Copying with stripping should ignore empty lines"
5463 );
5464}
5465
5466#[gpui::test]
5467async fn test_paste_multiline(cx: &mut TestAppContext) {
5468 init_test(cx, |_| {});
5469
5470 let mut cx = EditorTestContext::new(cx).await;
5471 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5472
5473 // Cut an indented block, without the leading whitespace.
5474 cx.set_state(indoc! {"
5475 const a: B = (
5476 c(),
5477 «d(
5478 e,
5479 f
5480 )ˇ»
5481 );
5482 "});
5483 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5484 cx.assert_editor_state(indoc! {"
5485 const a: B = (
5486 c(),
5487 ˇ
5488 );
5489 "});
5490
5491 // Paste it at the same position.
5492 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5493 cx.assert_editor_state(indoc! {"
5494 const a: B = (
5495 c(),
5496 d(
5497 e,
5498 f
5499 )ˇ
5500 );
5501 "});
5502
5503 // Paste it at a line with a lower indent level.
5504 cx.set_state(indoc! {"
5505 ˇ
5506 const a: B = (
5507 c(),
5508 );
5509 "});
5510 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5511 cx.assert_editor_state(indoc! {"
5512 d(
5513 e,
5514 f
5515 )ˇ
5516 const a: B = (
5517 c(),
5518 );
5519 "});
5520
5521 // Cut an indented block, with the leading whitespace.
5522 cx.set_state(indoc! {"
5523 const a: B = (
5524 c(),
5525 « d(
5526 e,
5527 f
5528 )
5529 ˇ»);
5530 "});
5531 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5532 cx.assert_editor_state(indoc! {"
5533 const a: B = (
5534 c(),
5535 ˇ);
5536 "});
5537
5538 // Paste it at the same position.
5539 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5540 cx.assert_editor_state(indoc! {"
5541 const a: B = (
5542 c(),
5543 d(
5544 e,
5545 f
5546 )
5547 ˇ);
5548 "});
5549
5550 // Paste it at a line with a higher indent level.
5551 cx.set_state(indoc! {"
5552 const a: B = (
5553 c(),
5554 d(
5555 e,
5556 fˇ
5557 )
5558 );
5559 "});
5560 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5561 cx.assert_editor_state(indoc! {"
5562 const a: B = (
5563 c(),
5564 d(
5565 e,
5566 f d(
5567 e,
5568 f
5569 )
5570 ˇ
5571 )
5572 );
5573 "});
5574
5575 // Copy an indented block, starting mid-line
5576 cx.set_state(indoc! {"
5577 const a: B = (
5578 c(),
5579 somethin«g(
5580 e,
5581 f
5582 )ˇ»
5583 );
5584 "});
5585 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5586
5587 // Paste it on a line with a lower indent level
5588 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5589 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5590 cx.assert_editor_state(indoc! {"
5591 const a: B = (
5592 c(),
5593 something(
5594 e,
5595 f
5596 )
5597 );
5598 g(
5599 e,
5600 f
5601 )ˇ"});
5602}
5603
5604#[gpui::test]
5605async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5606 init_test(cx, |_| {});
5607
5608 cx.write_to_clipboard(ClipboardItem::new_string(
5609 " d(\n e\n );\n".into(),
5610 ));
5611
5612 let mut cx = EditorTestContext::new(cx).await;
5613 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5614
5615 cx.set_state(indoc! {"
5616 fn a() {
5617 b();
5618 if c() {
5619 ˇ
5620 }
5621 }
5622 "});
5623
5624 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5625 cx.assert_editor_state(indoc! {"
5626 fn a() {
5627 b();
5628 if c() {
5629 d(
5630 e
5631 );
5632 ˇ
5633 }
5634 }
5635 "});
5636
5637 cx.set_state(indoc! {"
5638 fn a() {
5639 b();
5640 ˇ
5641 }
5642 "});
5643
5644 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5645 cx.assert_editor_state(indoc! {"
5646 fn a() {
5647 b();
5648 d(
5649 e
5650 );
5651 ˇ
5652 }
5653 "});
5654}
5655
5656#[gpui::test]
5657fn test_select_all(cx: &mut TestAppContext) {
5658 init_test(cx, |_| {});
5659
5660 let editor = cx.add_window(|window, cx| {
5661 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5662 build_editor(buffer, window, cx)
5663 });
5664 _ = editor.update(cx, |editor, window, cx| {
5665 editor.select_all(&SelectAll, window, cx);
5666 assert_eq!(
5667 editor.selections.display_ranges(cx),
5668 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5669 );
5670 });
5671}
5672
5673#[gpui::test]
5674fn test_select_line(cx: &mut TestAppContext) {
5675 init_test(cx, |_| {});
5676
5677 let editor = cx.add_window(|window, cx| {
5678 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5679 build_editor(buffer, window, cx)
5680 });
5681 _ = editor.update(cx, |editor, window, cx| {
5682 editor.change_selections(None, window, cx, |s| {
5683 s.select_display_ranges([
5684 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5685 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5686 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5687 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5688 ])
5689 });
5690 editor.select_line(&SelectLine, window, cx);
5691 assert_eq!(
5692 editor.selections.display_ranges(cx),
5693 vec![
5694 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5695 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5696 ]
5697 );
5698 });
5699
5700 _ = editor.update(cx, |editor, window, cx| {
5701 editor.select_line(&SelectLine, window, cx);
5702 assert_eq!(
5703 editor.selections.display_ranges(cx),
5704 vec![
5705 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5706 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5707 ]
5708 );
5709 });
5710
5711 _ = editor.update(cx, |editor, window, cx| {
5712 editor.select_line(&SelectLine, window, cx);
5713 assert_eq!(
5714 editor.selections.display_ranges(cx),
5715 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5716 );
5717 });
5718}
5719
5720#[gpui::test]
5721async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5722 init_test(cx, |_| {});
5723 let mut cx = EditorTestContext::new(cx).await;
5724
5725 #[track_caller]
5726 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5727 cx.set_state(initial_state);
5728 cx.update_editor(|e, window, cx| {
5729 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5730 });
5731 cx.assert_editor_state(expected_state);
5732 }
5733
5734 // Selection starts and ends at the middle of lines, left-to-right
5735 test(
5736 &mut cx,
5737 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5738 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5739 );
5740 // Same thing, right-to-left
5741 test(
5742 &mut cx,
5743 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5744 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5745 );
5746
5747 // Whole buffer, left-to-right, last line *doesn't* end with newline
5748 test(
5749 &mut cx,
5750 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5751 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5752 );
5753 // Same thing, right-to-left
5754 test(
5755 &mut cx,
5756 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5757 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5758 );
5759
5760 // Whole buffer, left-to-right, last line ends with newline
5761 test(
5762 &mut cx,
5763 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5764 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5765 );
5766 // Same thing, right-to-left
5767 test(
5768 &mut cx,
5769 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5770 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5771 );
5772
5773 // Starts at the end of a line, ends at the start of another
5774 test(
5775 &mut cx,
5776 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5777 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5778 );
5779}
5780
5781#[gpui::test]
5782async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5783 init_test(cx, |_| {});
5784
5785 let editor = cx.add_window(|window, cx| {
5786 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5787 build_editor(buffer, window, cx)
5788 });
5789
5790 // setup
5791 _ = editor.update(cx, |editor, window, cx| {
5792 editor.fold_creases(
5793 vec![
5794 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5795 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5796 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5797 ],
5798 true,
5799 window,
5800 cx,
5801 );
5802 assert_eq!(
5803 editor.display_text(cx),
5804 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5805 );
5806 });
5807
5808 _ = editor.update(cx, |editor, window, cx| {
5809 editor.change_selections(None, window, cx, |s| {
5810 s.select_display_ranges([
5811 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5812 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5813 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5814 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5815 ])
5816 });
5817 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5818 assert_eq!(
5819 editor.display_text(cx),
5820 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5821 );
5822 });
5823 EditorTestContext::for_editor(editor, cx)
5824 .await
5825 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5826
5827 _ = editor.update(cx, |editor, window, cx| {
5828 editor.change_selections(None, window, cx, |s| {
5829 s.select_display_ranges([
5830 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5831 ])
5832 });
5833 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5834 assert_eq!(
5835 editor.display_text(cx),
5836 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5837 );
5838 assert_eq!(
5839 editor.selections.display_ranges(cx),
5840 [
5841 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5842 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5843 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5844 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5845 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5846 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5847 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5848 ]
5849 );
5850 });
5851 EditorTestContext::for_editor(editor, cx)
5852 .await
5853 .assert_editor_state(
5854 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5855 );
5856}
5857
5858#[gpui::test]
5859async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5860 init_test(cx, |_| {});
5861
5862 let mut cx = EditorTestContext::new(cx).await;
5863
5864 cx.set_state(indoc!(
5865 r#"abc
5866 defˇghi
5867
5868 jk
5869 nlmo
5870 "#
5871 ));
5872
5873 cx.update_editor(|editor, window, cx| {
5874 editor.add_selection_above(&Default::default(), window, cx);
5875 });
5876
5877 cx.assert_editor_state(indoc!(
5878 r#"abcˇ
5879 defˇghi
5880
5881 jk
5882 nlmo
5883 "#
5884 ));
5885
5886 cx.update_editor(|editor, window, cx| {
5887 editor.add_selection_above(&Default::default(), window, cx);
5888 });
5889
5890 cx.assert_editor_state(indoc!(
5891 r#"abcˇ
5892 defˇghi
5893
5894 jk
5895 nlmo
5896 "#
5897 ));
5898
5899 cx.update_editor(|editor, window, cx| {
5900 editor.add_selection_below(&Default::default(), window, cx);
5901 });
5902
5903 cx.assert_editor_state(indoc!(
5904 r#"abc
5905 defˇghi
5906
5907 jk
5908 nlmo
5909 "#
5910 ));
5911
5912 cx.update_editor(|editor, window, cx| {
5913 editor.undo_selection(&Default::default(), window, cx);
5914 });
5915
5916 cx.assert_editor_state(indoc!(
5917 r#"abcˇ
5918 defˇghi
5919
5920 jk
5921 nlmo
5922 "#
5923 ));
5924
5925 cx.update_editor(|editor, window, cx| {
5926 editor.redo_selection(&Default::default(), window, cx);
5927 });
5928
5929 cx.assert_editor_state(indoc!(
5930 r#"abc
5931 defˇghi
5932
5933 jk
5934 nlmo
5935 "#
5936 ));
5937
5938 cx.update_editor(|editor, window, cx| {
5939 editor.add_selection_below(&Default::default(), window, cx);
5940 });
5941
5942 cx.assert_editor_state(indoc!(
5943 r#"abc
5944 defˇghi
5945
5946 jk
5947 nlmˇo
5948 "#
5949 ));
5950
5951 cx.update_editor(|editor, window, cx| {
5952 editor.add_selection_below(&Default::default(), window, cx);
5953 });
5954
5955 cx.assert_editor_state(indoc!(
5956 r#"abc
5957 defˇghi
5958
5959 jk
5960 nlmˇo
5961 "#
5962 ));
5963
5964 // change selections
5965 cx.set_state(indoc!(
5966 r#"abc
5967 def«ˇg»hi
5968
5969 jk
5970 nlmo
5971 "#
5972 ));
5973
5974 cx.update_editor(|editor, window, cx| {
5975 editor.add_selection_below(&Default::default(), window, cx);
5976 });
5977
5978 cx.assert_editor_state(indoc!(
5979 r#"abc
5980 def«ˇg»hi
5981
5982 jk
5983 nlm«ˇo»
5984 "#
5985 ));
5986
5987 cx.update_editor(|editor, window, cx| {
5988 editor.add_selection_below(&Default::default(), window, cx);
5989 });
5990
5991 cx.assert_editor_state(indoc!(
5992 r#"abc
5993 def«ˇg»hi
5994
5995 jk
5996 nlm«ˇo»
5997 "#
5998 ));
5999
6000 cx.update_editor(|editor, window, cx| {
6001 editor.add_selection_above(&Default::default(), window, cx);
6002 });
6003
6004 cx.assert_editor_state(indoc!(
6005 r#"abc
6006 def«ˇg»hi
6007
6008 jk
6009 nlmo
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#"abc
6019 def«ˇg»hi
6020
6021 jk
6022 nlmo
6023 "#
6024 ));
6025
6026 // Change selections again
6027 cx.set_state(indoc!(
6028 r#"a«bc
6029 defgˇ»hi
6030
6031 jk
6032 nlmo
6033 "#
6034 ));
6035
6036 cx.update_editor(|editor, window, cx| {
6037 editor.add_selection_below(&Default::default(), window, cx);
6038 });
6039
6040 cx.assert_editor_state(indoc!(
6041 r#"a«bcˇ»
6042 d«efgˇ»hi
6043
6044 j«kˇ»
6045 nlmo
6046 "#
6047 ));
6048
6049 cx.update_editor(|editor, window, cx| {
6050 editor.add_selection_below(&Default::default(), window, cx);
6051 });
6052 cx.assert_editor_state(indoc!(
6053 r#"a«bcˇ»
6054 d«efgˇ»hi
6055
6056 j«kˇ»
6057 n«lmoˇ»
6058 "#
6059 ));
6060 cx.update_editor(|editor, window, cx| {
6061 editor.add_selection_above(&Default::default(), window, cx);
6062 });
6063
6064 cx.assert_editor_state(indoc!(
6065 r#"a«bcˇ»
6066 d«efgˇ»hi
6067
6068 j«kˇ»
6069 nlmo
6070 "#
6071 ));
6072
6073 // Change selections again
6074 cx.set_state(indoc!(
6075 r#"abc
6076 d«ˇefghi
6077
6078 jk
6079 nlm»o
6080 "#
6081 ));
6082
6083 cx.update_editor(|editor, window, cx| {
6084 editor.add_selection_above(&Default::default(), window, cx);
6085 });
6086
6087 cx.assert_editor_state(indoc!(
6088 r#"a«ˇbc»
6089 d«ˇef»ghi
6090
6091 j«ˇk»
6092 n«ˇlm»o
6093 "#
6094 ));
6095
6096 cx.update_editor(|editor, window, cx| {
6097 editor.add_selection_below(&Default::default(), window, cx);
6098 });
6099
6100 cx.assert_editor_state(indoc!(
6101 r#"abc
6102 d«ˇef»ghi
6103
6104 j«ˇk»
6105 n«ˇlm»o
6106 "#
6107 ));
6108}
6109
6110#[gpui::test]
6111async fn test_select_next(cx: &mut TestAppContext) {
6112 init_test(cx, |_| {});
6113
6114 let mut cx = EditorTestContext::new(cx).await;
6115 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6116
6117 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6118 .unwrap();
6119 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6120
6121 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6122 .unwrap();
6123 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6124
6125 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6126 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6127
6128 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6129 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6130
6131 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6132 .unwrap();
6133 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6134
6135 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6136 .unwrap();
6137 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6138
6139 // Test selection direction should be preserved
6140 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6141
6142 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6143 .unwrap();
6144 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6145}
6146
6147#[gpui::test]
6148async fn test_select_all_matches(cx: &mut TestAppContext) {
6149 init_test(cx, |_| {});
6150
6151 let mut cx = EditorTestContext::new(cx).await;
6152
6153 // Test caret-only selections
6154 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6155 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6156 .unwrap();
6157 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6158
6159 // Test left-to-right selections
6160 cx.set_state("abc\n«abcˇ»\nabc");
6161 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6162 .unwrap();
6163 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6164
6165 // Test right-to-left selections
6166 cx.set_state("abc\n«ˇabc»\nabc");
6167 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6168 .unwrap();
6169 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6170
6171 // Test selecting whitespace with caret selection
6172 cx.set_state("abc\nˇ abc\nabc");
6173 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6174 .unwrap();
6175 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6176
6177 // Test selecting whitespace with left-to-right selection
6178 cx.set_state("abc\n«ˇ »abc\nabc");
6179 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6180 .unwrap();
6181 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6182
6183 // Test no matches with right-to-left selection
6184 cx.set_state("abc\n« ˇ»abc\nabc");
6185 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6186 .unwrap();
6187 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6188}
6189
6190#[gpui::test]
6191async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6192 init_test(cx, |_| {});
6193
6194 let mut cx = EditorTestContext::new(cx).await;
6195
6196 let large_body_1 = "\nd".repeat(200);
6197 let large_body_2 = "\ne".repeat(200);
6198
6199 cx.set_state(&format!(
6200 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6201 ));
6202 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6203 let scroll_position = editor.scroll_position(cx);
6204 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6205 scroll_position
6206 });
6207
6208 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6209 .unwrap();
6210 cx.assert_editor_state(&format!(
6211 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6212 ));
6213 let scroll_position_after_selection =
6214 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6215 assert_eq!(
6216 initial_scroll_position, scroll_position_after_selection,
6217 "Scroll position should not change after selecting all matches"
6218 );
6219}
6220
6221#[gpui::test]
6222async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6223 init_test(cx, |_| {});
6224
6225 let mut cx = EditorLspTestContext::new_rust(
6226 lsp::ServerCapabilities {
6227 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6228 ..Default::default()
6229 },
6230 cx,
6231 )
6232 .await;
6233
6234 cx.set_state(indoc! {"
6235 line 1
6236 line 2
6237 linˇe 3
6238 line 4
6239 line 5
6240 "});
6241
6242 // Make an edit
6243 cx.update_editor(|editor, window, cx| {
6244 editor.handle_input("X", window, cx);
6245 });
6246
6247 // Move cursor to a different position
6248 cx.update_editor(|editor, window, cx| {
6249 editor.change_selections(None, window, cx, |s| {
6250 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6251 });
6252 });
6253
6254 cx.assert_editor_state(indoc! {"
6255 line 1
6256 line 2
6257 linXe 3
6258 line 4
6259 liˇne 5
6260 "});
6261
6262 cx.lsp
6263 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6264 Ok(Some(vec![lsp::TextEdit::new(
6265 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6266 "PREFIX ".to_string(),
6267 )]))
6268 });
6269
6270 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6271 .unwrap()
6272 .await
6273 .unwrap();
6274
6275 cx.assert_editor_state(indoc! {"
6276 PREFIX line 1
6277 line 2
6278 linXe 3
6279 line 4
6280 liˇne 5
6281 "});
6282
6283 // Undo formatting
6284 cx.update_editor(|editor, window, cx| {
6285 editor.undo(&Default::default(), window, cx);
6286 });
6287
6288 // Verify cursor moved back to position after edit
6289 cx.assert_editor_state(indoc! {"
6290 line 1
6291 line 2
6292 linXˇe 3
6293 line 4
6294 line 5
6295 "});
6296}
6297
6298#[gpui::test]
6299async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6300 init_test(cx, |_| {});
6301
6302 let mut cx = EditorTestContext::new(cx).await;
6303 cx.set_state(
6304 r#"let foo = 2;
6305lˇet foo = 2;
6306let fooˇ = 2;
6307let foo = 2;
6308let foo = ˇ2;"#,
6309 );
6310
6311 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6312 .unwrap();
6313 cx.assert_editor_state(
6314 r#"let foo = 2;
6315«letˇ» foo = 2;
6316let «fooˇ» = 2;
6317let foo = 2;
6318let foo = «2ˇ»;"#,
6319 );
6320
6321 // noop for multiple selections with different contents
6322 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6323 .unwrap();
6324 cx.assert_editor_state(
6325 r#"let foo = 2;
6326«letˇ» foo = 2;
6327let «fooˇ» = 2;
6328let foo = 2;
6329let foo = «2ˇ»;"#,
6330 );
6331
6332 // Test last selection direction should be preserved
6333 cx.set_state(
6334 r#"let foo = 2;
6335let foo = 2;
6336let «fooˇ» = 2;
6337let «ˇfoo» = 2;
6338let foo = 2;"#,
6339 );
6340
6341 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6342 .unwrap();
6343 cx.assert_editor_state(
6344 r#"let foo = 2;
6345let foo = 2;
6346let «fooˇ» = 2;
6347let «ˇfoo» = 2;
6348let «ˇfoo» = 2;"#,
6349 );
6350}
6351
6352#[gpui::test]
6353async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6354 init_test(cx, |_| {});
6355
6356 let mut cx =
6357 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6358
6359 cx.assert_editor_state(indoc! {"
6360 ˇbbb
6361 ccc
6362
6363 bbb
6364 ccc
6365 "});
6366 cx.dispatch_action(SelectPrevious::default());
6367 cx.assert_editor_state(indoc! {"
6368 «bbbˇ»
6369 ccc
6370
6371 bbb
6372 ccc
6373 "});
6374 cx.dispatch_action(SelectPrevious::default());
6375 cx.assert_editor_state(indoc! {"
6376 «bbbˇ»
6377 ccc
6378
6379 «bbbˇ»
6380 ccc
6381 "});
6382}
6383
6384#[gpui::test]
6385async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6386 init_test(cx, |_| {});
6387
6388 let mut cx = EditorTestContext::new(cx).await;
6389 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6390
6391 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6392 .unwrap();
6393 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6394
6395 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6396 .unwrap();
6397 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6398
6399 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6400 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6401
6402 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6403 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6404
6405 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6406 .unwrap();
6407 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6408
6409 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6410 .unwrap();
6411 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6412}
6413
6414#[gpui::test]
6415async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6416 init_test(cx, |_| {});
6417
6418 let mut cx = EditorTestContext::new(cx).await;
6419 cx.set_state("aˇ");
6420
6421 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6422 .unwrap();
6423 cx.assert_editor_state("«aˇ»");
6424 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6425 .unwrap();
6426 cx.assert_editor_state("«aˇ»");
6427}
6428
6429#[gpui::test]
6430async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6431 init_test(cx, |_| {});
6432
6433 let mut cx = EditorTestContext::new(cx).await;
6434 cx.set_state(
6435 r#"let foo = 2;
6436lˇet foo = 2;
6437let fooˇ = 2;
6438let foo = 2;
6439let foo = ˇ2;"#,
6440 );
6441
6442 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6443 .unwrap();
6444 cx.assert_editor_state(
6445 r#"let foo = 2;
6446«letˇ» foo = 2;
6447let «fooˇ» = 2;
6448let foo = 2;
6449let foo = «2ˇ»;"#,
6450 );
6451
6452 // noop for multiple selections with different contents
6453 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6454 .unwrap();
6455 cx.assert_editor_state(
6456 r#"let foo = 2;
6457«letˇ» foo = 2;
6458let «fooˇ» = 2;
6459let foo = 2;
6460let foo = «2ˇ»;"#,
6461 );
6462}
6463
6464#[gpui::test]
6465async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6466 init_test(cx, |_| {});
6467
6468 let mut cx = EditorTestContext::new(cx).await;
6469 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6470
6471 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6472 .unwrap();
6473 // selection direction is preserved
6474 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6475
6476 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6477 .unwrap();
6478 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6479
6480 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6481 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6482
6483 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6484 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6485
6486 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6487 .unwrap();
6488 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6489
6490 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6491 .unwrap();
6492 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6493}
6494
6495#[gpui::test]
6496async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6497 init_test(cx, |_| {});
6498
6499 let language = Arc::new(Language::new(
6500 LanguageConfig::default(),
6501 Some(tree_sitter_rust::LANGUAGE.into()),
6502 ));
6503
6504 let text = r#"
6505 use mod1::mod2::{mod3, mod4};
6506
6507 fn fn_1(param1: bool, param2: &str) {
6508 let var1 = "text";
6509 }
6510 "#
6511 .unindent();
6512
6513 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6514 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6515 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6516
6517 editor
6518 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6519 .await;
6520
6521 editor.update_in(cx, |editor, window, cx| {
6522 editor.change_selections(None, window, cx, |s| {
6523 s.select_display_ranges([
6524 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6525 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6526 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6527 ]);
6528 });
6529 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6530 });
6531 editor.update(cx, |editor, cx| {
6532 assert_text_with_selections(
6533 editor,
6534 indoc! {r#"
6535 use mod1::mod2::{mod3, «mod4ˇ»};
6536
6537 fn fn_1«ˇ(param1: bool, param2: &str)» {
6538 let var1 = "«ˇtext»";
6539 }
6540 "#},
6541 cx,
6542 );
6543 });
6544
6545 editor.update_in(cx, |editor, window, cx| {
6546 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6547 });
6548 editor.update(cx, |editor, cx| {
6549 assert_text_with_selections(
6550 editor,
6551 indoc! {r#"
6552 use mod1::mod2::«{mod3, mod4}ˇ»;
6553
6554 «ˇfn fn_1(param1: bool, param2: &str) {
6555 let var1 = "text";
6556 }»
6557 "#},
6558 cx,
6559 );
6560 });
6561
6562 editor.update_in(cx, |editor, window, cx| {
6563 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6564 });
6565 assert_eq!(
6566 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6567 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6568 );
6569
6570 // Trying to expand the selected syntax node one more time has no effect.
6571 editor.update_in(cx, |editor, window, cx| {
6572 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6573 });
6574 assert_eq!(
6575 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6576 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6577 );
6578
6579 editor.update_in(cx, |editor, window, cx| {
6580 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6581 });
6582 editor.update(cx, |editor, cx| {
6583 assert_text_with_selections(
6584 editor,
6585 indoc! {r#"
6586 use mod1::mod2::«{mod3, mod4}ˇ»;
6587
6588 «ˇfn fn_1(param1: bool, param2: &str) {
6589 let var1 = "text";
6590 }»
6591 "#},
6592 cx,
6593 );
6594 });
6595
6596 editor.update_in(cx, |editor, window, cx| {
6597 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6598 });
6599 editor.update(cx, |editor, cx| {
6600 assert_text_with_selections(
6601 editor,
6602 indoc! {r#"
6603 use mod1::mod2::{mod3, «mod4ˇ»};
6604
6605 fn fn_1«ˇ(param1: bool, param2: &str)» {
6606 let var1 = "«ˇtext»";
6607 }
6608 "#},
6609 cx,
6610 );
6611 });
6612
6613 editor.update_in(cx, |editor, window, cx| {
6614 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6615 });
6616 editor.update(cx, |editor, cx| {
6617 assert_text_with_selections(
6618 editor,
6619 indoc! {r#"
6620 use mod1::mod2::{mod3, mo«ˇ»d4};
6621
6622 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6623 let var1 = "te«ˇ»xt";
6624 }
6625 "#},
6626 cx,
6627 );
6628 });
6629
6630 // Trying to shrink the selected syntax node one more time has no effect.
6631 editor.update_in(cx, |editor, window, cx| {
6632 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6633 });
6634 editor.update_in(cx, |editor, _, cx| {
6635 assert_text_with_selections(
6636 editor,
6637 indoc! {r#"
6638 use mod1::mod2::{mod3, mo«ˇ»d4};
6639
6640 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6641 let var1 = "te«ˇ»xt";
6642 }
6643 "#},
6644 cx,
6645 );
6646 });
6647
6648 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6649 // a fold.
6650 editor.update_in(cx, |editor, window, cx| {
6651 editor.fold_creases(
6652 vec![
6653 Crease::simple(
6654 Point::new(0, 21)..Point::new(0, 24),
6655 FoldPlaceholder::test(),
6656 ),
6657 Crease::simple(
6658 Point::new(3, 20)..Point::new(3, 22),
6659 FoldPlaceholder::test(),
6660 ),
6661 ],
6662 true,
6663 window,
6664 cx,
6665 );
6666 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6667 });
6668 editor.update(cx, |editor, cx| {
6669 assert_text_with_selections(
6670 editor,
6671 indoc! {r#"
6672 use mod1::mod2::«{mod3, mod4}ˇ»;
6673
6674 fn fn_1«ˇ(param1: bool, param2: &str)» {
6675 let var1 = "«ˇtext»";
6676 }
6677 "#},
6678 cx,
6679 );
6680 });
6681}
6682
6683#[gpui::test]
6684async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6685 init_test(cx, |_| {});
6686
6687 let language = Arc::new(Language::new(
6688 LanguageConfig::default(),
6689 Some(tree_sitter_rust::LANGUAGE.into()),
6690 ));
6691
6692 let text = "let a = 2;";
6693
6694 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6695 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6696 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6697
6698 editor
6699 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6700 .await;
6701
6702 // Test case 1: Cursor at end of word
6703 editor.update_in(cx, |editor, window, cx| {
6704 editor.change_selections(None, window, cx, |s| {
6705 s.select_display_ranges([
6706 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6707 ]);
6708 });
6709 });
6710 editor.update(cx, |editor, cx| {
6711 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6712 });
6713 editor.update_in(cx, |editor, window, cx| {
6714 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6715 });
6716 editor.update(cx, |editor, cx| {
6717 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6718 });
6719 editor.update_in(cx, |editor, window, cx| {
6720 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6721 });
6722 editor.update(cx, |editor, cx| {
6723 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6724 });
6725
6726 // Test case 2: Cursor at end of statement
6727 editor.update_in(cx, |editor, window, cx| {
6728 editor.change_selections(None, window, cx, |s| {
6729 s.select_display_ranges([
6730 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6731 ]);
6732 });
6733 });
6734 editor.update(cx, |editor, cx| {
6735 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6736 });
6737 editor.update_in(cx, |editor, window, cx| {
6738 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6739 });
6740 editor.update(cx, |editor, cx| {
6741 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6742 });
6743}
6744
6745#[gpui::test]
6746async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6747 init_test(cx, |_| {});
6748
6749 let language = Arc::new(Language::new(
6750 LanguageConfig::default(),
6751 Some(tree_sitter_rust::LANGUAGE.into()),
6752 ));
6753
6754 let text = r#"
6755 use mod1::mod2::{mod3, mod4};
6756
6757 fn fn_1(param1: bool, param2: &str) {
6758 let var1 = "hello world";
6759 }
6760 "#
6761 .unindent();
6762
6763 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6764 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6765 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6766
6767 editor
6768 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6769 .await;
6770
6771 // Test 1: Cursor on a letter of a string word
6772 editor.update_in(cx, |editor, window, cx| {
6773 editor.change_selections(None, window, cx, |s| {
6774 s.select_display_ranges([
6775 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6776 ]);
6777 });
6778 });
6779 editor.update_in(cx, |editor, window, cx| {
6780 assert_text_with_selections(
6781 editor,
6782 indoc! {r#"
6783 use mod1::mod2::{mod3, mod4};
6784
6785 fn fn_1(param1: bool, param2: &str) {
6786 let var1 = "hˇello world";
6787 }
6788 "#},
6789 cx,
6790 );
6791 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6792 assert_text_with_selections(
6793 editor,
6794 indoc! {r#"
6795 use mod1::mod2::{mod3, mod4};
6796
6797 fn fn_1(param1: bool, param2: &str) {
6798 let var1 = "«ˇhello» world";
6799 }
6800 "#},
6801 cx,
6802 );
6803 });
6804
6805 // Test 2: Partial selection within a word
6806 editor.update_in(cx, |editor, window, cx| {
6807 editor.change_selections(None, window, cx, |s| {
6808 s.select_display_ranges([
6809 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6810 ]);
6811 });
6812 });
6813 editor.update_in(cx, |editor, window, cx| {
6814 assert_text_with_selections(
6815 editor,
6816 indoc! {r#"
6817 use mod1::mod2::{mod3, mod4};
6818
6819 fn fn_1(param1: bool, param2: &str) {
6820 let var1 = "h«elˇ»lo world";
6821 }
6822 "#},
6823 cx,
6824 );
6825 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6826 assert_text_with_selections(
6827 editor,
6828 indoc! {r#"
6829 use mod1::mod2::{mod3, mod4};
6830
6831 fn fn_1(param1: bool, param2: &str) {
6832 let var1 = "«ˇhello» world";
6833 }
6834 "#},
6835 cx,
6836 );
6837 });
6838
6839 // Test 3: Complete word already selected
6840 editor.update_in(cx, |editor, window, cx| {
6841 editor.change_selections(None, window, cx, |s| {
6842 s.select_display_ranges([
6843 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6844 ]);
6845 });
6846 });
6847 editor.update_in(cx, |editor, window, cx| {
6848 assert_text_with_selections(
6849 editor,
6850 indoc! {r#"
6851 use mod1::mod2::{mod3, mod4};
6852
6853 fn fn_1(param1: bool, param2: &str) {
6854 let var1 = "«helloˇ» world";
6855 }
6856 "#},
6857 cx,
6858 );
6859 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6860 assert_text_with_selections(
6861 editor,
6862 indoc! {r#"
6863 use mod1::mod2::{mod3, mod4};
6864
6865 fn fn_1(param1: bool, param2: &str) {
6866 let var1 = "«hello worldˇ»";
6867 }
6868 "#},
6869 cx,
6870 );
6871 });
6872
6873 // Test 4: Selection spanning across words
6874 editor.update_in(cx, |editor, window, cx| {
6875 editor.change_selections(None, window, cx, |s| {
6876 s.select_display_ranges([
6877 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6878 ]);
6879 });
6880 });
6881 editor.update_in(cx, |editor, window, cx| {
6882 assert_text_with_selections(
6883 editor,
6884 indoc! {r#"
6885 use mod1::mod2::{mod3, mod4};
6886
6887 fn fn_1(param1: bool, param2: &str) {
6888 let var1 = "hel«lo woˇ»rld";
6889 }
6890 "#},
6891 cx,
6892 );
6893 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6894 assert_text_with_selections(
6895 editor,
6896 indoc! {r#"
6897 use mod1::mod2::{mod3, mod4};
6898
6899 fn fn_1(param1: bool, param2: &str) {
6900 let var1 = "«ˇhello world»";
6901 }
6902 "#},
6903 cx,
6904 );
6905 });
6906
6907 // Test 5: Expansion beyond string
6908 editor.update_in(cx, |editor, window, cx| {
6909 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6910 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6911 assert_text_with_selections(
6912 editor,
6913 indoc! {r#"
6914 use mod1::mod2::{mod3, mod4};
6915
6916 fn fn_1(param1: bool, param2: &str) {
6917 «ˇlet var1 = "hello world";»
6918 }
6919 "#},
6920 cx,
6921 );
6922 });
6923}
6924
6925#[gpui::test]
6926async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6927 init_test(cx, |_| {});
6928
6929 let base_text = r#"
6930 impl A {
6931 // this is an uncommitted comment
6932
6933 fn b() {
6934 c();
6935 }
6936
6937 // this is another uncommitted comment
6938
6939 fn d() {
6940 // e
6941 // f
6942 }
6943 }
6944
6945 fn g() {
6946 // h
6947 }
6948 "#
6949 .unindent();
6950
6951 let text = r#"
6952 ˇimpl A {
6953
6954 fn b() {
6955 c();
6956 }
6957
6958 fn d() {
6959 // e
6960 // f
6961 }
6962 }
6963
6964 fn g() {
6965 // h
6966 }
6967 "#
6968 .unindent();
6969
6970 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6971 cx.set_state(&text);
6972 cx.set_head_text(&base_text);
6973 cx.update_editor(|editor, window, cx| {
6974 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6975 });
6976
6977 cx.assert_state_with_diff(
6978 "
6979 ˇimpl A {
6980 - // this is an uncommitted comment
6981
6982 fn b() {
6983 c();
6984 }
6985
6986 - // this is another uncommitted comment
6987 -
6988 fn d() {
6989 // e
6990 // f
6991 }
6992 }
6993
6994 fn g() {
6995 // h
6996 }
6997 "
6998 .unindent(),
6999 );
7000
7001 let expected_display_text = "
7002 impl A {
7003 // this is an uncommitted comment
7004
7005 fn b() {
7006 ⋯
7007 }
7008
7009 // this is another uncommitted comment
7010
7011 fn d() {
7012 ⋯
7013 }
7014 }
7015
7016 fn g() {
7017 ⋯
7018 }
7019 "
7020 .unindent();
7021
7022 cx.update_editor(|editor, window, cx| {
7023 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7024 assert_eq!(editor.display_text(cx), expected_display_text);
7025 });
7026}
7027
7028#[gpui::test]
7029async fn test_autoindent(cx: &mut TestAppContext) {
7030 init_test(cx, |_| {});
7031
7032 let language = Arc::new(
7033 Language::new(
7034 LanguageConfig {
7035 brackets: BracketPairConfig {
7036 pairs: vec![
7037 BracketPair {
7038 start: "{".to_string(),
7039 end: "}".to_string(),
7040 close: false,
7041 surround: false,
7042 newline: true,
7043 },
7044 BracketPair {
7045 start: "(".to_string(),
7046 end: ")".to_string(),
7047 close: false,
7048 surround: false,
7049 newline: true,
7050 },
7051 ],
7052 ..Default::default()
7053 },
7054 ..Default::default()
7055 },
7056 Some(tree_sitter_rust::LANGUAGE.into()),
7057 )
7058 .with_indents_query(
7059 r#"
7060 (_ "(" ")" @end) @indent
7061 (_ "{" "}" @end) @indent
7062 "#,
7063 )
7064 .unwrap(),
7065 );
7066
7067 let text = "fn a() {}";
7068
7069 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7070 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7071 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7072 editor
7073 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7074 .await;
7075
7076 editor.update_in(cx, |editor, window, cx| {
7077 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7078 editor.newline(&Newline, window, cx);
7079 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7080 assert_eq!(
7081 editor.selections.ranges(cx),
7082 &[
7083 Point::new(1, 4)..Point::new(1, 4),
7084 Point::new(3, 4)..Point::new(3, 4),
7085 Point::new(5, 0)..Point::new(5, 0)
7086 ]
7087 );
7088 });
7089}
7090
7091#[gpui::test]
7092async fn test_autoindent_selections(cx: &mut TestAppContext) {
7093 init_test(cx, |_| {});
7094
7095 {
7096 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7097 cx.set_state(indoc! {"
7098 impl A {
7099
7100 fn b() {}
7101
7102 «fn c() {
7103
7104 }ˇ»
7105 }
7106 "});
7107
7108 cx.update_editor(|editor, window, cx| {
7109 editor.autoindent(&Default::default(), window, cx);
7110 });
7111
7112 cx.assert_editor_state(indoc! {"
7113 impl A {
7114
7115 fn b() {}
7116
7117 «fn c() {
7118
7119 }ˇ»
7120 }
7121 "});
7122 }
7123
7124 {
7125 let mut cx = EditorTestContext::new_multibuffer(
7126 cx,
7127 [indoc! { "
7128 impl A {
7129 «
7130 // a
7131 fn b(){}
7132 »
7133 «
7134 }
7135 fn c(){}
7136 »
7137 "}],
7138 );
7139
7140 let buffer = cx.update_editor(|editor, _, cx| {
7141 let buffer = editor.buffer().update(cx, |buffer, _| {
7142 buffer.all_buffers().iter().next().unwrap().clone()
7143 });
7144 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7145 buffer
7146 });
7147
7148 cx.run_until_parked();
7149 cx.update_editor(|editor, window, cx| {
7150 editor.select_all(&Default::default(), window, cx);
7151 editor.autoindent(&Default::default(), window, cx)
7152 });
7153 cx.run_until_parked();
7154
7155 cx.update(|_, cx| {
7156 assert_eq!(
7157 buffer.read(cx).text(),
7158 indoc! { "
7159 impl A {
7160
7161 // a
7162 fn b(){}
7163
7164
7165 }
7166 fn c(){}
7167
7168 " }
7169 )
7170 });
7171 }
7172}
7173
7174#[gpui::test]
7175async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7176 init_test(cx, |_| {});
7177
7178 let mut cx = EditorTestContext::new(cx).await;
7179
7180 let language = Arc::new(Language::new(
7181 LanguageConfig {
7182 brackets: BracketPairConfig {
7183 pairs: vec![
7184 BracketPair {
7185 start: "{".to_string(),
7186 end: "}".to_string(),
7187 close: true,
7188 surround: true,
7189 newline: true,
7190 },
7191 BracketPair {
7192 start: "(".to_string(),
7193 end: ")".to_string(),
7194 close: true,
7195 surround: true,
7196 newline: true,
7197 },
7198 BracketPair {
7199 start: "/*".to_string(),
7200 end: " */".to_string(),
7201 close: true,
7202 surround: true,
7203 newline: true,
7204 },
7205 BracketPair {
7206 start: "[".to_string(),
7207 end: "]".to_string(),
7208 close: false,
7209 surround: false,
7210 newline: true,
7211 },
7212 BracketPair {
7213 start: "\"".to_string(),
7214 end: "\"".to_string(),
7215 close: true,
7216 surround: true,
7217 newline: false,
7218 },
7219 BracketPair {
7220 start: "<".to_string(),
7221 end: ">".to_string(),
7222 close: false,
7223 surround: true,
7224 newline: true,
7225 },
7226 ],
7227 ..Default::default()
7228 },
7229 autoclose_before: "})]".to_string(),
7230 ..Default::default()
7231 },
7232 Some(tree_sitter_rust::LANGUAGE.into()),
7233 ));
7234
7235 cx.language_registry().add(language.clone());
7236 cx.update_buffer(|buffer, cx| {
7237 buffer.set_language(Some(language), cx);
7238 });
7239
7240 cx.set_state(
7241 &r#"
7242 🏀ˇ
7243 εˇ
7244 ❤️ˇ
7245 "#
7246 .unindent(),
7247 );
7248
7249 // autoclose multiple nested brackets at multiple cursors
7250 cx.update_editor(|editor, window, cx| {
7251 editor.handle_input("{", window, cx);
7252 editor.handle_input("{", window, cx);
7253 editor.handle_input("{", window, cx);
7254 });
7255 cx.assert_editor_state(
7256 &"
7257 🏀{{{ˇ}}}
7258 ε{{{ˇ}}}
7259 ❤️{{{ˇ}}}
7260 "
7261 .unindent(),
7262 );
7263
7264 // insert a different closing bracket
7265 cx.update_editor(|editor, window, cx| {
7266 editor.handle_input(")", window, cx);
7267 });
7268 cx.assert_editor_state(
7269 &"
7270 🏀{{{)ˇ}}}
7271 ε{{{)ˇ}}}
7272 ❤️{{{)ˇ}}}
7273 "
7274 .unindent(),
7275 );
7276
7277 // skip over the auto-closed brackets when typing a closing bracket
7278 cx.update_editor(|editor, window, cx| {
7279 editor.move_right(&MoveRight, window, cx);
7280 editor.handle_input("}", window, cx);
7281 editor.handle_input("}", window, cx);
7282 editor.handle_input("}", window, cx);
7283 });
7284 cx.assert_editor_state(
7285 &"
7286 🏀{{{)}}}}ˇ
7287 ε{{{)}}}}ˇ
7288 ❤️{{{)}}}}ˇ
7289 "
7290 .unindent(),
7291 );
7292
7293 // autoclose multi-character pairs
7294 cx.set_state(
7295 &"
7296 ˇ
7297 ˇ
7298 "
7299 .unindent(),
7300 );
7301 cx.update_editor(|editor, window, cx| {
7302 editor.handle_input("/", window, cx);
7303 editor.handle_input("*", window, cx);
7304 });
7305 cx.assert_editor_state(
7306 &"
7307 /*ˇ */
7308 /*ˇ */
7309 "
7310 .unindent(),
7311 );
7312
7313 // one cursor autocloses a multi-character pair, one cursor
7314 // does not autoclose.
7315 cx.set_state(
7316 &"
7317 /ˇ
7318 ˇ
7319 "
7320 .unindent(),
7321 );
7322 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7323 cx.assert_editor_state(
7324 &"
7325 /*ˇ */
7326 *ˇ
7327 "
7328 .unindent(),
7329 );
7330
7331 // Don't autoclose if the next character isn't whitespace and isn't
7332 // listed in the language's "autoclose_before" section.
7333 cx.set_state("ˇa b");
7334 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7335 cx.assert_editor_state("{ˇa b");
7336
7337 // Don't autoclose if `close` is false for the bracket pair
7338 cx.set_state("ˇ");
7339 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7340 cx.assert_editor_state("[ˇ");
7341
7342 // Surround with brackets if text is selected
7343 cx.set_state("«aˇ» b");
7344 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7345 cx.assert_editor_state("{«aˇ»} b");
7346
7347 // Autoclose when not immediately after a word character
7348 cx.set_state("a ˇ");
7349 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7350 cx.assert_editor_state("a \"ˇ\"");
7351
7352 // Autoclose pair where the start and end characters are the same
7353 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7354 cx.assert_editor_state("a \"\"ˇ");
7355
7356 // Don't autoclose when immediately after a word character
7357 cx.set_state("aˇ");
7358 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7359 cx.assert_editor_state("a\"ˇ");
7360
7361 // Do autoclose when after a non-word character
7362 cx.set_state("{ˇ");
7363 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7364 cx.assert_editor_state("{\"ˇ\"");
7365
7366 // Non identical pairs autoclose regardless of preceding character
7367 cx.set_state("aˇ");
7368 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7369 cx.assert_editor_state("a{ˇ}");
7370
7371 // Don't autoclose pair if autoclose is disabled
7372 cx.set_state("ˇ");
7373 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7374 cx.assert_editor_state("<ˇ");
7375
7376 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7377 cx.set_state("«aˇ» b");
7378 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7379 cx.assert_editor_state("<«aˇ»> b");
7380}
7381
7382#[gpui::test]
7383async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7384 init_test(cx, |settings| {
7385 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7386 });
7387
7388 let mut cx = EditorTestContext::new(cx).await;
7389
7390 let language = Arc::new(Language::new(
7391 LanguageConfig {
7392 brackets: BracketPairConfig {
7393 pairs: vec![
7394 BracketPair {
7395 start: "{".to_string(),
7396 end: "}".to_string(),
7397 close: true,
7398 surround: true,
7399 newline: true,
7400 },
7401 BracketPair {
7402 start: "(".to_string(),
7403 end: ")".to_string(),
7404 close: true,
7405 surround: true,
7406 newline: true,
7407 },
7408 BracketPair {
7409 start: "[".to_string(),
7410 end: "]".to_string(),
7411 close: false,
7412 surround: false,
7413 newline: true,
7414 },
7415 ],
7416 ..Default::default()
7417 },
7418 autoclose_before: "})]".to_string(),
7419 ..Default::default()
7420 },
7421 Some(tree_sitter_rust::LANGUAGE.into()),
7422 ));
7423
7424 cx.language_registry().add(language.clone());
7425 cx.update_buffer(|buffer, cx| {
7426 buffer.set_language(Some(language), cx);
7427 });
7428
7429 cx.set_state(
7430 &"
7431 ˇ
7432 ˇ
7433 ˇ
7434 "
7435 .unindent(),
7436 );
7437
7438 // ensure only matching closing brackets are skipped over
7439 cx.update_editor(|editor, window, cx| {
7440 editor.handle_input("}", window, cx);
7441 editor.move_left(&MoveLeft, window, cx);
7442 editor.handle_input(")", window, cx);
7443 editor.move_left(&MoveLeft, window, cx);
7444 });
7445 cx.assert_editor_state(
7446 &"
7447 ˇ)}
7448 ˇ)}
7449 ˇ)}
7450 "
7451 .unindent(),
7452 );
7453
7454 // skip-over closing brackets at multiple cursors
7455 cx.update_editor(|editor, window, cx| {
7456 editor.handle_input(")", window, cx);
7457 editor.handle_input("}", window, cx);
7458 });
7459 cx.assert_editor_state(
7460 &"
7461 )}ˇ
7462 )}ˇ
7463 )}ˇ
7464 "
7465 .unindent(),
7466 );
7467
7468 // ignore non-close brackets
7469 cx.update_editor(|editor, window, cx| {
7470 editor.handle_input("]", window, cx);
7471 editor.move_left(&MoveLeft, window, cx);
7472 editor.handle_input("]", window, cx);
7473 });
7474 cx.assert_editor_state(
7475 &"
7476 )}]ˇ]
7477 )}]ˇ]
7478 )}]ˇ]
7479 "
7480 .unindent(),
7481 );
7482}
7483
7484#[gpui::test]
7485async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7486 init_test(cx, |_| {});
7487
7488 let mut cx = EditorTestContext::new(cx).await;
7489
7490 let html_language = Arc::new(
7491 Language::new(
7492 LanguageConfig {
7493 name: "HTML".into(),
7494 brackets: BracketPairConfig {
7495 pairs: vec![
7496 BracketPair {
7497 start: "<".into(),
7498 end: ">".into(),
7499 close: true,
7500 ..Default::default()
7501 },
7502 BracketPair {
7503 start: "{".into(),
7504 end: "}".into(),
7505 close: true,
7506 ..Default::default()
7507 },
7508 BracketPair {
7509 start: "(".into(),
7510 end: ")".into(),
7511 close: true,
7512 ..Default::default()
7513 },
7514 ],
7515 ..Default::default()
7516 },
7517 autoclose_before: "})]>".into(),
7518 ..Default::default()
7519 },
7520 Some(tree_sitter_html::LANGUAGE.into()),
7521 )
7522 .with_injection_query(
7523 r#"
7524 (script_element
7525 (raw_text) @injection.content
7526 (#set! injection.language "javascript"))
7527 "#,
7528 )
7529 .unwrap(),
7530 );
7531
7532 let javascript_language = Arc::new(Language::new(
7533 LanguageConfig {
7534 name: "JavaScript".into(),
7535 brackets: BracketPairConfig {
7536 pairs: vec![
7537 BracketPair {
7538 start: "/*".into(),
7539 end: " */".into(),
7540 close: true,
7541 ..Default::default()
7542 },
7543 BracketPair {
7544 start: "{".into(),
7545 end: "}".into(),
7546 close: true,
7547 ..Default::default()
7548 },
7549 BracketPair {
7550 start: "(".into(),
7551 end: ")".into(),
7552 close: true,
7553 ..Default::default()
7554 },
7555 ],
7556 ..Default::default()
7557 },
7558 autoclose_before: "})]>".into(),
7559 ..Default::default()
7560 },
7561 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7562 ));
7563
7564 cx.language_registry().add(html_language.clone());
7565 cx.language_registry().add(javascript_language.clone());
7566
7567 cx.update_buffer(|buffer, cx| {
7568 buffer.set_language(Some(html_language), cx);
7569 });
7570
7571 cx.set_state(
7572 &r#"
7573 <body>ˇ
7574 <script>
7575 var x = 1;ˇ
7576 </script>
7577 </body>ˇ
7578 "#
7579 .unindent(),
7580 );
7581
7582 // Precondition: different languages are active at different locations.
7583 cx.update_editor(|editor, window, cx| {
7584 let snapshot = editor.snapshot(window, cx);
7585 let cursors = editor.selections.ranges::<usize>(cx);
7586 let languages = cursors
7587 .iter()
7588 .map(|c| snapshot.language_at(c.start).unwrap().name())
7589 .collect::<Vec<_>>();
7590 assert_eq!(
7591 languages,
7592 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7593 );
7594 });
7595
7596 // Angle brackets autoclose in HTML, but not JavaScript.
7597 cx.update_editor(|editor, window, cx| {
7598 editor.handle_input("<", window, cx);
7599 editor.handle_input("a", window, cx);
7600 });
7601 cx.assert_editor_state(
7602 &r#"
7603 <body><aˇ>
7604 <script>
7605 var x = 1;<aˇ
7606 </script>
7607 </body><aˇ>
7608 "#
7609 .unindent(),
7610 );
7611
7612 // Curly braces and parens autoclose in both HTML and JavaScript.
7613 cx.update_editor(|editor, window, cx| {
7614 editor.handle_input(" b=", window, cx);
7615 editor.handle_input("{", window, cx);
7616 editor.handle_input("c", window, cx);
7617 editor.handle_input("(", window, cx);
7618 });
7619 cx.assert_editor_state(
7620 &r#"
7621 <body><a b={c(ˇ)}>
7622 <script>
7623 var x = 1;<a b={c(ˇ)}
7624 </script>
7625 </body><a b={c(ˇ)}>
7626 "#
7627 .unindent(),
7628 );
7629
7630 // Brackets that were already autoclosed are skipped.
7631 cx.update_editor(|editor, window, cx| {
7632 editor.handle_input(")", window, cx);
7633 editor.handle_input("d", window, cx);
7634 editor.handle_input("}", window, cx);
7635 });
7636 cx.assert_editor_state(
7637 &r#"
7638 <body><a b={c()d}ˇ>
7639 <script>
7640 var x = 1;<a b={c()d}ˇ
7641 </script>
7642 </body><a b={c()d}ˇ>
7643 "#
7644 .unindent(),
7645 );
7646 cx.update_editor(|editor, window, cx| {
7647 editor.handle_input(">", window, cx);
7648 });
7649 cx.assert_editor_state(
7650 &r#"
7651 <body><a b={c()d}>ˇ
7652 <script>
7653 var x = 1;<a b={c()d}>ˇ
7654 </script>
7655 </body><a b={c()d}>ˇ
7656 "#
7657 .unindent(),
7658 );
7659
7660 // Reset
7661 cx.set_state(
7662 &r#"
7663 <body>ˇ
7664 <script>
7665 var x = 1;ˇ
7666 </script>
7667 </body>ˇ
7668 "#
7669 .unindent(),
7670 );
7671
7672 cx.update_editor(|editor, window, cx| {
7673 editor.handle_input("<", window, cx);
7674 });
7675 cx.assert_editor_state(
7676 &r#"
7677 <body><ˇ>
7678 <script>
7679 var x = 1;<ˇ
7680 </script>
7681 </body><ˇ>
7682 "#
7683 .unindent(),
7684 );
7685
7686 // When backspacing, the closing angle brackets are removed.
7687 cx.update_editor(|editor, window, cx| {
7688 editor.backspace(&Backspace, window, cx);
7689 });
7690 cx.assert_editor_state(
7691 &r#"
7692 <body>ˇ
7693 <script>
7694 var x = 1;ˇ
7695 </script>
7696 </body>ˇ
7697 "#
7698 .unindent(),
7699 );
7700
7701 // Block comments autoclose in JavaScript, but not HTML.
7702 cx.update_editor(|editor, window, cx| {
7703 editor.handle_input("/", window, cx);
7704 editor.handle_input("*", window, cx);
7705 });
7706 cx.assert_editor_state(
7707 &r#"
7708 <body>/*ˇ
7709 <script>
7710 var x = 1;/*ˇ */
7711 </script>
7712 </body>/*ˇ
7713 "#
7714 .unindent(),
7715 );
7716}
7717
7718#[gpui::test]
7719async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7720 init_test(cx, |_| {});
7721
7722 let mut cx = EditorTestContext::new(cx).await;
7723
7724 let rust_language = Arc::new(
7725 Language::new(
7726 LanguageConfig {
7727 name: "Rust".into(),
7728 brackets: serde_json::from_value(json!([
7729 { "start": "{", "end": "}", "close": true, "newline": true },
7730 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7731 ]))
7732 .unwrap(),
7733 autoclose_before: "})]>".into(),
7734 ..Default::default()
7735 },
7736 Some(tree_sitter_rust::LANGUAGE.into()),
7737 )
7738 .with_override_query("(string_literal) @string")
7739 .unwrap(),
7740 );
7741
7742 cx.language_registry().add(rust_language.clone());
7743 cx.update_buffer(|buffer, cx| {
7744 buffer.set_language(Some(rust_language), cx);
7745 });
7746
7747 cx.set_state(
7748 &r#"
7749 let x = ˇ
7750 "#
7751 .unindent(),
7752 );
7753
7754 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7755 cx.update_editor(|editor, window, cx| {
7756 editor.handle_input("\"", window, cx);
7757 });
7758 cx.assert_editor_state(
7759 &r#"
7760 let x = "ˇ"
7761 "#
7762 .unindent(),
7763 );
7764
7765 // Inserting another quotation mark. The cursor moves across the existing
7766 // automatically-inserted quotation mark.
7767 cx.update_editor(|editor, window, cx| {
7768 editor.handle_input("\"", window, cx);
7769 });
7770 cx.assert_editor_state(
7771 &r#"
7772 let x = ""ˇ
7773 "#
7774 .unindent(),
7775 );
7776
7777 // Reset
7778 cx.set_state(
7779 &r#"
7780 let x = ˇ
7781 "#
7782 .unindent(),
7783 );
7784
7785 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7786 cx.update_editor(|editor, window, cx| {
7787 editor.handle_input("\"", window, cx);
7788 editor.handle_input(" ", window, cx);
7789 editor.move_left(&Default::default(), window, cx);
7790 editor.handle_input("\\", window, cx);
7791 editor.handle_input("\"", window, cx);
7792 });
7793 cx.assert_editor_state(
7794 &r#"
7795 let x = "\"ˇ "
7796 "#
7797 .unindent(),
7798 );
7799
7800 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7801 // mark. Nothing is inserted.
7802 cx.update_editor(|editor, window, cx| {
7803 editor.move_right(&Default::default(), window, cx);
7804 editor.handle_input("\"", window, cx);
7805 });
7806 cx.assert_editor_state(
7807 &r#"
7808 let x = "\" "ˇ
7809 "#
7810 .unindent(),
7811 );
7812}
7813
7814#[gpui::test]
7815async fn test_surround_with_pair(cx: &mut TestAppContext) {
7816 init_test(cx, |_| {});
7817
7818 let language = Arc::new(Language::new(
7819 LanguageConfig {
7820 brackets: BracketPairConfig {
7821 pairs: vec![
7822 BracketPair {
7823 start: "{".to_string(),
7824 end: "}".to_string(),
7825 close: true,
7826 surround: true,
7827 newline: true,
7828 },
7829 BracketPair {
7830 start: "/* ".to_string(),
7831 end: "*/".to_string(),
7832 close: true,
7833 surround: true,
7834 ..Default::default()
7835 },
7836 ],
7837 ..Default::default()
7838 },
7839 ..Default::default()
7840 },
7841 Some(tree_sitter_rust::LANGUAGE.into()),
7842 ));
7843
7844 let text = r#"
7845 a
7846 b
7847 c
7848 "#
7849 .unindent();
7850
7851 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7852 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7853 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7854 editor
7855 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7856 .await;
7857
7858 editor.update_in(cx, |editor, window, cx| {
7859 editor.change_selections(None, window, cx, |s| {
7860 s.select_display_ranges([
7861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7862 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7863 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7864 ])
7865 });
7866
7867 editor.handle_input("{", window, cx);
7868 editor.handle_input("{", window, cx);
7869 editor.handle_input("{", window, cx);
7870 assert_eq!(
7871 editor.text(cx),
7872 "
7873 {{{a}}}
7874 {{{b}}}
7875 {{{c}}}
7876 "
7877 .unindent()
7878 );
7879 assert_eq!(
7880 editor.selections.display_ranges(cx),
7881 [
7882 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7883 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7884 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7885 ]
7886 );
7887
7888 editor.undo(&Undo, window, cx);
7889 editor.undo(&Undo, window, cx);
7890 editor.undo(&Undo, window, cx);
7891 assert_eq!(
7892 editor.text(cx),
7893 "
7894 a
7895 b
7896 c
7897 "
7898 .unindent()
7899 );
7900 assert_eq!(
7901 editor.selections.display_ranges(cx),
7902 [
7903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7904 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7905 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7906 ]
7907 );
7908
7909 // Ensure inserting the first character of a multi-byte bracket pair
7910 // doesn't surround the selections with the bracket.
7911 editor.handle_input("/", window, cx);
7912 assert_eq!(
7913 editor.text(cx),
7914 "
7915 /
7916 /
7917 /
7918 "
7919 .unindent()
7920 );
7921 assert_eq!(
7922 editor.selections.display_ranges(cx),
7923 [
7924 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7925 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7926 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7927 ]
7928 );
7929
7930 editor.undo(&Undo, window, cx);
7931 assert_eq!(
7932 editor.text(cx),
7933 "
7934 a
7935 b
7936 c
7937 "
7938 .unindent()
7939 );
7940 assert_eq!(
7941 editor.selections.display_ranges(cx),
7942 [
7943 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7944 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7945 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7946 ]
7947 );
7948
7949 // Ensure inserting the last character of a multi-byte bracket pair
7950 // doesn't surround the selections with the bracket.
7951 editor.handle_input("*", window, cx);
7952 assert_eq!(
7953 editor.text(cx),
7954 "
7955 *
7956 *
7957 *
7958 "
7959 .unindent()
7960 );
7961 assert_eq!(
7962 editor.selections.display_ranges(cx),
7963 [
7964 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7965 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7966 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7967 ]
7968 );
7969 });
7970}
7971
7972#[gpui::test]
7973async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7974 init_test(cx, |_| {});
7975
7976 let language = Arc::new(Language::new(
7977 LanguageConfig {
7978 brackets: BracketPairConfig {
7979 pairs: vec![BracketPair {
7980 start: "{".to_string(),
7981 end: "}".to_string(),
7982 close: true,
7983 surround: true,
7984 newline: true,
7985 }],
7986 ..Default::default()
7987 },
7988 autoclose_before: "}".to_string(),
7989 ..Default::default()
7990 },
7991 Some(tree_sitter_rust::LANGUAGE.into()),
7992 ));
7993
7994 let text = r#"
7995 a
7996 b
7997 c
7998 "#
7999 .unindent();
8000
8001 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8002 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8003 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8004 editor
8005 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8006 .await;
8007
8008 editor.update_in(cx, |editor, window, cx| {
8009 editor.change_selections(None, window, cx, |s| {
8010 s.select_ranges([
8011 Point::new(0, 1)..Point::new(0, 1),
8012 Point::new(1, 1)..Point::new(1, 1),
8013 Point::new(2, 1)..Point::new(2, 1),
8014 ])
8015 });
8016
8017 editor.handle_input("{", window, cx);
8018 editor.handle_input("{", window, cx);
8019 editor.handle_input("_", window, cx);
8020 assert_eq!(
8021 editor.text(cx),
8022 "
8023 a{{_}}
8024 b{{_}}
8025 c{{_}}
8026 "
8027 .unindent()
8028 );
8029 assert_eq!(
8030 editor.selections.ranges::<Point>(cx),
8031 [
8032 Point::new(0, 4)..Point::new(0, 4),
8033 Point::new(1, 4)..Point::new(1, 4),
8034 Point::new(2, 4)..Point::new(2, 4)
8035 ]
8036 );
8037
8038 editor.backspace(&Default::default(), window, cx);
8039 editor.backspace(&Default::default(), window, cx);
8040 assert_eq!(
8041 editor.text(cx),
8042 "
8043 a{}
8044 b{}
8045 c{}
8046 "
8047 .unindent()
8048 );
8049 assert_eq!(
8050 editor.selections.ranges::<Point>(cx),
8051 [
8052 Point::new(0, 2)..Point::new(0, 2),
8053 Point::new(1, 2)..Point::new(1, 2),
8054 Point::new(2, 2)..Point::new(2, 2)
8055 ]
8056 );
8057
8058 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8059 assert_eq!(
8060 editor.text(cx),
8061 "
8062 a
8063 b
8064 c
8065 "
8066 .unindent()
8067 );
8068 assert_eq!(
8069 editor.selections.ranges::<Point>(cx),
8070 [
8071 Point::new(0, 1)..Point::new(0, 1),
8072 Point::new(1, 1)..Point::new(1, 1),
8073 Point::new(2, 1)..Point::new(2, 1)
8074 ]
8075 );
8076 });
8077}
8078
8079#[gpui::test]
8080async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8081 init_test(cx, |settings| {
8082 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8083 });
8084
8085 let mut cx = EditorTestContext::new(cx).await;
8086
8087 let language = Arc::new(Language::new(
8088 LanguageConfig {
8089 brackets: BracketPairConfig {
8090 pairs: vec![
8091 BracketPair {
8092 start: "{".to_string(),
8093 end: "}".to_string(),
8094 close: true,
8095 surround: true,
8096 newline: true,
8097 },
8098 BracketPair {
8099 start: "(".to_string(),
8100 end: ")".to_string(),
8101 close: true,
8102 surround: true,
8103 newline: true,
8104 },
8105 BracketPair {
8106 start: "[".to_string(),
8107 end: "]".to_string(),
8108 close: false,
8109 surround: true,
8110 newline: true,
8111 },
8112 ],
8113 ..Default::default()
8114 },
8115 autoclose_before: "})]".to_string(),
8116 ..Default::default()
8117 },
8118 Some(tree_sitter_rust::LANGUAGE.into()),
8119 ));
8120
8121 cx.language_registry().add(language.clone());
8122 cx.update_buffer(|buffer, cx| {
8123 buffer.set_language(Some(language), cx);
8124 });
8125
8126 cx.set_state(
8127 &"
8128 {(ˇ)}
8129 [[ˇ]]
8130 {(ˇ)}
8131 "
8132 .unindent(),
8133 );
8134
8135 cx.update_editor(|editor, window, cx| {
8136 editor.backspace(&Default::default(), window, cx);
8137 editor.backspace(&Default::default(), window, cx);
8138 });
8139
8140 cx.assert_editor_state(
8141 &"
8142 ˇ
8143 ˇ]]
8144 ˇ
8145 "
8146 .unindent(),
8147 );
8148
8149 cx.update_editor(|editor, window, cx| {
8150 editor.handle_input("{", window, cx);
8151 editor.handle_input("{", window, cx);
8152 editor.move_right(&MoveRight, window, cx);
8153 editor.move_right(&MoveRight, window, cx);
8154 editor.move_left(&MoveLeft, window, cx);
8155 editor.move_left(&MoveLeft, window, cx);
8156 editor.backspace(&Default::default(), window, cx);
8157 });
8158
8159 cx.assert_editor_state(
8160 &"
8161 {ˇ}
8162 {ˇ}]]
8163 {ˇ}
8164 "
8165 .unindent(),
8166 );
8167
8168 cx.update_editor(|editor, window, cx| {
8169 editor.backspace(&Default::default(), window, cx);
8170 });
8171
8172 cx.assert_editor_state(
8173 &"
8174 ˇ
8175 ˇ]]
8176 ˇ
8177 "
8178 .unindent(),
8179 );
8180}
8181
8182#[gpui::test]
8183async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8184 init_test(cx, |_| {});
8185
8186 let language = Arc::new(Language::new(
8187 LanguageConfig::default(),
8188 Some(tree_sitter_rust::LANGUAGE.into()),
8189 ));
8190
8191 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8192 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8193 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8194 editor
8195 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8196 .await;
8197
8198 editor.update_in(cx, |editor, window, cx| {
8199 editor.set_auto_replace_emoji_shortcode(true);
8200
8201 editor.handle_input("Hello ", window, cx);
8202 editor.handle_input(":wave", window, cx);
8203 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8204
8205 editor.handle_input(":", window, cx);
8206 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8207
8208 editor.handle_input(" :smile", window, cx);
8209 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8210
8211 editor.handle_input(":", window, cx);
8212 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8213
8214 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8215 editor.handle_input(":wave", window, cx);
8216 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8217
8218 editor.handle_input(":", window, cx);
8219 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8220
8221 editor.handle_input(":1", window, cx);
8222 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8223
8224 editor.handle_input(":", window, cx);
8225 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8226
8227 // Ensure shortcode does not get replaced when it is part of a word
8228 editor.handle_input(" Test:wave", window, cx);
8229 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8230
8231 editor.handle_input(":", window, cx);
8232 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8233
8234 editor.set_auto_replace_emoji_shortcode(false);
8235
8236 // Ensure shortcode does not get replaced when auto replace is off
8237 editor.handle_input(" :wave", window, cx);
8238 assert_eq!(
8239 editor.text(cx),
8240 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8241 );
8242
8243 editor.handle_input(":", window, cx);
8244 assert_eq!(
8245 editor.text(cx),
8246 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8247 );
8248 });
8249}
8250
8251#[gpui::test]
8252async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8253 init_test(cx, |_| {});
8254
8255 let (text, insertion_ranges) = marked_text_ranges(
8256 indoc! {"
8257 ˇ
8258 "},
8259 false,
8260 );
8261
8262 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8263 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8264
8265 _ = editor.update_in(cx, |editor, window, cx| {
8266 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8267
8268 editor
8269 .insert_snippet(&insertion_ranges, snippet, window, cx)
8270 .unwrap();
8271
8272 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8273 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8274 assert_eq!(editor.text(cx), expected_text);
8275 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8276 }
8277
8278 assert(
8279 editor,
8280 cx,
8281 indoc! {"
8282 type «» =•
8283 "},
8284 );
8285
8286 assert!(editor.context_menu_visible(), "There should be a matches");
8287 });
8288}
8289
8290#[gpui::test]
8291async fn test_snippets(cx: &mut TestAppContext) {
8292 init_test(cx, |_| {});
8293
8294 let (text, insertion_ranges) = marked_text_ranges(
8295 indoc! {"
8296 a.ˇ b
8297 a.ˇ b
8298 a.ˇ b
8299 "},
8300 false,
8301 );
8302
8303 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8304 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8305
8306 editor.update_in(cx, |editor, window, cx| {
8307 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8308
8309 editor
8310 .insert_snippet(&insertion_ranges, snippet, window, cx)
8311 .unwrap();
8312
8313 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8314 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8315 assert_eq!(editor.text(cx), expected_text);
8316 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8317 }
8318
8319 assert(
8320 editor,
8321 cx,
8322 indoc! {"
8323 a.f(«one», two, «three») b
8324 a.f(«one», two, «three») b
8325 a.f(«one», two, «three») b
8326 "},
8327 );
8328
8329 // Can't move earlier than the first tab stop
8330 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8331 assert(
8332 editor,
8333 cx,
8334 indoc! {"
8335 a.f(«one», two, «three») b
8336 a.f(«one», two, «three») b
8337 a.f(«one», two, «three») b
8338 "},
8339 );
8340
8341 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8342 assert(
8343 editor,
8344 cx,
8345 indoc! {"
8346 a.f(one, «two», three) b
8347 a.f(one, «two», three) b
8348 a.f(one, «two», three) b
8349 "},
8350 );
8351
8352 editor.move_to_prev_snippet_tabstop(window, cx);
8353 assert(
8354 editor,
8355 cx,
8356 indoc! {"
8357 a.f(«one», two, «three») b
8358 a.f(«one», two, «three») b
8359 a.f(«one», two, «three») b
8360 "},
8361 );
8362
8363 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8364 assert(
8365 editor,
8366 cx,
8367 indoc! {"
8368 a.f(one, «two», three) b
8369 a.f(one, «two», three) b
8370 a.f(one, «two», three) b
8371 "},
8372 );
8373 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8374 assert(
8375 editor,
8376 cx,
8377 indoc! {"
8378 a.f(one, two, three)ˇ b
8379 a.f(one, two, three)ˇ b
8380 a.f(one, two, three)ˇ b
8381 "},
8382 );
8383
8384 // As soon as the last tab stop is reached, snippet state is gone
8385 editor.move_to_prev_snippet_tabstop(window, cx);
8386 assert(
8387 editor,
8388 cx,
8389 indoc! {"
8390 a.f(one, two, three)ˇ b
8391 a.f(one, two, three)ˇ b
8392 a.f(one, two, three)ˇ b
8393 "},
8394 );
8395 });
8396}
8397
8398#[gpui::test]
8399async fn test_document_format_during_save(cx: &mut TestAppContext) {
8400 init_test(cx, |_| {});
8401
8402 let fs = FakeFs::new(cx.executor());
8403 fs.insert_file(path!("/file.rs"), Default::default()).await;
8404
8405 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8406
8407 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8408 language_registry.add(rust_lang());
8409 let mut fake_servers = language_registry.register_fake_lsp(
8410 "Rust",
8411 FakeLspAdapter {
8412 capabilities: lsp::ServerCapabilities {
8413 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8414 ..Default::default()
8415 },
8416 ..Default::default()
8417 },
8418 );
8419
8420 let buffer = project
8421 .update(cx, |project, cx| {
8422 project.open_local_buffer(path!("/file.rs"), cx)
8423 })
8424 .await
8425 .unwrap();
8426
8427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8428 let (editor, cx) = cx.add_window_view(|window, cx| {
8429 build_editor_with_project(project.clone(), buffer, window, cx)
8430 });
8431 editor.update_in(cx, |editor, window, cx| {
8432 editor.set_text("one\ntwo\nthree\n", window, cx)
8433 });
8434 assert!(cx.read(|cx| editor.is_dirty(cx)));
8435
8436 cx.executor().start_waiting();
8437 let fake_server = fake_servers.next().await.unwrap();
8438
8439 {
8440 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8441 move |params, _| async move {
8442 assert_eq!(
8443 params.text_document.uri,
8444 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8445 );
8446 assert_eq!(params.options.tab_size, 4);
8447 Ok(Some(vec![lsp::TextEdit::new(
8448 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8449 ", ".to_string(),
8450 )]))
8451 },
8452 );
8453 let save = editor
8454 .update_in(cx, |editor, window, cx| {
8455 editor.save(true, project.clone(), window, cx)
8456 })
8457 .unwrap();
8458 cx.executor().start_waiting();
8459 save.await;
8460
8461 assert_eq!(
8462 editor.update(cx, |editor, cx| editor.text(cx)),
8463 "one, two\nthree\n"
8464 );
8465 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8466 }
8467
8468 {
8469 editor.update_in(cx, |editor, window, cx| {
8470 editor.set_text("one\ntwo\nthree\n", window, cx)
8471 });
8472 assert!(cx.read(|cx| editor.is_dirty(cx)));
8473
8474 // Ensure we can still save even if formatting hangs.
8475 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8476 move |params, _| async move {
8477 assert_eq!(
8478 params.text_document.uri,
8479 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8480 );
8481 futures::future::pending::<()>().await;
8482 unreachable!()
8483 },
8484 );
8485 let save = editor
8486 .update_in(cx, |editor, window, cx| {
8487 editor.save(true, project.clone(), window, cx)
8488 })
8489 .unwrap();
8490 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8491 cx.executor().start_waiting();
8492 save.await;
8493 assert_eq!(
8494 editor.update(cx, |editor, cx| editor.text(cx)),
8495 "one\ntwo\nthree\n"
8496 );
8497 }
8498
8499 // For non-dirty buffer, no formatting request should be sent
8500 {
8501 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8502
8503 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8504 panic!("Should not be invoked on non-dirty buffer");
8505 });
8506 let save = editor
8507 .update_in(cx, |editor, window, cx| {
8508 editor.save(true, project.clone(), window, cx)
8509 })
8510 .unwrap();
8511 cx.executor().start_waiting();
8512 save.await;
8513 }
8514
8515 // Set rust language override and assert overridden tabsize is sent to language server
8516 update_test_language_settings(cx, |settings| {
8517 settings.languages.insert(
8518 "Rust".into(),
8519 LanguageSettingsContent {
8520 tab_size: NonZeroU32::new(8),
8521 ..Default::default()
8522 },
8523 );
8524 });
8525
8526 {
8527 editor.update_in(cx, |editor, window, cx| {
8528 editor.set_text("somehting_new\n", window, cx)
8529 });
8530 assert!(cx.read(|cx| editor.is_dirty(cx)));
8531 let _formatting_request_signal = fake_server
8532 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8533 assert_eq!(
8534 params.text_document.uri,
8535 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8536 );
8537 assert_eq!(params.options.tab_size, 8);
8538 Ok(Some(vec![]))
8539 });
8540 let save = editor
8541 .update_in(cx, |editor, window, cx| {
8542 editor.save(true, project.clone(), window, cx)
8543 })
8544 .unwrap();
8545 cx.executor().start_waiting();
8546 save.await;
8547 }
8548}
8549
8550#[gpui::test]
8551async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8552 init_test(cx, |_| {});
8553
8554 let cols = 4;
8555 let rows = 10;
8556 let sample_text_1 = sample_text(rows, cols, 'a');
8557 assert_eq!(
8558 sample_text_1,
8559 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8560 );
8561 let sample_text_2 = sample_text(rows, cols, 'l');
8562 assert_eq!(
8563 sample_text_2,
8564 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8565 );
8566 let sample_text_3 = sample_text(rows, cols, 'v');
8567 assert_eq!(
8568 sample_text_3,
8569 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8570 );
8571
8572 let fs = FakeFs::new(cx.executor());
8573 fs.insert_tree(
8574 path!("/a"),
8575 json!({
8576 "main.rs": sample_text_1,
8577 "other.rs": sample_text_2,
8578 "lib.rs": sample_text_3,
8579 }),
8580 )
8581 .await;
8582
8583 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8584 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8585 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8586
8587 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8588 language_registry.add(rust_lang());
8589 let mut fake_servers = language_registry.register_fake_lsp(
8590 "Rust",
8591 FakeLspAdapter {
8592 capabilities: lsp::ServerCapabilities {
8593 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8594 ..Default::default()
8595 },
8596 ..Default::default()
8597 },
8598 );
8599
8600 let worktree = project.update(cx, |project, cx| {
8601 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8602 assert_eq!(worktrees.len(), 1);
8603 worktrees.pop().unwrap()
8604 });
8605 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8606
8607 let buffer_1 = project
8608 .update(cx, |project, cx| {
8609 project.open_buffer((worktree_id, "main.rs"), cx)
8610 })
8611 .await
8612 .unwrap();
8613 let buffer_2 = project
8614 .update(cx, |project, cx| {
8615 project.open_buffer((worktree_id, "other.rs"), cx)
8616 })
8617 .await
8618 .unwrap();
8619 let buffer_3 = project
8620 .update(cx, |project, cx| {
8621 project.open_buffer((worktree_id, "lib.rs"), cx)
8622 })
8623 .await
8624 .unwrap();
8625
8626 let multi_buffer = cx.new(|cx| {
8627 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8628 multi_buffer.push_excerpts(
8629 buffer_1.clone(),
8630 [
8631 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8632 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8633 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8634 ],
8635 cx,
8636 );
8637 multi_buffer.push_excerpts(
8638 buffer_2.clone(),
8639 [
8640 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8641 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8642 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8643 ],
8644 cx,
8645 );
8646 multi_buffer.push_excerpts(
8647 buffer_3.clone(),
8648 [
8649 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8650 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8651 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8652 ],
8653 cx,
8654 );
8655 multi_buffer
8656 });
8657 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8658 Editor::new(
8659 EditorMode::full(),
8660 multi_buffer,
8661 Some(project.clone()),
8662 window,
8663 cx,
8664 )
8665 });
8666
8667 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8668 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8669 s.select_ranges(Some(1..2))
8670 });
8671 editor.insert("|one|two|three|", window, cx);
8672 });
8673 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8674 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8675 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8676 s.select_ranges(Some(60..70))
8677 });
8678 editor.insert("|four|five|six|", window, cx);
8679 });
8680 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8681
8682 // First two buffers should be edited, but not the third one.
8683 assert_eq!(
8684 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8685 "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}",
8686 );
8687 buffer_1.update(cx, |buffer, _| {
8688 assert!(buffer.is_dirty());
8689 assert_eq!(
8690 buffer.text(),
8691 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8692 )
8693 });
8694 buffer_2.update(cx, |buffer, _| {
8695 assert!(buffer.is_dirty());
8696 assert_eq!(
8697 buffer.text(),
8698 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8699 )
8700 });
8701 buffer_3.update(cx, |buffer, _| {
8702 assert!(!buffer.is_dirty());
8703 assert_eq!(buffer.text(), sample_text_3,)
8704 });
8705 cx.executor().run_until_parked();
8706
8707 cx.executor().start_waiting();
8708 let save = multi_buffer_editor
8709 .update_in(cx, |editor, window, cx| {
8710 editor.save(true, project.clone(), window, cx)
8711 })
8712 .unwrap();
8713
8714 let fake_server = fake_servers.next().await.unwrap();
8715 fake_server
8716 .server
8717 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8718 Ok(Some(vec![lsp::TextEdit::new(
8719 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8720 format!("[{} formatted]", params.text_document.uri),
8721 )]))
8722 })
8723 .detach();
8724 save.await;
8725
8726 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8727 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8728 assert_eq!(
8729 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8730 uri!(
8731 "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}"
8732 ),
8733 );
8734 buffer_1.update(cx, |buffer, _| {
8735 assert!(!buffer.is_dirty());
8736 assert_eq!(
8737 buffer.text(),
8738 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8739 )
8740 });
8741 buffer_2.update(cx, |buffer, _| {
8742 assert!(!buffer.is_dirty());
8743 assert_eq!(
8744 buffer.text(),
8745 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8746 )
8747 });
8748 buffer_3.update(cx, |buffer, _| {
8749 assert!(!buffer.is_dirty());
8750 assert_eq!(buffer.text(), sample_text_3,)
8751 });
8752}
8753
8754#[gpui::test]
8755async fn test_range_format_during_save(cx: &mut TestAppContext) {
8756 init_test(cx, |_| {});
8757
8758 let fs = FakeFs::new(cx.executor());
8759 fs.insert_file(path!("/file.rs"), Default::default()).await;
8760
8761 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8762
8763 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8764 language_registry.add(rust_lang());
8765 let mut fake_servers = language_registry.register_fake_lsp(
8766 "Rust",
8767 FakeLspAdapter {
8768 capabilities: lsp::ServerCapabilities {
8769 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8770 ..Default::default()
8771 },
8772 ..Default::default()
8773 },
8774 );
8775
8776 let buffer = project
8777 .update(cx, |project, cx| {
8778 project.open_local_buffer(path!("/file.rs"), cx)
8779 })
8780 .await
8781 .unwrap();
8782
8783 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8784 let (editor, cx) = cx.add_window_view(|window, cx| {
8785 build_editor_with_project(project.clone(), buffer, window, cx)
8786 });
8787 editor.update_in(cx, |editor, window, cx| {
8788 editor.set_text("one\ntwo\nthree\n", window, cx)
8789 });
8790 assert!(cx.read(|cx| editor.is_dirty(cx)));
8791
8792 cx.executor().start_waiting();
8793 let fake_server = fake_servers.next().await.unwrap();
8794
8795 let save = editor
8796 .update_in(cx, |editor, window, cx| {
8797 editor.save(true, project.clone(), window, cx)
8798 })
8799 .unwrap();
8800 fake_server
8801 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8802 assert_eq!(
8803 params.text_document.uri,
8804 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8805 );
8806 assert_eq!(params.options.tab_size, 4);
8807 Ok(Some(vec![lsp::TextEdit::new(
8808 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8809 ", ".to_string(),
8810 )]))
8811 })
8812 .next()
8813 .await;
8814 cx.executor().start_waiting();
8815 save.await;
8816 assert_eq!(
8817 editor.update(cx, |editor, cx| editor.text(cx)),
8818 "one, two\nthree\n"
8819 );
8820 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8821
8822 editor.update_in(cx, |editor, window, cx| {
8823 editor.set_text("one\ntwo\nthree\n", window, cx)
8824 });
8825 assert!(cx.read(|cx| editor.is_dirty(cx)));
8826
8827 // Ensure we can still save even if formatting hangs.
8828 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8829 move |params, _| async move {
8830 assert_eq!(
8831 params.text_document.uri,
8832 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8833 );
8834 futures::future::pending::<()>().await;
8835 unreachable!()
8836 },
8837 );
8838 let save = editor
8839 .update_in(cx, |editor, window, cx| {
8840 editor.save(true, project.clone(), window, cx)
8841 })
8842 .unwrap();
8843 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8844 cx.executor().start_waiting();
8845 save.await;
8846 assert_eq!(
8847 editor.update(cx, |editor, cx| editor.text(cx)),
8848 "one\ntwo\nthree\n"
8849 );
8850 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8851
8852 // For non-dirty buffer, no formatting request should be sent
8853 let save = editor
8854 .update_in(cx, |editor, window, cx| {
8855 editor.save(true, project.clone(), window, cx)
8856 })
8857 .unwrap();
8858 let _pending_format_request = fake_server
8859 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8860 panic!("Should not be invoked on non-dirty buffer");
8861 })
8862 .next();
8863 cx.executor().start_waiting();
8864 save.await;
8865
8866 // Set Rust language override and assert overridden tabsize is sent to language server
8867 update_test_language_settings(cx, |settings| {
8868 settings.languages.insert(
8869 "Rust".into(),
8870 LanguageSettingsContent {
8871 tab_size: NonZeroU32::new(8),
8872 ..Default::default()
8873 },
8874 );
8875 });
8876
8877 editor.update_in(cx, |editor, window, cx| {
8878 editor.set_text("somehting_new\n", window, cx)
8879 });
8880 assert!(cx.read(|cx| editor.is_dirty(cx)));
8881 let save = editor
8882 .update_in(cx, |editor, window, cx| {
8883 editor.save(true, project.clone(), window, cx)
8884 })
8885 .unwrap();
8886 fake_server
8887 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8888 assert_eq!(
8889 params.text_document.uri,
8890 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8891 );
8892 assert_eq!(params.options.tab_size, 8);
8893 Ok(Some(vec![]))
8894 })
8895 .next()
8896 .await;
8897 cx.executor().start_waiting();
8898 save.await;
8899}
8900
8901#[gpui::test]
8902async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8903 init_test(cx, |settings| {
8904 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8905 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8906 ))
8907 });
8908
8909 let fs = FakeFs::new(cx.executor());
8910 fs.insert_file(path!("/file.rs"), Default::default()).await;
8911
8912 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8913
8914 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8915 language_registry.add(Arc::new(Language::new(
8916 LanguageConfig {
8917 name: "Rust".into(),
8918 matcher: LanguageMatcher {
8919 path_suffixes: vec!["rs".to_string()],
8920 ..Default::default()
8921 },
8922 ..LanguageConfig::default()
8923 },
8924 Some(tree_sitter_rust::LANGUAGE.into()),
8925 )));
8926 update_test_language_settings(cx, |settings| {
8927 // Enable Prettier formatting for the same buffer, and ensure
8928 // LSP is called instead of Prettier.
8929 settings.defaults.prettier = Some(PrettierSettings {
8930 allowed: true,
8931 ..PrettierSettings::default()
8932 });
8933 });
8934 let mut fake_servers = language_registry.register_fake_lsp(
8935 "Rust",
8936 FakeLspAdapter {
8937 capabilities: lsp::ServerCapabilities {
8938 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8939 ..Default::default()
8940 },
8941 ..Default::default()
8942 },
8943 );
8944
8945 let buffer = project
8946 .update(cx, |project, cx| {
8947 project.open_local_buffer(path!("/file.rs"), cx)
8948 })
8949 .await
8950 .unwrap();
8951
8952 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8953 let (editor, cx) = cx.add_window_view(|window, cx| {
8954 build_editor_with_project(project.clone(), buffer, window, cx)
8955 });
8956 editor.update_in(cx, |editor, window, cx| {
8957 editor.set_text("one\ntwo\nthree\n", window, cx)
8958 });
8959
8960 cx.executor().start_waiting();
8961 let fake_server = fake_servers.next().await.unwrap();
8962
8963 let format = editor
8964 .update_in(cx, |editor, window, cx| {
8965 editor.perform_format(
8966 project.clone(),
8967 FormatTrigger::Manual,
8968 FormatTarget::Buffers,
8969 window,
8970 cx,
8971 )
8972 })
8973 .unwrap();
8974 fake_server
8975 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8976 assert_eq!(
8977 params.text_document.uri,
8978 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8979 );
8980 assert_eq!(params.options.tab_size, 4);
8981 Ok(Some(vec![lsp::TextEdit::new(
8982 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8983 ", ".to_string(),
8984 )]))
8985 })
8986 .next()
8987 .await;
8988 cx.executor().start_waiting();
8989 format.await;
8990 assert_eq!(
8991 editor.update(cx, |editor, cx| editor.text(cx)),
8992 "one, two\nthree\n"
8993 );
8994
8995 editor.update_in(cx, |editor, window, cx| {
8996 editor.set_text("one\ntwo\nthree\n", window, cx)
8997 });
8998 // Ensure we don't lock if formatting hangs.
8999 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9000 move |params, _| async move {
9001 assert_eq!(
9002 params.text_document.uri,
9003 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9004 );
9005 futures::future::pending::<()>().await;
9006 unreachable!()
9007 },
9008 );
9009 let format = editor
9010 .update_in(cx, |editor, window, cx| {
9011 editor.perform_format(
9012 project,
9013 FormatTrigger::Manual,
9014 FormatTarget::Buffers,
9015 window,
9016 cx,
9017 )
9018 })
9019 .unwrap();
9020 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9021 cx.executor().start_waiting();
9022 format.await;
9023 assert_eq!(
9024 editor.update(cx, |editor, cx| editor.text(cx)),
9025 "one\ntwo\nthree\n"
9026 );
9027}
9028
9029#[gpui::test]
9030async fn test_multiple_formatters(cx: &mut TestAppContext) {
9031 init_test(cx, |settings| {
9032 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9033 settings.defaults.formatter =
9034 Some(language_settings::SelectedFormatter::List(FormatterList(
9035 vec![
9036 Formatter::LanguageServer { name: None },
9037 Formatter::CodeActions(
9038 [
9039 ("code-action-1".into(), true),
9040 ("code-action-2".into(), true),
9041 ]
9042 .into_iter()
9043 .collect(),
9044 ),
9045 ]
9046 .into(),
9047 )))
9048 });
9049
9050 let fs = FakeFs::new(cx.executor());
9051 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9052 .await;
9053
9054 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9055 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9056 language_registry.add(rust_lang());
9057
9058 let mut fake_servers = language_registry.register_fake_lsp(
9059 "Rust",
9060 FakeLspAdapter {
9061 capabilities: lsp::ServerCapabilities {
9062 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9063 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9064 commands: vec!["the-command-for-code-action-1".into()],
9065 ..Default::default()
9066 }),
9067 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9068 ..Default::default()
9069 },
9070 ..Default::default()
9071 },
9072 );
9073
9074 let buffer = project
9075 .update(cx, |project, cx| {
9076 project.open_local_buffer(path!("/file.rs"), cx)
9077 })
9078 .await
9079 .unwrap();
9080
9081 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9082 let (editor, cx) = cx.add_window_view(|window, cx| {
9083 build_editor_with_project(project.clone(), buffer, window, cx)
9084 });
9085
9086 cx.executor().start_waiting();
9087
9088 let fake_server = fake_servers.next().await.unwrap();
9089 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9090 move |_params, _| async move {
9091 Ok(Some(vec![lsp::TextEdit::new(
9092 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9093 "applied-formatting\n".to_string(),
9094 )]))
9095 },
9096 );
9097 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9098 move |params, _| async move {
9099 assert_eq!(
9100 params.context.only,
9101 Some(vec!["code-action-1".into(), "code-action-2".into()])
9102 );
9103 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9104 Ok(Some(vec![
9105 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9106 kind: Some("code-action-1".into()),
9107 edit: Some(lsp::WorkspaceEdit::new(
9108 [(
9109 uri.clone(),
9110 vec![lsp::TextEdit::new(
9111 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9112 "applied-code-action-1-edit\n".to_string(),
9113 )],
9114 )]
9115 .into_iter()
9116 .collect(),
9117 )),
9118 command: Some(lsp::Command {
9119 command: "the-command-for-code-action-1".into(),
9120 ..Default::default()
9121 }),
9122 ..Default::default()
9123 }),
9124 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9125 kind: Some("code-action-2".into()),
9126 edit: Some(lsp::WorkspaceEdit::new(
9127 [(
9128 uri.clone(),
9129 vec![lsp::TextEdit::new(
9130 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9131 "applied-code-action-2-edit\n".to_string(),
9132 )],
9133 )]
9134 .into_iter()
9135 .collect(),
9136 )),
9137 ..Default::default()
9138 }),
9139 ]))
9140 },
9141 );
9142
9143 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9144 move |params, _| async move { Ok(params) }
9145 });
9146
9147 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9148 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9149 let fake = fake_server.clone();
9150 let lock = command_lock.clone();
9151 move |params, _| {
9152 assert_eq!(params.command, "the-command-for-code-action-1");
9153 let fake = fake.clone();
9154 let lock = lock.clone();
9155 async move {
9156 lock.lock().await;
9157 fake.server
9158 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9159 label: None,
9160 edit: lsp::WorkspaceEdit {
9161 changes: Some(
9162 [(
9163 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9164 vec![lsp::TextEdit {
9165 range: lsp::Range::new(
9166 lsp::Position::new(0, 0),
9167 lsp::Position::new(0, 0),
9168 ),
9169 new_text: "applied-code-action-1-command\n".into(),
9170 }],
9171 )]
9172 .into_iter()
9173 .collect(),
9174 ),
9175 ..Default::default()
9176 },
9177 })
9178 .await
9179 .into_response()
9180 .unwrap();
9181 Ok(Some(json!(null)))
9182 }
9183 }
9184 });
9185
9186 cx.executor().start_waiting();
9187 editor
9188 .update_in(cx, |editor, window, cx| {
9189 editor.perform_format(
9190 project.clone(),
9191 FormatTrigger::Manual,
9192 FormatTarget::Buffers,
9193 window,
9194 cx,
9195 )
9196 })
9197 .unwrap()
9198 .await;
9199 editor.update(cx, |editor, cx| {
9200 assert_eq!(
9201 editor.text(cx),
9202 r#"
9203 applied-code-action-2-edit
9204 applied-code-action-1-command
9205 applied-code-action-1-edit
9206 applied-formatting
9207 one
9208 two
9209 three
9210 "#
9211 .unindent()
9212 );
9213 });
9214
9215 editor.update_in(cx, |editor, window, cx| {
9216 editor.undo(&Default::default(), window, cx);
9217 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9218 });
9219
9220 // Perform a manual edit while waiting for an LSP command
9221 // that's being run as part of a formatting code action.
9222 let lock_guard = command_lock.lock().await;
9223 let format = editor
9224 .update_in(cx, |editor, window, cx| {
9225 editor.perform_format(
9226 project.clone(),
9227 FormatTrigger::Manual,
9228 FormatTarget::Buffers,
9229 window,
9230 cx,
9231 )
9232 })
9233 .unwrap();
9234 cx.run_until_parked();
9235 editor.update(cx, |editor, cx| {
9236 assert_eq!(
9237 editor.text(cx),
9238 r#"
9239 applied-code-action-1-edit
9240 applied-formatting
9241 one
9242 two
9243 three
9244 "#
9245 .unindent()
9246 );
9247
9248 editor.buffer.update(cx, |buffer, cx| {
9249 let ix = buffer.len(cx);
9250 buffer.edit([(ix..ix, "edited\n")], None, cx);
9251 });
9252 });
9253
9254 // Allow the LSP command to proceed. Because the buffer was edited,
9255 // the second code action will not be run.
9256 drop(lock_guard);
9257 format.await;
9258 editor.update_in(cx, |editor, window, cx| {
9259 assert_eq!(
9260 editor.text(cx),
9261 r#"
9262 applied-code-action-1-command
9263 applied-code-action-1-edit
9264 applied-formatting
9265 one
9266 two
9267 three
9268 edited
9269 "#
9270 .unindent()
9271 );
9272
9273 // The manual edit is undone first, because it is the last thing the user did
9274 // (even though the command completed afterwards).
9275 editor.undo(&Default::default(), window, cx);
9276 assert_eq!(
9277 editor.text(cx),
9278 r#"
9279 applied-code-action-1-command
9280 applied-code-action-1-edit
9281 applied-formatting
9282 one
9283 two
9284 three
9285 "#
9286 .unindent()
9287 );
9288
9289 // All the formatting (including the command, which completed after the manual edit)
9290 // is undone together.
9291 editor.undo(&Default::default(), window, cx);
9292 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9293 });
9294}
9295
9296#[gpui::test]
9297async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9298 init_test(cx, |settings| {
9299 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9300 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9301 ))
9302 });
9303
9304 let fs = FakeFs::new(cx.executor());
9305 fs.insert_file(path!("/file.ts"), Default::default()).await;
9306
9307 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9308
9309 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9310 language_registry.add(Arc::new(Language::new(
9311 LanguageConfig {
9312 name: "TypeScript".into(),
9313 matcher: LanguageMatcher {
9314 path_suffixes: vec!["ts".to_string()],
9315 ..Default::default()
9316 },
9317 ..LanguageConfig::default()
9318 },
9319 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9320 )));
9321 update_test_language_settings(cx, |settings| {
9322 settings.defaults.prettier = Some(PrettierSettings {
9323 allowed: true,
9324 ..PrettierSettings::default()
9325 });
9326 });
9327 let mut fake_servers = language_registry.register_fake_lsp(
9328 "TypeScript",
9329 FakeLspAdapter {
9330 capabilities: lsp::ServerCapabilities {
9331 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9332 ..Default::default()
9333 },
9334 ..Default::default()
9335 },
9336 );
9337
9338 let buffer = project
9339 .update(cx, |project, cx| {
9340 project.open_local_buffer(path!("/file.ts"), cx)
9341 })
9342 .await
9343 .unwrap();
9344
9345 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9346 let (editor, cx) = cx.add_window_view(|window, cx| {
9347 build_editor_with_project(project.clone(), buffer, window, cx)
9348 });
9349 editor.update_in(cx, |editor, window, cx| {
9350 editor.set_text(
9351 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9352 window,
9353 cx,
9354 )
9355 });
9356
9357 cx.executor().start_waiting();
9358 let fake_server = fake_servers.next().await.unwrap();
9359
9360 let format = editor
9361 .update_in(cx, |editor, window, cx| {
9362 editor.perform_code_action_kind(
9363 project.clone(),
9364 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9365 window,
9366 cx,
9367 )
9368 })
9369 .unwrap();
9370 fake_server
9371 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9372 assert_eq!(
9373 params.text_document.uri,
9374 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9375 );
9376 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9377 lsp::CodeAction {
9378 title: "Organize Imports".to_string(),
9379 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9380 edit: Some(lsp::WorkspaceEdit {
9381 changes: Some(
9382 [(
9383 params.text_document.uri.clone(),
9384 vec![lsp::TextEdit::new(
9385 lsp::Range::new(
9386 lsp::Position::new(1, 0),
9387 lsp::Position::new(2, 0),
9388 ),
9389 "".to_string(),
9390 )],
9391 )]
9392 .into_iter()
9393 .collect(),
9394 ),
9395 ..Default::default()
9396 }),
9397 ..Default::default()
9398 },
9399 )]))
9400 })
9401 .next()
9402 .await;
9403 cx.executor().start_waiting();
9404 format.await;
9405 assert_eq!(
9406 editor.update(cx, |editor, cx| editor.text(cx)),
9407 "import { a } from 'module';\n\nconst x = a;\n"
9408 );
9409
9410 editor.update_in(cx, |editor, window, cx| {
9411 editor.set_text(
9412 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9413 window,
9414 cx,
9415 )
9416 });
9417 // Ensure we don't lock if code action hangs.
9418 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9419 move |params, _| async move {
9420 assert_eq!(
9421 params.text_document.uri,
9422 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9423 );
9424 futures::future::pending::<()>().await;
9425 unreachable!()
9426 },
9427 );
9428 let format = editor
9429 .update_in(cx, |editor, window, cx| {
9430 editor.perform_code_action_kind(
9431 project,
9432 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9433 window,
9434 cx,
9435 )
9436 })
9437 .unwrap();
9438 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9439 cx.executor().start_waiting();
9440 format.await;
9441 assert_eq!(
9442 editor.update(cx, |editor, cx| editor.text(cx)),
9443 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9444 );
9445}
9446
9447#[gpui::test]
9448async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9449 init_test(cx, |_| {});
9450
9451 let mut cx = EditorLspTestContext::new_rust(
9452 lsp::ServerCapabilities {
9453 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9454 ..Default::default()
9455 },
9456 cx,
9457 )
9458 .await;
9459
9460 cx.set_state(indoc! {"
9461 one.twoˇ
9462 "});
9463
9464 // The format request takes a long time. When it completes, it inserts
9465 // a newline and an indent before the `.`
9466 cx.lsp
9467 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9468 let executor = cx.background_executor().clone();
9469 async move {
9470 executor.timer(Duration::from_millis(100)).await;
9471 Ok(Some(vec![lsp::TextEdit {
9472 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9473 new_text: "\n ".into(),
9474 }]))
9475 }
9476 });
9477
9478 // Submit a format request.
9479 let format_1 = cx
9480 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9481 .unwrap();
9482 cx.executor().run_until_parked();
9483
9484 // Submit a second format request.
9485 let format_2 = cx
9486 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9487 .unwrap();
9488 cx.executor().run_until_parked();
9489
9490 // Wait for both format requests to complete
9491 cx.executor().advance_clock(Duration::from_millis(200));
9492 cx.executor().start_waiting();
9493 format_1.await.unwrap();
9494 cx.executor().start_waiting();
9495 format_2.await.unwrap();
9496
9497 // The formatting edits only happens once.
9498 cx.assert_editor_state(indoc! {"
9499 one
9500 .twoˇ
9501 "});
9502}
9503
9504#[gpui::test]
9505async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9506 init_test(cx, |settings| {
9507 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9508 });
9509
9510 let mut cx = EditorLspTestContext::new_rust(
9511 lsp::ServerCapabilities {
9512 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9513 ..Default::default()
9514 },
9515 cx,
9516 )
9517 .await;
9518
9519 // Set up a buffer white some trailing whitespace and no trailing newline.
9520 cx.set_state(
9521 &[
9522 "one ", //
9523 "twoˇ", //
9524 "three ", //
9525 "four", //
9526 ]
9527 .join("\n"),
9528 );
9529
9530 // Submit a format request.
9531 let format = cx
9532 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9533 .unwrap();
9534
9535 // Record which buffer changes have been sent to the language server
9536 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9537 cx.lsp
9538 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9539 let buffer_changes = buffer_changes.clone();
9540 move |params, _| {
9541 buffer_changes.lock().extend(
9542 params
9543 .content_changes
9544 .into_iter()
9545 .map(|e| (e.range.unwrap(), e.text)),
9546 );
9547 }
9548 });
9549
9550 // Handle formatting requests to the language server.
9551 cx.lsp
9552 .set_request_handler::<lsp::request::Formatting, _, _>({
9553 let buffer_changes = buffer_changes.clone();
9554 move |_, _| {
9555 // When formatting is requested, trailing whitespace has already been stripped,
9556 // and the trailing newline has already been added.
9557 assert_eq!(
9558 &buffer_changes.lock()[1..],
9559 &[
9560 (
9561 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9562 "".into()
9563 ),
9564 (
9565 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9566 "".into()
9567 ),
9568 (
9569 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9570 "\n".into()
9571 ),
9572 ]
9573 );
9574
9575 // Insert blank lines between each line of the buffer.
9576 async move {
9577 Ok(Some(vec![
9578 lsp::TextEdit {
9579 range: lsp::Range::new(
9580 lsp::Position::new(1, 0),
9581 lsp::Position::new(1, 0),
9582 ),
9583 new_text: "\n".into(),
9584 },
9585 lsp::TextEdit {
9586 range: lsp::Range::new(
9587 lsp::Position::new(2, 0),
9588 lsp::Position::new(2, 0),
9589 ),
9590 new_text: "\n".into(),
9591 },
9592 ]))
9593 }
9594 }
9595 });
9596
9597 // After formatting the buffer, the trailing whitespace is stripped,
9598 // a newline is appended, and the edits provided by the language server
9599 // have been applied.
9600 format.await.unwrap();
9601 cx.assert_editor_state(
9602 &[
9603 "one", //
9604 "", //
9605 "twoˇ", //
9606 "", //
9607 "three", //
9608 "four", //
9609 "", //
9610 ]
9611 .join("\n"),
9612 );
9613
9614 // Undoing the formatting undoes the trailing whitespace removal, the
9615 // trailing newline, and the LSP edits.
9616 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9617 cx.assert_editor_state(
9618 &[
9619 "one ", //
9620 "twoˇ", //
9621 "three ", //
9622 "four", //
9623 ]
9624 .join("\n"),
9625 );
9626}
9627
9628#[gpui::test]
9629async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9630 cx: &mut TestAppContext,
9631) {
9632 init_test(cx, |_| {});
9633
9634 cx.update(|cx| {
9635 cx.update_global::<SettingsStore, _>(|settings, cx| {
9636 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9637 settings.auto_signature_help = Some(true);
9638 });
9639 });
9640 });
9641
9642 let mut cx = EditorLspTestContext::new_rust(
9643 lsp::ServerCapabilities {
9644 signature_help_provider: Some(lsp::SignatureHelpOptions {
9645 ..Default::default()
9646 }),
9647 ..Default::default()
9648 },
9649 cx,
9650 )
9651 .await;
9652
9653 let language = Language::new(
9654 LanguageConfig {
9655 name: "Rust".into(),
9656 brackets: BracketPairConfig {
9657 pairs: vec![
9658 BracketPair {
9659 start: "{".to_string(),
9660 end: "}".to_string(),
9661 close: true,
9662 surround: true,
9663 newline: true,
9664 },
9665 BracketPair {
9666 start: "(".to_string(),
9667 end: ")".to_string(),
9668 close: true,
9669 surround: true,
9670 newline: true,
9671 },
9672 BracketPair {
9673 start: "/*".to_string(),
9674 end: " */".to_string(),
9675 close: true,
9676 surround: true,
9677 newline: true,
9678 },
9679 BracketPair {
9680 start: "[".to_string(),
9681 end: "]".to_string(),
9682 close: false,
9683 surround: false,
9684 newline: true,
9685 },
9686 BracketPair {
9687 start: "\"".to_string(),
9688 end: "\"".to_string(),
9689 close: true,
9690 surround: true,
9691 newline: false,
9692 },
9693 BracketPair {
9694 start: "<".to_string(),
9695 end: ">".to_string(),
9696 close: false,
9697 surround: true,
9698 newline: true,
9699 },
9700 ],
9701 ..Default::default()
9702 },
9703 autoclose_before: "})]".to_string(),
9704 ..Default::default()
9705 },
9706 Some(tree_sitter_rust::LANGUAGE.into()),
9707 );
9708 let language = Arc::new(language);
9709
9710 cx.language_registry().add(language.clone());
9711 cx.update_buffer(|buffer, cx| {
9712 buffer.set_language(Some(language), cx);
9713 });
9714
9715 cx.set_state(
9716 &r#"
9717 fn main() {
9718 sampleˇ
9719 }
9720 "#
9721 .unindent(),
9722 );
9723
9724 cx.update_editor(|editor, window, cx| {
9725 editor.handle_input("(", window, cx);
9726 });
9727 cx.assert_editor_state(
9728 &"
9729 fn main() {
9730 sample(ˇ)
9731 }
9732 "
9733 .unindent(),
9734 );
9735
9736 let mocked_response = lsp::SignatureHelp {
9737 signatures: vec![lsp::SignatureInformation {
9738 label: "fn sample(param1: u8, param2: u8)".to_string(),
9739 documentation: None,
9740 parameters: Some(vec![
9741 lsp::ParameterInformation {
9742 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9743 documentation: None,
9744 },
9745 lsp::ParameterInformation {
9746 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9747 documentation: None,
9748 },
9749 ]),
9750 active_parameter: None,
9751 }],
9752 active_signature: Some(0),
9753 active_parameter: Some(0),
9754 };
9755 handle_signature_help_request(&mut cx, mocked_response).await;
9756
9757 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9758 .await;
9759
9760 cx.editor(|editor, _, _| {
9761 let signature_help_state = editor.signature_help_state.popover().cloned();
9762 assert_eq!(
9763 signature_help_state.unwrap().label,
9764 "param1: u8, param2: u8"
9765 );
9766 });
9767}
9768
9769#[gpui::test]
9770async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9771 init_test(cx, |_| {});
9772
9773 cx.update(|cx| {
9774 cx.update_global::<SettingsStore, _>(|settings, cx| {
9775 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9776 settings.auto_signature_help = Some(false);
9777 settings.show_signature_help_after_edits = Some(false);
9778 });
9779 });
9780 });
9781
9782 let mut cx = EditorLspTestContext::new_rust(
9783 lsp::ServerCapabilities {
9784 signature_help_provider: Some(lsp::SignatureHelpOptions {
9785 ..Default::default()
9786 }),
9787 ..Default::default()
9788 },
9789 cx,
9790 )
9791 .await;
9792
9793 let language = Language::new(
9794 LanguageConfig {
9795 name: "Rust".into(),
9796 brackets: BracketPairConfig {
9797 pairs: vec![
9798 BracketPair {
9799 start: "{".to_string(),
9800 end: "}".to_string(),
9801 close: true,
9802 surround: true,
9803 newline: true,
9804 },
9805 BracketPair {
9806 start: "(".to_string(),
9807 end: ")".to_string(),
9808 close: true,
9809 surround: true,
9810 newline: true,
9811 },
9812 BracketPair {
9813 start: "/*".to_string(),
9814 end: " */".to_string(),
9815 close: true,
9816 surround: true,
9817 newline: true,
9818 },
9819 BracketPair {
9820 start: "[".to_string(),
9821 end: "]".to_string(),
9822 close: false,
9823 surround: false,
9824 newline: true,
9825 },
9826 BracketPair {
9827 start: "\"".to_string(),
9828 end: "\"".to_string(),
9829 close: true,
9830 surround: true,
9831 newline: false,
9832 },
9833 BracketPair {
9834 start: "<".to_string(),
9835 end: ">".to_string(),
9836 close: false,
9837 surround: true,
9838 newline: true,
9839 },
9840 ],
9841 ..Default::default()
9842 },
9843 autoclose_before: "})]".to_string(),
9844 ..Default::default()
9845 },
9846 Some(tree_sitter_rust::LANGUAGE.into()),
9847 );
9848 let language = Arc::new(language);
9849
9850 cx.language_registry().add(language.clone());
9851 cx.update_buffer(|buffer, cx| {
9852 buffer.set_language(Some(language), cx);
9853 });
9854
9855 // Ensure that signature_help is not called when no signature help is enabled.
9856 cx.set_state(
9857 &r#"
9858 fn main() {
9859 sampleˇ
9860 }
9861 "#
9862 .unindent(),
9863 );
9864 cx.update_editor(|editor, window, cx| {
9865 editor.handle_input("(", window, cx);
9866 });
9867 cx.assert_editor_state(
9868 &"
9869 fn main() {
9870 sample(ˇ)
9871 }
9872 "
9873 .unindent(),
9874 );
9875 cx.editor(|editor, _, _| {
9876 assert!(editor.signature_help_state.task().is_none());
9877 });
9878
9879 let mocked_response = lsp::SignatureHelp {
9880 signatures: vec![lsp::SignatureInformation {
9881 label: "fn sample(param1: u8, param2: u8)".to_string(),
9882 documentation: None,
9883 parameters: Some(vec![
9884 lsp::ParameterInformation {
9885 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9886 documentation: None,
9887 },
9888 lsp::ParameterInformation {
9889 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9890 documentation: None,
9891 },
9892 ]),
9893 active_parameter: None,
9894 }],
9895 active_signature: Some(0),
9896 active_parameter: Some(0),
9897 };
9898
9899 // Ensure that signature_help is called when enabled afte edits
9900 cx.update(|_, cx| {
9901 cx.update_global::<SettingsStore, _>(|settings, cx| {
9902 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9903 settings.auto_signature_help = Some(false);
9904 settings.show_signature_help_after_edits = Some(true);
9905 });
9906 });
9907 });
9908 cx.set_state(
9909 &r#"
9910 fn main() {
9911 sampleˇ
9912 }
9913 "#
9914 .unindent(),
9915 );
9916 cx.update_editor(|editor, window, cx| {
9917 editor.handle_input("(", window, cx);
9918 });
9919 cx.assert_editor_state(
9920 &"
9921 fn main() {
9922 sample(ˇ)
9923 }
9924 "
9925 .unindent(),
9926 );
9927 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9928 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9929 .await;
9930 cx.update_editor(|editor, _, _| {
9931 let signature_help_state = editor.signature_help_state.popover().cloned();
9932 assert!(signature_help_state.is_some());
9933 assert_eq!(
9934 signature_help_state.unwrap().label,
9935 "param1: u8, param2: u8"
9936 );
9937 editor.signature_help_state = SignatureHelpState::default();
9938 });
9939
9940 // Ensure that signature_help is called when auto signature help override is enabled
9941 cx.update(|_, cx| {
9942 cx.update_global::<SettingsStore, _>(|settings, cx| {
9943 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9944 settings.auto_signature_help = Some(true);
9945 settings.show_signature_help_after_edits = Some(false);
9946 });
9947 });
9948 });
9949 cx.set_state(
9950 &r#"
9951 fn main() {
9952 sampleˇ
9953 }
9954 "#
9955 .unindent(),
9956 );
9957 cx.update_editor(|editor, window, cx| {
9958 editor.handle_input("(", window, cx);
9959 });
9960 cx.assert_editor_state(
9961 &"
9962 fn main() {
9963 sample(ˇ)
9964 }
9965 "
9966 .unindent(),
9967 );
9968 handle_signature_help_request(&mut cx, mocked_response).await;
9969 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9970 .await;
9971 cx.editor(|editor, _, _| {
9972 let signature_help_state = editor.signature_help_state.popover().cloned();
9973 assert!(signature_help_state.is_some());
9974 assert_eq!(
9975 signature_help_state.unwrap().label,
9976 "param1: u8, param2: u8"
9977 );
9978 });
9979}
9980
9981#[gpui::test]
9982async fn test_signature_help(cx: &mut TestAppContext) {
9983 init_test(cx, |_| {});
9984 cx.update(|cx| {
9985 cx.update_global::<SettingsStore, _>(|settings, cx| {
9986 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9987 settings.auto_signature_help = Some(true);
9988 });
9989 });
9990 });
9991
9992 let mut cx = EditorLspTestContext::new_rust(
9993 lsp::ServerCapabilities {
9994 signature_help_provider: Some(lsp::SignatureHelpOptions {
9995 ..Default::default()
9996 }),
9997 ..Default::default()
9998 },
9999 cx,
10000 )
10001 .await;
10002
10003 // A test that directly calls `show_signature_help`
10004 cx.update_editor(|editor, window, cx| {
10005 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10006 });
10007
10008 let mocked_response = lsp::SignatureHelp {
10009 signatures: vec![lsp::SignatureInformation {
10010 label: "fn sample(param1: u8, param2: u8)".to_string(),
10011 documentation: None,
10012 parameters: Some(vec![
10013 lsp::ParameterInformation {
10014 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10015 documentation: None,
10016 },
10017 lsp::ParameterInformation {
10018 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10019 documentation: None,
10020 },
10021 ]),
10022 active_parameter: None,
10023 }],
10024 active_signature: Some(0),
10025 active_parameter: Some(0),
10026 };
10027 handle_signature_help_request(&mut cx, mocked_response).await;
10028
10029 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10030 .await;
10031
10032 cx.editor(|editor, _, _| {
10033 let signature_help_state = editor.signature_help_state.popover().cloned();
10034 assert!(signature_help_state.is_some());
10035 assert_eq!(
10036 signature_help_state.unwrap().label,
10037 "param1: u8, param2: u8"
10038 );
10039 });
10040
10041 // When exiting outside from inside the brackets, `signature_help` is closed.
10042 cx.set_state(indoc! {"
10043 fn main() {
10044 sample(ˇ);
10045 }
10046
10047 fn sample(param1: u8, param2: u8) {}
10048 "});
10049
10050 cx.update_editor(|editor, window, cx| {
10051 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10052 });
10053
10054 let mocked_response = lsp::SignatureHelp {
10055 signatures: Vec::new(),
10056 active_signature: None,
10057 active_parameter: None,
10058 };
10059 handle_signature_help_request(&mut cx, mocked_response).await;
10060
10061 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10062 .await;
10063
10064 cx.editor(|editor, _, _| {
10065 assert!(!editor.signature_help_state.is_shown());
10066 });
10067
10068 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10069 cx.set_state(indoc! {"
10070 fn main() {
10071 sample(ˇ);
10072 }
10073
10074 fn sample(param1: u8, param2: u8) {}
10075 "});
10076
10077 let mocked_response = lsp::SignatureHelp {
10078 signatures: vec![lsp::SignatureInformation {
10079 label: "fn sample(param1: u8, param2: u8)".to_string(),
10080 documentation: None,
10081 parameters: Some(vec![
10082 lsp::ParameterInformation {
10083 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10084 documentation: None,
10085 },
10086 lsp::ParameterInformation {
10087 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10088 documentation: None,
10089 },
10090 ]),
10091 active_parameter: None,
10092 }],
10093 active_signature: Some(0),
10094 active_parameter: Some(0),
10095 };
10096 handle_signature_help_request(&mut cx, mocked_response.clone()).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 // Restore the popover with more parameter input
10104 cx.set_state(indoc! {"
10105 fn main() {
10106 sample(param1, param2ˇ);
10107 }
10108
10109 fn sample(param1: u8, param2: u8) {}
10110 "});
10111
10112 let mocked_response = lsp::SignatureHelp {
10113 signatures: vec![lsp::SignatureInformation {
10114 label: "fn sample(param1: u8, param2: u8)".to_string(),
10115 documentation: None,
10116 parameters: Some(vec![
10117 lsp::ParameterInformation {
10118 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10119 documentation: None,
10120 },
10121 lsp::ParameterInformation {
10122 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10123 documentation: None,
10124 },
10125 ]),
10126 active_parameter: None,
10127 }],
10128 active_signature: Some(0),
10129 active_parameter: Some(1),
10130 };
10131 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10132 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10133 .await;
10134
10135 // When selecting a range, the popover is gone.
10136 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10137 cx.update_editor(|editor, window, cx| {
10138 editor.change_selections(None, window, cx, |s| {
10139 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10140 })
10141 });
10142 cx.assert_editor_state(indoc! {"
10143 fn main() {
10144 sample(param1, «ˇparam2»);
10145 }
10146
10147 fn sample(param1: u8, param2: u8) {}
10148 "});
10149 cx.editor(|editor, _, _| {
10150 assert!(!editor.signature_help_state.is_shown());
10151 });
10152
10153 // When unselecting again, the popover is back if within the brackets.
10154 cx.update_editor(|editor, window, cx| {
10155 editor.change_selections(None, window, cx, |s| {
10156 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10157 })
10158 });
10159 cx.assert_editor_state(indoc! {"
10160 fn main() {
10161 sample(param1, ˇparam2);
10162 }
10163
10164 fn sample(param1: u8, param2: u8) {}
10165 "});
10166 handle_signature_help_request(&mut cx, mocked_response).await;
10167 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10168 .await;
10169 cx.editor(|editor, _, _| {
10170 assert!(editor.signature_help_state.is_shown());
10171 });
10172
10173 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10174 cx.update_editor(|editor, window, cx| {
10175 editor.change_selections(None, window, cx, |s| {
10176 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10177 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10178 })
10179 });
10180 cx.assert_editor_state(indoc! {"
10181 fn main() {
10182 sample(param1, ˇparam2);
10183 }
10184
10185 fn sample(param1: u8, param2: u8) {}
10186 "});
10187
10188 let mocked_response = lsp::SignatureHelp {
10189 signatures: vec![lsp::SignatureInformation {
10190 label: "fn sample(param1: u8, param2: u8)".to_string(),
10191 documentation: None,
10192 parameters: Some(vec![
10193 lsp::ParameterInformation {
10194 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10195 documentation: None,
10196 },
10197 lsp::ParameterInformation {
10198 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10199 documentation: None,
10200 },
10201 ]),
10202 active_parameter: None,
10203 }],
10204 active_signature: Some(0),
10205 active_parameter: Some(1),
10206 };
10207 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10208 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10209 .await;
10210 cx.update_editor(|editor, _, cx| {
10211 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10212 });
10213 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10214 .await;
10215 cx.update_editor(|editor, window, cx| {
10216 editor.change_selections(None, window, cx, |s| {
10217 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10218 })
10219 });
10220 cx.assert_editor_state(indoc! {"
10221 fn main() {
10222 sample(param1, «ˇparam2»);
10223 }
10224
10225 fn sample(param1: u8, param2: u8) {}
10226 "});
10227 cx.update_editor(|editor, window, cx| {
10228 editor.change_selections(None, window, cx, |s| {
10229 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10230 })
10231 });
10232 cx.assert_editor_state(indoc! {"
10233 fn main() {
10234 sample(param1, ˇparam2);
10235 }
10236
10237 fn sample(param1: u8, param2: u8) {}
10238 "});
10239 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10240 .await;
10241}
10242
10243#[gpui::test]
10244async fn test_completion_mode(cx: &mut TestAppContext) {
10245 init_test(cx, |_| {});
10246 let mut cx = EditorLspTestContext::new_rust(
10247 lsp::ServerCapabilities {
10248 completion_provider: Some(lsp::CompletionOptions {
10249 resolve_provider: Some(true),
10250 ..Default::default()
10251 }),
10252 ..Default::default()
10253 },
10254 cx,
10255 )
10256 .await;
10257
10258 struct Run {
10259 run_description: &'static str,
10260 initial_state: String,
10261 buffer_marked_text: String,
10262 completion_text: &'static str,
10263 expected_with_insert_mode: String,
10264 expected_with_replace_mode: String,
10265 expected_with_replace_subsequence_mode: String,
10266 expected_with_replace_suffix_mode: String,
10267 }
10268
10269 let runs = [
10270 Run {
10271 run_description: "Start of word matches completion text",
10272 initial_state: "before ediˇ after".into(),
10273 buffer_marked_text: "before <edi|> after".into(),
10274 completion_text: "editor",
10275 expected_with_insert_mode: "before editorˇ after".into(),
10276 expected_with_replace_mode: "before editorˇ after".into(),
10277 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10278 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10279 },
10280 Run {
10281 run_description: "Accept same text at the middle of the word",
10282 initial_state: "before ediˇtor after".into(),
10283 buffer_marked_text: "before <edi|tor> after".into(),
10284 completion_text: "editor",
10285 expected_with_insert_mode: "before editorˇtor after".into(),
10286 expected_with_replace_mode: "before editorˇ after".into(),
10287 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10288 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10289 },
10290 Run {
10291 run_description: "End of word matches completion text -- cursor at end",
10292 initial_state: "before torˇ after".into(),
10293 buffer_marked_text: "before <tor|> after".into(),
10294 completion_text: "editor",
10295 expected_with_insert_mode: "before editorˇ after".into(),
10296 expected_with_replace_mode: "before editorˇ after".into(),
10297 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10298 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10299 },
10300 Run {
10301 run_description: "End of word matches completion text -- cursor at start",
10302 initial_state: "before ˇtor after".into(),
10303 buffer_marked_text: "before <|tor> after".into(),
10304 completion_text: "editor",
10305 expected_with_insert_mode: "before editorˇtor after".into(),
10306 expected_with_replace_mode: "before editorˇ after".into(),
10307 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10308 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10309 },
10310 Run {
10311 run_description: "Prepend text containing whitespace",
10312 initial_state: "pˇfield: bool".into(),
10313 buffer_marked_text: "<p|field>: bool".into(),
10314 completion_text: "pub ",
10315 expected_with_insert_mode: "pub ˇfield: bool".into(),
10316 expected_with_replace_mode: "pub ˇ: bool".into(),
10317 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10318 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10319 },
10320 Run {
10321 run_description: "Add element to start of list",
10322 initial_state: "[element_ˇelement_2]".into(),
10323 buffer_marked_text: "[<element_|element_2>]".into(),
10324 completion_text: "element_1",
10325 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10326 expected_with_replace_mode: "[element_1ˇ]".into(),
10327 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10328 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10329 },
10330 Run {
10331 run_description: "Add element to start of list -- first and second elements are equal",
10332 initial_state: "[elˇelement]".into(),
10333 buffer_marked_text: "[<el|element>]".into(),
10334 completion_text: "element",
10335 expected_with_insert_mode: "[elementˇelement]".into(),
10336 expected_with_replace_mode: "[elementˇ]".into(),
10337 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10338 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10339 },
10340 Run {
10341 run_description: "Ends with matching suffix",
10342 initial_state: "SubˇError".into(),
10343 buffer_marked_text: "<Sub|Error>".into(),
10344 completion_text: "SubscriptionError",
10345 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10346 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10347 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10348 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10349 },
10350 Run {
10351 run_description: "Suffix is a subsequence -- contiguous",
10352 initial_state: "SubˇErr".into(),
10353 buffer_marked_text: "<Sub|Err>".into(),
10354 completion_text: "SubscriptionError",
10355 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10356 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10357 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10358 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10359 },
10360 Run {
10361 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10362 initial_state: "Suˇscrirr".into(),
10363 buffer_marked_text: "<Su|scrirr>".into(),
10364 completion_text: "SubscriptionError",
10365 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10366 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10367 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10368 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10369 },
10370 Run {
10371 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10372 initial_state: "foo(indˇix)".into(),
10373 buffer_marked_text: "foo(<ind|ix>)".into(),
10374 completion_text: "node_index",
10375 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10376 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10377 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10378 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10379 },
10380 ];
10381
10382 for run in runs {
10383 let run_variations = [
10384 (LspInsertMode::Insert, run.expected_with_insert_mode),
10385 (LspInsertMode::Replace, run.expected_with_replace_mode),
10386 (
10387 LspInsertMode::ReplaceSubsequence,
10388 run.expected_with_replace_subsequence_mode,
10389 ),
10390 (
10391 LspInsertMode::ReplaceSuffix,
10392 run.expected_with_replace_suffix_mode,
10393 ),
10394 ];
10395
10396 for (lsp_insert_mode, expected_text) in run_variations {
10397 eprintln!(
10398 "run = {:?}, mode = {lsp_insert_mode:.?}",
10399 run.run_description,
10400 );
10401
10402 update_test_language_settings(&mut cx, |settings| {
10403 settings.defaults.completions = Some(CompletionSettings {
10404 lsp_insert_mode,
10405 words: WordsCompletionMode::Disabled,
10406 lsp: true,
10407 lsp_fetch_timeout_ms: 0,
10408 });
10409 });
10410
10411 cx.set_state(&run.initial_state);
10412 cx.update_editor(|editor, window, cx| {
10413 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10414 });
10415
10416 let counter = Arc::new(AtomicUsize::new(0));
10417 handle_completion_request_with_insert_and_replace(
10418 &mut cx,
10419 &run.buffer_marked_text,
10420 vec![run.completion_text],
10421 counter.clone(),
10422 )
10423 .await;
10424 cx.condition(|editor, _| editor.context_menu_visible())
10425 .await;
10426 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10427
10428 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10429 editor
10430 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10431 .unwrap()
10432 });
10433 cx.assert_editor_state(&expected_text);
10434 handle_resolve_completion_request(&mut cx, None).await;
10435 apply_additional_edits.await.unwrap();
10436 }
10437 }
10438}
10439
10440#[gpui::test]
10441async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10442 init_test(cx, |_| {});
10443 let mut cx = EditorLspTestContext::new_rust(
10444 lsp::ServerCapabilities {
10445 completion_provider: Some(lsp::CompletionOptions {
10446 resolve_provider: Some(true),
10447 ..Default::default()
10448 }),
10449 ..Default::default()
10450 },
10451 cx,
10452 )
10453 .await;
10454
10455 let initial_state = "SubˇError";
10456 let buffer_marked_text = "<Sub|Error>";
10457 let completion_text = "SubscriptionError";
10458 let expected_with_insert_mode = "SubscriptionErrorˇError";
10459 let expected_with_replace_mode = "SubscriptionErrorˇ";
10460
10461 update_test_language_settings(&mut cx, |settings| {
10462 settings.defaults.completions = Some(CompletionSettings {
10463 words: WordsCompletionMode::Disabled,
10464 // set the opposite here to ensure that the action is overriding the default behavior
10465 lsp_insert_mode: LspInsertMode::Insert,
10466 lsp: true,
10467 lsp_fetch_timeout_ms: 0,
10468 });
10469 });
10470
10471 cx.set_state(initial_state);
10472 cx.update_editor(|editor, window, cx| {
10473 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10474 });
10475
10476 let counter = Arc::new(AtomicUsize::new(0));
10477 handle_completion_request_with_insert_and_replace(
10478 &mut cx,
10479 &buffer_marked_text,
10480 vec![completion_text],
10481 counter.clone(),
10482 )
10483 .await;
10484 cx.condition(|editor, _| editor.context_menu_visible())
10485 .await;
10486 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10487
10488 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10489 editor
10490 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10491 .unwrap()
10492 });
10493 cx.assert_editor_state(&expected_with_replace_mode);
10494 handle_resolve_completion_request(&mut cx, None).await;
10495 apply_additional_edits.await.unwrap();
10496
10497 update_test_language_settings(&mut cx, |settings| {
10498 settings.defaults.completions = Some(CompletionSettings {
10499 words: WordsCompletionMode::Disabled,
10500 // set the opposite here to ensure that the action is overriding the default behavior
10501 lsp_insert_mode: LspInsertMode::Replace,
10502 lsp: true,
10503 lsp_fetch_timeout_ms: 0,
10504 });
10505 });
10506
10507 cx.set_state(initial_state);
10508 cx.update_editor(|editor, window, cx| {
10509 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10510 });
10511 handle_completion_request_with_insert_and_replace(
10512 &mut cx,
10513 &buffer_marked_text,
10514 vec![completion_text],
10515 counter.clone(),
10516 )
10517 .await;
10518 cx.condition(|editor, _| editor.context_menu_visible())
10519 .await;
10520 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10521
10522 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10523 editor
10524 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10525 .unwrap()
10526 });
10527 cx.assert_editor_state(&expected_with_insert_mode);
10528 handle_resolve_completion_request(&mut cx, None).await;
10529 apply_additional_edits.await.unwrap();
10530}
10531
10532#[gpui::test]
10533async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10534 init_test(cx, |_| {});
10535 let mut cx = EditorLspTestContext::new_rust(
10536 lsp::ServerCapabilities {
10537 completion_provider: Some(lsp::CompletionOptions {
10538 resolve_provider: Some(true),
10539 ..Default::default()
10540 }),
10541 ..Default::default()
10542 },
10543 cx,
10544 )
10545 .await;
10546
10547 // scenario: surrounding text matches completion text
10548 let completion_text = "to_offset";
10549 let initial_state = indoc! {"
10550 1. buf.to_offˇsuffix
10551 2. buf.to_offˇsuf
10552 3. buf.to_offˇfix
10553 4. buf.to_offˇ
10554 5. into_offˇensive
10555 6. ˇsuffix
10556 7. let ˇ //
10557 8. aaˇzz
10558 9. buf.to_off«zzzzzˇ»suffix
10559 10. buf.«ˇzzzzz»suffix
10560 11. to_off«ˇzzzzz»
10561
10562 buf.to_offˇsuffix // newest cursor
10563 "};
10564 let completion_marked_buffer = indoc! {"
10565 1. buf.to_offsuffix
10566 2. buf.to_offsuf
10567 3. buf.to_offfix
10568 4. buf.to_off
10569 5. into_offensive
10570 6. suffix
10571 7. let //
10572 8. aazz
10573 9. buf.to_offzzzzzsuffix
10574 10. buf.zzzzzsuffix
10575 11. to_offzzzzz
10576
10577 buf.<to_off|suffix> // newest cursor
10578 "};
10579 let expected = indoc! {"
10580 1. buf.to_offsetˇ
10581 2. buf.to_offsetˇsuf
10582 3. buf.to_offsetˇfix
10583 4. buf.to_offsetˇ
10584 5. into_offsetˇensive
10585 6. to_offsetˇsuffix
10586 7. let to_offsetˇ //
10587 8. aato_offsetˇzz
10588 9. buf.to_offsetˇ
10589 10. buf.to_offsetˇsuffix
10590 11. to_offsetˇ
10591
10592 buf.to_offsetˇ // newest cursor
10593 "};
10594 cx.set_state(initial_state);
10595 cx.update_editor(|editor, window, cx| {
10596 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10597 });
10598 handle_completion_request_with_insert_and_replace(
10599 &mut cx,
10600 completion_marked_buffer,
10601 vec![completion_text],
10602 Arc::new(AtomicUsize::new(0)),
10603 )
10604 .await;
10605 cx.condition(|editor, _| editor.context_menu_visible())
10606 .await;
10607 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10608 editor
10609 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10610 .unwrap()
10611 });
10612 cx.assert_editor_state(expected);
10613 handle_resolve_completion_request(&mut cx, None).await;
10614 apply_additional_edits.await.unwrap();
10615
10616 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10617 let completion_text = "foo_and_bar";
10618 let initial_state = indoc! {"
10619 1. ooanbˇ
10620 2. zooanbˇ
10621 3. ooanbˇz
10622 4. zooanbˇz
10623 5. ooanˇ
10624 6. oanbˇ
10625
10626 ooanbˇ
10627 "};
10628 let completion_marked_buffer = indoc! {"
10629 1. ooanb
10630 2. zooanb
10631 3. ooanbz
10632 4. zooanbz
10633 5. ooan
10634 6. oanb
10635
10636 <ooanb|>
10637 "};
10638 let expected = indoc! {"
10639 1. foo_and_barˇ
10640 2. zfoo_and_barˇ
10641 3. foo_and_barˇz
10642 4. zfoo_and_barˇz
10643 5. ooanfoo_and_barˇ
10644 6. oanbfoo_and_barˇ
10645
10646 foo_and_barˇ
10647 "};
10648 cx.set_state(initial_state);
10649 cx.update_editor(|editor, window, cx| {
10650 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10651 });
10652 handle_completion_request_with_insert_and_replace(
10653 &mut cx,
10654 completion_marked_buffer,
10655 vec![completion_text],
10656 Arc::new(AtomicUsize::new(0)),
10657 )
10658 .await;
10659 cx.condition(|editor, _| editor.context_menu_visible())
10660 .await;
10661 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10662 editor
10663 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10664 .unwrap()
10665 });
10666 cx.assert_editor_state(expected);
10667 handle_resolve_completion_request(&mut cx, None).await;
10668 apply_additional_edits.await.unwrap();
10669
10670 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10671 // (expects the same as if it was inserted at the end)
10672 let completion_text = "foo_and_bar";
10673 let initial_state = indoc! {"
10674 1. ooˇanb
10675 2. zooˇanb
10676 3. ooˇanbz
10677 4. zooˇanbz
10678
10679 ooˇanb
10680 "};
10681 let completion_marked_buffer = indoc! {"
10682 1. ooanb
10683 2. zooanb
10684 3. ooanbz
10685 4. zooanbz
10686
10687 <oo|anb>
10688 "};
10689 let expected = indoc! {"
10690 1. foo_and_barˇ
10691 2. zfoo_and_barˇ
10692 3. foo_and_barˇz
10693 4. zfoo_and_barˇz
10694
10695 foo_and_barˇ
10696 "};
10697 cx.set_state(initial_state);
10698 cx.update_editor(|editor, window, cx| {
10699 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10700 });
10701 handle_completion_request_with_insert_and_replace(
10702 &mut cx,
10703 completion_marked_buffer,
10704 vec![completion_text],
10705 Arc::new(AtomicUsize::new(0)),
10706 )
10707 .await;
10708 cx.condition(|editor, _| editor.context_menu_visible())
10709 .await;
10710 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10711 editor
10712 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10713 .unwrap()
10714 });
10715 cx.assert_editor_state(expected);
10716 handle_resolve_completion_request(&mut cx, None).await;
10717 apply_additional_edits.await.unwrap();
10718}
10719
10720// This used to crash
10721#[gpui::test]
10722async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10723 init_test(cx, |_| {});
10724
10725 let buffer_text = indoc! {"
10726 fn main() {
10727 10.satu;
10728
10729 //
10730 // separate cursors so they open in different excerpts (manually reproducible)
10731 //
10732
10733 10.satu20;
10734 }
10735 "};
10736 let multibuffer_text_with_selections = indoc! {"
10737 fn main() {
10738 10.satuˇ;
10739
10740 //
10741
10742 //
10743
10744 10.satuˇ20;
10745 }
10746 "};
10747 let expected_multibuffer = indoc! {"
10748 fn main() {
10749 10.saturating_sub()ˇ;
10750
10751 //
10752
10753 //
10754
10755 10.saturating_sub()ˇ;
10756 }
10757 "};
10758
10759 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10760 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10761
10762 let fs = FakeFs::new(cx.executor());
10763 fs.insert_tree(
10764 path!("/a"),
10765 json!({
10766 "main.rs": buffer_text,
10767 }),
10768 )
10769 .await;
10770
10771 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10772 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10773 language_registry.add(rust_lang());
10774 let mut fake_servers = language_registry.register_fake_lsp(
10775 "Rust",
10776 FakeLspAdapter {
10777 capabilities: lsp::ServerCapabilities {
10778 completion_provider: Some(lsp::CompletionOptions {
10779 resolve_provider: None,
10780 ..lsp::CompletionOptions::default()
10781 }),
10782 ..lsp::ServerCapabilities::default()
10783 },
10784 ..FakeLspAdapter::default()
10785 },
10786 );
10787 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10788 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10789 let buffer = project
10790 .update(cx, |project, cx| {
10791 project.open_local_buffer(path!("/a/main.rs"), cx)
10792 })
10793 .await
10794 .unwrap();
10795
10796 let multi_buffer = cx.new(|cx| {
10797 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10798 multi_buffer.push_excerpts(
10799 buffer.clone(),
10800 [ExcerptRange::new(0..first_excerpt_end)],
10801 cx,
10802 );
10803 multi_buffer.push_excerpts(
10804 buffer.clone(),
10805 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10806 cx,
10807 );
10808 multi_buffer
10809 });
10810
10811 let editor = workspace
10812 .update(cx, |_, window, cx| {
10813 cx.new(|cx| {
10814 Editor::new(
10815 EditorMode::Full {
10816 scale_ui_elements_with_buffer_font_size: false,
10817 show_active_line_background: false,
10818 sized_by_content: false,
10819 },
10820 multi_buffer.clone(),
10821 Some(project.clone()),
10822 window,
10823 cx,
10824 )
10825 })
10826 })
10827 .unwrap();
10828
10829 let pane = workspace
10830 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10831 .unwrap();
10832 pane.update_in(cx, |pane, window, cx| {
10833 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10834 });
10835
10836 let fake_server = fake_servers.next().await.unwrap();
10837
10838 editor.update_in(cx, |editor, window, cx| {
10839 editor.change_selections(None, window, cx, |s| {
10840 s.select_ranges([
10841 Point::new(1, 11)..Point::new(1, 11),
10842 Point::new(7, 11)..Point::new(7, 11),
10843 ])
10844 });
10845
10846 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10847 });
10848
10849 editor.update_in(cx, |editor, window, cx| {
10850 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10851 });
10852
10853 fake_server
10854 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10855 let completion_item = lsp::CompletionItem {
10856 label: "saturating_sub()".into(),
10857 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10858 lsp::InsertReplaceEdit {
10859 new_text: "saturating_sub()".to_owned(),
10860 insert: lsp::Range::new(
10861 lsp::Position::new(7, 7),
10862 lsp::Position::new(7, 11),
10863 ),
10864 replace: lsp::Range::new(
10865 lsp::Position::new(7, 7),
10866 lsp::Position::new(7, 13),
10867 ),
10868 },
10869 )),
10870 ..lsp::CompletionItem::default()
10871 };
10872
10873 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10874 })
10875 .next()
10876 .await
10877 .unwrap();
10878
10879 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10880 .await;
10881
10882 editor
10883 .update_in(cx, |editor, window, cx| {
10884 editor
10885 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10886 .unwrap()
10887 })
10888 .await
10889 .unwrap();
10890
10891 editor.update(cx, |editor, cx| {
10892 assert_text_with_selections(editor, expected_multibuffer, cx);
10893 })
10894}
10895
10896#[gpui::test]
10897async fn test_completion(cx: &mut TestAppContext) {
10898 init_test(cx, |_| {});
10899
10900 let mut cx = EditorLspTestContext::new_rust(
10901 lsp::ServerCapabilities {
10902 completion_provider: Some(lsp::CompletionOptions {
10903 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10904 resolve_provider: Some(true),
10905 ..Default::default()
10906 }),
10907 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10908 ..Default::default()
10909 },
10910 cx,
10911 )
10912 .await;
10913 let counter = Arc::new(AtomicUsize::new(0));
10914
10915 cx.set_state(indoc! {"
10916 oneˇ
10917 two
10918 three
10919 "});
10920 cx.simulate_keystroke(".");
10921 handle_completion_request(
10922 &mut cx,
10923 indoc! {"
10924 one.|<>
10925 two
10926 three
10927 "},
10928 vec!["first_completion", "second_completion"],
10929 counter.clone(),
10930 )
10931 .await;
10932 cx.condition(|editor, _| editor.context_menu_visible())
10933 .await;
10934 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10935
10936 let _handler = handle_signature_help_request(
10937 &mut cx,
10938 lsp::SignatureHelp {
10939 signatures: vec![lsp::SignatureInformation {
10940 label: "test signature".to_string(),
10941 documentation: None,
10942 parameters: Some(vec![lsp::ParameterInformation {
10943 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10944 documentation: None,
10945 }]),
10946 active_parameter: None,
10947 }],
10948 active_signature: None,
10949 active_parameter: None,
10950 },
10951 );
10952 cx.update_editor(|editor, window, cx| {
10953 assert!(
10954 !editor.signature_help_state.is_shown(),
10955 "No signature help was called for"
10956 );
10957 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10958 });
10959 cx.run_until_parked();
10960 cx.update_editor(|editor, _, _| {
10961 assert!(
10962 !editor.signature_help_state.is_shown(),
10963 "No signature help should be shown when completions menu is open"
10964 );
10965 });
10966
10967 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10968 editor.context_menu_next(&Default::default(), window, cx);
10969 editor
10970 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10971 .unwrap()
10972 });
10973 cx.assert_editor_state(indoc! {"
10974 one.second_completionˇ
10975 two
10976 three
10977 "});
10978
10979 handle_resolve_completion_request(
10980 &mut cx,
10981 Some(vec![
10982 (
10983 //This overlaps with the primary completion edit which is
10984 //misbehavior from the LSP spec, test that we filter it out
10985 indoc! {"
10986 one.second_ˇcompletion
10987 two
10988 threeˇ
10989 "},
10990 "overlapping additional edit",
10991 ),
10992 (
10993 indoc! {"
10994 one.second_completion
10995 two
10996 threeˇ
10997 "},
10998 "\nadditional edit",
10999 ),
11000 ]),
11001 )
11002 .await;
11003 apply_additional_edits.await.unwrap();
11004 cx.assert_editor_state(indoc! {"
11005 one.second_completionˇ
11006 two
11007 three
11008 additional edit
11009 "});
11010
11011 cx.set_state(indoc! {"
11012 one.second_completion
11013 twoˇ
11014 threeˇ
11015 additional edit
11016 "});
11017 cx.simulate_keystroke(" ");
11018 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11019 cx.simulate_keystroke("s");
11020 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11021
11022 cx.assert_editor_state(indoc! {"
11023 one.second_completion
11024 two sˇ
11025 three sˇ
11026 additional edit
11027 "});
11028 handle_completion_request(
11029 &mut cx,
11030 indoc! {"
11031 one.second_completion
11032 two s
11033 three <s|>
11034 additional edit
11035 "},
11036 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11037 counter.clone(),
11038 )
11039 .await;
11040 cx.condition(|editor, _| editor.context_menu_visible())
11041 .await;
11042 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11043
11044 cx.simulate_keystroke("i");
11045
11046 handle_completion_request(
11047 &mut cx,
11048 indoc! {"
11049 one.second_completion
11050 two si
11051 three <si|>
11052 additional edit
11053 "},
11054 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11055 counter.clone(),
11056 )
11057 .await;
11058 cx.condition(|editor, _| editor.context_menu_visible())
11059 .await;
11060 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11061
11062 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11063 editor
11064 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11065 .unwrap()
11066 });
11067 cx.assert_editor_state(indoc! {"
11068 one.second_completion
11069 two sixth_completionˇ
11070 three sixth_completionˇ
11071 additional edit
11072 "});
11073
11074 apply_additional_edits.await.unwrap();
11075
11076 update_test_language_settings(&mut cx, |settings| {
11077 settings.defaults.show_completions_on_input = Some(false);
11078 });
11079 cx.set_state("editorˇ");
11080 cx.simulate_keystroke(".");
11081 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11082 cx.simulate_keystrokes("c l o");
11083 cx.assert_editor_state("editor.cloˇ");
11084 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11085 cx.update_editor(|editor, window, cx| {
11086 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11087 });
11088 handle_completion_request(
11089 &mut cx,
11090 "editor.<clo|>",
11091 vec!["close", "clobber"],
11092 counter.clone(),
11093 )
11094 .await;
11095 cx.condition(|editor, _| editor.context_menu_visible())
11096 .await;
11097 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11098
11099 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11100 editor
11101 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11102 .unwrap()
11103 });
11104 cx.assert_editor_state("editor.closeˇ");
11105 handle_resolve_completion_request(&mut cx, None).await;
11106 apply_additional_edits.await.unwrap();
11107}
11108
11109#[gpui::test]
11110async fn test_word_completion(cx: &mut TestAppContext) {
11111 let lsp_fetch_timeout_ms = 10;
11112 init_test(cx, |language_settings| {
11113 language_settings.defaults.completions = Some(CompletionSettings {
11114 words: WordsCompletionMode::Fallback,
11115 lsp: true,
11116 lsp_fetch_timeout_ms: 10,
11117 lsp_insert_mode: LspInsertMode::Insert,
11118 });
11119 });
11120
11121 let mut cx = EditorLspTestContext::new_rust(
11122 lsp::ServerCapabilities {
11123 completion_provider: Some(lsp::CompletionOptions {
11124 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11125 ..lsp::CompletionOptions::default()
11126 }),
11127 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11128 ..lsp::ServerCapabilities::default()
11129 },
11130 cx,
11131 )
11132 .await;
11133
11134 let throttle_completions = Arc::new(AtomicBool::new(false));
11135
11136 let lsp_throttle_completions = throttle_completions.clone();
11137 let _completion_requests_handler =
11138 cx.lsp
11139 .server
11140 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11141 let lsp_throttle_completions = lsp_throttle_completions.clone();
11142 let cx = cx.clone();
11143 async move {
11144 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11145 cx.background_executor()
11146 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11147 .await;
11148 }
11149 Ok(Some(lsp::CompletionResponse::Array(vec![
11150 lsp::CompletionItem {
11151 label: "first".into(),
11152 ..lsp::CompletionItem::default()
11153 },
11154 lsp::CompletionItem {
11155 label: "last".into(),
11156 ..lsp::CompletionItem::default()
11157 },
11158 ])))
11159 }
11160 });
11161
11162 cx.set_state(indoc! {"
11163 oneˇ
11164 two
11165 three
11166 "});
11167 cx.simulate_keystroke(".");
11168 cx.executor().run_until_parked();
11169 cx.condition(|editor, _| editor.context_menu_visible())
11170 .await;
11171 cx.update_editor(|editor, window, cx| {
11172 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11173 {
11174 assert_eq!(
11175 completion_menu_entries(&menu),
11176 &["first", "last"],
11177 "When LSP server is fast to reply, no fallback word completions are used"
11178 );
11179 } else {
11180 panic!("expected completion menu to be open");
11181 }
11182 editor.cancel(&Cancel, window, cx);
11183 });
11184 cx.executor().run_until_parked();
11185 cx.condition(|editor, _| !editor.context_menu_visible())
11186 .await;
11187
11188 throttle_completions.store(true, atomic::Ordering::Release);
11189 cx.simulate_keystroke(".");
11190 cx.executor()
11191 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11192 cx.executor().run_until_parked();
11193 cx.condition(|editor, _| editor.context_menu_visible())
11194 .await;
11195 cx.update_editor(|editor, _, _| {
11196 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11197 {
11198 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11199 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11200 } else {
11201 panic!("expected completion menu to be open");
11202 }
11203 });
11204}
11205
11206#[gpui::test]
11207async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11208 init_test(cx, |language_settings| {
11209 language_settings.defaults.completions = Some(CompletionSettings {
11210 words: WordsCompletionMode::Enabled,
11211 lsp: true,
11212 lsp_fetch_timeout_ms: 0,
11213 lsp_insert_mode: LspInsertMode::Insert,
11214 });
11215 });
11216
11217 let mut cx = EditorLspTestContext::new_rust(
11218 lsp::ServerCapabilities {
11219 completion_provider: Some(lsp::CompletionOptions {
11220 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11221 ..lsp::CompletionOptions::default()
11222 }),
11223 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11224 ..lsp::ServerCapabilities::default()
11225 },
11226 cx,
11227 )
11228 .await;
11229
11230 let _completion_requests_handler =
11231 cx.lsp
11232 .server
11233 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11234 Ok(Some(lsp::CompletionResponse::Array(vec![
11235 lsp::CompletionItem {
11236 label: "first".into(),
11237 ..lsp::CompletionItem::default()
11238 },
11239 lsp::CompletionItem {
11240 label: "last".into(),
11241 ..lsp::CompletionItem::default()
11242 },
11243 ])))
11244 });
11245
11246 cx.set_state(indoc! {"ˇ
11247 first
11248 last
11249 second
11250 "});
11251 cx.simulate_keystroke(".");
11252 cx.executor().run_until_parked();
11253 cx.condition(|editor, _| editor.context_menu_visible())
11254 .await;
11255 cx.update_editor(|editor, _, _| {
11256 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11257 {
11258 assert_eq!(
11259 completion_menu_entries(&menu),
11260 &["first", "last", "second"],
11261 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11262 );
11263 } else {
11264 panic!("expected completion menu to be open");
11265 }
11266 });
11267}
11268
11269#[gpui::test]
11270async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11271 init_test(cx, |language_settings| {
11272 language_settings.defaults.completions = Some(CompletionSettings {
11273 words: WordsCompletionMode::Disabled,
11274 lsp: true,
11275 lsp_fetch_timeout_ms: 0,
11276 lsp_insert_mode: LspInsertMode::Insert,
11277 });
11278 });
11279
11280 let mut cx = EditorLspTestContext::new_rust(
11281 lsp::ServerCapabilities {
11282 completion_provider: Some(lsp::CompletionOptions {
11283 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11284 ..lsp::CompletionOptions::default()
11285 }),
11286 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11287 ..lsp::ServerCapabilities::default()
11288 },
11289 cx,
11290 )
11291 .await;
11292
11293 let _completion_requests_handler =
11294 cx.lsp
11295 .server
11296 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11297 panic!("LSP completions should not be queried when dealing with word completions")
11298 });
11299
11300 cx.set_state(indoc! {"ˇ
11301 first
11302 last
11303 second
11304 "});
11305 cx.update_editor(|editor, window, cx| {
11306 editor.show_word_completions(&ShowWordCompletions, window, cx);
11307 });
11308 cx.executor().run_until_parked();
11309 cx.condition(|editor, _| editor.context_menu_visible())
11310 .await;
11311 cx.update_editor(|editor, _, _| {
11312 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11313 {
11314 assert_eq!(
11315 completion_menu_entries(&menu),
11316 &["first", "last", "second"],
11317 "`ShowWordCompletions` action should show word completions"
11318 );
11319 } else {
11320 panic!("expected completion menu to be open");
11321 }
11322 });
11323
11324 cx.simulate_keystroke("l");
11325 cx.executor().run_until_parked();
11326 cx.condition(|editor, _| editor.context_menu_visible())
11327 .await;
11328 cx.update_editor(|editor, _, _| {
11329 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11330 {
11331 assert_eq!(
11332 completion_menu_entries(&menu),
11333 &["last"],
11334 "After showing word completions, further editing should filter them and not query the LSP"
11335 );
11336 } else {
11337 panic!("expected completion menu to be open");
11338 }
11339 });
11340}
11341
11342#[gpui::test]
11343async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11344 init_test(cx, |language_settings| {
11345 language_settings.defaults.completions = Some(CompletionSettings {
11346 words: WordsCompletionMode::Fallback,
11347 lsp: false,
11348 lsp_fetch_timeout_ms: 0,
11349 lsp_insert_mode: LspInsertMode::Insert,
11350 });
11351 });
11352
11353 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11354
11355 cx.set_state(indoc! {"ˇ
11356 0_usize
11357 let
11358 33
11359 4.5f32
11360 "});
11361 cx.update_editor(|editor, window, cx| {
11362 editor.show_completions(&ShowCompletions::default(), window, cx);
11363 });
11364 cx.executor().run_until_parked();
11365 cx.condition(|editor, _| editor.context_menu_visible())
11366 .await;
11367 cx.update_editor(|editor, window, cx| {
11368 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11369 {
11370 assert_eq!(
11371 completion_menu_entries(&menu),
11372 &["let"],
11373 "With no digits in the completion query, no digits should be in the word completions"
11374 );
11375 } else {
11376 panic!("expected completion menu to be open");
11377 }
11378 editor.cancel(&Cancel, window, cx);
11379 });
11380
11381 cx.set_state(indoc! {"3ˇ
11382 0_usize
11383 let
11384 3
11385 33.35f32
11386 "});
11387 cx.update_editor(|editor, window, cx| {
11388 editor.show_completions(&ShowCompletions::default(), window, cx);
11389 });
11390 cx.executor().run_until_parked();
11391 cx.condition(|editor, _| editor.context_menu_visible())
11392 .await;
11393 cx.update_editor(|editor, _, _| {
11394 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11395 {
11396 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11397 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11398 } else {
11399 panic!("expected completion menu to be open");
11400 }
11401 });
11402}
11403
11404fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11405 let position = || lsp::Position {
11406 line: params.text_document_position.position.line,
11407 character: params.text_document_position.position.character,
11408 };
11409 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11410 range: lsp::Range {
11411 start: position(),
11412 end: position(),
11413 },
11414 new_text: text.to_string(),
11415 }))
11416}
11417
11418#[gpui::test]
11419async fn test_multiline_completion(cx: &mut TestAppContext) {
11420 init_test(cx, |_| {});
11421
11422 let fs = FakeFs::new(cx.executor());
11423 fs.insert_tree(
11424 path!("/a"),
11425 json!({
11426 "main.ts": "a",
11427 }),
11428 )
11429 .await;
11430
11431 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11432 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11433 let typescript_language = Arc::new(Language::new(
11434 LanguageConfig {
11435 name: "TypeScript".into(),
11436 matcher: LanguageMatcher {
11437 path_suffixes: vec!["ts".to_string()],
11438 ..LanguageMatcher::default()
11439 },
11440 line_comments: vec!["// ".into()],
11441 ..LanguageConfig::default()
11442 },
11443 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11444 ));
11445 language_registry.add(typescript_language.clone());
11446 let mut fake_servers = language_registry.register_fake_lsp(
11447 "TypeScript",
11448 FakeLspAdapter {
11449 capabilities: lsp::ServerCapabilities {
11450 completion_provider: Some(lsp::CompletionOptions {
11451 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11452 ..lsp::CompletionOptions::default()
11453 }),
11454 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11455 ..lsp::ServerCapabilities::default()
11456 },
11457 // Emulate vtsls label generation
11458 label_for_completion: Some(Box::new(|item, _| {
11459 let text = if let Some(description) = item
11460 .label_details
11461 .as_ref()
11462 .and_then(|label_details| label_details.description.as_ref())
11463 {
11464 format!("{} {}", item.label, description)
11465 } else if let Some(detail) = &item.detail {
11466 format!("{} {}", item.label, detail)
11467 } else {
11468 item.label.clone()
11469 };
11470 let len = text.len();
11471 Some(language::CodeLabel {
11472 text,
11473 runs: Vec::new(),
11474 filter_range: 0..len,
11475 })
11476 })),
11477 ..FakeLspAdapter::default()
11478 },
11479 );
11480 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11481 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11482 let worktree_id = workspace
11483 .update(cx, |workspace, _window, cx| {
11484 workspace.project().update(cx, |project, cx| {
11485 project.worktrees(cx).next().unwrap().read(cx).id()
11486 })
11487 })
11488 .unwrap();
11489 let _buffer = project
11490 .update(cx, |project, cx| {
11491 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11492 })
11493 .await
11494 .unwrap();
11495 let editor = workspace
11496 .update(cx, |workspace, window, cx| {
11497 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11498 })
11499 .unwrap()
11500 .await
11501 .unwrap()
11502 .downcast::<Editor>()
11503 .unwrap();
11504 let fake_server = fake_servers.next().await.unwrap();
11505
11506 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11507 let multiline_label_2 = "a\nb\nc\n";
11508 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11509 let multiline_description = "d\ne\nf\n";
11510 let multiline_detail_2 = "g\nh\ni\n";
11511
11512 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11513 move |params, _| async move {
11514 Ok(Some(lsp::CompletionResponse::Array(vec![
11515 lsp::CompletionItem {
11516 label: multiline_label.to_string(),
11517 text_edit: gen_text_edit(¶ms, "new_text_1"),
11518 ..lsp::CompletionItem::default()
11519 },
11520 lsp::CompletionItem {
11521 label: "single line label 1".to_string(),
11522 detail: Some(multiline_detail.to_string()),
11523 text_edit: gen_text_edit(¶ms, "new_text_2"),
11524 ..lsp::CompletionItem::default()
11525 },
11526 lsp::CompletionItem {
11527 label: "single line label 2".to_string(),
11528 label_details: Some(lsp::CompletionItemLabelDetails {
11529 description: Some(multiline_description.to_string()),
11530 detail: None,
11531 }),
11532 text_edit: gen_text_edit(¶ms, "new_text_2"),
11533 ..lsp::CompletionItem::default()
11534 },
11535 lsp::CompletionItem {
11536 label: multiline_label_2.to_string(),
11537 detail: Some(multiline_detail_2.to_string()),
11538 text_edit: gen_text_edit(¶ms, "new_text_3"),
11539 ..lsp::CompletionItem::default()
11540 },
11541 lsp::CompletionItem {
11542 label: "Label with many spaces and \t but without newlines".to_string(),
11543 detail: Some(
11544 "Details with many spaces and \t but without newlines".to_string(),
11545 ),
11546 text_edit: gen_text_edit(¶ms, "new_text_4"),
11547 ..lsp::CompletionItem::default()
11548 },
11549 ])))
11550 },
11551 );
11552
11553 editor.update_in(cx, |editor, window, cx| {
11554 cx.focus_self(window);
11555 editor.move_to_end(&MoveToEnd, window, cx);
11556 editor.handle_input(".", window, cx);
11557 });
11558 cx.run_until_parked();
11559 completion_handle.next().await.unwrap();
11560
11561 editor.update(cx, |editor, _| {
11562 assert!(editor.context_menu_visible());
11563 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11564 {
11565 let completion_labels = menu
11566 .completions
11567 .borrow()
11568 .iter()
11569 .map(|c| c.label.text.clone())
11570 .collect::<Vec<_>>();
11571 assert_eq!(
11572 completion_labels,
11573 &[
11574 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11575 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11576 "single line label 2 d e f ",
11577 "a b c g h i ",
11578 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11579 ],
11580 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11581 );
11582
11583 for completion in menu
11584 .completions
11585 .borrow()
11586 .iter() {
11587 assert_eq!(
11588 completion.label.filter_range,
11589 0..completion.label.text.len(),
11590 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11591 );
11592 }
11593 } else {
11594 panic!("expected completion menu to be open");
11595 }
11596 });
11597}
11598
11599#[gpui::test]
11600async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11601 init_test(cx, |_| {});
11602 let mut cx = EditorLspTestContext::new_rust(
11603 lsp::ServerCapabilities {
11604 completion_provider: Some(lsp::CompletionOptions {
11605 trigger_characters: Some(vec![".".to_string()]),
11606 ..Default::default()
11607 }),
11608 ..Default::default()
11609 },
11610 cx,
11611 )
11612 .await;
11613 cx.lsp
11614 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11615 Ok(Some(lsp::CompletionResponse::Array(vec![
11616 lsp::CompletionItem {
11617 label: "first".into(),
11618 ..Default::default()
11619 },
11620 lsp::CompletionItem {
11621 label: "last".into(),
11622 ..Default::default()
11623 },
11624 ])))
11625 });
11626 cx.set_state("variableˇ");
11627 cx.simulate_keystroke(".");
11628 cx.executor().run_until_parked();
11629
11630 cx.update_editor(|editor, _, _| {
11631 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11632 {
11633 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11634 } else {
11635 panic!("expected completion menu to be open");
11636 }
11637 });
11638
11639 cx.update_editor(|editor, window, cx| {
11640 editor.move_page_down(&MovePageDown::default(), window, cx);
11641 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11642 {
11643 assert!(
11644 menu.selected_item == 1,
11645 "expected PageDown to select the last item from the context menu"
11646 );
11647 } else {
11648 panic!("expected completion menu to stay open after PageDown");
11649 }
11650 });
11651
11652 cx.update_editor(|editor, window, cx| {
11653 editor.move_page_up(&MovePageUp::default(), window, cx);
11654 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11655 {
11656 assert!(
11657 menu.selected_item == 0,
11658 "expected PageUp to select the first item from the context menu"
11659 );
11660 } else {
11661 panic!("expected completion menu to stay open after PageUp");
11662 }
11663 });
11664}
11665
11666#[gpui::test]
11667async fn test_as_is_completions(cx: &mut TestAppContext) {
11668 init_test(cx, |_| {});
11669 let mut cx = EditorLspTestContext::new_rust(
11670 lsp::ServerCapabilities {
11671 completion_provider: Some(lsp::CompletionOptions {
11672 ..Default::default()
11673 }),
11674 ..Default::default()
11675 },
11676 cx,
11677 )
11678 .await;
11679 cx.lsp
11680 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11681 Ok(Some(lsp::CompletionResponse::Array(vec![
11682 lsp::CompletionItem {
11683 label: "unsafe".into(),
11684 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11685 range: lsp::Range {
11686 start: lsp::Position {
11687 line: 1,
11688 character: 2,
11689 },
11690 end: lsp::Position {
11691 line: 1,
11692 character: 3,
11693 },
11694 },
11695 new_text: "unsafe".to_string(),
11696 })),
11697 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11698 ..Default::default()
11699 },
11700 ])))
11701 });
11702 cx.set_state("fn a() {}\n nˇ");
11703 cx.executor().run_until_parked();
11704 cx.update_editor(|editor, window, cx| {
11705 editor.show_completions(
11706 &ShowCompletions {
11707 trigger: Some("\n".into()),
11708 },
11709 window,
11710 cx,
11711 );
11712 });
11713 cx.executor().run_until_parked();
11714
11715 cx.update_editor(|editor, window, cx| {
11716 editor.confirm_completion(&Default::default(), window, cx)
11717 });
11718 cx.executor().run_until_parked();
11719 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11720}
11721
11722#[gpui::test]
11723async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11724 init_test(cx, |_| {});
11725
11726 let mut cx = EditorLspTestContext::new_rust(
11727 lsp::ServerCapabilities {
11728 completion_provider: Some(lsp::CompletionOptions {
11729 trigger_characters: Some(vec![".".to_string()]),
11730 resolve_provider: Some(true),
11731 ..Default::default()
11732 }),
11733 ..Default::default()
11734 },
11735 cx,
11736 )
11737 .await;
11738
11739 cx.set_state("fn main() { let a = 2ˇ; }");
11740 cx.simulate_keystroke(".");
11741 let completion_item = lsp::CompletionItem {
11742 label: "Some".into(),
11743 kind: Some(lsp::CompletionItemKind::SNIPPET),
11744 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11745 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11746 kind: lsp::MarkupKind::Markdown,
11747 value: "```rust\nSome(2)\n```".to_string(),
11748 })),
11749 deprecated: Some(false),
11750 sort_text: Some("Some".to_string()),
11751 filter_text: Some("Some".to_string()),
11752 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11753 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11754 range: lsp::Range {
11755 start: lsp::Position {
11756 line: 0,
11757 character: 22,
11758 },
11759 end: lsp::Position {
11760 line: 0,
11761 character: 22,
11762 },
11763 },
11764 new_text: "Some(2)".to_string(),
11765 })),
11766 additional_text_edits: Some(vec![lsp::TextEdit {
11767 range: lsp::Range {
11768 start: lsp::Position {
11769 line: 0,
11770 character: 20,
11771 },
11772 end: lsp::Position {
11773 line: 0,
11774 character: 22,
11775 },
11776 },
11777 new_text: "".to_string(),
11778 }]),
11779 ..Default::default()
11780 };
11781
11782 let closure_completion_item = completion_item.clone();
11783 let counter = Arc::new(AtomicUsize::new(0));
11784 let counter_clone = counter.clone();
11785 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11786 let task_completion_item = closure_completion_item.clone();
11787 counter_clone.fetch_add(1, atomic::Ordering::Release);
11788 async move {
11789 Ok(Some(lsp::CompletionResponse::Array(vec![
11790 task_completion_item,
11791 ])))
11792 }
11793 });
11794
11795 cx.condition(|editor, _| editor.context_menu_visible())
11796 .await;
11797 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11798 assert!(request.next().await.is_some());
11799 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11800
11801 cx.simulate_keystrokes("S o m");
11802 cx.condition(|editor, _| editor.context_menu_visible())
11803 .await;
11804 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11805 assert!(request.next().await.is_some());
11806 assert!(request.next().await.is_some());
11807 assert!(request.next().await.is_some());
11808 request.close();
11809 assert!(request.next().await.is_none());
11810 assert_eq!(
11811 counter.load(atomic::Ordering::Acquire),
11812 4,
11813 "With the completions menu open, only one LSP request should happen per input"
11814 );
11815}
11816
11817#[gpui::test]
11818async fn test_toggle_comment(cx: &mut TestAppContext) {
11819 init_test(cx, |_| {});
11820 let mut cx = EditorTestContext::new(cx).await;
11821 let language = Arc::new(Language::new(
11822 LanguageConfig {
11823 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11824 ..Default::default()
11825 },
11826 Some(tree_sitter_rust::LANGUAGE.into()),
11827 ));
11828 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11829
11830 // If multiple selections intersect a line, the line is only toggled once.
11831 cx.set_state(indoc! {"
11832 fn a() {
11833 «//b();
11834 ˇ»// «c();
11835 //ˇ» d();
11836 }
11837 "});
11838
11839 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11840
11841 cx.assert_editor_state(indoc! {"
11842 fn a() {
11843 «b();
11844 c();
11845 ˇ» d();
11846 }
11847 "});
11848
11849 // The comment prefix is inserted at the same column for every line in a
11850 // selection.
11851 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11852
11853 cx.assert_editor_state(indoc! {"
11854 fn a() {
11855 // «b();
11856 // c();
11857 ˇ»// d();
11858 }
11859 "});
11860
11861 // If a selection ends at the beginning of a line, that line is not toggled.
11862 cx.set_selections_state(indoc! {"
11863 fn a() {
11864 // b();
11865 «// c();
11866 ˇ» // d();
11867 }
11868 "});
11869
11870 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11871
11872 cx.assert_editor_state(indoc! {"
11873 fn a() {
11874 // b();
11875 «c();
11876 ˇ» // d();
11877 }
11878 "});
11879
11880 // If a selection span a single line and is empty, the line is toggled.
11881 cx.set_state(indoc! {"
11882 fn a() {
11883 a();
11884 b();
11885 ˇ
11886 }
11887 "});
11888
11889 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11890
11891 cx.assert_editor_state(indoc! {"
11892 fn a() {
11893 a();
11894 b();
11895 //•ˇ
11896 }
11897 "});
11898
11899 // If a selection span multiple lines, empty lines are not toggled.
11900 cx.set_state(indoc! {"
11901 fn a() {
11902 «a();
11903
11904 c();ˇ»
11905 }
11906 "});
11907
11908 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11909
11910 cx.assert_editor_state(indoc! {"
11911 fn a() {
11912 // «a();
11913
11914 // c();ˇ»
11915 }
11916 "});
11917
11918 // If a selection includes multiple comment prefixes, all lines are uncommented.
11919 cx.set_state(indoc! {"
11920 fn a() {
11921 «// a();
11922 /// b();
11923 //! c();ˇ»
11924 }
11925 "});
11926
11927 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11928
11929 cx.assert_editor_state(indoc! {"
11930 fn a() {
11931 «a();
11932 b();
11933 c();ˇ»
11934 }
11935 "});
11936}
11937
11938#[gpui::test]
11939async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11940 init_test(cx, |_| {});
11941 let mut cx = EditorTestContext::new(cx).await;
11942 let language = Arc::new(Language::new(
11943 LanguageConfig {
11944 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11945 ..Default::default()
11946 },
11947 Some(tree_sitter_rust::LANGUAGE.into()),
11948 ));
11949 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11950
11951 let toggle_comments = &ToggleComments {
11952 advance_downwards: false,
11953 ignore_indent: true,
11954 };
11955
11956 // If multiple selections intersect a line, the line is only toggled once.
11957 cx.set_state(indoc! {"
11958 fn a() {
11959 // «b();
11960 // c();
11961 // ˇ» d();
11962 }
11963 "});
11964
11965 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11966
11967 cx.assert_editor_state(indoc! {"
11968 fn a() {
11969 «b();
11970 c();
11971 ˇ» d();
11972 }
11973 "});
11974
11975 // The comment prefix is inserted at the beginning of each line
11976 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11977
11978 cx.assert_editor_state(indoc! {"
11979 fn a() {
11980 // «b();
11981 // c();
11982 // ˇ» d();
11983 }
11984 "});
11985
11986 // If a selection ends at the beginning of a line, that line is not toggled.
11987 cx.set_selections_state(indoc! {"
11988 fn a() {
11989 // b();
11990 // «c();
11991 ˇ»// d();
11992 }
11993 "});
11994
11995 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11996
11997 cx.assert_editor_state(indoc! {"
11998 fn a() {
11999 // b();
12000 «c();
12001 ˇ»// d();
12002 }
12003 "});
12004
12005 // If a selection span a single line and is empty, the line is toggled.
12006 cx.set_state(indoc! {"
12007 fn a() {
12008 a();
12009 b();
12010 ˇ
12011 }
12012 "});
12013
12014 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12015
12016 cx.assert_editor_state(indoc! {"
12017 fn a() {
12018 a();
12019 b();
12020 //ˇ
12021 }
12022 "});
12023
12024 // If a selection span multiple lines, empty lines are not toggled.
12025 cx.set_state(indoc! {"
12026 fn a() {
12027 «a();
12028
12029 c();ˇ»
12030 }
12031 "});
12032
12033 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12034
12035 cx.assert_editor_state(indoc! {"
12036 fn a() {
12037 // «a();
12038
12039 // c();ˇ»
12040 }
12041 "});
12042
12043 // If a selection includes multiple comment prefixes, all lines are uncommented.
12044 cx.set_state(indoc! {"
12045 fn a() {
12046 // «a();
12047 /// b();
12048 //! c();ˇ»
12049 }
12050 "});
12051
12052 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12053
12054 cx.assert_editor_state(indoc! {"
12055 fn a() {
12056 «a();
12057 b();
12058 c();ˇ»
12059 }
12060 "});
12061}
12062
12063#[gpui::test]
12064async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12065 init_test(cx, |_| {});
12066
12067 let language = Arc::new(Language::new(
12068 LanguageConfig {
12069 line_comments: vec!["// ".into()],
12070 ..Default::default()
12071 },
12072 Some(tree_sitter_rust::LANGUAGE.into()),
12073 ));
12074
12075 let mut cx = EditorTestContext::new(cx).await;
12076
12077 cx.language_registry().add(language.clone());
12078 cx.update_buffer(|buffer, cx| {
12079 buffer.set_language(Some(language), cx);
12080 });
12081
12082 let toggle_comments = &ToggleComments {
12083 advance_downwards: true,
12084 ignore_indent: false,
12085 };
12086
12087 // Single cursor on one line -> advance
12088 // Cursor moves horizontally 3 characters as well on non-blank line
12089 cx.set_state(indoc!(
12090 "fn a() {
12091 ˇdog();
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 catˇ();
12102 }"
12103 ));
12104
12105 // Single selection on one line -> don't advance
12106 cx.set_state(indoc!(
12107 "fn a() {
12108 «dog()ˇ»;
12109 cat();
12110 }"
12111 ));
12112 cx.update_editor(|editor, window, cx| {
12113 editor.toggle_comments(toggle_comments, window, cx);
12114 });
12115 cx.assert_editor_state(indoc!(
12116 "fn a() {
12117 // «dog()ˇ»;
12118 cat();
12119 }"
12120 ));
12121
12122 // Multiple cursors on one line -> advance
12123 cx.set_state(indoc!(
12124 "fn a() {
12125 ˇdˇog();
12126 cat();
12127 }"
12128 ));
12129 cx.update_editor(|editor, window, cx| {
12130 editor.toggle_comments(toggle_comments, window, cx);
12131 });
12132 cx.assert_editor_state(indoc!(
12133 "fn a() {
12134 // dog();
12135 catˇ(ˇ);
12136 }"
12137 ));
12138
12139 // Multiple cursors on one line, with selection -> don't advance
12140 cx.set_state(indoc!(
12141 "fn a() {
12142 ˇdˇog«()ˇ»;
12143 cat();
12144 }"
12145 ));
12146 cx.update_editor(|editor, window, cx| {
12147 editor.toggle_comments(toggle_comments, window, cx);
12148 });
12149 cx.assert_editor_state(indoc!(
12150 "fn a() {
12151 // ˇdˇog«()ˇ»;
12152 cat();
12153 }"
12154 ));
12155
12156 // Single cursor on one line -> advance
12157 // Cursor moves to column 0 on blank line
12158 cx.set_state(indoc!(
12159 "fn a() {
12160 ˇdog();
12161
12162 cat();
12163 }"
12164 ));
12165 cx.update_editor(|editor, window, cx| {
12166 editor.toggle_comments(toggle_comments, window, cx);
12167 });
12168 cx.assert_editor_state(indoc!(
12169 "fn a() {
12170 // dog();
12171 ˇ
12172 cat();
12173 }"
12174 ));
12175
12176 // Single cursor on one line -> advance
12177 // Cursor starts and ends at column 0
12178 cx.set_state(indoc!(
12179 "fn a() {
12180 ˇ dog();
12181 cat();
12182 }"
12183 ));
12184 cx.update_editor(|editor, window, cx| {
12185 editor.toggle_comments(toggle_comments, window, cx);
12186 });
12187 cx.assert_editor_state(indoc!(
12188 "fn a() {
12189 // dog();
12190 ˇ cat();
12191 }"
12192 ));
12193}
12194
12195#[gpui::test]
12196async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12197 init_test(cx, |_| {});
12198
12199 let mut cx = EditorTestContext::new(cx).await;
12200
12201 let html_language = Arc::new(
12202 Language::new(
12203 LanguageConfig {
12204 name: "HTML".into(),
12205 block_comment: Some(("<!-- ".into(), " -->".into())),
12206 ..Default::default()
12207 },
12208 Some(tree_sitter_html::LANGUAGE.into()),
12209 )
12210 .with_injection_query(
12211 r#"
12212 (script_element
12213 (raw_text) @injection.content
12214 (#set! injection.language "javascript"))
12215 "#,
12216 )
12217 .unwrap(),
12218 );
12219
12220 let javascript_language = Arc::new(Language::new(
12221 LanguageConfig {
12222 name: "JavaScript".into(),
12223 line_comments: vec!["// ".into()],
12224 ..Default::default()
12225 },
12226 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12227 ));
12228
12229 cx.language_registry().add(html_language.clone());
12230 cx.language_registry().add(javascript_language.clone());
12231 cx.update_buffer(|buffer, cx| {
12232 buffer.set_language(Some(html_language), cx);
12233 });
12234
12235 // Toggle comments for empty selections
12236 cx.set_state(
12237 &r#"
12238 <p>A</p>ˇ
12239 <p>B</p>ˇ
12240 <p>C</p>ˇ
12241 "#
12242 .unindent(),
12243 );
12244 cx.update_editor(|editor, window, cx| {
12245 editor.toggle_comments(&ToggleComments::default(), window, cx)
12246 });
12247 cx.assert_editor_state(
12248 &r#"
12249 <!-- <p>A</p>ˇ -->
12250 <!-- <p>B</p>ˇ -->
12251 <!-- <p>C</p>ˇ -->
12252 "#
12253 .unindent(),
12254 );
12255 cx.update_editor(|editor, window, cx| {
12256 editor.toggle_comments(&ToggleComments::default(), window, cx)
12257 });
12258 cx.assert_editor_state(
12259 &r#"
12260 <p>A</p>ˇ
12261 <p>B</p>ˇ
12262 <p>C</p>ˇ
12263 "#
12264 .unindent(),
12265 );
12266
12267 // Toggle comments for mixture of empty and non-empty selections, where
12268 // multiple selections occupy a given line.
12269 cx.set_state(
12270 &r#"
12271 <p>A«</p>
12272 <p>ˇ»B</p>ˇ
12273 <p>C«</p>
12274 <p>ˇ»D</p>ˇ
12275 "#
12276 .unindent(),
12277 );
12278
12279 cx.update_editor(|editor, window, cx| {
12280 editor.toggle_comments(&ToggleComments::default(), window, cx)
12281 });
12282 cx.assert_editor_state(
12283 &r#"
12284 <!-- <p>A«</p>
12285 <p>ˇ»B</p>ˇ -->
12286 <!-- <p>C«</p>
12287 <p>ˇ»D</p>ˇ -->
12288 "#
12289 .unindent(),
12290 );
12291 cx.update_editor(|editor, window, cx| {
12292 editor.toggle_comments(&ToggleComments::default(), window, cx)
12293 });
12294 cx.assert_editor_state(
12295 &r#"
12296 <p>A«</p>
12297 <p>ˇ»B</p>ˇ
12298 <p>C«</p>
12299 <p>ˇ»D</p>ˇ
12300 "#
12301 .unindent(),
12302 );
12303
12304 // Toggle comments when different languages are active for different
12305 // selections.
12306 cx.set_state(
12307 &r#"
12308 ˇ<script>
12309 ˇvar x = new Y();
12310 ˇ</script>
12311 "#
12312 .unindent(),
12313 );
12314 cx.executor().run_until_parked();
12315 cx.update_editor(|editor, window, cx| {
12316 editor.toggle_comments(&ToggleComments::default(), window, cx)
12317 });
12318 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12319 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12320 cx.assert_editor_state(
12321 &r#"
12322 <!-- ˇ<script> -->
12323 // ˇvar x = new Y();
12324 <!-- ˇ</script> -->
12325 "#
12326 .unindent(),
12327 );
12328}
12329
12330#[gpui::test]
12331fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12332 init_test(cx, |_| {});
12333
12334 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12335 let multibuffer = cx.new(|cx| {
12336 let mut multibuffer = MultiBuffer::new(ReadWrite);
12337 multibuffer.push_excerpts(
12338 buffer.clone(),
12339 [
12340 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12341 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12342 ],
12343 cx,
12344 );
12345 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12346 multibuffer
12347 });
12348
12349 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12350 editor.update_in(cx, |editor, window, cx| {
12351 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12352 editor.change_selections(None, window, cx, |s| {
12353 s.select_ranges([
12354 Point::new(0, 0)..Point::new(0, 0),
12355 Point::new(1, 0)..Point::new(1, 0),
12356 ])
12357 });
12358
12359 editor.handle_input("X", window, cx);
12360 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12361 assert_eq!(
12362 editor.selections.ranges(cx),
12363 [
12364 Point::new(0, 1)..Point::new(0, 1),
12365 Point::new(1, 1)..Point::new(1, 1),
12366 ]
12367 );
12368
12369 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12370 editor.change_selections(None, window, cx, |s| {
12371 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12372 });
12373 editor.backspace(&Default::default(), window, cx);
12374 assert_eq!(editor.text(cx), "Xa\nbbb");
12375 assert_eq!(
12376 editor.selections.ranges(cx),
12377 [Point::new(1, 0)..Point::new(1, 0)]
12378 );
12379
12380 editor.change_selections(None, window, cx, |s| {
12381 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12382 });
12383 editor.backspace(&Default::default(), window, cx);
12384 assert_eq!(editor.text(cx), "X\nbb");
12385 assert_eq!(
12386 editor.selections.ranges(cx),
12387 [Point::new(0, 1)..Point::new(0, 1)]
12388 );
12389 });
12390}
12391
12392#[gpui::test]
12393fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12394 init_test(cx, |_| {});
12395
12396 let markers = vec![('[', ']').into(), ('(', ')').into()];
12397 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12398 indoc! {"
12399 [aaaa
12400 (bbbb]
12401 cccc)",
12402 },
12403 markers.clone(),
12404 );
12405 let excerpt_ranges = markers.into_iter().map(|marker| {
12406 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12407 ExcerptRange::new(context.clone())
12408 });
12409 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12410 let multibuffer = cx.new(|cx| {
12411 let mut multibuffer = MultiBuffer::new(ReadWrite);
12412 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12413 multibuffer
12414 });
12415
12416 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12417 editor.update_in(cx, |editor, window, cx| {
12418 let (expected_text, selection_ranges) = marked_text_ranges(
12419 indoc! {"
12420 aaaa
12421 bˇbbb
12422 bˇbbˇb
12423 cccc"
12424 },
12425 true,
12426 );
12427 assert_eq!(editor.text(cx), expected_text);
12428 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12429
12430 editor.handle_input("X", window, cx);
12431
12432 let (expected_text, expected_selections) = marked_text_ranges(
12433 indoc! {"
12434 aaaa
12435 bXˇbbXb
12436 bXˇbbXˇb
12437 cccc"
12438 },
12439 false,
12440 );
12441 assert_eq!(editor.text(cx), expected_text);
12442 assert_eq!(editor.selections.ranges(cx), expected_selections);
12443
12444 editor.newline(&Newline, window, cx);
12445 let (expected_text, expected_selections) = marked_text_ranges(
12446 indoc! {"
12447 aaaa
12448 bX
12449 ˇbbX
12450 b
12451 bX
12452 ˇbbX
12453 ˇb
12454 cccc"
12455 },
12456 false,
12457 );
12458 assert_eq!(editor.text(cx), expected_text);
12459 assert_eq!(editor.selections.ranges(cx), expected_selections);
12460 });
12461}
12462
12463#[gpui::test]
12464fn test_refresh_selections(cx: &mut TestAppContext) {
12465 init_test(cx, |_| {});
12466
12467 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12468 let mut excerpt1_id = None;
12469 let multibuffer = cx.new(|cx| {
12470 let mut multibuffer = MultiBuffer::new(ReadWrite);
12471 excerpt1_id = multibuffer
12472 .push_excerpts(
12473 buffer.clone(),
12474 [
12475 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12476 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12477 ],
12478 cx,
12479 )
12480 .into_iter()
12481 .next();
12482 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12483 multibuffer
12484 });
12485
12486 let editor = cx.add_window(|window, cx| {
12487 let mut editor = build_editor(multibuffer.clone(), window, cx);
12488 let snapshot = editor.snapshot(window, cx);
12489 editor.change_selections(None, window, cx, |s| {
12490 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12491 });
12492 editor.begin_selection(
12493 Point::new(2, 1).to_display_point(&snapshot),
12494 true,
12495 1,
12496 window,
12497 cx,
12498 );
12499 assert_eq!(
12500 editor.selections.ranges(cx),
12501 [
12502 Point::new(1, 3)..Point::new(1, 3),
12503 Point::new(2, 1)..Point::new(2, 1),
12504 ]
12505 );
12506 editor
12507 });
12508
12509 // Refreshing selections is a no-op when excerpts haven't changed.
12510 _ = editor.update(cx, |editor, window, cx| {
12511 editor.change_selections(None, window, cx, |s| s.refresh());
12512 assert_eq!(
12513 editor.selections.ranges(cx),
12514 [
12515 Point::new(1, 3)..Point::new(1, 3),
12516 Point::new(2, 1)..Point::new(2, 1),
12517 ]
12518 );
12519 });
12520
12521 multibuffer.update(cx, |multibuffer, cx| {
12522 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12523 });
12524 _ = editor.update(cx, |editor, window, cx| {
12525 // Removing an excerpt causes the first selection to become degenerate.
12526 assert_eq!(
12527 editor.selections.ranges(cx),
12528 [
12529 Point::new(0, 0)..Point::new(0, 0),
12530 Point::new(0, 1)..Point::new(0, 1)
12531 ]
12532 );
12533
12534 // Refreshing selections will relocate the first selection to the original buffer
12535 // location.
12536 editor.change_selections(None, window, cx, |s| s.refresh());
12537 assert_eq!(
12538 editor.selections.ranges(cx),
12539 [
12540 Point::new(0, 1)..Point::new(0, 1),
12541 Point::new(0, 3)..Point::new(0, 3)
12542 ]
12543 );
12544 assert!(editor.selections.pending_anchor().is_some());
12545 });
12546}
12547
12548#[gpui::test]
12549fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12550 init_test(cx, |_| {});
12551
12552 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12553 let mut excerpt1_id = None;
12554 let multibuffer = cx.new(|cx| {
12555 let mut multibuffer = MultiBuffer::new(ReadWrite);
12556 excerpt1_id = multibuffer
12557 .push_excerpts(
12558 buffer.clone(),
12559 [
12560 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12561 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12562 ],
12563 cx,
12564 )
12565 .into_iter()
12566 .next();
12567 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12568 multibuffer
12569 });
12570
12571 let editor = cx.add_window(|window, cx| {
12572 let mut editor = build_editor(multibuffer.clone(), window, cx);
12573 let snapshot = editor.snapshot(window, cx);
12574 editor.begin_selection(
12575 Point::new(1, 3).to_display_point(&snapshot),
12576 false,
12577 1,
12578 window,
12579 cx,
12580 );
12581 assert_eq!(
12582 editor.selections.ranges(cx),
12583 [Point::new(1, 3)..Point::new(1, 3)]
12584 );
12585 editor
12586 });
12587
12588 multibuffer.update(cx, |multibuffer, cx| {
12589 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12590 });
12591 _ = editor.update(cx, |editor, window, cx| {
12592 assert_eq!(
12593 editor.selections.ranges(cx),
12594 [Point::new(0, 0)..Point::new(0, 0)]
12595 );
12596
12597 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12598 editor.change_selections(None, window, cx, |s| s.refresh());
12599 assert_eq!(
12600 editor.selections.ranges(cx),
12601 [Point::new(0, 3)..Point::new(0, 3)]
12602 );
12603 assert!(editor.selections.pending_anchor().is_some());
12604 });
12605}
12606
12607#[gpui::test]
12608async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12609 init_test(cx, |_| {});
12610
12611 let language = Arc::new(
12612 Language::new(
12613 LanguageConfig {
12614 brackets: BracketPairConfig {
12615 pairs: vec![
12616 BracketPair {
12617 start: "{".to_string(),
12618 end: "}".to_string(),
12619 close: true,
12620 surround: true,
12621 newline: true,
12622 },
12623 BracketPair {
12624 start: "/* ".to_string(),
12625 end: " */".to_string(),
12626 close: true,
12627 surround: true,
12628 newline: true,
12629 },
12630 ],
12631 ..Default::default()
12632 },
12633 ..Default::default()
12634 },
12635 Some(tree_sitter_rust::LANGUAGE.into()),
12636 )
12637 .with_indents_query("")
12638 .unwrap(),
12639 );
12640
12641 let text = concat!(
12642 "{ }\n", //
12643 " x\n", //
12644 " /* */\n", //
12645 "x\n", //
12646 "{{} }\n", //
12647 );
12648
12649 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12650 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12651 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12652 editor
12653 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12654 .await;
12655
12656 editor.update_in(cx, |editor, window, cx| {
12657 editor.change_selections(None, window, cx, |s| {
12658 s.select_display_ranges([
12659 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12660 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12661 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12662 ])
12663 });
12664 editor.newline(&Newline, window, cx);
12665
12666 assert_eq!(
12667 editor.buffer().read(cx).read(cx).text(),
12668 concat!(
12669 "{ \n", // Suppress rustfmt
12670 "\n", //
12671 "}\n", //
12672 " x\n", //
12673 " /* \n", //
12674 " \n", //
12675 " */\n", //
12676 "x\n", //
12677 "{{} \n", //
12678 "}\n", //
12679 )
12680 );
12681 });
12682}
12683
12684#[gpui::test]
12685fn test_highlighted_ranges(cx: &mut TestAppContext) {
12686 init_test(cx, |_| {});
12687
12688 let editor = cx.add_window(|window, cx| {
12689 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12690 build_editor(buffer.clone(), window, cx)
12691 });
12692
12693 _ = editor.update(cx, |editor, window, cx| {
12694 struct Type1;
12695 struct Type2;
12696
12697 let buffer = editor.buffer.read(cx).snapshot(cx);
12698
12699 let anchor_range =
12700 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12701
12702 editor.highlight_background::<Type1>(
12703 &[
12704 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12705 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12706 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12707 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12708 ],
12709 |_| Hsla::red(),
12710 cx,
12711 );
12712 editor.highlight_background::<Type2>(
12713 &[
12714 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12715 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12716 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12717 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12718 ],
12719 |_| Hsla::green(),
12720 cx,
12721 );
12722
12723 let snapshot = editor.snapshot(window, cx);
12724 let mut highlighted_ranges = editor.background_highlights_in_range(
12725 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12726 &snapshot,
12727 cx.theme().colors(),
12728 );
12729 // Enforce a consistent ordering based on color without relying on the ordering of the
12730 // highlight's `TypeId` which is non-executor.
12731 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12732 assert_eq!(
12733 highlighted_ranges,
12734 &[
12735 (
12736 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12737 Hsla::red(),
12738 ),
12739 (
12740 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12741 Hsla::red(),
12742 ),
12743 (
12744 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12745 Hsla::green(),
12746 ),
12747 (
12748 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12749 Hsla::green(),
12750 ),
12751 ]
12752 );
12753 assert_eq!(
12754 editor.background_highlights_in_range(
12755 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12756 &snapshot,
12757 cx.theme().colors(),
12758 ),
12759 &[(
12760 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12761 Hsla::red(),
12762 )]
12763 );
12764 });
12765}
12766
12767#[gpui::test]
12768async fn test_following(cx: &mut TestAppContext) {
12769 init_test(cx, |_| {});
12770
12771 let fs = FakeFs::new(cx.executor());
12772 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12773
12774 let buffer = project.update(cx, |project, cx| {
12775 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12776 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12777 });
12778 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12779 let follower = cx.update(|cx| {
12780 cx.open_window(
12781 WindowOptions {
12782 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12783 gpui::Point::new(px(0.), px(0.)),
12784 gpui::Point::new(px(10.), px(80.)),
12785 ))),
12786 ..Default::default()
12787 },
12788 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12789 )
12790 .unwrap()
12791 });
12792
12793 let is_still_following = Rc::new(RefCell::new(true));
12794 let follower_edit_event_count = Rc::new(RefCell::new(0));
12795 let pending_update = Rc::new(RefCell::new(None));
12796 let leader_entity = leader.root(cx).unwrap();
12797 let follower_entity = follower.root(cx).unwrap();
12798 _ = follower.update(cx, {
12799 let update = pending_update.clone();
12800 let is_still_following = is_still_following.clone();
12801 let follower_edit_event_count = follower_edit_event_count.clone();
12802 |_, window, cx| {
12803 cx.subscribe_in(
12804 &leader_entity,
12805 window,
12806 move |_, leader, event, window, cx| {
12807 leader.read(cx).add_event_to_update_proto(
12808 event,
12809 &mut update.borrow_mut(),
12810 window,
12811 cx,
12812 );
12813 },
12814 )
12815 .detach();
12816
12817 cx.subscribe_in(
12818 &follower_entity,
12819 window,
12820 move |_, _, event: &EditorEvent, _window, _cx| {
12821 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12822 *is_still_following.borrow_mut() = false;
12823 }
12824
12825 if let EditorEvent::BufferEdited = event {
12826 *follower_edit_event_count.borrow_mut() += 1;
12827 }
12828 },
12829 )
12830 .detach();
12831 }
12832 });
12833
12834 // Update the selections only
12835 _ = leader.update(cx, |leader, window, cx| {
12836 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12837 });
12838 follower
12839 .update(cx, |follower, window, cx| {
12840 follower.apply_update_proto(
12841 &project,
12842 pending_update.borrow_mut().take().unwrap(),
12843 window,
12844 cx,
12845 )
12846 })
12847 .unwrap()
12848 .await
12849 .unwrap();
12850 _ = follower.update(cx, |follower, _, cx| {
12851 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12852 });
12853 assert!(*is_still_following.borrow());
12854 assert_eq!(*follower_edit_event_count.borrow(), 0);
12855
12856 // Update the scroll position only
12857 _ = leader.update(cx, |leader, window, cx| {
12858 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12859 });
12860 follower
12861 .update(cx, |follower, window, cx| {
12862 follower.apply_update_proto(
12863 &project,
12864 pending_update.borrow_mut().take().unwrap(),
12865 window,
12866 cx,
12867 )
12868 })
12869 .unwrap()
12870 .await
12871 .unwrap();
12872 assert_eq!(
12873 follower
12874 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12875 .unwrap(),
12876 gpui::Point::new(1.5, 3.5)
12877 );
12878 assert!(*is_still_following.borrow());
12879 assert_eq!(*follower_edit_event_count.borrow(), 0);
12880
12881 // Update the selections and scroll position. The follower's scroll position is updated
12882 // via autoscroll, not via the leader's exact scroll position.
12883 _ = leader.update(cx, |leader, window, cx| {
12884 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12885 leader.request_autoscroll(Autoscroll::newest(), cx);
12886 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12887 });
12888 follower
12889 .update(cx, |follower, window, cx| {
12890 follower.apply_update_proto(
12891 &project,
12892 pending_update.borrow_mut().take().unwrap(),
12893 window,
12894 cx,
12895 )
12896 })
12897 .unwrap()
12898 .await
12899 .unwrap();
12900 _ = follower.update(cx, |follower, _, cx| {
12901 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12902 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12903 });
12904 assert!(*is_still_following.borrow());
12905
12906 // Creating a pending selection that precedes another selection
12907 _ = leader.update(cx, |leader, window, cx| {
12908 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12909 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12910 });
12911 follower
12912 .update(cx, |follower, window, cx| {
12913 follower.apply_update_proto(
12914 &project,
12915 pending_update.borrow_mut().take().unwrap(),
12916 window,
12917 cx,
12918 )
12919 })
12920 .unwrap()
12921 .await
12922 .unwrap();
12923 _ = follower.update(cx, |follower, _, cx| {
12924 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12925 });
12926 assert!(*is_still_following.borrow());
12927
12928 // Extend the pending selection so that it surrounds another selection
12929 _ = leader.update(cx, |leader, window, cx| {
12930 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12931 });
12932 follower
12933 .update(cx, |follower, window, cx| {
12934 follower.apply_update_proto(
12935 &project,
12936 pending_update.borrow_mut().take().unwrap(),
12937 window,
12938 cx,
12939 )
12940 })
12941 .unwrap()
12942 .await
12943 .unwrap();
12944 _ = follower.update(cx, |follower, _, cx| {
12945 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12946 });
12947
12948 // Scrolling locally breaks the follow
12949 _ = follower.update(cx, |follower, window, cx| {
12950 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12951 follower.set_scroll_anchor(
12952 ScrollAnchor {
12953 anchor: top_anchor,
12954 offset: gpui::Point::new(0.0, 0.5),
12955 },
12956 window,
12957 cx,
12958 );
12959 });
12960 assert!(!(*is_still_following.borrow()));
12961}
12962
12963#[gpui::test]
12964async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12965 init_test(cx, |_| {});
12966
12967 let fs = FakeFs::new(cx.executor());
12968 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12969 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12970 let pane = workspace
12971 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12972 .unwrap();
12973
12974 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12975
12976 let leader = pane.update_in(cx, |_, window, cx| {
12977 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12978 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12979 });
12980
12981 // Start following the editor when it has no excerpts.
12982 let mut state_message =
12983 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12984 let workspace_entity = workspace.root(cx).unwrap();
12985 let follower_1 = cx
12986 .update_window(*workspace.deref(), |_, window, cx| {
12987 Editor::from_state_proto(
12988 workspace_entity,
12989 ViewId {
12990 creator: CollaboratorId::PeerId(PeerId::default()),
12991 id: 0,
12992 },
12993 &mut state_message,
12994 window,
12995 cx,
12996 )
12997 })
12998 .unwrap()
12999 .unwrap()
13000 .await
13001 .unwrap();
13002
13003 let update_message = Rc::new(RefCell::new(None));
13004 follower_1.update_in(cx, {
13005 let update = update_message.clone();
13006 |_, window, cx| {
13007 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13008 leader.read(cx).add_event_to_update_proto(
13009 event,
13010 &mut update.borrow_mut(),
13011 window,
13012 cx,
13013 );
13014 })
13015 .detach();
13016 }
13017 });
13018
13019 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13020 (
13021 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13022 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13023 )
13024 });
13025
13026 // Insert some excerpts.
13027 leader.update(cx, |leader, cx| {
13028 leader.buffer.update(cx, |multibuffer, cx| {
13029 multibuffer.set_excerpts_for_path(
13030 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13031 buffer_1.clone(),
13032 vec![
13033 Point::row_range(0..3),
13034 Point::row_range(1..6),
13035 Point::row_range(12..15),
13036 ],
13037 0,
13038 cx,
13039 );
13040 multibuffer.set_excerpts_for_path(
13041 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13042 buffer_2.clone(),
13043 vec![Point::row_range(0..6), Point::row_range(8..12)],
13044 0,
13045 cx,
13046 );
13047 });
13048 });
13049
13050 // Apply the update of adding the excerpts.
13051 follower_1
13052 .update_in(cx, |follower, window, cx| {
13053 follower.apply_update_proto(
13054 &project,
13055 update_message.borrow().clone().unwrap(),
13056 window,
13057 cx,
13058 )
13059 })
13060 .await
13061 .unwrap();
13062 assert_eq!(
13063 follower_1.update(cx, |editor, cx| editor.text(cx)),
13064 leader.update(cx, |editor, cx| editor.text(cx))
13065 );
13066 update_message.borrow_mut().take();
13067
13068 // Start following separately after it already has excerpts.
13069 let mut state_message =
13070 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13071 let workspace_entity = workspace.root(cx).unwrap();
13072 let follower_2 = cx
13073 .update_window(*workspace.deref(), |_, window, cx| {
13074 Editor::from_state_proto(
13075 workspace_entity,
13076 ViewId {
13077 creator: CollaboratorId::PeerId(PeerId::default()),
13078 id: 0,
13079 },
13080 &mut state_message,
13081 window,
13082 cx,
13083 )
13084 })
13085 .unwrap()
13086 .unwrap()
13087 .await
13088 .unwrap();
13089 assert_eq!(
13090 follower_2.update(cx, |editor, cx| editor.text(cx)),
13091 leader.update(cx, |editor, cx| editor.text(cx))
13092 );
13093
13094 // Remove some excerpts.
13095 leader.update(cx, |leader, cx| {
13096 leader.buffer.update(cx, |multibuffer, cx| {
13097 let excerpt_ids = multibuffer.excerpt_ids();
13098 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13099 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13100 });
13101 });
13102
13103 // Apply the update of removing the excerpts.
13104 follower_1
13105 .update_in(cx, |follower, window, cx| {
13106 follower.apply_update_proto(
13107 &project,
13108 update_message.borrow().clone().unwrap(),
13109 window,
13110 cx,
13111 )
13112 })
13113 .await
13114 .unwrap();
13115 follower_2
13116 .update_in(cx, |follower, window, cx| {
13117 follower.apply_update_proto(
13118 &project,
13119 update_message.borrow().clone().unwrap(),
13120 window,
13121 cx,
13122 )
13123 })
13124 .await
13125 .unwrap();
13126 update_message.borrow_mut().take();
13127 assert_eq!(
13128 follower_1.update(cx, |editor, cx| editor.text(cx)),
13129 leader.update(cx, |editor, cx| editor.text(cx))
13130 );
13131}
13132
13133#[gpui::test]
13134async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13135 init_test(cx, |_| {});
13136
13137 let mut cx = EditorTestContext::new(cx).await;
13138 let lsp_store =
13139 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13140
13141 cx.set_state(indoc! {"
13142 ˇfn func(abc def: i32) -> u32 {
13143 }
13144 "});
13145
13146 cx.update(|_, cx| {
13147 lsp_store.update(cx, |lsp_store, cx| {
13148 lsp_store
13149 .update_diagnostics(
13150 LanguageServerId(0),
13151 lsp::PublishDiagnosticsParams {
13152 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13153 version: None,
13154 diagnostics: vec![
13155 lsp::Diagnostic {
13156 range: lsp::Range::new(
13157 lsp::Position::new(0, 11),
13158 lsp::Position::new(0, 12),
13159 ),
13160 severity: Some(lsp::DiagnosticSeverity::ERROR),
13161 ..Default::default()
13162 },
13163 lsp::Diagnostic {
13164 range: lsp::Range::new(
13165 lsp::Position::new(0, 12),
13166 lsp::Position::new(0, 15),
13167 ),
13168 severity: Some(lsp::DiagnosticSeverity::ERROR),
13169 ..Default::default()
13170 },
13171 lsp::Diagnostic {
13172 range: lsp::Range::new(
13173 lsp::Position::new(0, 25),
13174 lsp::Position::new(0, 28),
13175 ),
13176 severity: Some(lsp::DiagnosticSeverity::ERROR),
13177 ..Default::default()
13178 },
13179 ],
13180 },
13181 &[],
13182 cx,
13183 )
13184 .unwrap()
13185 });
13186 });
13187
13188 executor.run_until_parked();
13189
13190 cx.update_editor(|editor, window, cx| {
13191 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13192 });
13193
13194 cx.assert_editor_state(indoc! {"
13195 fn func(abc def: i32) -> ˇu32 {
13196 }
13197 "});
13198
13199 cx.update_editor(|editor, window, cx| {
13200 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13201 });
13202
13203 cx.assert_editor_state(indoc! {"
13204 fn func(abc ˇdef: i32) -> u32 {
13205 }
13206 "});
13207
13208 cx.update_editor(|editor, window, cx| {
13209 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13210 });
13211
13212 cx.assert_editor_state(indoc! {"
13213 fn func(abcˇ def: i32) -> u32 {
13214 }
13215 "});
13216
13217 cx.update_editor(|editor, window, cx| {
13218 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13219 });
13220
13221 cx.assert_editor_state(indoc! {"
13222 fn func(abc def: i32) -> ˇu32 {
13223 }
13224 "});
13225}
13226
13227#[gpui::test]
13228async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13229 init_test(cx, |_| {});
13230
13231 let mut cx = EditorTestContext::new(cx).await;
13232
13233 let diff_base = r#"
13234 use some::mod;
13235
13236 const A: u32 = 42;
13237
13238 fn main() {
13239 println!("hello");
13240
13241 println!("world");
13242 }
13243 "#
13244 .unindent();
13245
13246 // Edits are modified, removed, modified, added
13247 cx.set_state(
13248 &r#"
13249 use some::modified;
13250
13251 ˇ
13252 fn main() {
13253 println!("hello there");
13254
13255 println!("around the");
13256 println!("world");
13257 }
13258 "#
13259 .unindent(),
13260 );
13261
13262 cx.set_head_text(&diff_base);
13263 executor.run_until_parked();
13264
13265 cx.update_editor(|editor, window, cx| {
13266 //Wrap around the bottom of the buffer
13267 for _ in 0..3 {
13268 editor.go_to_next_hunk(&GoToHunk, window, cx);
13269 }
13270 });
13271
13272 cx.assert_editor_state(
13273 &r#"
13274 ˇuse some::modified;
13275
13276
13277 fn main() {
13278 println!("hello there");
13279
13280 println!("around the");
13281 println!("world");
13282 }
13283 "#
13284 .unindent(),
13285 );
13286
13287 cx.update_editor(|editor, window, cx| {
13288 //Wrap around the top of the buffer
13289 for _ in 0..2 {
13290 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13291 }
13292 });
13293
13294 cx.assert_editor_state(
13295 &r#"
13296 use some::modified;
13297
13298
13299 fn main() {
13300 ˇ println!("hello there");
13301
13302 println!("around the");
13303 println!("world");
13304 }
13305 "#
13306 .unindent(),
13307 );
13308
13309 cx.update_editor(|editor, window, cx| {
13310 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13311 });
13312
13313 cx.assert_editor_state(
13314 &r#"
13315 use some::modified;
13316
13317 ˇ
13318 fn main() {
13319 println!("hello there");
13320
13321 println!("around the");
13322 println!("world");
13323 }
13324 "#
13325 .unindent(),
13326 );
13327
13328 cx.update_editor(|editor, window, cx| {
13329 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13330 });
13331
13332 cx.assert_editor_state(
13333 &r#"
13334 ˇuse some::modified;
13335
13336
13337 fn main() {
13338 println!("hello there");
13339
13340 println!("around the");
13341 println!("world");
13342 }
13343 "#
13344 .unindent(),
13345 );
13346
13347 cx.update_editor(|editor, window, cx| {
13348 for _ in 0..2 {
13349 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13350 }
13351 });
13352
13353 cx.assert_editor_state(
13354 &r#"
13355 use some::modified;
13356
13357
13358 fn main() {
13359 ˇ println!("hello there");
13360
13361 println!("around the");
13362 println!("world");
13363 }
13364 "#
13365 .unindent(),
13366 );
13367
13368 cx.update_editor(|editor, window, cx| {
13369 editor.fold(&Fold, window, cx);
13370 });
13371
13372 cx.update_editor(|editor, window, cx| {
13373 editor.go_to_next_hunk(&GoToHunk, window, cx);
13374 });
13375
13376 cx.assert_editor_state(
13377 &r#"
13378 ˇuse some::modified;
13379
13380
13381 fn main() {
13382 println!("hello there");
13383
13384 println!("around the");
13385 println!("world");
13386 }
13387 "#
13388 .unindent(),
13389 );
13390}
13391
13392#[test]
13393fn test_split_words() {
13394 fn split(text: &str) -> Vec<&str> {
13395 split_words(text).collect()
13396 }
13397
13398 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13399 assert_eq!(split("hello_world"), &["hello_", "world"]);
13400 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13401 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13402 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13403 assert_eq!(split("helloworld"), &["helloworld"]);
13404
13405 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13406}
13407
13408#[gpui::test]
13409async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13410 init_test(cx, |_| {});
13411
13412 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13413 let mut assert = |before, after| {
13414 let _state_context = cx.set_state(before);
13415 cx.run_until_parked();
13416 cx.update_editor(|editor, window, cx| {
13417 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13418 });
13419 cx.run_until_parked();
13420 cx.assert_editor_state(after);
13421 };
13422
13423 // Outside bracket jumps to outside of matching bracket
13424 assert("console.logˇ(var);", "console.log(var)ˇ;");
13425 assert("console.log(var)ˇ;", "console.logˇ(var);");
13426
13427 // Inside bracket jumps to inside of matching bracket
13428 assert("console.log(ˇvar);", "console.log(varˇ);");
13429 assert("console.log(varˇ);", "console.log(ˇvar);");
13430
13431 // When outside a bracket and inside, favor jumping to the inside bracket
13432 assert(
13433 "console.log('foo', [1, 2, 3]ˇ);",
13434 "console.log(ˇ'foo', [1, 2, 3]);",
13435 );
13436 assert(
13437 "console.log(ˇ'foo', [1, 2, 3]);",
13438 "console.log('foo', [1, 2, 3]ˇ);",
13439 );
13440
13441 // Bias forward if two options are equally likely
13442 assert(
13443 "let result = curried_fun()ˇ();",
13444 "let result = curried_fun()()ˇ;",
13445 );
13446
13447 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13448 assert(
13449 indoc! {"
13450 function test() {
13451 console.log('test')ˇ
13452 }"},
13453 indoc! {"
13454 function test() {
13455 console.logˇ('test')
13456 }"},
13457 );
13458}
13459
13460#[gpui::test]
13461async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13462 init_test(cx, |_| {});
13463
13464 let fs = FakeFs::new(cx.executor());
13465 fs.insert_tree(
13466 path!("/a"),
13467 json!({
13468 "main.rs": "fn main() { let a = 5; }",
13469 "other.rs": "// Test file",
13470 }),
13471 )
13472 .await;
13473 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13474
13475 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13476 language_registry.add(Arc::new(Language::new(
13477 LanguageConfig {
13478 name: "Rust".into(),
13479 matcher: LanguageMatcher {
13480 path_suffixes: vec!["rs".to_string()],
13481 ..Default::default()
13482 },
13483 brackets: BracketPairConfig {
13484 pairs: vec![BracketPair {
13485 start: "{".to_string(),
13486 end: "}".to_string(),
13487 close: true,
13488 surround: true,
13489 newline: true,
13490 }],
13491 disabled_scopes_by_bracket_ix: Vec::new(),
13492 },
13493 ..Default::default()
13494 },
13495 Some(tree_sitter_rust::LANGUAGE.into()),
13496 )));
13497 let mut fake_servers = language_registry.register_fake_lsp(
13498 "Rust",
13499 FakeLspAdapter {
13500 capabilities: lsp::ServerCapabilities {
13501 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13502 first_trigger_character: "{".to_string(),
13503 more_trigger_character: None,
13504 }),
13505 ..Default::default()
13506 },
13507 ..Default::default()
13508 },
13509 );
13510
13511 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13512
13513 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13514
13515 let worktree_id = workspace
13516 .update(cx, |workspace, _, cx| {
13517 workspace.project().update(cx, |project, cx| {
13518 project.worktrees(cx).next().unwrap().read(cx).id()
13519 })
13520 })
13521 .unwrap();
13522
13523 let buffer = project
13524 .update(cx, |project, cx| {
13525 project.open_local_buffer(path!("/a/main.rs"), cx)
13526 })
13527 .await
13528 .unwrap();
13529 let editor_handle = workspace
13530 .update(cx, |workspace, window, cx| {
13531 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13532 })
13533 .unwrap()
13534 .await
13535 .unwrap()
13536 .downcast::<Editor>()
13537 .unwrap();
13538
13539 cx.executor().start_waiting();
13540 let fake_server = fake_servers.next().await.unwrap();
13541
13542 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13543 |params, _| async move {
13544 assert_eq!(
13545 params.text_document_position.text_document.uri,
13546 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13547 );
13548 assert_eq!(
13549 params.text_document_position.position,
13550 lsp::Position::new(0, 21),
13551 );
13552
13553 Ok(Some(vec![lsp::TextEdit {
13554 new_text: "]".to_string(),
13555 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13556 }]))
13557 },
13558 );
13559
13560 editor_handle.update_in(cx, |editor, window, cx| {
13561 window.focus(&editor.focus_handle(cx));
13562 editor.change_selections(None, window, cx, |s| {
13563 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13564 });
13565 editor.handle_input("{", window, cx);
13566 });
13567
13568 cx.executor().run_until_parked();
13569
13570 buffer.update(cx, |buffer, _| {
13571 assert_eq!(
13572 buffer.text(),
13573 "fn main() { let a = {5}; }",
13574 "No extra braces from on type formatting should appear in the buffer"
13575 )
13576 });
13577}
13578
13579#[gpui::test]
13580async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13581 init_test(cx, |_| {});
13582
13583 let fs = FakeFs::new(cx.executor());
13584 fs.insert_tree(
13585 path!("/a"),
13586 json!({
13587 "main.rs": "fn main() { let a = 5; }",
13588 "other.rs": "// Test file",
13589 }),
13590 )
13591 .await;
13592
13593 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13594
13595 let server_restarts = Arc::new(AtomicUsize::new(0));
13596 let closure_restarts = Arc::clone(&server_restarts);
13597 let language_server_name = "test language server";
13598 let language_name: LanguageName = "Rust".into();
13599
13600 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13601 language_registry.add(Arc::new(Language::new(
13602 LanguageConfig {
13603 name: language_name.clone(),
13604 matcher: LanguageMatcher {
13605 path_suffixes: vec!["rs".to_string()],
13606 ..Default::default()
13607 },
13608 ..Default::default()
13609 },
13610 Some(tree_sitter_rust::LANGUAGE.into()),
13611 )));
13612 let mut fake_servers = language_registry.register_fake_lsp(
13613 "Rust",
13614 FakeLspAdapter {
13615 name: language_server_name,
13616 initialization_options: Some(json!({
13617 "testOptionValue": true
13618 })),
13619 initializer: Some(Box::new(move |fake_server| {
13620 let task_restarts = Arc::clone(&closure_restarts);
13621 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13622 task_restarts.fetch_add(1, atomic::Ordering::Release);
13623 futures::future::ready(Ok(()))
13624 });
13625 })),
13626 ..Default::default()
13627 },
13628 );
13629
13630 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13631 let _buffer = project
13632 .update(cx, |project, cx| {
13633 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13634 })
13635 .await
13636 .unwrap();
13637 let _fake_server = fake_servers.next().await.unwrap();
13638 update_test_language_settings(cx, |language_settings| {
13639 language_settings.languages.insert(
13640 language_name.clone(),
13641 LanguageSettingsContent {
13642 tab_size: NonZeroU32::new(8),
13643 ..Default::default()
13644 },
13645 );
13646 });
13647 cx.executor().run_until_parked();
13648 assert_eq!(
13649 server_restarts.load(atomic::Ordering::Acquire),
13650 0,
13651 "Should not restart LSP server on an unrelated change"
13652 );
13653
13654 update_test_project_settings(cx, |project_settings| {
13655 project_settings.lsp.insert(
13656 "Some other server name".into(),
13657 LspSettings {
13658 binary: None,
13659 settings: None,
13660 initialization_options: Some(json!({
13661 "some other init value": false
13662 })),
13663 enable_lsp_tasks: false,
13664 },
13665 );
13666 });
13667 cx.executor().run_until_parked();
13668 assert_eq!(
13669 server_restarts.load(atomic::Ordering::Acquire),
13670 0,
13671 "Should not restart LSP server on an unrelated LSP settings change"
13672 );
13673
13674 update_test_project_settings(cx, |project_settings| {
13675 project_settings.lsp.insert(
13676 language_server_name.into(),
13677 LspSettings {
13678 binary: None,
13679 settings: None,
13680 initialization_options: Some(json!({
13681 "anotherInitValue": false
13682 })),
13683 enable_lsp_tasks: false,
13684 },
13685 );
13686 });
13687 cx.executor().run_until_parked();
13688 assert_eq!(
13689 server_restarts.load(atomic::Ordering::Acquire),
13690 1,
13691 "Should restart LSP server on a related LSP settings change"
13692 );
13693
13694 update_test_project_settings(cx, |project_settings| {
13695 project_settings.lsp.insert(
13696 language_server_name.into(),
13697 LspSettings {
13698 binary: None,
13699 settings: None,
13700 initialization_options: Some(json!({
13701 "anotherInitValue": false
13702 })),
13703 enable_lsp_tasks: false,
13704 },
13705 );
13706 });
13707 cx.executor().run_until_parked();
13708 assert_eq!(
13709 server_restarts.load(atomic::Ordering::Acquire),
13710 1,
13711 "Should not restart LSP server on a related LSP settings change that is the same"
13712 );
13713
13714 update_test_project_settings(cx, |project_settings| {
13715 project_settings.lsp.insert(
13716 language_server_name.into(),
13717 LspSettings {
13718 binary: None,
13719 settings: None,
13720 initialization_options: None,
13721 enable_lsp_tasks: false,
13722 },
13723 );
13724 });
13725 cx.executor().run_until_parked();
13726 assert_eq!(
13727 server_restarts.load(atomic::Ordering::Acquire),
13728 2,
13729 "Should restart LSP server on another related LSP settings change"
13730 );
13731}
13732
13733#[gpui::test]
13734async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13735 init_test(cx, |_| {});
13736
13737 let mut cx = EditorLspTestContext::new_rust(
13738 lsp::ServerCapabilities {
13739 completion_provider: Some(lsp::CompletionOptions {
13740 trigger_characters: Some(vec![".".to_string()]),
13741 resolve_provider: Some(true),
13742 ..Default::default()
13743 }),
13744 ..Default::default()
13745 },
13746 cx,
13747 )
13748 .await;
13749
13750 cx.set_state("fn main() { let a = 2ˇ; }");
13751 cx.simulate_keystroke(".");
13752 let completion_item = lsp::CompletionItem {
13753 label: "some".into(),
13754 kind: Some(lsp::CompletionItemKind::SNIPPET),
13755 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13756 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13757 kind: lsp::MarkupKind::Markdown,
13758 value: "```rust\nSome(2)\n```".to_string(),
13759 })),
13760 deprecated: Some(false),
13761 sort_text: Some("fffffff2".to_string()),
13762 filter_text: Some("some".to_string()),
13763 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13764 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13765 range: lsp::Range {
13766 start: lsp::Position {
13767 line: 0,
13768 character: 22,
13769 },
13770 end: lsp::Position {
13771 line: 0,
13772 character: 22,
13773 },
13774 },
13775 new_text: "Some(2)".to_string(),
13776 })),
13777 additional_text_edits: Some(vec![lsp::TextEdit {
13778 range: lsp::Range {
13779 start: lsp::Position {
13780 line: 0,
13781 character: 20,
13782 },
13783 end: lsp::Position {
13784 line: 0,
13785 character: 22,
13786 },
13787 },
13788 new_text: "".to_string(),
13789 }]),
13790 ..Default::default()
13791 };
13792
13793 let closure_completion_item = completion_item.clone();
13794 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13795 let task_completion_item = closure_completion_item.clone();
13796 async move {
13797 Ok(Some(lsp::CompletionResponse::Array(vec![
13798 task_completion_item,
13799 ])))
13800 }
13801 });
13802
13803 request.next().await;
13804
13805 cx.condition(|editor, _| editor.context_menu_visible())
13806 .await;
13807 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13808 editor
13809 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13810 .unwrap()
13811 });
13812 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13813
13814 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13815 let task_completion_item = completion_item.clone();
13816 async move { Ok(task_completion_item) }
13817 })
13818 .next()
13819 .await
13820 .unwrap();
13821 apply_additional_edits.await.unwrap();
13822 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13823}
13824
13825#[gpui::test]
13826async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13827 init_test(cx, |_| {});
13828
13829 let mut cx = EditorLspTestContext::new_rust(
13830 lsp::ServerCapabilities {
13831 completion_provider: Some(lsp::CompletionOptions {
13832 trigger_characters: Some(vec![".".to_string()]),
13833 resolve_provider: Some(true),
13834 ..Default::default()
13835 }),
13836 ..Default::default()
13837 },
13838 cx,
13839 )
13840 .await;
13841
13842 cx.set_state("fn main() { let a = 2ˇ; }");
13843 cx.simulate_keystroke(".");
13844
13845 let item1 = lsp::CompletionItem {
13846 label: "method id()".to_string(),
13847 filter_text: Some("id".to_string()),
13848 detail: None,
13849 documentation: None,
13850 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13851 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13852 new_text: ".id".to_string(),
13853 })),
13854 ..lsp::CompletionItem::default()
13855 };
13856
13857 let item2 = lsp::CompletionItem {
13858 label: "other".to_string(),
13859 filter_text: Some("other".to_string()),
13860 detail: None,
13861 documentation: None,
13862 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13863 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13864 new_text: ".other".to_string(),
13865 })),
13866 ..lsp::CompletionItem::default()
13867 };
13868
13869 let item1 = item1.clone();
13870 cx.set_request_handler::<lsp::request::Completion, _, _>({
13871 let item1 = item1.clone();
13872 move |_, _, _| {
13873 let item1 = item1.clone();
13874 let item2 = item2.clone();
13875 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13876 }
13877 })
13878 .next()
13879 .await;
13880
13881 cx.condition(|editor, _| editor.context_menu_visible())
13882 .await;
13883 cx.update_editor(|editor, _, _| {
13884 let context_menu = editor.context_menu.borrow_mut();
13885 let context_menu = context_menu
13886 .as_ref()
13887 .expect("Should have the context menu deployed");
13888 match context_menu {
13889 CodeContextMenu::Completions(completions_menu) => {
13890 let completions = completions_menu.completions.borrow_mut();
13891 assert_eq!(
13892 completions
13893 .iter()
13894 .map(|completion| &completion.label.text)
13895 .collect::<Vec<_>>(),
13896 vec!["method id()", "other"]
13897 )
13898 }
13899 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13900 }
13901 });
13902
13903 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13904 let item1 = item1.clone();
13905 move |_, item_to_resolve, _| {
13906 let item1 = item1.clone();
13907 async move {
13908 if item1 == item_to_resolve {
13909 Ok(lsp::CompletionItem {
13910 label: "method id()".to_string(),
13911 filter_text: Some("id".to_string()),
13912 detail: Some("Now resolved!".to_string()),
13913 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13914 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13915 range: lsp::Range::new(
13916 lsp::Position::new(0, 22),
13917 lsp::Position::new(0, 22),
13918 ),
13919 new_text: ".id".to_string(),
13920 })),
13921 ..lsp::CompletionItem::default()
13922 })
13923 } else {
13924 Ok(item_to_resolve)
13925 }
13926 }
13927 }
13928 })
13929 .next()
13930 .await
13931 .unwrap();
13932 cx.run_until_parked();
13933
13934 cx.update_editor(|editor, window, cx| {
13935 editor.context_menu_next(&Default::default(), window, cx);
13936 });
13937
13938 cx.update_editor(|editor, _, _| {
13939 let context_menu = editor.context_menu.borrow_mut();
13940 let context_menu = context_menu
13941 .as_ref()
13942 .expect("Should have the context menu deployed");
13943 match context_menu {
13944 CodeContextMenu::Completions(completions_menu) => {
13945 let completions = completions_menu.completions.borrow_mut();
13946 assert_eq!(
13947 completions
13948 .iter()
13949 .map(|completion| &completion.label.text)
13950 .collect::<Vec<_>>(),
13951 vec!["method id() Now resolved!", "other"],
13952 "Should update first completion label, but not second as the filter text did not match."
13953 );
13954 }
13955 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13956 }
13957 });
13958}
13959
13960#[gpui::test]
13961async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13962 init_test(cx, |_| {});
13963
13964 let mut cx = EditorLspTestContext::new_rust(
13965 lsp::ServerCapabilities {
13966 completion_provider: Some(lsp::CompletionOptions {
13967 trigger_characters: Some(vec![".".to_string()]),
13968 resolve_provider: Some(true),
13969 ..Default::default()
13970 }),
13971 ..Default::default()
13972 },
13973 cx,
13974 )
13975 .await;
13976
13977 cx.set_state("fn main() { let a = 2ˇ; }");
13978 cx.simulate_keystroke(".");
13979
13980 let unresolved_item_1 = lsp::CompletionItem {
13981 label: "id".to_string(),
13982 filter_text: Some("id".to_string()),
13983 detail: None,
13984 documentation: None,
13985 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13986 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13987 new_text: ".id".to_string(),
13988 })),
13989 ..lsp::CompletionItem::default()
13990 };
13991 let resolved_item_1 = lsp::CompletionItem {
13992 additional_text_edits: Some(vec![lsp::TextEdit {
13993 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13994 new_text: "!!".to_string(),
13995 }]),
13996 ..unresolved_item_1.clone()
13997 };
13998 let unresolved_item_2 = lsp::CompletionItem {
13999 label: "other".to_string(),
14000 filter_text: Some("other".to_string()),
14001 detail: None,
14002 documentation: None,
14003 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14004 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14005 new_text: ".other".to_string(),
14006 })),
14007 ..lsp::CompletionItem::default()
14008 };
14009 let resolved_item_2 = lsp::CompletionItem {
14010 additional_text_edits: Some(vec![lsp::TextEdit {
14011 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14012 new_text: "??".to_string(),
14013 }]),
14014 ..unresolved_item_2.clone()
14015 };
14016
14017 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14018 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14019 cx.lsp
14020 .server
14021 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14022 let unresolved_item_1 = unresolved_item_1.clone();
14023 let resolved_item_1 = resolved_item_1.clone();
14024 let unresolved_item_2 = unresolved_item_2.clone();
14025 let resolved_item_2 = resolved_item_2.clone();
14026 let resolve_requests_1 = resolve_requests_1.clone();
14027 let resolve_requests_2 = resolve_requests_2.clone();
14028 move |unresolved_request, _| {
14029 let unresolved_item_1 = unresolved_item_1.clone();
14030 let resolved_item_1 = resolved_item_1.clone();
14031 let unresolved_item_2 = unresolved_item_2.clone();
14032 let resolved_item_2 = resolved_item_2.clone();
14033 let resolve_requests_1 = resolve_requests_1.clone();
14034 let resolve_requests_2 = resolve_requests_2.clone();
14035 async move {
14036 if unresolved_request == unresolved_item_1 {
14037 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14038 Ok(resolved_item_1.clone())
14039 } else if unresolved_request == unresolved_item_2 {
14040 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14041 Ok(resolved_item_2.clone())
14042 } else {
14043 panic!("Unexpected completion item {unresolved_request:?}")
14044 }
14045 }
14046 }
14047 })
14048 .detach();
14049
14050 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14051 let unresolved_item_1 = unresolved_item_1.clone();
14052 let unresolved_item_2 = unresolved_item_2.clone();
14053 async move {
14054 Ok(Some(lsp::CompletionResponse::Array(vec![
14055 unresolved_item_1,
14056 unresolved_item_2,
14057 ])))
14058 }
14059 })
14060 .next()
14061 .await;
14062
14063 cx.condition(|editor, _| editor.context_menu_visible())
14064 .await;
14065 cx.update_editor(|editor, _, _| {
14066 let context_menu = editor.context_menu.borrow_mut();
14067 let context_menu = context_menu
14068 .as_ref()
14069 .expect("Should have the context menu deployed");
14070 match context_menu {
14071 CodeContextMenu::Completions(completions_menu) => {
14072 let completions = completions_menu.completions.borrow_mut();
14073 assert_eq!(
14074 completions
14075 .iter()
14076 .map(|completion| &completion.label.text)
14077 .collect::<Vec<_>>(),
14078 vec!["id", "other"]
14079 )
14080 }
14081 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14082 }
14083 });
14084 cx.run_until_parked();
14085
14086 cx.update_editor(|editor, window, cx| {
14087 editor.context_menu_next(&ContextMenuNext, window, cx);
14088 });
14089 cx.run_until_parked();
14090 cx.update_editor(|editor, window, cx| {
14091 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14092 });
14093 cx.run_until_parked();
14094 cx.update_editor(|editor, window, cx| {
14095 editor.context_menu_next(&ContextMenuNext, window, cx);
14096 });
14097 cx.run_until_parked();
14098 cx.update_editor(|editor, window, cx| {
14099 editor
14100 .compose_completion(&ComposeCompletion::default(), window, cx)
14101 .expect("No task returned")
14102 })
14103 .await
14104 .expect("Completion failed");
14105 cx.run_until_parked();
14106
14107 cx.update_editor(|editor, _, cx| {
14108 assert_eq!(
14109 resolve_requests_1.load(atomic::Ordering::Acquire),
14110 1,
14111 "Should always resolve once despite multiple selections"
14112 );
14113 assert_eq!(
14114 resolve_requests_2.load(atomic::Ordering::Acquire),
14115 1,
14116 "Should always resolve once after multiple selections and applying the completion"
14117 );
14118 assert_eq!(
14119 editor.text(cx),
14120 "fn main() { let a = ??.other; }",
14121 "Should use resolved data when applying the completion"
14122 );
14123 });
14124}
14125
14126#[gpui::test]
14127async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14128 init_test(cx, |_| {});
14129
14130 let item_0 = lsp::CompletionItem {
14131 label: "abs".into(),
14132 insert_text: Some("abs".into()),
14133 data: Some(json!({ "very": "special"})),
14134 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14135 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14136 lsp::InsertReplaceEdit {
14137 new_text: "abs".to_string(),
14138 insert: lsp::Range::default(),
14139 replace: lsp::Range::default(),
14140 },
14141 )),
14142 ..lsp::CompletionItem::default()
14143 };
14144 let items = iter::once(item_0.clone())
14145 .chain((11..51).map(|i| lsp::CompletionItem {
14146 label: format!("item_{}", i),
14147 insert_text: Some(format!("item_{}", i)),
14148 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14149 ..lsp::CompletionItem::default()
14150 }))
14151 .collect::<Vec<_>>();
14152
14153 let default_commit_characters = vec!["?".to_string()];
14154 let default_data = json!({ "default": "data"});
14155 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14156 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14157 let default_edit_range = lsp::Range {
14158 start: lsp::Position {
14159 line: 0,
14160 character: 5,
14161 },
14162 end: lsp::Position {
14163 line: 0,
14164 character: 5,
14165 },
14166 };
14167
14168 let mut cx = EditorLspTestContext::new_rust(
14169 lsp::ServerCapabilities {
14170 completion_provider: Some(lsp::CompletionOptions {
14171 trigger_characters: Some(vec![".".to_string()]),
14172 resolve_provider: Some(true),
14173 ..Default::default()
14174 }),
14175 ..Default::default()
14176 },
14177 cx,
14178 )
14179 .await;
14180
14181 cx.set_state("fn main() { let a = 2ˇ; }");
14182 cx.simulate_keystroke(".");
14183
14184 let completion_data = default_data.clone();
14185 let completion_characters = default_commit_characters.clone();
14186 let completion_items = items.clone();
14187 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14188 let default_data = completion_data.clone();
14189 let default_commit_characters = completion_characters.clone();
14190 let items = completion_items.clone();
14191 async move {
14192 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14193 items,
14194 item_defaults: Some(lsp::CompletionListItemDefaults {
14195 data: Some(default_data.clone()),
14196 commit_characters: Some(default_commit_characters.clone()),
14197 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14198 default_edit_range,
14199 )),
14200 insert_text_format: Some(default_insert_text_format),
14201 insert_text_mode: Some(default_insert_text_mode),
14202 }),
14203 ..lsp::CompletionList::default()
14204 })))
14205 }
14206 })
14207 .next()
14208 .await;
14209
14210 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14211 cx.lsp
14212 .server
14213 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14214 let closure_resolved_items = resolved_items.clone();
14215 move |item_to_resolve, _| {
14216 let closure_resolved_items = closure_resolved_items.clone();
14217 async move {
14218 closure_resolved_items.lock().push(item_to_resolve.clone());
14219 Ok(item_to_resolve)
14220 }
14221 }
14222 })
14223 .detach();
14224
14225 cx.condition(|editor, _| editor.context_menu_visible())
14226 .await;
14227 cx.run_until_parked();
14228 cx.update_editor(|editor, _, _| {
14229 let menu = editor.context_menu.borrow_mut();
14230 match menu.as_ref().expect("should have the completions menu") {
14231 CodeContextMenu::Completions(completions_menu) => {
14232 assert_eq!(
14233 completions_menu
14234 .entries
14235 .borrow()
14236 .iter()
14237 .map(|mat| mat.string.clone())
14238 .collect::<Vec<String>>(),
14239 items
14240 .iter()
14241 .map(|completion| completion.label.clone())
14242 .collect::<Vec<String>>()
14243 );
14244 }
14245 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14246 }
14247 });
14248 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14249 // with 4 from the end.
14250 assert_eq!(
14251 *resolved_items.lock(),
14252 [&items[0..16], &items[items.len() - 4..items.len()]]
14253 .concat()
14254 .iter()
14255 .cloned()
14256 .map(|mut item| {
14257 if item.data.is_none() {
14258 item.data = Some(default_data.clone());
14259 }
14260 item
14261 })
14262 .collect::<Vec<lsp::CompletionItem>>(),
14263 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14264 );
14265 resolved_items.lock().clear();
14266
14267 cx.update_editor(|editor, window, cx| {
14268 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14269 });
14270 cx.run_until_parked();
14271 // Completions that have already been resolved are skipped.
14272 assert_eq!(
14273 *resolved_items.lock(),
14274 items[items.len() - 16..items.len() - 4]
14275 .iter()
14276 .cloned()
14277 .map(|mut item| {
14278 if item.data.is_none() {
14279 item.data = Some(default_data.clone());
14280 }
14281 item
14282 })
14283 .collect::<Vec<lsp::CompletionItem>>()
14284 );
14285 resolved_items.lock().clear();
14286}
14287
14288#[gpui::test]
14289async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14290 init_test(cx, |_| {});
14291
14292 let mut cx = EditorLspTestContext::new(
14293 Language::new(
14294 LanguageConfig {
14295 matcher: LanguageMatcher {
14296 path_suffixes: vec!["jsx".into()],
14297 ..Default::default()
14298 },
14299 overrides: [(
14300 "element".into(),
14301 LanguageConfigOverride {
14302 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14303 ..Default::default()
14304 },
14305 )]
14306 .into_iter()
14307 .collect(),
14308 ..Default::default()
14309 },
14310 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14311 )
14312 .with_override_query("(jsx_self_closing_element) @element")
14313 .unwrap(),
14314 lsp::ServerCapabilities {
14315 completion_provider: Some(lsp::CompletionOptions {
14316 trigger_characters: Some(vec![":".to_string()]),
14317 ..Default::default()
14318 }),
14319 ..Default::default()
14320 },
14321 cx,
14322 )
14323 .await;
14324
14325 cx.lsp
14326 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14327 Ok(Some(lsp::CompletionResponse::Array(vec![
14328 lsp::CompletionItem {
14329 label: "bg-blue".into(),
14330 ..Default::default()
14331 },
14332 lsp::CompletionItem {
14333 label: "bg-red".into(),
14334 ..Default::default()
14335 },
14336 lsp::CompletionItem {
14337 label: "bg-yellow".into(),
14338 ..Default::default()
14339 },
14340 ])))
14341 });
14342
14343 cx.set_state(r#"<p class="bgˇ" />"#);
14344
14345 // Trigger completion when typing a dash, because the dash is an extra
14346 // word character in the 'element' scope, which contains the cursor.
14347 cx.simulate_keystroke("-");
14348 cx.executor().run_until_parked();
14349 cx.update_editor(|editor, _, _| {
14350 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14351 {
14352 assert_eq!(
14353 completion_menu_entries(&menu),
14354 &["bg-red", "bg-blue", "bg-yellow"]
14355 );
14356 } else {
14357 panic!("expected completion menu to be open");
14358 }
14359 });
14360
14361 cx.simulate_keystroke("l");
14362 cx.executor().run_until_parked();
14363 cx.update_editor(|editor, _, _| {
14364 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14365 {
14366 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14367 } else {
14368 panic!("expected completion menu to be open");
14369 }
14370 });
14371
14372 // When filtering completions, consider the character after the '-' to
14373 // be the start of a subword.
14374 cx.set_state(r#"<p class="yelˇ" />"#);
14375 cx.simulate_keystroke("l");
14376 cx.executor().run_until_parked();
14377 cx.update_editor(|editor, _, _| {
14378 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14379 {
14380 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14381 } else {
14382 panic!("expected completion menu to be open");
14383 }
14384 });
14385}
14386
14387fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14388 let entries = menu.entries.borrow();
14389 entries.iter().map(|mat| mat.string.clone()).collect()
14390}
14391
14392#[gpui::test]
14393async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14394 init_test(cx, |settings| {
14395 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14396 FormatterList(vec![Formatter::Prettier].into()),
14397 ))
14398 });
14399
14400 let fs = FakeFs::new(cx.executor());
14401 fs.insert_file(path!("/file.ts"), Default::default()).await;
14402
14403 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14404 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14405
14406 language_registry.add(Arc::new(Language::new(
14407 LanguageConfig {
14408 name: "TypeScript".into(),
14409 matcher: LanguageMatcher {
14410 path_suffixes: vec!["ts".to_string()],
14411 ..Default::default()
14412 },
14413 ..Default::default()
14414 },
14415 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14416 )));
14417 update_test_language_settings(cx, |settings| {
14418 settings.defaults.prettier = Some(PrettierSettings {
14419 allowed: true,
14420 ..PrettierSettings::default()
14421 });
14422 });
14423
14424 let test_plugin = "test_plugin";
14425 let _ = language_registry.register_fake_lsp(
14426 "TypeScript",
14427 FakeLspAdapter {
14428 prettier_plugins: vec![test_plugin],
14429 ..Default::default()
14430 },
14431 );
14432
14433 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14434 let buffer = project
14435 .update(cx, |project, cx| {
14436 project.open_local_buffer(path!("/file.ts"), cx)
14437 })
14438 .await
14439 .unwrap();
14440
14441 let buffer_text = "one\ntwo\nthree\n";
14442 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14443 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14444 editor.update_in(cx, |editor, window, cx| {
14445 editor.set_text(buffer_text, window, cx)
14446 });
14447
14448 editor
14449 .update_in(cx, |editor, window, cx| {
14450 editor.perform_format(
14451 project.clone(),
14452 FormatTrigger::Manual,
14453 FormatTarget::Buffers,
14454 window,
14455 cx,
14456 )
14457 })
14458 .unwrap()
14459 .await;
14460 assert_eq!(
14461 editor.update(cx, |editor, cx| editor.text(cx)),
14462 buffer_text.to_string() + prettier_format_suffix,
14463 "Test prettier formatting was not applied to the original buffer text",
14464 );
14465
14466 update_test_language_settings(cx, |settings| {
14467 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14468 });
14469 let format = editor.update_in(cx, |editor, window, cx| {
14470 editor.perform_format(
14471 project.clone(),
14472 FormatTrigger::Manual,
14473 FormatTarget::Buffers,
14474 window,
14475 cx,
14476 )
14477 });
14478 format.await.unwrap();
14479 assert_eq!(
14480 editor.update(cx, |editor, cx| editor.text(cx)),
14481 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14482 "Autoformatting (via test prettier) was not applied to the original buffer text",
14483 );
14484}
14485
14486#[gpui::test]
14487async fn test_addition_reverts(cx: &mut TestAppContext) {
14488 init_test(cx, |_| {});
14489 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14490 let base_text = indoc! {r#"
14491 struct Row;
14492 struct Row1;
14493 struct Row2;
14494
14495 struct Row4;
14496 struct Row5;
14497 struct Row6;
14498
14499 struct Row8;
14500 struct Row9;
14501 struct Row10;"#};
14502
14503 // When addition hunks are not adjacent to carets, no hunk revert is performed
14504 assert_hunk_revert(
14505 indoc! {r#"struct Row;
14506 struct Row1;
14507 struct Row1.1;
14508 struct Row1.2;
14509 struct Row2;ˇ
14510
14511 struct Row4;
14512 struct Row5;
14513 struct Row6;
14514
14515 struct Row8;
14516 ˇstruct Row9;
14517 struct Row9.1;
14518 struct Row9.2;
14519 struct Row9.3;
14520 struct Row10;"#},
14521 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14522 indoc! {r#"struct Row;
14523 struct Row1;
14524 struct Row1.1;
14525 struct Row1.2;
14526 struct Row2;ˇ
14527
14528 struct Row4;
14529 struct Row5;
14530 struct Row6;
14531
14532 struct Row8;
14533 ˇstruct Row9;
14534 struct Row9.1;
14535 struct Row9.2;
14536 struct Row9.3;
14537 struct Row10;"#},
14538 base_text,
14539 &mut cx,
14540 );
14541 // Same for selections
14542 assert_hunk_revert(
14543 indoc! {r#"struct Row;
14544 struct Row1;
14545 struct Row2;
14546 struct Row2.1;
14547 struct Row2.2;
14548 «ˇ
14549 struct Row4;
14550 struct» Row5;
14551 «struct Row6;
14552 ˇ»
14553 struct Row9.1;
14554 struct Row9.2;
14555 struct Row9.3;
14556 struct Row8;
14557 struct Row9;
14558 struct Row10;"#},
14559 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14560 indoc! {r#"struct Row;
14561 struct Row1;
14562 struct Row2;
14563 struct Row2.1;
14564 struct Row2.2;
14565 «ˇ
14566 struct Row4;
14567 struct» Row5;
14568 «struct Row6;
14569 ˇ»
14570 struct Row9.1;
14571 struct Row9.2;
14572 struct Row9.3;
14573 struct Row8;
14574 struct Row9;
14575 struct Row10;"#},
14576 base_text,
14577 &mut cx,
14578 );
14579
14580 // When carets and selections intersect the addition hunks, those are reverted.
14581 // Adjacent carets got merged.
14582 assert_hunk_revert(
14583 indoc! {r#"struct Row;
14584 ˇ// something on the top
14585 struct Row1;
14586 struct Row2;
14587 struct Roˇw3.1;
14588 struct Row2.2;
14589 struct Row2.3;ˇ
14590
14591 struct Row4;
14592 struct ˇRow5.1;
14593 struct Row5.2;
14594 struct «Rowˇ»5.3;
14595 struct Row5;
14596 struct Row6;
14597 ˇ
14598 struct Row9.1;
14599 struct «Rowˇ»9.2;
14600 struct «ˇRow»9.3;
14601 struct Row8;
14602 struct Row9;
14603 «ˇ// something on bottom»
14604 struct Row10;"#},
14605 vec![
14606 DiffHunkStatusKind::Added,
14607 DiffHunkStatusKind::Added,
14608 DiffHunkStatusKind::Added,
14609 DiffHunkStatusKind::Added,
14610 DiffHunkStatusKind::Added,
14611 ],
14612 indoc! {r#"struct Row;
14613 ˇstruct Row1;
14614 struct Row2;
14615 ˇ
14616 struct Row4;
14617 ˇstruct Row5;
14618 struct Row6;
14619 ˇ
14620 ˇstruct Row8;
14621 struct Row9;
14622 ˇstruct Row10;"#},
14623 base_text,
14624 &mut cx,
14625 );
14626}
14627
14628#[gpui::test]
14629async fn test_modification_reverts(cx: &mut TestAppContext) {
14630 init_test(cx, |_| {});
14631 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14632 let base_text = indoc! {r#"
14633 struct Row;
14634 struct Row1;
14635 struct Row2;
14636
14637 struct Row4;
14638 struct Row5;
14639 struct Row6;
14640
14641 struct Row8;
14642 struct Row9;
14643 struct Row10;"#};
14644
14645 // Modification hunks behave the same as the addition ones.
14646 assert_hunk_revert(
14647 indoc! {r#"struct Row;
14648 struct Row1;
14649 struct Row33;
14650 ˇ
14651 struct Row4;
14652 struct Row5;
14653 struct Row6;
14654 ˇ
14655 struct Row99;
14656 struct Row9;
14657 struct Row10;"#},
14658 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14659 indoc! {r#"struct Row;
14660 struct Row1;
14661 struct Row33;
14662 ˇ
14663 struct Row4;
14664 struct Row5;
14665 struct Row6;
14666 ˇ
14667 struct Row99;
14668 struct Row9;
14669 struct Row10;"#},
14670 base_text,
14671 &mut cx,
14672 );
14673 assert_hunk_revert(
14674 indoc! {r#"struct Row;
14675 struct Row1;
14676 struct Row33;
14677 «ˇ
14678 struct Row4;
14679 struct» Row5;
14680 «struct Row6;
14681 ˇ»
14682 struct Row99;
14683 struct Row9;
14684 struct Row10;"#},
14685 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14686 indoc! {r#"struct Row;
14687 struct Row1;
14688 struct Row33;
14689 «ˇ
14690 struct Row4;
14691 struct» Row5;
14692 «struct Row6;
14693 ˇ»
14694 struct Row99;
14695 struct Row9;
14696 struct Row10;"#},
14697 base_text,
14698 &mut cx,
14699 );
14700
14701 assert_hunk_revert(
14702 indoc! {r#"ˇstruct Row1.1;
14703 struct Row1;
14704 «ˇstr»uct Row22;
14705
14706 struct ˇRow44;
14707 struct Row5;
14708 struct «Rˇ»ow66;ˇ
14709
14710 «struˇ»ct Row88;
14711 struct Row9;
14712 struct Row1011;ˇ"#},
14713 vec![
14714 DiffHunkStatusKind::Modified,
14715 DiffHunkStatusKind::Modified,
14716 DiffHunkStatusKind::Modified,
14717 DiffHunkStatusKind::Modified,
14718 DiffHunkStatusKind::Modified,
14719 DiffHunkStatusKind::Modified,
14720 ],
14721 indoc! {r#"struct Row;
14722 ˇstruct Row1;
14723 struct Row2;
14724 ˇ
14725 struct Row4;
14726 ˇstruct Row5;
14727 struct Row6;
14728 ˇ
14729 struct Row8;
14730 ˇstruct Row9;
14731 struct Row10;ˇ"#},
14732 base_text,
14733 &mut cx,
14734 );
14735}
14736
14737#[gpui::test]
14738async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14739 init_test(cx, |_| {});
14740 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14741 let base_text = indoc! {r#"
14742 one
14743
14744 two
14745 three
14746 "#};
14747
14748 cx.set_head_text(base_text);
14749 cx.set_state("\nˇ\n");
14750 cx.executor().run_until_parked();
14751 cx.update_editor(|editor, _window, cx| {
14752 editor.expand_selected_diff_hunks(cx);
14753 });
14754 cx.executor().run_until_parked();
14755 cx.update_editor(|editor, window, cx| {
14756 editor.backspace(&Default::default(), window, cx);
14757 });
14758 cx.run_until_parked();
14759 cx.assert_state_with_diff(
14760 indoc! {r#"
14761
14762 - two
14763 - threeˇ
14764 +
14765 "#}
14766 .to_string(),
14767 );
14768}
14769
14770#[gpui::test]
14771async fn test_deletion_reverts(cx: &mut TestAppContext) {
14772 init_test(cx, |_| {});
14773 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14774 let base_text = indoc! {r#"struct Row;
14775struct Row1;
14776struct Row2;
14777
14778struct Row4;
14779struct Row5;
14780struct Row6;
14781
14782struct Row8;
14783struct Row9;
14784struct Row10;"#};
14785
14786 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14787 assert_hunk_revert(
14788 indoc! {r#"struct Row;
14789 struct Row2;
14790
14791 ˇstruct Row4;
14792 struct Row5;
14793 struct Row6;
14794 ˇ
14795 struct Row8;
14796 struct Row10;"#},
14797 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14798 indoc! {r#"struct Row;
14799 struct Row2;
14800
14801 ˇstruct Row4;
14802 struct Row5;
14803 struct Row6;
14804 ˇ
14805 struct Row8;
14806 struct Row10;"#},
14807 base_text,
14808 &mut cx,
14809 );
14810 assert_hunk_revert(
14811 indoc! {r#"struct Row;
14812 struct Row2;
14813
14814 «ˇstruct Row4;
14815 struct» Row5;
14816 «struct Row6;
14817 ˇ»
14818 struct Row8;
14819 struct Row10;"#},
14820 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14821 indoc! {r#"struct Row;
14822 struct Row2;
14823
14824 «ˇstruct Row4;
14825 struct» Row5;
14826 «struct Row6;
14827 ˇ»
14828 struct Row8;
14829 struct Row10;"#},
14830 base_text,
14831 &mut cx,
14832 );
14833
14834 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14835 assert_hunk_revert(
14836 indoc! {r#"struct Row;
14837 ˇstruct Row2;
14838
14839 struct Row4;
14840 struct Row5;
14841 struct Row6;
14842
14843 struct Row8;ˇ
14844 struct Row10;"#},
14845 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14846 indoc! {r#"struct Row;
14847 struct Row1;
14848 ˇstruct Row2;
14849
14850 struct Row4;
14851 struct Row5;
14852 struct Row6;
14853
14854 struct Row8;ˇ
14855 struct Row9;
14856 struct Row10;"#},
14857 base_text,
14858 &mut cx,
14859 );
14860 assert_hunk_revert(
14861 indoc! {r#"struct Row;
14862 struct Row2«ˇ;
14863 struct Row4;
14864 struct» Row5;
14865 «struct Row6;
14866
14867 struct Row8;ˇ»
14868 struct Row10;"#},
14869 vec![
14870 DiffHunkStatusKind::Deleted,
14871 DiffHunkStatusKind::Deleted,
14872 DiffHunkStatusKind::Deleted,
14873 ],
14874 indoc! {r#"struct Row;
14875 struct Row1;
14876 struct Row2«ˇ;
14877
14878 struct Row4;
14879 struct» Row5;
14880 «struct Row6;
14881
14882 struct Row8;ˇ»
14883 struct Row9;
14884 struct Row10;"#},
14885 base_text,
14886 &mut cx,
14887 );
14888}
14889
14890#[gpui::test]
14891async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14892 init_test(cx, |_| {});
14893
14894 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14895 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14896 let base_text_3 =
14897 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14898
14899 let text_1 = edit_first_char_of_every_line(base_text_1);
14900 let text_2 = edit_first_char_of_every_line(base_text_2);
14901 let text_3 = edit_first_char_of_every_line(base_text_3);
14902
14903 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14904 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14905 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14906
14907 let multibuffer = cx.new(|cx| {
14908 let mut multibuffer = MultiBuffer::new(ReadWrite);
14909 multibuffer.push_excerpts(
14910 buffer_1.clone(),
14911 [
14912 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14913 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14914 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14915 ],
14916 cx,
14917 );
14918 multibuffer.push_excerpts(
14919 buffer_2.clone(),
14920 [
14921 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14922 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14923 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14924 ],
14925 cx,
14926 );
14927 multibuffer.push_excerpts(
14928 buffer_3.clone(),
14929 [
14930 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14931 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14932 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14933 ],
14934 cx,
14935 );
14936 multibuffer
14937 });
14938
14939 let fs = FakeFs::new(cx.executor());
14940 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14941 let (editor, cx) = cx
14942 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14943 editor.update_in(cx, |editor, _window, cx| {
14944 for (buffer, diff_base) in [
14945 (buffer_1.clone(), base_text_1),
14946 (buffer_2.clone(), base_text_2),
14947 (buffer_3.clone(), base_text_3),
14948 ] {
14949 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14950 editor
14951 .buffer
14952 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14953 }
14954 });
14955 cx.executor().run_until_parked();
14956
14957 editor.update_in(cx, |editor, window, cx| {
14958 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}");
14959 editor.select_all(&SelectAll, window, cx);
14960 editor.git_restore(&Default::default(), window, cx);
14961 });
14962 cx.executor().run_until_parked();
14963
14964 // When all ranges are selected, all buffer hunks are reverted.
14965 editor.update(cx, |editor, cx| {
14966 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");
14967 });
14968 buffer_1.update(cx, |buffer, _| {
14969 assert_eq!(buffer.text(), base_text_1);
14970 });
14971 buffer_2.update(cx, |buffer, _| {
14972 assert_eq!(buffer.text(), base_text_2);
14973 });
14974 buffer_3.update(cx, |buffer, _| {
14975 assert_eq!(buffer.text(), base_text_3);
14976 });
14977
14978 editor.update_in(cx, |editor, window, cx| {
14979 editor.undo(&Default::default(), window, cx);
14980 });
14981
14982 editor.update_in(cx, |editor, window, cx| {
14983 editor.change_selections(None, window, cx, |s| {
14984 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14985 });
14986 editor.git_restore(&Default::default(), window, cx);
14987 });
14988
14989 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14990 // but not affect buffer_2 and its related excerpts.
14991 editor.update(cx, |editor, cx| {
14992 assert_eq!(
14993 editor.text(cx),
14994 "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}"
14995 );
14996 });
14997 buffer_1.update(cx, |buffer, _| {
14998 assert_eq!(buffer.text(), base_text_1);
14999 });
15000 buffer_2.update(cx, |buffer, _| {
15001 assert_eq!(
15002 buffer.text(),
15003 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15004 );
15005 });
15006 buffer_3.update(cx, |buffer, _| {
15007 assert_eq!(
15008 buffer.text(),
15009 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15010 );
15011 });
15012
15013 fn edit_first_char_of_every_line(text: &str) -> String {
15014 text.split('\n')
15015 .map(|line| format!("X{}", &line[1..]))
15016 .collect::<Vec<_>>()
15017 .join("\n")
15018 }
15019}
15020
15021#[gpui::test]
15022async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15023 init_test(cx, |_| {});
15024
15025 let cols = 4;
15026 let rows = 10;
15027 let sample_text_1 = sample_text(rows, cols, 'a');
15028 assert_eq!(
15029 sample_text_1,
15030 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15031 );
15032 let sample_text_2 = sample_text(rows, cols, 'l');
15033 assert_eq!(
15034 sample_text_2,
15035 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15036 );
15037 let sample_text_3 = sample_text(rows, cols, 'v');
15038 assert_eq!(
15039 sample_text_3,
15040 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15041 );
15042
15043 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15044 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15045 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15046
15047 let multi_buffer = cx.new(|cx| {
15048 let mut multibuffer = MultiBuffer::new(ReadWrite);
15049 multibuffer.push_excerpts(
15050 buffer_1.clone(),
15051 [
15052 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15053 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15054 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15055 ],
15056 cx,
15057 );
15058 multibuffer.push_excerpts(
15059 buffer_2.clone(),
15060 [
15061 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15062 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15063 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15064 ],
15065 cx,
15066 );
15067 multibuffer.push_excerpts(
15068 buffer_3.clone(),
15069 [
15070 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15071 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15072 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15073 ],
15074 cx,
15075 );
15076 multibuffer
15077 });
15078
15079 let fs = FakeFs::new(cx.executor());
15080 fs.insert_tree(
15081 "/a",
15082 json!({
15083 "main.rs": sample_text_1,
15084 "other.rs": sample_text_2,
15085 "lib.rs": sample_text_3,
15086 }),
15087 )
15088 .await;
15089 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15090 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15091 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15092 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15093 Editor::new(
15094 EditorMode::full(),
15095 multi_buffer,
15096 Some(project.clone()),
15097 window,
15098 cx,
15099 )
15100 });
15101 let multibuffer_item_id = workspace
15102 .update(cx, |workspace, window, cx| {
15103 assert!(
15104 workspace.active_item(cx).is_none(),
15105 "active item should be None before the first item is added"
15106 );
15107 workspace.add_item_to_active_pane(
15108 Box::new(multi_buffer_editor.clone()),
15109 None,
15110 true,
15111 window,
15112 cx,
15113 );
15114 let active_item = workspace
15115 .active_item(cx)
15116 .expect("should have an active item after adding the multi buffer");
15117 assert!(
15118 !active_item.is_singleton(cx),
15119 "A multi buffer was expected to active after adding"
15120 );
15121 active_item.item_id()
15122 })
15123 .unwrap();
15124 cx.executor().run_until_parked();
15125
15126 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15127 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15128 s.select_ranges(Some(1..2))
15129 });
15130 editor.open_excerpts(&OpenExcerpts, window, cx);
15131 });
15132 cx.executor().run_until_parked();
15133 let first_item_id = workspace
15134 .update(cx, |workspace, window, cx| {
15135 let active_item = workspace
15136 .active_item(cx)
15137 .expect("should have an active item after navigating into the 1st buffer");
15138 let first_item_id = active_item.item_id();
15139 assert_ne!(
15140 first_item_id, multibuffer_item_id,
15141 "Should navigate into the 1st buffer and activate it"
15142 );
15143 assert!(
15144 active_item.is_singleton(cx),
15145 "New active item should be a singleton buffer"
15146 );
15147 assert_eq!(
15148 active_item
15149 .act_as::<Editor>(cx)
15150 .expect("should have navigated into an editor for the 1st buffer")
15151 .read(cx)
15152 .text(cx),
15153 sample_text_1
15154 );
15155
15156 workspace
15157 .go_back(workspace.active_pane().downgrade(), window, cx)
15158 .detach_and_log_err(cx);
15159
15160 first_item_id
15161 })
15162 .unwrap();
15163 cx.executor().run_until_parked();
15164 workspace
15165 .update(cx, |workspace, _, cx| {
15166 let active_item = workspace
15167 .active_item(cx)
15168 .expect("should have an active item after navigating back");
15169 assert_eq!(
15170 active_item.item_id(),
15171 multibuffer_item_id,
15172 "Should navigate back to the multi buffer"
15173 );
15174 assert!(!active_item.is_singleton(cx));
15175 })
15176 .unwrap();
15177
15178 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15179 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15180 s.select_ranges(Some(39..40))
15181 });
15182 editor.open_excerpts(&OpenExcerpts, window, cx);
15183 });
15184 cx.executor().run_until_parked();
15185 let second_item_id = workspace
15186 .update(cx, |workspace, window, cx| {
15187 let active_item = workspace
15188 .active_item(cx)
15189 .expect("should have an active item after navigating into the 2nd buffer");
15190 let second_item_id = active_item.item_id();
15191 assert_ne!(
15192 second_item_id, multibuffer_item_id,
15193 "Should navigate away from the multibuffer"
15194 );
15195 assert_ne!(
15196 second_item_id, first_item_id,
15197 "Should navigate into the 2nd buffer and activate it"
15198 );
15199 assert!(
15200 active_item.is_singleton(cx),
15201 "New active item should be a singleton buffer"
15202 );
15203 assert_eq!(
15204 active_item
15205 .act_as::<Editor>(cx)
15206 .expect("should have navigated into an editor")
15207 .read(cx)
15208 .text(cx),
15209 sample_text_2
15210 );
15211
15212 workspace
15213 .go_back(workspace.active_pane().downgrade(), window, cx)
15214 .detach_and_log_err(cx);
15215
15216 second_item_id
15217 })
15218 .unwrap();
15219 cx.executor().run_until_parked();
15220 workspace
15221 .update(cx, |workspace, _, cx| {
15222 let active_item = workspace
15223 .active_item(cx)
15224 .expect("should have an active item after navigating back from the 2nd buffer");
15225 assert_eq!(
15226 active_item.item_id(),
15227 multibuffer_item_id,
15228 "Should navigate back from the 2nd buffer to the multi buffer"
15229 );
15230 assert!(!active_item.is_singleton(cx));
15231 })
15232 .unwrap();
15233
15234 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15235 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15236 s.select_ranges(Some(70..70))
15237 });
15238 editor.open_excerpts(&OpenExcerpts, window, cx);
15239 });
15240 cx.executor().run_until_parked();
15241 workspace
15242 .update(cx, |workspace, window, cx| {
15243 let active_item = workspace
15244 .active_item(cx)
15245 .expect("should have an active item after navigating into the 3rd buffer");
15246 let third_item_id = active_item.item_id();
15247 assert_ne!(
15248 third_item_id, multibuffer_item_id,
15249 "Should navigate into the 3rd buffer and activate it"
15250 );
15251 assert_ne!(third_item_id, first_item_id);
15252 assert_ne!(third_item_id, second_item_id);
15253 assert!(
15254 active_item.is_singleton(cx),
15255 "New active item should be a singleton buffer"
15256 );
15257 assert_eq!(
15258 active_item
15259 .act_as::<Editor>(cx)
15260 .expect("should have navigated into an editor")
15261 .read(cx)
15262 .text(cx),
15263 sample_text_3
15264 );
15265
15266 workspace
15267 .go_back(workspace.active_pane().downgrade(), window, cx)
15268 .detach_and_log_err(cx);
15269 })
15270 .unwrap();
15271 cx.executor().run_until_parked();
15272 workspace
15273 .update(cx, |workspace, _, cx| {
15274 let active_item = workspace
15275 .active_item(cx)
15276 .expect("should have an active item after navigating back from the 3rd buffer");
15277 assert_eq!(
15278 active_item.item_id(),
15279 multibuffer_item_id,
15280 "Should navigate back from the 3rd buffer to the multi buffer"
15281 );
15282 assert!(!active_item.is_singleton(cx));
15283 })
15284 .unwrap();
15285}
15286
15287#[gpui::test]
15288async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15289 init_test(cx, |_| {});
15290
15291 let mut cx = EditorTestContext::new(cx).await;
15292
15293 let diff_base = r#"
15294 use some::mod;
15295
15296 const A: u32 = 42;
15297
15298 fn main() {
15299 println!("hello");
15300
15301 println!("world");
15302 }
15303 "#
15304 .unindent();
15305
15306 cx.set_state(
15307 &r#"
15308 use some::modified;
15309
15310 ˇ
15311 fn main() {
15312 println!("hello there");
15313
15314 println!("around the");
15315 println!("world");
15316 }
15317 "#
15318 .unindent(),
15319 );
15320
15321 cx.set_head_text(&diff_base);
15322 executor.run_until_parked();
15323
15324 cx.update_editor(|editor, window, cx| {
15325 editor.go_to_next_hunk(&GoToHunk, window, cx);
15326 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15327 });
15328 executor.run_until_parked();
15329 cx.assert_state_with_diff(
15330 r#"
15331 use some::modified;
15332
15333
15334 fn main() {
15335 - println!("hello");
15336 + ˇ println!("hello there");
15337
15338 println!("around the");
15339 println!("world");
15340 }
15341 "#
15342 .unindent(),
15343 );
15344
15345 cx.update_editor(|editor, window, cx| {
15346 for _ in 0..2 {
15347 editor.go_to_next_hunk(&GoToHunk, window, cx);
15348 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15349 }
15350 });
15351 executor.run_until_parked();
15352 cx.assert_state_with_diff(
15353 r#"
15354 - use some::mod;
15355 + ˇuse some::modified;
15356
15357
15358 fn main() {
15359 - println!("hello");
15360 + println!("hello there");
15361
15362 + println!("around the");
15363 println!("world");
15364 }
15365 "#
15366 .unindent(),
15367 );
15368
15369 cx.update_editor(|editor, window, cx| {
15370 editor.go_to_next_hunk(&GoToHunk, window, cx);
15371 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15372 });
15373 executor.run_until_parked();
15374 cx.assert_state_with_diff(
15375 r#"
15376 - use some::mod;
15377 + use some::modified;
15378
15379 - const A: u32 = 42;
15380 ˇ
15381 fn main() {
15382 - println!("hello");
15383 + println!("hello there");
15384
15385 + println!("around the");
15386 println!("world");
15387 }
15388 "#
15389 .unindent(),
15390 );
15391
15392 cx.update_editor(|editor, window, cx| {
15393 editor.cancel(&Cancel, window, cx);
15394 });
15395
15396 cx.assert_state_with_diff(
15397 r#"
15398 use some::modified;
15399
15400 ˇ
15401 fn main() {
15402 println!("hello there");
15403
15404 println!("around the");
15405 println!("world");
15406 }
15407 "#
15408 .unindent(),
15409 );
15410}
15411
15412#[gpui::test]
15413async fn test_diff_base_change_with_expanded_diff_hunks(
15414 executor: BackgroundExecutor,
15415 cx: &mut TestAppContext,
15416) {
15417 init_test(cx, |_| {});
15418
15419 let mut cx = EditorTestContext::new(cx).await;
15420
15421 let diff_base = r#"
15422 use some::mod1;
15423 use some::mod2;
15424
15425 const A: u32 = 42;
15426 const B: u32 = 42;
15427 const C: u32 = 42;
15428
15429 fn main() {
15430 println!("hello");
15431
15432 println!("world");
15433 }
15434 "#
15435 .unindent();
15436
15437 cx.set_state(
15438 &r#"
15439 use some::mod2;
15440
15441 const A: u32 = 42;
15442 const C: u32 = 42;
15443
15444 fn main(ˇ) {
15445 //println!("hello");
15446
15447 println!("world");
15448 //
15449 //
15450 }
15451 "#
15452 .unindent(),
15453 );
15454
15455 cx.set_head_text(&diff_base);
15456 executor.run_until_parked();
15457
15458 cx.update_editor(|editor, window, cx| {
15459 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15460 });
15461 executor.run_until_parked();
15462 cx.assert_state_with_diff(
15463 r#"
15464 - use some::mod1;
15465 use some::mod2;
15466
15467 const A: u32 = 42;
15468 - const B: u32 = 42;
15469 const C: u32 = 42;
15470
15471 fn main(ˇ) {
15472 - println!("hello");
15473 + //println!("hello");
15474
15475 println!("world");
15476 + //
15477 + //
15478 }
15479 "#
15480 .unindent(),
15481 );
15482
15483 cx.set_head_text("new diff base!");
15484 executor.run_until_parked();
15485 cx.assert_state_with_diff(
15486 r#"
15487 - new diff base!
15488 + use some::mod2;
15489 +
15490 + const A: u32 = 42;
15491 + const C: u32 = 42;
15492 +
15493 + fn main(ˇ) {
15494 + //println!("hello");
15495 +
15496 + println!("world");
15497 + //
15498 + //
15499 + }
15500 "#
15501 .unindent(),
15502 );
15503}
15504
15505#[gpui::test]
15506async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15507 init_test(cx, |_| {});
15508
15509 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15510 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15511 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15512 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15513 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15514 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15515
15516 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15517 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15518 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15519
15520 let multi_buffer = cx.new(|cx| {
15521 let mut multibuffer = MultiBuffer::new(ReadWrite);
15522 multibuffer.push_excerpts(
15523 buffer_1.clone(),
15524 [
15525 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15526 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15527 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15528 ],
15529 cx,
15530 );
15531 multibuffer.push_excerpts(
15532 buffer_2.clone(),
15533 [
15534 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15535 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15536 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15537 ],
15538 cx,
15539 );
15540 multibuffer.push_excerpts(
15541 buffer_3.clone(),
15542 [
15543 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15544 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15545 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15546 ],
15547 cx,
15548 );
15549 multibuffer
15550 });
15551
15552 let editor =
15553 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15554 editor
15555 .update(cx, |editor, _window, cx| {
15556 for (buffer, diff_base) in [
15557 (buffer_1.clone(), file_1_old),
15558 (buffer_2.clone(), file_2_old),
15559 (buffer_3.clone(), file_3_old),
15560 ] {
15561 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15562 editor
15563 .buffer
15564 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15565 }
15566 })
15567 .unwrap();
15568
15569 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15570 cx.run_until_parked();
15571
15572 cx.assert_editor_state(
15573 &"
15574 ˇaaa
15575 ccc
15576 ddd
15577
15578 ggg
15579 hhh
15580
15581
15582 lll
15583 mmm
15584 NNN
15585
15586 qqq
15587 rrr
15588
15589 uuu
15590 111
15591 222
15592 333
15593
15594 666
15595 777
15596
15597 000
15598 !!!"
15599 .unindent(),
15600 );
15601
15602 cx.update_editor(|editor, window, cx| {
15603 editor.select_all(&SelectAll, window, cx);
15604 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15605 });
15606 cx.executor().run_until_parked();
15607
15608 cx.assert_state_with_diff(
15609 "
15610 «aaa
15611 - bbb
15612 ccc
15613 ddd
15614
15615 ggg
15616 hhh
15617
15618
15619 lll
15620 mmm
15621 - nnn
15622 + NNN
15623
15624 qqq
15625 rrr
15626
15627 uuu
15628 111
15629 222
15630 333
15631
15632 + 666
15633 777
15634
15635 000
15636 !!!ˇ»"
15637 .unindent(),
15638 );
15639}
15640
15641#[gpui::test]
15642async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15643 init_test(cx, |_| {});
15644
15645 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15646 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15647
15648 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15649 let multi_buffer = cx.new(|cx| {
15650 let mut multibuffer = MultiBuffer::new(ReadWrite);
15651 multibuffer.push_excerpts(
15652 buffer.clone(),
15653 [
15654 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15655 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15656 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15657 ],
15658 cx,
15659 );
15660 multibuffer
15661 });
15662
15663 let editor =
15664 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15665 editor
15666 .update(cx, |editor, _window, cx| {
15667 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15668 editor
15669 .buffer
15670 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15671 })
15672 .unwrap();
15673
15674 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15675 cx.run_until_parked();
15676
15677 cx.update_editor(|editor, window, cx| {
15678 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15679 });
15680 cx.executor().run_until_parked();
15681
15682 // When the start of a hunk coincides with the start of its excerpt,
15683 // the hunk is expanded. When the start of a a hunk is earlier than
15684 // the start of its excerpt, the hunk is not expanded.
15685 cx.assert_state_with_diff(
15686 "
15687 ˇaaa
15688 - bbb
15689 + BBB
15690
15691 - ddd
15692 - eee
15693 + DDD
15694 + EEE
15695 fff
15696
15697 iii
15698 "
15699 .unindent(),
15700 );
15701}
15702
15703#[gpui::test]
15704async fn test_edits_around_expanded_insertion_hunks(
15705 executor: BackgroundExecutor,
15706 cx: &mut TestAppContext,
15707) {
15708 init_test(cx, |_| {});
15709
15710 let mut cx = EditorTestContext::new(cx).await;
15711
15712 let diff_base = r#"
15713 use some::mod1;
15714 use some::mod2;
15715
15716 const A: u32 = 42;
15717
15718 fn main() {
15719 println!("hello");
15720
15721 println!("world");
15722 }
15723 "#
15724 .unindent();
15725 executor.run_until_parked();
15726 cx.set_state(
15727 &r#"
15728 use some::mod1;
15729 use some::mod2;
15730
15731 const A: u32 = 42;
15732 const B: u32 = 42;
15733 const C: u32 = 42;
15734 ˇ
15735
15736 fn main() {
15737 println!("hello");
15738
15739 println!("world");
15740 }
15741 "#
15742 .unindent(),
15743 );
15744
15745 cx.set_head_text(&diff_base);
15746 executor.run_until_parked();
15747
15748 cx.update_editor(|editor, window, cx| {
15749 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15750 });
15751 executor.run_until_parked();
15752
15753 cx.assert_state_with_diff(
15754 r#"
15755 use some::mod1;
15756 use some::mod2;
15757
15758 const A: u32 = 42;
15759 + const B: u32 = 42;
15760 + const C: u32 = 42;
15761 + ˇ
15762
15763 fn main() {
15764 println!("hello");
15765
15766 println!("world");
15767 }
15768 "#
15769 .unindent(),
15770 );
15771
15772 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15773 executor.run_until_parked();
15774
15775 cx.assert_state_with_diff(
15776 r#"
15777 use some::mod1;
15778 use some::mod2;
15779
15780 const A: u32 = 42;
15781 + const B: u32 = 42;
15782 + const C: u32 = 42;
15783 + const D: u32 = 42;
15784 + ˇ
15785
15786 fn main() {
15787 println!("hello");
15788
15789 println!("world");
15790 }
15791 "#
15792 .unindent(),
15793 );
15794
15795 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15796 executor.run_until_parked();
15797
15798 cx.assert_state_with_diff(
15799 r#"
15800 use some::mod1;
15801 use some::mod2;
15802
15803 const A: u32 = 42;
15804 + const B: u32 = 42;
15805 + const C: u32 = 42;
15806 + const D: u32 = 42;
15807 + const E: u32 = 42;
15808 + ˇ
15809
15810 fn main() {
15811 println!("hello");
15812
15813 println!("world");
15814 }
15815 "#
15816 .unindent(),
15817 );
15818
15819 cx.update_editor(|editor, window, cx| {
15820 editor.delete_line(&DeleteLine, window, cx);
15821 });
15822 executor.run_until_parked();
15823
15824 cx.assert_state_with_diff(
15825 r#"
15826 use some::mod1;
15827 use some::mod2;
15828
15829 const A: u32 = 42;
15830 + const B: u32 = 42;
15831 + const C: u32 = 42;
15832 + const D: u32 = 42;
15833 + const E: u32 = 42;
15834 ˇ
15835 fn main() {
15836 println!("hello");
15837
15838 println!("world");
15839 }
15840 "#
15841 .unindent(),
15842 );
15843
15844 cx.update_editor(|editor, window, cx| {
15845 editor.move_up(&MoveUp, window, cx);
15846 editor.delete_line(&DeleteLine, window, cx);
15847 editor.move_up(&MoveUp, window, cx);
15848 editor.delete_line(&DeleteLine, window, cx);
15849 editor.move_up(&MoveUp, window, cx);
15850 editor.delete_line(&DeleteLine, window, cx);
15851 });
15852 executor.run_until_parked();
15853 cx.assert_state_with_diff(
15854 r#"
15855 use some::mod1;
15856 use some::mod2;
15857
15858 const A: u32 = 42;
15859 + const B: u32 = 42;
15860 ˇ
15861 fn main() {
15862 println!("hello");
15863
15864 println!("world");
15865 }
15866 "#
15867 .unindent(),
15868 );
15869
15870 cx.update_editor(|editor, window, cx| {
15871 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15872 editor.delete_line(&DeleteLine, window, cx);
15873 });
15874 executor.run_until_parked();
15875 cx.assert_state_with_diff(
15876 r#"
15877 ˇ
15878 fn main() {
15879 println!("hello");
15880
15881 println!("world");
15882 }
15883 "#
15884 .unindent(),
15885 );
15886}
15887
15888#[gpui::test]
15889async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15890 init_test(cx, |_| {});
15891
15892 let mut cx = EditorTestContext::new(cx).await;
15893 cx.set_head_text(indoc! { "
15894 one
15895 two
15896 three
15897 four
15898 five
15899 "
15900 });
15901 cx.set_state(indoc! { "
15902 one
15903 ˇthree
15904 five
15905 "});
15906 cx.run_until_parked();
15907 cx.update_editor(|editor, window, cx| {
15908 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15909 });
15910 cx.assert_state_with_diff(
15911 indoc! { "
15912 one
15913 - two
15914 ˇthree
15915 - four
15916 five
15917 "}
15918 .to_string(),
15919 );
15920 cx.update_editor(|editor, window, cx| {
15921 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15922 });
15923
15924 cx.assert_state_with_diff(
15925 indoc! { "
15926 one
15927 ˇthree
15928 five
15929 "}
15930 .to_string(),
15931 );
15932
15933 cx.set_state(indoc! { "
15934 one
15935 ˇTWO
15936 three
15937 four
15938 five
15939 "});
15940 cx.run_until_parked();
15941 cx.update_editor(|editor, window, cx| {
15942 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15943 });
15944
15945 cx.assert_state_with_diff(
15946 indoc! { "
15947 one
15948 - two
15949 + ˇTWO
15950 three
15951 four
15952 five
15953 "}
15954 .to_string(),
15955 );
15956 cx.update_editor(|editor, window, cx| {
15957 editor.move_up(&Default::default(), window, cx);
15958 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15959 });
15960 cx.assert_state_with_diff(
15961 indoc! { "
15962 one
15963 ˇTWO
15964 three
15965 four
15966 five
15967 "}
15968 .to_string(),
15969 );
15970}
15971
15972#[gpui::test]
15973async fn test_edits_around_expanded_deletion_hunks(
15974 executor: BackgroundExecutor,
15975 cx: &mut TestAppContext,
15976) {
15977 init_test(cx, |_| {});
15978
15979 let mut cx = EditorTestContext::new(cx).await;
15980
15981 let diff_base = r#"
15982 use some::mod1;
15983 use some::mod2;
15984
15985 const A: u32 = 42;
15986 const B: u32 = 42;
15987 const C: u32 = 42;
15988
15989
15990 fn main() {
15991 println!("hello");
15992
15993 println!("world");
15994 }
15995 "#
15996 .unindent();
15997 executor.run_until_parked();
15998 cx.set_state(
15999 &r#"
16000 use some::mod1;
16001 use some::mod2;
16002
16003 ˇconst B: u32 = 42;
16004 const C: u32 = 42;
16005
16006
16007 fn main() {
16008 println!("hello");
16009
16010 println!("world");
16011 }
16012 "#
16013 .unindent(),
16014 );
16015
16016 cx.set_head_text(&diff_base);
16017 executor.run_until_parked();
16018
16019 cx.update_editor(|editor, window, cx| {
16020 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16021 });
16022 executor.run_until_parked();
16023
16024 cx.assert_state_with_diff(
16025 r#"
16026 use some::mod1;
16027 use some::mod2;
16028
16029 - const A: u32 = 42;
16030 ˇconst B: u32 = 42;
16031 const C: u32 = 42;
16032
16033
16034 fn main() {
16035 println!("hello");
16036
16037 println!("world");
16038 }
16039 "#
16040 .unindent(),
16041 );
16042
16043 cx.update_editor(|editor, window, cx| {
16044 editor.delete_line(&DeleteLine, window, cx);
16045 });
16046 executor.run_until_parked();
16047 cx.assert_state_with_diff(
16048 r#"
16049 use some::mod1;
16050 use some::mod2;
16051
16052 - const A: u32 = 42;
16053 - const B: u32 = 42;
16054 ˇconst C: u32 = 42;
16055
16056
16057 fn main() {
16058 println!("hello");
16059
16060 println!("world");
16061 }
16062 "#
16063 .unindent(),
16064 );
16065
16066 cx.update_editor(|editor, window, cx| {
16067 editor.delete_line(&DeleteLine, window, cx);
16068 });
16069 executor.run_until_parked();
16070 cx.assert_state_with_diff(
16071 r#"
16072 use some::mod1;
16073 use some::mod2;
16074
16075 - const A: u32 = 42;
16076 - const B: u32 = 42;
16077 - const C: u32 = 42;
16078 ˇ
16079
16080 fn main() {
16081 println!("hello");
16082
16083 println!("world");
16084 }
16085 "#
16086 .unindent(),
16087 );
16088
16089 cx.update_editor(|editor, window, cx| {
16090 editor.handle_input("replacement", window, cx);
16091 });
16092 executor.run_until_parked();
16093 cx.assert_state_with_diff(
16094 r#"
16095 use some::mod1;
16096 use some::mod2;
16097
16098 - const A: u32 = 42;
16099 - const B: u32 = 42;
16100 - const C: u32 = 42;
16101 -
16102 + replacementˇ
16103
16104 fn main() {
16105 println!("hello");
16106
16107 println!("world");
16108 }
16109 "#
16110 .unindent(),
16111 );
16112}
16113
16114#[gpui::test]
16115async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16116 init_test(cx, |_| {});
16117
16118 let mut cx = EditorTestContext::new(cx).await;
16119
16120 let base_text = r#"
16121 one
16122 two
16123 three
16124 four
16125 five
16126 "#
16127 .unindent();
16128 executor.run_until_parked();
16129 cx.set_state(
16130 &r#"
16131 one
16132 two
16133 fˇour
16134 five
16135 "#
16136 .unindent(),
16137 );
16138
16139 cx.set_head_text(&base_text);
16140 executor.run_until_parked();
16141
16142 cx.update_editor(|editor, window, cx| {
16143 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16144 });
16145 executor.run_until_parked();
16146
16147 cx.assert_state_with_diff(
16148 r#"
16149 one
16150 two
16151 - three
16152 fˇour
16153 five
16154 "#
16155 .unindent(),
16156 );
16157
16158 cx.update_editor(|editor, window, cx| {
16159 editor.backspace(&Backspace, window, cx);
16160 editor.backspace(&Backspace, window, cx);
16161 });
16162 executor.run_until_parked();
16163 cx.assert_state_with_diff(
16164 r#"
16165 one
16166 two
16167 - threeˇ
16168 - four
16169 + our
16170 five
16171 "#
16172 .unindent(),
16173 );
16174}
16175
16176#[gpui::test]
16177async fn test_edit_after_expanded_modification_hunk(
16178 executor: BackgroundExecutor,
16179 cx: &mut TestAppContext,
16180) {
16181 init_test(cx, |_| {});
16182
16183 let mut cx = EditorTestContext::new(cx).await;
16184
16185 let diff_base = r#"
16186 use some::mod1;
16187 use some::mod2;
16188
16189 const A: u32 = 42;
16190 const B: u32 = 42;
16191 const C: u32 = 42;
16192 const D: u32 = 42;
16193
16194
16195 fn main() {
16196 println!("hello");
16197
16198 println!("world");
16199 }"#
16200 .unindent();
16201
16202 cx.set_state(
16203 &r#"
16204 use some::mod1;
16205 use some::mod2;
16206
16207 const A: u32 = 42;
16208 const B: u32 = 42;
16209 const C: u32 = 43ˇ
16210 const D: u32 = 42;
16211
16212
16213 fn main() {
16214 println!("hello");
16215
16216 println!("world");
16217 }"#
16218 .unindent(),
16219 );
16220
16221 cx.set_head_text(&diff_base);
16222 executor.run_until_parked();
16223 cx.update_editor(|editor, window, cx| {
16224 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16225 });
16226 executor.run_until_parked();
16227
16228 cx.assert_state_with_diff(
16229 r#"
16230 use some::mod1;
16231 use some::mod2;
16232
16233 const A: u32 = 42;
16234 const B: u32 = 42;
16235 - const C: u32 = 42;
16236 + const C: u32 = 43ˇ
16237 const D: u32 = 42;
16238
16239
16240 fn main() {
16241 println!("hello");
16242
16243 println!("world");
16244 }"#
16245 .unindent(),
16246 );
16247
16248 cx.update_editor(|editor, window, cx| {
16249 editor.handle_input("\nnew_line\n", window, cx);
16250 });
16251 executor.run_until_parked();
16252
16253 cx.assert_state_with_diff(
16254 r#"
16255 use some::mod1;
16256 use some::mod2;
16257
16258 const A: u32 = 42;
16259 const B: u32 = 42;
16260 - const C: u32 = 42;
16261 + const C: u32 = 43
16262 + new_line
16263 + ˇ
16264 const D: u32 = 42;
16265
16266
16267 fn main() {
16268 println!("hello");
16269
16270 println!("world");
16271 }"#
16272 .unindent(),
16273 );
16274}
16275
16276#[gpui::test]
16277async fn test_stage_and_unstage_added_file_hunk(
16278 executor: BackgroundExecutor,
16279 cx: &mut TestAppContext,
16280) {
16281 init_test(cx, |_| {});
16282
16283 let mut cx = EditorTestContext::new(cx).await;
16284 cx.update_editor(|editor, _, cx| {
16285 editor.set_expand_all_diff_hunks(cx);
16286 });
16287
16288 let working_copy = r#"
16289 ˇfn main() {
16290 println!("hello, world!");
16291 }
16292 "#
16293 .unindent();
16294
16295 cx.set_state(&working_copy);
16296 executor.run_until_parked();
16297
16298 cx.assert_state_with_diff(
16299 r#"
16300 + ˇfn main() {
16301 + println!("hello, world!");
16302 + }
16303 "#
16304 .unindent(),
16305 );
16306 cx.assert_index_text(None);
16307
16308 cx.update_editor(|editor, window, cx| {
16309 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16310 });
16311 executor.run_until_parked();
16312 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16313 cx.assert_state_with_diff(
16314 r#"
16315 + ˇfn main() {
16316 + println!("hello, world!");
16317 + }
16318 "#
16319 .unindent(),
16320 );
16321
16322 cx.update_editor(|editor, window, cx| {
16323 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16324 });
16325 executor.run_until_parked();
16326 cx.assert_index_text(None);
16327}
16328
16329async fn setup_indent_guides_editor(
16330 text: &str,
16331 cx: &mut TestAppContext,
16332) -> (BufferId, EditorTestContext) {
16333 init_test(cx, |_| {});
16334
16335 let mut cx = EditorTestContext::new(cx).await;
16336
16337 let buffer_id = cx.update_editor(|editor, window, cx| {
16338 editor.set_text(text, window, cx);
16339 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16340
16341 buffer_ids[0]
16342 });
16343
16344 (buffer_id, cx)
16345}
16346
16347fn assert_indent_guides(
16348 range: Range<u32>,
16349 expected: Vec<IndentGuide>,
16350 active_indices: Option<Vec<usize>>,
16351 cx: &mut EditorTestContext,
16352) {
16353 let indent_guides = cx.update_editor(|editor, window, cx| {
16354 let snapshot = editor.snapshot(window, cx).display_snapshot;
16355 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16356 editor,
16357 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16358 true,
16359 &snapshot,
16360 cx,
16361 );
16362
16363 indent_guides.sort_by(|a, b| {
16364 a.depth.cmp(&b.depth).then(
16365 a.start_row
16366 .cmp(&b.start_row)
16367 .then(a.end_row.cmp(&b.end_row)),
16368 )
16369 });
16370 indent_guides
16371 });
16372
16373 if let Some(expected) = active_indices {
16374 let active_indices = cx.update_editor(|editor, window, cx| {
16375 let snapshot = editor.snapshot(window, cx).display_snapshot;
16376 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16377 });
16378
16379 assert_eq!(
16380 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16381 expected,
16382 "Active indent guide indices do not match"
16383 );
16384 }
16385
16386 assert_eq!(indent_guides, expected, "Indent guides do not match");
16387}
16388
16389fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16390 IndentGuide {
16391 buffer_id,
16392 start_row: MultiBufferRow(start_row),
16393 end_row: MultiBufferRow(end_row),
16394 depth,
16395 tab_size: 4,
16396 settings: IndentGuideSettings {
16397 enabled: true,
16398 line_width: 1,
16399 active_line_width: 1,
16400 ..Default::default()
16401 },
16402 }
16403}
16404
16405#[gpui::test]
16406async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16407 let (buffer_id, mut cx) = setup_indent_guides_editor(
16408 &"
16409 fn main() {
16410 let a = 1;
16411 }"
16412 .unindent(),
16413 cx,
16414 )
16415 .await;
16416
16417 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16418}
16419
16420#[gpui::test]
16421async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16422 let (buffer_id, mut cx) = setup_indent_guides_editor(
16423 &"
16424 fn main() {
16425 let a = 1;
16426 let b = 2;
16427 }"
16428 .unindent(),
16429 cx,
16430 )
16431 .await;
16432
16433 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16434}
16435
16436#[gpui::test]
16437async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16438 let (buffer_id, mut cx) = setup_indent_guides_editor(
16439 &"
16440 fn main() {
16441 let a = 1;
16442 if a == 3 {
16443 let b = 2;
16444 } else {
16445 let c = 3;
16446 }
16447 }"
16448 .unindent(),
16449 cx,
16450 )
16451 .await;
16452
16453 assert_indent_guides(
16454 0..8,
16455 vec![
16456 indent_guide(buffer_id, 1, 6, 0),
16457 indent_guide(buffer_id, 3, 3, 1),
16458 indent_guide(buffer_id, 5, 5, 1),
16459 ],
16460 None,
16461 &mut cx,
16462 );
16463}
16464
16465#[gpui::test]
16466async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16467 let (buffer_id, mut cx) = setup_indent_guides_editor(
16468 &"
16469 fn main() {
16470 let a = 1;
16471 let b = 2;
16472 let c = 3;
16473 }"
16474 .unindent(),
16475 cx,
16476 )
16477 .await;
16478
16479 assert_indent_guides(
16480 0..5,
16481 vec![
16482 indent_guide(buffer_id, 1, 3, 0),
16483 indent_guide(buffer_id, 2, 2, 1),
16484 ],
16485 None,
16486 &mut cx,
16487 );
16488}
16489
16490#[gpui::test]
16491async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16492 let (buffer_id, mut cx) = setup_indent_guides_editor(
16493 &"
16494 fn main() {
16495 let a = 1;
16496
16497 let c = 3;
16498 }"
16499 .unindent(),
16500 cx,
16501 )
16502 .await;
16503
16504 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16505}
16506
16507#[gpui::test]
16508async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16509 let (buffer_id, mut cx) = setup_indent_guides_editor(
16510 &"
16511 fn main() {
16512 let a = 1;
16513
16514 let c = 3;
16515
16516 if a == 3 {
16517 let b = 2;
16518 } else {
16519 let c = 3;
16520 }
16521 }"
16522 .unindent(),
16523 cx,
16524 )
16525 .await;
16526
16527 assert_indent_guides(
16528 0..11,
16529 vec![
16530 indent_guide(buffer_id, 1, 9, 0),
16531 indent_guide(buffer_id, 6, 6, 1),
16532 indent_guide(buffer_id, 8, 8, 1),
16533 ],
16534 None,
16535 &mut cx,
16536 );
16537}
16538
16539#[gpui::test]
16540async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16541 let (buffer_id, mut cx) = setup_indent_guides_editor(
16542 &"
16543 fn main() {
16544 let a = 1;
16545
16546 let c = 3;
16547
16548 if a == 3 {
16549 let b = 2;
16550 } else {
16551 let c = 3;
16552 }
16553 }"
16554 .unindent(),
16555 cx,
16556 )
16557 .await;
16558
16559 assert_indent_guides(
16560 1..11,
16561 vec![
16562 indent_guide(buffer_id, 1, 9, 0),
16563 indent_guide(buffer_id, 6, 6, 1),
16564 indent_guide(buffer_id, 8, 8, 1),
16565 ],
16566 None,
16567 &mut cx,
16568 );
16569}
16570
16571#[gpui::test]
16572async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16573 let (buffer_id, mut cx) = setup_indent_guides_editor(
16574 &"
16575 fn main() {
16576 let a = 1;
16577
16578 let c = 3;
16579
16580 if a == 3 {
16581 let b = 2;
16582 } else {
16583 let c = 3;
16584 }
16585 }"
16586 .unindent(),
16587 cx,
16588 )
16589 .await;
16590
16591 assert_indent_guides(
16592 1..10,
16593 vec![
16594 indent_guide(buffer_id, 1, 9, 0),
16595 indent_guide(buffer_id, 6, 6, 1),
16596 indent_guide(buffer_id, 8, 8, 1),
16597 ],
16598 None,
16599 &mut cx,
16600 );
16601}
16602
16603#[gpui::test]
16604async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16605 let (buffer_id, mut cx) = setup_indent_guides_editor(
16606 &"
16607 block1
16608 block2
16609 block3
16610 block4
16611 block2
16612 block1
16613 block1"
16614 .unindent(),
16615 cx,
16616 )
16617 .await;
16618
16619 assert_indent_guides(
16620 1..10,
16621 vec![
16622 indent_guide(buffer_id, 1, 4, 0),
16623 indent_guide(buffer_id, 2, 3, 1),
16624 indent_guide(buffer_id, 3, 3, 2),
16625 ],
16626 None,
16627 &mut cx,
16628 );
16629}
16630
16631#[gpui::test]
16632async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16633 let (buffer_id, mut cx) = setup_indent_guides_editor(
16634 &"
16635 block1
16636 block2
16637 block3
16638
16639 block1
16640 block1"
16641 .unindent(),
16642 cx,
16643 )
16644 .await;
16645
16646 assert_indent_guides(
16647 0..6,
16648 vec![
16649 indent_guide(buffer_id, 1, 2, 0),
16650 indent_guide(buffer_id, 2, 2, 1),
16651 ],
16652 None,
16653 &mut cx,
16654 );
16655}
16656
16657#[gpui::test]
16658async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16659 let (buffer_id, mut cx) = setup_indent_guides_editor(
16660 &"
16661 block1
16662
16663
16664
16665 block2
16666 "
16667 .unindent(),
16668 cx,
16669 )
16670 .await;
16671
16672 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16673}
16674
16675#[gpui::test]
16676async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16677 let (buffer_id, mut cx) = setup_indent_guides_editor(
16678 &"
16679 def a:
16680 \tb = 3
16681 \tif True:
16682 \t\tc = 4
16683 \t\td = 5
16684 \tprint(b)
16685 "
16686 .unindent(),
16687 cx,
16688 )
16689 .await;
16690
16691 assert_indent_guides(
16692 0..6,
16693 vec![
16694 indent_guide(buffer_id, 1, 5, 0),
16695 indent_guide(buffer_id, 3, 4, 1),
16696 ],
16697 None,
16698 &mut cx,
16699 );
16700}
16701
16702#[gpui::test]
16703async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16704 let (buffer_id, mut cx) = setup_indent_guides_editor(
16705 &"
16706 fn main() {
16707 let a = 1;
16708 }"
16709 .unindent(),
16710 cx,
16711 )
16712 .await;
16713
16714 cx.update_editor(|editor, window, cx| {
16715 editor.change_selections(None, window, cx, |s| {
16716 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16717 });
16718 });
16719
16720 assert_indent_guides(
16721 0..3,
16722 vec![indent_guide(buffer_id, 1, 1, 0)],
16723 Some(vec![0]),
16724 &mut cx,
16725 );
16726}
16727
16728#[gpui::test]
16729async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16730 let (buffer_id, mut cx) = setup_indent_guides_editor(
16731 &"
16732 fn main() {
16733 if 1 == 2 {
16734 let a = 1;
16735 }
16736 }"
16737 .unindent(),
16738 cx,
16739 )
16740 .await;
16741
16742 cx.update_editor(|editor, window, cx| {
16743 editor.change_selections(None, window, cx, |s| {
16744 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16745 });
16746 });
16747
16748 assert_indent_guides(
16749 0..4,
16750 vec![
16751 indent_guide(buffer_id, 1, 3, 0),
16752 indent_guide(buffer_id, 2, 2, 1),
16753 ],
16754 Some(vec![1]),
16755 &mut cx,
16756 );
16757
16758 cx.update_editor(|editor, window, cx| {
16759 editor.change_selections(None, window, cx, |s| {
16760 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16761 });
16762 });
16763
16764 assert_indent_guides(
16765 0..4,
16766 vec![
16767 indent_guide(buffer_id, 1, 3, 0),
16768 indent_guide(buffer_id, 2, 2, 1),
16769 ],
16770 Some(vec![1]),
16771 &mut cx,
16772 );
16773
16774 cx.update_editor(|editor, window, cx| {
16775 editor.change_selections(None, window, cx, |s| {
16776 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16777 });
16778 });
16779
16780 assert_indent_guides(
16781 0..4,
16782 vec![
16783 indent_guide(buffer_id, 1, 3, 0),
16784 indent_guide(buffer_id, 2, 2, 1),
16785 ],
16786 Some(vec![0]),
16787 &mut cx,
16788 );
16789}
16790
16791#[gpui::test]
16792async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16793 let (buffer_id, mut cx) = setup_indent_guides_editor(
16794 &"
16795 fn main() {
16796 let a = 1;
16797
16798 let b = 2;
16799 }"
16800 .unindent(),
16801 cx,
16802 )
16803 .await;
16804
16805 cx.update_editor(|editor, window, cx| {
16806 editor.change_selections(None, window, cx, |s| {
16807 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16808 });
16809 });
16810
16811 assert_indent_guides(
16812 0..5,
16813 vec![indent_guide(buffer_id, 1, 3, 0)],
16814 Some(vec![0]),
16815 &mut cx,
16816 );
16817}
16818
16819#[gpui::test]
16820async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16821 let (buffer_id, mut cx) = setup_indent_guides_editor(
16822 &"
16823 def m:
16824 a = 1
16825 pass"
16826 .unindent(),
16827 cx,
16828 )
16829 .await;
16830
16831 cx.update_editor(|editor, window, cx| {
16832 editor.change_selections(None, window, cx, |s| {
16833 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16834 });
16835 });
16836
16837 assert_indent_guides(
16838 0..3,
16839 vec![indent_guide(buffer_id, 1, 2, 0)],
16840 Some(vec![0]),
16841 &mut cx,
16842 );
16843}
16844
16845#[gpui::test]
16846async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16847 init_test(cx, |_| {});
16848 let mut cx = EditorTestContext::new(cx).await;
16849 let text = indoc! {
16850 "
16851 impl A {
16852 fn b() {
16853 0;
16854 3;
16855 5;
16856 6;
16857 7;
16858 }
16859 }
16860 "
16861 };
16862 let base_text = indoc! {
16863 "
16864 impl A {
16865 fn b() {
16866 0;
16867 1;
16868 2;
16869 3;
16870 4;
16871 }
16872 fn c() {
16873 5;
16874 6;
16875 7;
16876 }
16877 }
16878 "
16879 };
16880
16881 cx.update_editor(|editor, window, cx| {
16882 editor.set_text(text, window, cx);
16883
16884 editor.buffer().update(cx, |multibuffer, cx| {
16885 let buffer = multibuffer.as_singleton().unwrap();
16886 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16887
16888 multibuffer.set_all_diff_hunks_expanded(cx);
16889 multibuffer.add_diff(diff, cx);
16890
16891 buffer.read(cx).remote_id()
16892 })
16893 });
16894 cx.run_until_parked();
16895
16896 cx.assert_state_with_diff(
16897 indoc! { "
16898 impl A {
16899 fn b() {
16900 0;
16901 - 1;
16902 - 2;
16903 3;
16904 - 4;
16905 - }
16906 - fn c() {
16907 5;
16908 6;
16909 7;
16910 }
16911 }
16912 ˇ"
16913 }
16914 .to_string(),
16915 );
16916
16917 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16918 editor
16919 .snapshot(window, cx)
16920 .buffer_snapshot
16921 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16922 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16923 .collect::<Vec<_>>()
16924 });
16925 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16926 assert_eq!(
16927 actual_guides,
16928 vec![
16929 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16930 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16931 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16932 ]
16933 );
16934}
16935
16936#[gpui::test]
16937async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16938 init_test(cx, |_| {});
16939 let mut cx = EditorTestContext::new(cx).await;
16940
16941 let diff_base = r#"
16942 a
16943 b
16944 c
16945 "#
16946 .unindent();
16947
16948 cx.set_state(
16949 &r#"
16950 ˇA
16951 b
16952 C
16953 "#
16954 .unindent(),
16955 );
16956 cx.set_head_text(&diff_base);
16957 cx.update_editor(|editor, window, cx| {
16958 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16959 });
16960 executor.run_until_parked();
16961
16962 let both_hunks_expanded = r#"
16963 - a
16964 + ˇA
16965 b
16966 - c
16967 + C
16968 "#
16969 .unindent();
16970
16971 cx.assert_state_with_diff(both_hunks_expanded.clone());
16972
16973 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16974 let snapshot = editor.snapshot(window, cx);
16975 let hunks = editor
16976 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16977 .collect::<Vec<_>>();
16978 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16979 let buffer_id = hunks[0].buffer_id;
16980 hunks
16981 .into_iter()
16982 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16983 .collect::<Vec<_>>()
16984 });
16985 assert_eq!(hunk_ranges.len(), 2);
16986
16987 cx.update_editor(|editor, _, cx| {
16988 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16989 });
16990 executor.run_until_parked();
16991
16992 let second_hunk_expanded = r#"
16993 ˇA
16994 b
16995 - c
16996 + C
16997 "#
16998 .unindent();
16999
17000 cx.assert_state_with_diff(second_hunk_expanded);
17001
17002 cx.update_editor(|editor, _, cx| {
17003 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17004 });
17005 executor.run_until_parked();
17006
17007 cx.assert_state_with_diff(both_hunks_expanded.clone());
17008
17009 cx.update_editor(|editor, _, cx| {
17010 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17011 });
17012 executor.run_until_parked();
17013
17014 let first_hunk_expanded = r#"
17015 - a
17016 + ˇA
17017 b
17018 C
17019 "#
17020 .unindent();
17021
17022 cx.assert_state_with_diff(first_hunk_expanded);
17023
17024 cx.update_editor(|editor, _, cx| {
17025 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17026 });
17027 executor.run_until_parked();
17028
17029 cx.assert_state_with_diff(both_hunks_expanded);
17030
17031 cx.set_state(
17032 &r#"
17033 ˇA
17034 b
17035 "#
17036 .unindent(),
17037 );
17038 cx.run_until_parked();
17039
17040 // TODO this cursor position seems bad
17041 cx.assert_state_with_diff(
17042 r#"
17043 - ˇa
17044 + A
17045 b
17046 "#
17047 .unindent(),
17048 );
17049
17050 cx.update_editor(|editor, window, cx| {
17051 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17052 });
17053
17054 cx.assert_state_with_diff(
17055 r#"
17056 - ˇa
17057 + A
17058 b
17059 - c
17060 "#
17061 .unindent(),
17062 );
17063
17064 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17065 let snapshot = editor.snapshot(window, cx);
17066 let hunks = editor
17067 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17068 .collect::<Vec<_>>();
17069 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17070 let buffer_id = hunks[0].buffer_id;
17071 hunks
17072 .into_iter()
17073 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17074 .collect::<Vec<_>>()
17075 });
17076 assert_eq!(hunk_ranges.len(), 2);
17077
17078 cx.update_editor(|editor, _, cx| {
17079 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17080 });
17081 executor.run_until_parked();
17082
17083 cx.assert_state_with_diff(
17084 r#"
17085 - ˇa
17086 + A
17087 b
17088 "#
17089 .unindent(),
17090 );
17091}
17092
17093#[gpui::test]
17094async fn test_toggle_deletion_hunk_at_start_of_file(
17095 executor: BackgroundExecutor,
17096 cx: &mut TestAppContext,
17097) {
17098 init_test(cx, |_| {});
17099 let mut cx = EditorTestContext::new(cx).await;
17100
17101 let diff_base = r#"
17102 a
17103 b
17104 c
17105 "#
17106 .unindent();
17107
17108 cx.set_state(
17109 &r#"
17110 ˇb
17111 c
17112 "#
17113 .unindent(),
17114 );
17115 cx.set_head_text(&diff_base);
17116 cx.update_editor(|editor, window, cx| {
17117 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17118 });
17119 executor.run_until_parked();
17120
17121 let hunk_expanded = r#"
17122 - a
17123 ˇb
17124 c
17125 "#
17126 .unindent();
17127
17128 cx.assert_state_with_diff(hunk_expanded.clone());
17129
17130 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17131 let snapshot = editor.snapshot(window, cx);
17132 let hunks = editor
17133 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17134 .collect::<Vec<_>>();
17135 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17136 let buffer_id = hunks[0].buffer_id;
17137 hunks
17138 .into_iter()
17139 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17140 .collect::<Vec<_>>()
17141 });
17142 assert_eq!(hunk_ranges.len(), 1);
17143
17144 cx.update_editor(|editor, _, cx| {
17145 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17146 });
17147 executor.run_until_parked();
17148
17149 let hunk_collapsed = r#"
17150 ˇb
17151 c
17152 "#
17153 .unindent();
17154
17155 cx.assert_state_with_diff(hunk_collapsed);
17156
17157 cx.update_editor(|editor, _, cx| {
17158 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17159 });
17160 executor.run_until_parked();
17161
17162 cx.assert_state_with_diff(hunk_expanded.clone());
17163}
17164
17165#[gpui::test]
17166async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17167 init_test(cx, |_| {});
17168
17169 let fs = FakeFs::new(cx.executor());
17170 fs.insert_tree(
17171 path!("/test"),
17172 json!({
17173 ".git": {},
17174 "file-1": "ONE\n",
17175 "file-2": "TWO\n",
17176 "file-3": "THREE\n",
17177 }),
17178 )
17179 .await;
17180
17181 fs.set_head_for_repo(
17182 path!("/test/.git").as_ref(),
17183 &[
17184 ("file-1".into(), "one\n".into()),
17185 ("file-2".into(), "two\n".into()),
17186 ("file-3".into(), "three\n".into()),
17187 ],
17188 );
17189
17190 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17191 let mut buffers = vec![];
17192 for i in 1..=3 {
17193 let buffer = project
17194 .update(cx, |project, cx| {
17195 let path = format!(path!("/test/file-{}"), i);
17196 project.open_local_buffer(path, cx)
17197 })
17198 .await
17199 .unwrap();
17200 buffers.push(buffer);
17201 }
17202
17203 let multibuffer = cx.new(|cx| {
17204 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17205 multibuffer.set_all_diff_hunks_expanded(cx);
17206 for buffer in &buffers {
17207 let snapshot = buffer.read(cx).snapshot();
17208 multibuffer.set_excerpts_for_path(
17209 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17210 buffer.clone(),
17211 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17212 DEFAULT_MULTIBUFFER_CONTEXT,
17213 cx,
17214 );
17215 }
17216 multibuffer
17217 });
17218
17219 let editor = cx.add_window(|window, cx| {
17220 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17221 });
17222 cx.run_until_parked();
17223
17224 let snapshot = editor
17225 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17226 .unwrap();
17227 let hunks = snapshot
17228 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17229 .map(|hunk| match hunk {
17230 DisplayDiffHunk::Unfolded {
17231 display_row_range, ..
17232 } => display_row_range,
17233 DisplayDiffHunk::Folded { .. } => unreachable!(),
17234 })
17235 .collect::<Vec<_>>();
17236 assert_eq!(
17237 hunks,
17238 [
17239 DisplayRow(2)..DisplayRow(4),
17240 DisplayRow(7)..DisplayRow(9),
17241 DisplayRow(12)..DisplayRow(14),
17242 ]
17243 );
17244}
17245
17246#[gpui::test]
17247async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17248 init_test(cx, |_| {});
17249
17250 let mut cx = EditorTestContext::new(cx).await;
17251 cx.set_head_text(indoc! { "
17252 one
17253 two
17254 three
17255 four
17256 five
17257 "
17258 });
17259 cx.set_index_text(indoc! { "
17260 one
17261 two
17262 three
17263 four
17264 five
17265 "
17266 });
17267 cx.set_state(indoc! {"
17268 one
17269 TWO
17270 ˇTHREE
17271 FOUR
17272 five
17273 "});
17274 cx.run_until_parked();
17275 cx.update_editor(|editor, window, cx| {
17276 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17277 });
17278 cx.run_until_parked();
17279 cx.assert_index_text(Some(indoc! {"
17280 one
17281 TWO
17282 THREE
17283 FOUR
17284 five
17285 "}));
17286 cx.set_state(indoc! { "
17287 one
17288 TWO
17289 ˇTHREE-HUNDRED
17290 FOUR
17291 five
17292 "});
17293 cx.run_until_parked();
17294 cx.update_editor(|editor, window, cx| {
17295 let snapshot = editor.snapshot(window, cx);
17296 let hunks = editor
17297 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17298 .collect::<Vec<_>>();
17299 assert_eq!(hunks.len(), 1);
17300 assert_eq!(
17301 hunks[0].status(),
17302 DiffHunkStatus {
17303 kind: DiffHunkStatusKind::Modified,
17304 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17305 }
17306 );
17307
17308 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17309 });
17310 cx.run_until_parked();
17311 cx.assert_index_text(Some(indoc! {"
17312 one
17313 TWO
17314 THREE-HUNDRED
17315 FOUR
17316 five
17317 "}));
17318}
17319
17320#[gpui::test]
17321fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17322 init_test(cx, |_| {});
17323
17324 let editor = cx.add_window(|window, cx| {
17325 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17326 build_editor(buffer, window, cx)
17327 });
17328
17329 let render_args = Arc::new(Mutex::new(None));
17330 let snapshot = editor
17331 .update(cx, |editor, window, cx| {
17332 let snapshot = editor.buffer().read(cx).snapshot(cx);
17333 let range =
17334 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17335
17336 struct RenderArgs {
17337 row: MultiBufferRow,
17338 folded: bool,
17339 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17340 }
17341
17342 let crease = Crease::inline(
17343 range,
17344 FoldPlaceholder::test(),
17345 {
17346 let toggle_callback = render_args.clone();
17347 move |row, folded, callback, _window, _cx| {
17348 *toggle_callback.lock() = Some(RenderArgs {
17349 row,
17350 folded,
17351 callback,
17352 });
17353 div()
17354 }
17355 },
17356 |_row, _folded, _window, _cx| div(),
17357 );
17358
17359 editor.insert_creases(Some(crease), cx);
17360 let snapshot = editor.snapshot(window, cx);
17361 let _div = snapshot.render_crease_toggle(
17362 MultiBufferRow(1),
17363 false,
17364 cx.entity().clone(),
17365 window,
17366 cx,
17367 );
17368 snapshot
17369 })
17370 .unwrap();
17371
17372 let render_args = render_args.lock().take().unwrap();
17373 assert_eq!(render_args.row, MultiBufferRow(1));
17374 assert!(!render_args.folded);
17375 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17376
17377 cx.update_window(*editor, |_, window, cx| {
17378 (render_args.callback)(true, window, cx)
17379 })
17380 .unwrap();
17381 let snapshot = editor
17382 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17383 .unwrap();
17384 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17385
17386 cx.update_window(*editor, |_, window, cx| {
17387 (render_args.callback)(false, window, cx)
17388 })
17389 .unwrap();
17390 let snapshot = editor
17391 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17392 .unwrap();
17393 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17394}
17395
17396#[gpui::test]
17397async fn test_input_text(cx: &mut TestAppContext) {
17398 init_test(cx, |_| {});
17399 let mut cx = EditorTestContext::new(cx).await;
17400
17401 cx.set_state(
17402 &r#"ˇone
17403 two
17404
17405 three
17406 fourˇ
17407 five
17408
17409 siˇx"#
17410 .unindent(),
17411 );
17412
17413 cx.dispatch_action(HandleInput(String::new()));
17414 cx.assert_editor_state(
17415 &r#"ˇone
17416 two
17417
17418 three
17419 fourˇ
17420 five
17421
17422 siˇx"#
17423 .unindent(),
17424 );
17425
17426 cx.dispatch_action(HandleInput("AAAA".to_string()));
17427 cx.assert_editor_state(
17428 &r#"AAAAˇone
17429 two
17430
17431 three
17432 fourAAAAˇ
17433 five
17434
17435 siAAAAˇx"#
17436 .unindent(),
17437 );
17438}
17439
17440#[gpui::test]
17441async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17442 init_test(cx, |_| {});
17443
17444 let mut cx = EditorTestContext::new(cx).await;
17445 cx.set_state(
17446 r#"let foo = 1;
17447let foo = 2;
17448let foo = 3;
17449let fooˇ = 4;
17450let foo = 5;
17451let foo = 6;
17452let foo = 7;
17453let foo = 8;
17454let foo = 9;
17455let foo = 10;
17456let foo = 11;
17457let foo = 12;
17458let foo = 13;
17459let foo = 14;
17460let foo = 15;"#,
17461 );
17462
17463 cx.update_editor(|e, window, cx| {
17464 assert_eq!(
17465 e.next_scroll_position,
17466 NextScrollCursorCenterTopBottom::Center,
17467 "Default next scroll direction is center",
17468 );
17469
17470 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17471 assert_eq!(
17472 e.next_scroll_position,
17473 NextScrollCursorCenterTopBottom::Top,
17474 "After center, next scroll direction should be top",
17475 );
17476
17477 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17478 assert_eq!(
17479 e.next_scroll_position,
17480 NextScrollCursorCenterTopBottom::Bottom,
17481 "After top, next scroll direction should be bottom",
17482 );
17483
17484 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17485 assert_eq!(
17486 e.next_scroll_position,
17487 NextScrollCursorCenterTopBottom::Center,
17488 "After bottom, scrolling should start over",
17489 );
17490
17491 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17492 assert_eq!(
17493 e.next_scroll_position,
17494 NextScrollCursorCenterTopBottom::Top,
17495 "Scrolling continues if retriggered fast enough"
17496 );
17497 });
17498
17499 cx.executor()
17500 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17501 cx.executor().run_until_parked();
17502 cx.update_editor(|e, _, _| {
17503 assert_eq!(
17504 e.next_scroll_position,
17505 NextScrollCursorCenterTopBottom::Center,
17506 "If scrolling is not triggered fast enough, it should reset"
17507 );
17508 });
17509}
17510
17511#[gpui::test]
17512async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17513 init_test(cx, |_| {});
17514 let mut cx = EditorLspTestContext::new_rust(
17515 lsp::ServerCapabilities {
17516 definition_provider: Some(lsp::OneOf::Left(true)),
17517 references_provider: Some(lsp::OneOf::Left(true)),
17518 ..lsp::ServerCapabilities::default()
17519 },
17520 cx,
17521 )
17522 .await;
17523
17524 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17525 let go_to_definition = cx
17526 .lsp
17527 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17528 move |params, _| async move {
17529 if empty_go_to_definition {
17530 Ok(None)
17531 } else {
17532 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17533 uri: params.text_document_position_params.text_document.uri,
17534 range: lsp::Range::new(
17535 lsp::Position::new(4, 3),
17536 lsp::Position::new(4, 6),
17537 ),
17538 })))
17539 }
17540 },
17541 );
17542 let references = cx
17543 .lsp
17544 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17545 Ok(Some(vec![lsp::Location {
17546 uri: params.text_document_position.text_document.uri,
17547 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17548 }]))
17549 });
17550 (go_to_definition, references)
17551 };
17552
17553 cx.set_state(
17554 &r#"fn one() {
17555 let mut a = ˇtwo();
17556 }
17557
17558 fn two() {}"#
17559 .unindent(),
17560 );
17561 set_up_lsp_handlers(false, &mut cx);
17562 let navigated = cx
17563 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17564 .await
17565 .expect("Failed to navigate to definition");
17566 assert_eq!(
17567 navigated,
17568 Navigated::Yes,
17569 "Should have navigated to definition from the GetDefinition response"
17570 );
17571 cx.assert_editor_state(
17572 &r#"fn one() {
17573 let mut a = two();
17574 }
17575
17576 fn «twoˇ»() {}"#
17577 .unindent(),
17578 );
17579
17580 let editors = cx.update_workspace(|workspace, _, cx| {
17581 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17582 });
17583 cx.update_editor(|_, _, test_editor_cx| {
17584 assert_eq!(
17585 editors.len(),
17586 1,
17587 "Initially, only one, test, editor should be open in the workspace"
17588 );
17589 assert_eq!(
17590 test_editor_cx.entity(),
17591 editors.last().expect("Asserted len is 1").clone()
17592 );
17593 });
17594
17595 set_up_lsp_handlers(true, &mut cx);
17596 let navigated = cx
17597 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17598 .await
17599 .expect("Failed to navigate to lookup references");
17600 assert_eq!(
17601 navigated,
17602 Navigated::Yes,
17603 "Should have navigated to references as a fallback after empty GoToDefinition response"
17604 );
17605 // We should not change the selections in the existing file,
17606 // if opening another milti buffer with the references
17607 cx.assert_editor_state(
17608 &r#"fn one() {
17609 let mut a = two();
17610 }
17611
17612 fn «twoˇ»() {}"#
17613 .unindent(),
17614 );
17615 let editors = cx.update_workspace(|workspace, _, cx| {
17616 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17617 });
17618 cx.update_editor(|_, _, test_editor_cx| {
17619 assert_eq!(
17620 editors.len(),
17621 2,
17622 "After falling back to references search, we open a new editor with the results"
17623 );
17624 let references_fallback_text = editors
17625 .into_iter()
17626 .find(|new_editor| *new_editor != test_editor_cx.entity())
17627 .expect("Should have one non-test editor now")
17628 .read(test_editor_cx)
17629 .text(test_editor_cx);
17630 assert_eq!(
17631 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17632 "Should use the range from the references response and not the GoToDefinition one"
17633 );
17634 });
17635}
17636
17637#[gpui::test]
17638async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17639 init_test(cx, |_| {});
17640 cx.update(|cx| {
17641 let mut editor_settings = EditorSettings::get_global(cx).clone();
17642 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17643 EditorSettings::override_global(editor_settings, cx);
17644 });
17645 let mut cx = EditorLspTestContext::new_rust(
17646 lsp::ServerCapabilities {
17647 definition_provider: Some(lsp::OneOf::Left(true)),
17648 references_provider: Some(lsp::OneOf::Left(true)),
17649 ..lsp::ServerCapabilities::default()
17650 },
17651 cx,
17652 )
17653 .await;
17654 let original_state = r#"fn one() {
17655 let mut a = ˇtwo();
17656 }
17657
17658 fn two() {}"#
17659 .unindent();
17660 cx.set_state(&original_state);
17661
17662 let mut go_to_definition = cx
17663 .lsp
17664 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17665 move |_, _| async move { Ok(None) },
17666 );
17667 let _references = cx
17668 .lsp
17669 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17670 panic!("Should not call for references with no go to definition fallback")
17671 });
17672
17673 let navigated = cx
17674 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17675 .await
17676 .expect("Failed to navigate to lookup references");
17677 go_to_definition
17678 .next()
17679 .await
17680 .expect("Should have called the go_to_definition handler");
17681
17682 assert_eq!(
17683 navigated,
17684 Navigated::No,
17685 "Should have navigated to references as a fallback after empty GoToDefinition response"
17686 );
17687 cx.assert_editor_state(&original_state);
17688 let editors = cx.update_workspace(|workspace, _, cx| {
17689 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17690 });
17691 cx.update_editor(|_, _, _| {
17692 assert_eq!(
17693 editors.len(),
17694 1,
17695 "After unsuccessful fallback, no other editor should have been opened"
17696 );
17697 });
17698}
17699
17700#[gpui::test]
17701async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17702 init_test(cx, |_| {});
17703
17704 let language = Arc::new(Language::new(
17705 LanguageConfig::default(),
17706 Some(tree_sitter_rust::LANGUAGE.into()),
17707 ));
17708
17709 let text = r#"
17710 #[cfg(test)]
17711 mod tests() {
17712 #[test]
17713 fn runnable_1() {
17714 let a = 1;
17715 }
17716
17717 #[test]
17718 fn runnable_2() {
17719 let a = 1;
17720 let b = 2;
17721 }
17722 }
17723 "#
17724 .unindent();
17725
17726 let fs = FakeFs::new(cx.executor());
17727 fs.insert_file("/file.rs", Default::default()).await;
17728
17729 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17730 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17731 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17732 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17733 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17734
17735 let editor = cx.new_window_entity(|window, cx| {
17736 Editor::new(
17737 EditorMode::full(),
17738 multi_buffer,
17739 Some(project.clone()),
17740 window,
17741 cx,
17742 )
17743 });
17744
17745 editor.update_in(cx, |editor, window, cx| {
17746 let snapshot = editor.buffer().read(cx).snapshot(cx);
17747 editor.tasks.insert(
17748 (buffer.read(cx).remote_id(), 3),
17749 RunnableTasks {
17750 templates: vec![],
17751 offset: snapshot.anchor_before(43),
17752 column: 0,
17753 extra_variables: HashMap::default(),
17754 context_range: BufferOffset(43)..BufferOffset(85),
17755 },
17756 );
17757 editor.tasks.insert(
17758 (buffer.read(cx).remote_id(), 8),
17759 RunnableTasks {
17760 templates: vec![],
17761 offset: snapshot.anchor_before(86),
17762 column: 0,
17763 extra_variables: HashMap::default(),
17764 context_range: BufferOffset(86)..BufferOffset(191),
17765 },
17766 );
17767
17768 // Test finding task when cursor is inside function body
17769 editor.change_selections(None, window, cx, |s| {
17770 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17771 });
17772 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17773 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17774
17775 // Test finding task when cursor is on function name
17776 editor.change_selections(None, window, cx, |s| {
17777 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17778 });
17779 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17780 assert_eq!(row, 8, "Should find task when cursor is on function name");
17781 });
17782}
17783
17784#[gpui::test]
17785async fn test_folding_buffers(cx: &mut TestAppContext) {
17786 init_test(cx, |_| {});
17787
17788 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17789 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17790 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17791
17792 let fs = FakeFs::new(cx.executor());
17793 fs.insert_tree(
17794 path!("/a"),
17795 json!({
17796 "first.rs": sample_text_1,
17797 "second.rs": sample_text_2,
17798 "third.rs": sample_text_3,
17799 }),
17800 )
17801 .await;
17802 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17803 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17804 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17805 let worktree = project.update(cx, |project, cx| {
17806 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17807 assert_eq!(worktrees.len(), 1);
17808 worktrees.pop().unwrap()
17809 });
17810 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17811
17812 let buffer_1 = project
17813 .update(cx, |project, cx| {
17814 project.open_buffer((worktree_id, "first.rs"), cx)
17815 })
17816 .await
17817 .unwrap();
17818 let buffer_2 = project
17819 .update(cx, |project, cx| {
17820 project.open_buffer((worktree_id, "second.rs"), cx)
17821 })
17822 .await
17823 .unwrap();
17824 let buffer_3 = project
17825 .update(cx, |project, cx| {
17826 project.open_buffer((worktree_id, "third.rs"), cx)
17827 })
17828 .await
17829 .unwrap();
17830
17831 let multi_buffer = cx.new(|cx| {
17832 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17833 multi_buffer.push_excerpts(
17834 buffer_1.clone(),
17835 [
17836 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17837 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17838 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17839 ],
17840 cx,
17841 );
17842 multi_buffer.push_excerpts(
17843 buffer_2.clone(),
17844 [
17845 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17846 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17847 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17848 ],
17849 cx,
17850 );
17851 multi_buffer.push_excerpts(
17852 buffer_3.clone(),
17853 [
17854 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17855 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17856 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17857 ],
17858 cx,
17859 );
17860 multi_buffer
17861 });
17862 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17863 Editor::new(
17864 EditorMode::full(),
17865 multi_buffer.clone(),
17866 Some(project.clone()),
17867 window,
17868 cx,
17869 )
17870 });
17871
17872 assert_eq!(
17873 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17874 "\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",
17875 );
17876
17877 multi_buffer_editor.update(cx, |editor, cx| {
17878 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17879 });
17880 assert_eq!(
17881 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17882 "\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",
17883 "After folding the first buffer, its text should not be displayed"
17884 );
17885
17886 multi_buffer_editor.update(cx, |editor, cx| {
17887 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17888 });
17889 assert_eq!(
17890 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17891 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17892 "After folding the second buffer, its text should not be displayed"
17893 );
17894
17895 multi_buffer_editor.update(cx, |editor, cx| {
17896 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17897 });
17898 assert_eq!(
17899 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17900 "\n\n\n\n\n",
17901 "After folding the third buffer, its text should not be displayed"
17902 );
17903
17904 // Emulate selection inside the fold logic, that should work
17905 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17906 editor
17907 .snapshot(window, cx)
17908 .next_line_boundary(Point::new(0, 4));
17909 });
17910
17911 multi_buffer_editor.update(cx, |editor, cx| {
17912 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17913 });
17914 assert_eq!(
17915 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17916 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17917 "After unfolding the second buffer, its text should be displayed"
17918 );
17919
17920 // Typing inside of buffer 1 causes that buffer to be unfolded.
17921 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17922 assert_eq!(
17923 multi_buffer
17924 .read(cx)
17925 .snapshot(cx)
17926 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17927 .collect::<String>(),
17928 "bbbb"
17929 );
17930 editor.change_selections(None, window, cx, |selections| {
17931 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17932 });
17933 editor.handle_input("B", window, cx);
17934 });
17935
17936 assert_eq!(
17937 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17938 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17939 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17940 );
17941
17942 multi_buffer_editor.update(cx, |editor, cx| {
17943 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17944 });
17945 assert_eq!(
17946 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17947 "\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",
17948 "After unfolding the all buffers, all original text should be displayed"
17949 );
17950}
17951
17952#[gpui::test]
17953async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17954 init_test(cx, |_| {});
17955
17956 let sample_text_1 = "1111\n2222\n3333".to_string();
17957 let sample_text_2 = "4444\n5555\n6666".to_string();
17958 let sample_text_3 = "7777\n8888\n9999".to_string();
17959
17960 let fs = FakeFs::new(cx.executor());
17961 fs.insert_tree(
17962 path!("/a"),
17963 json!({
17964 "first.rs": sample_text_1,
17965 "second.rs": sample_text_2,
17966 "third.rs": sample_text_3,
17967 }),
17968 )
17969 .await;
17970 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17971 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17972 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17973 let worktree = project.update(cx, |project, cx| {
17974 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17975 assert_eq!(worktrees.len(), 1);
17976 worktrees.pop().unwrap()
17977 });
17978 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17979
17980 let buffer_1 = project
17981 .update(cx, |project, cx| {
17982 project.open_buffer((worktree_id, "first.rs"), cx)
17983 })
17984 .await
17985 .unwrap();
17986 let buffer_2 = project
17987 .update(cx, |project, cx| {
17988 project.open_buffer((worktree_id, "second.rs"), cx)
17989 })
17990 .await
17991 .unwrap();
17992 let buffer_3 = project
17993 .update(cx, |project, cx| {
17994 project.open_buffer((worktree_id, "third.rs"), cx)
17995 })
17996 .await
17997 .unwrap();
17998
17999 let multi_buffer = cx.new(|cx| {
18000 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18001 multi_buffer.push_excerpts(
18002 buffer_1.clone(),
18003 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18004 cx,
18005 );
18006 multi_buffer.push_excerpts(
18007 buffer_2.clone(),
18008 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18009 cx,
18010 );
18011 multi_buffer.push_excerpts(
18012 buffer_3.clone(),
18013 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18014 cx,
18015 );
18016 multi_buffer
18017 });
18018
18019 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18020 Editor::new(
18021 EditorMode::full(),
18022 multi_buffer,
18023 Some(project.clone()),
18024 window,
18025 cx,
18026 )
18027 });
18028
18029 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18030 assert_eq!(
18031 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18032 full_text,
18033 );
18034
18035 multi_buffer_editor.update(cx, |editor, cx| {
18036 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18037 });
18038 assert_eq!(
18039 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18040 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18041 "After folding the first buffer, its text should not be displayed"
18042 );
18043
18044 multi_buffer_editor.update(cx, |editor, cx| {
18045 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18046 });
18047
18048 assert_eq!(
18049 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18050 "\n\n\n\n\n\n7777\n8888\n9999",
18051 "After folding the second buffer, its text should not be displayed"
18052 );
18053
18054 multi_buffer_editor.update(cx, |editor, cx| {
18055 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18056 });
18057 assert_eq!(
18058 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18059 "\n\n\n\n\n",
18060 "After folding the third buffer, its text should not be displayed"
18061 );
18062
18063 multi_buffer_editor.update(cx, |editor, cx| {
18064 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18065 });
18066 assert_eq!(
18067 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18068 "\n\n\n\n4444\n5555\n6666\n\n",
18069 "After unfolding the second buffer, its text should be displayed"
18070 );
18071
18072 multi_buffer_editor.update(cx, |editor, cx| {
18073 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18074 });
18075 assert_eq!(
18076 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18077 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18078 "After unfolding the first buffer, its text should be displayed"
18079 );
18080
18081 multi_buffer_editor.update(cx, |editor, cx| {
18082 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18083 });
18084 assert_eq!(
18085 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18086 full_text,
18087 "After unfolding all buffers, all original text should be displayed"
18088 );
18089}
18090
18091#[gpui::test]
18092async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18093 init_test(cx, |_| {});
18094
18095 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18096
18097 let fs = FakeFs::new(cx.executor());
18098 fs.insert_tree(
18099 path!("/a"),
18100 json!({
18101 "main.rs": sample_text,
18102 }),
18103 )
18104 .await;
18105 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18106 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18107 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18108 let worktree = project.update(cx, |project, cx| {
18109 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18110 assert_eq!(worktrees.len(), 1);
18111 worktrees.pop().unwrap()
18112 });
18113 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18114
18115 let buffer_1 = project
18116 .update(cx, |project, cx| {
18117 project.open_buffer((worktree_id, "main.rs"), cx)
18118 })
18119 .await
18120 .unwrap();
18121
18122 let multi_buffer = cx.new(|cx| {
18123 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18124 multi_buffer.push_excerpts(
18125 buffer_1.clone(),
18126 [ExcerptRange::new(
18127 Point::new(0, 0)
18128 ..Point::new(
18129 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18130 0,
18131 ),
18132 )],
18133 cx,
18134 );
18135 multi_buffer
18136 });
18137 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18138 Editor::new(
18139 EditorMode::full(),
18140 multi_buffer,
18141 Some(project.clone()),
18142 window,
18143 cx,
18144 )
18145 });
18146
18147 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18148 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18149 enum TestHighlight {}
18150 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18151 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18152 editor.highlight_text::<TestHighlight>(
18153 vec![highlight_range.clone()],
18154 HighlightStyle::color(Hsla::green()),
18155 cx,
18156 );
18157 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18158 });
18159
18160 let full_text = format!("\n\n{sample_text}");
18161 assert_eq!(
18162 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18163 full_text,
18164 );
18165}
18166
18167#[gpui::test]
18168async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18169 init_test(cx, |_| {});
18170 cx.update(|cx| {
18171 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18172 "keymaps/default-linux.json",
18173 cx,
18174 )
18175 .unwrap();
18176 cx.bind_keys(default_key_bindings);
18177 });
18178
18179 let (editor, cx) = cx.add_window_view(|window, cx| {
18180 let multi_buffer = MultiBuffer::build_multi(
18181 [
18182 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18183 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18184 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18185 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18186 ],
18187 cx,
18188 );
18189 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18190
18191 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18192 // fold all but the second buffer, so that we test navigating between two
18193 // adjacent folded buffers, as well as folded buffers at the start and
18194 // end the multibuffer
18195 editor.fold_buffer(buffer_ids[0], cx);
18196 editor.fold_buffer(buffer_ids[2], cx);
18197 editor.fold_buffer(buffer_ids[3], cx);
18198
18199 editor
18200 });
18201 cx.simulate_resize(size(px(1000.), px(1000.)));
18202
18203 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18204 cx.assert_excerpts_with_selections(indoc! {"
18205 [EXCERPT]
18206 ˇ[FOLDED]
18207 [EXCERPT]
18208 a1
18209 b1
18210 [EXCERPT]
18211 [FOLDED]
18212 [EXCERPT]
18213 [FOLDED]
18214 "
18215 });
18216 cx.simulate_keystroke("down");
18217 cx.assert_excerpts_with_selections(indoc! {"
18218 [EXCERPT]
18219 [FOLDED]
18220 [EXCERPT]
18221 ˇa1
18222 b1
18223 [EXCERPT]
18224 [FOLDED]
18225 [EXCERPT]
18226 [FOLDED]
18227 "
18228 });
18229 cx.simulate_keystroke("down");
18230 cx.assert_excerpts_with_selections(indoc! {"
18231 [EXCERPT]
18232 [FOLDED]
18233 [EXCERPT]
18234 a1
18235 ˇb1
18236 [EXCERPT]
18237 [FOLDED]
18238 [EXCERPT]
18239 [FOLDED]
18240 "
18241 });
18242 cx.simulate_keystroke("down");
18243 cx.assert_excerpts_with_selections(indoc! {"
18244 [EXCERPT]
18245 [FOLDED]
18246 [EXCERPT]
18247 a1
18248 b1
18249 ˇ[EXCERPT]
18250 [FOLDED]
18251 [EXCERPT]
18252 [FOLDED]
18253 "
18254 });
18255 cx.simulate_keystroke("down");
18256 cx.assert_excerpts_with_selections(indoc! {"
18257 [EXCERPT]
18258 [FOLDED]
18259 [EXCERPT]
18260 a1
18261 b1
18262 [EXCERPT]
18263 ˇ[FOLDED]
18264 [EXCERPT]
18265 [FOLDED]
18266 "
18267 });
18268 for _ in 0..5 {
18269 cx.simulate_keystroke("down");
18270 cx.assert_excerpts_with_selections(indoc! {"
18271 [EXCERPT]
18272 [FOLDED]
18273 [EXCERPT]
18274 a1
18275 b1
18276 [EXCERPT]
18277 [FOLDED]
18278 [EXCERPT]
18279 ˇ[FOLDED]
18280 "
18281 });
18282 }
18283
18284 cx.simulate_keystroke("up");
18285 cx.assert_excerpts_with_selections(indoc! {"
18286 [EXCERPT]
18287 [FOLDED]
18288 [EXCERPT]
18289 a1
18290 b1
18291 [EXCERPT]
18292 ˇ[FOLDED]
18293 [EXCERPT]
18294 [FOLDED]
18295 "
18296 });
18297 cx.simulate_keystroke("up");
18298 cx.assert_excerpts_with_selections(indoc! {"
18299 [EXCERPT]
18300 [FOLDED]
18301 [EXCERPT]
18302 a1
18303 b1
18304 ˇ[EXCERPT]
18305 [FOLDED]
18306 [EXCERPT]
18307 [FOLDED]
18308 "
18309 });
18310 cx.simulate_keystroke("up");
18311 cx.assert_excerpts_with_selections(indoc! {"
18312 [EXCERPT]
18313 [FOLDED]
18314 [EXCERPT]
18315 a1
18316 ˇb1
18317 [EXCERPT]
18318 [FOLDED]
18319 [EXCERPT]
18320 [FOLDED]
18321 "
18322 });
18323 cx.simulate_keystroke("up");
18324 cx.assert_excerpts_with_selections(indoc! {"
18325 [EXCERPT]
18326 [FOLDED]
18327 [EXCERPT]
18328 ˇa1
18329 b1
18330 [EXCERPT]
18331 [FOLDED]
18332 [EXCERPT]
18333 [FOLDED]
18334 "
18335 });
18336 for _ in 0..5 {
18337 cx.simulate_keystroke("up");
18338 cx.assert_excerpts_with_selections(indoc! {"
18339 [EXCERPT]
18340 ˇ[FOLDED]
18341 [EXCERPT]
18342 a1
18343 b1
18344 [EXCERPT]
18345 [FOLDED]
18346 [EXCERPT]
18347 [FOLDED]
18348 "
18349 });
18350 }
18351}
18352
18353#[gpui::test]
18354async fn test_inline_completion_text(cx: &mut TestAppContext) {
18355 init_test(cx, |_| {});
18356
18357 // Simple insertion
18358 assert_highlighted_edits(
18359 "Hello, world!",
18360 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18361 true,
18362 cx,
18363 |highlighted_edits, cx| {
18364 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18365 assert_eq!(highlighted_edits.highlights.len(), 1);
18366 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18367 assert_eq!(
18368 highlighted_edits.highlights[0].1.background_color,
18369 Some(cx.theme().status().created_background)
18370 );
18371 },
18372 )
18373 .await;
18374
18375 // Replacement
18376 assert_highlighted_edits(
18377 "This is a test.",
18378 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18379 false,
18380 cx,
18381 |highlighted_edits, cx| {
18382 assert_eq!(highlighted_edits.text, "That is a test.");
18383 assert_eq!(highlighted_edits.highlights.len(), 1);
18384 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18385 assert_eq!(
18386 highlighted_edits.highlights[0].1.background_color,
18387 Some(cx.theme().status().created_background)
18388 );
18389 },
18390 )
18391 .await;
18392
18393 // Multiple edits
18394 assert_highlighted_edits(
18395 "Hello, world!",
18396 vec![
18397 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18398 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18399 ],
18400 false,
18401 cx,
18402 |highlighted_edits, cx| {
18403 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18404 assert_eq!(highlighted_edits.highlights.len(), 2);
18405 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18406 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18407 assert_eq!(
18408 highlighted_edits.highlights[0].1.background_color,
18409 Some(cx.theme().status().created_background)
18410 );
18411 assert_eq!(
18412 highlighted_edits.highlights[1].1.background_color,
18413 Some(cx.theme().status().created_background)
18414 );
18415 },
18416 )
18417 .await;
18418
18419 // Multiple lines with edits
18420 assert_highlighted_edits(
18421 "First line\nSecond line\nThird line\nFourth line",
18422 vec![
18423 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18424 (
18425 Point::new(2, 0)..Point::new(2, 10),
18426 "New third line".to_string(),
18427 ),
18428 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18429 ],
18430 false,
18431 cx,
18432 |highlighted_edits, cx| {
18433 assert_eq!(
18434 highlighted_edits.text,
18435 "Second modified\nNew third line\nFourth updated line"
18436 );
18437 assert_eq!(highlighted_edits.highlights.len(), 3);
18438 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18439 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18440 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18441 for highlight in &highlighted_edits.highlights {
18442 assert_eq!(
18443 highlight.1.background_color,
18444 Some(cx.theme().status().created_background)
18445 );
18446 }
18447 },
18448 )
18449 .await;
18450}
18451
18452#[gpui::test]
18453async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18454 init_test(cx, |_| {});
18455
18456 // Deletion
18457 assert_highlighted_edits(
18458 "Hello, world!",
18459 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18460 true,
18461 cx,
18462 |highlighted_edits, cx| {
18463 assert_eq!(highlighted_edits.text, "Hello, world!");
18464 assert_eq!(highlighted_edits.highlights.len(), 1);
18465 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18466 assert_eq!(
18467 highlighted_edits.highlights[0].1.background_color,
18468 Some(cx.theme().status().deleted_background)
18469 );
18470 },
18471 )
18472 .await;
18473
18474 // Insertion
18475 assert_highlighted_edits(
18476 "Hello, world!",
18477 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18478 true,
18479 cx,
18480 |highlighted_edits, cx| {
18481 assert_eq!(highlighted_edits.highlights.len(), 1);
18482 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18483 assert_eq!(
18484 highlighted_edits.highlights[0].1.background_color,
18485 Some(cx.theme().status().created_background)
18486 );
18487 },
18488 )
18489 .await;
18490}
18491
18492async fn assert_highlighted_edits(
18493 text: &str,
18494 edits: Vec<(Range<Point>, String)>,
18495 include_deletions: bool,
18496 cx: &mut TestAppContext,
18497 assertion_fn: impl Fn(HighlightedText, &App),
18498) {
18499 let window = cx.add_window(|window, cx| {
18500 let buffer = MultiBuffer::build_simple(text, cx);
18501 Editor::new(EditorMode::full(), buffer, None, window, cx)
18502 });
18503 let cx = &mut VisualTestContext::from_window(*window, cx);
18504
18505 let (buffer, snapshot) = window
18506 .update(cx, |editor, _window, cx| {
18507 (
18508 editor.buffer().clone(),
18509 editor.buffer().read(cx).snapshot(cx),
18510 )
18511 })
18512 .unwrap();
18513
18514 let edits = edits
18515 .into_iter()
18516 .map(|(range, edit)| {
18517 (
18518 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18519 edit,
18520 )
18521 })
18522 .collect::<Vec<_>>();
18523
18524 let text_anchor_edits = edits
18525 .clone()
18526 .into_iter()
18527 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18528 .collect::<Vec<_>>();
18529
18530 let edit_preview = window
18531 .update(cx, |_, _window, cx| {
18532 buffer
18533 .read(cx)
18534 .as_singleton()
18535 .unwrap()
18536 .read(cx)
18537 .preview_edits(text_anchor_edits.into(), cx)
18538 })
18539 .unwrap()
18540 .await;
18541
18542 cx.update(|_window, cx| {
18543 let highlighted_edits = inline_completion_edit_text(
18544 &snapshot.as_singleton().unwrap().2,
18545 &edits,
18546 &edit_preview,
18547 include_deletions,
18548 cx,
18549 );
18550 assertion_fn(highlighted_edits, cx)
18551 });
18552}
18553
18554#[track_caller]
18555fn assert_breakpoint(
18556 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18557 path: &Arc<Path>,
18558 expected: Vec<(u32, Breakpoint)>,
18559) {
18560 if expected.len() == 0usize {
18561 assert!(!breakpoints.contains_key(path), "{}", path.display());
18562 } else {
18563 let mut breakpoint = breakpoints
18564 .get(path)
18565 .unwrap()
18566 .into_iter()
18567 .map(|breakpoint| {
18568 (
18569 breakpoint.row,
18570 Breakpoint {
18571 message: breakpoint.message.clone(),
18572 state: breakpoint.state,
18573 condition: breakpoint.condition.clone(),
18574 hit_condition: breakpoint.hit_condition.clone(),
18575 },
18576 )
18577 })
18578 .collect::<Vec<_>>();
18579
18580 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18581
18582 assert_eq!(expected, breakpoint);
18583 }
18584}
18585
18586fn add_log_breakpoint_at_cursor(
18587 editor: &mut Editor,
18588 log_message: &str,
18589 window: &mut Window,
18590 cx: &mut Context<Editor>,
18591) {
18592 let (anchor, bp) = editor
18593 .breakpoints_at_cursors(window, cx)
18594 .first()
18595 .and_then(|(anchor, bp)| {
18596 if let Some(bp) = bp {
18597 Some((*anchor, bp.clone()))
18598 } else {
18599 None
18600 }
18601 })
18602 .unwrap_or_else(|| {
18603 let cursor_position: Point = editor.selections.newest(cx).head();
18604
18605 let breakpoint_position = editor
18606 .snapshot(window, cx)
18607 .display_snapshot
18608 .buffer_snapshot
18609 .anchor_before(Point::new(cursor_position.row, 0));
18610
18611 (breakpoint_position, Breakpoint::new_log(&log_message))
18612 });
18613
18614 editor.edit_breakpoint_at_anchor(
18615 anchor,
18616 bp,
18617 BreakpointEditAction::EditLogMessage(log_message.into()),
18618 cx,
18619 );
18620}
18621
18622#[gpui::test]
18623async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18624 init_test(cx, |_| {});
18625
18626 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18627 let fs = FakeFs::new(cx.executor());
18628 fs.insert_tree(
18629 path!("/a"),
18630 json!({
18631 "main.rs": sample_text,
18632 }),
18633 )
18634 .await;
18635 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18636 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18637 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18638
18639 let fs = FakeFs::new(cx.executor());
18640 fs.insert_tree(
18641 path!("/a"),
18642 json!({
18643 "main.rs": sample_text,
18644 }),
18645 )
18646 .await;
18647 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18648 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18649 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18650 let worktree_id = workspace
18651 .update(cx, |workspace, _window, cx| {
18652 workspace.project().update(cx, |project, cx| {
18653 project.worktrees(cx).next().unwrap().read(cx).id()
18654 })
18655 })
18656 .unwrap();
18657
18658 let buffer = project
18659 .update(cx, |project, cx| {
18660 project.open_buffer((worktree_id, "main.rs"), cx)
18661 })
18662 .await
18663 .unwrap();
18664
18665 let (editor, cx) = cx.add_window_view(|window, cx| {
18666 Editor::new(
18667 EditorMode::full(),
18668 MultiBuffer::build_from_buffer(buffer, cx),
18669 Some(project.clone()),
18670 window,
18671 cx,
18672 )
18673 });
18674
18675 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18676 let abs_path = project.read_with(cx, |project, cx| {
18677 project
18678 .absolute_path(&project_path, cx)
18679 .map(|path_buf| Arc::from(path_buf.to_owned()))
18680 .unwrap()
18681 });
18682
18683 // assert we can add breakpoint on the first line
18684 editor.update_in(cx, |editor, window, cx| {
18685 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18686 editor.move_to_end(&MoveToEnd, window, cx);
18687 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18688 });
18689
18690 let breakpoints = editor.update(cx, |editor, cx| {
18691 editor
18692 .breakpoint_store()
18693 .as_ref()
18694 .unwrap()
18695 .read(cx)
18696 .all_breakpoints(cx)
18697 .clone()
18698 });
18699
18700 assert_eq!(1, breakpoints.len());
18701 assert_breakpoint(
18702 &breakpoints,
18703 &abs_path,
18704 vec![
18705 (0, Breakpoint::new_standard()),
18706 (3, Breakpoint::new_standard()),
18707 ],
18708 );
18709
18710 editor.update_in(cx, |editor, window, cx| {
18711 editor.move_to_beginning(&MoveToBeginning, window, cx);
18712 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18713 });
18714
18715 let breakpoints = editor.update(cx, |editor, cx| {
18716 editor
18717 .breakpoint_store()
18718 .as_ref()
18719 .unwrap()
18720 .read(cx)
18721 .all_breakpoints(cx)
18722 .clone()
18723 });
18724
18725 assert_eq!(1, breakpoints.len());
18726 assert_breakpoint(
18727 &breakpoints,
18728 &abs_path,
18729 vec![(3, Breakpoint::new_standard())],
18730 );
18731
18732 editor.update_in(cx, |editor, window, cx| {
18733 editor.move_to_end(&MoveToEnd, window, cx);
18734 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18735 });
18736
18737 let breakpoints = editor.update(cx, |editor, cx| {
18738 editor
18739 .breakpoint_store()
18740 .as_ref()
18741 .unwrap()
18742 .read(cx)
18743 .all_breakpoints(cx)
18744 .clone()
18745 });
18746
18747 assert_eq!(0, breakpoints.len());
18748 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18749}
18750
18751#[gpui::test]
18752async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18753 init_test(cx, |_| {});
18754
18755 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18756
18757 let fs = FakeFs::new(cx.executor());
18758 fs.insert_tree(
18759 path!("/a"),
18760 json!({
18761 "main.rs": sample_text,
18762 }),
18763 )
18764 .await;
18765 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18766 let (workspace, cx) =
18767 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18768
18769 let worktree_id = workspace.update(cx, |workspace, cx| {
18770 workspace.project().update(cx, |project, cx| {
18771 project.worktrees(cx).next().unwrap().read(cx).id()
18772 })
18773 });
18774
18775 let buffer = project
18776 .update(cx, |project, cx| {
18777 project.open_buffer((worktree_id, "main.rs"), cx)
18778 })
18779 .await
18780 .unwrap();
18781
18782 let (editor, cx) = cx.add_window_view(|window, cx| {
18783 Editor::new(
18784 EditorMode::full(),
18785 MultiBuffer::build_from_buffer(buffer, cx),
18786 Some(project.clone()),
18787 window,
18788 cx,
18789 )
18790 });
18791
18792 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18793 let abs_path = project.read_with(cx, |project, cx| {
18794 project
18795 .absolute_path(&project_path, cx)
18796 .map(|path_buf| Arc::from(path_buf.to_owned()))
18797 .unwrap()
18798 });
18799
18800 editor.update_in(cx, |editor, window, cx| {
18801 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18802 });
18803
18804 let breakpoints = editor.update(cx, |editor, cx| {
18805 editor
18806 .breakpoint_store()
18807 .as_ref()
18808 .unwrap()
18809 .read(cx)
18810 .all_breakpoints(cx)
18811 .clone()
18812 });
18813
18814 assert_breakpoint(
18815 &breakpoints,
18816 &abs_path,
18817 vec![(0, Breakpoint::new_log("hello world"))],
18818 );
18819
18820 // Removing a log message from a log breakpoint should remove it
18821 editor.update_in(cx, |editor, window, cx| {
18822 add_log_breakpoint_at_cursor(editor, "", window, cx);
18823 });
18824
18825 let breakpoints = editor.update(cx, |editor, cx| {
18826 editor
18827 .breakpoint_store()
18828 .as_ref()
18829 .unwrap()
18830 .read(cx)
18831 .all_breakpoints(cx)
18832 .clone()
18833 });
18834
18835 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18836
18837 editor.update_in(cx, |editor, window, cx| {
18838 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18839 editor.move_to_end(&MoveToEnd, window, cx);
18840 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18841 // Not adding a log message to a standard breakpoint shouldn't remove it
18842 add_log_breakpoint_at_cursor(editor, "", window, cx);
18843 });
18844
18845 let breakpoints = editor.update(cx, |editor, cx| {
18846 editor
18847 .breakpoint_store()
18848 .as_ref()
18849 .unwrap()
18850 .read(cx)
18851 .all_breakpoints(cx)
18852 .clone()
18853 });
18854
18855 assert_breakpoint(
18856 &breakpoints,
18857 &abs_path,
18858 vec![
18859 (0, Breakpoint::new_standard()),
18860 (3, Breakpoint::new_standard()),
18861 ],
18862 );
18863
18864 editor.update_in(cx, |editor, window, cx| {
18865 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18866 });
18867
18868 let breakpoints = editor.update(cx, |editor, cx| {
18869 editor
18870 .breakpoint_store()
18871 .as_ref()
18872 .unwrap()
18873 .read(cx)
18874 .all_breakpoints(cx)
18875 .clone()
18876 });
18877
18878 assert_breakpoint(
18879 &breakpoints,
18880 &abs_path,
18881 vec![
18882 (0, Breakpoint::new_standard()),
18883 (3, Breakpoint::new_log("hello world")),
18884 ],
18885 );
18886
18887 editor.update_in(cx, |editor, window, cx| {
18888 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18889 });
18890
18891 let breakpoints = editor.update(cx, |editor, cx| {
18892 editor
18893 .breakpoint_store()
18894 .as_ref()
18895 .unwrap()
18896 .read(cx)
18897 .all_breakpoints(cx)
18898 .clone()
18899 });
18900
18901 assert_breakpoint(
18902 &breakpoints,
18903 &abs_path,
18904 vec![
18905 (0, Breakpoint::new_standard()),
18906 (3, Breakpoint::new_log("hello Earth!!")),
18907 ],
18908 );
18909}
18910
18911/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18912/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18913/// or when breakpoints were placed out of order. This tests for a regression too
18914#[gpui::test]
18915async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18916 init_test(cx, |_| {});
18917
18918 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18919 let fs = FakeFs::new(cx.executor());
18920 fs.insert_tree(
18921 path!("/a"),
18922 json!({
18923 "main.rs": sample_text,
18924 }),
18925 )
18926 .await;
18927 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18928 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18929 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18930
18931 let fs = FakeFs::new(cx.executor());
18932 fs.insert_tree(
18933 path!("/a"),
18934 json!({
18935 "main.rs": sample_text,
18936 }),
18937 )
18938 .await;
18939 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18940 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18941 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18942 let worktree_id = workspace
18943 .update(cx, |workspace, _window, cx| {
18944 workspace.project().update(cx, |project, cx| {
18945 project.worktrees(cx).next().unwrap().read(cx).id()
18946 })
18947 })
18948 .unwrap();
18949
18950 let buffer = project
18951 .update(cx, |project, cx| {
18952 project.open_buffer((worktree_id, "main.rs"), cx)
18953 })
18954 .await
18955 .unwrap();
18956
18957 let (editor, cx) = cx.add_window_view(|window, cx| {
18958 Editor::new(
18959 EditorMode::full(),
18960 MultiBuffer::build_from_buffer(buffer, cx),
18961 Some(project.clone()),
18962 window,
18963 cx,
18964 )
18965 });
18966
18967 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18968 let abs_path = project.read_with(cx, |project, cx| {
18969 project
18970 .absolute_path(&project_path, cx)
18971 .map(|path_buf| Arc::from(path_buf.to_owned()))
18972 .unwrap()
18973 });
18974
18975 // assert we can add breakpoint on the first line
18976 editor.update_in(cx, |editor, window, cx| {
18977 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18978 editor.move_to_end(&MoveToEnd, window, cx);
18979 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18980 editor.move_up(&MoveUp, window, cx);
18981 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18982 });
18983
18984 let breakpoints = editor.update(cx, |editor, cx| {
18985 editor
18986 .breakpoint_store()
18987 .as_ref()
18988 .unwrap()
18989 .read(cx)
18990 .all_breakpoints(cx)
18991 .clone()
18992 });
18993
18994 assert_eq!(1, breakpoints.len());
18995 assert_breakpoint(
18996 &breakpoints,
18997 &abs_path,
18998 vec![
18999 (0, Breakpoint::new_standard()),
19000 (2, Breakpoint::new_standard()),
19001 (3, Breakpoint::new_standard()),
19002 ],
19003 );
19004
19005 editor.update_in(cx, |editor, window, cx| {
19006 editor.move_to_beginning(&MoveToBeginning, window, cx);
19007 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19008 editor.move_to_end(&MoveToEnd, window, cx);
19009 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19010 // Disabling a breakpoint that doesn't exist should do nothing
19011 editor.move_up(&MoveUp, window, cx);
19012 editor.move_up(&MoveUp, window, cx);
19013 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19014 });
19015
19016 let breakpoints = editor.update(cx, |editor, cx| {
19017 editor
19018 .breakpoint_store()
19019 .as_ref()
19020 .unwrap()
19021 .read(cx)
19022 .all_breakpoints(cx)
19023 .clone()
19024 });
19025
19026 let disable_breakpoint = {
19027 let mut bp = Breakpoint::new_standard();
19028 bp.state = BreakpointState::Disabled;
19029 bp
19030 };
19031
19032 assert_eq!(1, breakpoints.len());
19033 assert_breakpoint(
19034 &breakpoints,
19035 &abs_path,
19036 vec![
19037 (0, disable_breakpoint.clone()),
19038 (2, Breakpoint::new_standard()),
19039 (3, disable_breakpoint.clone()),
19040 ],
19041 );
19042
19043 editor.update_in(cx, |editor, window, cx| {
19044 editor.move_to_beginning(&MoveToBeginning, window, cx);
19045 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19046 editor.move_to_end(&MoveToEnd, window, cx);
19047 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19048 editor.move_up(&MoveUp, window, cx);
19049 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19050 });
19051
19052 let breakpoints = editor.update(cx, |editor, cx| {
19053 editor
19054 .breakpoint_store()
19055 .as_ref()
19056 .unwrap()
19057 .read(cx)
19058 .all_breakpoints(cx)
19059 .clone()
19060 });
19061
19062 assert_eq!(1, breakpoints.len());
19063 assert_breakpoint(
19064 &breakpoints,
19065 &abs_path,
19066 vec![
19067 (0, Breakpoint::new_standard()),
19068 (2, disable_breakpoint),
19069 (3, Breakpoint::new_standard()),
19070 ],
19071 );
19072}
19073
19074#[gpui::test]
19075async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19076 init_test(cx, |_| {});
19077 let capabilities = lsp::ServerCapabilities {
19078 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19079 prepare_provider: Some(true),
19080 work_done_progress_options: Default::default(),
19081 })),
19082 ..Default::default()
19083 };
19084 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19085
19086 cx.set_state(indoc! {"
19087 struct Fˇoo {}
19088 "});
19089
19090 cx.update_editor(|editor, _, cx| {
19091 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19092 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19093 editor.highlight_background::<DocumentHighlightRead>(
19094 &[highlight_range],
19095 |c| c.editor_document_highlight_read_background,
19096 cx,
19097 );
19098 });
19099
19100 let mut prepare_rename_handler = cx
19101 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19102 move |_, _, _| async move {
19103 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19104 start: lsp::Position {
19105 line: 0,
19106 character: 7,
19107 },
19108 end: lsp::Position {
19109 line: 0,
19110 character: 10,
19111 },
19112 })))
19113 },
19114 );
19115 let prepare_rename_task = cx
19116 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19117 .expect("Prepare rename was not started");
19118 prepare_rename_handler.next().await.unwrap();
19119 prepare_rename_task.await.expect("Prepare rename failed");
19120
19121 let mut rename_handler =
19122 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19123 let edit = lsp::TextEdit {
19124 range: lsp::Range {
19125 start: lsp::Position {
19126 line: 0,
19127 character: 7,
19128 },
19129 end: lsp::Position {
19130 line: 0,
19131 character: 10,
19132 },
19133 },
19134 new_text: "FooRenamed".to_string(),
19135 };
19136 Ok(Some(lsp::WorkspaceEdit::new(
19137 // Specify the same edit twice
19138 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19139 )))
19140 });
19141 let rename_task = cx
19142 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19143 .expect("Confirm rename was not started");
19144 rename_handler.next().await.unwrap();
19145 rename_task.await.expect("Confirm rename failed");
19146 cx.run_until_parked();
19147
19148 // Despite two edits, only one is actually applied as those are identical
19149 cx.assert_editor_state(indoc! {"
19150 struct FooRenamedˇ {}
19151 "});
19152}
19153
19154#[gpui::test]
19155async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19156 init_test(cx, |_| {});
19157 // These capabilities indicate that the server does not support prepare rename.
19158 let capabilities = lsp::ServerCapabilities {
19159 rename_provider: Some(lsp::OneOf::Left(true)),
19160 ..Default::default()
19161 };
19162 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19163
19164 cx.set_state(indoc! {"
19165 struct Fˇoo {}
19166 "});
19167
19168 cx.update_editor(|editor, _window, cx| {
19169 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19170 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19171 editor.highlight_background::<DocumentHighlightRead>(
19172 &[highlight_range],
19173 |c| c.editor_document_highlight_read_background,
19174 cx,
19175 );
19176 });
19177
19178 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19179 .expect("Prepare rename was not started")
19180 .await
19181 .expect("Prepare rename failed");
19182
19183 let mut rename_handler =
19184 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19185 let edit = lsp::TextEdit {
19186 range: lsp::Range {
19187 start: lsp::Position {
19188 line: 0,
19189 character: 7,
19190 },
19191 end: lsp::Position {
19192 line: 0,
19193 character: 10,
19194 },
19195 },
19196 new_text: "FooRenamed".to_string(),
19197 };
19198 Ok(Some(lsp::WorkspaceEdit::new(
19199 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19200 )))
19201 });
19202 let rename_task = cx
19203 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19204 .expect("Confirm rename was not started");
19205 rename_handler.next().await.unwrap();
19206 rename_task.await.expect("Confirm rename failed");
19207 cx.run_until_parked();
19208
19209 // Correct range is renamed, as `surrounding_word` is used to find it.
19210 cx.assert_editor_state(indoc! {"
19211 struct FooRenamedˇ {}
19212 "});
19213}
19214
19215#[gpui::test]
19216async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19217 init_test(cx, |_| {});
19218 let mut cx = EditorTestContext::new(cx).await;
19219
19220 let language = Arc::new(
19221 Language::new(
19222 LanguageConfig::default(),
19223 Some(tree_sitter_html::LANGUAGE.into()),
19224 )
19225 .with_brackets_query(
19226 r#"
19227 ("<" @open "/>" @close)
19228 ("</" @open ">" @close)
19229 ("<" @open ">" @close)
19230 ("\"" @open "\"" @close)
19231 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19232 "#,
19233 )
19234 .unwrap(),
19235 );
19236 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19237
19238 cx.set_state(indoc! {"
19239 <span>ˇ</span>
19240 "});
19241 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19242 cx.assert_editor_state(indoc! {"
19243 <span>
19244 ˇ
19245 </span>
19246 "});
19247
19248 cx.set_state(indoc! {"
19249 <span><span></span>ˇ</span>
19250 "});
19251 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19252 cx.assert_editor_state(indoc! {"
19253 <span><span></span>
19254 ˇ</span>
19255 "});
19256
19257 cx.set_state(indoc! {"
19258 <span>ˇ
19259 </span>
19260 "});
19261 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19262 cx.assert_editor_state(indoc! {"
19263 <span>
19264 ˇ
19265 </span>
19266 "});
19267}
19268
19269#[gpui::test(iterations = 10)]
19270async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19271 init_test(cx, |_| {});
19272
19273 let fs = FakeFs::new(cx.executor());
19274 fs.insert_tree(
19275 path!("/dir"),
19276 json!({
19277 "a.ts": "a",
19278 }),
19279 )
19280 .await;
19281
19282 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19283 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19284 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19285
19286 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19287 language_registry.add(Arc::new(Language::new(
19288 LanguageConfig {
19289 name: "TypeScript".into(),
19290 matcher: LanguageMatcher {
19291 path_suffixes: vec!["ts".to_string()],
19292 ..Default::default()
19293 },
19294 ..Default::default()
19295 },
19296 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19297 )));
19298 let mut fake_language_servers = language_registry.register_fake_lsp(
19299 "TypeScript",
19300 FakeLspAdapter {
19301 capabilities: lsp::ServerCapabilities {
19302 code_lens_provider: Some(lsp::CodeLensOptions {
19303 resolve_provider: Some(true),
19304 }),
19305 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19306 commands: vec!["_the/command".to_string()],
19307 ..lsp::ExecuteCommandOptions::default()
19308 }),
19309 ..lsp::ServerCapabilities::default()
19310 },
19311 ..FakeLspAdapter::default()
19312 },
19313 );
19314
19315 let (buffer, _handle) = project
19316 .update(cx, |p, cx| {
19317 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19318 })
19319 .await
19320 .unwrap();
19321 cx.executor().run_until_parked();
19322
19323 let fake_server = fake_language_servers.next().await.unwrap();
19324
19325 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19326 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19327 drop(buffer_snapshot);
19328 let actions = cx
19329 .update_window(*workspace, |_, window, cx| {
19330 project.code_actions(&buffer, anchor..anchor, window, cx)
19331 })
19332 .unwrap();
19333
19334 fake_server
19335 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19336 Ok(Some(vec![
19337 lsp::CodeLens {
19338 range: lsp::Range::default(),
19339 command: Some(lsp::Command {
19340 title: "Code lens command".to_owned(),
19341 command: "_the/command".to_owned(),
19342 arguments: None,
19343 }),
19344 data: None,
19345 },
19346 lsp::CodeLens {
19347 range: lsp::Range::default(),
19348 command: Some(lsp::Command {
19349 title: "Command not in capabilities".to_owned(),
19350 command: "not in capabilities".to_owned(),
19351 arguments: None,
19352 }),
19353 data: None,
19354 },
19355 lsp::CodeLens {
19356 range: lsp::Range {
19357 start: lsp::Position {
19358 line: 1,
19359 character: 1,
19360 },
19361 end: lsp::Position {
19362 line: 1,
19363 character: 1,
19364 },
19365 },
19366 command: Some(lsp::Command {
19367 title: "Command not in range".to_owned(),
19368 command: "_the/command".to_owned(),
19369 arguments: None,
19370 }),
19371 data: None,
19372 },
19373 ]))
19374 })
19375 .next()
19376 .await;
19377
19378 let actions = actions.await.unwrap();
19379 assert_eq!(
19380 actions.len(),
19381 1,
19382 "Should have only one valid action for the 0..0 range"
19383 );
19384 let action = actions[0].clone();
19385 let apply = project.update(cx, |project, cx| {
19386 project.apply_code_action(buffer.clone(), action, true, cx)
19387 });
19388
19389 // Resolving the code action does not populate its edits. In absence of
19390 // edits, we must execute the given command.
19391 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19392 |mut lens, _| async move {
19393 let lens_command = lens.command.as_mut().expect("should have a command");
19394 assert_eq!(lens_command.title, "Code lens command");
19395 lens_command.arguments = Some(vec![json!("the-argument")]);
19396 Ok(lens)
19397 },
19398 );
19399
19400 // While executing the command, the language server sends the editor
19401 // a `workspaceEdit` request.
19402 fake_server
19403 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19404 let fake = fake_server.clone();
19405 move |params, _| {
19406 assert_eq!(params.command, "_the/command");
19407 let fake = fake.clone();
19408 async move {
19409 fake.server
19410 .request::<lsp::request::ApplyWorkspaceEdit>(
19411 lsp::ApplyWorkspaceEditParams {
19412 label: None,
19413 edit: lsp::WorkspaceEdit {
19414 changes: Some(
19415 [(
19416 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19417 vec![lsp::TextEdit {
19418 range: lsp::Range::new(
19419 lsp::Position::new(0, 0),
19420 lsp::Position::new(0, 0),
19421 ),
19422 new_text: "X".into(),
19423 }],
19424 )]
19425 .into_iter()
19426 .collect(),
19427 ),
19428 ..Default::default()
19429 },
19430 },
19431 )
19432 .await
19433 .into_response()
19434 .unwrap();
19435 Ok(Some(json!(null)))
19436 }
19437 }
19438 })
19439 .next()
19440 .await;
19441
19442 // Applying the code lens command returns a project transaction containing the edits
19443 // sent by the language server in its `workspaceEdit` request.
19444 let transaction = apply.await.unwrap();
19445 assert!(transaction.0.contains_key(&buffer));
19446 buffer.update(cx, |buffer, cx| {
19447 assert_eq!(buffer.text(), "Xa");
19448 buffer.undo(cx);
19449 assert_eq!(buffer.text(), "a");
19450 });
19451}
19452
19453#[gpui::test]
19454async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19455 init_test(cx, |_| {});
19456
19457 let fs = FakeFs::new(cx.executor());
19458 let main_text = r#"fn main() {
19459println!("1");
19460println!("2");
19461println!("3");
19462println!("4");
19463println!("5");
19464}"#;
19465 let lib_text = "mod foo {}";
19466 fs.insert_tree(
19467 path!("/a"),
19468 json!({
19469 "lib.rs": lib_text,
19470 "main.rs": main_text,
19471 }),
19472 )
19473 .await;
19474
19475 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19476 let (workspace, cx) =
19477 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19478 let worktree_id = workspace.update(cx, |workspace, cx| {
19479 workspace.project().update(cx, |project, cx| {
19480 project.worktrees(cx).next().unwrap().read(cx).id()
19481 })
19482 });
19483
19484 let expected_ranges = vec![
19485 Point::new(0, 0)..Point::new(0, 0),
19486 Point::new(1, 0)..Point::new(1, 1),
19487 Point::new(2, 0)..Point::new(2, 2),
19488 Point::new(3, 0)..Point::new(3, 3),
19489 ];
19490
19491 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19492 let editor_1 = workspace
19493 .update_in(cx, |workspace, window, cx| {
19494 workspace.open_path(
19495 (worktree_id, "main.rs"),
19496 Some(pane_1.downgrade()),
19497 true,
19498 window,
19499 cx,
19500 )
19501 })
19502 .unwrap()
19503 .await
19504 .downcast::<Editor>()
19505 .unwrap();
19506 pane_1.update(cx, |pane, cx| {
19507 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19508 open_editor.update(cx, |editor, cx| {
19509 assert_eq!(
19510 editor.display_text(cx),
19511 main_text,
19512 "Original main.rs text on initial open",
19513 );
19514 assert_eq!(
19515 editor
19516 .selections
19517 .all::<Point>(cx)
19518 .into_iter()
19519 .map(|s| s.range())
19520 .collect::<Vec<_>>(),
19521 vec![Point::zero()..Point::zero()],
19522 "Default selections on initial open",
19523 );
19524 })
19525 });
19526 editor_1.update_in(cx, |editor, window, cx| {
19527 editor.change_selections(None, window, cx, |s| {
19528 s.select_ranges(expected_ranges.clone());
19529 });
19530 });
19531
19532 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19533 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19534 });
19535 let editor_2 = workspace
19536 .update_in(cx, |workspace, window, cx| {
19537 workspace.open_path(
19538 (worktree_id, "main.rs"),
19539 Some(pane_2.downgrade()),
19540 true,
19541 window,
19542 cx,
19543 )
19544 })
19545 .unwrap()
19546 .await
19547 .downcast::<Editor>()
19548 .unwrap();
19549 pane_2.update(cx, |pane, cx| {
19550 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19551 open_editor.update(cx, |editor, cx| {
19552 assert_eq!(
19553 editor.display_text(cx),
19554 main_text,
19555 "Original main.rs text on initial open in another panel",
19556 );
19557 assert_eq!(
19558 editor
19559 .selections
19560 .all::<Point>(cx)
19561 .into_iter()
19562 .map(|s| s.range())
19563 .collect::<Vec<_>>(),
19564 vec![Point::zero()..Point::zero()],
19565 "Default selections on initial open in another panel",
19566 );
19567 })
19568 });
19569
19570 editor_2.update_in(cx, |editor, window, cx| {
19571 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19572 });
19573
19574 let _other_editor_1 = workspace
19575 .update_in(cx, |workspace, window, cx| {
19576 workspace.open_path(
19577 (worktree_id, "lib.rs"),
19578 Some(pane_1.downgrade()),
19579 true,
19580 window,
19581 cx,
19582 )
19583 })
19584 .unwrap()
19585 .await
19586 .downcast::<Editor>()
19587 .unwrap();
19588 pane_1
19589 .update_in(cx, |pane, window, cx| {
19590 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19591 .unwrap()
19592 })
19593 .await
19594 .unwrap();
19595 drop(editor_1);
19596 pane_1.update(cx, |pane, cx| {
19597 pane.active_item()
19598 .unwrap()
19599 .downcast::<Editor>()
19600 .unwrap()
19601 .update(cx, |editor, cx| {
19602 assert_eq!(
19603 editor.display_text(cx),
19604 lib_text,
19605 "Other file should be open and active",
19606 );
19607 });
19608 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19609 });
19610
19611 let _other_editor_2 = workspace
19612 .update_in(cx, |workspace, window, cx| {
19613 workspace.open_path(
19614 (worktree_id, "lib.rs"),
19615 Some(pane_2.downgrade()),
19616 true,
19617 window,
19618 cx,
19619 )
19620 })
19621 .unwrap()
19622 .await
19623 .downcast::<Editor>()
19624 .unwrap();
19625 pane_2
19626 .update_in(cx, |pane, window, cx| {
19627 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19628 .unwrap()
19629 })
19630 .await
19631 .unwrap();
19632 drop(editor_2);
19633 pane_2.update(cx, |pane, cx| {
19634 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19635 open_editor.update(cx, |editor, cx| {
19636 assert_eq!(
19637 editor.display_text(cx),
19638 lib_text,
19639 "Other file should be open and active in another panel too",
19640 );
19641 });
19642 assert_eq!(
19643 pane.items().count(),
19644 1,
19645 "No other editors should be open in another pane",
19646 );
19647 });
19648
19649 let _editor_1_reopened = workspace
19650 .update_in(cx, |workspace, window, cx| {
19651 workspace.open_path(
19652 (worktree_id, "main.rs"),
19653 Some(pane_1.downgrade()),
19654 true,
19655 window,
19656 cx,
19657 )
19658 })
19659 .unwrap()
19660 .await
19661 .downcast::<Editor>()
19662 .unwrap();
19663 let _editor_2_reopened = workspace
19664 .update_in(cx, |workspace, window, cx| {
19665 workspace.open_path(
19666 (worktree_id, "main.rs"),
19667 Some(pane_2.downgrade()),
19668 true,
19669 window,
19670 cx,
19671 )
19672 })
19673 .unwrap()
19674 .await
19675 .downcast::<Editor>()
19676 .unwrap();
19677 pane_1.update(cx, |pane, cx| {
19678 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19679 open_editor.update(cx, |editor, cx| {
19680 assert_eq!(
19681 editor.display_text(cx),
19682 main_text,
19683 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19684 );
19685 assert_eq!(
19686 editor
19687 .selections
19688 .all::<Point>(cx)
19689 .into_iter()
19690 .map(|s| s.range())
19691 .collect::<Vec<_>>(),
19692 expected_ranges,
19693 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19694 );
19695 })
19696 });
19697 pane_2.update(cx, |pane, cx| {
19698 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19699 open_editor.update(cx, |editor, cx| {
19700 assert_eq!(
19701 editor.display_text(cx),
19702 r#"fn main() {
19703⋯rintln!("1");
19704⋯intln!("2");
19705⋯ntln!("3");
19706println!("4");
19707println!("5");
19708}"#,
19709 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19710 );
19711 assert_eq!(
19712 editor
19713 .selections
19714 .all::<Point>(cx)
19715 .into_iter()
19716 .map(|s| s.range())
19717 .collect::<Vec<_>>(),
19718 vec![Point::zero()..Point::zero()],
19719 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19720 );
19721 })
19722 });
19723}
19724
19725#[gpui::test]
19726async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19727 init_test(cx, |_| {});
19728
19729 let fs = FakeFs::new(cx.executor());
19730 let main_text = r#"fn main() {
19731println!("1");
19732println!("2");
19733println!("3");
19734println!("4");
19735println!("5");
19736}"#;
19737 let lib_text = "mod foo {}";
19738 fs.insert_tree(
19739 path!("/a"),
19740 json!({
19741 "lib.rs": lib_text,
19742 "main.rs": main_text,
19743 }),
19744 )
19745 .await;
19746
19747 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19748 let (workspace, cx) =
19749 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19750 let worktree_id = workspace.update(cx, |workspace, cx| {
19751 workspace.project().update(cx, |project, cx| {
19752 project.worktrees(cx).next().unwrap().read(cx).id()
19753 })
19754 });
19755
19756 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19757 let editor = workspace
19758 .update_in(cx, |workspace, window, cx| {
19759 workspace.open_path(
19760 (worktree_id, "main.rs"),
19761 Some(pane.downgrade()),
19762 true,
19763 window,
19764 cx,
19765 )
19766 })
19767 .unwrap()
19768 .await
19769 .downcast::<Editor>()
19770 .unwrap();
19771 pane.update(cx, |pane, cx| {
19772 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19773 open_editor.update(cx, |editor, cx| {
19774 assert_eq!(
19775 editor.display_text(cx),
19776 main_text,
19777 "Original main.rs text on initial open",
19778 );
19779 })
19780 });
19781 editor.update_in(cx, |editor, window, cx| {
19782 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19783 });
19784
19785 cx.update_global(|store: &mut SettingsStore, cx| {
19786 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19787 s.restore_on_file_reopen = Some(false);
19788 });
19789 });
19790 editor.update_in(cx, |editor, window, cx| {
19791 editor.fold_ranges(
19792 vec![
19793 Point::new(1, 0)..Point::new(1, 1),
19794 Point::new(2, 0)..Point::new(2, 2),
19795 Point::new(3, 0)..Point::new(3, 3),
19796 ],
19797 false,
19798 window,
19799 cx,
19800 );
19801 });
19802 pane.update_in(cx, |pane, window, cx| {
19803 pane.close_all_items(&CloseAllItems::default(), window, cx)
19804 .unwrap()
19805 })
19806 .await
19807 .unwrap();
19808 pane.update(cx, |pane, _| {
19809 assert!(pane.active_item().is_none());
19810 });
19811 cx.update_global(|store: &mut SettingsStore, cx| {
19812 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19813 s.restore_on_file_reopen = Some(true);
19814 });
19815 });
19816
19817 let _editor_reopened = workspace
19818 .update_in(cx, |workspace, window, cx| {
19819 workspace.open_path(
19820 (worktree_id, "main.rs"),
19821 Some(pane.downgrade()),
19822 true,
19823 window,
19824 cx,
19825 )
19826 })
19827 .unwrap()
19828 .await
19829 .downcast::<Editor>()
19830 .unwrap();
19831 pane.update(cx, |pane, cx| {
19832 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19833 open_editor.update(cx, |editor, cx| {
19834 assert_eq!(
19835 editor.display_text(cx),
19836 main_text,
19837 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19838 );
19839 })
19840 });
19841}
19842
19843#[gpui::test]
19844async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19845 struct EmptyModalView {
19846 focus_handle: gpui::FocusHandle,
19847 }
19848 impl EventEmitter<DismissEvent> for EmptyModalView {}
19849 impl Render for EmptyModalView {
19850 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19851 div()
19852 }
19853 }
19854 impl Focusable for EmptyModalView {
19855 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19856 self.focus_handle.clone()
19857 }
19858 }
19859 impl workspace::ModalView for EmptyModalView {}
19860 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19861 EmptyModalView {
19862 focus_handle: cx.focus_handle(),
19863 }
19864 }
19865
19866 init_test(cx, |_| {});
19867
19868 let fs = FakeFs::new(cx.executor());
19869 let project = Project::test(fs, [], cx).await;
19870 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19871 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19872 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19873 let editor = cx.new_window_entity(|window, cx| {
19874 Editor::new(
19875 EditorMode::full(),
19876 buffer,
19877 Some(project.clone()),
19878 window,
19879 cx,
19880 )
19881 });
19882 workspace
19883 .update(cx, |workspace, window, cx| {
19884 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19885 })
19886 .unwrap();
19887 editor.update_in(cx, |editor, window, cx| {
19888 editor.open_context_menu(&OpenContextMenu, window, cx);
19889 assert!(editor.mouse_context_menu.is_some());
19890 });
19891 workspace
19892 .update(cx, |workspace, window, cx| {
19893 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19894 })
19895 .unwrap();
19896 cx.read(|cx| {
19897 assert!(editor.read(cx).mouse_context_menu.is_none());
19898 });
19899}
19900
19901#[gpui::test]
19902async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19903 init_test(cx, |_| {});
19904
19905 let fs = FakeFs::new(cx.executor());
19906 fs.insert_file(path!("/file.html"), Default::default())
19907 .await;
19908
19909 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19910
19911 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19912 let html_language = Arc::new(Language::new(
19913 LanguageConfig {
19914 name: "HTML".into(),
19915 matcher: LanguageMatcher {
19916 path_suffixes: vec!["html".to_string()],
19917 ..LanguageMatcher::default()
19918 },
19919 brackets: BracketPairConfig {
19920 pairs: vec![BracketPair {
19921 start: "<".into(),
19922 end: ">".into(),
19923 close: true,
19924 ..Default::default()
19925 }],
19926 ..Default::default()
19927 },
19928 ..Default::default()
19929 },
19930 Some(tree_sitter_html::LANGUAGE.into()),
19931 ));
19932 language_registry.add(html_language);
19933 let mut fake_servers = language_registry.register_fake_lsp(
19934 "HTML",
19935 FakeLspAdapter {
19936 capabilities: lsp::ServerCapabilities {
19937 completion_provider: Some(lsp::CompletionOptions {
19938 resolve_provider: Some(true),
19939 ..Default::default()
19940 }),
19941 ..Default::default()
19942 },
19943 ..Default::default()
19944 },
19945 );
19946
19947 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19948 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19949
19950 let worktree_id = workspace
19951 .update(cx, |workspace, _window, cx| {
19952 workspace.project().update(cx, |project, cx| {
19953 project.worktrees(cx).next().unwrap().read(cx).id()
19954 })
19955 })
19956 .unwrap();
19957 project
19958 .update(cx, |project, cx| {
19959 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19960 })
19961 .await
19962 .unwrap();
19963 let editor = workspace
19964 .update(cx, |workspace, window, cx| {
19965 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19966 })
19967 .unwrap()
19968 .await
19969 .unwrap()
19970 .downcast::<Editor>()
19971 .unwrap();
19972
19973 let fake_server = fake_servers.next().await.unwrap();
19974 editor.update_in(cx, |editor, window, cx| {
19975 editor.set_text("<ad></ad>", window, cx);
19976 editor.change_selections(None, window, cx, |selections| {
19977 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
19978 });
19979 let Some((buffer, _)) = editor
19980 .buffer
19981 .read(cx)
19982 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
19983 else {
19984 panic!("Failed to get buffer for selection position");
19985 };
19986 let buffer = buffer.read(cx);
19987 let buffer_id = buffer.remote_id();
19988 let opening_range =
19989 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
19990 let closing_range =
19991 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
19992 let mut linked_ranges = HashMap::default();
19993 linked_ranges.insert(
19994 buffer_id,
19995 vec![(opening_range.clone(), vec![closing_range.clone()])],
19996 );
19997 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
19998 });
19999 let mut completion_handle =
20000 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20001 Ok(Some(lsp::CompletionResponse::Array(vec![
20002 lsp::CompletionItem {
20003 label: "head".to_string(),
20004 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20005 lsp::InsertReplaceEdit {
20006 new_text: "head".to_string(),
20007 insert: lsp::Range::new(
20008 lsp::Position::new(0, 1),
20009 lsp::Position::new(0, 3),
20010 ),
20011 replace: lsp::Range::new(
20012 lsp::Position::new(0, 1),
20013 lsp::Position::new(0, 3),
20014 ),
20015 },
20016 )),
20017 ..Default::default()
20018 },
20019 ])))
20020 });
20021 editor.update_in(cx, |editor, window, cx| {
20022 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20023 });
20024 cx.run_until_parked();
20025 completion_handle.next().await.unwrap();
20026 editor.update(cx, |editor, _| {
20027 assert!(
20028 editor.context_menu_visible(),
20029 "Completion menu should be visible"
20030 );
20031 });
20032 editor.update_in(cx, |editor, window, cx| {
20033 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20034 });
20035 cx.executor().run_until_parked();
20036 editor.update(cx, |editor, cx| {
20037 assert_eq!(editor.text(cx), "<head></head>");
20038 });
20039}
20040
20041#[gpui::test]
20042async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20043 init_test(cx, |_| {});
20044
20045 let fs = FakeFs::new(cx.executor());
20046 fs.insert_tree(
20047 path!("/root"),
20048 json!({
20049 "a": {
20050 "main.rs": "fn main() {}",
20051 },
20052 "foo": {
20053 "bar": {
20054 "external_file.rs": "pub mod external {}",
20055 }
20056 }
20057 }),
20058 )
20059 .await;
20060
20061 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20062 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20063 language_registry.add(rust_lang());
20064 let _fake_servers = language_registry.register_fake_lsp(
20065 "Rust",
20066 FakeLspAdapter {
20067 ..FakeLspAdapter::default()
20068 },
20069 );
20070 let (workspace, cx) =
20071 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20072 let worktree_id = workspace.update(cx, |workspace, cx| {
20073 workspace.project().update(cx, |project, cx| {
20074 project.worktrees(cx).next().unwrap().read(cx).id()
20075 })
20076 });
20077
20078 let assert_language_servers_count =
20079 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20080 project.update(cx, |project, cx| {
20081 let current = project
20082 .lsp_store()
20083 .read(cx)
20084 .as_local()
20085 .unwrap()
20086 .language_servers
20087 .len();
20088 assert_eq!(expected, current, "{context}");
20089 });
20090 };
20091
20092 assert_language_servers_count(
20093 0,
20094 "No servers should be running before any file is open",
20095 cx,
20096 );
20097 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20098 let main_editor = workspace
20099 .update_in(cx, |workspace, window, cx| {
20100 workspace.open_path(
20101 (worktree_id, "main.rs"),
20102 Some(pane.downgrade()),
20103 true,
20104 window,
20105 cx,
20106 )
20107 })
20108 .unwrap()
20109 .await
20110 .downcast::<Editor>()
20111 .unwrap();
20112 pane.update(cx, |pane, cx| {
20113 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20114 open_editor.update(cx, |editor, cx| {
20115 assert_eq!(
20116 editor.display_text(cx),
20117 "fn main() {}",
20118 "Original main.rs text on initial open",
20119 );
20120 });
20121 assert_eq!(open_editor, main_editor);
20122 });
20123 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20124
20125 let external_editor = workspace
20126 .update_in(cx, |workspace, window, cx| {
20127 workspace.open_abs_path(
20128 PathBuf::from("/root/foo/bar/external_file.rs"),
20129 OpenOptions::default(),
20130 window,
20131 cx,
20132 )
20133 })
20134 .await
20135 .expect("opening external file")
20136 .downcast::<Editor>()
20137 .expect("downcasted external file's open element to editor");
20138 pane.update(cx, |pane, cx| {
20139 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20140 open_editor.update(cx, |editor, cx| {
20141 assert_eq!(
20142 editor.display_text(cx),
20143 "pub mod external {}",
20144 "External file is open now",
20145 );
20146 });
20147 assert_eq!(open_editor, external_editor);
20148 });
20149 assert_language_servers_count(
20150 1,
20151 "Second, external, *.rs file should join the existing server",
20152 cx,
20153 );
20154
20155 pane.update_in(cx, |pane, window, cx| {
20156 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20157 })
20158 .unwrap()
20159 .await
20160 .unwrap();
20161 pane.update_in(cx, |pane, window, cx| {
20162 pane.navigate_backward(window, cx);
20163 });
20164 cx.run_until_parked();
20165 pane.update(cx, |pane, cx| {
20166 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20167 open_editor.update(cx, |editor, cx| {
20168 assert_eq!(
20169 editor.display_text(cx),
20170 "pub mod external {}",
20171 "External file is open now",
20172 );
20173 });
20174 });
20175 assert_language_servers_count(
20176 1,
20177 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20178 cx,
20179 );
20180
20181 cx.update(|_, cx| {
20182 workspace::reload(&workspace::Reload::default(), cx);
20183 });
20184 assert_language_servers_count(
20185 1,
20186 "After reloading the worktree with local and external files opened, only one project should be started",
20187 cx,
20188 );
20189}
20190
20191fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20192 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20193 point..point
20194}
20195
20196fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20197 let (text, ranges) = marked_text_ranges(marked_text, true);
20198 assert_eq!(editor.text(cx), text);
20199 assert_eq!(
20200 editor.selections.ranges(cx),
20201 ranges,
20202 "Assert selections are {}",
20203 marked_text
20204 );
20205}
20206
20207pub fn handle_signature_help_request(
20208 cx: &mut EditorLspTestContext,
20209 mocked_response: lsp::SignatureHelp,
20210) -> impl Future<Output = ()> + use<> {
20211 let mut request =
20212 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20213 let mocked_response = mocked_response.clone();
20214 async move { Ok(Some(mocked_response)) }
20215 });
20216
20217 async move {
20218 request.next().await;
20219 }
20220}
20221
20222/// Handle completion request passing a marked string specifying where the completion
20223/// should be triggered from using '|' character, what range should be replaced, and what completions
20224/// should be returned using '<' and '>' to delimit the range.
20225///
20226/// Also see `handle_completion_request_with_insert_and_replace`.
20227#[track_caller]
20228pub fn handle_completion_request(
20229 cx: &mut EditorLspTestContext,
20230 marked_string: &str,
20231 completions: Vec<&'static str>,
20232 counter: Arc<AtomicUsize>,
20233) -> impl Future<Output = ()> {
20234 let complete_from_marker: TextRangeMarker = '|'.into();
20235 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20236 let (_, mut marked_ranges) = marked_text_ranges_by(
20237 marked_string,
20238 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20239 );
20240
20241 let complete_from_position =
20242 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20243 let replace_range =
20244 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20245
20246 let mut request =
20247 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20248 let completions = completions.clone();
20249 counter.fetch_add(1, atomic::Ordering::Release);
20250 async move {
20251 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20252 assert_eq!(
20253 params.text_document_position.position,
20254 complete_from_position
20255 );
20256 Ok(Some(lsp::CompletionResponse::Array(
20257 completions
20258 .iter()
20259 .map(|completion_text| lsp::CompletionItem {
20260 label: completion_text.to_string(),
20261 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20262 range: replace_range,
20263 new_text: completion_text.to_string(),
20264 })),
20265 ..Default::default()
20266 })
20267 .collect(),
20268 )))
20269 }
20270 });
20271
20272 async move {
20273 request.next().await;
20274 }
20275}
20276
20277/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20278/// given instead, which also contains an `insert` range.
20279///
20280/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20281/// that is, `replace_range.start..cursor_pos`.
20282pub fn handle_completion_request_with_insert_and_replace(
20283 cx: &mut EditorLspTestContext,
20284 marked_string: &str,
20285 completions: Vec<&'static str>,
20286 counter: Arc<AtomicUsize>,
20287) -> impl Future<Output = ()> {
20288 let complete_from_marker: TextRangeMarker = '|'.into();
20289 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20290 let (_, mut marked_ranges) = marked_text_ranges_by(
20291 marked_string,
20292 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20293 );
20294
20295 let complete_from_position =
20296 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20297 let replace_range =
20298 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20299
20300 let mut request =
20301 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20302 let completions = completions.clone();
20303 counter.fetch_add(1, atomic::Ordering::Release);
20304 async move {
20305 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20306 assert_eq!(
20307 params.text_document_position.position, complete_from_position,
20308 "marker `|` position doesn't match",
20309 );
20310 Ok(Some(lsp::CompletionResponse::Array(
20311 completions
20312 .iter()
20313 .map(|completion_text| lsp::CompletionItem {
20314 label: completion_text.to_string(),
20315 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20316 lsp::InsertReplaceEdit {
20317 insert: lsp::Range {
20318 start: replace_range.start,
20319 end: complete_from_position,
20320 },
20321 replace: replace_range,
20322 new_text: completion_text.to_string(),
20323 },
20324 )),
20325 ..Default::default()
20326 })
20327 .collect(),
20328 )))
20329 }
20330 });
20331
20332 async move {
20333 request.next().await;
20334 }
20335}
20336
20337fn handle_resolve_completion_request(
20338 cx: &mut EditorLspTestContext,
20339 edits: Option<Vec<(&'static str, &'static str)>>,
20340) -> impl Future<Output = ()> {
20341 let edits = edits.map(|edits| {
20342 edits
20343 .iter()
20344 .map(|(marked_string, new_text)| {
20345 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20346 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20347 lsp::TextEdit::new(replace_range, new_text.to_string())
20348 })
20349 .collect::<Vec<_>>()
20350 });
20351
20352 let mut request =
20353 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20354 let edits = edits.clone();
20355 async move {
20356 Ok(lsp::CompletionItem {
20357 additional_text_edits: edits,
20358 ..Default::default()
20359 })
20360 }
20361 });
20362
20363 async move {
20364 request.next().await;
20365 }
20366}
20367
20368pub(crate) fn update_test_language_settings(
20369 cx: &mut TestAppContext,
20370 f: impl Fn(&mut AllLanguageSettingsContent),
20371) {
20372 cx.update(|cx| {
20373 SettingsStore::update_global(cx, |store, cx| {
20374 store.update_user_settings::<AllLanguageSettings>(cx, f);
20375 });
20376 });
20377}
20378
20379pub(crate) fn update_test_project_settings(
20380 cx: &mut TestAppContext,
20381 f: impl Fn(&mut ProjectSettings),
20382) {
20383 cx.update(|cx| {
20384 SettingsStore::update_global(cx, |store, cx| {
20385 store.update_user_settings::<ProjectSettings>(cx, f);
20386 });
20387 });
20388}
20389
20390pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20391 cx.update(|cx| {
20392 assets::Assets.load_test_fonts(cx);
20393 let store = SettingsStore::test(cx);
20394 cx.set_global(store);
20395 theme::init(theme::LoadThemes::JustBase, cx);
20396 release_channel::init(SemanticVersion::default(), cx);
20397 client::init_settings(cx);
20398 language::init(cx);
20399 Project::init_settings(cx);
20400 workspace::init_settings(cx);
20401 crate::init(cx);
20402 });
20403
20404 update_test_language_settings(cx, f);
20405}
20406
20407#[track_caller]
20408fn assert_hunk_revert(
20409 not_reverted_text_with_selections: &str,
20410 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
20411 expected_reverted_text_with_selections: &str,
20412 base_text: &str,
20413 cx: &mut EditorLspTestContext,
20414) {
20415 cx.set_state(not_reverted_text_with_selections);
20416 cx.set_head_text(base_text);
20417 cx.executor().run_until_parked();
20418
20419 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
20420 let snapshot = editor.snapshot(window, cx);
20421 let reverted_hunk_statuses = snapshot
20422 .buffer_snapshot
20423 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
20424 .map(|hunk| hunk.status().kind)
20425 .collect::<Vec<_>>();
20426
20427 editor.git_restore(&Default::default(), window, cx);
20428 reverted_hunk_statuses
20429 });
20430 cx.executor().run_until_parked();
20431 cx.assert_editor_state(expected_reverted_text_with_selections);
20432 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20433}