1use super::*;
2use crate::{
3 JoinLines,
4 scroll::scroll_amount::ScrollAmount,
5 test::{
6 assert_text_with_selections, build_editor,
7 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
8 editor_test_context::EditorTestContext,
9 select_ranges,
10 },
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions, div,
17};
18use indoc::indoc;
19use language::{
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, Point,
24 language_settings::{
25 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
26 LanguageSettingsContent, LspInsertMode, PrettierSettings,
27 },
28};
29use language_settings::{Formatter, FormatterList, IndentGuideSettings};
30use lsp::CompletionParams;
31use multi_buffer::{IndentGuide, PathKey};
32use parking_lot::Mutex;
33use pretty_assertions::{assert_eq, assert_ne};
34use project::{
35 FakeFs,
36 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
37 project_settings::{LspSettings, ProjectSettings},
38};
39use serde_json::{self, json};
40use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
41use std::{
42 iter,
43 sync::atomic::{self, AtomicUsize},
44};
45use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
46use text::ToPoint as _;
47use unindent::Unindent;
48use util::{
49 assert_set_eq, path,
50 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
51 uri,
52};
53use workspace::{
54 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
55 item::{FollowEvent, FollowableItem, Item, ItemHandle},
56};
57
58#[gpui::test]
59fn test_edit_events(cx: &mut TestAppContext) {
60 init_test(cx, |_| {});
61
62 let buffer = cx.new(|cx| {
63 let mut buffer = language::Buffer::local("123456", cx);
64 buffer.set_group_interval(Duration::from_secs(1));
65 buffer
66 });
67
68 let events = Rc::new(RefCell::new(Vec::new()));
69 let editor1 = cx.add_window({
70 let events = events.clone();
71 |window, cx| {
72 let entity = cx.entity().clone();
73 cx.subscribe_in(
74 &entity,
75 window,
76 move |_, _, event: &EditorEvent, _, _| match event {
77 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
78 EditorEvent::BufferEdited => {
79 events.borrow_mut().push(("editor1", "buffer edited"))
80 }
81 _ => {}
82 },
83 )
84 .detach();
85 Editor::for_buffer(buffer.clone(), None, window, cx)
86 }
87 });
88
89 let editor2 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 cx.subscribe_in(
93 &cx.entity().clone(),
94 window,
95 move |_, _, event: &EditorEvent, _, _| match event {
96 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
97 EditorEvent::BufferEdited => {
98 events.borrow_mut().push(("editor2", "buffer edited"))
99 }
100 _ => {}
101 },
102 )
103 .detach();
104 Editor::for_buffer(buffer.clone(), None, window, cx)
105 }
106 });
107
108 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
109
110 // Mutating editor 1 will emit an `Edited` event only for that editor.
111 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
112 assert_eq!(
113 mem::take(&mut *events.borrow_mut()),
114 [
115 ("editor1", "edited"),
116 ("editor1", "buffer edited"),
117 ("editor2", "buffer edited"),
118 ]
119 );
120
121 // Mutating editor 2 will emit an `Edited` event only for that editor.
122 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor2", "edited"),
127 ("editor1", "buffer edited"),
128 ("editor2", "buffer edited"),
129 ]
130 );
131
132 // Undoing on editor 1 will emit an `Edited` event only for that editor.
133 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor1", "edited"),
138 ("editor1", "buffer edited"),
139 ("editor2", "buffer edited"),
140 ]
141 );
142
143 // Redoing on editor 1 will emit an `Edited` event only for that editor.
144 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
145 assert_eq!(
146 mem::take(&mut *events.borrow_mut()),
147 [
148 ("editor1", "edited"),
149 ("editor1", "buffer edited"),
150 ("editor2", "buffer edited"),
151 ]
152 );
153
154 // Undoing on editor 2 will emit an `Edited` event only for that editor.
155 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
156 assert_eq!(
157 mem::take(&mut *events.borrow_mut()),
158 [
159 ("editor2", "edited"),
160 ("editor1", "buffer edited"),
161 ("editor2", "buffer edited"),
162 ]
163 );
164
165 // Redoing on editor 2 will emit an `Edited` event only for that editor.
166 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
167 assert_eq!(
168 mem::take(&mut *events.borrow_mut()),
169 [
170 ("editor2", "edited"),
171 ("editor1", "buffer edited"),
172 ("editor2", "buffer edited"),
173 ]
174 );
175
176 // No event is emitted when the mutation is a no-op.
177 _ = editor2.update(cx, |editor, window, cx| {
178 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
179
180 editor.backspace(&Backspace, window, cx);
181 });
182 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
183}
184
185#[gpui::test]
186fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
187 init_test(cx, |_| {});
188
189 let mut now = Instant::now();
190 let group_interval = Duration::from_millis(1);
191 let buffer = cx.new(|cx| {
192 let mut buf = language::Buffer::local("123456", cx);
193 buf.set_group_interval(group_interval);
194 buf
195 });
196 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
197 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
198
199 _ = editor.update(cx, |editor, window, cx| {
200 editor.start_transaction_at(now, window, cx);
201 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
202
203 editor.insert("cd", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cd56");
206 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
207
208 editor.start_transaction_at(now, window, cx);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
210 editor.insert("e", window, cx);
211 editor.end_transaction_at(now, cx);
212 assert_eq!(editor.text(cx), "12cde6");
213 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
214
215 now += group_interval + Duration::from_millis(1);
216 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
217
218 // Simulate an edit in another editor
219 buffer.update(cx, |buffer, cx| {
220 buffer.start_transaction_at(now, cx);
221 buffer.edit([(0..1, "a")], None, cx);
222 buffer.edit([(1..1, "b")], None, cx);
223 buffer.end_transaction_at(now, cx);
224 });
225
226 assert_eq!(editor.text(cx), "ab2cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
228
229 // Last transaction happened past the group interval in a different editor.
230 // Undo it individually and don't restore selections.
231 editor.undo(&Undo, window, cx);
232 assert_eq!(editor.text(cx), "12cde6");
233 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
234
235 // First two transactions happened within the group interval in this editor.
236 // Undo them together and restore selections.
237 editor.undo(&Undo, window, cx);
238 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
239 assert_eq!(editor.text(cx), "123456");
240 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
241
242 // Redo the first two transactions together.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
246
247 // Redo the last transaction on its own.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "ab2cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
251
252 // Test empty transactions.
253 editor.start_transaction_at(now, window, cx);
254 editor.end_transaction_at(now, cx);
255 editor.undo(&Undo, window, cx);
256 assert_eq!(editor.text(cx), "12cde6");
257 });
258}
259
260#[gpui::test]
261fn test_ime_composition(cx: &mut TestAppContext) {
262 init_test(cx, |_| {});
263
264 let buffer = cx.new(|cx| {
265 let mut buffer = language::Buffer::local("abcde", cx);
266 // Ensure automatic grouping doesn't occur.
267 buffer.set_group_interval(Duration::ZERO);
268 buffer
269 });
270
271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
272 cx.add_window(|window, cx| {
273 let mut editor = build_editor(buffer.clone(), window, cx);
274
275 // Start a new IME composition.
276 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
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 assert_eq!(editor.text(cx), "äbcde");
280 assert_eq!(
281 editor.marked_text_ranges(cx),
282 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
283 );
284
285 // Finalize IME composition.
286 editor.replace_text_in_range(None, "ā", window, cx);
287 assert_eq!(editor.text(cx), "ābcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // IME composition edits are grouped and are undone/redone at once.
291 editor.undo(&Default::default(), window, cx);
292 assert_eq!(editor.text(cx), "abcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294 editor.redo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "ābcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297
298 // Start a new IME composition.
299 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Undoing during an IME composition cancels it.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
311 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
312 assert_eq!(editor.text(cx), "ābcdè");
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
316 );
317
318 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
319 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
320 assert_eq!(editor.text(cx), "ābcdę");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with multiple cursors.
324 editor.change_selections(None, window, cx, |s| {
325 s.select_ranges([
326 OffsetUtf16(1)..OffsetUtf16(1),
327 OffsetUtf16(3)..OffsetUtf16(3),
328 OffsetUtf16(5)..OffsetUtf16(5),
329 ])
330 });
331 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
332 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![
336 OffsetUtf16(0)..OffsetUtf16(3),
337 OffsetUtf16(4)..OffsetUtf16(7),
338 OffsetUtf16(8)..OffsetUtf16(11)
339 ])
340 );
341
342 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
343 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
344 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
345 assert_eq!(
346 editor.marked_text_ranges(cx),
347 Some(vec![
348 OffsetUtf16(1)..OffsetUtf16(2),
349 OffsetUtf16(5)..OffsetUtf16(6),
350 OffsetUtf16(9)..OffsetUtf16(10)
351 ])
352 );
353
354 // Finalize IME composition with multiple cursors.
355 editor.replace_text_in_range(Some(9..10), "2", window, cx);
356 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 editor
360 });
361}
362
363#[gpui::test]
364fn test_selection_with_mouse(cx: &mut TestAppContext) {
365 init_test(cx, |_| {});
366
367 let editor = cx.add_window(|window, cx| {
368 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
369 build_editor(buffer, window, cx)
370 });
371
372 _ = editor.update(cx, |editor, window, cx| {
373 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
374 });
375 assert_eq!(
376 editor
377 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
378 .unwrap(),
379 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
380 );
381
382 _ = editor.update(cx, |editor, window, cx| {
383 editor.update_selection(
384 DisplayPoint::new(DisplayRow(3), 3),
385 0,
386 gpui::Point::<f32>::default(),
387 window,
388 cx,
389 );
390 });
391
392 assert_eq!(
393 editor
394 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
395 .unwrap(),
396 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
397 );
398
399 _ = editor.update(cx, |editor, window, cx| {
400 editor.update_selection(
401 DisplayPoint::new(DisplayRow(1), 1),
402 0,
403 gpui::Point::<f32>::default(),
404 window,
405 cx,
406 );
407 });
408
409 assert_eq!(
410 editor
411 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
412 .unwrap(),
413 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
414 );
415
416 _ = editor.update(cx, |editor, window, cx| {
417 editor.end_selection(window, cx);
418 editor.update_selection(
419 DisplayPoint::new(DisplayRow(3), 3),
420 0,
421 gpui::Point::<f32>::default(),
422 window,
423 cx,
424 );
425 });
426
427 assert_eq!(
428 editor
429 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
430 .unwrap(),
431 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
432 );
433
434 _ = editor.update(cx, |editor, window, cx| {
435 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
436 editor.update_selection(
437 DisplayPoint::new(DisplayRow(0), 0),
438 0,
439 gpui::Point::<f32>::default(),
440 window,
441 cx,
442 );
443 });
444
445 assert_eq!(
446 editor
447 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
448 .unwrap(),
449 [
450 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
451 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
452 ]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.end_selection(window, cx);
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
464 );
465}
466
467#[gpui::test]
468fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
469 init_test(cx, |_| {});
470
471 let editor = cx.add_window(|window, cx| {
472 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
473 build_editor(buffer, window, cx)
474 });
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.end_selection(window, cx);
490 });
491
492 assert_eq!(
493 editor
494 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
495 .unwrap(),
496 [
497 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
498 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
499 ]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
504 });
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.end_selection(window, cx);
508 });
509
510 assert_eq!(
511 editor
512 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
513 .unwrap(),
514 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
515 );
516}
517
518#[gpui::test]
519fn test_canceling_pending_selection(cx: &mut TestAppContext) {
520 init_test(cx, |_| {});
521
522 let editor = cx.add_window(|window, cx| {
523 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
524 build_editor(buffer, window, cx)
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
529 assert_eq!(
530 editor.selections.display_ranges(cx),
531 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
532 );
533 });
534
535 _ = editor.update(cx, |editor, window, cx| {
536 editor.update_selection(
537 DisplayPoint::new(DisplayRow(3), 3),
538 0,
539 gpui::Point::<f32>::default(),
540 window,
541 cx,
542 );
543 assert_eq!(
544 editor.selections.display_ranges(cx),
545 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
546 );
547 });
548
549 _ = editor.update(cx, |editor, window, cx| {
550 editor.cancel(&Cancel, window, cx);
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(1), 1),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563}
564
565#[gpui::test]
566fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
567 init_test(cx, |_| {});
568
569 let editor = cx.add_window(|window, cx| {
570 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
571 build_editor(buffer, window, cx)
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
576 assert_eq!(
577 editor.selections.display_ranges(cx),
578 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
579 );
580
581 editor.move_down(&Default::default(), window, cx);
582 assert_eq!(
583 editor.selections.display_ranges(cx),
584 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
585 );
586
587 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
591 );
592
593 editor.move_up(&Default::default(), window, cx);
594 assert_eq!(
595 editor.selections.display_ranges(cx),
596 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
597 );
598 });
599}
600
601#[gpui::test]
602fn test_clone(cx: &mut TestAppContext) {
603 init_test(cx, |_| {});
604
605 let (text, selection_ranges) = marked_text_ranges(
606 indoc! {"
607 one
608 two
609 threeˇ
610 four
611 fiveˇ
612 "},
613 true,
614 );
615
616 let editor = cx.add_window(|window, cx| {
617 let buffer = MultiBuffer::build_simple(&text, cx);
618 build_editor(buffer, window, cx)
619 });
620
621 _ = editor.update(cx, |editor, window, cx| {
622 editor.change_selections(None, window, cx, |s| {
623 s.select_ranges(selection_ranges.clone())
624 });
625 editor.fold_creases(
626 vec![
627 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
628 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
629 ],
630 true,
631 window,
632 cx,
633 );
634 });
635
636 let cloned_editor = editor
637 .update(cx, |editor, _, cx| {
638 cx.open_window(Default::default(), |window, cx| {
639 cx.new(|cx| editor.clone(window, cx))
640 })
641 })
642 .unwrap()
643 .unwrap();
644
645 let snapshot = editor
646 .update(cx, |e, window, cx| e.snapshot(window, cx))
647 .unwrap();
648 let cloned_snapshot = cloned_editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651
652 assert_eq!(
653 cloned_editor
654 .update(cx, |e, _, cx| e.display_text(cx))
655 .unwrap(),
656 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
657 );
658 assert_eq!(
659 cloned_snapshot
660 .folds_in_range(0..text.len())
661 .collect::<Vec<_>>(),
662 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
663 );
664 assert_set_eq!(
665 cloned_editor
666 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
667 .unwrap(),
668 editor
669 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
670 .unwrap()
671 );
672 assert_set_eq!(
673 cloned_editor
674 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
675 .unwrap(),
676 editor
677 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
678 .unwrap()
679 );
680}
681
682#[gpui::test]
683async fn test_navigation_history(cx: &mut TestAppContext) {
684 init_test(cx, |_| {});
685
686 use workspace::item::Item;
687
688 let fs = FakeFs::new(cx.executor());
689 let project = Project::test(fs, [], cx).await;
690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
691 let pane = workspace
692 .update(cx, |workspace, _, _| workspace.active_pane().clone())
693 .unwrap();
694
695 _ = workspace.update(cx, |_v, window, cx| {
696 cx.new(|cx| {
697 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
698 let mut editor = build_editor(buffer.clone(), window, cx);
699 let handle = cx.entity();
700 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
701
702 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
703 editor.nav_history.as_mut().unwrap().pop_backward(cx)
704 }
705
706 // Move the cursor a small distance.
707 // Nothing is added to the navigation history.
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
711 ])
712 });
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
716 ])
717 });
718 assert!(pop_history(&mut editor, cx).is_none());
719
720 // Move the cursor a large distance.
721 // The history can jump back to the previous position.
722 editor.change_selections(None, window, cx, |s| {
723 s.select_display_ranges([
724 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
725 ])
726 });
727 let nav_entry = pop_history(&mut editor, cx).unwrap();
728 editor.navigate(nav_entry.data.unwrap(), window, cx);
729 assert_eq!(nav_entry.item.id(), cx.entity_id());
730 assert_eq!(
731 editor.selections.display_ranges(cx),
732 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
733 );
734 assert!(pop_history(&mut editor, cx).is_none());
735
736 // Move the cursor a small distance via the mouse.
737 // Nothing is added to the navigation history.
738 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
739 editor.end_selection(window, cx);
740 assert_eq!(
741 editor.selections.display_ranges(cx),
742 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
743 );
744 assert!(pop_history(&mut editor, cx).is_none());
745
746 // Move the cursor a large distance via the mouse.
747 // The history can jump back to the previous position.
748 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
749 editor.end_selection(window, cx);
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
753 );
754 let nav_entry = pop_history(&mut editor, cx).unwrap();
755 editor.navigate(nav_entry.data.unwrap(), window, cx);
756 assert_eq!(nav_entry.item.id(), cx.entity_id());
757 assert_eq!(
758 editor.selections.display_ranges(cx),
759 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
760 );
761 assert!(pop_history(&mut editor, cx).is_none());
762
763 // Set scroll position to check later
764 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
765 let original_scroll_position = editor.scroll_manager.anchor();
766
767 // Jump to the end of the document and adjust scroll
768 editor.move_to_end(&MoveToEnd, window, cx);
769 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
770 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
771
772 let nav_entry = pop_history(&mut editor, cx).unwrap();
773 editor.navigate(nav_entry.data.unwrap(), window, cx);
774 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
777 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
778 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
779 let invalid_point = Point::new(9999, 0);
780 editor.navigate(
781 Box::new(NavigationData {
782 cursor_anchor: invalid_anchor,
783 cursor_position: invalid_point,
784 scroll_anchor: ScrollAnchor {
785 anchor: invalid_anchor,
786 offset: Default::default(),
787 },
788 scroll_top_row: invalid_point.row,
789 }),
790 window,
791 cx,
792 );
793 assert_eq!(
794 editor.selections.display_ranges(cx),
795 &[editor.max_point(cx)..editor.max_point(cx)]
796 );
797 assert_eq!(
798 editor.scroll_position(cx),
799 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
800 );
801
802 editor
803 })
804 });
805}
806
807#[gpui::test]
808fn test_cancel(cx: &mut TestAppContext) {
809 init_test(cx, |_| {});
810
811 let editor = cx.add_window(|window, cx| {
812 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
813 build_editor(buffer, window, cx)
814 });
815
816 _ = editor.update(cx, |editor, window, cx| {
817 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
818 editor.update_selection(
819 DisplayPoint::new(DisplayRow(1), 1),
820 0,
821 gpui::Point::<f32>::default(),
822 window,
823 cx,
824 );
825 editor.end_selection(window, cx);
826
827 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
828 editor.update_selection(
829 DisplayPoint::new(DisplayRow(0), 3),
830 0,
831 gpui::Point::<f32>::default(),
832 window,
833 cx,
834 );
835 editor.end_selection(window, cx);
836 assert_eq!(
837 editor.selections.display_ranges(cx),
838 [
839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
840 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
841 ]
842 );
843 });
844
845 _ = editor.update(cx, |editor, window, cx| {
846 editor.cancel(&Cancel, window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
850 );
851 });
852
853 _ = editor.update(cx, |editor, window, cx| {
854 editor.cancel(&Cancel, window, cx);
855 assert_eq!(
856 editor.selections.display_ranges(cx),
857 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
858 );
859 });
860}
861
862#[gpui::test]
863fn test_fold_action(cx: &mut TestAppContext) {
864 init_test(cx, |_| {});
865
866 let editor = cx.add_window(|window, cx| {
867 let buffer = MultiBuffer::build_simple(
868 &"
869 impl Foo {
870 // Hello!
871
872 fn a() {
873 1
874 }
875
876 fn b() {
877 2
878 }
879
880 fn c() {
881 3
882 }
883 }
884 "
885 .unindent(),
886 cx,
887 );
888 build_editor(buffer.clone(), window, cx)
889 });
890
891 _ = editor.update(cx, |editor, window, cx| {
892 editor.change_selections(None, window, cx, |s| {
893 s.select_display_ranges([
894 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
895 ]);
896 });
897 editor.fold(&Fold, window, cx);
898 assert_eq!(
899 editor.display_text(cx),
900 "
901 impl Foo {
902 // Hello!
903
904 fn a() {
905 1
906 }
907
908 fn b() {⋯
909 }
910
911 fn c() {⋯
912 }
913 }
914 "
915 .unindent(),
916 );
917
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {⋯
923 }
924 "
925 .unindent(),
926 );
927
928 editor.unfold_lines(&UnfoldLines, window, cx);
929 assert_eq!(
930 editor.display_text(cx),
931 "
932 impl Foo {
933 // Hello!
934
935 fn a() {
936 1
937 }
938
939 fn b() {⋯
940 }
941
942 fn c() {⋯
943 }
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 editor.buffer.read(cx).read(cx).text()
953 );
954 });
955}
956
957#[gpui::test]
958fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
959 init_test(cx, |_| {});
960
961 let editor = cx.add_window(|window, cx| {
962 let buffer = MultiBuffer::build_simple(
963 &"
964 class Foo:
965 # Hello!
966
967 def a():
968 print(1)
969
970 def b():
971 print(2)
972
973 def c():
974 print(3)
975 "
976 .unindent(),
977 cx,
978 );
979 build_editor(buffer.clone(), window, cx)
980 });
981
982 _ = editor.update(cx, |editor, window, cx| {
983 editor.change_selections(None, window, cx, |s| {
984 s.select_display_ranges([
985 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
986 ]);
987 });
988 editor.fold(&Fold, window, cx);
989 assert_eq!(
990 editor.display_text(cx),
991 "
992 class Foo:
993 # Hello!
994
995 def a():
996 print(1)
997
998 def b():⋯
999
1000 def c():⋯
1001 "
1002 .unindent(),
1003 );
1004
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 class Foo:⋯
1010 "
1011 .unindent(),
1012 );
1013
1014 editor.unfold_lines(&UnfoldLines, window, cx);
1015 assert_eq!(
1016 editor.display_text(cx),
1017 "
1018 class Foo:
1019 # Hello!
1020
1021 def a():
1022 print(1)
1023
1024 def b():⋯
1025
1026 def c():⋯
1027 "
1028 .unindent(),
1029 );
1030
1031 editor.unfold_lines(&UnfoldLines, window, cx);
1032 assert_eq!(
1033 editor.display_text(cx),
1034 editor.buffer.read(cx).read(cx).text()
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 class Foo:
1047 # Hello!
1048
1049 def a():
1050 print(1)
1051
1052 def b():
1053 print(2)
1054
1055
1056 def c():
1057 print(3)
1058
1059
1060 "
1061 .unindent(),
1062 cx,
1063 );
1064 build_editor(buffer.clone(), window, cx)
1065 });
1066
1067 _ = editor.update(cx, |editor, window, cx| {
1068 editor.change_selections(None, window, cx, |s| {
1069 s.select_display_ranges([
1070 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1071 ]);
1072 });
1073 editor.fold(&Fold, window, cx);
1074 assert_eq!(
1075 editor.display_text(cx),
1076 "
1077 class Foo:
1078 # Hello!
1079
1080 def a():
1081 print(1)
1082
1083 def b():⋯
1084
1085
1086 def c():⋯
1087
1088
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:⋯
1098
1099
1100 "
1101 .unindent(),
1102 );
1103
1104 editor.unfold_lines(&UnfoldLines, window, cx);
1105 assert_eq!(
1106 editor.display_text(cx),
1107 "
1108 class Foo:
1109 # Hello!
1110
1111 def a():
1112 print(1)
1113
1114 def b():⋯
1115
1116
1117 def c():⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_at_level(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148
1149 class Bar:
1150 # World!
1151
1152 def a():
1153 print(1)
1154
1155 def b():
1156 print(2)
1157
1158
1159 "
1160 .unindent(),
1161 cx,
1162 );
1163 build_editor(buffer.clone(), window, cx)
1164 });
1165
1166 _ = editor.update(cx, |editor, window, cx| {
1167 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():⋯
1175
1176 def b():⋯
1177
1178
1179 class Bar:
1180 # World!
1181
1182 def a():⋯
1183
1184 def b():⋯
1185
1186
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:⋯
1196
1197
1198 class Bar:⋯
1199
1200
1201 "
1202 .unindent(),
1203 );
1204
1205 editor.unfold_all(&UnfoldAll, window, cx);
1206 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():
1217 print(2)
1218
1219
1220 class Bar:
1221 # World!
1222
1223 def a():
1224 print(1)
1225
1226 def b():
1227 print(2)
1228
1229
1230 "
1231 .unindent(),
1232 );
1233
1234 assert_eq!(
1235 editor.display_text(cx),
1236 editor.buffer.read(cx).read(cx).text()
1237 );
1238 });
1239}
1240
1241#[gpui::test]
1242fn test_move_cursor(cx: &mut TestAppContext) {
1243 init_test(cx, |_| {});
1244
1245 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1246 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1247
1248 buffer.update(cx, |buffer, cx| {
1249 buffer.edit(
1250 vec![
1251 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1252 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1253 ],
1254 None,
1255 cx,
1256 );
1257 });
1258 _ = editor.update(cx, |editor, window, cx| {
1259 assert_eq!(
1260 editor.selections.display_ranges(cx),
1261 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1262 );
1263
1264 editor.move_down(&MoveDown, window, cx);
1265 assert_eq!(
1266 editor.selections.display_ranges(cx),
1267 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1268 );
1269
1270 editor.move_right(&MoveRight, window, cx);
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1274 );
1275
1276 editor.move_left(&MoveLeft, window, cx);
1277 assert_eq!(
1278 editor.selections.display_ranges(cx),
1279 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1280 );
1281
1282 editor.move_up(&MoveUp, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1286 );
1287
1288 editor.move_to_end(&MoveToEnd, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1292 );
1293
1294 editor.move_to_beginning(&MoveToBeginning, window, cx);
1295 assert_eq!(
1296 editor.selections.display_ranges(cx),
1297 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1298 );
1299
1300 editor.change_selections(None, window, cx, |s| {
1301 s.select_display_ranges([
1302 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1303 ]);
1304 });
1305 editor.select_to_beginning(&SelectToBeginning, window, cx);
1306 assert_eq!(
1307 editor.selections.display_ranges(cx),
1308 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1309 );
1310
1311 editor.select_to_end(&SelectToEnd, window, cx);
1312 assert_eq!(
1313 editor.selections.display_ranges(cx),
1314 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1315 );
1316 });
1317}
1318
1319// TODO: Re-enable this test
1320#[cfg(target_os = "macos")]
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["//".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 //ˇ
2775 "});
2776 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2777 cx.set_state(indoc! {"
2778 ˇ// Foo
2779 "});
2780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2781 cx.assert_editor_state(indoc! {"
2782
2783 ˇ// Foo
2784 "});
2785 }
2786 // Ensure that comment continuations can be disabled.
2787 update_test_language_settings(cx, |settings| {
2788 settings.defaults.extend_comment_on_newline = Some(false);
2789 });
2790 let mut cx = EditorTestContext::new(cx).await;
2791 cx.set_state(indoc! {"
2792 // Fooˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 ˇ
2798 "});
2799}
2800
2801#[gpui::test]
2802fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2803 init_test(cx, |_| {});
2804
2805 let editor = cx.add_window(|window, cx| {
2806 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2807 let mut editor = build_editor(buffer.clone(), window, cx);
2808 editor.change_selections(None, window, cx, |s| {
2809 s.select_ranges([3..4, 11..12, 19..20])
2810 });
2811 editor
2812 });
2813
2814 _ = editor.update(cx, |editor, window, cx| {
2815 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2816 editor.buffer.update(cx, |buffer, cx| {
2817 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2818 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2819 });
2820 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2821
2822 editor.insert("Z", window, cx);
2823 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2824
2825 // The selections are moved after the inserted characters
2826 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2827 });
2828}
2829
2830#[gpui::test]
2831async fn test_tab(cx: &mut TestAppContext) {
2832 init_test(cx, |settings| {
2833 settings.defaults.tab_size = NonZeroU32::new(3)
2834 });
2835
2836 let mut cx = EditorTestContext::new(cx).await;
2837 cx.set_state(indoc! {"
2838 ˇabˇc
2839 ˇ🏀ˇ🏀ˇefg
2840 dˇ
2841 "});
2842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2843 cx.assert_editor_state(indoc! {"
2844 ˇab ˇc
2845 ˇ🏀 ˇ🏀 ˇefg
2846 d ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 a
2851 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2852 "});
2853 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 a
2856 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2857 "});
2858}
2859
2860#[gpui::test]
2861async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let mut cx = EditorTestContext::new(cx).await;
2865 let language = Arc::new(
2866 Language::new(
2867 LanguageConfig::default(),
2868 Some(tree_sitter_rust::LANGUAGE.into()),
2869 )
2870 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2871 .unwrap(),
2872 );
2873 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2874
2875 // cursors that are already at the suggested indent level insert
2876 // a soft tab. cursors that are to the left of the suggested indent
2877 // auto-indent their line.
2878 cx.set_state(indoc! {"
2879 ˇ
2880 const a: B = (
2881 c(
2882 d(
2883 ˇ
2884 )
2885 ˇ
2886 ˇ )
2887 );
2888 "});
2889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2890 cx.assert_editor_state(indoc! {"
2891 ˇ
2892 const a: B = (
2893 c(
2894 d(
2895 ˇ
2896 )
2897 ˇ
2898 ˇ)
2899 );
2900 "});
2901
2902 // handle auto-indent when there are multiple cursors on the same line
2903 cx.set_state(indoc! {"
2904 const a: B = (
2905 c(
2906 ˇ ˇ
2907 ˇ )
2908 );
2909 "});
2910 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 const a: B = (
2913 c(
2914 ˇ
2915 ˇ)
2916 );
2917 "});
2918}
2919
2920#[gpui::test]
2921async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
2922 init_test(cx, |settings| {
2923 settings.defaults.tab_size = NonZeroU32::new(3)
2924 });
2925
2926 let mut cx = EditorTestContext::new(cx).await;
2927 cx.set_state(indoc! {"
2928 ˇ
2929 \t ˇ
2930 \t ˇ
2931 \t ˇ
2932 \t \t\t \t \t\t \t\t \t \t ˇ
2933 "});
2934
2935 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2936 cx.assert_editor_state(indoc! {"
2937 ˇ
2938 \t ˇ
2939 \t ˇ
2940 \t ˇ
2941 \t \t\t \t \t\t \t\t \t \t ˇ
2942 "});
2943}
2944
2945#[gpui::test]
2946async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
2947 init_test(cx, |settings| {
2948 settings.defaults.tab_size = NonZeroU32::new(4)
2949 });
2950
2951 let language = Arc::new(
2952 Language::new(
2953 LanguageConfig::default(),
2954 Some(tree_sitter_rust::LANGUAGE.into()),
2955 )
2956 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2957 .unwrap(),
2958 );
2959
2960 let mut cx = EditorTestContext::new(cx).await;
2961 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2962 cx.set_state(indoc! {"
2963 fn a() {
2964 if b {
2965 \t ˇc
2966 }
2967 }
2968 "});
2969
2970 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 fn a() {
2973 if b {
2974 ˇc
2975 }
2976 }
2977 "});
2978}
2979
2980#[gpui::test]
2981async fn test_indent_outdent(cx: &mut TestAppContext) {
2982 init_test(cx, |settings| {
2983 settings.defaults.tab_size = NonZeroU32::new(4);
2984 });
2985
2986 let mut cx = EditorTestContext::new(cx).await;
2987
2988 cx.set_state(indoc! {"
2989 «oneˇ» «twoˇ»
2990 three
2991 four
2992 "});
2993 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 «oneˇ» «twoˇ»
2996 three
2997 four
2998 "});
2999
3000 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3001 cx.assert_editor_state(indoc! {"
3002 «oneˇ» «twoˇ»
3003 three
3004 four
3005 "});
3006
3007 // select across line ending
3008 cx.set_state(indoc! {"
3009 one two
3010 t«hree
3011 ˇ» four
3012 "});
3013 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3014 cx.assert_editor_state(indoc! {"
3015 one two
3016 t«hree
3017 ˇ» four
3018 "});
3019
3020 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 one two
3023 t«hree
3024 ˇ» four
3025 "});
3026
3027 // Ensure that indenting/outdenting works when the cursor is at column 0.
3028 cx.set_state(indoc! {"
3029 one two
3030 ˇthree
3031 four
3032 "});
3033 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3034 cx.assert_editor_state(indoc! {"
3035 one two
3036 ˇthree
3037 four
3038 "});
3039
3040 cx.set_state(indoc! {"
3041 one two
3042 ˇ three
3043 four
3044 "});
3045 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3046 cx.assert_editor_state(indoc! {"
3047 one two
3048 ˇthree
3049 four
3050 "});
3051}
3052
3053#[gpui::test]
3054async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3055 init_test(cx, |settings| {
3056 settings.defaults.hard_tabs = Some(true);
3057 });
3058
3059 let mut cx = EditorTestContext::new(cx).await;
3060
3061 // select two ranges on one line
3062 cx.set_state(indoc! {"
3063 «oneˇ» «twoˇ»
3064 three
3065 four
3066 "});
3067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 \t«oneˇ» «twoˇ»
3070 three
3071 four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 \t\t«oneˇ» «twoˇ»
3076 three
3077 four
3078 "});
3079 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 \t«oneˇ» «twoˇ»
3082 three
3083 four
3084 "});
3085 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3086 cx.assert_editor_state(indoc! {"
3087 «oneˇ» «twoˇ»
3088 three
3089 four
3090 "});
3091
3092 // select across a line ending
3093 cx.set_state(indoc! {"
3094 one two
3095 t«hree
3096 ˇ»four
3097 "});
3098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3099 cx.assert_editor_state(indoc! {"
3100 one two
3101 \tt«hree
3102 ˇ»four
3103 "});
3104 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 \t\tt«hree
3108 ˇ»four
3109 "});
3110 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 \tt«hree
3114 ˇ»four
3115 "});
3116 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 one two
3119 t«hree
3120 ˇ»four
3121 "});
3122
3123 // Ensure that indenting/outdenting works when the cursor is at column 0.
3124 cx.set_state(indoc! {"
3125 one two
3126 ˇthree
3127 four
3128 "});
3129 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3130 cx.assert_editor_state(indoc! {"
3131 one two
3132 ˇthree
3133 four
3134 "});
3135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 one two
3138 \tˇthree
3139 four
3140 "});
3141 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3142 cx.assert_editor_state(indoc! {"
3143 one two
3144 ˇthree
3145 four
3146 "});
3147}
3148
3149#[gpui::test]
3150fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3151 init_test(cx, |settings| {
3152 settings.languages.extend([
3153 (
3154 "TOML".into(),
3155 LanguageSettingsContent {
3156 tab_size: NonZeroU32::new(2),
3157 ..Default::default()
3158 },
3159 ),
3160 (
3161 "Rust".into(),
3162 LanguageSettingsContent {
3163 tab_size: NonZeroU32::new(4),
3164 ..Default::default()
3165 },
3166 ),
3167 ]);
3168 });
3169
3170 let toml_language = Arc::new(Language::new(
3171 LanguageConfig {
3172 name: "TOML".into(),
3173 ..Default::default()
3174 },
3175 None,
3176 ));
3177 let rust_language = Arc::new(Language::new(
3178 LanguageConfig {
3179 name: "Rust".into(),
3180 ..Default::default()
3181 },
3182 None,
3183 ));
3184
3185 let toml_buffer =
3186 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3187 let rust_buffer =
3188 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3189 let multibuffer = cx.new(|cx| {
3190 let mut multibuffer = MultiBuffer::new(ReadWrite);
3191 multibuffer.push_excerpts(
3192 toml_buffer.clone(),
3193 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3194 cx,
3195 );
3196 multibuffer.push_excerpts(
3197 rust_buffer.clone(),
3198 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3199 cx,
3200 );
3201 multibuffer
3202 });
3203
3204 cx.add_window(|window, cx| {
3205 let mut editor = build_editor(multibuffer, window, cx);
3206
3207 assert_eq!(
3208 editor.text(cx),
3209 indoc! {"
3210 a = 1
3211 b = 2
3212
3213 const c: usize = 3;
3214 "}
3215 );
3216
3217 select_ranges(
3218 &mut editor,
3219 indoc! {"
3220 «aˇ» = 1
3221 b = 2
3222
3223 «const c:ˇ» usize = 3;
3224 "},
3225 window,
3226 cx,
3227 );
3228
3229 editor.tab(&Tab, window, cx);
3230 assert_text_with_selections(
3231 &mut editor,
3232 indoc! {"
3233 «aˇ» = 1
3234 b = 2
3235
3236 «const c:ˇ» usize = 3;
3237 "},
3238 cx,
3239 );
3240 editor.backtab(&Backtab, window, cx);
3241 assert_text_with_selections(
3242 &mut editor,
3243 indoc! {"
3244 «aˇ» = 1
3245 b = 2
3246
3247 «const c:ˇ» usize = 3;
3248 "},
3249 cx,
3250 );
3251
3252 editor
3253 });
3254}
3255
3256#[gpui::test]
3257async fn test_backspace(cx: &mut TestAppContext) {
3258 init_test(cx, |_| {});
3259
3260 let mut cx = EditorTestContext::new(cx).await;
3261
3262 // Basic backspace
3263 cx.set_state(indoc! {"
3264 onˇe two three
3265 fou«rˇ» five six
3266 seven «ˇeight nine
3267 »ten
3268 "});
3269 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3270 cx.assert_editor_state(indoc! {"
3271 oˇe two three
3272 fouˇ five six
3273 seven ˇten
3274 "});
3275
3276 // Test backspace inside and around indents
3277 cx.set_state(indoc! {"
3278 zero
3279 ˇone
3280 ˇtwo
3281 ˇ ˇ ˇ three
3282 ˇ ˇ four
3283 "});
3284 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3285 cx.assert_editor_state(indoc! {"
3286 zero
3287 ˇone
3288 ˇtwo
3289 ˇ threeˇ four
3290 "});
3291}
3292
3293#[gpui::test]
3294async fn test_delete(cx: &mut TestAppContext) {
3295 init_test(cx, |_| {});
3296
3297 let mut cx = EditorTestContext::new(cx).await;
3298 cx.set_state(indoc! {"
3299 onˇe two three
3300 fou«rˇ» five six
3301 seven «ˇeight nine
3302 »ten
3303 "});
3304 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3305 cx.assert_editor_state(indoc! {"
3306 onˇ two three
3307 fouˇ five six
3308 seven ˇten
3309 "});
3310}
3311
3312#[gpui::test]
3313fn test_delete_line(cx: &mut TestAppContext) {
3314 init_test(cx, |_| {});
3315
3316 let editor = cx.add_window(|window, cx| {
3317 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3318 build_editor(buffer, window, cx)
3319 });
3320 _ = editor.update(cx, |editor, window, cx| {
3321 editor.change_selections(None, window, cx, |s| {
3322 s.select_display_ranges([
3323 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3324 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3325 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3326 ])
3327 });
3328 editor.delete_line(&DeleteLine, window, cx);
3329 assert_eq!(editor.display_text(cx), "ghi");
3330 assert_eq!(
3331 editor.selections.display_ranges(cx),
3332 vec![
3333 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3334 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3335 ]
3336 );
3337 });
3338
3339 let editor = cx.add_window(|window, cx| {
3340 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3341 build_editor(buffer, window, cx)
3342 });
3343 _ = editor.update(cx, |editor, window, cx| {
3344 editor.change_selections(None, window, cx, |s| {
3345 s.select_display_ranges([
3346 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3347 ])
3348 });
3349 editor.delete_line(&DeleteLine, window, cx);
3350 assert_eq!(editor.display_text(cx), "ghi\n");
3351 assert_eq!(
3352 editor.selections.display_ranges(cx),
3353 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3354 );
3355 });
3356}
3357
3358#[gpui::test]
3359fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3360 init_test(cx, |_| {});
3361
3362 cx.add_window(|window, cx| {
3363 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3364 let mut editor = build_editor(buffer.clone(), window, cx);
3365 let buffer = buffer.read(cx).as_singleton().unwrap();
3366
3367 assert_eq!(
3368 editor.selections.ranges::<Point>(cx),
3369 &[Point::new(0, 0)..Point::new(0, 0)]
3370 );
3371
3372 // When on single line, replace newline at end by space
3373 editor.join_lines(&JoinLines, window, cx);
3374 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3375 assert_eq!(
3376 editor.selections.ranges::<Point>(cx),
3377 &[Point::new(0, 3)..Point::new(0, 3)]
3378 );
3379
3380 // When multiple lines are selected, remove newlines that are spanned by the selection
3381 editor.change_selections(None, window, cx, |s| {
3382 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3383 });
3384 editor.join_lines(&JoinLines, window, cx);
3385 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3386 assert_eq!(
3387 editor.selections.ranges::<Point>(cx),
3388 &[Point::new(0, 11)..Point::new(0, 11)]
3389 );
3390
3391 // Undo should be transactional
3392 editor.undo(&Undo, window, cx);
3393 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3394 assert_eq!(
3395 editor.selections.ranges::<Point>(cx),
3396 &[Point::new(0, 5)..Point::new(2, 2)]
3397 );
3398
3399 // When joining an empty line don't insert a space
3400 editor.change_selections(None, window, cx, |s| {
3401 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3402 });
3403 editor.join_lines(&JoinLines, window, cx);
3404 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3405 assert_eq!(
3406 editor.selections.ranges::<Point>(cx),
3407 [Point::new(2, 3)..Point::new(2, 3)]
3408 );
3409
3410 // We can remove trailing newlines
3411 editor.join_lines(&JoinLines, window, cx);
3412 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3413 assert_eq!(
3414 editor.selections.ranges::<Point>(cx),
3415 [Point::new(2, 3)..Point::new(2, 3)]
3416 );
3417
3418 // We don't blow up on the last line
3419 editor.join_lines(&JoinLines, window, cx);
3420 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3421 assert_eq!(
3422 editor.selections.ranges::<Point>(cx),
3423 [Point::new(2, 3)..Point::new(2, 3)]
3424 );
3425
3426 // reset to test indentation
3427 editor.buffer.update(cx, |buffer, cx| {
3428 buffer.edit(
3429 [
3430 (Point::new(1, 0)..Point::new(1, 2), " "),
3431 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3432 ],
3433 None,
3434 cx,
3435 )
3436 });
3437
3438 // We remove any leading spaces
3439 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3440 editor.change_selections(None, window, cx, |s| {
3441 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3442 });
3443 editor.join_lines(&JoinLines, window, cx);
3444 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3445
3446 // We don't insert a space for a line containing only spaces
3447 editor.join_lines(&JoinLines, window, cx);
3448 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3449
3450 // We ignore any leading tabs
3451 editor.join_lines(&JoinLines, window, cx);
3452 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3453
3454 editor
3455 });
3456}
3457
3458#[gpui::test]
3459fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3460 init_test(cx, |_| {});
3461
3462 cx.add_window(|window, cx| {
3463 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3464 let mut editor = build_editor(buffer.clone(), window, cx);
3465 let buffer = buffer.read(cx).as_singleton().unwrap();
3466
3467 editor.change_selections(None, window, cx, |s| {
3468 s.select_ranges([
3469 Point::new(0, 2)..Point::new(1, 1),
3470 Point::new(1, 2)..Point::new(1, 2),
3471 Point::new(3, 1)..Point::new(3, 2),
3472 ])
3473 });
3474
3475 editor.join_lines(&JoinLines, window, cx);
3476 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3477
3478 assert_eq!(
3479 editor.selections.ranges::<Point>(cx),
3480 [
3481 Point::new(0, 7)..Point::new(0, 7),
3482 Point::new(1, 3)..Point::new(1, 3)
3483 ]
3484 );
3485 editor
3486 });
3487}
3488
3489#[gpui::test]
3490async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3491 init_test(cx, |_| {});
3492
3493 let mut cx = EditorTestContext::new(cx).await;
3494
3495 let diff_base = r#"
3496 Line 0
3497 Line 1
3498 Line 2
3499 Line 3
3500 "#
3501 .unindent();
3502
3503 cx.set_state(
3504 &r#"
3505 ˇLine 0
3506 Line 1
3507 Line 2
3508 Line 3
3509 "#
3510 .unindent(),
3511 );
3512
3513 cx.set_head_text(&diff_base);
3514 executor.run_until_parked();
3515
3516 // Join lines
3517 cx.update_editor(|editor, window, cx| {
3518 editor.join_lines(&JoinLines, window, cx);
3519 });
3520 executor.run_until_parked();
3521
3522 cx.assert_editor_state(
3523 &r#"
3524 Line 0ˇ Line 1
3525 Line 2
3526 Line 3
3527 "#
3528 .unindent(),
3529 );
3530 // Join again
3531 cx.update_editor(|editor, window, cx| {
3532 editor.join_lines(&JoinLines, window, cx);
3533 });
3534 executor.run_until_parked();
3535
3536 cx.assert_editor_state(
3537 &r#"
3538 Line 0 Line 1ˇ Line 2
3539 Line 3
3540 "#
3541 .unindent(),
3542 );
3543}
3544
3545#[gpui::test]
3546async fn test_custom_newlines_cause_no_false_positive_diffs(
3547 executor: BackgroundExecutor,
3548 cx: &mut TestAppContext,
3549) {
3550 init_test(cx, |_| {});
3551 let mut cx = EditorTestContext::new(cx).await;
3552 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3553 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3554 executor.run_until_parked();
3555
3556 cx.update_editor(|editor, window, cx| {
3557 let snapshot = editor.snapshot(window, cx);
3558 assert_eq!(
3559 snapshot
3560 .buffer_snapshot
3561 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3562 .collect::<Vec<_>>(),
3563 Vec::new(),
3564 "Should not have any diffs for files with custom newlines"
3565 );
3566 });
3567}
3568
3569#[gpui::test]
3570async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3571 init_test(cx, |_| {});
3572
3573 let mut cx = EditorTestContext::new(cx).await;
3574
3575 // Test sort_lines_case_insensitive()
3576 cx.set_state(indoc! {"
3577 «z
3578 y
3579 x
3580 Z
3581 Y
3582 Xˇ»
3583 "});
3584 cx.update_editor(|e, window, cx| {
3585 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3586 });
3587 cx.assert_editor_state(indoc! {"
3588 «x
3589 X
3590 y
3591 Y
3592 z
3593 Zˇ»
3594 "});
3595
3596 // Test reverse_lines()
3597 cx.set_state(indoc! {"
3598 «5
3599 4
3600 3
3601 2
3602 1ˇ»
3603 "});
3604 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3605 cx.assert_editor_state(indoc! {"
3606 «1
3607 2
3608 3
3609 4
3610 5ˇ»
3611 "});
3612
3613 // Skip testing shuffle_line()
3614
3615 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3616 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3617
3618 // Don't manipulate when cursor is on single line, but expand the selection
3619 cx.set_state(indoc! {"
3620 ddˇdd
3621 ccc
3622 bb
3623 a
3624 "});
3625 cx.update_editor(|e, window, cx| {
3626 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3627 });
3628 cx.assert_editor_state(indoc! {"
3629 «ddddˇ»
3630 ccc
3631 bb
3632 a
3633 "});
3634
3635 // Basic manipulate case
3636 // Start selection moves to column 0
3637 // End of selection shrinks to fit shorter line
3638 cx.set_state(indoc! {"
3639 dd«d
3640 ccc
3641 bb
3642 aaaaaˇ»
3643 "});
3644 cx.update_editor(|e, window, cx| {
3645 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3646 });
3647 cx.assert_editor_state(indoc! {"
3648 «aaaaa
3649 bb
3650 ccc
3651 dddˇ»
3652 "});
3653
3654 // Manipulate case with newlines
3655 cx.set_state(indoc! {"
3656 dd«d
3657 ccc
3658
3659 bb
3660 aaaaa
3661
3662 ˇ»
3663 "});
3664 cx.update_editor(|e, window, cx| {
3665 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3666 });
3667 cx.assert_editor_state(indoc! {"
3668 «
3669
3670 aaaaa
3671 bb
3672 ccc
3673 dddˇ»
3674
3675 "});
3676
3677 // Adding new line
3678 cx.set_state(indoc! {"
3679 aa«a
3680 bbˇ»b
3681 "});
3682 cx.update_editor(|e, window, cx| {
3683 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3684 });
3685 cx.assert_editor_state(indoc! {"
3686 «aaa
3687 bbb
3688 added_lineˇ»
3689 "});
3690
3691 // Removing line
3692 cx.set_state(indoc! {"
3693 aa«a
3694 bbbˇ»
3695 "});
3696 cx.update_editor(|e, window, cx| {
3697 e.manipulate_lines(window, cx, |lines| {
3698 lines.pop();
3699 })
3700 });
3701 cx.assert_editor_state(indoc! {"
3702 «aaaˇ»
3703 "});
3704
3705 // Removing all lines
3706 cx.set_state(indoc! {"
3707 aa«a
3708 bbbˇ»
3709 "});
3710 cx.update_editor(|e, window, cx| {
3711 e.manipulate_lines(window, cx, |lines| {
3712 lines.drain(..);
3713 })
3714 });
3715 cx.assert_editor_state(indoc! {"
3716 ˇ
3717 "});
3718}
3719
3720#[gpui::test]
3721async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3722 init_test(cx, |_| {});
3723
3724 let mut cx = EditorTestContext::new(cx).await;
3725
3726 // Consider continuous selection as single selection
3727 cx.set_state(indoc! {"
3728 Aaa«aa
3729 cˇ»c«c
3730 bb
3731 aaaˇ»aa
3732 "});
3733 cx.update_editor(|e, window, cx| {
3734 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3735 });
3736 cx.assert_editor_state(indoc! {"
3737 «Aaaaa
3738 ccc
3739 bb
3740 aaaaaˇ»
3741 "});
3742
3743 cx.set_state(indoc! {"
3744 Aaa«aa
3745 cˇ»c«c
3746 bb
3747 aaaˇ»aa
3748 "});
3749 cx.update_editor(|e, window, cx| {
3750 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3751 });
3752 cx.assert_editor_state(indoc! {"
3753 «Aaaaa
3754 ccc
3755 bbˇ»
3756 "});
3757
3758 // Consider non continuous selection as distinct dedup operations
3759 cx.set_state(indoc! {"
3760 «aaaaa
3761 bb
3762 aaaaa
3763 aaaaaˇ»
3764
3765 aaa«aaˇ»
3766 "});
3767 cx.update_editor(|e, window, cx| {
3768 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3769 });
3770 cx.assert_editor_state(indoc! {"
3771 «aaaaa
3772 bbˇ»
3773
3774 «aaaaaˇ»
3775 "});
3776}
3777
3778#[gpui::test]
3779async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3780 init_test(cx, |_| {});
3781
3782 let mut cx = EditorTestContext::new(cx).await;
3783
3784 cx.set_state(indoc! {"
3785 «Aaa
3786 aAa
3787 Aaaˇ»
3788 "});
3789 cx.update_editor(|e, window, cx| {
3790 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3791 });
3792 cx.assert_editor_state(indoc! {"
3793 «Aaa
3794 aAaˇ»
3795 "});
3796
3797 cx.set_state(indoc! {"
3798 «Aaa
3799 aAa
3800 aaAˇ»
3801 "});
3802 cx.update_editor(|e, window, cx| {
3803 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3804 });
3805 cx.assert_editor_state(indoc! {"
3806 «Aaaˇ»
3807 "});
3808}
3809
3810#[gpui::test]
3811async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3812 init_test(cx, |_| {});
3813
3814 let mut cx = EditorTestContext::new(cx).await;
3815
3816 // Manipulate with multiple selections on a single line
3817 cx.set_state(indoc! {"
3818 dd«dd
3819 cˇ»c«c
3820 bb
3821 aaaˇ»aa
3822 "});
3823 cx.update_editor(|e, window, cx| {
3824 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3825 });
3826 cx.assert_editor_state(indoc! {"
3827 «aaaaa
3828 bb
3829 ccc
3830 ddddˇ»
3831 "});
3832
3833 // Manipulate with multiple disjoin selections
3834 cx.set_state(indoc! {"
3835 5«
3836 4
3837 3
3838 2
3839 1ˇ»
3840
3841 dd«dd
3842 ccc
3843 bb
3844 aaaˇ»aa
3845 "});
3846 cx.update_editor(|e, window, cx| {
3847 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3848 });
3849 cx.assert_editor_state(indoc! {"
3850 «1
3851 2
3852 3
3853 4
3854 5ˇ»
3855
3856 «aaaaa
3857 bb
3858 ccc
3859 ddddˇ»
3860 "});
3861
3862 // Adding lines on each selection
3863 cx.set_state(indoc! {"
3864 2«
3865 1ˇ»
3866
3867 bb«bb
3868 aaaˇ»aa
3869 "});
3870 cx.update_editor(|e, window, cx| {
3871 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3872 });
3873 cx.assert_editor_state(indoc! {"
3874 «2
3875 1
3876 added lineˇ»
3877
3878 «bbbb
3879 aaaaa
3880 added lineˇ»
3881 "});
3882
3883 // Removing lines on each selection
3884 cx.set_state(indoc! {"
3885 2«
3886 1ˇ»
3887
3888 bb«bb
3889 aaaˇ»aa
3890 "});
3891 cx.update_editor(|e, window, cx| {
3892 e.manipulate_lines(window, cx, |lines| {
3893 lines.pop();
3894 })
3895 });
3896 cx.assert_editor_state(indoc! {"
3897 «2ˇ»
3898
3899 «bbbbˇ»
3900 "});
3901}
3902
3903#[gpui::test]
3904async fn test_toggle_case(cx: &mut TestAppContext) {
3905 init_test(cx, |_| {});
3906
3907 let mut cx = EditorTestContext::new(cx).await;
3908
3909 // If all lower case -> upper case
3910 cx.set_state(indoc! {"
3911 «hello worldˇ»
3912 "});
3913 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3914 cx.assert_editor_state(indoc! {"
3915 «HELLO WORLDˇ»
3916 "});
3917
3918 // If all upper case -> lower case
3919 cx.set_state(indoc! {"
3920 «HELLO WORLDˇ»
3921 "});
3922 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3923 cx.assert_editor_state(indoc! {"
3924 «hello worldˇ»
3925 "});
3926
3927 // If any upper case characters are identified -> lower case
3928 // This matches JetBrains IDEs
3929 cx.set_state(indoc! {"
3930 «hEllo worldˇ»
3931 "});
3932 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3933 cx.assert_editor_state(indoc! {"
3934 «hello worldˇ»
3935 "});
3936}
3937
3938#[gpui::test]
3939async fn test_manipulate_text(cx: &mut TestAppContext) {
3940 init_test(cx, |_| {});
3941
3942 let mut cx = EditorTestContext::new(cx).await;
3943
3944 // Test convert_to_upper_case()
3945 cx.set_state(indoc! {"
3946 «hello worldˇ»
3947 "});
3948 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 «HELLO WORLDˇ»
3951 "});
3952
3953 // Test convert_to_lower_case()
3954 cx.set_state(indoc! {"
3955 «HELLO WORLDˇ»
3956 "});
3957 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3958 cx.assert_editor_state(indoc! {"
3959 «hello worldˇ»
3960 "});
3961
3962 // Test multiple line, single selection case
3963 cx.set_state(indoc! {"
3964 «The quick brown
3965 fox jumps over
3966 the lazy dogˇ»
3967 "});
3968 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3969 cx.assert_editor_state(indoc! {"
3970 «The Quick Brown
3971 Fox Jumps Over
3972 The Lazy Dogˇ»
3973 "});
3974
3975 // Test multiple line, single selection case
3976 cx.set_state(indoc! {"
3977 «The quick brown
3978 fox jumps over
3979 the lazy dogˇ»
3980 "});
3981 cx.update_editor(|e, window, cx| {
3982 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3983 });
3984 cx.assert_editor_state(indoc! {"
3985 «TheQuickBrown
3986 FoxJumpsOver
3987 TheLazyDogˇ»
3988 "});
3989
3990 // From here on out, test more complex cases of manipulate_text()
3991
3992 // Test no selection case - should affect words cursors are in
3993 // Cursor at beginning, middle, and end of word
3994 cx.set_state(indoc! {"
3995 ˇhello big beauˇtiful worldˇ
3996 "});
3997 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3998 cx.assert_editor_state(indoc! {"
3999 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4000 "});
4001
4002 // Test multiple selections on a single line and across multiple lines
4003 cx.set_state(indoc! {"
4004 «Theˇ» quick «brown
4005 foxˇ» jumps «overˇ»
4006 the «lazyˇ» dog
4007 "});
4008 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4009 cx.assert_editor_state(indoc! {"
4010 «THEˇ» quick «BROWN
4011 FOXˇ» jumps «OVERˇ»
4012 the «LAZYˇ» dog
4013 "});
4014
4015 // Test case where text length grows
4016 cx.set_state(indoc! {"
4017 «tschüߡ»
4018 "});
4019 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4020 cx.assert_editor_state(indoc! {"
4021 «TSCHÜSSˇ»
4022 "});
4023
4024 // Test to make sure we don't crash when text shrinks
4025 cx.set_state(indoc! {"
4026 aaa_bbbˇ
4027 "});
4028 cx.update_editor(|e, window, cx| {
4029 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4030 });
4031 cx.assert_editor_state(indoc! {"
4032 «aaaBbbˇ»
4033 "});
4034
4035 // Test to make sure we all aware of the fact that each word can grow and shrink
4036 // Final selections should be aware of this fact
4037 cx.set_state(indoc! {"
4038 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4039 "});
4040 cx.update_editor(|e, window, cx| {
4041 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4042 });
4043 cx.assert_editor_state(indoc! {"
4044 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4045 "});
4046
4047 cx.set_state(indoc! {"
4048 «hElLo, WoRld!ˇ»
4049 "});
4050 cx.update_editor(|e, window, cx| {
4051 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4052 });
4053 cx.assert_editor_state(indoc! {"
4054 «HeLlO, wOrLD!ˇ»
4055 "});
4056}
4057
4058#[gpui::test]
4059fn test_duplicate_line(cx: &mut TestAppContext) {
4060 init_test(cx, |_| {});
4061
4062 let editor = cx.add_window(|window, cx| {
4063 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4064 build_editor(buffer, window, cx)
4065 });
4066 _ = editor.update(cx, |editor, window, cx| {
4067 editor.change_selections(None, window, cx, |s| {
4068 s.select_display_ranges([
4069 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4070 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4071 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4072 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4073 ])
4074 });
4075 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4076 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4077 assert_eq!(
4078 editor.selections.display_ranges(cx),
4079 vec![
4080 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4081 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4082 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4084 ]
4085 );
4086 });
4087
4088 let editor = cx.add_window(|window, cx| {
4089 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4090 build_editor(buffer, window, cx)
4091 });
4092 _ = editor.update(cx, |editor, window, cx| {
4093 editor.change_selections(None, window, cx, |s| {
4094 s.select_display_ranges([
4095 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4096 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4097 ])
4098 });
4099 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4100 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4101 assert_eq!(
4102 editor.selections.display_ranges(cx),
4103 vec![
4104 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4105 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4106 ]
4107 );
4108 });
4109
4110 // With `move_upwards` the selections stay in place, except for
4111 // the lines inserted above them
4112 let editor = cx.add_window(|window, cx| {
4113 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4114 build_editor(buffer, window, cx)
4115 });
4116 _ = editor.update(cx, |editor, window, cx| {
4117 editor.change_selections(None, window, cx, |s| {
4118 s.select_display_ranges([
4119 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4120 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4121 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4122 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4123 ])
4124 });
4125 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4126 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4127 assert_eq!(
4128 editor.selections.display_ranges(cx),
4129 vec![
4130 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4131 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4132 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4133 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4134 ]
4135 );
4136 });
4137
4138 let editor = cx.add_window(|window, cx| {
4139 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4140 build_editor(buffer, window, cx)
4141 });
4142 _ = editor.update(cx, |editor, window, cx| {
4143 editor.change_selections(None, window, cx, |s| {
4144 s.select_display_ranges([
4145 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4146 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4147 ])
4148 });
4149 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4150 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4151 assert_eq!(
4152 editor.selections.display_ranges(cx),
4153 vec![
4154 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4155 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4156 ]
4157 );
4158 });
4159
4160 let editor = cx.add_window(|window, cx| {
4161 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4162 build_editor(buffer, window, cx)
4163 });
4164 _ = editor.update(cx, |editor, window, cx| {
4165 editor.change_selections(None, window, cx, |s| {
4166 s.select_display_ranges([
4167 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4168 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4169 ])
4170 });
4171 editor.duplicate_selection(&DuplicateSelection, window, cx);
4172 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4173 assert_eq!(
4174 editor.selections.display_ranges(cx),
4175 vec![
4176 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4177 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4178 ]
4179 );
4180 });
4181}
4182
4183#[gpui::test]
4184fn test_move_line_up_down(cx: &mut TestAppContext) {
4185 init_test(cx, |_| {});
4186
4187 let editor = cx.add_window(|window, cx| {
4188 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4189 build_editor(buffer, window, cx)
4190 });
4191 _ = editor.update(cx, |editor, window, cx| {
4192 editor.fold_creases(
4193 vec![
4194 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4195 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4196 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4197 ],
4198 true,
4199 window,
4200 cx,
4201 );
4202 editor.change_selections(None, window, cx, |s| {
4203 s.select_display_ranges([
4204 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4205 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4206 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4207 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4208 ])
4209 });
4210 assert_eq!(
4211 editor.display_text(cx),
4212 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4213 );
4214
4215 editor.move_line_up(&MoveLineUp, window, cx);
4216 assert_eq!(
4217 editor.display_text(cx),
4218 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4219 );
4220 assert_eq!(
4221 editor.selections.display_ranges(cx),
4222 vec![
4223 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4224 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4225 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4226 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4227 ]
4228 );
4229 });
4230
4231 _ = editor.update(cx, |editor, window, cx| {
4232 editor.move_line_down(&MoveLineDown, window, cx);
4233 assert_eq!(
4234 editor.display_text(cx),
4235 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4236 );
4237 assert_eq!(
4238 editor.selections.display_ranges(cx),
4239 vec![
4240 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4241 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4242 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4243 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4244 ]
4245 );
4246 });
4247
4248 _ = editor.update(cx, |editor, window, cx| {
4249 editor.move_line_down(&MoveLineDown, window, cx);
4250 assert_eq!(
4251 editor.display_text(cx),
4252 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4253 );
4254 assert_eq!(
4255 editor.selections.display_ranges(cx),
4256 vec![
4257 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4258 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4259 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4260 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4261 ]
4262 );
4263 });
4264
4265 _ = editor.update(cx, |editor, window, cx| {
4266 editor.move_line_up(&MoveLineUp, window, cx);
4267 assert_eq!(
4268 editor.display_text(cx),
4269 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4270 );
4271 assert_eq!(
4272 editor.selections.display_ranges(cx),
4273 vec![
4274 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4275 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4276 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4277 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4278 ]
4279 );
4280 });
4281}
4282
4283#[gpui::test]
4284fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4285 init_test(cx, |_| {});
4286
4287 let editor = cx.add_window(|window, cx| {
4288 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4289 build_editor(buffer, window, cx)
4290 });
4291 _ = editor.update(cx, |editor, window, cx| {
4292 let snapshot = editor.buffer.read(cx).snapshot(cx);
4293 editor.insert_blocks(
4294 [BlockProperties {
4295 style: BlockStyle::Fixed,
4296 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4297 height: Some(1),
4298 render: Arc::new(|_| div().into_any()),
4299 priority: 0,
4300 }],
4301 Some(Autoscroll::fit()),
4302 cx,
4303 );
4304 editor.change_selections(None, window, cx, |s| {
4305 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4306 });
4307 editor.move_line_down(&MoveLineDown, window, cx);
4308 });
4309}
4310
4311#[gpui::test]
4312async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4313 init_test(cx, |_| {});
4314
4315 let mut cx = EditorTestContext::new(cx).await;
4316 cx.set_state(
4317 &"
4318 ˇzero
4319 one
4320 two
4321 three
4322 four
4323 five
4324 "
4325 .unindent(),
4326 );
4327
4328 // Create a four-line block that replaces three lines of text.
4329 cx.update_editor(|editor, window, cx| {
4330 let snapshot = editor.snapshot(window, cx);
4331 let snapshot = &snapshot.buffer_snapshot;
4332 let placement = BlockPlacement::Replace(
4333 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4334 );
4335 editor.insert_blocks(
4336 [BlockProperties {
4337 placement,
4338 height: Some(4),
4339 style: BlockStyle::Sticky,
4340 render: Arc::new(|_| gpui::div().into_any_element()),
4341 priority: 0,
4342 }],
4343 None,
4344 cx,
4345 );
4346 });
4347
4348 // Move down so that the cursor touches the block.
4349 cx.update_editor(|editor, window, cx| {
4350 editor.move_down(&Default::default(), window, cx);
4351 });
4352 cx.assert_editor_state(
4353 &"
4354 zero
4355 «one
4356 two
4357 threeˇ»
4358 four
4359 five
4360 "
4361 .unindent(),
4362 );
4363
4364 // Move down past the block.
4365 cx.update_editor(|editor, window, cx| {
4366 editor.move_down(&Default::default(), window, cx);
4367 });
4368 cx.assert_editor_state(
4369 &"
4370 zero
4371 one
4372 two
4373 three
4374 ˇfour
4375 five
4376 "
4377 .unindent(),
4378 );
4379}
4380
4381#[gpui::test]
4382fn test_transpose(cx: &mut TestAppContext) {
4383 init_test(cx, |_| {});
4384
4385 _ = cx.add_window(|window, cx| {
4386 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4387 editor.set_style(EditorStyle::default(), window, cx);
4388 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4389 editor.transpose(&Default::default(), window, cx);
4390 assert_eq!(editor.text(cx), "bac");
4391 assert_eq!(editor.selections.ranges(cx), [2..2]);
4392
4393 editor.transpose(&Default::default(), window, cx);
4394 assert_eq!(editor.text(cx), "bca");
4395 assert_eq!(editor.selections.ranges(cx), [3..3]);
4396
4397 editor.transpose(&Default::default(), window, cx);
4398 assert_eq!(editor.text(cx), "bac");
4399 assert_eq!(editor.selections.ranges(cx), [3..3]);
4400
4401 editor
4402 });
4403
4404 _ = cx.add_window(|window, cx| {
4405 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4406 editor.set_style(EditorStyle::default(), window, cx);
4407 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4408 editor.transpose(&Default::default(), window, cx);
4409 assert_eq!(editor.text(cx), "acb\nde");
4410 assert_eq!(editor.selections.ranges(cx), [3..3]);
4411
4412 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4413 editor.transpose(&Default::default(), window, cx);
4414 assert_eq!(editor.text(cx), "acbd\ne");
4415 assert_eq!(editor.selections.ranges(cx), [5..5]);
4416
4417 editor.transpose(&Default::default(), window, cx);
4418 assert_eq!(editor.text(cx), "acbde\n");
4419 assert_eq!(editor.selections.ranges(cx), [6..6]);
4420
4421 editor.transpose(&Default::default(), window, cx);
4422 assert_eq!(editor.text(cx), "acbd\ne");
4423 assert_eq!(editor.selections.ranges(cx), [6..6]);
4424
4425 editor
4426 });
4427
4428 _ = cx.add_window(|window, cx| {
4429 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4430 editor.set_style(EditorStyle::default(), window, cx);
4431 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4432 editor.transpose(&Default::default(), window, cx);
4433 assert_eq!(editor.text(cx), "bacd\ne");
4434 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4435
4436 editor.transpose(&Default::default(), window, cx);
4437 assert_eq!(editor.text(cx), "bcade\n");
4438 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4439
4440 editor.transpose(&Default::default(), window, cx);
4441 assert_eq!(editor.text(cx), "bcda\ne");
4442 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4443
4444 editor.transpose(&Default::default(), window, cx);
4445 assert_eq!(editor.text(cx), "bcade\n");
4446 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4447
4448 editor.transpose(&Default::default(), window, cx);
4449 assert_eq!(editor.text(cx), "bcaed\n");
4450 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4451
4452 editor
4453 });
4454
4455 _ = cx.add_window(|window, cx| {
4456 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4457 editor.set_style(EditorStyle::default(), window, cx);
4458 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4459 editor.transpose(&Default::default(), window, cx);
4460 assert_eq!(editor.text(cx), "🏀🍐✋");
4461 assert_eq!(editor.selections.ranges(cx), [8..8]);
4462
4463 editor.transpose(&Default::default(), window, cx);
4464 assert_eq!(editor.text(cx), "🏀✋🍐");
4465 assert_eq!(editor.selections.ranges(cx), [11..11]);
4466
4467 editor.transpose(&Default::default(), window, cx);
4468 assert_eq!(editor.text(cx), "🏀🍐✋");
4469 assert_eq!(editor.selections.ranges(cx), [11..11]);
4470
4471 editor
4472 });
4473}
4474
4475#[gpui::test]
4476async fn test_rewrap(cx: &mut TestAppContext) {
4477 init_test(cx, |settings| {
4478 settings.languages.extend([
4479 (
4480 "Markdown".into(),
4481 LanguageSettingsContent {
4482 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4483 ..Default::default()
4484 },
4485 ),
4486 (
4487 "Plain Text".into(),
4488 LanguageSettingsContent {
4489 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4490 ..Default::default()
4491 },
4492 ),
4493 ])
4494 });
4495
4496 let mut cx = EditorTestContext::new(cx).await;
4497
4498 let language_with_c_comments = Arc::new(Language::new(
4499 LanguageConfig {
4500 line_comments: vec!["// ".into()],
4501 ..LanguageConfig::default()
4502 },
4503 None,
4504 ));
4505 let language_with_pound_comments = Arc::new(Language::new(
4506 LanguageConfig {
4507 line_comments: vec!["# ".into()],
4508 ..LanguageConfig::default()
4509 },
4510 None,
4511 ));
4512 let markdown_language = Arc::new(Language::new(
4513 LanguageConfig {
4514 name: "Markdown".into(),
4515 ..LanguageConfig::default()
4516 },
4517 None,
4518 ));
4519 let language_with_doc_comments = Arc::new(Language::new(
4520 LanguageConfig {
4521 line_comments: vec!["// ".into(), "/// ".into()],
4522 ..LanguageConfig::default()
4523 },
4524 Some(tree_sitter_rust::LANGUAGE.into()),
4525 ));
4526
4527 let plaintext_language = Arc::new(Language::new(
4528 LanguageConfig {
4529 name: "Plain Text".into(),
4530 ..LanguageConfig::default()
4531 },
4532 None,
4533 ));
4534
4535 assert_rewrap(
4536 indoc! {"
4537 // ˇ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.
4538 "},
4539 indoc! {"
4540 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4541 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4542 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4543 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4544 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4545 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4546 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4547 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4548 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4549 // porttitor id. Aliquam id accumsan eros.
4550 "},
4551 language_with_c_comments.clone(),
4552 &mut cx,
4553 );
4554
4555 // Test that rewrapping works inside of a selection
4556 assert_rewrap(
4557 indoc! {"
4558 «// 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.ˇ»
4559 "},
4560 indoc! {"
4561 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4562 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4563 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4564 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4565 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4566 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4567 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4568 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4569 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4570 // porttitor id. Aliquam id accumsan eros.ˇ»
4571 "},
4572 language_with_c_comments.clone(),
4573 &mut cx,
4574 );
4575
4576 // Test that cursors that expand to the same region are collapsed.
4577 assert_rewrap(
4578 indoc! {"
4579 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4580 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4581 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4582 // ˇ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.
4583 "},
4584 indoc! {"
4585 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4586 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4587 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4588 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4589 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4590 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4591 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4592 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4593 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4594 // porttitor id. Aliquam id accumsan eros.
4595 "},
4596 language_with_c_comments.clone(),
4597 &mut cx,
4598 );
4599
4600 // Test that non-contiguous selections are treated separately.
4601 assert_rewrap(
4602 indoc! {"
4603 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4604 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4605 //
4606 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4607 // ˇ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.
4608 "},
4609 indoc! {"
4610 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4611 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4612 // auctor, eu lacinia sapien scelerisque.
4613 //
4614 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4615 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4616 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4617 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4618 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4619 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4620 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4621 "},
4622 language_with_c_comments.clone(),
4623 &mut cx,
4624 );
4625
4626 // Test that different comment prefixes are supported.
4627 assert_rewrap(
4628 indoc! {"
4629 # ˇ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.
4630 "},
4631 indoc! {"
4632 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4633 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4634 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4635 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4636 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4637 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4638 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4639 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4640 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4641 # accumsan eros.
4642 "},
4643 language_with_pound_comments.clone(),
4644 &mut cx,
4645 );
4646
4647 // Test that rewrapping is ignored outside of comments in most languages.
4648 assert_rewrap(
4649 indoc! {"
4650 /// Adds two numbers.
4651 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4652 fn add(a: u32, b: u32) -> u32 {
4653 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ˇ
4654 }
4655 "},
4656 indoc! {"
4657 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4658 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4659 fn add(a: u32, b: u32) -> u32 {
4660 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ˇ
4661 }
4662 "},
4663 language_with_doc_comments.clone(),
4664 &mut cx,
4665 );
4666
4667 // Test that rewrapping works in Markdown and Plain Text languages.
4668 assert_rewrap(
4669 indoc! {"
4670 # Hello
4671
4672 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.
4673 "},
4674 indoc! {"
4675 # Hello
4676
4677 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4678 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4679 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4680 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4681 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4682 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4683 Integer sit amet scelerisque nisi.
4684 "},
4685 markdown_language,
4686 &mut cx,
4687 );
4688
4689 assert_rewrap(
4690 indoc! {"
4691 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.
4692 "},
4693 indoc! {"
4694 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4695 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4696 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4697 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4698 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4699 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4700 Integer sit amet scelerisque nisi.
4701 "},
4702 plaintext_language,
4703 &mut cx,
4704 );
4705
4706 // Test rewrapping unaligned comments in a selection.
4707 assert_rewrap(
4708 indoc! {"
4709 fn foo() {
4710 if true {
4711 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4712 // Praesent semper egestas tellus id dignissim.ˇ»
4713 do_something();
4714 } else {
4715 //
4716 }
4717 }
4718 "},
4719 indoc! {"
4720 fn foo() {
4721 if true {
4722 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4723 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4724 // egestas tellus id dignissim.ˇ»
4725 do_something();
4726 } else {
4727 //
4728 }
4729 }
4730 "},
4731 language_with_doc_comments.clone(),
4732 &mut cx,
4733 );
4734
4735 assert_rewrap(
4736 indoc! {"
4737 fn foo() {
4738 if true {
4739 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4740 // Praesent semper egestas tellus id dignissim.»
4741 do_something();
4742 } else {
4743 //
4744 }
4745
4746 }
4747 "},
4748 indoc! {"
4749 fn foo() {
4750 if true {
4751 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4752 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4753 // egestas tellus id dignissim.»
4754 do_something();
4755 } else {
4756 //
4757 }
4758
4759 }
4760 "},
4761 language_with_doc_comments.clone(),
4762 &mut cx,
4763 );
4764
4765 #[track_caller]
4766 fn assert_rewrap(
4767 unwrapped_text: &str,
4768 wrapped_text: &str,
4769 language: Arc<Language>,
4770 cx: &mut EditorTestContext,
4771 ) {
4772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4773 cx.set_state(unwrapped_text);
4774 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4775 cx.assert_editor_state(wrapped_text);
4776 }
4777}
4778
4779#[gpui::test]
4780async fn test_hard_wrap(cx: &mut TestAppContext) {
4781 init_test(cx, |_| {});
4782 let mut cx = EditorTestContext::new(cx).await;
4783
4784 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4785 cx.update_editor(|editor, _, cx| {
4786 editor.set_hard_wrap(Some(14), cx);
4787 });
4788
4789 cx.set_state(indoc!(
4790 "
4791 one two three ˇ
4792 "
4793 ));
4794 cx.simulate_input("four");
4795 cx.run_until_parked();
4796
4797 cx.assert_editor_state(indoc!(
4798 "
4799 one two three
4800 fourˇ
4801 "
4802 ));
4803
4804 cx.update_editor(|editor, window, cx| {
4805 editor.newline(&Default::default(), window, cx);
4806 });
4807 cx.run_until_parked();
4808 cx.assert_editor_state(indoc!(
4809 "
4810 one two three
4811 four
4812 ˇ
4813 "
4814 ));
4815
4816 cx.simulate_input("five");
4817 cx.run_until_parked();
4818 cx.assert_editor_state(indoc!(
4819 "
4820 one two three
4821 four
4822 fiveˇ
4823 "
4824 ));
4825
4826 cx.update_editor(|editor, window, cx| {
4827 editor.newline(&Default::default(), window, cx);
4828 });
4829 cx.run_until_parked();
4830 cx.simulate_input("# ");
4831 cx.run_until_parked();
4832 cx.assert_editor_state(indoc!(
4833 "
4834 one two three
4835 four
4836 five
4837 # ˇ
4838 "
4839 ));
4840
4841 cx.update_editor(|editor, window, cx| {
4842 editor.newline(&Default::default(), window, cx);
4843 });
4844 cx.run_until_parked();
4845 cx.assert_editor_state(indoc!(
4846 "
4847 one two three
4848 four
4849 five
4850 #\x20
4851 #ˇ
4852 "
4853 ));
4854
4855 cx.simulate_input(" 6");
4856 cx.run_until_parked();
4857 cx.assert_editor_state(indoc!(
4858 "
4859 one two three
4860 four
4861 five
4862 #
4863 # 6ˇ
4864 "
4865 ));
4866}
4867
4868#[gpui::test]
4869async fn test_clipboard(cx: &mut TestAppContext) {
4870 init_test(cx, |_| {});
4871
4872 let mut cx = EditorTestContext::new(cx).await;
4873
4874 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4875 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4876 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4877
4878 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4879 cx.set_state("two ˇfour ˇsix ˇ");
4880 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4881 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4882
4883 // Paste again but with only two cursors. Since the number of cursors doesn't
4884 // match the number of slices in the clipboard, the entire clipboard text
4885 // is pasted at each cursor.
4886 cx.set_state("ˇtwo one✅ four three six five ˇ");
4887 cx.update_editor(|e, window, cx| {
4888 e.handle_input("( ", window, cx);
4889 e.paste(&Paste, window, cx);
4890 e.handle_input(") ", window, cx);
4891 });
4892 cx.assert_editor_state(
4893 &([
4894 "( one✅ ",
4895 "three ",
4896 "five ) ˇtwo one✅ four three six five ( one✅ ",
4897 "three ",
4898 "five ) ˇ",
4899 ]
4900 .join("\n")),
4901 );
4902
4903 // Cut with three selections, one of which is full-line.
4904 cx.set_state(indoc! {"
4905 1«2ˇ»3
4906 4ˇ567
4907 «8ˇ»9"});
4908 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4909 cx.assert_editor_state(indoc! {"
4910 1ˇ3
4911 ˇ9"});
4912
4913 // Paste with three selections, noticing how the copied selection that was full-line
4914 // gets inserted before the second cursor.
4915 cx.set_state(indoc! {"
4916 1ˇ3
4917 9ˇ
4918 «oˇ»ne"});
4919 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4920 cx.assert_editor_state(indoc! {"
4921 12ˇ3
4922 4567
4923 9ˇ
4924 8ˇne"});
4925
4926 // Copy with a single cursor only, which writes the whole line into the clipboard.
4927 cx.set_state(indoc! {"
4928 The quick brown
4929 fox juˇmps over
4930 the lazy dog"});
4931 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4932 assert_eq!(
4933 cx.read_from_clipboard()
4934 .and_then(|item| item.text().as_deref().map(str::to_string)),
4935 Some("fox jumps over\n".to_string())
4936 );
4937
4938 // Paste with three selections, noticing how the copied full-line selection is inserted
4939 // before the empty selections but replaces the selection that is non-empty.
4940 cx.set_state(indoc! {"
4941 Tˇhe quick brown
4942 «foˇ»x jumps over
4943 tˇhe lazy dog"});
4944 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4945 cx.assert_editor_state(indoc! {"
4946 fox jumps over
4947 Tˇhe quick brown
4948 fox jumps over
4949 ˇx jumps over
4950 fox jumps over
4951 tˇhe lazy dog"});
4952}
4953
4954#[gpui::test]
4955async fn test_copy_trim(cx: &mut TestAppContext) {
4956 init_test(cx, |_| {});
4957
4958 let mut cx = EditorTestContext::new(cx).await;
4959 cx.set_state(
4960 r#" «for selection in selections.iter() {
4961 let mut start = selection.start;
4962 let mut end = selection.end;
4963 let is_entire_line = selection.is_empty();
4964 if is_entire_line {
4965 start = Point::new(start.row, 0);ˇ»
4966 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4967 }
4968 "#,
4969 );
4970 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4971 assert_eq!(
4972 cx.read_from_clipboard()
4973 .and_then(|item| item.text().as_deref().map(str::to_string)),
4974 Some(
4975 "for selection in selections.iter() {
4976 let mut start = selection.start;
4977 let mut end = selection.end;
4978 let is_entire_line = selection.is_empty();
4979 if is_entire_line {
4980 start = Point::new(start.row, 0);"
4981 .to_string()
4982 ),
4983 "Regular copying preserves all indentation selected",
4984 );
4985 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4986 assert_eq!(
4987 cx.read_from_clipboard()
4988 .and_then(|item| item.text().as_deref().map(str::to_string)),
4989 Some(
4990 "for selection in selections.iter() {
4991let mut start = selection.start;
4992let mut end = selection.end;
4993let is_entire_line = selection.is_empty();
4994if is_entire_line {
4995 start = Point::new(start.row, 0);"
4996 .to_string()
4997 ),
4998 "Copying with stripping should strip all leading whitespaces"
4999 );
5000
5001 cx.set_state(
5002 r#" « for selection in selections.iter() {
5003 let mut start = selection.start;
5004 let mut end = selection.end;
5005 let is_entire_line = selection.is_empty();
5006 if is_entire_line {
5007 start = Point::new(start.row, 0);ˇ»
5008 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5009 }
5010 "#,
5011 );
5012 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5013 assert_eq!(
5014 cx.read_from_clipboard()
5015 .and_then(|item| item.text().as_deref().map(str::to_string)),
5016 Some(
5017 " for selection in selections.iter() {
5018 let mut start = selection.start;
5019 let mut end = selection.end;
5020 let is_entire_line = selection.is_empty();
5021 if is_entire_line {
5022 start = Point::new(start.row, 0);"
5023 .to_string()
5024 ),
5025 "Regular copying preserves all indentation selected",
5026 );
5027 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5028 assert_eq!(
5029 cx.read_from_clipboard()
5030 .and_then(|item| item.text().as_deref().map(str::to_string)),
5031 Some(
5032 "for selection in selections.iter() {
5033let mut start = selection.start;
5034let mut end = selection.end;
5035let is_entire_line = selection.is_empty();
5036if is_entire_line {
5037 start = Point::new(start.row, 0);"
5038 .to_string()
5039 ),
5040 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5041 );
5042
5043 cx.set_state(
5044 r#" «ˇ for selection in selections.iter() {
5045 let mut start = selection.start;
5046 let mut end = selection.end;
5047 let is_entire_line = selection.is_empty();
5048 if is_entire_line {
5049 start = Point::new(start.row, 0);»
5050 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5051 }
5052 "#,
5053 );
5054 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5055 assert_eq!(
5056 cx.read_from_clipboard()
5057 .and_then(|item| item.text().as_deref().map(str::to_string)),
5058 Some(
5059 " for selection in selections.iter() {
5060 let mut start = selection.start;
5061 let mut end = selection.end;
5062 let is_entire_line = selection.is_empty();
5063 if is_entire_line {
5064 start = Point::new(start.row, 0);"
5065 .to_string()
5066 ),
5067 "Regular copying for reverse selection works the same",
5068 );
5069 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5070 assert_eq!(
5071 cx.read_from_clipboard()
5072 .and_then(|item| item.text().as_deref().map(str::to_string)),
5073 Some(
5074 "for selection in selections.iter() {
5075let mut start = selection.start;
5076let mut end = selection.end;
5077let is_entire_line = selection.is_empty();
5078if is_entire_line {
5079 start = Point::new(start.row, 0);"
5080 .to_string()
5081 ),
5082 "Copying with stripping for reverse selection works the same"
5083 );
5084
5085 cx.set_state(
5086 r#" for selection «in selections.iter() {
5087 let mut start = selection.start;
5088 let mut end = selection.end;
5089 let is_entire_line = selection.is_empty();
5090 if is_entire_line {
5091 start = Point::new(start.row, 0);ˇ»
5092 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5093 }
5094 "#,
5095 );
5096 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5097 assert_eq!(
5098 cx.read_from_clipboard()
5099 .and_then(|item| item.text().as_deref().map(str::to_string)),
5100 Some(
5101 "in selections.iter() {
5102 let mut start = selection.start;
5103 let mut end = selection.end;
5104 let is_entire_line = selection.is_empty();
5105 if is_entire_line {
5106 start = Point::new(start.row, 0);"
5107 .to_string()
5108 ),
5109 "When selecting past the indent, the copying works as usual",
5110 );
5111 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5112 assert_eq!(
5113 cx.read_from_clipboard()
5114 .and_then(|item| item.text().as_deref().map(str::to_string)),
5115 Some(
5116 "in selections.iter() {
5117 let mut start = selection.start;
5118 let mut end = selection.end;
5119 let is_entire_line = selection.is_empty();
5120 if is_entire_line {
5121 start = Point::new(start.row, 0);"
5122 .to_string()
5123 ),
5124 "When selecting past the indent, nothing is trimmed"
5125 );
5126}
5127
5128#[gpui::test]
5129async fn test_paste_multiline(cx: &mut TestAppContext) {
5130 init_test(cx, |_| {});
5131
5132 let mut cx = EditorTestContext::new(cx).await;
5133 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5134
5135 // Cut an indented block, without the leading whitespace.
5136 cx.set_state(indoc! {"
5137 const a: B = (
5138 c(),
5139 «d(
5140 e,
5141 f
5142 )ˇ»
5143 );
5144 "});
5145 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5146 cx.assert_editor_state(indoc! {"
5147 const a: B = (
5148 c(),
5149 ˇ
5150 );
5151 "});
5152
5153 // Paste it at the same position.
5154 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5155 cx.assert_editor_state(indoc! {"
5156 const a: B = (
5157 c(),
5158 d(
5159 e,
5160 f
5161 )ˇ
5162 );
5163 "});
5164
5165 // Paste it at a line with a lower indent level.
5166 cx.set_state(indoc! {"
5167 ˇ
5168 const a: B = (
5169 c(),
5170 );
5171 "});
5172 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5173 cx.assert_editor_state(indoc! {"
5174 d(
5175 e,
5176 f
5177 )ˇ
5178 const a: B = (
5179 c(),
5180 );
5181 "});
5182
5183 // Cut an indented block, with the leading whitespace.
5184 cx.set_state(indoc! {"
5185 const a: B = (
5186 c(),
5187 « d(
5188 e,
5189 f
5190 )
5191 ˇ»);
5192 "});
5193 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5194 cx.assert_editor_state(indoc! {"
5195 const a: B = (
5196 c(),
5197 ˇ);
5198 "});
5199
5200 // Paste it at the same position.
5201 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5202 cx.assert_editor_state(indoc! {"
5203 const a: B = (
5204 c(),
5205 d(
5206 e,
5207 f
5208 )
5209 ˇ);
5210 "});
5211
5212 // Paste it at a line with a higher indent level.
5213 cx.set_state(indoc! {"
5214 const a: B = (
5215 c(),
5216 d(
5217 e,
5218 fˇ
5219 )
5220 );
5221 "});
5222 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5223 cx.assert_editor_state(indoc! {"
5224 const a: B = (
5225 c(),
5226 d(
5227 e,
5228 f d(
5229 e,
5230 f
5231 )
5232 ˇ
5233 )
5234 );
5235 "});
5236
5237 // Copy an indented block, starting mid-line
5238 cx.set_state(indoc! {"
5239 const a: B = (
5240 c(),
5241 somethin«g(
5242 e,
5243 f
5244 )ˇ»
5245 );
5246 "});
5247 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5248
5249 // Paste it on a line with a lower indent level
5250 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5251 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5252 cx.assert_editor_state(indoc! {"
5253 const a: B = (
5254 c(),
5255 something(
5256 e,
5257 f
5258 )
5259 );
5260 g(
5261 e,
5262 f
5263 )ˇ"});
5264}
5265
5266#[gpui::test]
5267async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5268 init_test(cx, |_| {});
5269
5270 cx.write_to_clipboard(ClipboardItem::new_string(
5271 " d(\n e\n );\n".into(),
5272 ));
5273
5274 let mut cx = EditorTestContext::new(cx).await;
5275 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5276
5277 cx.set_state(indoc! {"
5278 fn a() {
5279 b();
5280 if c() {
5281 ˇ
5282 }
5283 }
5284 "});
5285
5286 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5287 cx.assert_editor_state(indoc! {"
5288 fn a() {
5289 b();
5290 if c() {
5291 d(
5292 e
5293 );
5294 ˇ
5295 }
5296 }
5297 "});
5298
5299 cx.set_state(indoc! {"
5300 fn a() {
5301 b();
5302 ˇ
5303 }
5304 "});
5305
5306 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5307 cx.assert_editor_state(indoc! {"
5308 fn a() {
5309 b();
5310 d(
5311 e
5312 );
5313 ˇ
5314 }
5315 "});
5316}
5317
5318#[gpui::test]
5319fn test_select_all(cx: &mut TestAppContext) {
5320 init_test(cx, |_| {});
5321
5322 let editor = cx.add_window(|window, cx| {
5323 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5324 build_editor(buffer, window, cx)
5325 });
5326 _ = editor.update(cx, |editor, window, cx| {
5327 editor.select_all(&SelectAll, window, cx);
5328 assert_eq!(
5329 editor.selections.display_ranges(cx),
5330 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5331 );
5332 });
5333}
5334
5335#[gpui::test]
5336fn test_select_line(cx: &mut TestAppContext) {
5337 init_test(cx, |_| {});
5338
5339 let editor = cx.add_window(|window, cx| {
5340 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5341 build_editor(buffer, window, cx)
5342 });
5343 _ = editor.update(cx, |editor, window, cx| {
5344 editor.change_selections(None, window, cx, |s| {
5345 s.select_display_ranges([
5346 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5347 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5348 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5349 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5350 ])
5351 });
5352 editor.select_line(&SelectLine, window, cx);
5353 assert_eq!(
5354 editor.selections.display_ranges(cx),
5355 vec![
5356 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5357 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5358 ]
5359 );
5360 });
5361
5362 _ = editor.update(cx, |editor, window, cx| {
5363 editor.select_line(&SelectLine, window, cx);
5364 assert_eq!(
5365 editor.selections.display_ranges(cx),
5366 vec![
5367 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5368 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5369 ]
5370 );
5371 });
5372
5373 _ = editor.update(cx, |editor, window, cx| {
5374 editor.select_line(&SelectLine, window, cx);
5375 assert_eq!(
5376 editor.selections.display_ranges(cx),
5377 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5378 );
5379 });
5380}
5381
5382#[gpui::test]
5383async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5384 init_test(cx, |_| {});
5385 let mut cx = EditorTestContext::new(cx).await;
5386
5387 #[track_caller]
5388 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5389 cx.set_state(initial_state);
5390 cx.update_editor(|e, window, cx| {
5391 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5392 });
5393 cx.assert_editor_state(expected_state);
5394 }
5395
5396 // Selection starts and ends at the middle of lines, left-to-right
5397 test(
5398 &mut cx,
5399 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5400 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5401 );
5402 // Same thing, right-to-left
5403 test(
5404 &mut cx,
5405 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5406 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5407 );
5408
5409 // Whole buffer, left-to-right, last line *doesn't* end with newline
5410 test(
5411 &mut cx,
5412 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5413 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5414 );
5415 // Same thing, right-to-left
5416 test(
5417 &mut cx,
5418 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5419 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5420 );
5421
5422 // Whole buffer, left-to-right, last line ends with newline
5423 test(
5424 &mut cx,
5425 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5426 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5427 );
5428 // Same thing, right-to-left
5429 test(
5430 &mut cx,
5431 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5432 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5433 );
5434
5435 // Starts at the end of a line, ends at the start of another
5436 test(
5437 &mut cx,
5438 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5439 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5440 );
5441}
5442
5443#[gpui::test]
5444async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5445 init_test(cx, |_| {});
5446
5447 let editor = cx.add_window(|window, cx| {
5448 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5449 build_editor(buffer, window, cx)
5450 });
5451
5452 // setup
5453 _ = editor.update(cx, |editor, window, cx| {
5454 editor.fold_creases(
5455 vec![
5456 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5457 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5458 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5459 ],
5460 true,
5461 window,
5462 cx,
5463 );
5464 assert_eq!(
5465 editor.display_text(cx),
5466 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5467 );
5468 });
5469
5470 _ = editor.update(cx, |editor, window, cx| {
5471 editor.change_selections(None, window, cx, |s| {
5472 s.select_display_ranges([
5473 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5474 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5475 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5476 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5477 ])
5478 });
5479 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5480 assert_eq!(
5481 editor.display_text(cx),
5482 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5483 );
5484 });
5485 EditorTestContext::for_editor(editor, cx)
5486 .await
5487 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5488
5489 _ = editor.update(cx, |editor, window, cx| {
5490 editor.change_selections(None, window, cx, |s| {
5491 s.select_display_ranges([
5492 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5493 ])
5494 });
5495 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5496 assert_eq!(
5497 editor.display_text(cx),
5498 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5499 );
5500 assert_eq!(
5501 editor.selections.display_ranges(cx),
5502 [
5503 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5504 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5505 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5506 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5507 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5508 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5509 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5510 ]
5511 );
5512 });
5513 EditorTestContext::for_editor(editor, cx)
5514 .await
5515 .assert_editor_state(
5516 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5517 );
5518}
5519
5520#[gpui::test]
5521async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5522 init_test(cx, |_| {});
5523
5524 let mut cx = EditorTestContext::new(cx).await;
5525
5526 cx.set_state(indoc!(
5527 r#"abc
5528 defˇghi
5529
5530 jk
5531 nlmo
5532 "#
5533 ));
5534
5535 cx.update_editor(|editor, window, cx| {
5536 editor.add_selection_above(&Default::default(), window, cx);
5537 });
5538
5539 cx.assert_editor_state(indoc!(
5540 r#"abcˇ
5541 defˇghi
5542
5543 jk
5544 nlmo
5545 "#
5546 ));
5547
5548 cx.update_editor(|editor, window, cx| {
5549 editor.add_selection_above(&Default::default(), window, cx);
5550 });
5551
5552 cx.assert_editor_state(indoc!(
5553 r#"abcˇ
5554 defˇghi
5555
5556 jk
5557 nlmo
5558 "#
5559 ));
5560
5561 cx.update_editor(|editor, window, cx| {
5562 editor.add_selection_below(&Default::default(), window, cx);
5563 });
5564
5565 cx.assert_editor_state(indoc!(
5566 r#"abc
5567 defˇghi
5568
5569 jk
5570 nlmo
5571 "#
5572 ));
5573
5574 cx.update_editor(|editor, window, cx| {
5575 editor.undo_selection(&Default::default(), window, cx);
5576 });
5577
5578 cx.assert_editor_state(indoc!(
5579 r#"abcˇ
5580 defˇghi
5581
5582 jk
5583 nlmo
5584 "#
5585 ));
5586
5587 cx.update_editor(|editor, window, cx| {
5588 editor.redo_selection(&Default::default(), window, cx);
5589 });
5590
5591 cx.assert_editor_state(indoc!(
5592 r#"abc
5593 defˇghi
5594
5595 jk
5596 nlmo
5597 "#
5598 ));
5599
5600 cx.update_editor(|editor, window, cx| {
5601 editor.add_selection_below(&Default::default(), window, cx);
5602 });
5603
5604 cx.assert_editor_state(indoc!(
5605 r#"abc
5606 defˇghi
5607
5608 jk
5609 nlmˇo
5610 "#
5611 ));
5612
5613 cx.update_editor(|editor, window, cx| {
5614 editor.add_selection_below(&Default::default(), window, cx);
5615 });
5616
5617 cx.assert_editor_state(indoc!(
5618 r#"abc
5619 defˇghi
5620
5621 jk
5622 nlmˇo
5623 "#
5624 ));
5625
5626 // change selections
5627 cx.set_state(indoc!(
5628 r#"abc
5629 def«ˇg»hi
5630
5631 jk
5632 nlmo
5633 "#
5634 ));
5635
5636 cx.update_editor(|editor, window, cx| {
5637 editor.add_selection_below(&Default::default(), window, cx);
5638 });
5639
5640 cx.assert_editor_state(indoc!(
5641 r#"abc
5642 def«ˇg»hi
5643
5644 jk
5645 nlm«ˇo»
5646 "#
5647 ));
5648
5649 cx.update_editor(|editor, window, cx| {
5650 editor.add_selection_below(&Default::default(), window, cx);
5651 });
5652
5653 cx.assert_editor_state(indoc!(
5654 r#"abc
5655 def«ˇg»hi
5656
5657 jk
5658 nlm«ˇo»
5659 "#
5660 ));
5661
5662 cx.update_editor(|editor, window, cx| {
5663 editor.add_selection_above(&Default::default(), window, cx);
5664 });
5665
5666 cx.assert_editor_state(indoc!(
5667 r#"abc
5668 def«ˇg»hi
5669
5670 jk
5671 nlmo
5672 "#
5673 ));
5674
5675 cx.update_editor(|editor, window, cx| {
5676 editor.add_selection_above(&Default::default(), window, cx);
5677 });
5678
5679 cx.assert_editor_state(indoc!(
5680 r#"abc
5681 def«ˇg»hi
5682
5683 jk
5684 nlmo
5685 "#
5686 ));
5687
5688 // Change selections again
5689 cx.set_state(indoc!(
5690 r#"a«bc
5691 defgˇ»hi
5692
5693 jk
5694 nlmo
5695 "#
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.add_selection_below(&Default::default(), window, cx);
5700 });
5701
5702 cx.assert_editor_state(indoc!(
5703 r#"a«bcˇ»
5704 d«efgˇ»hi
5705
5706 j«kˇ»
5707 nlmo
5708 "#
5709 ));
5710
5711 cx.update_editor(|editor, window, cx| {
5712 editor.add_selection_below(&Default::default(), window, cx);
5713 });
5714 cx.assert_editor_state(indoc!(
5715 r#"a«bcˇ»
5716 d«efgˇ»hi
5717
5718 j«kˇ»
5719 n«lmoˇ»
5720 "#
5721 ));
5722 cx.update_editor(|editor, window, cx| {
5723 editor.add_selection_above(&Default::default(), window, cx);
5724 });
5725
5726 cx.assert_editor_state(indoc!(
5727 r#"a«bcˇ»
5728 d«efgˇ»hi
5729
5730 j«kˇ»
5731 nlmo
5732 "#
5733 ));
5734
5735 // Change selections again
5736 cx.set_state(indoc!(
5737 r#"abc
5738 d«ˇefghi
5739
5740 jk
5741 nlm»o
5742 "#
5743 ));
5744
5745 cx.update_editor(|editor, window, cx| {
5746 editor.add_selection_above(&Default::default(), window, cx);
5747 });
5748
5749 cx.assert_editor_state(indoc!(
5750 r#"a«ˇbc»
5751 d«ˇef»ghi
5752
5753 j«ˇk»
5754 n«ˇlm»o
5755 "#
5756 ));
5757
5758 cx.update_editor(|editor, window, cx| {
5759 editor.add_selection_below(&Default::default(), window, cx);
5760 });
5761
5762 cx.assert_editor_state(indoc!(
5763 r#"abc
5764 d«ˇef»ghi
5765
5766 j«ˇk»
5767 n«ˇlm»o
5768 "#
5769 ));
5770}
5771
5772#[gpui::test]
5773async fn test_select_next(cx: &mut TestAppContext) {
5774 init_test(cx, |_| {});
5775
5776 let mut cx = EditorTestContext::new(cx).await;
5777 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5778
5779 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5780 .unwrap();
5781 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5782
5783 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5784 .unwrap();
5785 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5786
5787 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5788 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5789
5790 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5791 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5792
5793 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5794 .unwrap();
5795 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5796
5797 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5798 .unwrap();
5799 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5800}
5801
5802#[gpui::test]
5803async fn test_select_all_matches(cx: &mut TestAppContext) {
5804 init_test(cx, |_| {});
5805
5806 let mut cx = EditorTestContext::new(cx).await;
5807
5808 // Test caret-only selections
5809 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5810 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5811 .unwrap();
5812 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5813
5814 // Test left-to-right selections
5815 cx.set_state("abc\n«abcˇ»\nabc");
5816 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5817 .unwrap();
5818 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5819
5820 // Test right-to-left selections
5821 cx.set_state("abc\n«ˇabc»\nabc");
5822 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5823 .unwrap();
5824 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5825
5826 // Test selecting whitespace with caret selection
5827 cx.set_state("abc\nˇ abc\nabc");
5828 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5829 .unwrap();
5830 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5831
5832 // Test selecting whitespace with left-to-right selection
5833 cx.set_state("abc\n«ˇ »abc\nabc");
5834 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5835 .unwrap();
5836 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5837
5838 // Test no matches with right-to-left selection
5839 cx.set_state("abc\n« ˇ»abc\nabc");
5840 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5841 .unwrap();
5842 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5843}
5844
5845#[gpui::test]
5846async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
5847 init_test(cx, |_| {});
5848
5849 let mut cx = EditorTestContext::new(cx).await;
5850
5851 let large_body_1 = "\nd".repeat(200);
5852 let large_body_2 = "\ne".repeat(200);
5853
5854 cx.set_state(&format!(
5855 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
5856 ));
5857 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
5858 let scroll_position = editor.scroll_position(cx);
5859 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
5860 scroll_position
5861 });
5862
5863 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5864 .unwrap();
5865 cx.assert_editor_state(&format!(
5866 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
5867 ));
5868 let scroll_position_after_selection =
5869 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
5870 assert_eq!(
5871 initial_scroll_position, scroll_position_after_selection,
5872 "Scroll position should not change after selecting all matches"
5873 );
5874}
5875
5876#[gpui::test]
5877async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5878 init_test(cx, |_| {});
5879
5880 let mut cx = EditorTestContext::new(cx).await;
5881 cx.set_state(
5882 r#"let foo = 2;
5883lˇet foo = 2;
5884let fooˇ = 2;
5885let foo = 2;
5886let foo = ˇ2;"#,
5887 );
5888
5889 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5890 .unwrap();
5891 cx.assert_editor_state(
5892 r#"let foo = 2;
5893«letˇ» foo = 2;
5894let «fooˇ» = 2;
5895let foo = 2;
5896let foo = «2ˇ»;"#,
5897 );
5898
5899 // noop for multiple selections with different contents
5900 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5901 .unwrap();
5902 cx.assert_editor_state(
5903 r#"let foo = 2;
5904«letˇ» foo = 2;
5905let «fooˇ» = 2;
5906let foo = 2;
5907let foo = «2ˇ»;"#,
5908 );
5909}
5910
5911#[gpui::test]
5912async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5913 init_test(cx, |_| {});
5914
5915 let mut cx =
5916 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5917
5918 cx.assert_editor_state(indoc! {"
5919 ˇbbb
5920 ccc
5921
5922 bbb
5923 ccc
5924 "});
5925 cx.dispatch_action(SelectPrevious::default());
5926 cx.assert_editor_state(indoc! {"
5927 «bbbˇ»
5928 ccc
5929
5930 bbb
5931 ccc
5932 "});
5933 cx.dispatch_action(SelectPrevious::default());
5934 cx.assert_editor_state(indoc! {"
5935 «bbbˇ»
5936 ccc
5937
5938 «bbbˇ»
5939 ccc
5940 "});
5941}
5942
5943#[gpui::test]
5944async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5945 init_test(cx, |_| {});
5946
5947 let mut cx = EditorTestContext::new(cx).await;
5948 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5949
5950 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5951 .unwrap();
5952 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5953
5954 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5955 .unwrap();
5956 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5957
5958 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5959 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5960
5961 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5962 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5963
5964 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5965 .unwrap();
5966 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5967
5968 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5969 .unwrap();
5970 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5971
5972 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5973 .unwrap();
5974 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5975}
5976
5977#[gpui::test]
5978async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5979 init_test(cx, |_| {});
5980
5981 let mut cx = EditorTestContext::new(cx).await;
5982 cx.set_state("aˇ");
5983
5984 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5985 .unwrap();
5986 cx.assert_editor_state("«aˇ»");
5987 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5988 .unwrap();
5989 cx.assert_editor_state("«aˇ»");
5990}
5991
5992#[gpui::test]
5993async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5994 init_test(cx, |_| {});
5995
5996 let mut cx = EditorTestContext::new(cx).await;
5997 cx.set_state(
5998 r#"let foo = 2;
5999lˇet foo = 2;
6000let fooˇ = 2;
6001let foo = 2;
6002let foo = ˇ2;"#,
6003 );
6004
6005 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6006 .unwrap();
6007 cx.assert_editor_state(
6008 r#"let foo = 2;
6009«letˇ» foo = 2;
6010let «fooˇ» = 2;
6011let foo = 2;
6012let foo = «2ˇ»;"#,
6013 );
6014
6015 // noop for multiple selections with different contents
6016 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6017 .unwrap();
6018 cx.assert_editor_state(
6019 r#"let foo = 2;
6020«letˇ» foo = 2;
6021let «fooˇ» = 2;
6022let foo = 2;
6023let foo = «2ˇ»;"#,
6024 );
6025}
6026
6027#[gpui::test]
6028async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6029 init_test(cx, |_| {});
6030
6031 let mut cx = EditorTestContext::new(cx).await;
6032 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6033
6034 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6035 .unwrap();
6036 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6037
6038 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6039 .unwrap();
6040 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6041
6042 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6043 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6044
6045 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6046 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6047
6048 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6049 .unwrap();
6050 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
6051
6052 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6053 .unwrap();
6054 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6055}
6056
6057#[gpui::test]
6058async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6059 init_test(cx, |_| {});
6060
6061 let language = Arc::new(Language::new(
6062 LanguageConfig::default(),
6063 Some(tree_sitter_rust::LANGUAGE.into()),
6064 ));
6065
6066 let text = r#"
6067 use mod1::mod2::{mod3, mod4};
6068
6069 fn fn_1(param1: bool, param2: &str) {
6070 let var1 = "text";
6071 }
6072 "#
6073 .unindent();
6074
6075 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6076 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6077 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6078
6079 editor
6080 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6081 .await;
6082
6083 editor.update_in(cx, |editor, window, cx| {
6084 editor.change_selections(None, window, cx, |s| {
6085 s.select_display_ranges([
6086 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6087 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6088 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6089 ]);
6090 });
6091 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6092 });
6093 editor.update(cx, |editor, cx| {
6094 assert_text_with_selections(
6095 editor,
6096 indoc! {r#"
6097 use mod1::mod2::{mod3, «mod4ˇ»};
6098
6099 fn fn_1«ˇ(param1: bool, param2: &str)» {
6100 let var1 = "«ˇtext»";
6101 }
6102 "#},
6103 cx,
6104 );
6105 });
6106
6107 editor.update_in(cx, |editor, window, cx| {
6108 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6109 });
6110 editor.update(cx, |editor, cx| {
6111 assert_text_with_selections(
6112 editor,
6113 indoc! {r#"
6114 use mod1::mod2::«{mod3, mod4}ˇ»;
6115
6116 «ˇfn fn_1(param1: bool, param2: &str) {
6117 let var1 = "text";
6118 }»
6119 "#},
6120 cx,
6121 );
6122 });
6123
6124 editor.update_in(cx, |editor, window, cx| {
6125 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6126 });
6127 assert_eq!(
6128 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6129 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6130 );
6131
6132 // Trying to expand the selected syntax node one more time has no effect.
6133 editor.update_in(cx, |editor, window, cx| {
6134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6135 });
6136 assert_eq!(
6137 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6138 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6139 );
6140
6141 editor.update_in(cx, |editor, window, cx| {
6142 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6143 });
6144 editor.update(cx, |editor, cx| {
6145 assert_text_with_selections(
6146 editor,
6147 indoc! {r#"
6148 use mod1::mod2::«{mod3, mod4}ˇ»;
6149
6150 «ˇfn fn_1(param1: bool, param2: &str) {
6151 let var1 = "text";
6152 }»
6153 "#},
6154 cx,
6155 );
6156 });
6157
6158 editor.update_in(cx, |editor, window, cx| {
6159 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6160 });
6161 editor.update(cx, |editor, cx| {
6162 assert_text_with_selections(
6163 editor,
6164 indoc! {r#"
6165 use mod1::mod2::{mod3, «mod4ˇ»};
6166
6167 fn fn_1«ˇ(param1: bool, param2: &str)» {
6168 let var1 = "«ˇtext»";
6169 }
6170 "#},
6171 cx,
6172 );
6173 });
6174
6175 editor.update_in(cx, |editor, window, cx| {
6176 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6177 });
6178 editor.update(cx, |editor, cx| {
6179 assert_text_with_selections(
6180 editor,
6181 indoc! {r#"
6182 use mod1::mod2::{mod3, mo«ˇ»d4};
6183
6184 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6185 let var1 = "te«ˇ»xt";
6186 }
6187 "#},
6188 cx,
6189 );
6190 });
6191
6192 // Trying to shrink the selected syntax node one more time has no effect.
6193 editor.update_in(cx, |editor, window, cx| {
6194 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6195 });
6196 editor.update_in(cx, |editor, _, cx| {
6197 assert_text_with_selections(
6198 editor,
6199 indoc! {r#"
6200 use mod1::mod2::{mod3, mo«ˇ»d4};
6201
6202 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6203 let var1 = "te«ˇ»xt";
6204 }
6205 "#},
6206 cx,
6207 );
6208 });
6209
6210 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6211 // a fold.
6212 editor.update_in(cx, |editor, window, cx| {
6213 editor.fold_creases(
6214 vec![
6215 Crease::simple(
6216 Point::new(0, 21)..Point::new(0, 24),
6217 FoldPlaceholder::test(),
6218 ),
6219 Crease::simple(
6220 Point::new(3, 20)..Point::new(3, 22),
6221 FoldPlaceholder::test(),
6222 ),
6223 ],
6224 true,
6225 window,
6226 cx,
6227 );
6228 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6229 });
6230 editor.update(cx, |editor, cx| {
6231 assert_text_with_selections(
6232 editor,
6233 indoc! {r#"
6234 use mod1::mod2::«{mod3, mod4}ˇ»;
6235
6236 fn fn_1«ˇ(param1: bool, param2: &str)» {
6237 «ˇlet var1 = "text";»
6238 }
6239 "#},
6240 cx,
6241 );
6242 });
6243}
6244
6245#[gpui::test]
6246async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6247 init_test(cx, |_| {});
6248
6249 let base_text = r#"
6250 impl A {
6251 // this is an uncommitted comment
6252
6253 fn b() {
6254 c();
6255 }
6256
6257 // this is another uncommitted comment
6258
6259 fn d() {
6260 // e
6261 // f
6262 }
6263 }
6264
6265 fn g() {
6266 // h
6267 }
6268 "#
6269 .unindent();
6270
6271 let text = r#"
6272 ˇimpl A {
6273
6274 fn b() {
6275 c();
6276 }
6277
6278 fn d() {
6279 // e
6280 // f
6281 }
6282 }
6283
6284 fn g() {
6285 // h
6286 }
6287 "#
6288 .unindent();
6289
6290 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6291 cx.set_state(&text);
6292 cx.set_head_text(&base_text);
6293 cx.update_editor(|editor, window, cx| {
6294 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6295 });
6296
6297 cx.assert_state_with_diff(
6298 "
6299 ˇimpl A {
6300 - // this is an uncommitted comment
6301
6302 fn b() {
6303 c();
6304 }
6305
6306 - // this is another uncommitted comment
6307 -
6308 fn d() {
6309 // e
6310 // f
6311 }
6312 }
6313
6314 fn g() {
6315 // h
6316 }
6317 "
6318 .unindent(),
6319 );
6320
6321 let expected_display_text = "
6322 impl A {
6323 // this is an uncommitted comment
6324
6325 fn b() {
6326 ⋯
6327 }
6328
6329 // this is another uncommitted comment
6330
6331 fn d() {
6332 ⋯
6333 }
6334 }
6335
6336 fn g() {
6337 ⋯
6338 }
6339 "
6340 .unindent();
6341
6342 cx.update_editor(|editor, window, cx| {
6343 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6344 assert_eq!(editor.display_text(cx), expected_display_text);
6345 });
6346}
6347
6348#[gpui::test]
6349async fn test_autoindent(cx: &mut TestAppContext) {
6350 init_test(cx, |_| {});
6351
6352 let language = Arc::new(
6353 Language::new(
6354 LanguageConfig {
6355 brackets: BracketPairConfig {
6356 pairs: vec![
6357 BracketPair {
6358 start: "{".to_string(),
6359 end: "}".to_string(),
6360 close: false,
6361 surround: false,
6362 newline: true,
6363 },
6364 BracketPair {
6365 start: "(".to_string(),
6366 end: ")".to_string(),
6367 close: false,
6368 surround: false,
6369 newline: true,
6370 },
6371 ],
6372 ..Default::default()
6373 },
6374 ..Default::default()
6375 },
6376 Some(tree_sitter_rust::LANGUAGE.into()),
6377 )
6378 .with_indents_query(
6379 r#"
6380 (_ "(" ")" @end) @indent
6381 (_ "{" "}" @end) @indent
6382 "#,
6383 )
6384 .unwrap(),
6385 );
6386
6387 let text = "fn a() {}";
6388
6389 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6390 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6391 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6392 editor
6393 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6394 .await;
6395
6396 editor.update_in(cx, |editor, window, cx| {
6397 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6398 editor.newline(&Newline, window, cx);
6399 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6400 assert_eq!(
6401 editor.selections.ranges(cx),
6402 &[
6403 Point::new(1, 4)..Point::new(1, 4),
6404 Point::new(3, 4)..Point::new(3, 4),
6405 Point::new(5, 0)..Point::new(5, 0)
6406 ]
6407 );
6408 });
6409}
6410
6411#[gpui::test]
6412async fn test_autoindent_selections(cx: &mut TestAppContext) {
6413 init_test(cx, |_| {});
6414
6415 {
6416 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6417 cx.set_state(indoc! {"
6418 impl A {
6419
6420 fn b() {}
6421
6422 «fn c() {
6423
6424 }ˇ»
6425 }
6426 "});
6427
6428 cx.update_editor(|editor, window, cx| {
6429 editor.autoindent(&Default::default(), window, cx);
6430 });
6431
6432 cx.assert_editor_state(indoc! {"
6433 impl A {
6434
6435 fn b() {}
6436
6437 «fn c() {
6438
6439 }ˇ»
6440 }
6441 "});
6442 }
6443
6444 {
6445 let mut cx = EditorTestContext::new_multibuffer(
6446 cx,
6447 [indoc! { "
6448 impl A {
6449 «
6450 // a
6451 fn b(){}
6452 »
6453 «
6454 }
6455 fn c(){}
6456 »
6457 "}],
6458 );
6459
6460 let buffer = cx.update_editor(|editor, _, cx| {
6461 let buffer = editor.buffer().update(cx, |buffer, _| {
6462 buffer.all_buffers().iter().next().unwrap().clone()
6463 });
6464 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6465 buffer
6466 });
6467
6468 cx.run_until_parked();
6469 cx.update_editor(|editor, window, cx| {
6470 editor.select_all(&Default::default(), window, cx);
6471 editor.autoindent(&Default::default(), window, cx)
6472 });
6473 cx.run_until_parked();
6474
6475 cx.update(|_, cx| {
6476 assert_eq!(
6477 buffer.read(cx).text(),
6478 indoc! { "
6479 impl A {
6480
6481 // a
6482 fn b(){}
6483
6484
6485 }
6486 fn c(){}
6487
6488 " }
6489 )
6490 });
6491 }
6492}
6493
6494#[gpui::test]
6495async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6496 init_test(cx, |_| {});
6497
6498 let mut cx = EditorTestContext::new(cx).await;
6499
6500 let language = Arc::new(Language::new(
6501 LanguageConfig {
6502 brackets: BracketPairConfig {
6503 pairs: vec![
6504 BracketPair {
6505 start: "{".to_string(),
6506 end: "}".to_string(),
6507 close: true,
6508 surround: true,
6509 newline: true,
6510 },
6511 BracketPair {
6512 start: "(".to_string(),
6513 end: ")".to_string(),
6514 close: true,
6515 surround: true,
6516 newline: true,
6517 },
6518 BracketPair {
6519 start: "/*".to_string(),
6520 end: " */".to_string(),
6521 close: true,
6522 surround: true,
6523 newline: true,
6524 },
6525 BracketPair {
6526 start: "[".to_string(),
6527 end: "]".to_string(),
6528 close: false,
6529 surround: false,
6530 newline: true,
6531 },
6532 BracketPair {
6533 start: "\"".to_string(),
6534 end: "\"".to_string(),
6535 close: true,
6536 surround: true,
6537 newline: false,
6538 },
6539 BracketPair {
6540 start: "<".to_string(),
6541 end: ">".to_string(),
6542 close: false,
6543 surround: true,
6544 newline: true,
6545 },
6546 ],
6547 ..Default::default()
6548 },
6549 autoclose_before: "})]".to_string(),
6550 ..Default::default()
6551 },
6552 Some(tree_sitter_rust::LANGUAGE.into()),
6553 ));
6554
6555 cx.language_registry().add(language.clone());
6556 cx.update_buffer(|buffer, cx| {
6557 buffer.set_language(Some(language), cx);
6558 });
6559
6560 cx.set_state(
6561 &r#"
6562 🏀ˇ
6563 εˇ
6564 ❤️ˇ
6565 "#
6566 .unindent(),
6567 );
6568
6569 // autoclose multiple nested brackets at multiple cursors
6570 cx.update_editor(|editor, window, cx| {
6571 editor.handle_input("{", window, cx);
6572 editor.handle_input("{", window, cx);
6573 editor.handle_input("{", window, cx);
6574 });
6575 cx.assert_editor_state(
6576 &"
6577 🏀{{{ˇ}}}
6578 ε{{{ˇ}}}
6579 ❤️{{{ˇ}}}
6580 "
6581 .unindent(),
6582 );
6583
6584 // insert a different closing bracket
6585 cx.update_editor(|editor, window, cx| {
6586 editor.handle_input(")", window, cx);
6587 });
6588 cx.assert_editor_state(
6589 &"
6590 🏀{{{)ˇ}}}
6591 ε{{{)ˇ}}}
6592 ❤️{{{)ˇ}}}
6593 "
6594 .unindent(),
6595 );
6596
6597 // skip over the auto-closed brackets when typing a closing bracket
6598 cx.update_editor(|editor, window, cx| {
6599 editor.move_right(&MoveRight, window, cx);
6600 editor.handle_input("}", window, cx);
6601 editor.handle_input("}", window, cx);
6602 editor.handle_input("}", window, cx);
6603 });
6604 cx.assert_editor_state(
6605 &"
6606 🏀{{{)}}}}ˇ
6607 ε{{{)}}}}ˇ
6608 ❤️{{{)}}}}ˇ
6609 "
6610 .unindent(),
6611 );
6612
6613 // autoclose multi-character pairs
6614 cx.set_state(
6615 &"
6616 ˇ
6617 ˇ
6618 "
6619 .unindent(),
6620 );
6621 cx.update_editor(|editor, window, cx| {
6622 editor.handle_input("/", window, cx);
6623 editor.handle_input("*", window, cx);
6624 });
6625 cx.assert_editor_state(
6626 &"
6627 /*ˇ */
6628 /*ˇ */
6629 "
6630 .unindent(),
6631 );
6632
6633 // one cursor autocloses a multi-character pair, one cursor
6634 // does not autoclose.
6635 cx.set_state(
6636 &"
6637 /ˇ
6638 ˇ
6639 "
6640 .unindent(),
6641 );
6642 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6643 cx.assert_editor_state(
6644 &"
6645 /*ˇ */
6646 *ˇ
6647 "
6648 .unindent(),
6649 );
6650
6651 // Don't autoclose if the next character isn't whitespace and isn't
6652 // listed in the language's "autoclose_before" section.
6653 cx.set_state("ˇa b");
6654 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6655 cx.assert_editor_state("{ˇa b");
6656
6657 // Don't autoclose if `close` is false for the bracket pair
6658 cx.set_state("ˇ");
6659 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6660 cx.assert_editor_state("[ˇ");
6661
6662 // Surround with brackets if text is selected
6663 cx.set_state("«aˇ» b");
6664 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6665 cx.assert_editor_state("{«aˇ»} b");
6666
6667 // Autoclose when not immediately after a word character
6668 cx.set_state("a ˇ");
6669 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6670 cx.assert_editor_state("a \"ˇ\"");
6671
6672 // Autoclose pair where the start and end characters are the same
6673 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6674 cx.assert_editor_state("a \"\"ˇ");
6675
6676 // Don't autoclose when immediately after a word character
6677 cx.set_state("aˇ");
6678 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6679 cx.assert_editor_state("a\"ˇ");
6680
6681 // Do autoclose when after a non-word character
6682 cx.set_state("{ˇ");
6683 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6684 cx.assert_editor_state("{\"ˇ\"");
6685
6686 // Non identical pairs autoclose regardless of preceding character
6687 cx.set_state("aˇ");
6688 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6689 cx.assert_editor_state("a{ˇ}");
6690
6691 // Don't autoclose pair if autoclose is disabled
6692 cx.set_state("ˇ");
6693 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6694 cx.assert_editor_state("<ˇ");
6695
6696 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6697 cx.set_state("«aˇ» b");
6698 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6699 cx.assert_editor_state("<«aˇ»> b");
6700}
6701
6702#[gpui::test]
6703async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6704 init_test(cx, |settings| {
6705 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6706 });
6707
6708 let mut cx = EditorTestContext::new(cx).await;
6709
6710 let language = Arc::new(Language::new(
6711 LanguageConfig {
6712 brackets: BracketPairConfig {
6713 pairs: vec![
6714 BracketPair {
6715 start: "{".to_string(),
6716 end: "}".to_string(),
6717 close: true,
6718 surround: true,
6719 newline: true,
6720 },
6721 BracketPair {
6722 start: "(".to_string(),
6723 end: ")".to_string(),
6724 close: true,
6725 surround: true,
6726 newline: true,
6727 },
6728 BracketPair {
6729 start: "[".to_string(),
6730 end: "]".to_string(),
6731 close: false,
6732 surround: false,
6733 newline: true,
6734 },
6735 ],
6736 ..Default::default()
6737 },
6738 autoclose_before: "})]".to_string(),
6739 ..Default::default()
6740 },
6741 Some(tree_sitter_rust::LANGUAGE.into()),
6742 ));
6743
6744 cx.language_registry().add(language.clone());
6745 cx.update_buffer(|buffer, cx| {
6746 buffer.set_language(Some(language), cx);
6747 });
6748
6749 cx.set_state(
6750 &"
6751 ˇ
6752 ˇ
6753 ˇ
6754 "
6755 .unindent(),
6756 );
6757
6758 // ensure only matching closing brackets are skipped over
6759 cx.update_editor(|editor, window, cx| {
6760 editor.handle_input("}", window, cx);
6761 editor.move_left(&MoveLeft, window, cx);
6762 editor.handle_input(")", window, cx);
6763 editor.move_left(&MoveLeft, window, cx);
6764 });
6765 cx.assert_editor_state(
6766 &"
6767 ˇ)}
6768 ˇ)}
6769 ˇ)}
6770 "
6771 .unindent(),
6772 );
6773
6774 // skip-over closing brackets at multiple cursors
6775 cx.update_editor(|editor, window, cx| {
6776 editor.handle_input(")", window, cx);
6777 editor.handle_input("}", window, cx);
6778 });
6779 cx.assert_editor_state(
6780 &"
6781 )}ˇ
6782 )}ˇ
6783 )}ˇ
6784 "
6785 .unindent(),
6786 );
6787
6788 // ignore non-close brackets
6789 cx.update_editor(|editor, window, cx| {
6790 editor.handle_input("]", window, cx);
6791 editor.move_left(&MoveLeft, window, cx);
6792 editor.handle_input("]", window, cx);
6793 });
6794 cx.assert_editor_state(
6795 &"
6796 )}]ˇ]
6797 )}]ˇ]
6798 )}]ˇ]
6799 "
6800 .unindent(),
6801 );
6802}
6803
6804#[gpui::test]
6805async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6806 init_test(cx, |_| {});
6807
6808 let mut cx = EditorTestContext::new(cx).await;
6809
6810 let html_language = Arc::new(
6811 Language::new(
6812 LanguageConfig {
6813 name: "HTML".into(),
6814 brackets: BracketPairConfig {
6815 pairs: vec![
6816 BracketPair {
6817 start: "<".into(),
6818 end: ">".into(),
6819 close: true,
6820 ..Default::default()
6821 },
6822 BracketPair {
6823 start: "{".into(),
6824 end: "}".into(),
6825 close: true,
6826 ..Default::default()
6827 },
6828 BracketPair {
6829 start: "(".into(),
6830 end: ")".into(),
6831 close: true,
6832 ..Default::default()
6833 },
6834 ],
6835 ..Default::default()
6836 },
6837 autoclose_before: "})]>".into(),
6838 ..Default::default()
6839 },
6840 Some(tree_sitter_html::LANGUAGE.into()),
6841 )
6842 .with_injection_query(
6843 r#"
6844 (script_element
6845 (raw_text) @injection.content
6846 (#set! injection.language "javascript"))
6847 "#,
6848 )
6849 .unwrap(),
6850 );
6851
6852 let javascript_language = Arc::new(Language::new(
6853 LanguageConfig {
6854 name: "JavaScript".into(),
6855 brackets: BracketPairConfig {
6856 pairs: vec![
6857 BracketPair {
6858 start: "/*".into(),
6859 end: " */".into(),
6860 close: true,
6861 ..Default::default()
6862 },
6863 BracketPair {
6864 start: "{".into(),
6865 end: "}".into(),
6866 close: true,
6867 ..Default::default()
6868 },
6869 BracketPair {
6870 start: "(".into(),
6871 end: ")".into(),
6872 close: true,
6873 ..Default::default()
6874 },
6875 ],
6876 ..Default::default()
6877 },
6878 autoclose_before: "})]>".into(),
6879 ..Default::default()
6880 },
6881 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6882 ));
6883
6884 cx.language_registry().add(html_language.clone());
6885 cx.language_registry().add(javascript_language.clone());
6886
6887 cx.update_buffer(|buffer, cx| {
6888 buffer.set_language(Some(html_language), cx);
6889 });
6890
6891 cx.set_state(
6892 &r#"
6893 <body>ˇ
6894 <script>
6895 var x = 1;ˇ
6896 </script>
6897 </body>ˇ
6898 "#
6899 .unindent(),
6900 );
6901
6902 // Precondition: different languages are active at different locations.
6903 cx.update_editor(|editor, window, cx| {
6904 let snapshot = editor.snapshot(window, cx);
6905 let cursors = editor.selections.ranges::<usize>(cx);
6906 let languages = cursors
6907 .iter()
6908 .map(|c| snapshot.language_at(c.start).unwrap().name())
6909 .collect::<Vec<_>>();
6910 assert_eq!(
6911 languages,
6912 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6913 );
6914 });
6915
6916 // Angle brackets autoclose in HTML, but not JavaScript.
6917 cx.update_editor(|editor, window, cx| {
6918 editor.handle_input("<", window, cx);
6919 editor.handle_input("a", window, cx);
6920 });
6921 cx.assert_editor_state(
6922 &r#"
6923 <body><aˇ>
6924 <script>
6925 var x = 1;<aˇ
6926 </script>
6927 </body><aˇ>
6928 "#
6929 .unindent(),
6930 );
6931
6932 // Curly braces and parens autoclose in both HTML and JavaScript.
6933 cx.update_editor(|editor, window, cx| {
6934 editor.handle_input(" b=", window, cx);
6935 editor.handle_input("{", window, cx);
6936 editor.handle_input("c", window, cx);
6937 editor.handle_input("(", window, cx);
6938 });
6939 cx.assert_editor_state(
6940 &r#"
6941 <body><a b={c(ˇ)}>
6942 <script>
6943 var x = 1;<a b={c(ˇ)}
6944 </script>
6945 </body><a b={c(ˇ)}>
6946 "#
6947 .unindent(),
6948 );
6949
6950 // Brackets that were already autoclosed are skipped.
6951 cx.update_editor(|editor, window, cx| {
6952 editor.handle_input(")", window, cx);
6953 editor.handle_input("d", window, cx);
6954 editor.handle_input("}", window, cx);
6955 });
6956 cx.assert_editor_state(
6957 &r#"
6958 <body><a b={c()d}ˇ>
6959 <script>
6960 var x = 1;<a b={c()d}ˇ
6961 </script>
6962 </body><a b={c()d}ˇ>
6963 "#
6964 .unindent(),
6965 );
6966 cx.update_editor(|editor, window, cx| {
6967 editor.handle_input(">", window, cx);
6968 });
6969 cx.assert_editor_state(
6970 &r#"
6971 <body><a b={c()d}>ˇ
6972 <script>
6973 var x = 1;<a b={c()d}>ˇ
6974 </script>
6975 </body><a b={c()d}>ˇ
6976 "#
6977 .unindent(),
6978 );
6979
6980 // Reset
6981 cx.set_state(
6982 &r#"
6983 <body>ˇ
6984 <script>
6985 var x = 1;ˇ
6986 </script>
6987 </body>ˇ
6988 "#
6989 .unindent(),
6990 );
6991
6992 cx.update_editor(|editor, window, cx| {
6993 editor.handle_input("<", window, cx);
6994 });
6995 cx.assert_editor_state(
6996 &r#"
6997 <body><ˇ>
6998 <script>
6999 var x = 1;<ˇ
7000 </script>
7001 </body><ˇ>
7002 "#
7003 .unindent(),
7004 );
7005
7006 // When backspacing, the closing angle brackets are removed.
7007 cx.update_editor(|editor, window, cx| {
7008 editor.backspace(&Backspace, window, cx);
7009 });
7010 cx.assert_editor_state(
7011 &r#"
7012 <body>ˇ
7013 <script>
7014 var x = 1;ˇ
7015 </script>
7016 </body>ˇ
7017 "#
7018 .unindent(),
7019 );
7020
7021 // Block comments autoclose in JavaScript, but not HTML.
7022 cx.update_editor(|editor, window, cx| {
7023 editor.handle_input("/", window, cx);
7024 editor.handle_input("*", window, cx);
7025 });
7026 cx.assert_editor_state(
7027 &r#"
7028 <body>/*ˇ
7029 <script>
7030 var x = 1;/*ˇ */
7031 </script>
7032 </body>/*ˇ
7033 "#
7034 .unindent(),
7035 );
7036}
7037
7038#[gpui::test]
7039async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7040 init_test(cx, |_| {});
7041
7042 let mut cx = EditorTestContext::new(cx).await;
7043
7044 let rust_language = Arc::new(
7045 Language::new(
7046 LanguageConfig {
7047 name: "Rust".into(),
7048 brackets: serde_json::from_value(json!([
7049 { "start": "{", "end": "}", "close": true, "newline": true },
7050 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7051 ]))
7052 .unwrap(),
7053 autoclose_before: "})]>".into(),
7054 ..Default::default()
7055 },
7056 Some(tree_sitter_rust::LANGUAGE.into()),
7057 )
7058 .with_override_query("(string_literal) @string")
7059 .unwrap(),
7060 );
7061
7062 cx.language_registry().add(rust_language.clone());
7063 cx.update_buffer(|buffer, cx| {
7064 buffer.set_language(Some(rust_language), cx);
7065 });
7066
7067 cx.set_state(
7068 &r#"
7069 let x = ˇ
7070 "#
7071 .unindent(),
7072 );
7073
7074 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7075 cx.update_editor(|editor, window, cx| {
7076 editor.handle_input("\"", window, cx);
7077 });
7078 cx.assert_editor_state(
7079 &r#"
7080 let x = "ˇ"
7081 "#
7082 .unindent(),
7083 );
7084
7085 // Inserting another quotation mark. The cursor moves across the existing
7086 // automatically-inserted quotation mark.
7087 cx.update_editor(|editor, window, cx| {
7088 editor.handle_input("\"", window, cx);
7089 });
7090 cx.assert_editor_state(
7091 &r#"
7092 let x = ""ˇ
7093 "#
7094 .unindent(),
7095 );
7096
7097 // Reset
7098 cx.set_state(
7099 &r#"
7100 let x = ˇ
7101 "#
7102 .unindent(),
7103 );
7104
7105 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7106 cx.update_editor(|editor, window, cx| {
7107 editor.handle_input("\"", window, cx);
7108 editor.handle_input(" ", window, cx);
7109 editor.move_left(&Default::default(), window, cx);
7110 editor.handle_input("\\", window, cx);
7111 editor.handle_input("\"", window, cx);
7112 });
7113 cx.assert_editor_state(
7114 &r#"
7115 let x = "\"ˇ "
7116 "#
7117 .unindent(),
7118 );
7119
7120 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7121 // mark. Nothing is inserted.
7122 cx.update_editor(|editor, window, cx| {
7123 editor.move_right(&Default::default(), window, cx);
7124 editor.handle_input("\"", window, cx);
7125 });
7126 cx.assert_editor_state(
7127 &r#"
7128 let x = "\" "ˇ
7129 "#
7130 .unindent(),
7131 );
7132}
7133
7134#[gpui::test]
7135async fn test_surround_with_pair(cx: &mut TestAppContext) {
7136 init_test(cx, |_| {});
7137
7138 let language = Arc::new(Language::new(
7139 LanguageConfig {
7140 brackets: BracketPairConfig {
7141 pairs: vec![
7142 BracketPair {
7143 start: "{".to_string(),
7144 end: "}".to_string(),
7145 close: true,
7146 surround: true,
7147 newline: true,
7148 },
7149 BracketPair {
7150 start: "/* ".to_string(),
7151 end: "*/".to_string(),
7152 close: true,
7153 surround: true,
7154 ..Default::default()
7155 },
7156 ],
7157 ..Default::default()
7158 },
7159 ..Default::default()
7160 },
7161 Some(tree_sitter_rust::LANGUAGE.into()),
7162 ));
7163
7164 let text = r#"
7165 a
7166 b
7167 c
7168 "#
7169 .unindent();
7170
7171 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7172 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7173 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7174 editor
7175 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7176 .await;
7177
7178 editor.update_in(cx, |editor, window, cx| {
7179 editor.change_selections(None, window, cx, |s| {
7180 s.select_display_ranges([
7181 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7182 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7183 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7184 ])
7185 });
7186
7187 editor.handle_input("{", window, cx);
7188 editor.handle_input("{", window, cx);
7189 editor.handle_input("{", window, cx);
7190 assert_eq!(
7191 editor.text(cx),
7192 "
7193 {{{a}}}
7194 {{{b}}}
7195 {{{c}}}
7196 "
7197 .unindent()
7198 );
7199 assert_eq!(
7200 editor.selections.display_ranges(cx),
7201 [
7202 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7203 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7204 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7205 ]
7206 );
7207
7208 editor.undo(&Undo, window, cx);
7209 editor.undo(&Undo, window, cx);
7210 editor.undo(&Undo, window, cx);
7211 assert_eq!(
7212 editor.text(cx),
7213 "
7214 a
7215 b
7216 c
7217 "
7218 .unindent()
7219 );
7220 assert_eq!(
7221 editor.selections.display_ranges(cx),
7222 [
7223 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7224 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7225 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7226 ]
7227 );
7228
7229 // Ensure inserting the first character of a multi-byte bracket pair
7230 // doesn't surround the selections with the bracket.
7231 editor.handle_input("/", window, cx);
7232 assert_eq!(
7233 editor.text(cx),
7234 "
7235 /
7236 /
7237 /
7238 "
7239 .unindent()
7240 );
7241 assert_eq!(
7242 editor.selections.display_ranges(cx),
7243 [
7244 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7245 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7246 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7247 ]
7248 );
7249
7250 editor.undo(&Undo, window, cx);
7251 assert_eq!(
7252 editor.text(cx),
7253 "
7254 a
7255 b
7256 c
7257 "
7258 .unindent()
7259 );
7260 assert_eq!(
7261 editor.selections.display_ranges(cx),
7262 [
7263 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7264 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7265 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7266 ]
7267 );
7268
7269 // Ensure inserting the last character of a multi-byte bracket pair
7270 // doesn't surround the selections with the bracket.
7271 editor.handle_input("*", window, cx);
7272 assert_eq!(
7273 editor.text(cx),
7274 "
7275 *
7276 *
7277 *
7278 "
7279 .unindent()
7280 );
7281 assert_eq!(
7282 editor.selections.display_ranges(cx),
7283 [
7284 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7285 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7286 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7287 ]
7288 );
7289 });
7290}
7291
7292#[gpui::test]
7293async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7294 init_test(cx, |_| {});
7295
7296 let language = Arc::new(Language::new(
7297 LanguageConfig {
7298 brackets: BracketPairConfig {
7299 pairs: vec![BracketPair {
7300 start: "{".to_string(),
7301 end: "}".to_string(),
7302 close: true,
7303 surround: true,
7304 newline: true,
7305 }],
7306 ..Default::default()
7307 },
7308 autoclose_before: "}".to_string(),
7309 ..Default::default()
7310 },
7311 Some(tree_sitter_rust::LANGUAGE.into()),
7312 ));
7313
7314 let text = r#"
7315 a
7316 b
7317 c
7318 "#
7319 .unindent();
7320
7321 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7322 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7323 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7324 editor
7325 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7326 .await;
7327
7328 editor.update_in(cx, |editor, window, cx| {
7329 editor.change_selections(None, window, cx, |s| {
7330 s.select_ranges([
7331 Point::new(0, 1)..Point::new(0, 1),
7332 Point::new(1, 1)..Point::new(1, 1),
7333 Point::new(2, 1)..Point::new(2, 1),
7334 ])
7335 });
7336
7337 editor.handle_input("{", window, cx);
7338 editor.handle_input("{", window, cx);
7339 editor.handle_input("_", window, cx);
7340 assert_eq!(
7341 editor.text(cx),
7342 "
7343 a{{_}}
7344 b{{_}}
7345 c{{_}}
7346 "
7347 .unindent()
7348 );
7349 assert_eq!(
7350 editor.selections.ranges::<Point>(cx),
7351 [
7352 Point::new(0, 4)..Point::new(0, 4),
7353 Point::new(1, 4)..Point::new(1, 4),
7354 Point::new(2, 4)..Point::new(2, 4)
7355 ]
7356 );
7357
7358 editor.backspace(&Default::default(), window, cx);
7359 editor.backspace(&Default::default(), window, cx);
7360 assert_eq!(
7361 editor.text(cx),
7362 "
7363 a{}
7364 b{}
7365 c{}
7366 "
7367 .unindent()
7368 );
7369 assert_eq!(
7370 editor.selections.ranges::<Point>(cx),
7371 [
7372 Point::new(0, 2)..Point::new(0, 2),
7373 Point::new(1, 2)..Point::new(1, 2),
7374 Point::new(2, 2)..Point::new(2, 2)
7375 ]
7376 );
7377
7378 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7379 assert_eq!(
7380 editor.text(cx),
7381 "
7382 a
7383 b
7384 c
7385 "
7386 .unindent()
7387 );
7388 assert_eq!(
7389 editor.selections.ranges::<Point>(cx),
7390 [
7391 Point::new(0, 1)..Point::new(0, 1),
7392 Point::new(1, 1)..Point::new(1, 1),
7393 Point::new(2, 1)..Point::new(2, 1)
7394 ]
7395 );
7396 });
7397}
7398
7399#[gpui::test]
7400async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7401 init_test(cx, |settings| {
7402 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7403 });
7404
7405 let mut cx = EditorTestContext::new(cx).await;
7406
7407 let language = Arc::new(Language::new(
7408 LanguageConfig {
7409 brackets: BracketPairConfig {
7410 pairs: vec![
7411 BracketPair {
7412 start: "{".to_string(),
7413 end: "}".to_string(),
7414 close: true,
7415 surround: true,
7416 newline: true,
7417 },
7418 BracketPair {
7419 start: "(".to_string(),
7420 end: ")".to_string(),
7421 close: true,
7422 surround: true,
7423 newline: true,
7424 },
7425 BracketPair {
7426 start: "[".to_string(),
7427 end: "]".to_string(),
7428 close: false,
7429 surround: true,
7430 newline: true,
7431 },
7432 ],
7433 ..Default::default()
7434 },
7435 autoclose_before: "})]".to_string(),
7436 ..Default::default()
7437 },
7438 Some(tree_sitter_rust::LANGUAGE.into()),
7439 ));
7440
7441 cx.language_registry().add(language.clone());
7442 cx.update_buffer(|buffer, cx| {
7443 buffer.set_language(Some(language), cx);
7444 });
7445
7446 cx.set_state(
7447 &"
7448 {(ˇ)}
7449 [[ˇ]]
7450 {(ˇ)}
7451 "
7452 .unindent(),
7453 );
7454
7455 cx.update_editor(|editor, window, cx| {
7456 editor.backspace(&Default::default(), window, cx);
7457 editor.backspace(&Default::default(), window, cx);
7458 });
7459
7460 cx.assert_editor_state(
7461 &"
7462 ˇ
7463 ˇ]]
7464 ˇ
7465 "
7466 .unindent(),
7467 );
7468
7469 cx.update_editor(|editor, window, cx| {
7470 editor.handle_input("{", window, cx);
7471 editor.handle_input("{", window, cx);
7472 editor.move_right(&MoveRight, window, cx);
7473 editor.move_right(&MoveRight, window, cx);
7474 editor.move_left(&MoveLeft, window, cx);
7475 editor.move_left(&MoveLeft, window, cx);
7476 editor.backspace(&Default::default(), window, cx);
7477 });
7478
7479 cx.assert_editor_state(
7480 &"
7481 {ˇ}
7482 {ˇ}]]
7483 {ˇ}
7484 "
7485 .unindent(),
7486 );
7487
7488 cx.update_editor(|editor, window, cx| {
7489 editor.backspace(&Default::default(), window, cx);
7490 });
7491
7492 cx.assert_editor_state(
7493 &"
7494 ˇ
7495 ˇ]]
7496 ˇ
7497 "
7498 .unindent(),
7499 );
7500}
7501
7502#[gpui::test]
7503async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7504 init_test(cx, |_| {});
7505
7506 let language = Arc::new(Language::new(
7507 LanguageConfig::default(),
7508 Some(tree_sitter_rust::LANGUAGE.into()),
7509 ));
7510
7511 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7512 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7513 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7514 editor
7515 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7516 .await;
7517
7518 editor.update_in(cx, |editor, window, cx| {
7519 editor.set_auto_replace_emoji_shortcode(true);
7520
7521 editor.handle_input("Hello ", window, cx);
7522 editor.handle_input(":wave", window, cx);
7523 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7524
7525 editor.handle_input(":", window, cx);
7526 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7527
7528 editor.handle_input(" :smile", window, cx);
7529 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7530
7531 editor.handle_input(":", window, cx);
7532 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7533
7534 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7535 editor.handle_input(":wave", window, cx);
7536 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7537
7538 editor.handle_input(":", window, cx);
7539 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7540
7541 editor.handle_input(":1", window, cx);
7542 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7543
7544 editor.handle_input(":", window, cx);
7545 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7546
7547 // Ensure shortcode does not get replaced when it is part of a word
7548 editor.handle_input(" Test:wave", window, cx);
7549 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7550
7551 editor.handle_input(":", window, cx);
7552 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7553
7554 editor.set_auto_replace_emoji_shortcode(false);
7555
7556 // Ensure shortcode does not get replaced when auto replace is off
7557 editor.handle_input(" :wave", window, cx);
7558 assert_eq!(
7559 editor.text(cx),
7560 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7561 );
7562
7563 editor.handle_input(":", window, cx);
7564 assert_eq!(
7565 editor.text(cx),
7566 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7567 );
7568 });
7569}
7570
7571#[gpui::test]
7572async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7573 init_test(cx, |_| {});
7574
7575 let (text, insertion_ranges) = marked_text_ranges(
7576 indoc! {"
7577 ˇ
7578 "},
7579 false,
7580 );
7581
7582 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7583 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7584
7585 _ = editor.update_in(cx, |editor, window, cx| {
7586 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7587
7588 editor
7589 .insert_snippet(&insertion_ranges, snippet, window, cx)
7590 .unwrap();
7591
7592 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7593 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7594 assert_eq!(editor.text(cx), expected_text);
7595 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7596 }
7597
7598 assert(
7599 editor,
7600 cx,
7601 indoc! {"
7602 type «» =•
7603 "},
7604 );
7605
7606 assert!(editor.context_menu_visible(), "There should be a matches");
7607 });
7608}
7609
7610#[gpui::test]
7611async fn test_snippets(cx: &mut TestAppContext) {
7612 init_test(cx, |_| {});
7613
7614 let (text, insertion_ranges) = marked_text_ranges(
7615 indoc! {"
7616 a.ˇ b
7617 a.ˇ b
7618 a.ˇ b
7619 "},
7620 false,
7621 );
7622
7623 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7624 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7625
7626 editor.update_in(cx, |editor, window, cx| {
7627 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7628
7629 editor
7630 .insert_snippet(&insertion_ranges, snippet, window, cx)
7631 .unwrap();
7632
7633 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7634 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7635 assert_eq!(editor.text(cx), expected_text);
7636 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7637 }
7638
7639 assert(
7640 editor,
7641 cx,
7642 indoc! {"
7643 a.f(«one», two, «three») b
7644 a.f(«one», two, «three») b
7645 a.f(«one», two, «three») b
7646 "},
7647 );
7648
7649 // Can't move earlier than the first tab stop
7650 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7651 assert(
7652 editor,
7653 cx,
7654 indoc! {"
7655 a.f(«one», two, «three») b
7656 a.f(«one», two, «three») b
7657 a.f(«one», two, «three») b
7658 "},
7659 );
7660
7661 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7662 assert(
7663 editor,
7664 cx,
7665 indoc! {"
7666 a.f(one, «two», three) b
7667 a.f(one, «two», three) b
7668 a.f(one, «two», three) b
7669 "},
7670 );
7671
7672 editor.move_to_prev_snippet_tabstop(window, cx);
7673 assert(
7674 editor,
7675 cx,
7676 indoc! {"
7677 a.f(«one», two, «three») b
7678 a.f(«one», two, «three») b
7679 a.f(«one», two, «three») b
7680 "},
7681 );
7682
7683 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7684 assert(
7685 editor,
7686 cx,
7687 indoc! {"
7688 a.f(one, «two», three) b
7689 a.f(one, «two», three) b
7690 a.f(one, «two», three) b
7691 "},
7692 );
7693 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7694 assert(
7695 editor,
7696 cx,
7697 indoc! {"
7698 a.f(one, two, three)ˇ b
7699 a.f(one, two, three)ˇ b
7700 a.f(one, two, three)ˇ b
7701 "},
7702 );
7703
7704 // As soon as the last tab stop is reached, snippet state is gone
7705 editor.move_to_prev_snippet_tabstop(window, cx);
7706 assert(
7707 editor,
7708 cx,
7709 indoc! {"
7710 a.f(one, two, three)ˇ b
7711 a.f(one, two, three)ˇ b
7712 a.f(one, two, three)ˇ b
7713 "},
7714 );
7715 });
7716}
7717
7718#[gpui::test]
7719async fn test_document_format_during_save(cx: &mut TestAppContext) {
7720 init_test(cx, |_| {});
7721
7722 let fs = FakeFs::new(cx.executor());
7723 fs.insert_file(path!("/file.rs"), Default::default()).await;
7724
7725 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7726
7727 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7728 language_registry.add(rust_lang());
7729 let mut fake_servers = language_registry.register_fake_lsp(
7730 "Rust",
7731 FakeLspAdapter {
7732 capabilities: lsp::ServerCapabilities {
7733 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7734 ..Default::default()
7735 },
7736 ..Default::default()
7737 },
7738 );
7739
7740 let buffer = project
7741 .update(cx, |project, cx| {
7742 project.open_local_buffer(path!("/file.rs"), cx)
7743 })
7744 .await
7745 .unwrap();
7746
7747 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7748 let (editor, cx) = cx.add_window_view(|window, cx| {
7749 build_editor_with_project(project.clone(), buffer, window, cx)
7750 });
7751 editor.update_in(cx, |editor, window, cx| {
7752 editor.set_text("one\ntwo\nthree\n", window, cx)
7753 });
7754 assert!(cx.read(|cx| editor.is_dirty(cx)));
7755
7756 cx.executor().start_waiting();
7757 let fake_server = fake_servers.next().await.unwrap();
7758
7759 let save = editor
7760 .update_in(cx, |editor, window, cx| {
7761 editor.save(true, project.clone(), window, cx)
7762 })
7763 .unwrap();
7764 fake_server
7765 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7766 assert_eq!(
7767 params.text_document.uri,
7768 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7769 );
7770 assert_eq!(params.options.tab_size, 4);
7771 Ok(Some(vec![lsp::TextEdit::new(
7772 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7773 ", ".to_string(),
7774 )]))
7775 })
7776 .next()
7777 .await;
7778 cx.executor().start_waiting();
7779 save.await;
7780
7781 assert_eq!(
7782 editor.update(cx, |editor, cx| editor.text(cx)),
7783 "one, two\nthree\n"
7784 );
7785 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7786
7787 editor.update_in(cx, |editor, window, cx| {
7788 editor.set_text("one\ntwo\nthree\n", window, cx)
7789 });
7790 assert!(cx.read(|cx| editor.is_dirty(cx)));
7791
7792 // Ensure we can still save even if formatting hangs.
7793 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7794 move |params, _| async move {
7795 assert_eq!(
7796 params.text_document.uri,
7797 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7798 );
7799 futures::future::pending::<()>().await;
7800 unreachable!()
7801 },
7802 );
7803 let save = editor
7804 .update_in(cx, |editor, window, cx| {
7805 editor.save(true, project.clone(), window, cx)
7806 })
7807 .unwrap();
7808 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7809 cx.executor().start_waiting();
7810 save.await;
7811 assert_eq!(
7812 editor.update(cx, |editor, cx| editor.text(cx)),
7813 "one\ntwo\nthree\n"
7814 );
7815 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7816
7817 // For non-dirty buffer, no formatting request should be sent
7818 let save = editor
7819 .update_in(cx, |editor, window, cx| {
7820 editor.save(true, project.clone(), window, cx)
7821 })
7822 .unwrap();
7823 let _pending_format_request = fake_server
7824 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7825 panic!("Should not be invoked on non-dirty buffer");
7826 })
7827 .next();
7828 cx.executor().start_waiting();
7829 save.await;
7830
7831 // Set rust language override and assert overridden tabsize is sent to language server
7832 update_test_language_settings(cx, |settings| {
7833 settings.languages.insert(
7834 "Rust".into(),
7835 LanguageSettingsContent {
7836 tab_size: NonZeroU32::new(8),
7837 ..Default::default()
7838 },
7839 );
7840 });
7841
7842 editor.update_in(cx, |editor, window, cx| {
7843 editor.set_text("somehting_new\n", window, cx)
7844 });
7845 assert!(cx.read(|cx| editor.is_dirty(cx)));
7846 let save = editor
7847 .update_in(cx, |editor, window, cx| {
7848 editor.save(true, project.clone(), window, cx)
7849 })
7850 .unwrap();
7851 fake_server
7852 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7853 assert_eq!(
7854 params.text_document.uri,
7855 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7856 );
7857 assert_eq!(params.options.tab_size, 8);
7858 Ok(Some(vec![]))
7859 })
7860 .next()
7861 .await;
7862 cx.executor().start_waiting();
7863 save.await;
7864}
7865
7866#[gpui::test]
7867async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7868 init_test(cx, |_| {});
7869
7870 let cols = 4;
7871 let rows = 10;
7872 let sample_text_1 = sample_text(rows, cols, 'a');
7873 assert_eq!(
7874 sample_text_1,
7875 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7876 );
7877 let sample_text_2 = sample_text(rows, cols, 'l');
7878 assert_eq!(
7879 sample_text_2,
7880 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7881 );
7882 let sample_text_3 = sample_text(rows, cols, 'v');
7883 assert_eq!(
7884 sample_text_3,
7885 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7886 );
7887
7888 let fs = FakeFs::new(cx.executor());
7889 fs.insert_tree(
7890 path!("/a"),
7891 json!({
7892 "main.rs": sample_text_1,
7893 "other.rs": sample_text_2,
7894 "lib.rs": sample_text_3,
7895 }),
7896 )
7897 .await;
7898
7899 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7900 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7901 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7902
7903 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7904 language_registry.add(rust_lang());
7905 let mut fake_servers = language_registry.register_fake_lsp(
7906 "Rust",
7907 FakeLspAdapter {
7908 capabilities: lsp::ServerCapabilities {
7909 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7910 ..Default::default()
7911 },
7912 ..Default::default()
7913 },
7914 );
7915
7916 let worktree = project.update(cx, |project, cx| {
7917 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7918 assert_eq!(worktrees.len(), 1);
7919 worktrees.pop().unwrap()
7920 });
7921 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7922
7923 let buffer_1 = project
7924 .update(cx, |project, cx| {
7925 project.open_buffer((worktree_id, "main.rs"), cx)
7926 })
7927 .await
7928 .unwrap();
7929 let buffer_2 = project
7930 .update(cx, |project, cx| {
7931 project.open_buffer((worktree_id, "other.rs"), cx)
7932 })
7933 .await
7934 .unwrap();
7935 let buffer_3 = project
7936 .update(cx, |project, cx| {
7937 project.open_buffer((worktree_id, "lib.rs"), cx)
7938 })
7939 .await
7940 .unwrap();
7941
7942 let multi_buffer = cx.new(|cx| {
7943 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7944 multi_buffer.push_excerpts(
7945 buffer_1.clone(),
7946 [
7947 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7948 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7949 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7950 ],
7951 cx,
7952 );
7953 multi_buffer.push_excerpts(
7954 buffer_2.clone(),
7955 [
7956 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7957 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7958 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7959 ],
7960 cx,
7961 );
7962 multi_buffer.push_excerpts(
7963 buffer_3.clone(),
7964 [
7965 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7966 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7967 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7968 ],
7969 cx,
7970 );
7971 multi_buffer
7972 });
7973 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7974 Editor::new(
7975 EditorMode::Full,
7976 multi_buffer,
7977 Some(project.clone()),
7978 window,
7979 cx,
7980 )
7981 });
7982
7983 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7984 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7985 s.select_ranges(Some(1..2))
7986 });
7987 editor.insert("|one|two|three|", window, cx);
7988 });
7989 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7990 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7991 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7992 s.select_ranges(Some(60..70))
7993 });
7994 editor.insert("|four|five|six|", window, cx);
7995 });
7996 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7997
7998 // First two buffers should be edited, but not the third one.
7999 assert_eq!(
8000 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8001 "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}",
8002 );
8003 buffer_1.update(cx, |buffer, _| {
8004 assert!(buffer.is_dirty());
8005 assert_eq!(
8006 buffer.text(),
8007 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8008 )
8009 });
8010 buffer_2.update(cx, |buffer, _| {
8011 assert!(buffer.is_dirty());
8012 assert_eq!(
8013 buffer.text(),
8014 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8015 )
8016 });
8017 buffer_3.update(cx, |buffer, _| {
8018 assert!(!buffer.is_dirty());
8019 assert_eq!(buffer.text(), sample_text_3,)
8020 });
8021 cx.executor().run_until_parked();
8022
8023 cx.executor().start_waiting();
8024 let save = multi_buffer_editor
8025 .update_in(cx, |editor, window, cx| {
8026 editor.save(true, project.clone(), window, cx)
8027 })
8028 .unwrap();
8029
8030 let fake_server = fake_servers.next().await.unwrap();
8031 fake_server
8032 .server
8033 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8034 Ok(Some(vec![lsp::TextEdit::new(
8035 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8036 format!("[{} formatted]", params.text_document.uri),
8037 )]))
8038 })
8039 .detach();
8040 save.await;
8041
8042 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8043 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8044 assert_eq!(
8045 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8046 uri!(
8047 "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}"
8048 ),
8049 );
8050 buffer_1.update(cx, |buffer, _| {
8051 assert!(!buffer.is_dirty());
8052 assert_eq!(
8053 buffer.text(),
8054 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8055 )
8056 });
8057 buffer_2.update(cx, |buffer, _| {
8058 assert!(!buffer.is_dirty());
8059 assert_eq!(
8060 buffer.text(),
8061 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8062 )
8063 });
8064 buffer_3.update(cx, |buffer, _| {
8065 assert!(!buffer.is_dirty());
8066 assert_eq!(buffer.text(), sample_text_3,)
8067 });
8068}
8069
8070#[gpui::test]
8071async fn test_range_format_during_save(cx: &mut TestAppContext) {
8072 init_test(cx, |_| {});
8073
8074 let fs = FakeFs::new(cx.executor());
8075 fs.insert_file(path!("/file.rs"), Default::default()).await;
8076
8077 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8078
8079 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8080 language_registry.add(rust_lang());
8081 let mut fake_servers = language_registry.register_fake_lsp(
8082 "Rust",
8083 FakeLspAdapter {
8084 capabilities: lsp::ServerCapabilities {
8085 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8086 ..Default::default()
8087 },
8088 ..Default::default()
8089 },
8090 );
8091
8092 let buffer = project
8093 .update(cx, |project, cx| {
8094 project.open_local_buffer(path!("/file.rs"), cx)
8095 })
8096 .await
8097 .unwrap();
8098
8099 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8100 let (editor, cx) = cx.add_window_view(|window, cx| {
8101 build_editor_with_project(project.clone(), buffer, window, cx)
8102 });
8103 editor.update_in(cx, |editor, window, cx| {
8104 editor.set_text("one\ntwo\nthree\n", window, cx)
8105 });
8106 assert!(cx.read(|cx| editor.is_dirty(cx)));
8107
8108 cx.executor().start_waiting();
8109 let fake_server = fake_servers.next().await.unwrap();
8110
8111 let save = editor
8112 .update_in(cx, |editor, window, cx| {
8113 editor.save(true, project.clone(), window, cx)
8114 })
8115 .unwrap();
8116 fake_server
8117 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8118 assert_eq!(
8119 params.text_document.uri,
8120 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8121 );
8122 assert_eq!(params.options.tab_size, 4);
8123 Ok(Some(vec![lsp::TextEdit::new(
8124 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8125 ", ".to_string(),
8126 )]))
8127 })
8128 .next()
8129 .await;
8130 cx.executor().start_waiting();
8131 save.await;
8132 assert_eq!(
8133 editor.update(cx, |editor, cx| editor.text(cx)),
8134 "one, two\nthree\n"
8135 );
8136 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8137
8138 editor.update_in(cx, |editor, window, cx| {
8139 editor.set_text("one\ntwo\nthree\n", window, cx)
8140 });
8141 assert!(cx.read(|cx| editor.is_dirty(cx)));
8142
8143 // Ensure we can still save even if formatting hangs.
8144 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8145 move |params, _| async move {
8146 assert_eq!(
8147 params.text_document.uri,
8148 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8149 );
8150 futures::future::pending::<()>().await;
8151 unreachable!()
8152 },
8153 );
8154 let save = editor
8155 .update_in(cx, |editor, window, cx| {
8156 editor.save(true, project.clone(), window, cx)
8157 })
8158 .unwrap();
8159 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8160 cx.executor().start_waiting();
8161 save.await;
8162 assert_eq!(
8163 editor.update(cx, |editor, cx| editor.text(cx)),
8164 "one\ntwo\nthree\n"
8165 );
8166 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8167
8168 // For non-dirty buffer, no formatting request should be sent
8169 let save = editor
8170 .update_in(cx, |editor, window, cx| {
8171 editor.save(true, project.clone(), window, cx)
8172 })
8173 .unwrap();
8174 let _pending_format_request = fake_server
8175 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8176 panic!("Should not be invoked on non-dirty buffer");
8177 })
8178 .next();
8179 cx.executor().start_waiting();
8180 save.await;
8181
8182 // Set Rust language override and assert overridden tabsize is sent to language server
8183 update_test_language_settings(cx, |settings| {
8184 settings.languages.insert(
8185 "Rust".into(),
8186 LanguageSettingsContent {
8187 tab_size: NonZeroU32::new(8),
8188 ..Default::default()
8189 },
8190 );
8191 });
8192
8193 editor.update_in(cx, |editor, window, cx| {
8194 editor.set_text("somehting_new\n", window, cx)
8195 });
8196 assert!(cx.read(|cx| editor.is_dirty(cx)));
8197 let save = editor
8198 .update_in(cx, |editor, window, cx| {
8199 editor.save(true, project.clone(), window, cx)
8200 })
8201 .unwrap();
8202 fake_server
8203 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8204 assert_eq!(
8205 params.text_document.uri,
8206 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8207 );
8208 assert_eq!(params.options.tab_size, 8);
8209 Ok(Some(vec![]))
8210 })
8211 .next()
8212 .await;
8213 cx.executor().start_waiting();
8214 save.await;
8215}
8216
8217#[gpui::test]
8218async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8219 init_test(cx, |settings| {
8220 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8221 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8222 ))
8223 });
8224
8225 let fs = FakeFs::new(cx.executor());
8226 fs.insert_file(path!("/file.rs"), Default::default()).await;
8227
8228 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8229
8230 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8231 language_registry.add(Arc::new(Language::new(
8232 LanguageConfig {
8233 name: "Rust".into(),
8234 matcher: LanguageMatcher {
8235 path_suffixes: vec!["rs".to_string()],
8236 ..Default::default()
8237 },
8238 ..LanguageConfig::default()
8239 },
8240 Some(tree_sitter_rust::LANGUAGE.into()),
8241 )));
8242 update_test_language_settings(cx, |settings| {
8243 // Enable Prettier formatting for the same buffer, and ensure
8244 // LSP is called instead of Prettier.
8245 settings.defaults.prettier = Some(PrettierSettings {
8246 allowed: true,
8247 ..PrettierSettings::default()
8248 });
8249 });
8250 let mut fake_servers = language_registry.register_fake_lsp(
8251 "Rust",
8252 FakeLspAdapter {
8253 capabilities: lsp::ServerCapabilities {
8254 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8255 ..Default::default()
8256 },
8257 ..Default::default()
8258 },
8259 );
8260
8261 let buffer = project
8262 .update(cx, |project, cx| {
8263 project.open_local_buffer(path!("/file.rs"), cx)
8264 })
8265 .await
8266 .unwrap();
8267
8268 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8269 let (editor, cx) = cx.add_window_view(|window, cx| {
8270 build_editor_with_project(project.clone(), buffer, window, cx)
8271 });
8272 editor.update_in(cx, |editor, window, cx| {
8273 editor.set_text("one\ntwo\nthree\n", window, cx)
8274 });
8275
8276 cx.executor().start_waiting();
8277 let fake_server = fake_servers.next().await.unwrap();
8278
8279 let format = editor
8280 .update_in(cx, |editor, window, cx| {
8281 editor.perform_format(
8282 project.clone(),
8283 FormatTrigger::Manual,
8284 FormatTarget::Buffers,
8285 window,
8286 cx,
8287 )
8288 })
8289 .unwrap();
8290 fake_server
8291 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8292 assert_eq!(
8293 params.text_document.uri,
8294 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8295 );
8296 assert_eq!(params.options.tab_size, 4);
8297 Ok(Some(vec![lsp::TextEdit::new(
8298 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8299 ", ".to_string(),
8300 )]))
8301 })
8302 .next()
8303 .await;
8304 cx.executor().start_waiting();
8305 format.await;
8306 assert_eq!(
8307 editor.update(cx, |editor, cx| editor.text(cx)),
8308 "one, two\nthree\n"
8309 );
8310
8311 editor.update_in(cx, |editor, window, cx| {
8312 editor.set_text("one\ntwo\nthree\n", window, cx)
8313 });
8314 // Ensure we don't lock if formatting hangs.
8315 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8316 move |params, _| async move {
8317 assert_eq!(
8318 params.text_document.uri,
8319 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8320 );
8321 futures::future::pending::<()>().await;
8322 unreachable!()
8323 },
8324 );
8325 let format = editor
8326 .update_in(cx, |editor, window, cx| {
8327 editor.perform_format(
8328 project,
8329 FormatTrigger::Manual,
8330 FormatTarget::Buffers,
8331 window,
8332 cx,
8333 )
8334 })
8335 .unwrap();
8336 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8337 cx.executor().start_waiting();
8338 format.await;
8339 assert_eq!(
8340 editor.update(cx, |editor, cx| editor.text(cx)),
8341 "one\ntwo\nthree\n"
8342 );
8343}
8344
8345#[gpui::test]
8346async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8347 init_test(cx, |settings| {
8348 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8349 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8350 ))
8351 });
8352
8353 let fs = FakeFs::new(cx.executor());
8354 fs.insert_file(path!("/file.ts"), Default::default()).await;
8355
8356 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8357
8358 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8359 language_registry.add(Arc::new(Language::new(
8360 LanguageConfig {
8361 name: "TypeScript".into(),
8362 matcher: LanguageMatcher {
8363 path_suffixes: vec!["ts".to_string()],
8364 ..Default::default()
8365 },
8366 ..LanguageConfig::default()
8367 },
8368 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8369 )));
8370 update_test_language_settings(cx, |settings| {
8371 settings.defaults.prettier = Some(PrettierSettings {
8372 allowed: true,
8373 ..PrettierSettings::default()
8374 });
8375 });
8376 let mut fake_servers = language_registry.register_fake_lsp(
8377 "TypeScript",
8378 FakeLspAdapter {
8379 capabilities: lsp::ServerCapabilities {
8380 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8381 ..Default::default()
8382 },
8383 ..Default::default()
8384 },
8385 );
8386
8387 let buffer = project
8388 .update(cx, |project, cx| {
8389 project.open_local_buffer(path!("/file.ts"), cx)
8390 })
8391 .await
8392 .unwrap();
8393
8394 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8395 let (editor, cx) = cx.add_window_view(|window, cx| {
8396 build_editor_with_project(project.clone(), buffer, window, cx)
8397 });
8398 editor.update_in(cx, |editor, window, cx| {
8399 editor.set_text(
8400 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8401 window,
8402 cx,
8403 )
8404 });
8405
8406 cx.executor().start_waiting();
8407 let fake_server = fake_servers.next().await.unwrap();
8408
8409 let format = editor
8410 .update_in(cx, |editor, window, cx| {
8411 editor.perform_code_action_kind(
8412 project.clone(),
8413 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8414 window,
8415 cx,
8416 )
8417 })
8418 .unwrap();
8419 fake_server
8420 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8421 assert_eq!(
8422 params.text_document.uri,
8423 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8424 );
8425 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8426 lsp::CodeAction {
8427 title: "Organize Imports".to_string(),
8428 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8429 edit: Some(lsp::WorkspaceEdit {
8430 changes: Some(
8431 [(
8432 params.text_document.uri.clone(),
8433 vec![lsp::TextEdit::new(
8434 lsp::Range::new(
8435 lsp::Position::new(1, 0),
8436 lsp::Position::new(2, 0),
8437 ),
8438 "".to_string(),
8439 )],
8440 )]
8441 .into_iter()
8442 .collect(),
8443 ),
8444 ..Default::default()
8445 }),
8446 ..Default::default()
8447 },
8448 )]))
8449 })
8450 .next()
8451 .await;
8452 cx.executor().start_waiting();
8453 format.await;
8454 assert_eq!(
8455 editor.update(cx, |editor, cx| editor.text(cx)),
8456 "import { a } from 'module';\n\nconst x = a;\n"
8457 );
8458
8459 editor.update_in(cx, |editor, window, cx| {
8460 editor.set_text(
8461 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8462 window,
8463 cx,
8464 )
8465 });
8466 // Ensure we don't lock if code action hangs.
8467 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8468 move |params, _| async move {
8469 assert_eq!(
8470 params.text_document.uri,
8471 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8472 );
8473 futures::future::pending::<()>().await;
8474 unreachable!()
8475 },
8476 );
8477 let format = editor
8478 .update_in(cx, |editor, window, cx| {
8479 editor.perform_code_action_kind(
8480 project,
8481 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8482 window,
8483 cx,
8484 )
8485 })
8486 .unwrap();
8487 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8488 cx.executor().start_waiting();
8489 format.await;
8490 assert_eq!(
8491 editor.update(cx, |editor, cx| editor.text(cx)),
8492 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8493 );
8494}
8495
8496#[gpui::test]
8497async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8498 init_test(cx, |_| {});
8499
8500 let mut cx = EditorLspTestContext::new_rust(
8501 lsp::ServerCapabilities {
8502 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8503 ..Default::default()
8504 },
8505 cx,
8506 )
8507 .await;
8508
8509 cx.set_state(indoc! {"
8510 one.twoˇ
8511 "});
8512
8513 // The format request takes a long time. When it completes, it inserts
8514 // a newline and an indent before the `.`
8515 cx.lsp
8516 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8517 let executor = cx.background_executor().clone();
8518 async move {
8519 executor.timer(Duration::from_millis(100)).await;
8520 Ok(Some(vec![lsp::TextEdit {
8521 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8522 new_text: "\n ".into(),
8523 }]))
8524 }
8525 });
8526
8527 // Submit a format request.
8528 let format_1 = cx
8529 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8530 .unwrap();
8531 cx.executor().run_until_parked();
8532
8533 // Submit a second format request.
8534 let format_2 = cx
8535 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8536 .unwrap();
8537 cx.executor().run_until_parked();
8538
8539 // Wait for both format requests to complete
8540 cx.executor().advance_clock(Duration::from_millis(200));
8541 cx.executor().start_waiting();
8542 format_1.await.unwrap();
8543 cx.executor().start_waiting();
8544 format_2.await.unwrap();
8545
8546 // The formatting edits only happens once.
8547 cx.assert_editor_state(indoc! {"
8548 one
8549 .twoˇ
8550 "});
8551}
8552
8553#[gpui::test]
8554async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8555 init_test(cx, |settings| {
8556 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8557 });
8558
8559 let mut cx = EditorLspTestContext::new_rust(
8560 lsp::ServerCapabilities {
8561 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8562 ..Default::default()
8563 },
8564 cx,
8565 )
8566 .await;
8567
8568 // Set up a buffer white some trailing whitespace and no trailing newline.
8569 cx.set_state(
8570 &[
8571 "one ", //
8572 "twoˇ", //
8573 "three ", //
8574 "four", //
8575 ]
8576 .join("\n"),
8577 );
8578
8579 // Submit a format request.
8580 let format = cx
8581 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8582 .unwrap();
8583
8584 // Record which buffer changes have been sent to the language server
8585 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8586 cx.lsp
8587 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8588 let buffer_changes = buffer_changes.clone();
8589 move |params, _| {
8590 buffer_changes.lock().extend(
8591 params
8592 .content_changes
8593 .into_iter()
8594 .map(|e| (e.range.unwrap(), e.text)),
8595 );
8596 }
8597 });
8598
8599 // Handle formatting requests to the language server.
8600 cx.lsp
8601 .set_request_handler::<lsp::request::Formatting, _, _>({
8602 let buffer_changes = buffer_changes.clone();
8603 move |_, _| {
8604 // When formatting is requested, trailing whitespace has already been stripped,
8605 // and the trailing newline has already been added.
8606 assert_eq!(
8607 &buffer_changes.lock()[1..],
8608 &[
8609 (
8610 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8611 "".into()
8612 ),
8613 (
8614 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8615 "".into()
8616 ),
8617 (
8618 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8619 "\n".into()
8620 ),
8621 ]
8622 );
8623
8624 // Insert blank lines between each line of the buffer.
8625 async move {
8626 Ok(Some(vec![
8627 lsp::TextEdit {
8628 range: lsp::Range::new(
8629 lsp::Position::new(1, 0),
8630 lsp::Position::new(1, 0),
8631 ),
8632 new_text: "\n".into(),
8633 },
8634 lsp::TextEdit {
8635 range: lsp::Range::new(
8636 lsp::Position::new(2, 0),
8637 lsp::Position::new(2, 0),
8638 ),
8639 new_text: "\n".into(),
8640 },
8641 ]))
8642 }
8643 }
8644 });
8645
8646 // After formatting the buffer, the trailing whitespace is stripped,
8647 // a newline is appended, and the edits provided by the language server
8648 // have been applied.
8649 format.await.unwrap();
8650 cx.assert_editor_state(
8651 &[
8652 "one", //
8653 "", //
8654 "twoˇ", //
8655 "", //
8656 "three", //
8657 "four", //
8658 "", //
8659 ]
8660 .join("\n"),
8661 );
8662
8663 // Undoing the formatting undoes the trailing whitespace removal, the
8664 // trailing newline, and the LSP edits.
8665 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8666 cx.assert_editor_state(
8667 &[
8668 "one ", //
8669 "twoˇ", //
8670 "three ", //
8671 "four", //
8672 ]
8673 .join("\n"),
8674 );
8675}
8676
8677#[gpui::test]
8678async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8679 cx: &mut TestAppContext,
8680) {
8681 init_test(cx, |_| {});
8682
8683 cx.update(|cx| {
8684 cx.update_global::<SettingsStore, _>(|settings, cx| {
8685 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8686 settings.auto_signature_help = Some(true);
8687 });
8688 });
8689 });
8690
8691 let mut cx = EditorLspTestContext::new_rust(
8692 lsp::ServerCapabilities {
8693 signature_help_provider: Some(lsp::SignatureHelpOptions {
8694 ..Default::default()
8695 }),
8696 ..Default::default()
8697 },
8698 cx,
8699 )
8700 .await;
8701
8702 let language = Language::new(
8703 LanguageConfig {
8704 name: "Rust".into(),
8705 brackets: BracketPairConfig {
8706 pairs: vec![
8707 BracketPair {
8708 start: "{".to_string(),
8709 end: "}".to_string(),
8710 close: true,
8711 surround: true,
8712 newline: true,
8713 },
8714 BracketPair {
8715 start: "(".to_string(),
8716 end: ")".to_string(),
8717 close: true,
8718 surround: true,
8719 newline: true,
8720 },
8721 BracketPair {
8722 start: "/*".to_string(),
8723 end: " */".to_string(),
8724 close: true,
8725 surround: true,
8726 newline: true,
8727 },
8728 BracketPair {
8729 start: "[".to_string(),
8730 end: "]".to_string(),
8731 close: false,
8732 surround: false,
8733 newline: true,
8734 },
8735 BracketPair {
8736 start: "\"".to_string(),
8737 end: "\"".to_string(),
8738 close: true,
8739 surround: true,
8740 newline: false,
8741 },
8742 BracketPair {
8743 start: "<".to_string(),
8744 end: ">".to_string(),
8745 close: false,
8746 surround: true,
8747 newline: true,
8748 },
8749 ],
8750 ..Default::default()
8751 },
8752 autoclose_before: "})]".to_string(),
8753 ..Default::default()
8754 },
8755 Some(tree_sitter_rust::LANGUAGE.into()),
8756 );
8757 let language = Arc::new(language);
8758
8759 cx.language_registry().add(language.clone());
8760 cx.update_buffer(|buffer, cx| {
8761 buffer.set_language(Some(language), cx);
8762 });
8763
8764 cx.set_state(
8765 &r#"
8766 fn main() {
8767 sampleˇ
8768 }
8769 "#
8770 .unindent(),
8771 );
8772
8773 cx.update_editor(|editor, window, cx| {
8774 editor.handle_input("(", window, cx);
8775 });
8776 cx.assert_editor_state(
8777 &"
8778 fn main() {
8779 sample(ˇ)
8780 }
8781 "
8782 .unindent(),
8783 );
8784
8785 let mocked_response = lsp::SignatureHelp {
8786 signatures: vec![lsp::SignatureInformation {
8787 label: "fn sample(param1: u8, param2: u8)".to_string(),
8788 documentation: None,
8789 parameters: Some(vec![
8790 lsp::ParameterInformation {
8791 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8792 documentation: None,
8793 },
8794 lsp::ParameterInformation {
8795 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8796 documentation: None,
8797 },
8798 ]),
8799 active_parameter: None,
8800 }],
8801 active_signature: Some(0),
8802 active_parameter: Some(0),
8803 };
8804 handle_signature_help_request(&mut cx, mocked_response).await;
8805
8806 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8807 .await;
8808
8809 cx.editor(|editor, _, _| {
8810 let signature_help_state = editor.signature_help_state.popover().cloned();
8811 assert_eq!(
8812 signature_help_state.unwrap().label,
8813 "param1: u8, param2: u8"
8814 );
8815 });
8816}
8817
8818#[gpui::test]
8819async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8820 init_test(cx, |_| {});
8821
8822 cx.update(|cx| {
8823 cx.update_global::<SettingsStore, _>(|settings, cx| {
8824 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8825 settings.auto_signature_help = Some(false);
8826 settings.show_signature_help_after_edits = Some(false);
8827 });
8828 });
8829 });
8830
8831 let mut cx = EditorLspTestContext::new_rust(
8832 lsp::ServerCapabilities {
8833 signature_help_provider: Some(lsp::SignatureHelpOptions {
8834 ..Default::default()
8835 }),
8836 ..Default::default()
8837 },
8838 cx,
8839 )
8840 .await;
8841
8842 let language = Language::new(
8843 LanguageConfig {
8844 name: "Rust".into(),
8845 brackets: BracketPairConfig {
8846 pairs: vec![
8847 BracketPair {
8848 start: "{".to_string(),
8849 end: "}".to_string(),
8850 close: true,
8851 surround: true,
8852 newline: true,
8853 },
8854 BracketPair {
8855 start: "(".to_string(),
8856 end: ")".to_string(),
8857 close: true,
8858 surround: true,
8859 newline: true,
8860 },
8861 BracketPair {
8862 start: "/*".to_string(),
8863 end: " */".to_string(),
8864 close: true,
8865 surround: true,
8866 newline: true,
8867 },
8868 BracketPair {
8869 start: "[".to_string(),
8870 end: "]".to_string(),
8871 close: false,
8872 surround: false,
8873 newline: true,
8874 },
8875 BracketPair {
8876 start: "\"".to_string(),
8877 end: "\"".to_string(),
8878 close: true,
8879 surround: true,
8880 newline: false,
8881 },
8882 BracketPair {
8883 start: "<".to_string(),
8884 end: ">".to_string(),
8885 close: false,
8886 surround: true,
8887 newline: true,
8888 },
8889 ],
8890 ..Default::default()
8891 },
8892 autoclose_before: "})]".to_string(),
8893 ..Default::default()
8894 },
8895 Some(tree_sitter_rust::LANGUAGE.into()),
8896 );
8897 let language = Arc::new(language);
8898
8899 cx.language_registry().add(language.clone());
8900 cx.update_buffer(|buffer, cx| {
8901 buffer.set_language(Some(language), cx);
8902 });
8903
8904 // Ensure that signature_help is not called when no signature help is enabled.
8905 cx.set_state(
8906 &r#"
8907 fn main() {
8908 sampleˇ
8909 }
8910 "#
8911 .unindent(),
8912 );
8913 cx.update_editor(|editor, window, cx| {
8914 editor.handle_input("(", window, cx);
8915 });
8916 cx.assert_editor_state(
8917 &"
8918 fn main() {
8919 sample(ˇ)
8920 }
8921 "
8922 .unindent(),
8923 );
8924 cx.editor(|editor, _, _| {
8925 assert!(editor.signature_help_state.task().is_none());
8926 });
8927
8928 let mocked_response = lsp::SignatureHelp {
8929 signatures: vec![lsp::SignatureInformation {
8930 label: "fn sample(param1: u8, param2: u8)".to_string(),
8931 documentation: None,
8932 parameters: Some(vec![
8933 lsp::ParameterInformation {
8934 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8935 documentation: None,
8936 },
8937 lsp::ParameterInformation {
8938 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8939 documentation: None,
8940 },
8941 ]),
8942 active_parameter: None,
8943 }],
8944 active_signature: Some(0),
8945 active_parameter: Some(0),
8946 };
8947
8948 // Ensure that signature_help is called when enabled afte edits
8949 cx.update(|_, cx| {
8950 cx.update_global::<SettingsStore, _>(|settings, cx| {
8951 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8952 settings.auto_signature_help = Some(false);
8953 settings.show_signature_help_after_edits = Some(true);
8954 });
8955 });
8956 });
8957 cx.set_state(
8958 &r#"
8959 fn main() {
8960 sampleˇ
8961 }
8962 "#
8963 .unindent(),
8964 );
8965 cx.update_editor(|editor, window, cx| {
8966 editor.handle_input("(", window, cx);
8967 });
8968 cx.assert_editor_state(
8969 &"
8970 fn main() {
8971 sample(ˇ)
8972 }
8973 "
8974 .unindent(),
8975 );
8976 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8977 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8978 .await;
8979 cx.update_editor(|editor, _, _| {
8980 let signature_help_state = editor.signature_help_state.popover().cloned();
8981 assert!(signature_help_state.is_some());
8982 assert_eq!(
8983 signature_help_state.unwrap().label,
8984 "param1: u8, param2: u8"
8985 );
8986 editor.signature_help_state = SignatureHelpState::default();
8987 });
8988
8989 // Ensure that signature_help is called when auto signature help override is enabled
8990 cx.update(|_, cx| {
8991 cx.update_global::<SettingsStore, _>(|settings, cx| {
8992 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8993 settings.auto_signature_help = Some(true);
8994 settings.show_signature_help_after_edits = Some(false);
8995 });
8996 });
8997 });
8998 cx.set_state(
8999 &r#"
9000 fn main() {
9001 sampleˇ
9002 }
9003 "#
9004 .unindent(),
9005 );
9006 cx.update_editor(|editor, window, cx| {
9007 editor.handle_input("(", window, cx);
9008 });
9009 cx.assert_editor_state(
9010 &"
9011 fn main() {
9012 sample(ˇ)
9013 }
9014 "
9015 .unindent(),
9016 );
9017 handle_signature_help_request(&mut cx, mocked_response).await;
9018 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9019 .await;
9020 cx.editor(|editor, _, _| {
9021 let signature_help_state = editor.signature_help_state.popover().cloned();
9022 assert!(signature_help_state.is_some());
9023 assert_eq!(
9024 signature_help_state.unwrap().label,
9025 "param1: u8, param2: u8"
9026 );
9027 });
9028}
9029
9030#[gpui::test]
9031async fn test_signature_help(cx: &mut TestAppContext) {
9032 init_test(cx, |_| {});
9033 cx.update(|cx| {
9034 cx.update_global::<SettingsStore, _>(|settings, cx| {
9035 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9036 settings.auto_signature_help = Some(true);
9037 });
9038 });
9039 });
9040
9041 let mut cx = EditorLspTestContext::new_rust(
9042 lsp::ServerCapabilities {
9043 signature_help_provider: Some(lsp::SignatureHelpOptions {
9044 ..Default::default()
9045 }),
9046 ..Default::default()
9047 },
9048 cx,
9049 )
9050 .await;
9051
9052 // A test that directly calls `show_signature_help`
9053 cx.update_editor(|editor, window, cx| {
9054 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9055 });
9056
9057 let mocked_response = lsp::SignatureHelp {
9058 signatures: vec![lsp::SignatureInformation {
9059 label: "fn sample(param1: u8, param2: u8)".to_string(),
9060 documentation: None,
9061 parameters: Some(vec![
9062 lsp::ParameterInformation {
9063 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9064 documentation: None,
9065 },
9066 lsp::ParameterInformation {
9067 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9068 documentation: None,
9069 },
9070 ]),
9071 active_parameter: None,
9072 }],
9073 active_signature: Some(0),
9074 active_parameter: Some(0),
9075 };
9076 handle_signature_help_request(&mut cx, mocked_response).await;
9077
9078 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9079 .await;
9080
9081 cx.editor(|editor, _, _| {
9082 let signature_help_state = editor.signature_help_state.popover().cloned();
9083 assert!(signature_help_state.is_some());
9084 assert_eq!(
9085 signature_help_state.unwrap().label,
9086 "param1: u8, param2: u8"
9087 );
9088 });
9089
9090 // When exiting outside from inside the brackets, `signature_help` is closed.
9091 cx.set_state(indoc! {"
9092 fn main() {
9093 sample(ˇ);
9094 }
9095
9096 fn sample(param1: u8, param2: u8) {}
9097 "});
9098
9099 cx.update_editor(|editor, window, cx| {
9100 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9101 });
9102
9103 let mocked_response = lsp::SignatureHelp {
9104 signatures: Vec::new(),
9105 active_signature: None,
9106 active_parameter: None,
9107 };
9108 handle_signature_help_request(&mut cx, mocked_response).await;
9109
9110 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9111 .await;
9112
9113 cx.editor(|editor, _, _| {
9114 assert!(!editor.signature_help_state.is_shown());
9115 });
9116
9117 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9118 cx.set_state(indoc! {"
9119 fn main() {
9120 sample(ˇ);
9121 }
9122
9123 fn sample(param1: u8, param2: u8) {}
9124 "});
9125
9126 let mocked_response = lsp::SignatureHelp {
9127 signatures: vec![lsp::SignatureInformation {
9128 label: "fn sample(param1: u8, param2: u8)".to_string(),
9129 documentation: None,
9130 parameters: Some(vec![
9131 lsp::ParameterInformation {
9132 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9133 documentation: None,
9134 },
9135 lsp::ParameterInformation {
9136 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9137 documentation: None,
9138 },
9139 ]),
9140 active_parameter: None,
9141 }],
9142 active_signature: Some(0),
9143 active_parameter: Some(0),
9144 };
9145 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9146 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9147 .await;
9148 cx.editor(|editor, _, _| {
9149 assert!(editor.signature_help_state.is_shown());
9150 });
9151
9152 // Restore the popover with more parameter input
9153 cx.set_state(indoc! {"
9154 fn main() {
9155 sample(param1, param2ˇ);
9156 }
9157
9158 fn sample(param1: u8, param2: u8) {}
9159 "});
9160
9161 let mocked_response = lsp::SignatureHelp {
9162 signatures: vec![lsp::SignatureInformation {
9163 label: "fn sample(param1: u8, param2: u8)".to_string(),
9164 documentation: None,
9165 parameters: Some(vec![
9166 lsp::ParameterInformation {
9167 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9168 documentation: None,
9169 },
9170 lsp::ParameterInformation {
9171 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9172 documentation: None,
9173 },
9174 ]),
9175 active_parameter: None,
9176 }],
9177 active_signature: Some(0),
9178 active_parameter: Some(1),
9179 };
9180 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9181 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9182 .await;
9183
9184 // When selecting a range, the popover is gone.
9185 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9186 cx.update_editor(|editor, window, cx| {
9187 editor.change_selections(None, window, cx, |s| {
9188 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9189 })
9190 });
9191 cx.assert_editor_state(indoc! {"
9192 fn main() {
9193 sample(param1, «ˇparam2»);
9194 }
9195
9196 fn sample(param1: u8, param2: u8) {}
9197 "});
9198 cx.editor(|editor, _, _| {
9199 assert!(!editor.signature_help_state.is_shown());
9200 });
9201
9202 // When unselecting again, the popover is back if within the brackets.
9203 cx.update_editor(|editor, window, cx| {
9204 editor.change_selections(None, window, cx, |s| {
9205 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9206 })
9207 });
9208 cx.assert_editor_state(indoc! {"
9209 fn main() {
9210 sample(param1, ˇparam2);
9211 }
9212
9213 fn sample(param1: u8, param2: u8) {}
9214 "});
9215 handle_signature_help_request(&mut cx, mocked_response).await;
9216 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9217 .await;
9218 cx.editor(|editor, _, _| {
9219 assert!(editor.signature_help_state.is_shown());
9220 });
9221
9222 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9223 cx.update_editor(|editor, window, cx| {
9224 editor.change_selections(None, window, cx, |s| {
9225 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9226 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9227 })
9228 });
9229 cx.assert_editor_state(indoc! {"
9230 fn main() {
9231 sample(param1, ˇparam2);
9232 }
9233
9234 fn sample(param1: u8, param2: u8) {}
9235 "});
9236
9237 let mocked_response = lsp::SignatureHelp {
9238 signatures: vec![lsp::SignatureInformation {
9239 label: "fn sample(param1: u8, param2: u8)".to_string(),
9240 documentation: None,
9241 parameters: Some(vec![
9242 lsp::ParameterInformation {
9243 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9244 documentation: None,
9245 },
9246 lsp::ParameterInformation {
9247 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9248 documentation: None,
9249 },
9250 ]),
9251 active_parameter: None,
9252 }],
9253 active_signature: Some(0),
9254 active_parameter: Some(1),
9255 };
9256 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9257 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9258 .await;
9259 cx.update_editor(|editor, _, cx| {
9260 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9261 });
9262 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9263 .await;
9264 cx.update_editor(|editor, window, cx| {
9265 editor.change_selections(None, window, cx, |s| {
9266 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9267 })
9268 });
9269 cx.assert_editor_state(indoc! {"
9270 fn main() {
9271 sample(param1, «ˇparam2»);
9272 }
9273
9274 fn sample(param1: u8, param2: u8) {}
9275 "});
9276 cx.update_editor(|editor, window, cx| {
9277 editor.change_selections(None, window, cx, |s| {
9278 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9279 })
9280 });
9281 cx.assert_editor_state(indoc! {"
9282 fn main() {
9283 sample(param1, ˇparam2);
9284 }
9285
9286 fn sample(param1: u8, param2: u8) {}
9287 "});
9288 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9289 .await;
9290}
9291
9292#[gpui::test]
9293async fn test_completion_mode(cx: &mut TestAppContext) {
9294 init_test(cx, |_| {});
9295 let mut cx = EditorLspTestContext::new_rust(
9296 lsp::ServerCapabilities {
9297 completion_provider: Some(lsp::CompletionOptions {
9298 resolve_provider: Some(true),
9299 ..Default::default()
9300 }),
9301 ..Default::default()
9302 },
9303 cx,
9304 )
9305 .await;
9306
9307 struct Run {
9308 run_description: &'static str,
9309 initial_state: String,
9310 buffer_marked_text: String,
9311 completion_text: &'static str,
9312 expected_with_insert_mode: String,
9313 expected_with_replace_mode: String,
9314 expected_with_replace_subsequence_mode: String,
9315 expected_with_replace_suffix_mode: String,
9316 }
9317
9318 let runs = [
9319 Run {
9320 run_description: "Start of word matches completion text",
9321 initial_state: "before ediˇ after".into(),
9322 buffer_marked_text: "before <edi|> after".into(),
9323 completion_text: "editor",
9324 expected_with_insert_mode: "before editorˇ after".into(),
9325 expected_with_replace_mode: "before editorˇ after".into(),
9326 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9327 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9328 },
9329 Run {
9330 run_description: "Accept same text at the middle of the word",
9331 initial_state: "before ediˇtor after".into(),
9332 buffer_marked_text: "before <edi|tor> after".into(),
9333 completion_text: "editor",
9334 expected_with_insert_mode: "before editorˇtor after".into(),
9335 expected_with_replace_mode: "before ediˇtor after".into(),
9336 expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
9337 expected_with_replace_suffix_mode: "before ediˇtor after".into(),
9338 },
9339 Run {
9340 run_description: "End of word matches completion text -- cursor at end",
9341 initial_state: "before torˇ after".into(),
9342 buffer_marked_text: "before <tor|> after".into(),
9343 completion_text: "editor",
9344 expected_with_insert_mode: "before editorˇ after".into(),
9345 expected_with_replace_mode: "before editorˇ after".into(),
9346 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9347 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9348 },
9349 Run {
9350 run_description: "End of word matches completion text -- cursor at start",
9351 initial_state: "before ˇtor after".into(),
9352 buffer_marked_text: "before <|tor> after".into(),
9353 completion_text: "editor",
9354 expected_with_insert_mode: "before editorˇtor after".into(),
9355 expected_with_replace_mode: "before editorˇ after".into(),
9356 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9357 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9358 },
9359 Run {
9360 run_description: "Prepend text containing whitespace",
9361 initial_state: "pˇfield: bool".into(),
9362 buffer_marked_text: "<p|field>: bool".into(),
9363 completion_text: "pub ",
9364 expected_with_insert_mode: "pub ˇfield: bool".into(),
9365 expected_with_replace_mode: "pub ˇ: bool".into(),
9366 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9367 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9368 },
9369 Run {
9370 run_description: "Add element to start of list",
9371 initial_state: "[element_ˇelement_2]".into(),
9372 buffer_marked_text: "[<element_|element_2>]".into(),
9373 completion_text: "element_1",
9374 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9375 expected_with_replace_mode: "[element_1ˇ]".into(),
9376 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9377 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9378 },
9379 Run {
9380 run_description: "Add element to start of list -- first and second elements are equal",
9381 initial_state: "[elˇelement]".into(),
9382 buffer_marked_text: "[<el|element>]".into(),
9383 completion_text: "element",
9384 expected_with_insert_mode: "[elementˇelement]".into(),
9385 expected_with_replace_mode: "[elˇement]".into(),
9386 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9387 expected_with_replace_suffix_mode: "[elˇement]".into(),
9388 },
9389 Run {
9390 run_description: "Ends with matching suffix",
9391 initial_state: "SubˇError".into(),
9392 buffer_marked_text: "<Sub|Error>".into(),
9393 completion_text: "SubscriptionError",
9394 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
9395 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9396 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9397 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9398 },
9399 Run {
9400 run_description: "Suffix is a subsequence -- contiguous",
9401 initial_state: "SubˇErr".into(),
9402 buffer_marked_text: "<Sub|Err>".into(),
9403 completion_text: "SubscriptionError",
9404 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
9405 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9406 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9407 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9408 },
9409 Run {
9410 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9411 initial_state: "Suˇscrirr".into(),
9412 buffer_marked_text: "<Su|scrirr>".into(),
9413 completion_text: "SubscriptionError",
9414 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
9415 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9416 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9417 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9418 },
9419 Run {
9420 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9421 initial_state: "foo(indˇix)".into(),
9422 buffer_marked_text: "foo(<ind|ix>)".into(),
9423 completion_text: "node_index",
9424 expected_with_insert_mode: "foo(node_indexˇix)".into(),
9425 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9426 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9427 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9428 },
9429 ];
9430
9431 for run in runs {
9432 let run_variations = [
9433 (LspInsertMode::Insert, run.expected_with_insert_mode),
9434 (LspInsertMode::Replace, run.expected_with_replace_mode),
9435 (
9436 LspInsertMode::ReplaceSubsequence,
9437 run.expected_with_replace_subsequence_mode,
9438 ),
9439 (
9440 LspInsertMode::ReplaceSuffix,
9441 run.expected_with_replace_suffix_mode,
9442 ),
9443 ];
9444
9445 for (lsp_insert_mode, expected_text) in run_variations {
9446 eprintln!(
9447 "run = {:?}, mode = {lsp_insert_mode:.?}",
9448 run.run_description,
9449 );
9450
9451 update_test_language_settings(&mut cx, |settings| {
9452 settings.defaults.completions = Some(CompletionSettings {
9453 lsp_insert_mode,
9454 words: WordsCompletionMode::Disabled,
9455 lsp: true,
9456 lsp_fetch_timeout_ms: 0,
9457 });
9458 });
9459
9460 cx.set_state(&run.initial_state);
9461 cx.update_editor(|editor, window, cx| {
9462 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9463 });
9464
9465 let counter = Arc::new(AtomicUsize::new(0));
9466 handle_completion_request_with_insert_and_replace(
9467 &mut cx,
9468 &run.buffer_marked_text,
9469 vec![run.completion_text],
9470 counter.clone(),
9471 )
9472 .await;
9473 cx.condition(|editor, _| editor.context_menu_visible())
9474 .await;
9475 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9476
9477 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9478 editor
9479 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9480 .unwrap()
9481 });
9482 cx.assert_editor_state(&expected_text);
9483 handle_resolve_completion_request(&mut cx, None).await;
9484 apply_additional_edits.await.unwrap();
9485 }
9486 }
9487}
9488
9489#[gpui::test]
9490async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
9491 init_test(cx, |_| {});
9492 let mut cx = EditorLspTestContext::new_rust(
9493 lsp::ServerCapabilities {
9494 completion_provider: Some(lsp::CompletionOptions {
9495 resolve_provider: Some(true),
9496 ..Default::default()
9497 }),
9498 ..Default::default()
9499 },
9500 cx,
9501 )
9502 .await;
9503
9504 let initial_state = "SubˇError";
9505 let buffer_marked_text = "<Sub|Error>";
9506 let completion_text = "SubscriptionError";
9507 let expected_with_insert_mode = "SubscriptionErrorˇError";
9508 let expected_with_replace_mode = "SubscriptionErrorˇ";
9509
9510 update_test_language_settings(&mut cx, |settings| {
9511 settings.defaults.completions = Some(CompletionSettings {
9512 words: WordsCompletionMode::Disabled,
9513 // set the opposite here to ensure that the action is overriding the default behavior
9514 lsp_insert_mode: LspInsertMode::Insert,
9515 lsp: true,
9516 lsp_fetch_timeout_ms: 0,
9517 });
9518 });
9519
9520 cx.set_state(initial_state);
9521 cx.update_editor(|editor, window, cx| {
9522 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9523 });
9524
9525 let counter = Arc::new(AtomicUsize::new(0));
9526 handle_completion_request_with_insert_and_replace(
9527 &mut cx,
9528 &buffer_marked_text,
9529 vec![completion_text],
9530 counter.clone(),
9531 )
9532 .await;
9533 cx.condition(|editor, _| editor.context_menu_visible())
9534 .await;
9535 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9536
9537 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9538 editor
9539 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
9540 .unwrap()
9541 });
9542 cx.assert_editor_state(&expected_with_replace_mode);
9543 handle_resolve_completion_request(&mut cx, None).await;
9544 apply_additional_edits.await.unwrap();
9545
9546 update_test_language_settings(&mut cx, |settings| {
9547 settings.defaults.completions = Some(CompletionSettings {
9548 words: WordsCompletionMode::Disabled,
9549 // set the opposite here to ensure that the action is overriding the default behavior
9550 lsp_insert_mode: LspInsertMode::Replace,
9551 lsp: true,
9552 lsp_fetch_timeout_ms: 0,
9553 });
9554 });
9555
9556 cx.set_state(initial_state);
9557 cx.update_editor(|editor, window, cx| {
9558 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9559 });
9560 handle_completion_request_with_insert_and_replace(
9561 &mut cx,
9562 &buffer_marked_text,
9563 vec![completion_text],
9564 counter.clone(),
9565 )
9566 .await;
9567 cx.condition(|editor, _| editor.context_menu_visible())
9568 .await;
9569 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9570
9571 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9572 editor
9573 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
9574 .unwrap()
9575 });
9576 cx.assert_editor_state(&expected_with_insert_mode);
9577 handle_resolve_completion_request(&mut cx, None).await;
9578 apply_additional_edits.await.unwrap();
9579}
9580
9581#[gpui::test]
9582async fn test_completion(cx: &mut TestAppContext) {
9583 init_test(cx, |_| {});
9584
9585 let mut cx = EditorLspTestContext::new_rust(
9586 lsp::ServerCapabilities {
9587 completion_provider: Some(lsp::CompletionOptions {
9588 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9589 resolve_provider: Some(true),
9590 ..Default::default()
9591 }),
9592 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9593 ..Default::default()
9594 },
9595 cx,
9596 )
9597 .await;
9598 let counter = Arc::new(AtomicUsize::new(0));
9599
9600 cx.set_state(indoc! {"
9601 oneˇ
9602 two
9603 three
9604 "});
9605 cx.simulate_keystroke(".");
9606 handle_completion_request(
9607 &mut cx,
9608 indoc! {"
9609 one.|<>
9610 two
9611 three
9612 "},
9613 vec!["first_completion", "second_completion"],
9614 counter.clone(),
9615 )
9616 .await;
9617 cx.condition(|editor, _| editor.context_menu_visible())
9618 .await;
9619 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9620
9621 let _handler = handle_signature_help_request(
9622 &mut cx,
9623 lsp::SignatureHelp {
9624 signatures: vec![lsp::SignatureInformation {
9625 label: "test signature".to_string(),
9626 documentation: None,
9627 parameters: Some(vec![lsp::ParameterInformation {
9628 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9629 documentation: None,
9630 }]),
9631 active_parameter: None,
9632 }],
9633 active_signature: None,
9634 active_parameter: None,
9635 },
9636 );
9637 cx.update_editor(|editor, window, cx| {
9638 assert!(
9639 !editor.signature_help_state.is_shown(),
9640 "No signature help was called for"
9641 );
9642 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9643 });
9644 cx.run_until_parked();
9645 cx.update_editor(|editor, _, _| {
9646 assert!(
9647 !editor.signature_help_state.is_shown(),
9648 "No signature help should be shown when completions menu is open"
9649 );
9650 });
9651
9652 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9653 editor.context_menu_next(&Default::default(), window, cx);
9654 editor
9655 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9656 .unwrap()
9657 });
9658 cx.assert_editor_state(indoc! {"
9659 one.second_completionˇ
9660 two
9661 three
9662 "});
9663
9664 handle_resolve_completion_request(
9665 &mut cx,
9666 Some(vec![
9667 (
9668 //This overlaps with the primary completion edit which is
9669 //misbehavior from the LSP spec, test that we filter it out
9670 indoc! {"
9671 one.second_ˇcompletion
9672 two
9673 threeˇ
9674 "},
9675 "overlapping additional edit",
9676 ),
9677 (
9678 indoc! {"
9679 one.second_completion
9680 two
9681 threeˇ
9682 "},
9683 "\nadditional edit",
9684 ),
9685 ]),
9686 )
9687 .await;
9688 apply_additional_edits.await.unwrap();
9689 cx.assert_editor_state(indoc! {"
9690 one.second_completionˇ
9691 two
9692 three
9693 additional edit
9694 "});
9695
9696 cx.set_state(indoc! {"
9697 one.second_completion
9698 twoˇ
9699 threeˇ
9700 additional edit
9701 "});
9702 cx.simulate_keystroke(" ");
9703 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9704 cx.simulate_keystroke("s");
9705 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9706
9707 cx.assert_editor_state(indoc! {"
9708 one.second_completion
9709 two sˇ
9710 three sˇ
9711 additional edit
9712 "});
9713 handle_completion_request(
9714 &mut cx,
9715 indoc! {"
9716 one.second_completion
9717 two s
9718 three <s|>
9719 additional edit
9720 "},
9721 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9722 counter.clone(),
9723 )
9724 .await;
9725 cx.condition(|editor, _| editor.context_menu_visible())
9726 .await;
9727 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9728
9729 cx.simulate_keystroke("i");
9730
9731 handle_completion_request(
9732 &mut cx,
9733 indoc! {"
9734 one.second_completion
9735 two si
9736 three <si|>
9737 additional edit
9738 "},
9739 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9740 counter.clone(),
9741 )
9742 .await;
9743 cx.condition(|editor, _| editor.context_menu_visible())
9744 .await;
9745 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9746
9747 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9748 editor
9749 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9750 .unwrap()
9751 });
9752 cx.assert_editor_state(indoc! {"
9753 one.second_completion
9754 two sixth_completionˇ
9755 three sixth_completionˇ
9756 additional edit
9757 "});
9758
9759 apply_additional_edits.await.unwrap();
9760
9761 update_test_language_settings(&mut cx, |settings| {
9762 settings.defaults.show_completions_on_input = Some(false);
9763 });
9764 cx.set_state("editorˇ");
9765 cx.simulate_keystroke(".");
9766 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9767 cx.simulate_keystrokes("c l o");
9768 cx.assert_editor_state("editor.cloˇ");
9769 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9770 cx.update_editor(|editor, window, cx| {
9771 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9772 });
9773 handle_completion_request(
9774 &mut cx,
9775 "editor.<clo|>",
9776 vec!["close", "clobber"],
9777 counter.clone(),
9778 )
9779 .await;
9780 cx.condition(|editor, _| editor.context_menu_visible())
9781 .await;
9782 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9783
9784 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9785 editor
9786 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9787 .unwrap()
9788 });
9789 cx.assert_editor_state("editor.closeˇ");
9790 handle_resolve_completion_request(&mut cx, None).await;
9791 apply_additional_edits.await.unwrap();
9792}
9793
9794#[gpui::test]
9795async fn test_word_completion(cx: &mut TestAppContext) {
9796 let lsp_fetch_timeout_ms = 10;
9797 init_test(cx, |language_settings| {
9798 language_settings.defaults.completions = Some(CompletionSettings {
9799 words: WordsCompletionMode::Fallback,
9800 lsp: true,
9801 lsp_fetch_timeout_ms: 10,
9802 lsp_insert_mode: LspInsertMode::Insert,
9803 });
9804 });
9805
9806 let mut cx = EditorLspTestContext::new_rust(
9807 lsp::ServerCapabilities {
9808 completion_provider: Some(lsp::CompletionOptions {
9809 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9810 ..lsp::CompletionOptions::default()
9811 }),
9812 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9813 ..lsp::ServerCapabilities::default()
9814 },
9815 cx,
9816 )
9817 .await;
9818
9819 let throttle_completions = Arc::new(AtomicBool::new(false));
9820
9821 let lsp_throttle_completions = throttle_completions.clone();
9822 let _completion_requests_handler =
9823 cx.lsp
9824 .server
9825 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9826 let lsp_throttle_completions = lsp_throttle_completions.clone();
9827 let cx = cx.clone();
9828 async move {
9829 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9830 cx.background_executor()
9831 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9832 .await;
9833 }
9834 Ok(Some(lsp::CompletionResponse::Array(vec![
9835 lsp::CompletionItem {
9836 label: "first".into(),
9837 ..lsp::CompletionItem::default()
9838 },
9839 lsp::CompletionItem {
9840 label: "last".into(),
9841 ..lsp::CompletionItem::default()
9842 },
9843 ])))
9844 }
9845 });
9846
9847 cx.set_state(indoc! {"
9848 oneˇ
9849 two
9850 three
9851 "});
9852 cx.simulate_keystroke(".");
9853 cx.executor().run_until_parked();
9854 cx.condition(|editor, _| editor.context_menu_visible())
9855 .await;
9856 cx.update_editor(|editor, window, cx| {
9857 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9858 {
9859 assert_eq!(
9860 completion_menu_entries(&menu),
9861 &["first", "last"],
9862 "When LSP server is fast to reply, no fallback word completions are used"
9863 );
9864 } else {
9865 panic!("expected completion menu to be open");
9866 }
9867 editor.cancel(&Cancel, window, cx);
9868 });
9869 cx.executor().run_until_parked();
9870 cx.condition(|editor, _| !editor.context_menu_visible())
9871 .await;
9872
9873 throttle_completions.store(true, atomic::Ordering::Release);
9874 cx.simulate_keystroke(".");
9875 cx.executor()
9876 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9877 cx.executor().run_until_parked();
9878 cx.condition(|editor, _| editor.context_menu_visible())
9879 .await;
9880 cx.update_editor(|editor, _, _| {
9881 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9882 {
9883 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9884 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9885 } else {
9886 panic!("expected completion menu to be open");
9887 }
9888 });
9889}
9890
9891#[gpui::test]
9892async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9893 init_test(cx, |language_settings| {
9894 language_settings.defaults.completions = Some(CompletionSettings {
9895 words: WordsCompletionMode::Enabled,
9896 lsp: true,
9897 lsp_fetch_timeout_ms: 0,
9898 lsp_insert_mode: LspInsertMode::Insert,
9899 });
9900 });
9901
9902 let mut cx = EditorLspTestContext::new_rust(
9903 lsp::ServerCapabilities {
9904 completion_provider: Some(lsp::CompletionOptions {
9905 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9906 ..lsp::CompletionOptions::default()
9907 }),
9908 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9909 ..lsp::ServerCapabilities::default()
9910 },
9911 cx,
9912 )
9913 .await;
9914
9915 let _completion_requests_handler =
9916 cx.lsp
9917 .server
9918 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9919 Ok(Some(lsp::CompletionResponse::Array(vec![
9920 lsp::CompletionItem {
9921 label: "first".into(),
9922 ..lsp::CompletionItem::default()
9923 },
9924 lsp::CompletionItem {
9925 label: "last".into(),
9926 ..lsp::CompletionItem::default()
9927 },
9928 ])))
9929 });
9930
9931 cx.set_state(indoc! {"ˇ
9932 first
9933 last
9934 second
9935 "});
9936 cx.simulate_keystroke(".");
9937 cx.executor().run_until_parked();
9938 cx.condition(|editor, _| editor.context_menu_visible())
9939 .await;
9940 cx.update_editor(|editor, _, _| {
9941 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9942 {
9943 assert_eq!(
9944 completion_menu_entries(&menu),
9945 &["first", "last", "second"],
9946 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9947 );
9948 } else {
9949 panic!("expected completion menu to be open");
9950 }
9951 });
9952}
9953
9954#[gpui::test]
9955async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9956 init_test(cx, |language_settings| {
9957 language_settings.defaults.completions = Some(CompletionSettings {
9958 words: WordsCompletionMode::Disabled,
9959 lsp: true,
9960 lsp_fetch_timeout_ms: 0,
9961 lsp_insert_mode: LspInsertMode::Insert,
9962 });
9963 });
9964
9965 let mut cx = EditorLspTestContext::new_rust(
9966 lsp::ServerCapabilities {
9967 completion_provider: Some(lsp::CompletionOptions {
9968 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9969 ..lsp::CompletionOptions::default()
9970 }),
9971 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9972 ..lsp::ServerCapabilities::default()
9973 },
9974 cx,
9975 )
9976 .await;
9977
9978 let _completion_requests_handler =
9979 cx.lsp
9980 .server
9981 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9982 panic!("LSP completions should not be queried when dealing with word completions")
9983 });
9984
9985 cx.set_state(indoc! {"ˇ
9986 first
9987 last
9988 second
9989 "});
9990 cx.update_editor(|editor, window, cx| {
9991 editor.show_word_completions(&ShowWordCompletions, window, cx);
9992 });
9993 cx.executor().run_until_parked();
9994 cx.condition(|editor, _| editor.context_menu_visible())
9995 .await;
9996 cx.update_editor(|editor, _, _| {
9997 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9998 {
9999 assert_eq!(
10000 completion_menu_entries(&menu),
10001 &["first", "last", "second"],
10002 "`ShowWordCompletions` action should show word completions"
10003 );
10004 } else {
10005 panic!("expected completion menu to be open");
10006 }
10007 });
10008
10009 cx.simulate_keystroke("l");
10010 cx.executor().run_until_parked();
10011 cx.condition(|editor, _| editor.context_menu_visible())
10012 .await;
10013 cx.update_editor(|editor, _, _| {
10014 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10015 {
10016 assert_eq!(
10017 completion_menu_entries(&menu),
10018 &["last"],
10019 "After showing word completions, further editing should filter them and not query the LSP"
10020 );
10021 } else {
10022 panic!("expected completion menu to be open");
10023 }
10024 });
10025}
10026
10027#[gpui::test]
10028async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
10029 init_test(cx, |language_settings| {
10030 language_settings.defaults.completions = Some(CompletionSettings {
10031 words: WordsCompletionMode::Fallback,
10032 lsp: false,
10033 lsp_fetch_timeout_ms: 0,
10034 lsp_insert_mode: LspInsertMode::Insert,
10035 });
10036 });
10037
10038 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
10039
10040 cx.set_state(indoc! {"ˇ
10041 0_usize
10042 let
10043 33
10044 4.5f32
10045 "});
10046 cx.update_editor(|editor, window, cx| {
10047 editor.show_completions(&ShowCompletions::default(), window, cx);
10048 });
10049 cx.executor().run_until_parked();
10050 cx.condition(|editor, _| editor.context_menu_visible())
10051 .await;
10052 cx.update_editor(|editor, window, cx| {
10053 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10054 {
10055 assert_eq!(
10056 completion_menu_entries(&menu),
10057 &["let"],
10058 "With no digits in the completion query, no digits should be in the word completions"
10059 );
10060 } else {
10061 panic!("expected completion menu to be open");
10062 }
10063 editor.cancel(&Cancel, window, cx);
10064 });
10065
10066 cx.set_state(indoc! {"3ˇ
10067 0_usize
10068 let
10069 3
10070 33.35f32
10071 "});
10072 cx.update_editor(|editor, window, cx| {
10073 editor.show_completions(&ShowCompletions::default(), window, cx);
10074 });
10075 cx.executor().run_until_parked();
10076 cx.condition(|editor, _| editor.context_menu_visible())
10077 .await;
10078 cx.update_editor(|editor, _, _| {
10079 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10080 {
10081 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
10082 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
10083 } else {
10084 panic!("expected completion menu to be open");
10085 }
10086 });
10087}
10088
10089fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
10090 let position = || lsp::Position {
10091 line: params.text_document_position.position.line,
10092 character: params.text_document_position.position.character,
10093 };
10094 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10095 range: lsp::Range {
10096 start: position(),
10097 end: position(),
10098 },
10099 new_text: text.to_string(),
10100 }))
10101}
10102
10103#[gpui::test]
10104async fn test_multiline_completion(cx: &mut TestAppContext) {
10105 init_test(cx, |_| {});
10106
10107 let fs = FakeFs::new(cx.executor());
10108 fs.insert_tree(
10109 path!("/a"),
10110 json!({
10111 "main.ts": "a",
10112 }),
10113 )
10114 .await;
10115
10116 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10117 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10118 let typescript_language = Arc::new(Language::new(
10119 LanguageConfig {
10120 name: "TypeScript".into(),
10121 matcher: LanguageMatcher {
10122 path_suffixes: vec!["ts".to_string()],
10123 ..LanguageMatcher::default()
10124 },
10125 line_comments: vec!["// ".into()],
10126 ..LanguageConfig::default()
10127 },
10128 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10129 ));
10130 language_registry.add(typescript_language.clone());
10131 let mut fake_servers = language_registry.register_fake_lsp(
10132 "TypeScript",
10133 FakeLspAdapter {
10134 capabilities: lsp::ServerCapabilities {
10135 completion_provider: Some(lsp::CompletionOptions {
10136 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10137 ..lsp::CompletionOptions::default()
10138 }),
10139 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10140 ..lsp::ServerCapabilities::default()
10141 },
10142 // Emulate vtsls label generation
10143 label_for_completion: Some(Box::new(|item, _| {
10144 let text = if let Some(description) = item
10145 .label_details
10146 .as_ref()
10147 .and_then(|label_details| label_details.description.as_ref())
10148 {
10149 format!("{} {}", item.label, description)
10150 } else if let Some(detail) = &item.detail {
10151 format!("{} {}", item.label, detail)
10152 } else {
10153 item.label.clone()
10154 };
10155 let len = text.len();
10156 Some(language::CodeLabel {
10157 text,
10158 runs: Vec::new(),
10159 filter_range: 0..len,
10160 })
10161 })),
10162 ..FakeLspAdapter::default()
10163 },
10164 );
10165 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10166 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10167 let worktree_id = workspace
10168 .update(cx, |workspace, _window, cx| {
10169 workspace.project().update(cx, |project, cx| {
10170 project.worktrees(cx).next().unwrap().read(cx).id()
10171 })
10172 })
10173 .unwrap();
10174 let _buffer = project
10175 .update(cx, |project, cx| {
10176 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
10177 })
10178 .await
10179 .unwrap();
10180 let editor = workspace
10181 .update(cx, |workspace, window, cx| {
10182 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10183 })
10184 .unwrap()
10185 .await
10186 .unwrap()
10187 .downcast::<Editor>()
10188 .unwrap();
10189 let fake_server = fake_servers.next().await.unwrap();
10190
10191 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10192 let multiline_label_2 = "a\nb\nc\n";
10193 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10194 let multiline_description = "d\ne\nf\n";
10195 let multiline_detail_2 = "g\nh\ni\n";
10196
10197 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10198 move |params, _| async move {
10199 Ok(Some(lsp::CompletionResponse::Array(vec![
10200 lsp::CompletionItem {
10201 label: multiline_label.to_string(),
10202 text_edit: gen_text_edit(¶ms, "new_text_1"),
10203 ..lsp::CompletionItem::default()
10204 },
10205 lsp::CompletionItem {
10206 label: "single line label 1".to_string(),
10207 detail: Some(multiline_detail.to_string()),
10208 text_edit: gen_text_edit(¶ms, "new_text_2"),
10209 ..lsp::CompletionItem::default()
10210 },
10211 lsp::CompletionItem {
10212 label: "single line label 2".to_string(),
10213 label_details: Some(lsp::CompletionItemLabelDetails {
10214 description: Some(multiline_description.to_string()),
10215 detail: None,
10216 }),
10217 text_edit: gen_text_edit(¶ms, "new_text_2"),
10218 ..lsp::CompletionItem::default()
10219 },
10220 lsp::CompletionItem {
10221 label: multiline_label_2.to_string(),
10222 detail: Some(multiline_detail_2.to_string()),
10223 text_edit: gen_text_edit(¶ms, "new_text_3"),
10224 ..lsp::CompletionItem::default()
10225 },
10226 lsp::CompletionItem {
10227 label: "Label with many spaces and \t but without newlines".to_string(),
10228 detail: Some(
10229 "Details with many spaces and \t but without newlines".to_string(),
10230 ),
10231 text_edit: gen_text_edit(¶ms, "new_text_4"),
10232 ..lsp::CompletionItem::default()
10233 },
10234 ])))
10235 },
10236 );
10237
10238 editor.update_in(cx, |editor, window, cx| {
10239 cx.focus_self(window);
10240 editor.move_to_end(&MoveToEnd, window, cx);
10241 editor.handle_input(".", window, cx);
10242 });
10243 cx.run_until_parked();
10244 completion_handle.next().await.unwrap();
10245
10246 editor.update(cx, |editor, _| {
10247 assert!(editor.context_menu_visible());
10248 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10249 {
10250 let completion_labels = menu
10251 .completions
10252 .borrow()
10253 .iter()
10254 .map(|c| c.label.text.clone())
10255 .collect::<Vec<_>>();
10256 assert_eq!(
10257 completion_labels,
10258 &[
10259 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10260 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10261 "single line label 2 d e f ",
10262 "a b c g h i ",
10263 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
10264 ],
10265 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
10266 );
10267
10268 for completion in menu
10269 .completions
10270 .borrow()
10271 .iter() {
10272 assert_eq!(
10273 completion.label.filter_range,
10274 0..completion.label.text.len(),
10275 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
10276 );
10277 }
10278 } else {
10279 panic!("expected completion menu to be open");
10280 }
10281 });
10282}
10283
10284#[gpui::test]
10285async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
10286 init_test(cx, |_| {});
10287 let mut cx = EditorLspTestContext::new_rust(
10288 lsp::ServerCapabilities {
10289 completion_provider: Some(lsp::CompletionOptions {
10290 trigger_characters: Some(vec![".".to_string()]),
10291 ..Default::default()
10292 }),
10293 ..Default::default()
10294 },
10295 cx,
10296 )
10297 .await;
10298 cx.lsp
10299 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10300 Ok(Some(lsp::CompletionResponse::Array(vec![
10301 lsp::CompletionItem {
10302 label: "first".into(),
10303 ..Default::default()
10304 },
10305 lsp::CompletionItem {
10306 label: "last".into(),
10307 ..Default::default()
10308 },
10309 ])))
10310 });
10311 cx.set_state("variableˇ");
10312 cx.simulate_keystroke(".");
10313 cx.executor().run_until_parked();
10314
10315 cx.update_editor(|editor, _, _| {
10316 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10317 {
10318 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
10319 } else {
10320 panic!("expected completion menu to be open");
10321 }
10322 });
10323
10324 cx.update_editor(|editor, window, cx| {
10325 editor.move_page_down(&MovePageDown::default(), window, cx);
10326 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10327 {
10328 assert!(
10329 menu.selected_item == 1,
10330 "expected PageDown to select the last item from the context menu"
10331 );
10332 } else {
10333 panic!("expected completion menu to stay open after PageDown");
10334 }
10335 });
10336
10337 cx.update_editor(|editor, window, cx| {
10338 editor.move_page_up(&MovePageUp::default(), window, cx);
10339 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10340 {
10341 assert!(
10342 menu.selected_item == 0,
10343 "expected PageUp to select the first item from the context menu"
10344 );
10345 } else {
10346 panic!("expected completion menu to stay open after PageUp");
10347 }
10348 });
10349}
10350
10351#[gpui::test]
10352async fn test_completion_sort(cx: &mut TestAppContext) {
10353 init_test(cx, |_| {});
10354 let mut cx = EditorLspTestContext::new_rust(
10355 lsp::ServerCapabilities {
10356 completion_provider: Some(lsp::CompletionOptions {
10357 trigger_characters: Some(vec![".".to_string()]),
10358 ..Default::default()
10359 }),
10360 ..Default::default()
10361 },
10362 cx,
10363 )
10364 .await;
10365 cx.lsp
10366 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10367 Ok(Some(lsp::CompletionResponse::Array(vec![
10368 lsp::CompletionItem {
10369 label: "Range".into(),
10370 sort_text: Some("a".into()),
10371 ..Default::default()
10372 },
10373 lsp::CompletionItem {
10374 label: "r".into(),
10375 sort_text: Some("b".into()),
10376 ..Default::default()
10377 },
10378 lsp::CompletionItem {
10379 label: "ret".into(),
10380 sort_text: Some("c".into()),
10381 ..Default::default()
10382 },
10383 lsp::CompletionItem {
10384 label: "return".into(),
10385 sort_text: Some("d".into()),
10386 ..Default::default()
10387 },
10388 lsp::CompletionItem {
10389 label: "slice".into(),
10390 sort_text: Some("d".into()),
10391 ..Default::default()
10392 },
10393 ])))
10394 });
10395 cx.set_state("rˇ");
10396 cx.executor().run_until_parked();
10397 cx.update_editor(|editor, window, cx| {
10398 editor.show_completions(
10399 &ShowCompletions {
10400 trigger: Some("r".into()),
10401 },
10402 window,
10403 cx,
10404 );
10405 });
10406 cx.executor().run_until_parked();
10407
10408 cx.update_editor(|editor, _, _| {
10409 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10410 {
10411 assert_eq!(
10412 completion_menu_entries(&menu),
10413 &["r", "ret", "Range", "return"]
10414 );
10415 } else {
10416 panic!("expected completion menu to be open");
10417 }
10418 });
10419}
10420
10421#[gpui::test]
10422async fn test_as_is_completions(cx: &mut TestAppContext) {
10423 init_test(cx, |_| {});
10424 let mut cx = EditorLspTestContext::new_rust(
10425 lsp::ServerCapabilities {
10426 completion_provider: Some(lsp::CompletionOptions {
10427 ..Default::default()
10428 }),
10429 ..Default::default()
10430 },
10431 cx,
10432 )
10433 .await;
10434 cx.lsp
10435 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10436 Ok(Some(lsp::CompletionResponse::Array(vec![
10437 lsp::CompletionItem {
10438 label: "unsafe".into(),
10439 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10440 range: lsp::Range {
10441 start: lsp::Position {
10442 line: 1,
10443 character: 2,
10444 },
10445 end: lsp::Position {
10446 line: 1,
10447 character: 3,
10448 },
10449 },
10450 new_text: "unsafe".to_string(),
10451 })),
10452 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
10453 ..Default::default()
10454 },
10455 ])))
10456 });
10457 cx.set_state("fn a() {}\n nˇ");
10458 cx.executor().run_until_parked();
10459 cx.update_editor(|editor, window, cx| {
10460 editor.show_completions(
10461 &ShowCompletions {
10462 trigger: Some("\n".into()),
10463 },
10464 window,
10465 cx,
10466 );
10467 });
10468 cx.executor().run_until_parked();
10469
10470 cx.update_editor(|editor, window, cx| {
10471 editor.confirm_completion(&Default::default(), window, cx)
10472 });
10473 cx.executor().run_until_parked();
10474 cx.assert_editor_state("fn a() {}\n unsafeˇ");
10475}
10476
10477#[gpui::test]
10478async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10479 init_test(cx, |_| {});
10480
10481 let mut cx = EditorLspTestContext::new_rust(
10482 lsp::ServerCapabilities {
10483 completion_provider: Some(lsp::CompletionOptions {
10484 trigger_characters: Some(vec![".".to_string()]),
10485 resolve_provider: Some(true),
10486 ..Default::default()
10487 }),
10488 ..Default::default()
10489 },
10490 cx,
10491 )
10492 .await;
10493
10494 cx.set_state("fn main() { let a = 2ˇ; }");
10495 cx.simulate_keystroke(".");
10496 let completion_item = lsp::CompletionItem {
10497 label: "Some".into(),
10498 kind: Some(lsp::CompletionItemKind::SNIPPET),
10499 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10500 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10501 kind: lsp::MarkupKind::Markdown,
10502 value: "```rust\nSome(2)\n```".to_string(),
10503 })),
10504 deprecated: Some(false),
10505 sort_text: Some("Some".to_string()),
10506 filter_text: Some("Some".to_string()),
10507 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10508 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10509 range: lsp::Range {
10510 start: lsp::Position {
10511 line: 0,
10512 character: 22,
10513 },
10514 end: lsp::Position {
10515 line: 0,
10516 character: 22,
10517 },
10518 },
10519 new_text: "Some(2)".to_string(),
10520 })),
10521 additional_text_edits: Some(vec![lsp::TextEdit {
10522 range: lsp::Range {
10523 start: lsp::Position {
10524 line: 0,
10525 character: 20,
10526 },
10527 end: lsp::Position {
10528 line: 0,
10529 character: 22,
10530 },
10531 },
10532 new_text: "".to_string(),
10533 }]),
10534 ..Default::default()
10535 };
10536
10537 let closure_completion_item = completion_item.clone();
10538 let counter = Arc::new(AtomicUsize::new(0));
10539 let counter_clone = counter.clone();
10540 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10541 let task_completion_item = closure_completion_item.clone();
10542 counter_clone.fetch_add(1, atomic::Ordering::Release);
10543 async move {
10544 Ok(Some(lsp::CompletionResponse::Array(vec![
10545 task_completion_item,
10546 ])))
10547 }
10548 });
10549
10550 cx.condition(|editor, _| editor.context_menu_visible())
10551 .await;
10552 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10553 assert!(request.next().await.is_some());
10554 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10555
10556 cx.simulate_keystrokes("S o m");
10557 cx.condition(|editor, _| editor.context_menu_visible())
10558 .await;
10559 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10560 assert!(request.next().await.is_some());
10561 assert!(request.next().await.is_some());
10562 assert!(request.next().await.is_some());
10563 request.close();
10564 assert!(request.next().await.is_none());
10565 assert_eq!(
10566 counter.load(atomic::Ordering::Acquire),
10567 4,
10568 "With the completions menu open, only one LSP request should happen per input"
10569 );
10570}
10571
10572#[gpui::test]
10573async fn test_toggle_comment(cx: &mut TestAppContext) {
10574 init_test(cx, |_| {});
10575 let mut cx = EditorTestContext::new(cx).await;
10576 let language = Arc::new(Language::new(
10577 LanguageConfig {
10578 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10579 ..Default::default()
10580 },
10581 Some(tree_sitter_rust::LANGUAGE.into()),
10582 ));
10583 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10584
10585 // If multiple selections intersect a line, the line is only toggled once.
10586 cx.set_state(indoc! {"
10587 fn a() {
10588 «//b();
10589 ˇ»// «c();
10590 //ˇ» d();
10591 }
10592 "});
10593
10594 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10595
10596 cx.assert_editor_state(indoc! {"
10597 fn a() {
10598 «b();
10599 c();
10600 ˇ» d();
10601 }
10602 "});
10603
10604 // The comment prefix is inserted at the same column for every line in a
10605 // selection.
10606 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10607
10608 cx.assert_editor_state(indoc! {"
10609 fn a() {
10610 // «b();
10611 // c();
10612 ˇ»// d();
10613 }
10614 "});
10615
10616 // If a selection ends at the beginning of a line, that line is not toggled.
10617 cx.set_selections_state(indoc! {"
10618 fn a() {
10619 // b();
10620 «// c();
10621 ˇ» // d();
10622 }
10623 "});
10624
10625 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10626
10627 cx.assert_editor_state(indoc! {"
10628 fn a() {
10629 // b();
10630 «c();
10631 ˇ» // d();
10632 }
10633 "});
10634
10635 // If a selection span a single line and is empty, the line is toggled.
10636 cx.set_state(indoc! {"
10637 fn a() {
10638 a();
10639 b();
10640 ˇ
10641 }
10642 "});
10643
10644 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10645
10646 cx.assert_editor_state(indoc! {"
10647 fn a() {
10648 a();
10649 b();
10650 //•ˇ
10651 }
10652 "});
10653
10654 // If a selection span multiple lines, empty lines are not toggled.
10655 cx.set_state(indoc! {"
10656 fn a() {
10657 «a();
10658
10659 c();ˇ»
10660 }
10661 "});
10662
10663 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10664
10665 cx.assert_editor_state(indoc! {"
10666 fn a() {
10667 // «a();
10668
10669 // c();ˇ»
10670 }
10671 "});
10672
10673 // If a selection includes multiple comment prefixes, all lines are uncommented.
10674 cx.set_state(indoc! {"
10675 fn a() {
10676 «// a();
10677 /// b();
10678 //! c();ˇ»
10679 }
10680 "});
10681
10682 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10683
10684 cx.assert_editor_state(indoc! {"
10685 fn a() {
10686 «a();
10687 b();
10688 c();ˇ»
10689 }
10690 "});
10691}
10692
10693#[gpui::test]
10694async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10695 init_test(cx, |_| {});
10696 let mut cx = EditorTestContext::new(cx).await;
10697 let language = Arc::new(Language::new(
10698 LanguageConfig {
10699 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10700 ..Default::default()
10701 },
10702 Some(tree_sitter_rust::LANGUAGE.into()),
10703 ));
10704 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10705
10706 let toggle_comments = &ToggleComments {
10707 advance_downwards: false,
10708 ignore_indent: true,
10709 };
10710
10711 // If multiple selections intersect a line, the line is only toggled once.
10712 cx.set_state(indoc! {"
10713 fn a() {
10714 // «b();
10715 // c();
10716 // ˇ» d();
10717 }
10718 "});
10719
10720 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10721
10722 cx.assert_editor_state(indoc! {"
10723 fn a() {
10724 «b();
10725 c();
10726 ˇ» d();
10727 }
10728 "});
10729
10730 // The comment prefix is inserted at the beginning of each line
10731 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10732
10733 cx.assert_editor_state(indoc! {"
10734 fn a() {
10735 // «b();
10736 // c();
10737 // ˇ» d();
10738 }
10739 "});
10740
10741 // If a selection ends at the beginning of a line, that line is not toggled.
10742 cx.set_selections_state(indoc! {"
10743 fn a() {
10744 // b();
10745 // «c();
10746 ˇ»// d();
10747 }
10748 "});
10749
10750 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10751
10752 cx.assert_editor_state(indoc! {"
10753 fn a() {
10754 // b();
10755 «c();
10756 ˇ»// d();
10757 }
10758 "});
10759
10760 // If a selection span a single line and is empty, the line is toggled.
10761 cx.set_state(indoc! {"
10762 fn a() {
10763 a();
10764 b();
10765 ˇ
10766 }
10767 "});
10768
10769 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10770
10771 cx.assert_editor_state(indoc! {"
10772 fn a() {
10773 a();
10774 b();
10775 //ˇ
10776 }
10777 "});
10778
10779 // If a selection span multiple lines, empty lines are not toggled.
10780 cx.set_state(indoc! {"
10781 fn a() {
10782 «a();
10783
10784 c();ˇ»
10785 }
10786 "});
10787
10788 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10789
10790 cx.assert_editor_state(indoc! {"
10791 fn a() {
10792 // «a();
10793
10794 // c();ˇ»
10795 }
10796 "});
10797
10798 // If a selection includes multiple comment prefixes, all lines are uncommented.
10799 cx.set_state(indoc! {"
10800 fn a() {
10801 // «a();
10802 /// b();
10803 //! c();ˇ»
10804 }
10805 "});
10806
10807 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10808
10809 cx.assert_editor_state(indoc! {"
10810 fn a() {
10811 «a();
10812 b();
10813 c();ˇ»
10814 }
10815 "});
10816}
10817
10818#[gpui::test]
10819async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10820 init_test(cx, |_| {});
10821
10822 let language = Arc::new(Language::new(
10823 LanguageConfig {
10824 line_comments: vec!["// ".into()],
10825 ..Default::default()
10826 },
10827 Some(tree_sitter_rust::LANGUAGE.into()),
10828 ));
10829
10830 let mut cx = EditorTestContext::new(cx).await;
10831
10832 cx.language_registry().add(language.clone());
10833 cx.update_buffer(|buffer, cx| {
10834 buffer.set_language(Some(language), cx);
10835 });
10836
10837 let toggle_comments = &ToggleComments {
10838 advance_downwards: true,
10839 ignore_indent: false,
10840 };
10841
10842 // Single cursor on one line -> advance
10843 // Cursor moves horizontally 3 characters as well on non-blank line
10844 cx.set_state(indoc!(
10845 "fn a() {
10846 ˇdog();
10847 cat();
10848 }"
10849 ));
10850 cx.update_editor(|editor, window, cx| {
10851 editor.toggle_comments(toggle_comments, window, cx);
10852 });
10853 cx.assert_editor_state(indoc!(
10854 "fn a() {
10855 // dog();
10856 catˇ();
10857 }"
10858 ));
10859
10860 // Single selection on one line -> don't advance
10861 cx.set_state(indoc!(
10862 "fn a() {
10863 «dog()ˇ»;
10864 cat();
10865 }"
10866 ));
10867 cx.update_editor(|editor, window, cx| {
10868 editor.toggle_comments(toggle_comments, window, cx);
10869 });
10870 cx.assert_editor_state(indoc!(
10871 "fn a() {
10872 // «dog()ˇ»;
10873 cat();
10874 }"
10875 ));
10876
10877 // Multiple cursors on one line -> advance
10878 cx.set_state(indoc!(
10879 "fn a() {
10880 ˇdˇog();
10881 cat();
10882 }"
10883 ));
10884 cx.update_editor(|editor, window, cx| {
10885 editor.toggle_comments(toggle_comments, window, cx);
10886 });
10887 cx.assert_editor_state(indoc!(
10888 "fn a() {
10889 // dog();
10890 catˇ(ˇ);
10891 }"
10892 ));
10893
10894 // Multiple cursors on one line, with selection -> don't advance
10895 cx.set_state(indoc!(
10896 "fn a() {
10897 ˇdˇog«()ˇ»;
10898 cat();
10899 }"
10900 ));
10901 cx.update_editor(|editor, window, cx| {
10902 editor.toggle_comments(toggle_comments, window, cx);
10903 });
10904 cx.assert_editor_state(indoc!(
10905 "fn a() {
10906 // ˇdˇog«()ˇ»;
10907 cat();
10908 }"
10909 ));
10910
10911 // Single cursor on one line -> advance
10912 // Cursor moves to column 0 on blank line
10913 cx.set_state(indoc!(
10914 "fn a() {
10915 ˇdog();
10916
10917 cat();
10918 }"
10919 ));
10920 cx.update_editor(|editor, window, cx| {
10921 editor.toggle_comments(toggle_comments, window, cx);
10922 });
10923 cx.assert_editor_state(indoc!(
10924 "fn a() {
10925 // dog();
10926 ˇ
10927 cat();
10928 }"
10929 ));
10930
10931 // Single cursor on one line -> advance
10932 // Cursor starts and ends at column 0
10933 cx.set_state(indoc!(
10934 "fn a() {
10935 ˇ dog();
10936 cat();
10937 }"
10938 ));
10939 cx.update_editor(|editor, window, cx| {
10940 editor.toggle_comments(toggle_comments, window, cx);
10941 });
10942 cx.assert_editor_state(indoc!(
10943 "fn a() {
10944 // dog();
10945 ˇ cat();
10946 }"
10947 ));
10948}
10949
10950#[gpui::test]
10951async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10952 init_test(cx, |_| {});
10953
10954 let mut cx = EditorTestContext::new(cx).await;
10955
10956 let html_language = Arc::new(
10957 Language::new(
10958 LanguageConfig {
10959 name: "HTML".into(),
10960 block_comment: Some(("<!-- ".into(), " -->".into())),
10961 ..Default::default()
10962 },
10963 Some(tree_sitter_html::LANGUAGE.into()),
10964 )
10965 .with_injection_query(
10966 r#"
10967 (script_element
10968 (raw_text) @injection.content
10969 (#set! injection.language "javascript"))
10970 "#,
10971 )
10972 .unwrap(),
10973 );
10974
10975 let javascript_language = Arc::new(Language::new(
10976 LanguageConfig {
10977 name: "JavaScript".into(),
10978 line_comments: vec!["// ".into()],
10979 ..Default::default()
10980 },
10981 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10982 ));
10983
10984 cx.language_registry().add(html_language.clone());
10985 cx.language_registry().add(javascript_language.clone());
10986 cx.update_buffer(|buffer, cx| {
10987 buffer.set_language(Some(html_language), cx);
10988 });
10989
10990 // Toggle comments for empty selections
10991 cx.set_state(
10992 &r#"
10993 <p>A</p>ˇ
10994 <p>B</p>ˇ
10995 <p>C</p>ˇ
10996 "#
10997 .unindent(),
10998 );
10999 cx.update_editor(|editor, window, cx| {
11000 editor.toggle_comments(&ToggleComments::default(), window, cx)
11001 });
11002 cx.assert_editor_state(
11003 &r#"
11004 <!-- <p>A</p>ˇ -->
11005 <!-- <p>B</p>ˇ -->
11006 <!-- <p>C</p>ˇ -->
11007 "#
11008 .unindent(),
11009 );
11010 cx.update_editor(|editor, window, cx| {
11011 editor.toggle_comments(&ToggleComments::default(), window, cx)
11012 });
11013 cx.assert_editor_state(
11014 &r#"
11015 <p>A</p>ˇ
11016 <p>B</p>ˇ
11017 <p>C</p>ˇ
11018 "#
11019 .unindent(),
11020 );
11021
11022 // Toggle comments for mixture of empty and non-empty selections, where
11023 // multiple selections occupy a given line.
11024 cx.set_state(
11025 &r#"
11026 <p>A«</p>
11027 <p>ˇ»B</p>ˇ
11028 <p>C«</p>
11029 <p>ˇ»D</p>ˇ
11030 "#
11031 .unindent(),
11032 );
11033
11034 cx.update_editor(|editor, window, cx| {
11035 editor.toggle_comments(&ToggleComments::default(), window, cx)
11036 });
11037 cx.assert_editor_state(
11038 &r#"
11039 <!-- <p>A«</p>
11040 <p>ˇ»B</p>ˇ -->
11041 <!-- <p>C«</p>
11042 <p>ˇ»D</p>ˇ -->
11043 "#
11044 .unindent(),
11045 );
11046 cx.update_editor(|editor, window, cx| {
11047 editor.toggle_comments(&ToggleComments::default(), window, cx)
11048 });
11049 cx.assert_editor_state(
11050 &r#"
11051 <p>A«</p>
11052 <p>ˇ»B</p>ˇ
11053 <p>C«</p>
11054 <p>ˇ»D</p>ˇ
11055 "#
11056 .unindent(),
11057 );
11058
11059 // Toggle comments when different languages are active for different
11060 // selections.
11061 cx.set_state(
11062 &r#"
11063 ˇ<script>
11064 ˇvar x = new Y();
11065 ˇ</script>
11066 "#
11067 .unindent(),
11068 );
11069 cx.executor().run_until_parked();
11070 cx.update_editor(|editor, window, cx| {
11071 editor.toggle_comments(&ToggleComments::default(), window, cx)
11072 });
11073 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11074 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11075 cx.assert_editor_state(
11076 &r#"
11077 <!-- ˇ<script> -->
11078 // ˇvar x = new Y();
11079 <!-- ˇ</script> -->
11080 "#
11081 .unindent(),
11082 );
11083}
11084
11085#[gpui::test]
11086fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11087 init_test(cx, |_| {});
11088
11089 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11090 let multibuffer = cx.new(|cx| {
11091 let mut multibuffer = MultiBuffer::new(ReadWrite);
11092 multibuffer.push_excerpts(
11093 buffer.clone(),
11094 [
11095 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
11096 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
11097 ],
11098 cx,
11099 );
11100 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
11101 multibuffer
11102 });
11103
11104 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11105 editor.update_in(cx, |editor, window, cx| {
11106 assert_eq!(editor.text(cx), "aaaa\nbbbb");
11107 editor.change_selections(None, window, cx, |s| {
11108 s.select_ranges([
11109 Point::new(0, 0)..Point::new(0, 0),
11110 Point::new(1, 0)..Point::new(1, 0),
11111 ])
11112 });
11113
11114 editor.handle_input("X", window, cx);
11115 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
11116 assert_eq!(
11117 editor.selections.ranges(cx),
11118 [
11119 Point::new(0, 1)..Point::new(0, 1),
11120 Point::new(1, 1)..Point::new(1, 1),
11121 ]
11122 );
11123
11124 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
11125 editor.change_selections(None, window, cx, |s| {
11126 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
11127 });
11128 editor.backspace(&Default::default(), window, cx);
11129 assert_eq!(editor.text(cx), "Xa\nbbb");
11130 assert_eq!(
11131 editor.selections.ranges(cx),
11132 [Point::new(1, 0)..Point::new(1, 0)]
11133 );
11134
11135 editor.change_selections(None, window, cx, |s| {
11136 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
11137 });
11138 editor.backspace(&Default::default(), window, cx);
11139 assert_eq!(editor.text(cx), "X\nbb");
11140 assert_eq!(
11141 editor.selections.ranges(cx),
11142 [Point::new(0, 1)..Point::new(0, 1)]
11143 );
11144 });
11145}
11146
11147#[gpui::test]
11148fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
11149 init_test(cx, |_| {});
11150
11151 let markers = vec![('[', ']').into(), ('(', ')').into()];
11152 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
11153 indoc! {"
11154 [aaaa
11155 (bbbb]
11156 cccc)",
11157 },
11158 markers.clone(),
11159 );
11160 let excerpt_ranges = markers.into_iter().map(|marker| {
11161 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
11162 ExcerptRange::new(context.clone())
11163 });
11164 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
11165 let multibuffer = cx.new(|cx| {
11166 let mut multibuffer = MultiBuffer::new(ReadWrite);
11167 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
11168 multibuffer
11169 });
11170
11171 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11172 editor.update_in(cx, |editor, window, cx| {
11173 let (expected_text, selection_ranges) = marked_text_ranges(
11174 indoc! {"
11175 aaaa
11176 bˇbbb
11177 bˇbbˇb
11178 cccc"
11179 },
11180 true,
11181 );
11182 assert_eq!(editor.text(cx), expected_text);
11183 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11184
11185 editor.handle_input("X", window, cx);
11186
11187 let (expected_text, expected_selections) = marked_text_ranges(
11188 indoc! {"
11189 aaaa
11190 bXˇbbXb
11191 bXˇbbXˇb
11192 cccc"
11193 },
11194 false,
11195 );
11196 assert_eq!(editor.text(cx), expected_text);
11197 assert_eq!(editor.selections.ranges(cx), expected_selections);
11198
11199 editor.newline(&Newline, window, cx);
11200 let (expected_text, expected_selections) = marked_text_ranges(
11201 indoc! {"
11202 aaaa
11203 bX
11204 ˇbbX
11205 b
11206 bX
11207 ˇbbX
11208 ˇb
11209 cccc"
11210 },
11211 false,
11212 );
11213 assert_eq!(editor.text(cx), expected_text);
11214 assert_eq!(editor.selections.ranges(cx), expected_selections);
11215 });
11216}
11217
11218#[gpui::test]
11219fn test_refresh_selections(cx: &mut TestAppContext) {
11220 init_test(cx, |_| {});
11221
11222 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11223 let mut excerpt1_id = None;
11224 let multibuffer = cx.new(|cx| {
11225 let mut multibuffer = MultiBuffer::new(ReadWrite);
11226 excerpt1_id = multibuffer
11227 .push_excerpts(
11228 buffer.clone(),
11229 [
11230 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11231 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11232 ],
11233 cx,
11234 )
11235 .into_iter()
11236 .next();
11237 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11238 multibuffer
11239 });
11240
11241 let editor = cx.add_window(|window, cx| {
11242 let mut editor = build_editor(multibuffer.clone(), window, cx);
11243 let snapshot = editor.snapshot(window, cx);
11244 editor.change_selections(None, window, cx, |s| {
11245 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11246 });
11247 editor.begin_selection(
11248 Point::new(2, 1).to_display_point(&snapshot),
11249 true,
11250 1,
11251 window,
11252 cx,
11253 );
11254 assert_eq!(
11255 editor.selections.ranges(cx),
11256 [
11257 Point::new(1, 3)..Point::new(1, 3),
11258 Point::new(2, 1)..Point::new(2, 1),
11259 ]
11260 );
11261 editor
11262 });
11263
11264 // Refreshing selections is a no-op when excerpts haven't changed.
11265 _ = editor.update(cx, |editor, window, cx| {
11266 editor.change_selections(None, window, cx, |s| s.refresh());
11267 assert_eq!(
11268 editor.selections.ranges(cx),
11269 [
11270 Point::new(1, 3)..Point::new(1, 3),
11271 Point::new(2, 1)..Point::new(2, 1),
11272 ]
11273 );
11274 });
11275
11276 multibuffer.update(cx, |multibuffer, cx| {
11277 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11278 });
11279 _ = editor.update(cx, |editor, window, cx| {
11280 // Removing an excerpt causes the first selection to become degenerate.
11281 assert_eq!(
11282 editor.selections.ranges(cx),
11283 [
11284 Point::new(0, 0)..Point::new(0, 0),
11285 Point::new(0, 1)..Point::new(0, 1)
11286 ]
11287 );
11288
11289 // Refreshing selections will relocate the first selection to the original buffer
11290 // location.
11291 editor.change_selections(None, window, cx, |s| s.refresh());
11292 assert_eq!(
11293 editor.selections.ranges(cx),
11294 [
11295 Point::new(0, 1)..Point::new(0, 1),
11296 Point::new(0, 3)..Point::new(0, 3)
11297 ]
11298 );
11299 assert!(editor.selections.pending_anchor().is_some());
11300 });
11301}
11302
11303#[gpui::test]
11304fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
11305 init_test(cx, |_| {});
11306
11307 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11308 let mut excerpt1_id = None;
11309 let multibuffer = cx.new(|cx| {
11310 let mut multibuffer = MultiBuffer::new(ReadWrite);
11311 excerpt1_id = multibuffer
11312 .push_excerpts(
11313 buffer.clone(),
11314 [
11315 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11316 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11317 ],
11318 cx,
11319 )
11320 .into_iter()
11321 .next();
11322 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11323 multibuffer
11324 });
11325
11326 let editor = cx.add_window(|window, cx| {
11327 let mut editor = build_editor(multibuffer.clone(), window, cx);
11328 let snapshot = editor.snapshot(window, cx);
11329 editor.begin_selection(
11330 Point::new(1, 3).to_display_point(&snapshot),
11331 false,
11332 1,
11333 window,
11334 cx,
11335 );
11336 assert_eq!(
11337 editor.selections.ranges(cx),
11338 [Point::new(1, 3)..Point::new(1, 3)]
11339 );
11340 editor
11341 });
11342
11343 multibuffer.update(cx, |multibuffer, cx| {
11344 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11345 });
11346 _ = editor.update(cx, |editor, window, cx| {
11347 assert_eq!(
11348 editor.selections.ranges(cx),
11349 [Point::new(0, 0)..Point::new(0, 0)]
11350 );
11351
11352 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
11353 editor.change_selections(None, window, cx, |s| s.refresh());
11354 assert_eq!(
11355 editor.selections.ranges(cx),
11356 [Point::new(0, 3)..Point::new(0, 3)]
11357 );
11358 assert!(editor.selections.pending_anchor().is_some());
11359 });
11360}
11361
11362#[gpui::test]
11363async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
11364 init_test(cx, |_| {});
11365
11366 let language = Arc::new(
11367 Language::new(
11368 LanguageConfig {
11369 brackets: BracketPairConfig {
11370 pairs: vec![
11371 BracketPair {
11372 start: "{".to_string(),
11373 end: "}".to_string(),
11374 close: true,
11375 surround: true,
11376 newline: true,
11377 },
11378 BracketPair {
11379 start: "/* ".to_string(),
11380 end: " */".to_string(),
11381 close: true,
11382 surround: true,
11383 newline: true,
11384 },
11385 ],
11386 ..Default::default()
11387 },
11388 ..Default::default()
11389 },
11390 Some(tree_sitter_rust::LANGUAGE.into()),
11391 )
11392 .with_indents_query("")
11393 .unwrap(),
11394 );
11395
11396 let text = concat!(
11397 "{ }\n", //
11398 " x\n", //
11399 " /* */\n", //
11400 "x\n", //
11401 "{{} }\n", //
11402 );
11403
11404 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11405 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11406 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11407 editor
11408 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11409 .await;
11410
11411 editor.update_in(cx, |editor, window, cx| {
11412 editor.change_selections(None, window, cx, |s| {
11413 s.select_display_ranges([
11414 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11415 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11416 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11417 ])
11418 });
11419 editor.newline(&Newline, window, cx);
11420
11421 assert_eq!(
11422 editor.buffer().read(cx).read(cx).text(),
11423 concat!(
11424 "{ \n", // Suppress rustfmt
11425 "\n", //
11426 "}\n", //
11427 " x\n", //
11428 " /* \n", //
11429 " \n", //
11430 " */\n", //
11431 "x\n", //
11432 "{{} \n", //
11433 "}\n", //
11434 )
11435 );
11436 });
11437}
11438
11439#[gpui::test]
11440fn test_highlighted_ranges(cx: &mut TestAppContext) {
11441 init_test(cx, |_| {});
11442
11443 let editor = cx.add_window(|window, cx| {
11444 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11445 build_editor(buffer.clone(), window, cx)
11446 });
11447
11448 _ = editor.update(cx, |editor, window, cx| {
11449 struct Type1;
11450 struct Type2;
11451
11452 let buffer = editor.buffer.read(cx).snapshot(cx);
11453
11454 let anchor_range =
11455 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11456
11457 editor.highlight_background::<Type1>(
11458 &[
11459 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11460 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11461 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11462 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11463 ],
11464 |_| Hsla::red(),
11465 cx,
11466 );
11467 editor.highlight_background::<Type2>(
11468 &[
11469 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11470 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11471 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11472 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11473 ],
11474 |_| Hsla::green(),
11475 cx,
11476 );
11477
11478 let snapshot = editor.snapshot(window, cx);
11479 let mut highlighted_ranges = editor.background_highlights_in_range(
11480 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11481 &snapshot,
11482 cx.theme().colors(),
11483 );
11484 // Enforce a consistent ordering based on color without relying on the ordering of the
11485 // highlight's `TypeId` which is non-executor.
11486 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11487 assert_eq!(
11488 highlighted_ranges,
11489 &[
11490 (
11491 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11492 Hsla::red(),
11493 ),
11494 (
11495 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11496 Hsla::red(),
11497 ),
11498 (
11499 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11500 Hsla::green(),
11501 ),
11502 (
11503 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11504 Hsla::green(),
11505 ),
11506 ]
11507 );
11508 assert_eq!(
11509 editor.background_highlights_in_range(
11510 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11511 &snapshot,
11512 cx.theme().colors(),
11513 ),
11514 &[(
11515 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11516 Hsla::red(),
11517 )]
11518 );
11519 });
11520}
11521
11522#[gpui::test]
11523async fn test_following(cx: &mut TestAppContext) {
11524 init_test(cx, |_| {});
11525
11526 let fs = FakeFs::new(cx.executor());
11527 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11528
11529 let buffer = project.update(cx, |project, cx| {
11530 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11531 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11532 });
11533 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11534 let follower = cx.update(|cx| {
11535 cx.open_window(
11536 WindowOptions {
11537 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11538 gpui::Point::new(px(0.), px(0.)),
11539 gpui::Point::new(px(10.), px(80.)),
11540 ))),
11541 ..Default::default()
11542 },
11543 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11544 )
11545 .unwrap()
11546 });
11547
11548 let is_still_following = Rc::new(RefCell::new(true));
11549 let follower_edit_event_count = Rc::new(RefCell::new(0));
11550 let pending_update = Rc::new(RefCell::new(None));
11551 let leader_entity = leader.root(cx).unwrap();
11552 let follower_entity = follower.root(cx).unwrap();
11553 _ = follower.update(cx, {
11554 let update = pending_update.clone();
11555 let is_still_following = is_still_following.clone();
11556 let follower_edit_event_count = follower_edit_event_count.clone();
11557 |_, window, cx| {
11558 cx.subscribe_in(
11559 &leader_entity,
11560 window,
11561 move |_, leader, event, window, cx| {
11562 leader.read(cx).add_event_to_update_proto(
11563 event,
11564 &mut update.borrow_mut(),
11565 window,
11566 cx,
11567 );
11568 },
11569 )
11570 .detach();
11571
11572 cx.subscribe_in(
11573 &follower_entity,
11574 window,
11575 move |_, _, event: &EditorEvent, _window, _cx| {
11576 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11577 *is_still_following.borrow_mut() = false;
11578 }
11579
11580 if let EditorEvent::BufferEdited = event {
11581 *follower_edit_event_count.borrow_mut() += 1;
11582 }
11583 },
11584 )
11585 .detach();
11586 }
11587 });
11588
11589 // Update the selections only
11590 _ = leader.update(cx, |leader, window, cx| {
11591 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11592 });
11593 follower
11594 .update(cx, |follower, window, cx| {
11595 follower.apply_update_proto(
11596 &project,
11597 pending_update.borrow_mut().take().unwrap(),
11598 window,
11599 cx,
11600 )
11601 })
11602 .unwrap()
11603 .await
11604 .unwrap();
11605 _ = follower.update(cx, |follower, _, cx| {
11606 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11607 });
11608 assert!(*is_still_following.borrow());
11609 assert_eq!(*follower_edit_event_count.borrow(), 0);
11610
11611 // Update the scroll position only
11612 _ = leader.update(cx, |leader, window, cx| {
11613 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11614 });
11615 follower
11616 .update(cx, |follower, window, cx| {
11617 follower.apply_update_proto(
11618 &project,
11619 pending_update.borrow_mut().take().unwrap(),
11620 window,
11621 cx,
11622 )
11623 })
11624 .unwrap()
11625 .await
11626 .unwrap();
11627 assert_eq!(
11628 follower
11629 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11630 .unwrap(),
11631 gpui::Point::new(1.5, 3.5)
11632 );
11633 assert!(*is_still_following.borrow());
11634 assert_eq!(*follower_edit_event_count.borrow(), 0);
11635
11636 // Update the selections and scroll position. The follower's scroll position is updated
11637 // via autoscroll, not via the leader's exact scroll position.
11638 _ = leader.update(cx, |leader, window, cx| {
11639 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11640 leader.request_autoscroll(Autoscroll::newest(), cx);
11641 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11642 });
11643 follower
11644 .update(cx, |follower, window, cx| {
11645 follower.apply_update_proto(
11646 &project,
11647 pending_update.borrow_mut().take().unwrap(),
11648 window,
11649 cx,
11650 )
11651 })
11652 .unwrap()
11653 .await
11654 .unwrap();
11655 _ = follower.update(cx, |follower, _, cx| {
11656 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11657 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11658 });
11659 assert!(*is_still_following.borrow());
11660
11661 // Creating a pending selection that precedes another selection
11662 _ = leader.update(cx, |leader, window, cx| {
11663 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11664 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11665 });
11666 follower
11667 .update(cx, |follower, window, cx| {
11668 follower.apply_update_proto(
11669 &project,
11670 pending_update.borrow_mut().take().unwrap(),
11671 window,
11672 cx,
11673 )
11674 })
11675 .unwrap()
11676 .await
11677 .unwrap();
11678 _ = follower.update(cx, |follower, _, cx| {
11679 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11680 });
11681 assert!(*is_still_following.borrow());
11682
11683 // Extend the pending selection so that it surrounds another selection
11684 _ = leader.update(cx, |leader, window, cx| {
11685 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11686 });
11687 follower
11688 .update(cx, |follower, window, cx| {
11689 follower.apply_update_proto(
11690 &project,
11691 pending_update.borrow_mut().take().unwrap(),
11692 window,
11693 cx,
11694 )
11695 })
11696 .unwrap()
11697 .await
11698 .unwrap();
11699 _ = follower.update(cx, |follower, _, cx| {
11700 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11701 });
11702
11703 // Scrolling locally breaks the follow
11704 _ = follower.update(cx, |follower, window, cx| {
11705 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11706 follower.set_scroll_anchor(
11707 ScrollAnchor {
11708 anchor: top_anchor,
11709 offset: gpui::Point::new(0.0, 0.5),
11710 },
11711 window,
11712 cx,
11713 );
11714 });
11715 assert!(!(*is_still_following.borrow()));
11716}
11717
11718#[gpui::test]
11719async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11720 init_test(cx, |_| {});
11721
11722 let fs = FakeFs::new(cx.executor());
11723 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11725 let pane = workspace
11726 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11727 .unwrap();
11728
11729 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11730
11731 let leader = pane.update_in(cx, |_, window, cx| {
11732 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11733 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11734 });
11735
11736 // Start following the editor when it has no excerpts.
11737 let mut state_message =
11738 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11739 let workspace_entity = workspace.root(cx).unwrap();
11740 let follower_1 = cx
11741 .update_window(*workspace.deref(), |_, window, cx| {
11742 Editor::from_state_proto(
11743 workspace_entity,
11744 ViewId {
11745 creator: Default::default(),
11746 id: 0,
11747 },
11748 &mut state_message,
11749 window,
11750 cx,
11751 )
11752 })
11753 .unwrap()
11754 .unwrap()
11755 .await
11756 .unwrap();
11757
11758 let update_message = Rc::new(RefCell::new(None));
11759 follower_1.update_in(cx, {
11760 let update = update_message.clone();
11761 |_, window, cx| {
11762 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11763 leader.read(cx).add_event_to_update_proto(
11764 event,
11765 &mut update.borrow_mut(),
11766 window,
11767 cx,
11768 );
11769 })
11770 .detach();
11771 }
11772 });
11773
11774 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11775 (
11776 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11777 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11778 )
11779 });
11780
11781 // Insert some excerpts.
11782 leader.update(cx, |leader, cx| {
11783 leader.buffer.update(cx, |multibuffer, cx| {
11784 let excerpt_ids = multibuffer.push_excerpts(
11785 buffer_1.clone(),
11786 [
11787 ExcerptRange::new(1..6),
11788 ExcerptRange::new(12..15),
11789 ExcerptRange::new(0..3),
11790 ],
11791 cx,
11792 );
11793 multibuffer.insert_excerpts_after(
11794 excerpt_ids[0],
11795 buffer_2.clone(),
11796 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
11797 cx,
11798 );
11799 });
11800 });
11801
11802 // Apply the update of adding the excerpts.
11803 follower_1
11804 .update_in(cx, |follower, window, cx| {
11805 follower.apply_update_proto(
11806 &project,
11807 update_message.borrow().clone().unwrap(),
11808 window,
11809 cx,
11810 )
11811 })
11812 .await
11813 .unwrap();
11814 assert_eq!(
11815 follower_1.update(cx, |editor, cx| editor.text(cx)),
11816 leader.update(cx, |editor, cx| editor.text(cx))
11817 );
11818 update_message.borrow_mut().take();
11819
11820 // Start following separately after it already has excerpts.
11821 let mut state_message =
11822 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11823 let workspace_entity = workspace.root(cx).unwrap();
11824 let follower_2 = cx
11825 .update_window(*workspace.deref(), |_, window, cx| {
11826 Editor::from_state_proto(
11827 workspace_entity,
11828 ViewId {
11829 creator: Default::default(),
11830 id: 0,
11831 },
11832 &mut state_message,
11833 window,
11834 cx,
11835 )
11836 })
11837 .unwrap()
11838 .unwrap()
11839 .await
11840 .unwrap();
11841 assert_eq!(
11842 follower_2.update(cx, |editor, cx| editor.text(cx)),
11843 leader.update(cx, |editor, cx| editor.text(cx))
11844 );
11845
11846 // Remove some excerpts.
11847 leader.update(cx, |leader, cx| {
11848 leader.buffer.update(cx, |multibuffer, cx| {
11849 let excerpt_ids = multibuffer.excerpt_ids();
11850 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11851 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11852 });
11853 });
11854
11855 // Apply the update of removing the excerpts.
11856 follower_1
11857 .update_in(cx, |follower, window, cx| {
11858 follower.apply_update_proto(
11859 &project,
11860 update_message.borrow().clone().unwrap(),
11861 window,
11862 cx,
11863 )
11864 })
11865 .await
11866 .unwrap();
11867 follower_2
11868 .update_in(cx, |follower, window, cx| {
11869 follower.apply_update_proto(
11870 &project,
11871 update_message.borrow().clone().unwrap(),
11872 window,
11873 cx,
11874 )
11875 })
11876 .await
11877 .unwrap();
11878 update_message.borrow_mut().take();
11879 assert_eq!(
11880 follower_1.update(cx, |editor, cx| editor.text(cx)),
11881 leader.update(cx, |editor, cx| editor.text(cx))
11882 );
11883}
11884
11885#[gpui::test]
11886async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11887 init_test(cx, |_| {});
11888
11889 let mut cx = EditorTestContext::new(cx).await;
11890 let lsp_store =
11891 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11892
11893 cx.set_state(indoc! {"
11894 ˇfn func(abc def: i32) -> u32 {
11895 }
11896 "});
11897
11898 cx.update(|_, cx| {
11899 lsp_store.update(cx, |lsp_store, cx| {
11900 lsp_store
11901 .update_diagnostics(
11902 LanguageServerId(0),
11903 lsp::PublishDiagnosticsParams {
11904 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11905 version: None,
11906 diagnostics: vec![
11907 lsp::Diagnostic {
11908 range: lsp::Range::new(
11909 lsp::Position::new(0, 11),
11910 lsp::Position::new(0, 12),
11911 ),
11912 severity: Some(lsp::DiagnosticSeverity::ERROR),
11913 ..Default::default()
11914 },
11915 lsp::Diagnostic {
11916 range: lsp::Range::new(
11917 lsp::Position::new(0, 12),
11918 lsp::Position::new(0, 15),
11919 ),
11920 severity: Some(lsp::DiagnosticSeverity::ERROR),
11921 ..Default::default()
11922 },
11923 lsp::Diagnostic {
11924 range: lsp::Range::new(
11925 lsp::Position::new(0, 25),
11926 lsp::Position::new(0, 28),
11927 ),
11928 severity: Some(lsp::DiagnosticSeverity::ERROR),
11929 ..Default::default()
11930 },
11931 ],
11932 },
11933 &[],
11934 cx,
11935 )
11936 .unwrap()
11937 });
11938 });
11939
11940 executor.run_until_parked();
11941
11942 cx.update_editor(|editor, window, cx| {
11943 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11944 });
11945
11946 cx.assert_editor_state(indoc! {"
11947 fn func(abc def: i32) -> ˇu32 {
11948 }
11949 "});
11950
11951 cx.update_editor(|editor, window, cx| {
11952 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11953 });
11954
11955 cx.assert_editor_state(indoc! {"
11956 fn func(abc ˇdef: i32) -> u32 {
11957 }
11958 "});
11959
11960 cx.update_editor(|editor, window, cx| {
11961 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11962 });
11963
11964 cx.assert_editor_state(indoc! {"
11965 fn func(abcˇ def: i32) -> u32 {
11966 }
11967 "});
11968
11969 cx.update_editor(|editor, window, cx| {
11970 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11971 });
11972
11973 cx.assert_editor_state(indoc! {"
11974 fn func(abc def: i32) -> ˇu32 {
11975 }
11976 "});
11977}
11978
11979#[gpui::test]
11980async fn cycle_through_same_place_diagnostics(
11981 executor: BackgroundExecutor,
11982 cx: &mut TestAppContext,
11983) {
11984 init_test(cx, |_| {});
11985
11986 let mut cx = EditorTestContext::new(cx).await;
11987 let lsp_store =
11988 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11989
11990 cx.set_state(indoc! {"
11991 ˇfn func(abc def: i32) -> u32 {
11992 }
11993 "});
11994
11995 cx.update(|_, cx| {
11996 lsp_store.update(cx, |lsp_store, cx| {
11997 lsp_store
11998 .update_diagnostics(
11999 LanguageServerId(0),
12000 lsp::PublishDiagnosticsParams {
12001 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12002 version: None,
12003 diagnostics: vec![
12004 lsp::Diagnostic {
12005 range: lsp::Range::new(
12006 lsp::Position::new(0, 11),
12007 lsp::Position::new(0, 12),
12008 ),
12009 severity: Some(lsp::DiagnosticSeverity::ERROR),
12010 ..Default::default()
12011 },
12012 lsp::Diagnostic {
12013 range: lsp::Range::new(
12014 lsp::Position::new(0, 12),
12015 lsp::Position::new(0, 15),
12016 ),
12017 severity: Some(lsp::DiagnosticSeverity::ERROR),
12018 ..Default::default()
12019 },
12020 lsp::Diagnostic {
12021 range: lsp::Range::new(
12022 lsp::Position::new(0, 12),
12023 lsp::Position::new(0, 15),
12024 ),
12025 severity: Some(lsp::DiagnosticSeverity::ERROR),
12026 ..Default::default()
12027 },
12028 lsp::Diagnostic {
12029 range: lsp::Range::new(
12030 lsp::Position::new(0, 25),
12031 lsp::Position::new(0, 28),
12032 ),
12033 severity: Some(lsp::DiagnosticSeverity::ERROR),
12034 ..Default::default()
12035 },
12036 ],
12037 },
12038 &[],
12039 cx,
12040 )
12041 .unwrap()
12042 });
12043 });
12044 executor.run_until_parked();
12045
12046 //// Backward
12047
12048 // Fourth diagnostic
12049 cx.update_editor(|editor, window, cx| {
12050 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12051 });
12052 cx.assert_editor_state(indoc! {"
12053 fn func(abc def: i32) -> ˇu32 {
12054 }
12055 "});
12056
12057 // Third diagnostic
12058 cx.update_editor(|editor, window, cx| {
12059 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12060 });
12061 cx.assert_editor_state(indoc! {"
12062 fn func(abc ˇdef: i32) -> u32 {
12063 }
12064 "});
12065
12066 // Second diagnostic, same place
12067 cx.update_editor(|editor, window, cx| {
12068 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12069 });
12070 cx.assert_editor_state(indoc! {"
12071 fn func(abc ˇdef: i32) -> u32 {
12072 }
12073 "});
12074
12075 // First diagnostic
12076 cx.update_editor(|editor, window, cx| {
12077 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12078 });
12079 cx.assert_editor_state(indoc! {"
12080 fn func(abcˇ def: i32) -> u32 {
12081 }
12082 "});
12083
12084 // Wrapped over, fourth diagnostic
12085 cx.update_editor(|editor, window, cx| {
12086 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12087 });
12088 cx.assert_editor_state(indoc! {"
12089 fn func(abc def: i32) -> ˇu32 {
12090 }
12091 "});
12092
12093 cx.update_editor(|editor, window, cx| {
12094 editor.move_to_beginning(&MoveToBeginning, window, cx);
12095 });
12096 cx.assert_editor_state(indoc! {"
12097 ˇfn func(abc def: i32) -> u32 {
12098 }
12099 "});
12100
12101 //// Forward
12102
12103 // First diagnostic
12104 cx.update_editor(|editor, window, cx| {
12105 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12106 });
12107 cx.assert_editor_state(indoc! {"
12108 fn func(abcˇ def: i32) -> u32 {
12109 }
12110 "});
12111
12112 // Second diagnostic
12113 cx.update_editor(|editor, window, cx| {
12114 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12115 });
12116 cx.assert_editor_state(indoc! {"
12117 fn func(abc ˇdef: i32) -> u32 {
12118 }
12119 "});
12120
12121 // Third diagnostic, same place
12122 cx.update_editor(|editor, window, cx| {
12123 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12124 });
12125 cx.assert_editor_state(indoc! {"
12126 fn func(abc ˇdef: i32) -> u32 {
12127 }
12128 "});
12129
12130 // Fourth diagnostic
12131 cx.update_editor(|editor, window, cx| {
12132 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12133 });
12134 cx.assert_editor_state(indoc! {"
12135 fn func(abc def: i32) -> ˇu32 {
12136 }
12137 "});
12138
12139 // Wrapped around, first diagnostic
12140 cx.update_editor(|editor, window, cx| {
12141 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12142 });
12143 cx.assert_editor_state(indoc! {"
12144 fn func(abcˇ def: i32) -> u32 {
12145 }
12146 "});
12147}
12148
12149#[gpui::test]
12150async fn active_diagnostics_dismiss_after_invalidation(
12151 executor: BackgroundExecutor,
12152 cx: &mut TestAppContext,
12153) {
12154 init_test(cx, |_| {});
12155
12156 let mut cx = EditorTestContext::new(cx).await;
12157 let lsp_store =
12158 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12159
12160 cx.set_state(indoc! {"
12161 ˇfn func(abc def: i32) -> u32 {
12162 }
12163 "});
12164
12165 let message = "Something's wrong!";
12166 cx.update(|_, cx| {
12167 lsp_store.update(cx, |lsp_store, cx| {
12168 lsp_store
12169 .update_diagnostics(
12170 LanguageServerId(0),
12171 lsp::PublishDiagnosticsParams {
12172 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12173 version: None,
12174 diagnostics: vec![lsp::Diagnostic {
12175 range: lsp::Range::new(
12176 lsp::Position::new(0, 11),
12177 lsp::Position::new(0, 12),
12178 ),
12179 severity: Some(lsp::DiagnosticSeverity::ERROR),
12180 message: message.to_string(),
12181 ..Default::default()
12182 }],
12183 },
12184 &[],
12185 cx,
12186 )
12187 .unwrap()
12188 });
12189 });
12190 executor.run_until_parked();
12191
12192 cx.update_editor(|editor, window, cx| {
12193 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12194 assert_eq!(
12195 editor
12196 .active_diagnostics
12197 .as_ref()
12198 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
12199 Some(message),
12200 "Should have a diagnostics group activated"
12201 );
12202 });
12203 cx.assert_editor_state(indoc! {"
12204 fn func(abcˇ def: i32) -> u32 {
12205 }
12206 "});
12207
12208 cx.update(|_, cx| {
12209 lsp_store.update(cx, |lsp_store, cx| {
12210 lsp_store
12211 .update_diagnostics(
12212 LanguageServerId(0),
12213 lsp::PublishDiagnosticsParams {
12214 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12215 version: None,
12216 diagnostics: Vec::new(),
12217 },
12218 &[],
12219 cx,
12220 )
12221 .unwrap()
12222 });
12223 });
12224 executor.run_until_parked();
12225 cx.update_editor(|editor, _, _| {
12226 assert_eq!(
12227 editor.active_diagnostics, None,
12228 "After no diagnostics set to the editor, no diagnostics should be active"
12229 );
12230 });
12231 cx.assert_editor_state(indoc! {"
12232 fn func(abcˇ def: i32) -> u32 {
12233 }
12234 "});
12235
12236 cx.update_editor(|editor, window, cx| {
12237 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12238 assert_eq!(
12239 editor.active_diagnostics, None,
12240 "Should be no diagnostics to go to and activate"
12241 );
12242 });
12243 cx.assert_editor_state(indoc! {"
12244 fn func(abcˇ def: i32) -> u32 {
12245 }
12246 "});
12247}
12248
12249#[gpui::test]
12250async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12251 init_test(cx, |_| {});
12252
12253 let mut cx = EditorTestContext::new(cx).await;
12254
12255 cx.set_state(indoc! {"
12256 fn func(abˇc def: i32) -> u32 {
12257 }
12258 "});
12259 let lsp_store =
12260 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12261
12262 cx.update(|_, cx| {
12263 lsp_store.update(cx, |lsp_store, cx| {
12264 lsp_store.update_diagnostics(
12265 LanguageServerId(0),
12266 lsp::PublishDiagnosticsParams {
12267 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12268 version: None,
12269 diagnostics: vec![lsp::Diagnostic {
12270 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12271 severity: Some(lsp::DiagnosticSeverity::ERROR),
12272 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12273 ..Default::default()
12274 }],
12275 },
12276 &[],
12277 cx,
12278 )
12279 })
12280 }).unwrap();
12281 cx.run_until_parked();
12282 cx.update_editor(|editor, window, cx| {
12283 hover_popover::hover(editor, &Default::default(), window, cx)
12284 });
12285 cx.run_until_parked();
12286 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12287}
12288
12289#[gpui::test]
12290async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12291 init_test(cx, |_| {});
12292
12293 let mut cx = EditorTestContext::new(cx).await;
12294
12295 let diff_base = r#"
12296 use some::mod;
12297
12298 const A: u32 = 42;
12299
12300 fn main() {
12301 println!("hello");
12302
12303 println!("world");
12304 }
12305 "#
12306 .unindent();
12307
12308 // Edits are modified, removed, modified, added
12309 cx.set_state(
12310 &r#"
12311 use some::modified;
12312
12313 ˇ
12314 fn main() {
12315 println!("hello there");
12316
12317 println!("around the");
12318 println!("world");
12319 }
12320 "#
12321 .unindent(),
12322 );
12323
12324 cx.set_head_text(&diff_base);
12325 executor.run_until_parked();
12326
12327 cx.update_editor(|editor, window, cx| {
12328 //Wrap around the bottom of the buffer
12329 for _ in 0..3 {
12330 editor.go_to_next_hunk(&GoToHunk, window, cx);
12331 }
12332 });
12333
12334 cx.assert_editor_state(
12335 &r#"
12336 ˇuse some::modified;
12337
12338
12339 fn main() {
12340 println!("hello there");
12341
12342 println!("around the");
12343 println!("world");
12344 }
12345 "#
12346 .unindent(),
12347 );
12348
12349 cx.update_editor(|editor, window, cx| {
12350 //Wrap around the top of the buffer
12351 for _ in 0..2 {
12352 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12353 }
12354 });
12355
12356 cx.assert_editor_state(
12357 &r#"
12358 use some::modified;
12359
12360
12361 fn main() {
12362 ˇ println!("hello there");
12363
12364 println!("around the");
12365 println!("world");
12366 }
12367 "#
12368 .unindent(),
12369 );
12370
12371 cx.update_editor(|editor, window, cx| {
12372 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12373 });
12374
12375 cx.assert_editor_state(
12376 &r#"
12377 use some::modified;
12378
12379 ˇ
12380 fn main() {
12381 println!("hello there");
12382
12383 println!("around the");
12384 println!("world");
12385 }
12386 "#
12387 .unindent(),
12388 );
12389
12390 cx.update_editor(|editor, window, cx| {
12391 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12392 });
12393
12394 cx.assert_editor_state(
12395 &r#"
12396 ˇuse some::modified;
12397
12398
12399 fn main() {
12400 println!("hello there");
12401
12402 println!("around the");
12403 println!("world");
12404 }
12405 "#
12406 .unindent(),
12407 );
12408
12409 cx.update_editor(|editor, window, cx| {
12410 for _ in 0..2 {
12411 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12412 }
12413 });
12414
12415 cx.assert_editor_state(
12416 &r#"
12417 use some::modified;
12418
12419
12420 fn main() {
12421 ˇ println!("hello there");
12422
12423 println!("around the");
12424 println!("world");
12425 }
12426 "#
12427 .unindent(),
12428 );
12429
12430 cx.update_editor(|editor, window, cx| {
12431 editor.fold(&Fold, window, cx);
12432 });
12433
12434 cx.update_editor(|editor, window, cx| {
12435 editor.go_to_next_hunk(&GoToHunk, window, cx);
12436 });
12437
12438 cx.assert_editor_state(
12439 &r#"
12440 ˇuse some::modified;
12441
12442
12443 fn main() {
12444 println!("hello there");
12445
12446 println!("around the");
12447 println!("world");
12448 }
12449 "#
12450 .unindent(),
12451 );
12452}
12453
12454#[test]
12455fn test_split_words() {
12456 fn split(text: &str) -> Vec<&str> {
12457 split_words(text).collect()
12458 }
12459
12460 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12461 assert_eq!(split("hello_world"), &["hello_", "world"]);
12462 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12463 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12464 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12465 assert_eq!(split("helloworld"), &["helloworld"]);
12466
12467 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12468}
12469
12470#[gpui::test]
12471async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12472 init_test(cx, |_| {});
12473
12474 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12475 let mut assert = |before, after| {
12476 let _state_context = cx.set_state(before);
12477 cx.run_until_parked();
12478 cx.update_editor(|editor, window, cx| {
12479 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12480 });
12481 cx.run_until_parked();
12482 cx.assert_editor_state(after);
12483 };
12484
12485 // Outside bracket jumps to outside of matching bracket
12486 assert("console.logˇ(var);", "console.log(var)ˇ;");
12487 assert("console.log(var)ˇ;", "console.logˇ(var);");
12488
12489 // Inside bracket jumps to inside of matching bracket
12490 assert("console.log(ˇvar);", "console.log(varˇ);");
12491 assert("console.log(varˇ);", "console.log(ˇvar);");
12492
12493 // When outside a bracket and inside, favor jumping to the inside bracket
12494 assert(
12495 "console.log('foo', [1, 2, 3]ˇ);",
12496 "console.log(ˇ'foo', [1, 2, 3]);",
12497 );
12498 assert(
12499 "console.log(ˇ'foo', [1, 2, 3]);",
12500 "console.log('foo', [1, 2, 3]ˇ);",
12501 );
12502
12503 // Bias forward if two options are equally likely
12504 assert(
12505 "let result = curried_fun()ˇ();",
12506 "let result = curried_fun()()ˇ;",
12507 );
12508
12509 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12510 assert(
12511 indoc! {"
12512 function test() {
12513 console.log('test')ˇ
12514 }"},
12515 indoc! {"
12516 function test() {
12517 console.logˇ('test')
12518 }"},
12519 );
12520}
12521
12522#[gpui::test]
12523async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12524 init_test(cx, |_| {});
12525
12526 let fs = FakeFs::new(cx.executor());
12527 fs.insert_tree(
12528 path!("/a"),
12529 json!({
12530 "main.rs": "fn main() { let a = 5; }",
12531 "other.rs": "// Test file",
12532 }),
12533 )
12534 .await;
12535 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12536
12537 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12538 language_registry.add(Arc::new(Language::new(
12539 LanguageConfig {
12540 name: "Rust".into(),
12541 matcher: LanguageMatcher {
12542 path_suffixes: vec!["rs".to_string()],
12543 ..Default::default()
12544 },
12545 brackets: BracketPairConfig {
12546 pairs: vec![BracketPair {
12547 start: "{".to_string(),
12548 end: "}".to_string(),
12549 close: true,
12550 surround: true,
12551 newline: true,
12552 }],
12553 disabled_scopes_by_bracket_ix: Vec::new(),
12554 },
12555 ..Default::default()
12556 },
12557 Some(tree_sitter_rust::LANGUAGE.into()),
12558 )));
12559 let mut fake_servers = language_registry.register_fake_lsp(
12560 "Rust",
12561 FakeLspAdapter {
12562 capabilities: lsp::ServerCapabilities {
12563 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12564 first_trigger_character: "{".to_string(),
12565 more_trigger_character: None,
12566 }),
12567 ..Default::default()
12568 },
12569 ..Default::default()
12570 },
12571 );
12572
12573 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12574
12575 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12576
12577 let worktree_id = workspace
12578 .update(cx, |workspace, _, cx| {
12579 workspace.project().update(cx, |project, cx| {
12580 project.worktrees(cx).next().unwrap().read(cx).id()
12581 })
12582 })
12583 .unwrap();
12584
12585 let buffer = project
12586 .update(cx, |project, cx| {
12587 project.open_local_buffer(path!("/a/main.rs"), cx)
12588 })
12589 .await
12590 .unwrap();
12591 let editor_handle = workspace
12592 .update(cx, |workspace, window, cx| {
12593 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12594 })
12595 .unwrap()
12596 .await
12597 .unwrap()
12598 .downcast::<Editor>()
12599 .unwrap();
12600
12601 cx.executor().start_waiting();
12602 let fake_server = fake_servers.next().await.unwrap();
12603
12604 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12605 |params, _| async move {
12606 assert_eq!(
12607 params.text_document_position.text_document.uri,
12608 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12609 );
12610 assert_eq!(
12611 params.text_document_position.position,
12612 lsp::Position::new(0, 21),
12613 );
12614
12615 Ok(Some(vec![lsp::TextEdit {
12616 new_text: "]".to_string(),
12617 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12618 }]))
12619 },
12620 );
12621
12622 editor_handle.update_in(cx, |editor, window, cx| {
12623 window.focus(&editor.focus_handle(cx));
12624 editor.change_selections(None, window, cx, |s| {
12625 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12626 });
12627 editor.handle_input("{", window, cx);
12628 });
12629
12630 cx.executor().run_until_parked();
12631
12632 buffer.update(cx, |buffer, _| {
12633 assert_eq!(
12634 buffer.text(),
12635 "fn main() { let a = {5}; }",
12636 "No extra braces from on type formatting should appear in the buffer"
12637 )
12638 });
12639}
12640
12641#[gpui::test]
12642async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12643 init_test(cx, |_| {});
12644
12645 let fs = FakeFs::new(cx.executor());
12646 fs.insert_tree(
12647 path!("/a"),
12648 json!({
12649 "main.rs": "fn main() { let a = 5; }",
12650 "other.rs": "// Test file",
12651 }),
12652 )
12653 .await;
12654
12655 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12656
12657 let server_restarts = Arc::new(AtomicUsize::new(0));
12658 let closure_restarts = Arc::clone(&server_restarts);
12659 let language_server_name = "test language server";
12660 let language_name: LanguageName = "Rust".into();
12661
12662 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12663 language_registry.add(Arc::new(Language::new(
12664 LanguageConfig {
12665 name: language_name.clone(),
12666 matcher: LanguageMatcher {
12667 path_suffixes: vec!["rs".to_string()],
12668 ..Default::default()
12669 },
12670 ..Default::default()
12671 },
12672 Some(tree_sitter_rust::LANGUAGE.into()),
12673 )));
12674 let mut fake_servers = language_registry.register_fake_lsp(
12675 "Rust",
12676 FakeLspAdapter {
12677 name: language_server_name,
12678 initialization_options: Some(json!({
12679 "testOptionValue": true
12680 })),
12681 initializer: Some(Box::new(move |fake_server| {
12682 let task_restarts = Arc::clone(&closure_restarts);
12683 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12684 task_restarts.fetch_add(1, atomic::Ordering::Release);
12685 futures::future::ready(Ok(()))
12686 });
12687 })),
12688 ..Default::default()
12689 },
12690 );
12691
12692 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12693 let _buffer = project
12694 .update(cx, |project, cx| {
12695 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12696 })
12697 .await
12698 .unwrap();
12699 let _fake_server = fake_servers.next().await.unwrap();
12700 update_test_language_settings(cx, |language_settings| {
12701 language_settings.languages.insert(
12702 language_name.clone(),
12703 LanguageSettingsContent {
12704 tab_size: NonZeroU32::new(8),
12705 ..Default::default()
12706 },
12707 );
12708 });
12709 cx.executor().run_until_parked();
12710 assert_eq!(
12711 server_restarts.load(atomic::Ordering::Acquire),
12712 0,
12713 "Should not restart LSP server on an unrelated change"
12714 );
12715
12716 update_test_project_settings(cx, |project_settings| {
12717 project_settings.lsp.insert(
12718 "Some other server name".into(),
12719 LspSettings {
12720 binary: None,
12721 settings: None,
12722 initialization_options: Some(json!({
12723 "some other init value": false
12724 })),
12725 enable_lsp_tasks: false,
12726 },
12727 );
12728 });
12729 cx.executor().run_until_parked();
12730 assert_eq!(
12731 server_restarts.load(atomic::Ordering::Acquire),
12732 0,
12733 "Should not restart LSP server on an unrelated LSP settings change"
12734 );
12735
12736 update_test_project_settings(cx, |project_settings| {
12737 project_settings.lsp.insert(
12738 language_server_name.into(),
12739 LspSettings {
12740 binary: None,
12741 settings: None,
12742 initialization_options: Some(json!({
12743 "anotherInitValue": false
12744 })),
12745 enable_lsp_tasks: false,
12746 },
12747 );
12748 });
12749 cx.executor().run_until_parked();
12750 assert_eq!(
12751 server_restarts.load(atomic::Ordering::Acquire),
12752 1,
12753 "Should restart LSP server on a related LSP settings change"
12754 );
12755
12756 update_test_project_settings(cx, |project_settings| {
12757 project_settings.lsp.insert(
12758 language_server_name.into(),
12759 LspSettings {
12760 binary: None,
12761 settings: None,
12762 initialization_options: Some(json!({
12763 "anotherInitValue": false
12764 })),
12765 enable_lsp_tasks: false,
12766 },
12767 );
12768 });
12769 cx.executor().run_until_parked();
12770 assert_eq!(
12771 server_restarts.load(atomic::Ordering::Acquire),
12772 1,
12773 "Should not restart LSP server on a related LSP settings change that is the same"
12774 );
12775
12776 update_test_project_settings(cx, |project_settings| {
12777 project_settings.lsp.insert(
12778 language_server_name.into(),
12779 LspSettings {
12780 binary: None,
12781 settings: None,
12782 initialization_options: None,
12783 enable_lsp_tasks: false,
12784 },
12785 );
12786 });
12787 cx.executor().run_until_parked();
12788 assert_eq!(
12789 server_restarts.load(atomic::Ordering::Acquire),
12790 2,
12791 "Should restart LSP server on another related LSP settings change"
12792 );
12793}
12794
12795#[gpui::test]
12796async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12797 init_test(cx, |_| {});
12798
12799 let mut cx = EditorLspTestContext::new_rust(
12800 lsp::ServerCapabilities {
12801 completion_provider: Some(lsp::CompletionOptions {
12802 trigger_characters: Some(vec![".".to_string()]),
12803 resolve_provider: Some(true),
12804 ..Default::default()
12805 }),
12806 ..Default::default()
12807 },
12808 cx,
12809 )
12810 .await;
12811
12812 cx.set_state("fn main() { let a = 2ˇ; }");
12813 cx.simulate_keystroke(".");
12814 let completion_item = lsp::CompletionItem {
12815 label: "some".into(),
12816 kind: Some(lsp::CompletionItemKind::SNIPPET),
12817 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12818 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12819 kind: lsp::MarkupKind::Markdown,
12820 value: "```rust\nSome(2)\n```".to_string(),
12821 })),
12822 deprecated: Some(false),
12823 sort_text: Some("fffffff2".to_string()),
12824 filter_text: Some("some".to_string()),
12825 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12826 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12827 range: lsp::Range {
12828 start: lsp::Position {
12829 line: 0,
12830 character: 22,
12831 },
12832 end: lsp::Position {
12833 line: 0,
12834 character: 22,
12835 },
12836 },
12837 new_text: "Some(2)".to_string(),
12838 })),
12839 additional_text_edits: Some(vec![lsp::TextEdit {
12840 range: lsp::Range {
12841 start: lsp::Position {
12842 line: 0,
12843 character: 20,
12844 },
12845 end: lsp::Position {
12846 line: 0,
12847 character: 22,
12848 },
12849 },
12850 new_text: "".to_string(),
12851 }]),
12852 ..Default::default()
12853 };
12854
12855 let closure_completion_item = completion_item.clone();
12856 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12857 let task_completion_item = closure_completion_item.clone();
12858 async move {
12859 Ok(Some(lsp::CompletionResponse::Array(vec![
12860 task_completion_item,
12861 ])))
12862 }
12863 });
12864
12865 request.next().await;
12866
12867 cx.condition(|editor, _| editor.context_menu_visible())
12868 .await;
12869 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12870 editor
12871 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12872 .unwrap()
12873 });
12874 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12875
12876 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12877 let task_completion_item = completion_item.clone();
12878 async move { Ok(task_completion_item) }
12879 })
12880 .next()
12881 .await
12882 .unwrap();
12883 apply_additional_edits.await.unwrap();
12884 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12885}
12886
12887#[gpui::test]
12888async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12889 init_test(cx, |_| {});
12890
12891 let mut cx = EditorLspTestContext::new_rust(
12892 lsp::ServerCapabilities {
12893 completion_provider: Some(lsp::CompletionOptions {
12894 trigger_characters: Some(vec![".".to_string()]),
12895 resolve_provider: Some(true),
12896 ..Default::default()
12897 }),
12898 ..Default::default()
12899 },
12900 cx,
12901 )
12902 .await;
12903
12904 cx.set_state("fn main() { let a = 2ˇ; }");
12905 cx.simulate_keystroke(".");
12906
12907 let item1 = lsp::CompletionItem {
12908 label: "method id()".to_string(),
12909 filter_text: Some("id".to_string()),
12910 detail: None,
12911 documentation: None,
12912 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12913 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12914 new_text: ".id".to_string(),
12915 })),
12916 ..lsp::CompletionItem::default()
12917 };
12918
12919 let item2 = lsp::CompletionItem {
12920 label: "other".to_string(),
12921 filter_text: Some("other".to_string()),
12922 detail: None,
12923 documentation: None,
12924 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12925 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12926 new_text: ".other".to_string(),
12927 })),
12928 ..lsp::CompletionItem::default()
12929 };
12930
12931 let item1 = item1.clone();
12932 cx.set_request_handler::<lsp::request::Completion, _, _>({
12933 let item1 = item1.clone();
12934 move |_, _, _| {
12935 let item1 = item1.clone();
12936 let item2 = item2.clone();
12937 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12938 }
12939 })
12940 .next()
12941 .await;
12942
12943 cx.condition(|editor, _| editor.context_menu_visible())
12944 .await;
12945 cx.update_editor(|editor, _, _| {
12946 let context_menu = editor.context_menu.borrow_mut();
12947 let context_menu = context_menu
12948 .as_ref()
12949 .expect("Should have the context menu deployed");
12950 match context_menu {
12951 CodeContextMenu::Completions(completions_menu) => {
12952 let completions = completions_menu.completions.borrow_mut();
12953 assert_eq!(
12954 completions
12955 .iter()
12956 .map(|completion| &completion.label.text)
12957 .collect::<Vec<_>>(),
12958 vec!["method id()", "other"]
12959 )
12960 }
12961 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12962 }
12963 });
12964
12965 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12966 let item1 = item1.clone();
12967 move |_, item_to_resolve, _| {
12968 let item1 = item1.clone();
12969 async move {
12970 if item1 == item_to_resolve {
12971 Ok(lsp::CompletionItem {
12972 label: "method id()".to_string(),
12973 filter_text: Some("id".to_string()),
12974 detail: Some("Now resolved!".to_string()),
12975 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12976 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12977 range: lsp::Range::new(
12978 lsp::Position::new(0, 22),
12979 lsp::Position::new(0, 22),
12980 ),
12981 new_text: ".id".to_string(),
12982 })),
12983 ..lsp::CompletionItem::default()
12984 })
12985 } else {
12986 Ok(item_to_resolve)
12987 }
12988 }
12989 }
12990 })
12991 .next()
12992 .await
12993 .unwrap();
12994 cx.run_until_parked();
12995
12996 cx.update_editor(|editor, window, cx| {
12997 editor.context_menu_next(&Default::default(), window, cx);
12998 });
12999
13000 cx.update_editor(|editor, _, _| {
13001 let context_menu = editor.context_menu.borrow_mut();
13002 let context_menu = context_menu
13003 .as_ref()
13004 .expect("Should have the context menu deployed");
13005 match context_menu {
13006 CodeContextMenu::Completions(completions_menu) => {
13007 let completions = completions_menu.completions.borrow_mut();
13008 assert_eq!(
13009 completions
13010 .iter()
13011 .map(|completion| &completion.label.text)
13012 .collect::<Vec<_>>(),
13013 vec!["method id() Now resolved!", "other"],
13014 "Should update first completion label, but not second as the filter text did not match."
13015 );
13016 }
13017 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13018 }
13019 });
13020}
13021
13022#[gpui::test]
13023async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13024 init_test(cx, |_| {});
13025
13026 let mut cx = EditorLspTestContext::new_rust(
13027 lsp::ServerCapabilities {
13028 completion_provider: Some(lsp::CompletionOptions {
13029 trigger_characters: Some(vec![".".to_string()]),
13030 resolve_provider: Some(true),
13031 ..Default::default()
13032 }),
13033 ..Default::default()
13034 },
13035 cx,
13036 )
13037 .await;
13038
13039 cx.set_state("fn main() { let a = 2ˇ; }");
13040 cx.simulate_keystroke(".");
13041
13042 let unresolved_item_1 = lsp::CompletionItem {
13043 label: "id".to_string(),
13044 filter_text: Some("id".to_string()),
13045 detail: None,
13046 documentation: None,
13047 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13048 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13049 new_text: ".id".to_string(),
13050 })),
13051 ..lsp::CompletionItem::default()
13052 };
13053 let resolved_item_1 = lsp::CompletionItem {
13054 additional_text_edits: Some(vec![lsp::TextEdit {
13055 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13056 new_text: "!!".to_string(),
13057 }]),
13058 ..unresolved_item_1.clone()
13059 };
13060 let unresolved_item_2 = lsp::CompletionItem {
13061 label: "other".to_string(),
13062 filter_text: Some("other".to_string()),
13063 detail: None,
13064 documentation: None,
13065 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13066 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13067 new_text: ".other".to_string(),
13068 })),
13069 ..lsp::CompletionItem::default()
13070 };
13071 let resolved_item_2 = lsp::CompletionItem {
13072 additional_text_edits: Some(vec![lsp::TextEdit {
13073 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13074 new_text: "??".to_string(),
13075 }]),
13076 ..unresolved_item_2.clone()
13077 };
13078
13079 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13080 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13081 cx.lsp
13082 .server
13083 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13084 let unresolved_item_1 = unresolved_item_1.clone();
13085 let resolved_item_1 = resolved_item_1.clone();
13086 let unresolved_item_2 = unresolved_item_2.clone();
13087 let resolved_item_2 = resolved_item_2.clone();
13088 let resolve_requests_1 = resolve_requests_1.clone();
13089 let resolve_requests_2 = resolve_requests_2.clone();
13090 move |unresolved_request, _| {
13091 let unresolved_item_1 = unresolved_item_1.clone();
13092 let resolved_item_1 = resolved_item_1.clone();
13093 let unresolved_item_2 = unresolved_item_2.clone();
13094 let resolved_item_2 = resolved_item_2.clone();
13095 let resolve_requests_1 = resolve_requests_1.clone();
13096 let resolve_requests_2 = resolve_requests_2.clone();
13097 async move {
13098 if unresolved_request == unresolved_item_1 {
13099 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13100 Ok(resolved_item_1.clone())
13101 } else if unresolved_request == unresolved_item_2 {
13102 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13103 Ok(resolved_item_2.clone())
13104 } else {
13105 panic!("Unexpected completion item {unresolved_request:?}")
13106 }
13107 }
13108 }
13109 })
13110 .detach();
13111
13112 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13113 let unresolved_item_1 = unresolved_item_1.clone();
13114 let unresolved_item_2 = unresolved_item_2.clone();
13115 async move {
13116 Ok(Some(lsp::CompletionResponse::Array(vec![
13117 unresolved_item_1,
13118 unresolved_item_2,
13119 ])))
13120 }
13121 })
13122 .next()
13123 .await;
13124
13125 cx.condition(|editor, _| editor.context_menu_visible())
13126 .await;
13127 cx.update_editor(|editor, _, _| {
13128 let context_menu = editor.context_menu.borrow_mut();
13129 let context_menu = context_menu
13130 .as_ref()
13131 .expect("Should have the context menu deployed");
13132 match context_menu {
13133 CodeContextMenu::Completions(completions_menu) => {
13134 let completions = completions_menu.completions.borrow_mut();
13135 assert_eq!(
13136 completions
13137 .iter()
13138 .map(|completion| &completion.label.text)
13139 .collect::<Vec<_>>(),
13140 vec!["id", "other"]
13141 )
13142 }
13143 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13144 }
13145 });
13146 cx.run_until_parked();
13147
13148 cx.update_editor(|editor, window, cx| {
13149 editor.context_menu_next(&ContextMenuNext, window, cx);
13150 });
13151 cx.run_until_parked();
13152 cx.update_editor(|editor, window, cx| {
13153 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13154 });
13155 cx.run_until_parked();
13156 cx.update_editor(|editor, window, cx| {
13157 editor.context_menu_next(&ContextMenuNext, window, cx);
13158 });
13159 cx.run_until_parked();
13160 cx.update_editor(|editor, window, cx| {
13161 editor
13162 .compose_completion(&ComposeCompletion::default(), window, cx)
13163 .expect("No task returned")
13164 })
13165 .await
13166 .expect("Completion failed");
13167 cx.run_until_parked();
13168
13169 cx.update_editor(|editor, _, cx| {
13170 assert_eq!(
13171 resolve_requests_1.load(atomic::Ordering::Acquire),
13172 1,
13173 "Should always resolve once despite multiple selections"
13174 );
13175 assert_eq!(
13176 resolve_requests_2.load(atomic::Ordering::Acquire),
13177 1,
13178 "Should always resolve once after multiple selections and applying the completion"
13179 );
13180 assert_eq!(
13181 editor.text(cx),
13182 "fn main() { let a = ??.other; }",
13183 "Should use resolved data when applying the completion"
13184 );
13185 });
13186}
13187
13188#[gpui::test]
13189async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13190 init_test(cx, |_| {});
13191
13192 let item_0 = lsp::CompletionItem {
13193 label: "abs".into(),
13194 insert_text: Some("abs".into()),
13195 data: Some(json!({ "very": "special"})),
13196 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13197 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13198 lsp::InsertReplaceEdit {
13199 new_text: "abs".to_string(),
13200 insert: lsp::Range::default(),
13201 replace: lsp::Range::default(),
13202 },
13203 )),
13204 ..lsp::CompletionItem::default()
13205 };
13206 let items = iter::once(item_0.clone())
13207 .chain((11..51).map(|i| lsp::CompletionItem {
13208 label: format!("item_{}", i),
13209 insert_text: Some(format!("item_{}", i)),
13210 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13211 ..lsp::CompletionItem::default()
13212 }))
13213 .collect::<Vec<_>>();
13214
13215 let default_commit_characters = vec!["?".to_string()];
13216 let default_data = json!({ "default": "data"});
13217 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13218 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13219 let default_edit_range = lsp::Range {
13220 start: lsp::Position {
13221 line: 0,
13222 character: 5,
13223 },
13224 end: lsp::Position {
13225 line: 0,
13226 character: 5,
13227 },
13228 };
13229
13230 let mut cx = EditorLspTestContext::new_rust(
13231 lsp::ServerCapabilities {
13232 completion_provider: Some(lsp::CompletionOptions {
13233 trigger_characters: Some(vec![".".to_string()]),
13234 resolve_provider: Some(true),
13235 ..Default::default()
13236 }),
13237 ..Default::default()
13238 },
13239 cx,
13240 )
13241 .await;
13242
13243 cx.set_state("fn main() { let a = 2ˇ; }");
13244 cx.simulate_keystroke(".");
13245
13246 let completion_data = default_data.clone();
13247 let completion_characters = default_commit_characters.clone();
13248 let completion_items = items.clone();
13249 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13250 let default_data = completion_data.clone();
13251 let default_commit_characters = completion_characters.clone();
13252 let items = completion_items.clone();
13253 async move {
13254 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13255 items,
13256 item_defaults: Some(lsp::CompletionListItemDefaults {
13257 data: Some(default_data.clone()),
13258 commit_characters: Some(default_commit_characters.clone()),
13259 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13260 default_edit_range,
13261 )),
13262 insert_text_format: Some(default_insert_text_format),
13263 insert_text_mode: Some(default_insert_text_mode),
13264 }),
13265 ..lsp::CompletionList::default()
13266 })))
13267 }
13268 })
13269 .next()
13270 .await;
13271
13272 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13273 cx.lsp
13274 .server
13275 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13276 let closure_resolved_items = resolved_items.clone();
13277 move |item_to_resolve, _| {
13278 let closure_resolved_items = closure_resolved_items.clone();
13279 async move {
13280 closure_resolved_items.lock().push(item_to_resolve.clone());
13281 Ok(item_to_resolve)
13282 }
13283 }
13284 })
13285 .detach();
13286
13287 cx.condition(|editor, _| editor.context_menu_visible())
13288 .await;
13289 cx.run_until_parked();
13290 cx.update_editor(|editor, _, _| {
13291 let menu = editor.context_menu.borrow_mut();
13292 match menu.as_ref().expect("should have the completions menu") {
13293 CodeContextMenu::Completions(completions_menu) => {
13294 assert_eq!(
13295 completions_menu
13296 .entries
13297 .borrow()
13298 .iter()
13299 .map(|mat| mat.string.clone())
13300 .collect::<Vec<String>>(),
13301 items
13302 .iter()
13303 .map(|completion| completion.label.clone())
13304 .collect::<Vec<String>>()
13305 );
13306 }
13307 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13308 }
13309 });
13310 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13311 // with 4 from the end.
13312 assert_eq!(
13313 *resolved_items.lock(),
13314 [&items[0..16], &items[items.len() - 4..items.len()]]
13315 .concat()
13316 .iter()
13317 .cloned()
13318 .map(|mut item| {
13319 if item.data.is_none() {
13320 item.data = Some(default_data.clone());
13321 }
13322 item
13323 })
13324 .collect::<Vec<lsp::CompletionItem>>(),
13325 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13326 );
13327 resolved_items.lock().clear();
13328
13329 cx.update_editor(|editor, window, cx| {
13330 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13331 });
13332 cx.run_until_parked();
13333 // Completions that have already been resolved are skipped.
13334 assert_eq!(
13335 *resolved_items.lock(),
13336 items[items.len() - 16..items.len() - 4]
13337 .iter()
13338 .cloned()
13339 .map(|mut item| {
13340 if item.data.is_none() {
13341 item.data = Some(default_data.clone());
13342 }
13343 item
13344 })
13345 .collect::<Vec<lsp::CompletionItem>>()
13346 );
13347 resolved_items.lock().clear();
13348}
13349
13350#[gpui::test]
13351async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13352 init_test(cx, |_| {});
13353
13354 let mut cx = EditorLspTestContext::new(
13355 Language::new(
13356 LanguageConfig {
13357 matcher: LanguageMatcher {
13358 path_suffixes: vec!["jsx".into()],
13359 ..Default::default()
13360 },
13361 overrides: [(
13362 "element".into(),
13363 LanguageConfigOverride {
13364 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13365 ..Default::default()
13366 },
13367 )]
13368 .into_iter()
13369 .collect(),
13370 ..Default::default()
13371 },
13372 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13373 )
13374 .with_override_query("(jsx_self_closing_element) @element")
13375 .unwrap(),
13376 lsp::ServerCapabilities {
13377 completion_provider: Some(lsp::CompletionOptions {
13378 trigger_characters: Some(vec![":".to_string()]),
13379 ..Default::default()
13380 }),
13381 ..Default::default()
13382 },
13383 cx,
13384 )
13385 .await;
13386
13387 cx.lsp
13388 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13389 Ok(Some(lsp::CompletionResponse::Array(vec![
13390 lsp::CompletionItem {
13391 label: "bg-blue".into(),
13392 ..Default::default()
13393 },
13394 lsp::CompletionItem {
13395 label: "bg-red".into(),
13396 ..Default::default()
13397 },
13398 lsp::CompletionItem {
13399 label: "bg-yellow".into(),
13400 ..Default::default()
13401 },
13402 ])))
13403 });
13404
13405 cx.set_state(r#"<p class="bgˇ" />"#);
13406
13407 // Trigger completion when typing a dash, because the dash is an extra
13408 // word character in the 'element' scope, which contains the cursor.
13409 cx.simulate_keystroke("-");
13410 cx.executor().run_until_parked();
13411 cx.update_editor(|editor, _, _| {
13412 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13413 {
13414 assert_eq!(
13415 completion_menu_entries(&menu),
13416 &["bg-red", "bg-blue", "bg-yellow"]
13417 );
13418 } else {
13419 panic!("expected completion menu to be open");
13420 }
13421 });
13422
13423 cx.simulate_keystroke("l");
13424 cx.executor().run_until_parked();
13425 cx.update_editor(|editor, _, _| {
13426 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13427 {
13428 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13429 } else {
13430 panic!("expected completion menu to be open");
13431 }
13432 });
13433
13434 // When filtering completions, consider the character after the '-' to
13435 // be the start of a subword.
13436 cx.set_state(r#"<p class="yelˇ" />"#);
13437 cx.simulate_keystroke("l");
13438 cx.executor().run_until_parked();
13439 cx.update_editor(|editor, _, _| {
13440 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13441 {
13442 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13443 } else {
13444 panic!("expected completion menu to be open");
13445 }
13446 });
13447}
13448
13449fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13450 let entries = menu.entries.borrow();
13451 entries.iter().map(|mat| mat.string.clone()).collect()
13452}
13453
13454#[gpui::test]
13455async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13456 init_test(cx, |settings| {
13457 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13458 FormatterList(vec![Formatter::Prettier].into()),
13459 ))
13460 });
13461
13462 let fs = FakeFs::new(cx.executor());
13463 fs.insert_file(path!("/file.ts"), Default::default()).await;
13464
13465 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13466 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13467
13468 language_registry.add(Arc::new(Language::new(
13469 LanguageConfig {
13470 name: "TypeScript".into(),
13471 matcher: LanguageMatcher {
13472 path_suffixes: vec!["ts".to_string()],
13473 ..Default::default()
13474 },
13475 ..Default::default()
13476 },
13477 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13478 )));
13479 update_test_language_settings(cx, |settings| {
13480 settings.defaults.prettier = Some(PrettierSettings {
13481 allowed: true,
13482 ..PrettierSettings::default()
13483 });
13484 });
13485
13486 let test_plugin = "test_plugin";
13487 let _ = language_registry.register_fake_lsp(
13488 "TypeScript",
13489 FakeLspAdapter {
13490 prettier_plugins: vec![test_plugin],
13491 ..Default::default()
13492 },
13493 );
13494
13495 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13496 let buffer = project
13497 .update(cx, |project, cx| {
13498 project.open_local_buffer(path!("/file.ts"), cx)
13499 })
13500 .await
13501 .unwrap();
13502
13503 let buffer_text = "one\ntwo\nthree\n";
13504 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13505 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13506 editor.update_in(cx, |editor, window, cx| {
13507 editor.set_text(buffer_text, window, cx)
13508 });
13509
13510 editor
13511 .update_in(cx, |editor, window, cx| {
13512 editor.perform_format(
13513 project.clone(),
13514 FormatTrigger::Manual,
13515 FormatTarget::Buffers,
13516 window,
13517 cx,
13518 )
13519 })
13520 .unwrap()
13521 .await;
13522 assert_eq!(
13523 editor.update(cx, |editor, cx| editor.text(cx)),
13524 buffer_text.to_string() + prettier_format_suffix,
13525 "Test prettier formatting was not applied to the original buffer text",
13526 );
13527
13528 update_test_language_settings(cx, |settings| {
13529 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13530 });
13531 let format = editor.update_in(cx, |editor, window, cx| {
13532 editor.perform_format(
13533 project.clone(),
13534 FormatTrigger::Manual,
13535 FormatTarget::Buffers,
13536 window,
13537 cx,
13538 )
13539 });
13540 format.await.unwrap();
13541 assert_eq!(
13542 editor.update(cx, |editor, cx| editor.text(cx)),
13543 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13544 "Autoformatting (via test prettier) was not applied to the original buffer text",
13545 );
13546}
13547
13548#[gpui::test]
13549async fn test_addition_reverts(cx: &mut TestAppContext) {
13550 init_test(cx, |_| {});
13551 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13552 let base_text = indoc! {r#"
13553 struct Row;
13554 struct Row1;
13555 struct Row2;
13556
13557 struct Row4;
13558 struct Row5;
13559 struct Row6;
13560
13561 struct Row8;
13562 struct Row9;
13563 struct Row10;"#};
13564
13565 // When addition hunks are not adjacent to carets, no hunk revert is performed
13566 assert_hunk_revert(
13567 indoc! {r#"struct Row;
13568 struct Row1;
13569 struct Row1.1;
13570 struct Row1.2;
13571 struct Row2;ˇ
13572
13573 struct Row4;
13574 struct Row5;
13575 struct Row6;
13576
13577 struct Row8;
13578 ˇstruct Row9;
13579 struct Row9.1;
13580 struct Row9.2;
13581 struct Row9.3;
13582 struct Row10;"#},
13583 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13584 indoc! {r#"struct Row;
13585 struct Row1;
13586 struct Row1.1;
13587 struct Row1.2;
13588 struct Row2;ˇ
13589
13590 struct Row4;
13591 struct Row5;
13592 struct Row6;
13593
13594 struct Row8;
13595 ˇstruct Row9;
13596 struct Row9.1;
13597 struct Row9.2;
13598 struct Row9.3;
13599 struct Row10;"#},
13600 base_text,
13601 &mut cx,
13602 );
13603 // Same for selections
13604 assert_hunk_revert(
13605 indoc! {r#"struct Row;
13606 struct Row1;
13607 struct Row2;
13608 struct Row2.1;
13609 struct Row2.2;
13610 «ˇ
13611 struct Row4;
13612 struct» Row5;
13613 «struct Row6;
13614 ˇ»
13615 struct Row9.1;
13616 struct Row9.2;
13617 struct Row9.3;
13618 struct Row8;
13619 struct Row9;
13620 struct Row10;"#},
13621 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13622 indoc! {r#"struct Row;
13623 struct Row1;
13624 struct Row2;
13625 struct Row2.1;
13626 struct Row2.2;
13627 «ˇ
13628 struct Row4;
13629 struct» Row5;
13630 «struct Row6;
13631 ˇ»
13632 struct Row9.1;
13633 struct Row9.2;
13634 struct Row9.3;
13635 struct Row8;
13636 struct Row9;
13637 struct Row10;"#},
13638 base_text,
13639 &mut cx,
13640 );
13641
13642 // When carets and selections intersect the addition hunks, those are reverted.
13643 // Adjacent carets got merged.
13644 assert_hunk_revert(
13645 indoc! {r#"struct Row;
13646 ˇ// something on the top
13647 struct Row1;
13648 struct Row2;
13649 struct Roˇw3.1;
13650 struct Row2.2;
13651 struct Row2.3;ˇ
13652
13653 struct Row4;
13654 struct ˇRow5.1;
13655 struct Row5.2;
13656 struct «Rowˇ»5.3;
13657 struct Row5;
13658 struct Row6;
13659 ˇ
13660 struct Row9.1;
13661 struct «Rowˇ»9.2;
13662 struct «ˇRow»9.3;
13663 struct Row8;
13664 struct Row9;
13665 «ˇ// something on bottom»
13666 struct Row10;"#},
13667 vec![
13668 DiffHunkStatusKind::Added,
13669 DiffHunkStatusKind::Added,
13670 DiffHunkStatusKind::Added,
13671 DiffHunkStatusKind::Added,
13672 DiffHunkStatusKind::Added,
13673 ],
13674 indoc! {r#"struct Row;
13675 ˇstruct Row1;
13676 struct Row2;
13677 ˇ
13678 struct Row4;
13679 ˇstruct Row5;
13680 struct Row6;
13681 ˇ
13682 ˇstruct Row8;
13683 struct Row9;
13684 ˇstruct Row10;"#},
13685 base_text,
13686 &mut cx,
13687 );
13688}
13689
13690#[gpui::test]
13691async fn test_modification_reverts(cx: &mut TestAppContext) {
13692 init_test(cx, |_| {});
13693 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13694 let base_text = indoc! {r#"
13695 struct Row;
13696 struct Row1;
13697 struct Row2;
13698
13699 struct Row4;
13700 struct Row5;
13701 struct Row6;
13702
13703 struct Row8;
13704 struct Row9;
13705 struct Row10;"#};
13706
13707 // Modification hunks behave the same as the addition ones.
13708 assert_hunk_revert(
13709 indoc! {r#"struct Row;
13710 struct Row1;
13711 struct Row33;
13712 ˇ
13713 struct Row4;
13714 struct Row5;
13715 struct Row6;
13716 ˇ
13717 struct Row99;
13718 struct Row9;
13719 struct Row10;"#},
13720 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13721 indoc! {r#"struct Row;
13722 struct Row1;
13723 struct Row33;
13724 ˇ
13725 struct Row4;
13726 struct Row5;
13727 struct Row6;
13728 ˇ
13729 struct Row99;
13730 struct Row9;
13731 struct Row10;"#},
13732 base_text,
13733 &mut cx,
13734 );
13735 assert_hunk_revert(
13736 indoc! {r#"struct Row;
13737 struct Row1;
13738 struct Row33;
13739 «ˇ
13740 struct Row4;
13741 struct» Row5;
13742 «struct Row6;
13743 ˇ»
13744 struct Row99;
13745 struct Row9;
13746 struct Row10;"#},
13747 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13748 indoc! {r#"struct Row;
13749 struct Row1;
13750 struct Row33;
13751 «ˇ
13752 struct Row4;
13753 struct» Row5;
13754 «struct Row6;
13755 ˇ»
13756 struct Row99;
13757 struct Row9;
13758 struct Row10;"#},
13759 base_text,
13760 &mut cx,
13761 );
13762
13763 assert_hunk_revert(
13764 indoc! {r#"ˇstruct Row1.1;
13765 struct Row1;
13766 «ˇstr»uct Row22;
13767
13768 struct ˇRow44;
13769 struct Row5;
13770 struct «Rˇ»ow66;ˇ
13771
13772 «struˇ»ct Row88;
13773 struct Row9;
13774 struct Row1011;ˇ"#},
13775 vec![
13776 DiffHunkStatusKind::Modified,
13777 DiffHunkStatusKind::Modified,
13778 DiffHunkStatusKind::Modified,
13779 DiffHunkStatusKind::Modified,
13780 DiffHunkStatusKind::Modified,
13781 DiffHunkStatusKind::Modified,
13782 ],
13783 indoc! {r#"struct Row;
13784 ˇstruct Row1;
13785 struct Row2;
13786 ˇ
13787 struct Row4;
13788 ˇstruct Row5;
13789 struct Row6;
13790 ˇ
13791 struct Row8;
13792 ˇstruct Row9;
13793 struct Row10;ˇ"#},
13794 base_text,
13795 &mut cx,
13796 );
13797}
13798
13799#[gpui::test]
13800async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13801 init_test(cx, |_| {});
13802 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13803 let base_text = indoc! {r#"
13804 one
13805
13806 two
13807 three
13808 "#};
13809
13810 cx.set_head_text(base_text);
13811 cx.set_state("\nˇ\n");
13812 cx.executor().run_until_parked();
13813 cx.update_editor(|editor, _window, cx| {
13814 editor.expand_selected_diff_hunks(cx);
13815 });
13816 cx.executor().run_until_parked();
13817 cx.update_editor(|editor, window, cx| {
13818 editor.backspace(&Default::default(), window, cx);
13819 });
13820 cx.run_until_parked();
13821 cx.assert_state_with_diff(
13822 indoc! {r#"
13823
13824 - two
13825 - threeˇ
13826 +
13827 "#}
13828 .to_string(),
13829 );
13830}
13831
13832#[gpui::test]
13833async fn test_deletion_reverts(cx: &mut TestAppContext) {
13834 init_test(cx, |_| {});
13835 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13836 let base_text = indoc! {r#"struct Row;
13837struct Row1;
13838struct Row2;
13839
13840struct Row4;
13841struct Row5;
13842struct Row6;
13843
13844struct Row8;
13845struct Row9;
13846struct Row10;"#};
13847
13848 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13849 assert_hunk_revert(
13850 indoc! {r#"struct Row;
13851 struct Row2;
13852
13853 ˇstruct Row4;
13854 struct Row5;
13855 struct Row6;
13856 ˇ
13857 struct Row8;
13858 struct Row10;"#},
13859 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13860 indoc! {r#"struct Row;
13861 struct Row2;
13862
13863 ˇstruct Row4;
13864 struct Row5;
13865 struct Row6;
13866 ˇ
13867 struct Row8;
13868 struct Row10;"#},
13869 base_text,
13870 &mut cx,
13871 );
13872 assert_hunk_revert(
13873 indoc! {r#"struct Row;
13874 struct Row2;
13875
13876 «ˇstruct Row4;
13877 struct» Row5;
13878 «struct Row6;
13879 ˇ»
13880 struct Row8;
13881 struct Row10;"#},
13882 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13883 indoc! {r#"struct Row;
13884 struct Row2;
13885
13886 «ˇstruct Row4;
13887 struct» Row5;
13888 «struct Row6;
13889 ˇ»
13890 struct Row8;
13891 struct Row10;"#},
13892 base_text,
13893 &mut cx,
13894 );
13895
13896 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13897 assert_hunk_revert(
13898 indoc! {r#"struct Row;
13899 ˇstruct Row2;
13900
13901 struct Row4;
13902 struct Row5;
13903 struct Row6;
13904
13905 struct Row8;ˇ
13906 struct Row10;"#},
13907 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13908 indoc! {r#"struct Row;
13909 struct Row1;
13910 ˇstruct Row2;
13911
13912 struct Row4;
13913 struct Row5;
13914 struct Row6;
13915
13916 struct Row8;ˇ
13917 struct Row9;
13918 struct Row10;"#},
13919 base_text,
13920 &mut cx,
13921 );
13922 assert_hunk_revert(
13923 indoc! {r#"struct Row;
13924 struct Row2«ˇ;
13925 struct Row4;
13926 struct» Row5;
13927 «struct Row6;
13928
13929 struct Row8;ˇ»
13930 struct Row10;"#},
13931 vec![
13932 DiffHunkStatusKind::Deleted,
13933 DiffHunkStatusKind::Deleted,
13934 DiffHunkStatusKind::Deleted,
13935 ],
13936 indoc! {r#"struct Row;
13937 struct Row1;
13938 struct Row2«ˇ;
13939
13940 struct Row4;
13941 struct» Row5;
13942 «struct Row6;
13943
13944 struct Row8;ˇ»
13945 struct Row9;
13946 struct Row10;"#},
13947 base_text,
13948 &mut cx,
13949 );
13950}
13951
13952#[gpui::test]
13953async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13954 init_test(cx, |_| {});
13955
13956 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13957 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13958 let base_text_3 =
13959 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13960
13961 let text_1 = edit_first_char_of_every_line(base_text_1);
13962 let text_2 = edit_first_char_of_every_line(base_text_2);
13963 let text_3 = edit_first_char_of_every_line(base_text_3);
13964
13965 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13966 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13967 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13968
13969 let multibuffer = cx.new(|cx| {
13970 let mut multibuffer = MultiBuffer::new(ReadWrite);
13971 multibuffer.push_excerpts(
13972 buffer_1.clone(),
13973 [
13974 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13975 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13976 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13977 ],
13978 cx,
13979 );
13980 multibuffer.push_excerpts(
13981 buffer_2.clone(),
13982 [
13983 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13984 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13985 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13986 ],
13987 cx,
13988 );
13989 multibuffer.push_excerpts(
13990 buffer_3.clone(),
13991 [
13992 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13993 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13994 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13995 ],
13996 cx,
13997 );
13998 multibuffer
13999 });
14000
14001 let fs = FakeFs::new(cx.executor());
14002 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14003 let (editor, cx) = cx
14004 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14005 editor.update_in(cx, |editor, _window, cx| {
14006 for (buffer, diff_base) in [
14007 (buffer_1.clone(), base_text_1),
14008 (buffer_2.clone(), base_text_2),
14009 (buffer_3.clone(), base_text_3),
14010 ] {
14011 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14012 editor
14013 .buffer
14014 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14015 }
14016 });
14017 cx.executor().run_until_parked();
14018
14019 editor.update_in(cx, |editor, window, cx| {
14020 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}");
14021 editor.select_all(&SelectAll, window, cx);
14022 editor.git_restore(&Default::default(), window, cx);
14023 });
14024 cx.executor().run_until_parked();
14025
14026 // When all ranges are selected, all buffer hunks are reverted.
14027 editor.update(cx, |editor, cx| {
14028 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");
14029 });
14030 buffer_1.update(cx, |buffer, _| {
14031 assert_eq!(buffer.text(), base_text_1);
14032 });
14033 buffer_2.update(cx, |buffer, _| {
14034 assert_eq!(buffer.text(), base_text_2);
14035 });
14036 buffer_3.update(cx, |buffer, _| {
14037 assert_eq!(buffer.text(), base_text_3);
14038 });
14039
14040 editor.update_in(cx, |editor, window, cx| {
14041 editor.undo(&Default::default(), window, cx);
14042 });
14043
14044 editor.update_in(cx, |editor, window, cx| {
14045 editor.change_selections(None, window, cx, |s| {
14046 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14047 });
14048 editor.git_restore(&Default::default(), window, cx);
14049 });
14050
14051 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14052 // but not affect buffer_2 and its related excerpts.
14053 editor.update(cx, |editor, cx| {
14054 assert_eq!(
14055 editor.text(cx),
14056 "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}"
14057 );
14058 });
14059 buffer_1.update(cx, |buffer, _| {
14060 assert_eq!(buffer.text(), base_text_1);
14061 });
14062 buffer_2.update(cx, |buffer, _| {
14063 assert_eq!(
14064 buffer.text(),
14065 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14066 );
14067 });
14068 buffer_3.update(cx, |buffer, _| {
14069 assert_eq!(
14070 buffer.text(),
14071 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14072 );
14073 });
14074
14075 fn edit_first_char_of_every_line(text: &str) -> String {
14076 text.split('\n')
14077 .map(|line| format!("X{}", &line[1..]))
14078 .collect::<Vec<_>>()
14079 .join("\n")
14080 }
14081}
14082
14083#[gpui::test]
14084async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14085 init_test(cx, |_| {});
14086
14087 let cols = 4;
14088 let rows = 10;
14089 let sample_text_1 = sample_text(rows, cols, 'a');
14090 assert_eq!(
14091 sample_text_1,
14092 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14093 );
14094 let sample_text_2 = sample_text(rows, cols, 'l');
14095 assert_eq!(
14096 sample_text_2,
14097 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14098 );
14099 let sample_text_3 = sample_text(rows, cols, 'v');
14100 assert_eq!(
14101 sample_text_3,
14102 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14103 );
14104
14105 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14106 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14107 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14108
14109 let multi_buffer = cx.new(|cx| {
14110 let mut multibuffer = MultiBuffer::new(ReadWrite);
14111 multibuffer.push_excerpts(
14112 buffer_1.clone(),
14113 [
14114 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14115 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14116 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14117 ],
14118 cx,
14119 );
14120 multibuffer.push_excerpts(
14121 buffer_2.clone(),
14122 [
14123 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14124 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14125 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14126 ],
14127 cx,
14128 );
14129 multibuffer.push_excerpts(
14130 buffer_3.clone(),
14131 [
14132 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14133 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14134 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14135 ],
14136 cx,
14137 );
14138 multibuffer
14139 });
14140
14141 let fs = FakeFs::new(cx.executor());
14142 fs.insert_tree(
14143 "/a",
14144 json!({
14145 "main.rs": sample_text_1,
14146 "other.rs": sample_text_2,
14147 "lib.rs": sample_text_3,
14148 }),
14149 )
14150 .await;
14151 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14152 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14153 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14154 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14155 Editor::new(
14156 EditorMode::Full,
14157 multi_buffer,
14158 Some(project.clone()),
14159 window,
14160 cx,
14161 )
14162 });
14163 let multibuffer_item_id = workspace
14164 .update(cx, |workspace, window, cx| {
14165 assert!(
14166 workspace.active_item(cx).is_none(),
14167 "active item should be None before the first item is added"
14168 );
14169 workspace.add_item_to_active_pane(
14170 Box::new(multi_buffer_editor.clone()),
14171 None,
14172 true,
14173 window,
14174 cx,
14175 );
14176 let active_item = workspace
14177 .active_item(cx)
14178 .expect("should have an active item after adding the multi buffer");
14179 assert!(
14180 !active_item.is_singleton(cx),
14181 "A multi buffer was expected to active after adding"
14182 );
14183 active_item.item_id()
14184 })
14185 .unwrap();
14186 cx.executor().run_until_parked();
14187
14188 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14189 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14190 s.select_ranges(Some(1..2))
14191 });
14192 editor.open_excerpts(&OpenExcerpts, window, cx);
14193 });
14194 cx.executor().run_until_parked();
14195 let first_item_id = workspace
14196 .update(cx, |workspace, window, cx| {
14197 let active_item = workspace
14198 .active_item(cx)
14199 .expect("should have an active item after navigating into the 1st buffer");
14200 let first_item_id = active_item.item_id();
14201 assert_ne!(
14202 first_item_id, multibuffer_item_id,
14203 "Should navigate into the 1st buffer and activate it"
14204 );
14205 assert!(
14206 active_item.is_singleton(cx),
14207 "New active item should be a singleton buffer"
14208 );
14209 assert_eq!(
14210 active_item
14211 .act_as::<Editor>(cx)
14212 .expect("should have navigated into an editor for the 1st buffer")
14213 .read(cx)
14214 .text(cx),
14215 sample_text_1
14216 );
14217
14218 workspace
14219 .go_back(workspace.active_pane().downgrade(), window, cx)
14220 .detach_and_log_err(cx);
14221
14222 first_item_id
14223 })
14224 .unwrap();
14225 cx.executor().run_until_parked();
14226 workspace
14227 .update(cx, |workspace, _, cx| {
14228 let active_item = workspace
14229 .active_item(cx)
14230 .expect("should have an active item after navigating back");
14231 assert_eq!(
14232 active_item.item_id(),
14233 multibuffer_item_id,
14234 "Should navigate back to the multi buffer"
14235 );
14236 assert!(!active_item.is_singleton(cx));
14237 })
14238 .unwrap();
14239
14240 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14241 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14242 s.select_ranges(Some(39..40))
14243 });
14244 editor.open_excerpts(&OpenExcerpts, window, cx);
14245 });
14246 cx.executor().run_until_parked();
14247 let second_item_id = workspace
14248 .update(cx, |workspace, window, cx| {
14249 let active_item = workspace
14250 .active_item(cx)
14251 .expect("should have an active item after navigating into the 2nd buffer");
14252 let second_item_id = active_item.item_id();
14253 assert_ne!(
14254 second_item_id, multibuffer_item_id,
14255 "Should navigate away from the multibuffer"
14256 );
14257 assert_ne!(
14258 second_item_id, first_item_id,
14259 "Should navigate into the 2nd buffer and activate it"
14260 );
14261 assert!(
14262 active_item.is_singleton(cx),
14263 "New active item should be a singleton buffer"
14264 );
14265 assert_eq!(
14266 active_item
14267 .act_as::<Editor>(cx)
14268 .expect("should have navigated into an editor")
14269 .read(cx)
14270 .text(cx),
14271 sample_text_2
14272 );
14273
14274 workspace
14275 .go_back(workspace.active_pane().downgrade(), window, cx)
14276 .detach_and_log_err(cx);
14277
14278 second_item_id
14279 })
14280 .unwrap();
14281 cx.executor().run_until_parked();
14282 workspace
14283 .update(cx, |workspace, _, cx| {
14284 let active_item = workspace
14285 .active_item(cx)
14286 .expect("should have an active item after navigating back from the 2nd buffer");
14287 assert_eq!(
14288 active_item.item_id(),
14289 multibuffer_item_id,
14290 "Should navigate back from the 2nd buffer to the multi buffer"
14291 );
14292 assert!(!active_item.is_singleton(cx));
14293 })
14294 .unwrap();
14295
14296 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14297 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14298 s.select_ranges(Some(70..70))
14299 });
14300 editor.open_excerpts(&OpenExcerpts, window, cx);
14301 });
14302 cx.executor().run_until_parked();
14303 workspace
14304 .update(cx, |workspace, window, cx| {
14305 let active_item = workspace
14306 .active_item(cx)
14307 .expect("should have an active item after navigating into the 3rd buffer");
14308 let third_item_id = active_item.item_id();
14309 assert_ne!(
14310 third_item_id, multibuffer_item_id,
14311 "Should navigate into the 3rd buffer and activate it"
14312 );
14313 assert_ne!(third_item_id, first_item_id);
14314 assert_ne!(third_item_id, second_item_id);
14315 assert!(
14316 active_item.is_singleton(cx),
14317 "New active item should be a singleton buffer"
14318 );
14319 assert_eq!(
14320 active_item
14321 .act_as::<Editor>(cx)
14322 .expect("should have navigated into an editor")
14323 .read(cx)
14324 .text(cx),
14325 sample_text_3
14326 );
14327
14328 workspace
14329 .go_back(workspace.active_pane().downgrade(), window, cx)
14330 .detach_and_log_err(cx);
14331 })
14332 .unwrap();
14333 cx.executor().run_until_parked();
14334 workspace
14335 .update(cx, |workspace, _, cx| {
14336 let active_item = workspace
14337 .active_item(cx)
14338 .expect("should have an active item after navigating back from the 3rd buffer");
14339 assert_eq!(
14340 active_item.item_id(),
14341 multibuffer_item_id,
14342 "Should navigate back from the 3rd buffer to the multi buffer"
14343 );
14344 assert!(!active_item.is_singleton(cx));
14345 })
14346 .unwrap();
14347}
14348
14349#[gpui::test]
14350async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14351 init_test(cx, |_| {});
14352
14353 let mut cx = EditorTestContext::new(cx).await;
14354
14355 let diff_base = r#"
14356 use some::mod;
14357
14358 const A: u32 = 42;
14359
14360 fn main() {
14361 println!("hello");
14362
14363 println!("world");
14364 }
14365 "#
14366 .unindent();
14367
14368 cx.set_state(
14369 &r#"
14370 use some::modified;
14371
14372 ˇ
14373 fn main() {
14374 println!("hello there");
14375
14376 println!("around the");
14377 println!("world");
14378 }
14379 "#
14380 .unindent(),
14381 );
14382
14383 cx.set_head_text(&diff_base);
14384 executor.run_until_parked();
14385
14386 cx.update_editor(|editor, window, cx| {
14387 editor.go_to_next_hunk(&GoToHunk, window, cx);
14388 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14389 });
14390 executor.run_until_parked();
14391 cx.assert_state_with_diff(
14392 r#"
14393 use some::modified;
14394
14395
14396 fn main() {
14397 - println!("hello");
14398 + ˇ println!("hello there");
14399
14400 println!("around the");
14401 println!("world");
14402 }
14403 "#
14404 .unindent(),
14405 );
14406
14407 cx.update_editor(|editor, window, cx| {
14408 for _ in 0..2 {
14409 editor.go_to_next_hunk(&GoToHunk, window, cx);
14410 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14411 }
14412 });
14413 executor.run_until_parked();
14414 cx.assert_state_with_diff(
14415 r#"
14416 - use some::mod;
14417 + ˇuse some::modified;
14418
14419
14420 fn main() {
14421 - println!("hello");
14422 + println!("hello there");
14423
14424 + println!("around the");
14425 println!("world");
14426 }
14427 "#
14428 .unindent(),
14429 );
14430
14431 cx.update_editor(|editor, window, cx| {
14432 editor.go_to_next_hunk(&GoToHunk, window, cx);
14433 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14434 });
14435 executor.run_until_parked();
14436 cx.assert_state_with_diff(
14437 r#"
14438 - use some::mod;
14439 + use some::modified;
14440
14441 - const A: u32 = 42;
14442 ˇ
14443 fn main() {
14444 - println!("hello");
14445 + println!("hello there");
14446
14447 + println!("around the");
14448 println!("world");
14449 }
14450 "#
14451 .unindent(),
14452 );
14453
14454 cx.update_editor(|editor, window, cx| {
14455 editor.cancel(&Cancel, window, cx);
14456 });
14457
14458 cx.assert_state_with_diff(
14459 r#"
14460 use some::modified;
14461
14462 ˇ
14463 fn main() {
14464 println!("hello there");
14465
14466 println!("around the");
14467 println!("world");
14468 }
14469 "#
14470 .unindent(),
14471 );
14472}
14473
14474#[gpui::test]
14475async fn test_diff_base_change_with_expanded_diff_hunks(
14476 executor: BackgroundExecutor,
14477 cx: &mut TestAppContext,
14478) {
14479 init_test(cx, |_| {});
14480
14481 let mut cx = EditorTestContext::new(cx).await;
14482
14483 let diff_base = r#"
14484 use some::mod1;
14485 use some::mod2;
14486
14487 const A: u32 = 42;
14488 const B: u32 = 42;
14489 const C: u32 = 42;
14490
14491 fn main() {
14492 println!("hello");
14493
14494 println!("world");
14495 }
14496 "#
14497 .unindent();
14498
14499 cx.set_state(
14500 &r#"
14501 use some::mod2;
14502
14503 const A: u32 = 42;
14504 const C: u32 = 42;
14505
14506 fn main(ˇ) {
14507 //println!("hello");
14508
14509 println!("world");
14510 //
14511 //
14512 }
14513 "#
14514 .unindent(),
14515 );
14516
14517 cx.set_head_text(&diff_base);
14518 executor.run_until_parked();
14519
14520 cx.update_editor(|editor, window, cx| {
14521 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14522 });
14523 executor.run_until_parked();
14524 cx.assert_state_with_diff(
14525 r#"
14526 - use some::mod1;
14527 use some::mod2;
14528
14529 const A: u32 = 42;
14530 - const B: u32 = 42;
14531 const C: u32 = 42;
14532
14533 fn main(ˇ) {
14534 - println!("hello");
14535 + //println!("hello");
14536
14537 println!("world");
14538 + //
14539 + //
14540 }
14541 "#
14542 .unindent(),
14543 );
14544
14545 cx.set_head_text("new diff base!");
14546 executor.run_until_parked();
14547 cx.assert_state_with_diff(
14548 r#"
14549 - new diff base!
14550 + use some::mod2;
14551 +
14552 + const A: u32 = 42;
14553 + const C: u32 = 42;
14554 +
14555 + fn main(ˇ) {
14556 + //println!("hello");
14557 +
14558 + println!("world");
14559 + //
14560 + //
14561 + }
14562 "#
14563 .unindent(),
14564 );
14565}
14566
14567#[gpui::test]
14568async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14569 init_test(cx, |_| {});
14570
14571 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14572 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14573 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14574 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14575 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14576 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14577
14578 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14579 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14580 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14581
14582 let multi_buffer = cx.new(|cx| {
14583 let mut multibuffer = MultiBuffer::new(ReadWrite);
14584 multibuffer.push_excerpts(
14585 buffer_1.clone(),
14586 [
14587 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14588 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14589 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14590 ],
14591 cx,
14592 );
14593 multibuffer.push_excerpts(
14594 buffer_2.clone(),
14595 [
14596 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14597 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14598 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14599 ],
14600 cx,
14601 );
14602 multibuffer.push_excerpts(
14603 buffer_3.clone(),
14604 [
14605 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14606 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14607 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14608 ],
14609 cx,
14610 );
14611 multibuffer
14612 });
14613
14614 let editor =
14615 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14616 editor
14617 .update(cx, |editor, _window, cx| {
14618 for (buffer, diff_base) in [
14619 (buffer_1.clone(), file_1_old),
14620 (buffer_2.clone(), file_2_old),
14621 (buffer_3.clone(), file_3_old),
14622 ] {
14623 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14624 editor
14625 .buffer
14626 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14627 }
14628 })
14629 .unwrap();
14630
14631 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14632 cx.run_until_parked();
14633
14634 cx.assert_editor_state(
14635 &"
14636 ˇaaa
14637 ccc
14638 ddd
14639
14640 ggg
14641 hhh
14642
14643
14644 lll
14645 mmm
14646 NNN
14647
14648 qqq
14649 rrr
14650
14651 uuu
14652 111
14653 222
14654 333
14655
14656 666
14657 777
14658
14659 000
14660 !!!"
14661 .unindent(),
14662 );
14663
14664 cx.update_editor(|editor, window, cx| {
14665 editor.select_all(&SelectAll, window, cx);
14666 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14667 });
14668 cx.executor().run_until_parked();
14669
14670 cx.assert_state_with_diff(
14671 "
14672 «aaa
14673 - bbb
14674 ccc
14675 ddd
14676
14677 ggg
14678 hhh
14679
14680
14681 lll
14682 mmm
14683 - nnn
14684 + NNN
14685
14686 qqq
14687 rrr
14688
14689 uuu
14690 111
14691 222
14692 333
14693
14694 + 666
14695 777
14696
14697 000
14698 !!!ˇ»"
14699 .unindent(),
14700 );
14701}
14702
14703#[gpui::test]
14704async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14705 init_test(cx, |_| {});
14706
14707 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14708 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14709
14710 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14711 let multi_buffer = cx.new(|cx| {
14712 let mut multibuffer = MultiBuffer::new(ReadWrite);
14713 multibuffer.push_excerpts(
14714 buffer.clone(),
14715 [
14716 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
14717 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
14718 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
14719 ],
14720 cx,
14721 );
14722 multibuffer
14723 });
14724
14725 let editor =
14726 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14727 editor
14728 .update(cx, |editor, _window, cx| {
14729 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14730 editor
14731 .buffer
14732 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14733 })
14734 .unwrap();
14735
14736 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14737 cx.run_until_parked();
14738
14739 cx.update_editor(|editor, window, cx| {
14740 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14741 });
14742 cx.executor().run_until_parked();
14743
14744 // When the start of a hunk coincides with the start of its excerpt,
14745 // the hunk is expanded. When the start of a a hunk is earlier than
14746 // the start of its excerpt, the hunk is not expanded.
14747 cx.assert_state_with_diff(
14748 "
14749 ˇaaa
14750 - bbb
14751 + BBB
14752
14753 - ddd
14754 - eee
14755 + DDD
14756 + EEE
14757 fff
14758
14759 iii
14760 "
14761 .unindent(),
14762 );
14763}
14764
14765#[gpui::test]
14766async fn test_edits_around_expanded_insertion_hunks(
14767 executor: BackgroundExecutor,
14768 cx: &mut TestAppContext,
14769) {
14770 init_test(cx, |_| {});
14771
14772 let mut cx = EditorTestContext::new(cx).await;
14773
14774 let diff_base = r#"
14775 use some::mod1;
14776 use some::mod2;
14777
14778 const A: u32 = 42;
14779
14780 fn main() {
14781 println!("hello");
14782
14783 println!("world");
14784 }
14785 "#
14786 .unindent();
14787 executor.run_until_parked();
14788 cx.set_state(
14789 &r#"
14790 use some::mod1;
14791 use some::mod2;
14792
14793 const A: u32 = 42;
14794 const B: u32 = 42;
14795 const C: u32 = 42;
14796 ˇ
14797
14798 fn main() {
14799 println!("hello");
14800
14801 println!("world");
14802 }
14803 "#
14804 .unindent(),
14805 );
14806
14807 cx.set_head_text(&diff_base);
14808 executor.run_until_parked();
14809
14810 cx.update_editor(|editor, window, cx| {
14811 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14812 });
14813 executor.run_until_parked();
14814
14815 cx.assert_state_with_diff(
14816 r#"
14817 use some::mod1;
14818 use some::mod2;
14819
14820 const A: u32 = 42;
14821 + const B: u32 = 42;
14822 + const C: u32 = 42;
14823 + ˇ
14824
14825 fn main() {
14826 println!("hello");
14827
14828 println!("world");
14829 }
14830 "#
14831 .unindent(),
14832 );
14833
14834 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14835 executor.run_until_parked();
14836
14837 cx.assert_state_with_diff(
14838 r#"
14839 use some::mod1;
14840 use some::mod2;
14841
14842 const A: u32 = 42;
14843 + const B: u32 = 42;
14844 + const C: u32 = 42;
14845 + const D: u32 = 42;
14846 + ˇ
14847
14848 fn main() {
14849 println!("hello");
14850
14851 println!("world");
14852 }
14853 "#
14854 .unindent(),
14855 );
14856
14857 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14858 executor.run_until_parked();
14859
14860 cx.assert_state_with_diff(
14861 r#"
14862 use some::mod1;
14863 use some::mod2;
14864
14865 const A: u32 = 42;
14866 + const B: u32 = 42;
14867 + const C: u32 = 42;
14868 + const D: u32 = 42;
14869 + const E: u32 = 42;
14870 + ˇ
14871
14872 fn main() {
14873 println!("hello");
14874
14875 println!("world");
14876 }
14877 "#
14878 .unindent(),
14879 );
14880
14881 cx.update_editor(|editor, window, cx| {
14882 editor.delete_line(&DeleteLine, window, cx);
14883 });
14884 executor.run_until_parked();
14885
14886 cx.assert_state_with_diff(
14887 r#"
14888 use some::mod1;
14889 use some::mod2;
14890
14891 const A: u32 = 42;
14892 + const B: u32 = 42;
14893 + const C: u32 = 42;
14894 + const D: u32 = 42;
14895 + const E: u32 = 42;
14896 ˇ
14897 fn main() {
14898 println!("hello");
14899
14900 println!("world");
14901 }
14902 "#
14903 .unindent(),
14904 );
14905
14906 cx.update_editor(|editor, window, cx| {
14907 editor.move_up(&MoveUp, window, cx);
14908 editor.delete_line(&DeleteLine, window, cx);
14909 editor.move_up(&MoveUp, window, cx);
14910 editor.delete_line(&DeleteLine, window, cx);
14911 editor.move_up(&MoveUp, window, cx);
14912 editor.delete_line(&DeleteLine, window, cx);
14913 });
14914 executor.run_until_parked();
14915 cx.assert_state_with_diff(
14916 r#"
14917 use some::mod1;
14918 use some::mod2;
14919
14920 const A: u32 = 42;
14921 + const B: u32 = 42;
14922 ˇ
14923 fn main() {
14924 println!("hello");
14925
14926 println!("world");
14927 }
14928 "#
14929 .unindent(),
14930 );
14931
14932 cx.update_editor(|editor, window, cx| {
14933 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14934 editor.delete_line(&DeleteLine, window, cx);
14935 });
14936 executor.run_until_parked();
14937 cx.assert_state_with_diff(
14938 r#"
14939 ˇ
14940 fn main() {
14941 println!("hello");
14942
14943 println!("world");
14944 }
14945 "#
14946 .unindent(),
14947 );
14948}
14949
14950#[gpui::test]
14951async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14952 init_test(cx, |_| {});
14953
14954 let mut cx = EditorTestContext::new(cx).await;
14955 cx.set_head_text(indoc! { "
14956 one
14957 two
14958 three
14959 four
14960 five
14961 "
14962 });
14963 cx.set_state(indoc! { "
14964 one
14965 ˇthree
14966 five
14967 "});
14968 cx.run_until_parked();
14969 cx.update_editor(|editor, window, cx| {
14970 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14971 });
14972 cx.assert_state_with_diff(
14973 indoc! { "
14974 one
14975 - two
14976 ˇthree
14977 - four
14978 five
14979 "}
14980 .to_string(),
14981 );
14982 cx.update_editor(|editor, window, cx| {
14983 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14984 });
14985
14986 cx.assert_state_with_diff(
14987 indoc! { "
14988 one
14989 ˇthree
14990 five
14991 "}
14992 .to_string(),
14993 );
14994
14995 cx.set_state(indoc! { "
14996 one
14997 ˇTWO
14998 three
14999 four
15000 five
15001 "});
15002 cx.run_until_parked();
15003 cx.update_editor(|editor, window, cx| {
15004 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15005 });
15006
15007 cx.assert_state_with_diff(
15008 indoc! { "
15009 one
15010 - two
15011 + ˇTWO
15012 three
15013 four
15014 five
15015 "}
15016 .to_string(),
15017 );
15018 cx.update_editor(|editor, window, cx| {
15019 editor.move_up(&Default::default(), window, cx);
15020 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15021 });
15022 cx.assert_state_with_diff(
15023 indoc! { "
15024 one
15025 ˇTWO
15026 three
15027 four
15028 five
15029 "}
15030 .to_string(),
15031 );
15032}
15033
15034#[gpui::test]
15035async fn test_edits_around_expanded_deletion_hunks(
15036 executor: BackgroundExecutor,
15037 cx: &mut TestAppContext,
15038) {
15039 init_test(cx, |_| {});
15040
15041 let mut cx = EditorTestContext::new(cx).await;
15042
15043 let diff_base = r#"
15044 use some::mod1;
15045 use some::mod2;
15046
15047 const A: u32 = 42;
15048 const B: u32 = 42;
15049 const C: u32 = 42;
15050
15051
15052 fn main() {
15053 println!("hello");
15054
15055 println!("world");
15056 }
15057 "#
15058 .unindent();
15059 executor.run_until_parked();
15060 cx.set_state(
15061 &r#"
15062 use some::mod1;
15063 use some::mod2;
15064
15065 ˇconst B: u32 = 42;
15066 const C: u32 = 42;
15067
15068
15069 fn main() {
15070 println!("hello");
15071
15072 println!("world");
15073 }
15074 "#
15075 .unindent(),
15076 );
15077
15078 cx.set_head_text(&diff_base);
15079 executor.run_until_parked();
15080
15081 cx.update_editor(|editor, window, cx| {
15082 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15083 });
15084 executor.run_until_parked();
15085
15086 cx.assert_state_with_diff(
15087 r#"
15088 use some::mod1;
15089 use some::mod2;
15090
15091 - const A: u32 = 42;
15092 ˇconst B: u32 = 42;
15093 const C: u32 = 42;
15094
15095
15096 fn main() {
15097 println!("hello");
15098
15099 println!("world");
15100 }
15101 "#
15102 .unindent(),
15103 );
15104
15105 cx.update_editor(|editor, window, cx| {
15106 editor.delete_line(&DeleteLine, window, cx);
15107 });
15108 executor.run_until_parked();
15109 cx.assert_state_with_diff(
15110 r#"
15111 use some::mod1;
15112 use some::mod2;
15113
15114 - const A: u32 = 42;
15115 - const B: u32 = 42;
15116 ˇconst C: u32 = 42;
15117
15118
15119 fn main() {
15120 println!("hello");
15121
15122 println!("world");
15123 }
15124 "#
15125 .unindent(),
15126 );
15127
15128 cx.update_editor(|editor, window, cx| {
15129 editor.delete_line(&DeleteLine, window, cx);
15130 });
15131 executor.run_until_parked();
15132 cx.assert_state_with_diff(
15133 r#"
15134 use some::mod1;
15135 use some::mod2;
15136
15137 - const A: u32 = 42;
15138 - const B: u32 = 42;
15139 - const C: u32 = 42;
15140 ˇ
15141
15142 fn main() {
15143 println!("hello");
15144
15145 println!("world");
15146 }
15147 "#
15148 .unindent(),
15149 );
15150
15151 cx.update_editor(|editor, window, cx| {
15152 editor.handle_input("replacement", window, cx);
15153 });
15154 executor.run_until_parked();
15155 cx.assert_state_with_diff(
15156 r#"
15157 use some::mod1;
15158 use some::mod2;
15159
15160 - const A: u32 = 42;
15161 - const B: u32 = 42;
15162 - const C: u32 = 42;
15163 -
15164 + replacementˇ
15165
15166 fn main() {
15167 println!("hello");
15168
15169 println!("world");
15170 }
15171 "#
15172 .unindent(),
15173 );
15174}
15175
15176#[gpui::test]
15177async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15178 init_test(cx, |_| {});
15179
15180 let mut cx = EditorTestContext::new(cx).await;
15181
15182 let base_text = r#"
15183 one
15184 two
15185 three
15186 four
15187 five
15188 "#
15189 .unindent();
15190 executor.run_until_parked();
15191 cx.set_state(
15192 &r#"
15193 one
15194 two
15195 fˇour
15196 five
15197 "#
15198 .unindent(),
15199 );
15200
15201 cx.set_head_text(&base_text);
15202 executor.run_until_parked();
15203
15204 cx.update_editor(|editor, window, cx| {
15205 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15206 });
15207 executor.run_until_parked();
15208
15209 cx.assert_state_with_diff(
15210 r#"
15211 one
15212 two
15213 - three
15214 fˇour
15215 five
15216 "#
15217 .unindent(),
15218 );
15219
15220 cx.update_editor(|editor, window, cx| {
15221 editor.backspace(&Backspace, window, cx);
15222 editor.backspace(&Backspace, window, cx);
15223 });
15224 executor.run_until_parked();
15225 cx.assert_state_with_diff(
15226 r#"
15227 one
15228 two
15229 - threeˇ
15230 - four
15231 + our
15232 five
15233 "#
15234 .unindent(),
15235 );
15236}
15237
15238#[gpui::test]
15239async fn test_edit_after_expanded_modification_hunk(
15240 executor: BackgroundExecutor,
15241 cx: &mut TestAppContext,
15242) {
15243 init_test(cx, |_| {});
15244
15245 let mut cx = EditorTestContext::new(cx).await;
15246
15247 let diff_base = r#"
15248 use some::mod1;
15249 use some::mod2;
15250
15251 const A: u32 = 42;
15252 const B: u32 = 42;
15253 const C: u32 = 42;
15254 const D: u32 = 42;
15255
15256
15257 fn main() {
15258 println!("hello");
15259
15260 println!("world");
15261 }"#
15262 .unindent();
15263
15264 cx.set_state(
15265 &r#"
15266 use some::mod1;
15267 use some::mod2;
15268
15269 const A: u32 = 42;
15270 const B: u32 = 42;
15271 const C: u32 = 43ˇ
15272 const D: u32 = 42;
15273
15274
15275 fn main() {
15276 println!("hello");
15277
15278 println!("world");
15279 }"#
15280 .unindent(),
15281 );
15282
15283 cx.set_head_text(&diff_base);
15284 executor.run_until_parked();
15285 cx.update_editor(|editor, window, cx| {
15286 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15287 });
15288 executor.run_until_parked();
15289
15290 cx.assert_state_with_diff(
15291 r#"
15292 use some::mod1;
15293 use some::mod2;
15294
15295 const A: u32 = 42;
15296 const B: u32 = 42;
15297 - const C: u32 = 42;
15298 + const C: u32 = 43ˇ
15299 const D: u32 = 42;
15300
15301
15302 fn main() {
15303 println!("hello");
15304
15305 println!("world");
15306 }"#
15307 .unindent(),
15308 );
15309
15310 cx.update_editor(|editor, window, cx| {
15311 editor.handle_input("\nnew_line\n", window, cx);
15312 });
15313 executor.run_until_parked();
15314
15315 cx.assert_state_with_diff(
15316 r#"
15317 use some::mod1;
15318 use some::mod2;
15319
15320 const A: u32 = 42;
15321 const B: u32 = 42;
15322 - const C: u32 = 42;
15323 + const C: u32 = 43
15324 + new_line
15325 + ˇ
15326 const D: u32 = 42;
15327
15328
15329 fn main() {
15330 println!("hello");
15331
15332 println!("world");
15333 }"#
15334 .unindent(),
15335 );
15336}
15337
15338#[gpui::test]
15339async fn test_stage_and_unstage_added_file_hunk(
15340 executor: BackgroundExecutor,
15341 cx: &mut TestAppContext,
15342) {
15343 init_test(cx, |_| {});
15344
15345 let mut cx = EditorTestContext::new(cx).await;
15346 cx.update_editor(|editor, _, cx| {
15347 editor.set_expand_all_diff_hunks(cx);
15348 });
15349
15350 let working_copy = r#"
15351 ˇfn main() {
15352 println!("hello, world!");
15353 }
15354 "#
15355 .unindent();
15356
15357 cx.set_state(&working_copy);
15358 executor.run_until_parked();
15359
15360 cx.assert_state_with_diff(
15361 r#"
15362 + ˇfn main() {
15363 + println!("hello, world!");
15364 + }
15365 "#
15366 .unindent(),
15367 );
15368 cx.assert_index_text(None);
15369
15370 cx.update_editor(|editor, window, cx| {
15371 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15372 });
15373 executor.run_until_parked();
15374 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15375 cx.assert_state_with_diff(
15376 r#"
15377 + ˇfn main() {
15378 + println!("hello, world!");
15379 + }
15380 "#
15381 .unindent(),
15382 );
15383
15384 cx.update_editor(|editor, window, cx| {
15385 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15386 });
15387 executor.run_until_parked();
15388 cx.assert_index_text(None);
15389}
15390
15391async fn setup_indent_guides_editor(
15392 text: &str,
15393 cx: &mut TestAppContext,
15394) -> (BufferId, EditorTestContext) {
15395 init_test(cx, |_| {});
15396
15397 let mut cx = EditorTestContext::new(cx).await;
15398
15399 let buffer_id = cx.update_editor(|editor, window, cx| {
15400 editor.set_text(text, window, cx);
15401 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15402
15403 buffer_ids[0]
15404 });
15405
15406 (buffer_id, cx)
15407}
15408
15409fn assert_indent_guides(
15410 range: Range<u32>,
15411 expected: Vec<IndentGuide>,
15412 active_indices: Option<Vec<usize>>,
15413 cx: &mut EditorTestContext,
15414) {
15415 let indent_guides = cx.update_editor(|editor, window, cx| {
15416 let snapshot = editor.snapshot(window, cx).display_snapshot;
15417 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15418 editor,
15419 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15420 true,
15421 &snapshot,
15422 cx,
15423 );
15424
15425 indent_guides.sort_by(|a, b| {
15426 a.depth.cmp(&b.depth).then(
15427 a.start_row
15428 .cmp(&b.start_row)
15429 .then(a.end_row.cmp(&b.end_row)),
15430 )
15431 });
15432 indent_guides
15433 });
15434
15435 if let Some(expected) = active_indices {
15436 let active_indices = cx.update_editor(|editor, window, cx| {
15437 let snapshot = editor.snapshot(window, cx).display_snapshot;
15438 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15439 });
15440
15441 assert_eq!(
15442 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15443 expected,
15444 "Active indent guide indices do not match"
15445 );
15446 }
15447
15448 assert_eq!(indent_guides, expected, "Indent guides do not match");
15449}
15450
15451fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15452 IndentGuide {
15453 buffer_id,
15454 start_row: MultiBufferRow(start_row),
15455 end_row: MultiBufferRow(end_row),
15456 depth,
15457 tab_size: 4,
15458 settings: IndentGuideSettings {
15459 enabled: true,
15460 line_width: 1,
15461 active_line_width: 1,
15462 ..Default::default()
15463 },
15464 }
15465}
15466
15467#[gpui::test]
15468async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15469 let (buffer_id, mut cx) = setup_indent_guides_editor(
15470 &"
15471 fn main() {
15472 let a = 1;
15473 }"
15474 .unindent(),
15475 cx,
15476 )
15477 .await;
15478
15479 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15480}
15481
15482#[gpui::test]
15483async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15484 let (buffer_id, mut cx) = setup_indent_guides_editor(
15485 &"
15486 fn main() {
15487 let a = 1;
15488 let b = 2;
15489 }"
15490 .unindent(),
15491 cx,
15492 )
15493 .await;
15494
15495 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15496}
15497
15498#[gpui::test]
15499async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15500 let (buffer_id, mut cx) = setup_indent_guides_editor(
15501 &"
15502 fn main() {
15503 let a = 1;
15504 if a == 3 {
15505 let b = 2;
15506 } else {
15507 let c = 3;
15508 }
15509 }"
15510 .unindent(),
15511 cx,
15512 )
15513 .await;
15514
15515 assert_indent_guides(
15516 0..8,
15517 vec![
15518 indent_guide(buffer_id, 1, 6, 0),
15519 indent_guide(buffer_id, 3, 3, 1),
15520 indent_guide(buffer_id, 5, 5, 1),
15521 ],
15522 None,
15523 &mut cx,
15524 );
15525}
15526
15527#[gpui::test]
15528async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15529 let (buffer_id, mut cx) = setup_indent_guides_editor(
15530 &"
15531 fn main() {
15532 let a = 1;
15533 let b = 2;
15534 let c = 3;
15535 }"
15536 .unindent(),
15537 cx,
15538 )
15539 .await;
15540
15541 assert_indent_guides(
15542 0..5,
15543 vec![
15544 indent_guide(buffer_id, 1, 3, 0),
15545 indent_guide(buffer_id, 2, 2, 1),
15546 ],
15547 None,
15548 &mut cx,
15549 );
15550}
15551
15552#[gpui::test]
15553async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15554 let (buffer_id, mut cx) = setup_indent_guides_editor(
15555 &"
15556 fn main() {
15557 let a = 1;
15558
15559 let c = 3;
15560 }"
15561 .unindent(),
15562 cx,
15563 )
15564 .await;
15565
15566 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15567}
15568
15569#[gpui::test]
15570async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15571 let (buffer_id, mut cx) = setup_indent_guides_editor(
15572 &"
15573 fn main() {
15574 let a = 1;
15575
15576 let c = 3;
15577
15578 if a == 3 {
15579 let b = 2;
15580 } else {
15581 let c = 3;
15582 }
15583 }"
15584 .unindent(),
15585 cx,
15586 )
15587 .await;
15588
15589 assert_indent_guides(
15590 0..11,
15591 vec![
15592 indent_guide(buffer_id, 1, 9, 0),
15593 indent_guide(buffer_id, 6, 6, 1),
15594 indent_guide(buffer_id, 8, 8, 1),
15595 ],
15596 None,
15597 &mut cx,
15598 );
15599}
15600
15601#[gpui::test]
15602async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15603 let (buffer_id, mut cx) = setup_indent_guides_editor(
15604 &"
15605 fn main() {
15606 let a = 1;
15607
15608 let c = 3;
15609
15610 if a == 3 {
15611 let b = 2;
15612 } else {
15613 let c = 3;
15614 }
15615 }"
15616 .unindent(),
15617 cx,
15618 )
15619 .await;
15620
15621 assert_indent_guides(
15622 1..11,
15623 vec![
15624 indent_guide(buffer_id, 1, 9, 0),
15625 indent_guide(buffer_id, 6, 6, 1),
15626 indent_guide(buffer_id, 8, 8, 1),
15627 ],
15628 None,
15629 &mut cx,
15630 );
15631}
15632
15633#[gpui::test]
15634async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15635 let (buffer_id, mut cx) = setup_indent_guides_editor(
15636 &"
15637 fn main() {
15638 let a = 1;
15639
15640 let c = 3;
15641
15642 if a == 3 {
15643 let b = 2;
15644 } else {
15645 let c = 3;
15646 }
15647 }"
15648 .unindent(),
15649 cx,
15650 )
15651 .await;
15652
15653 assert_indent_guides(
15654 1..10,
15655 vec![
15656 indent_guide(buffer_id, 1, 9, 0),
15657 indent_guide(buffer_id, 6, 6, 1),
15658 indent_guide(buffer_id, 8, 8, 1),
15659 ],
15660 None,
15661 &mut cx,
15662 );
15663}
15664
15665#[gpui::test]
15666async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15667 let (buffer_id, mut cx) = setup_indent_guides_editor(
15668 &"
15669 block1
15670 block2
15671 block3
15672 block4
15673 block2
15674 block1
15675 block1"
15676 .unindent(),
15677 cx,
15678 )
15679 .await;
15680
15681 assert_indent_guides(
15682 1..10,
15683 vec![
15684 indent_guide(buffer_id, 1, 4, 0),
15685 indent_guide(buffer_id, 2, 3, 1),
15686 indent_guide(buffer_id, 3, 3, 2),
15687 ],
15688 None,
15689 &mut cx,
15690 );
15691}
15692
15693#[gpui::test]
15694async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15695 let (buffer_id, mut cx) = setup_indent_guides_editor(
15696 &"
15697 block1
15698 block2
15699 block3
15700
15701 block1
15702 block1"
15703 .unindent(),
15704 cx,
15705 )
15706 .await;
15707
15708 assert_indent_guides(
15709 0..6,
15710 vec![
15711 indent_guide(buffer_id, 1, 2, 0),
15712 indent_guide(buffer_id, 2, 2, 1),
15713 ],
15714 None,
15715 &mut cx,
15716 );
15717}
15718
15719#[gpui::test]
15720async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15721 let (buffer_id, mut cx) = setup_indent_guides_editor(
15722 &"
15723 block1
15724
15725
15726
15727 block2
15728 "
15729 .unindent(),
15730 cx,
15731 )
15732 .await;
15733
15734 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15735}
15736
15737#[gpui::test]
15738async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15739 let (buffer_id, mut cx) = setup_indent_guides_editor(
15740 &"
15741 def a:
15742 \tb = 3
15743 \tif True:
15744 \t\tc = 4
15745 \t\td = 5
15746 \tprint(b)
15747 "
15748 .unindent(),
15749 cx,
15750 )
15751 .await;
15752
15753 assert_indent_guides(
15754 0..6,
15755 vec![
15756 indent_guide(buffer_id, 1, 6, 0),
15757 indent_guide(buffer_id, 3, 4, 1),
15758 ],
15759 None,
15760 &mut cx,
15761 );
15762}
15763
15764#[gpui::test]
15765async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15766 let (buffer_id, mut cx) = setup_indent_guides_editor(
15767 &"
15768 fn main() {
15769 let a = 1;
15770 }"
15771 .unindent(),
15772 cx,
15773 )
15774 .await;
15775
15776 cx.update_editor(|editor, window, cx| {
15777 editor.change_selections(None, window, cx, |s| {
15778 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15779 });
15780 });
15781
15782 assert_indent_guides(
15783 0..3,
15784 vec![indent_guide(buffer_id, 1, 1, 0)],
15785 Some(vec![0]),
15786 &mut cx,
15787 );
15788}
15789
15790#[gpui::test]
15791async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15792 let (buffer_id, mut cx) = setup_indent_guides_editor(
15793 &"
15794 fn main() {
15795 if 1 == 2 {
15796 let a = 1;
15797 }
15798 }"
15799 .unindent(),
15800 cx,
15801 )
15802 .await;
15803
15804 cx.update_editor(|editor, window, cx| {
15805 editor.change_selections(None, window, cx, |s| {
15806 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15807 });
15808 });
15809
15810 assert_indent_guides(
15811 0..4,
15812 vec![
15813 indent_guide(buffer_id, 1, 3, 0),
15814 indent_guide(buffer_id, 2, 2, 1),
15815 ],
15816 Some(vec![1]),
15817 &mut cx,
15818 );
15819
15820 cx.update_editor(|editor, window, cx| {
15821 editor.change_selections(None, window, cx, |s| {
15822 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15823 });
15824 });
15825
15826 assert_indent_guides(
15827 0..4,
15828 vec![
15829 indent_guide(buffer_id, 1, 3, 0),
15830 indent_guide(buffer_id, 2, 2, 1),
15831 ],
15832 Some(vec![1]),
15833 &mut cx,
15834 );
15835
15836 cx.update_editor(|editor, window, cx| {
15837 editor.change_selections(None, window, cx, |s| {
15838 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15839 });
15840 });
15841
15842 assert_indent_guides(
15843 0..4,
15844 vec![
15845 indent_guide(buffer_id, 1, 3, 0),
15846 indent_guide(buffer_id, 2, 2, 1),
15847 ],
15848 Some(vec![0]),
15849 &mut cx,
15850 );
15851}
15852
15853#[gpui::test]
15854async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15855 let (buffer_id, mut cx) = setup_indent_guides_editor(
15856 &"
15857 fn main() {
15858 let a = 1;
15859
15860 let b = 2;
15861 }"
15862 .unindent(),
15863 cx,
15864 )
15865 .await;
15866
15867 cx.update_editor(|editor, window, cx| {
15868 editor.change_selections(None, window, cx, |s| {
15869 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15870 });
15871 });
15872
15873 assert_indent_guides(
15874 0..5,
15875 vec![indent_guide(buffer_id, 1, 3, 0)],
15876 Some(vec![0]),
15877 &mut cx,
15878 );
15879}
15880
15881#[gpui::test]
15882async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15883 let (buffer_id, mut cx) = setup_indent_guides_editor(
15884 &"
15885 def m:
15886 a = 1
15887 pass"
15888 .unindent(),
15889 cx,
15890 )
15891 .await;
15892
15893 cx.update_editor(|editor, window, cx| {
15894 editor.change_selections(None, window, cx, |s| {
15895 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15896 });
15897 });
15898
15899 assert_indent_guides(
15900 0..3,
15901 vec![indent_guide(buffer_id, 1, 2, 0)],
15902 Some(vec![0]),
15903 &mut cx,
15904 );
15905}
15906
15907#[gpui::test]
15908async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15909 init_test(cx, |_| {});
15910 let mut cx = EditorTestContext::new(cx).await;
15911 let text = indoc! {
15912 "
15913 impl A {
15914 fn b() {
15915 0;
15916 3;
15917 5;
15918 6;
15919 7;
15920 }
15921 }
15922 "
15923 };
15924 let base_text = indoc! {
15925 "
15926 impl A {
15927 fn b() {
15928 0;
15929 1;
15930 2;
15931 3;
15932 4;
15933 }
15934 fn c() {
15935 5;
15936 6;
15937 7;
15938 }
15939 }
15940 "
15941 };
15942
15943 cx.update_editor(|editor, window, cx| {
15944 editor.set_text(text, window, cx);
15945
15946 editor.buffer().update(cx, |multibuffer, cx| {
15947 let buffer = multibuffer.as_singleton().unwrap();
15948 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15949
15950 multibuffer.set_all_diff_hunks_expanded(cx);
15951 multibuffer.add_diff(diff, cx);
15952
15953 buffer.read(cx).remote_id()
15954 })
15955 });
15956 cx.run_until_parked();
15957
15958 cx.assert_state_with_diff(
15959 indoc! { "
15960 impl A {
15961 fn b() {
15962 0;
15963 - 1;
15964 - 2;
15965 3;
15966 - 4;
15967 - }
15968 - fn c() {
15969 5;
15970 6;
15971 7;
15972 }
15973 }
15974 ˇ"
15975 }
15976 .to_string(),
15977 );
15978
15979 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15980 editor
15981 .snapshot(window, cx)
15982 .buffer_snapshot
15983 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15984 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15985 .collect::<Vec<_>>()
15986 });
15987 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15988 assert_eq!(
15989 actual_guides,
15990 vec![
15991 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15992 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15993 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15994 ]
15995 );
15996}
15997
15998#[gpui::test]
15999async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16000 init_test(cx, |_| {});
16001 let mut cx = EditorTestContext::new(cx).await;
16002
16003 let diff_base = r#"
16004 a
16005 b
16006 c
16007 "#
16008 .unindent();
16009
16010 cx.set_state(
16011 &r#"
16012 ˇA
16013 b
16014 C
16015 "#
16016 .unindent(),
16017 );
16018 cx.set_head_text(&diff_base);
16019 cx.update_editor(|editor, window, cx| {
16020 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16021 });
16022 executor.run_until_parked();
16023
16024 let both_hunks_expanded = r#"
16025 - a
16026 + ˇA
16027 b
16028 - c
16029 + C
16030 "#
16031 .unindent();
16032
16033 cx.assert_state_with_diff(both_hunks_expanded.clone());
16034
16035 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16036 let snapshot = editor.snapshot(window, cx);
16037 let hunks = editor
16038 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16039 .collect::<Vec<_>>();
16040 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16041 let buffer_id = hunks[0].buffer_id;
16042 hunks
16043 .into_iter()
16044 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16045 .collect::<Vec<_>>()
16046 });
16047 assert_eq!(hunk_ranges.len(), 2);
16048
16049 cx.update_editor(|editor, _, cx| {
16050 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16051 });
16052 executor.run_until_parked();
16053
16054 let second_hunk_expanded = r#"
16055 ˇA
16056 b
16057 - c
16058 + C
16059 "#
16060 .unindent();
16061
16062 cx.assert_state_with_diff(second_hunk_expanded);
16063
16064 cx.update_editor(|editor, _, cx| {
16065 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16066 });
16067 executor.run_until_parked();
16068
16069 cx.assert_state_with_diff(both_hunks_expanded.clone());
16070
16071 cx.update_editor(|editor, _, cx| {
16072 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16073 });
16074 executor.run_until_parked();
16075
16076 let first_hunk_expanded = r#"
16077 - a
16078 + ˇA
16079 b
16080 C
16081 "#
16082 .unindent();
16083
16084 cx.assert_state_with_diff(first_hunk_expanded);
16085
16086 cx.update_editor(|editor, _, cx| {
16087 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16088 });
16089 executor.run_until_parked();
16090
16091 cx.assert_state_with_diff(both_hunks_expanded);
16092
16093 cx.set_state(
16094 &r#"
16095 ˇA
16096 b
16097 "#
16098 .unindent(),
16099 );
16100 cx.run_until_parked();
16101
16102 // TODO this cursor position seems bad
16103 cx.assert_state_with_diff(
16104 r#"
16105 - ˇa
16106 + A
16107 b
16108 "#
16109 .unindent(),
16110 );
16111
16112 cx.update_editor(|editor, window, cx| {
16113 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16114 });
16115
16116 cx.assert_state_with_diff(
16117 r#"
16118 - ˇa
16119 + A
16120 b
16121 - c
16122 "#
16123 .unindent(),
16124 );
16125
16126 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16127 let snapshot = editor.snapshot(window, cx);
16128 let hunks = editor
16129 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16130 .collect::<Vec<_>>();
16131 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16132 let buffer_id = hunks[0].buffer_id;
16133 hunks
16134 .into_iter()
16135 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16136 .collect::<Vec<_>>()
16137 });
16138 assert_eq!(hunk_ranges.len(), 2);
16139
16140 cx.update_editor(|editor, _, cx| {
16141 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16142 });
16143 executor.run_until_parked();
16144
16145 cx.assert_state_with_diff(
16146 r#"
16147 - ˇa
16148 + A
16149 b
16150 "#
16151 .unindent(),
16152 );
16153}
16154
16155#[gpui::test]
16156async fn test_toggle_deletion_hunk_at_start_of_file(
16157 executor: BackgroundExecutor,
16158 cx: &mut TestAppContext,
16159) {
16160 init_test(cx, |_| {});
16161 let mut cx = EditorTestContext::new(cx).await;
16162
16163 let diff_base = r#"
16164 a
16165 b
16166 c
16167 "#
16168 .unindent();
16169
16170 cx.set_state(
16171 &r#"
16172 ˇb
16173 c
16174 "#
16175 .unindent(),
16176 );
16177 cx.set_head_text(&diff_base);
16178 cx.update_editor(|editor, window, cx| {
16179 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16180 });
16181 executor.run_until_parked();
16182
16183 let hunk_expanded = r#"
16184 - a
16185 ˇb
16186 c
16187 "#
16188 .unindent();
16189
16190 cx.assert_state_with_diff(hunk_expanded.clone());
16191
16192 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16193 let snapshot = editor.snapshot(window, cx);
16194 let hunks = editor
16195 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16196 .collect::<Vec<_>>();
16197 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16198 let buffer_id = hunks[0].buffer_id;
16199 hunks
16200 .into_iter()
16201 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16202 .collect::<Vec<_>>()
16203 });
16204 assert_eq!(hunk_ranges.len(), 1);
16205
16206 cx.update_editor(|editor, _, cx| {
16207 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16208 });
16209 executor.run_until_parked();
16210
16211 let hunk_collapsed = r#"
16212 ˇb
16213 c
16214 "#
16215 .unindent();
16216
16217 cx.assert_state_with_diff(hunk_collapsed);
16218
16219 cx.update_editor(|editor, _, cx| {
16220 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16221 });
16222 executor.run_until_parked();
16223
16224 cx.assert_state_with_diff(hunk_expanded.clone());
16225}
16226
16227#[gpui::test]
16228async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16229 init_test(cx, |_| {});
16230
16231 let fs = FakeFs::new(cx.executor());
16232 fs.insert_tree(
16233 path!("/test"),
16234 json!({
16235 ".git": {},
16236 "file-1": "ONE\n",
16237 "file-2": "TWO\n",
16238 "file-3": "THREE\n",
16239 }),
16240 )
16241 .await;
16242
16243 fs.set_head_for_repo(
16244 path!("/test/.git").as_ref(),
16245 &[
16246 ("file-1".into(), "one\n".into()),
16247 ("file-2".into(), "two\n".into()),
16248 ("file-3".into(), "three\n".into()),
16249 ],
16250 );
16251
16252 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16253 let mut buffers = vec![];
16254 for i in 1..=3 {
16255 let buffer = project
16256 .update(cx, |project, cx| {
16257 let path = format!(path!("/test/file-{}"), i);
16258 project.open_local_buffer(path, cx)
16259 })
16260 .await
16261 .unwrap();
16262 buffers.push(buffer);
16263 }
16264
16265 let multibuffer = cx.new(|cx| {
16266 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16267 multibuffer.set_all_diff_hunks_expanded(cx);
16268 for buffer in &buffers {
16269 let snapshot = buffer.read(cx).snapshot();
16270 multibuffer.set_excerpts_for_path(
16271 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16272 buffer.clone(),
16273 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16274 DEFAULT_MULTIBUFFER_CONTEXT,
16275 cx,
16276 );
16277 }
16278 multibuffer
16279 });
16280
16281 let editor = cx.add_window(|window, cx| {
16282 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16283 });
16284 cx.run_until_parked();
16285
16286 let snapshot = editor
16287 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16288 .unwrap();
16289 let hunks = snapshot
16290 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16291 .map(|hunk| match hunk {
16292 DisplayDiffHunk::Unfolded {
16293 display_row_range, ..
16294 } => display_row_range,
16295 DisplayDiffHunk::Folded { .. } => unreachable!(),
16296 })
16297 .collect::<Vec<_>>();
16298 assert_eq!(
16299 hunks,
16300 [
16301 DisplayRow(2)..DisplayRow(4),
16302 DisplayRow(7)..DisplayRow(9),
16303 DisplayRow(12)..DisplayRow(14),
16304 ]
16305 );
16306}
16307
16308#[gpui::test]
16309async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16310 init_test(cx, |_| {});
16311
16312 let mut cx = EditorTestContext::new(cx).await;
16313 cx.set_head_text(indoc! { "
16314 one
16315 two
16316 three
16317 four
16318 five
16319 "
16320 });
16321 cx.set_index_text(indoc! { "
16322 one
16323 two
16324 three
16325 four
16326 five
16327 "
16328 });
16329 cx.set_state(indoc! {"
16330 one
16331 TWO
16332 ˇTHREE
16333 FOUR
16334 five
16335 "});
16336 cx.run_until_parked();
16337 cx.update_editor(|editor, window, cx| {
16338 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16339 });
16340 cx.run_until_parked();
16341 cx.assert_index_text(Some(indoc! {"
16342 one
16343 TWO
16344 THREE
16345 FOUR
16346 five
16347 "}));
16348 cx.set_state(indoc! { "
16349 one
16350 TWO
16351 ˇTHREE-HUNDRED
16352 FOUR
16353 five
16354 "});
16355 cx.run_until_parked();
16356 cx.update_editor(|editor, window, cx| {
16357 let snapshot = editor.snapshot(window, cx);
16358 let hunks = editor
16359 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16360 .collect::<Vec<_>>();
16361 assert_eq!(hunks.len(), 1);
16362 assert_eq!(
16363 hunks[0].status(),
16364 DiffHunkStatus {
16365 kind: DiffHunkStatusKind::Modified,
16366 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16367 }
16368 );
16369
16370 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16371 });
16372 cx.run_until_parked();
16373 cx.assert_index_text(Some(indoc! {"
16374 one
16375 TWO
16376 THREE-HUNDRED
16377 FOUR
16378 five
16379 "}));
16380}
16381
16382#[gpui::test]
16383fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16384 init_test(cx, |_| {});
16385
16386 let editor = cx.add_window(|window, cx| {
16387 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16388 build_editor(buffer, window, cx)
16389 });
16390
16391 let render_args = Arc::new(Mutex::new(None));
16392 let snapshot = editor
16393 .update(cx, |editor, window, cx| {
16394 let snapshot = editor.buffer().read(cx).snapshot(cx);
16395 let range =
16396 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16397
16398 struct RenderArgs {
16399 row: MultiBufferRow,
16400 folded: bool,
16401 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16402 }
16403
16404 let crease = Crease::inline(
16405 range,
16406 FoldPlaceholder::test(),
16407 {
16408 let toggle_callback = render_args.clone();
16409 move |row, folded, callback, _window, _cx| {
16410 *toggle_callback.lock() = Some(RenderArgs {
16411 row,
16412 folded,
16413 callback,
16414 });
16415 div()
16416 }
16417 },
16418 |_row, _folded, _window, _cx| div(),
16419 );
16420
16421 editor.insert_creases(Some(crease), cx);
16422 let snapshot = editor.snapshot(window, cx);
16423 let _div = snapshot.render_crease_toggle(
16424 MultiBufferRow(1),
16425 false,
16426 cx.entity().clone(),
16427 window,
16428 cx,
16429 );
16430 snapshot
16431 })
16432 .unwrap();
16433
16434 let render_args = render_args.lock().take().unwrap();
16435 assert_eq!(render_args.row, MultiBufferRow(1));
16436 assert!(!render_args.folded);
16437 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16438
16439 cx.update_window(*editor, |_, window, cx| {
16440 (render_args.callback)(true, window, cx)
16441 })
16442 .unwrap();
16443 let snapshot = editor
16444 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16445 .unwrap();
16446 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16447
16448 cx.update_window(*editor, |_, window, cx| {
16449 (render_args.callback)(false, window, cx)
16450 })
16451 .unwrap();
16452 let snapshot = editor
16453 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16454 .unwrap();
16455 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16456}
16457
16458#[gpui::test]
16459async fn test_input_text(cx: &mut TestAppContext) {
16460 init_test(cx, |_| {});
16461 let mut cx = EditorTestContext::new(cx).await;
16462
16463 cx.set_state(
16464 &r#"ˇone
16465 two
16466
16467 three
16468 fourˇ
16469 five
16470
16471 siˇx"#
16472 .unindent(),
16473 );
16474
16475 cx.dispatch_action(HandleInput(String::new()));
16476 cx.assert_editor_state(
16477 &r#"ˇone
16478 two
16479
16480 three
16481 fourˇ
16482 five
16483
16484 siˇx"#
16485 .unindent(),
16486 );
16487
16488 cx.dispatch_action(HandleInput("AAAA".to_string()));
16489 cx.assert_editor_state(
16490 &r#"AAAAˇone
16491 two
16492
16493 three
16494 fourAAAAˇ
16495 five
16496
16497 siAAAAˇx"#
16498 .unindent(),
16499 );
16500}
16501
16502#[gpui::test]
16503async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16504 init_test(cx, |_| {});
16505
16506 let mut cx = EditorTestContext::new(cx).await;
16507 cx.set_state(
16508 r#"let foo = 1;
16509let foo = 2;
16510let foo = 3;
16511let fooˇ = 4;
16512let foo = 5;
16513let foo = 6;
16514let foo = 7;
16515let foo = 8;
16516let foo = 9;
16517let foo = 10;
16518let foo = 11;
16519let foo = 12;
16520let foo = 13;
16521let foo = 14;
16522let foo = 15;"#,
16523 );
16524
16525 cx.update_editor(|e, window, cx| {
16526 assert_eq!(
16527 e.next_scroll_position,
16528 NextScrollCursorCenterTopBottom::Center,
16529 "Default next scroll direction is center",
16530 );
16531
16532 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16533 assert_eq!(
16534 e.next_scroll_position,
16535 NextScrollCursorCenterTopBottom::Top,
16536 "After center, next scroll direction should be top",
16537 );
16538
16539 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16540 assert_eq!(
16541 e.next_scroll_position,
16542 NextScrollCursorCenterTopBottom::Bottom,
16543 "After top, next scroll direction should be bottom",
16544 );
16545
16546 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16547 assert_eq!(
16548 e.next_scroll_position,
16549 NextScrollCursorCenterTopBottom::Center,
16550 "After bottom, scrolling should start over",
16551 );
16552
16553 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16554 assert_eq!(
16555 e.next_scroll_position,
16556 NextScrollCursorCenterTopBottom::Top,
16557 "Scrolling continues if retriggered fast enough"
16558 );
16559 });
16560
16561 cx.executor()
16562 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16563 cx.executor().run_until_parked();
16564 cx.update_editor(|e, _, _| {
16565 assert_eq!(
16566 e.next_scroll_position,
16567 NextScrollCursorCenterTopBottom::Center,
16568 "If scrolling is not triggered fast enough, it should reset"
16569 );
16570 });
16571}
16572
16573#[gpui::test]
16574async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16575 init_test(cx, |_| {});
16576 let mut cx = EditorLspTestContext::new_rust(
16577 lsp::ServerCapabilities {
16578 definition_provider: Some(lsp::OneOf::Left(true)),
16579 references_provider: Some(lsp::OneOf::Left(true)),
16580 ..lsp::ServerCapabilities::default()
16581 },
16582 cx,
16583 )
16584 .await;
16585
16586 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16587 let go_to_definition = cx
16588 .lsp
16589 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16590 move |params, _| async move {
16591 if empty_go_to_definition {
16592 Ok(None)
16593 } else {
16594 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16595 uri: params.text_document_position_params.text_document.uri,
16596 range: lsp::Range::new(
16597 lsp::Position::new(4, 3),
16598 lsp::Position::new(4, 6),
16599 ),
16600 })))
16601 }
16602 },
16603 );
16604 let references = cx
16605 .lsp
16606 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16607 Ok(Some(vec![lsp::Location {
16608 uri: params.text_document_position.text_document.uri,
16609 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16610 }]))
16611 });
16612 (go_to_definition, references)
16613 };
16614
16615 cx.set_state(
16616 &r#"fn one() {
16617 let mut a = ˇtwo();
16618 }
16619
16620 fn two() {}"#
16621 .unindent(),
16622 );
16623 set_up_lsp_handlers(false, &mut cx);
16624 let navigated = cx
16625 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16626 .await
16627 .expect("Failed to navigate to definition");
16628 assert_eq!(
16629 navigated,
16630 Navigated::Yes,
16631 "Should have navigated to definition from the GetDefinition response"
16632 );
16633 cx.assert_editor_state(
16634 &r#"fn one() {
16635 let mut a = two();
16636 }
16637
16638 fn «twoˇ»() {}"#
16639 .unindent(),
16640 );
16641
16642 let editors = cx.update_workspace(|workspace, _, cx| {
16643 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16644 });
16645 cx.update_editor(|_, _, test_editor_cx| {
16646 assert_eq!(
16647 editors.len(),
16648 1,
16649 "Initially, only one, test, editor should be open in the workspace"
16650 );
16651 assert_eq!(
16652 test_editor_cx.entity(),
16653 editors.last().expect("Asserted len is 1").clone()
16654 );
16655 });
16656
16657 set_up_lsp_handlers(true, &mut cx);
16658 let navigated = cx
16659 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16660 .await
16661 .expect("Failed to navigate to lookup references");
16662 assert_eq!(
16663 navigated,
16664 Navigated::Yes,
16665 "Should have navigated to references as a fallback after empty GoToDefinition response"
16666 );
16667 // We should not change the selections in the existing file,
16668 // if opening another milti buffer with the references
16669 cx.assert_editor_state(
16670 &r#"fn one() {
16671 let mut a = two();
16672 }
16673
16674 fn «twoˇ»() {}"#
16675 .unindent(),
16676 );
16677 let editors = cx.update_workspace(|workspace, _, cx| {
16678 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16679 });
16680 cx.update_editor(|_, _, test_editor_cx| {
16681 assert_eq!(
16682 editors.len(),
16683 2,
16684 "After falling back to references search, we open a new editor with the results"
16685 );
16686 let references_fallback_text = editors
16687 .into_iter()
16688 .find(|new_editor| *new_editor != test_editor_cx.entity())
16689 .expect("Should have one non-test editor now")
16690 .read(test_editor_cx)
16691 .text(test_editor_cx);
16692 assert_eq!(
16693 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16694 "Should use the range from the references response and not the GoToDefinition one"
16695 );
16696 });
16697}
16698
16699#[gpui::test]
16700async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16701 init_test(cx, |_| {});
16702 cx.update(|cx| {
16703 let mut editor_settings = EditorSettings::get_global(cx).clone();
16704 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16705 EditorSettings::override_global(editor_settings, cx);
16706 });
16707 let mut cx = EditorLspTestContext::new_rust(
16708 lsp::ServerCapabilities {
16709 definition_provider: Some(lsp::OneOf::Left(true)),
16710 references_provider: Some(lsp::OneOf::Left(true)),
16711 ..lsp::ServerCapabilities::default()
16712 },
16713 cx,
16714 )
16715 .await;
16716 let original_state = r#"fn one() {
16717 let mut a = ˇtwo();
16718 }
16719
16720 fn two() {}"#
16721 .unindent();
16722 cx.set_state(&original_state);
16723
16724 let mut go_to_definition = cx
16725 .lsp
16726 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16727 move |_, _| async move { Ok(None) },
16728 );
16729 let _references = cx
16730 .lsp
16731 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16732 panic!("Should not call for references with no go to definition fallback")
16733 });
16734
16735 let navigated = cx
16736 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16737 .await
16738 .expect("Failed to navigate to lookup references");
16739 go_to_definition
16740 .next()
16741 .await
16742 .expect("Should have called the go_to_definition handler");
16743
16744 assert_eq!(
16745 navigated,
16746 Navigated::No,
16747 "Should have navigated to references as a fallback after empty GoToDefinition response"
16748 );
16749 cx.assert_editor_state(&original_state);
16750 let editors = cx.update_workspace(|workspace, _, cx| {
16751 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16752 });
16753 cx.update_editor(|_, _, _| {
16754 assert_eq!(
16755 editors.len(),
16756 1,
16757 "After unsuccessful fallback, no other editor should have been opened"
16758 );
16759 });
16760}
16761
16762#[gpui::test]
16763async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16764 init_test(cx, |_| {});
16765
16766 let language = Arc::new(Language::new(
16767 LanguageConfig::default(),
16768 Some(tree_sitter_rust::LANGUAGE.into()),
16769 ));
16770
16771 let text = r#"
16772 #[cfg(test)]
16773 mod tests() {
16774 #[test]
16775 fn runnable_1() {
16776 let a = 1;
16777 }
16778
16779 #[test]
16780 fn runnable_2() {
16781 let a = 1;
16782 let b = 2;
16783 }
16784 }
16785 "#
16786 .unindent();
16787
16788 let fs = FakeFs::new(cx.executor());
16789 fs.insert_file("/file.rs", Default::default()).await;
16790
16791 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16792 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16793 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16794 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16795 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16796
16797 let editor = cx.new_window_entity(|window, cx| {
16798 Editor::new(
16799 EditorMode::Full,
16800 multi_buffer,
16801 Some(project.clone()),
16802 window,
16803 cx,
16804 )
16805 });
16806
16807 editor.update_in(cx, |editor, window, cx| {
16808 let snapshot = editor.buffer().read(cx).snapshot(cx);
16809 editor.tasks.insert(
16810 (buffer.read(cx).remote_id(), 3),
16811 RunnableTasks {
16812 templates: vec![],
16813 offset: snapshot.anchor_before(43),
16814 column: 0,
16815 extra_variables: HashMap::default(),
16816 context_range: BufferOffset(43)..BufferOffset(85),
16817 },
16818 );
16819 editor.tasks.insert(
16820 (buffer.read(cx).remote_id(), 8),
16821 RunnableTasks {
16822 templates: vec![],
16823 offset: snapshot.anchor_before(86),
16824 column: 0,
16825 extra_variables: HashMap::default(),
16826 context_range: BufferOffset(86)..BufferOffset(191),
16827 },
16828 );
16829
16830 // Test finding task when cursor is inside function body
16831 editor.change_selections(None, window, cx, |s| {
16832 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16833 });
16834 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16835 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16836
16837 // Test finding task when cursor is on function name
16838 editor.change_selections(None, window, cx, |s| {
16839 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16840 });
16841 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16842 assert_eq!(row, 8, "Should find task when cursor is on function name");
16843 });
16844}
16845
16846#[gpui::test]
16847async fn test_folding_buffers(cx: &mut TestAppContext) {
16848 init_test(cx, |_| {});
16849
16850 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16851 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16852 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16853
16854 let fs = FakeFs::new(cx.executor());
16855 fs.insert_tree(
16856 path!("/a"),
16857 json!({
16858 "first.rs": sample_text_1,
16859 "second.rs": sample_text_2,
16860 "third.rs": sample_text_3,
16861 }),
16862 )
16863 .await;
16864 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16865 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16866 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16867 let worktree = project.update(cx, |project, cx| {
16868 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16869 assert_eq!(worktrees.len(), 1);
16870 worktrees.pop().unwrap()
16871 });
16872 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16873
16874 let buffer_1 = project
16875 .update(cx, |project, cx| {
16876 project.open_buffer((worktree_id, "first.rs"), cx)
16877 })
16878 .await
16879 .unwrap();
16880 let buffer_2 = project
16881 .update(cx, |project, cx| {
16882 project.open_buffer((worktree_id, "second.rs"), cx)
16883 })
16884 .await
16885 .unwrap();
16886 let buffer_3 = project
16887 .update(cx, |project, cx| {
16888 project.open_buffer((worktree_id, "third.rs"), cx)
16889 })
16890 .await
16891 .unwrap();
16892
16893 let multi_buffer = cx.new(|cx| {
16894 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16895 multi_buffer.push_excerpts(
16896 buffer_1.clone(),
16897 [
16898 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16899 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16900 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16901 ],
16902 cx,
16903 );
16904 multi_buffer.push_excerpts(
16905 buffer_2.clone(),
16906 [
16907 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16908 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16909 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16910 ],
16911 cx,
16912 );
16913 multi_buffer.push_excerpts(
16914 buffer_3.clone(),
16915 [
16916 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16917 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16918 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16919 ],
16920 cx,
16921 );
16922 multi_buffer
16923 });
16924 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16925 Editor::new(
16926 EditorMode::Full,
16927 multi_buffer.clone(),
16928 Some(project.clone()),
16929 window,
16930 cx,
16931 )
16932 });
16933
16934 assert_eq!(
16935 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16936 "\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",
16937 );
16938
16939 multi_buffer_editor.update(cx, |editor, cx| {
16940 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16941 });
16942 assert_eq!(
16943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16944 "\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",
16945 "After folding the first buffer, its text should not be displayed"
16946 );
16947
16948 multi_buffer_editor.update(cx, |editor, cx| {
16949 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16950 });
16951 assert_eq!(
16952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16953 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16954 "After folding the second buffer, its text should not be displayed"
16955 );
16956
16957 multi_buffer_editor.update(cx, |editor, cx| {
16958 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16959 });
16960 assert_eq!(
16961 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16962 "\n\n\n\n\n",
16963 "After folding the third buffer, its text should not be displayed"
16964 );
16965
16966 // Emulate selection inside the fold logic, that should work
16967 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16968 editor
16969 .snapshot(window, cx)
16970 .next_line_boundary(Point::new(0, 4));
16971 });
16972
16973 multi_buffer_editor.update(cx, |editor, cx| {
16974 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16975 });
16976 assert_eq!(
16977 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16978 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16979 "After unfolding the second buffer, its text should be displayed"
16980 );
16981
16982 // Typing inside of buffer 1 causes that buffer to be unfolded.
16983 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16984 assert_eq!(
16985 multi_buffer
16986 .read(cx)
16987 .snapshot(cx)
16988 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16989 .collect::<String>(),
16990 "bbbb"
16991 );
16992 editor.change_selections(None, window, cx, |selections| {
16993 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16994 });
16995 editor.handle_input("B", window, cx);
16996 });
16997
16998 assert_eq!(
16999 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17000 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17001 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17002 );
17003
17004 multi_buffer_editor.update(cx, |editor, cx| {
17005 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17006 });
17007 assert_eq!(
17008 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17009 "\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",
17010 "After unfolding the all buffers, all original text should be displayed"
17011 );
17012}
17013
17014#[gpui::test]
17015async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17016 init_test(cx, |_| {});
17017
17018 let sample_text_1 = "1111\n2222\n3333".to_string();
17019 let sample_text_2 = "4444\n5555\n6666".to_string();
17020 let sample_text_3 = "7777\n8888\n9999".to_string();
17021
17022 let fs = FakeFs::new(cx.executor());
17023 fs.insert_tree(
17024 path!("/a"),
17025 json!({
17026 "first.rs": sample_text_1,
17027 "second.rs": sample_text_2,
17028 "third.rs": sample_text_3,
17029 }),
17030 )
17031 .await;
17032 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17033 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17034 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17035 let worktree = project.update(cx, |project, cx| {
17036 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17037 assert_eq!(worktrees.len(), 1);
17038 worktrees.pop().unwrap()
17039 });
17040 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17041
17042 let buffer_1 = project
17043 .update(cx, |project, cx| {
17044 project.open_buffer((worktree_id, "first.rs"), cx)
17045 })
17046 .await
17047 .unwrap();
17048 let buffer_2 = project
17049 .update(cx, |project, cx| {
17050 project.open_buffer((worktree_id, "second.rs"), cx)
17051 })
17052 .await
17053 .unwrap();
17054 let buffer_3 = project
17055 .update(cx, |project, cx| {
17056 project.open_buffer((worktree_id, "third.rs"), cx)
17057 })
17058 .await
17059 .unwrap();
17060
17061 let multi_buffer = cx.new(|cx| {
17062 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17063 multi_buffer.push_excerpts(
17064 buffer_1.clone(),
17065 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17066 cx,
17067 );
17068 multi_buffer.push_excerpts(
17069 buffer_2.clone(),
17070 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17071 cx,
17072 );
17073 multi_buffer.push_excerpts(
17074 buffer_3.clone(),
17075 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17076 cx,
17077 );
17078 multi_buffer
17079 });
17080
17081 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17082 Editor::new(
17083 EditorMode::Full,
17084 multi_buffer,
17085 Some(project.clone()),
17086 window,
17087 cx,
17088 )
17089 });
17090
17091 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17092 assert_eq!(
17093 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17094 full_text,
17095 );
17096
17097 multi_buffer_editor.update(cx, |editor, cx| {
17098 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17099 });
17100 assert_eq!(
17101 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17102 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17103 "After folding the first buffer, its text should not be displayed"
17104 );
17105
17106 multi_buffer_editor.update(cx, |editor, cx| {
17107 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17108 });
17109
17110 assert_eq!(
17111 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17112 "\n\n\n\n\n\n7777\n8888\n9999",
17113 "After folding the second buffer, its text should not be displayed"
17114 );
17115
17116 multi_buffer_editor.update(cx, |editor, cx| {
17117 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17118 });
17119 assert_eq!(
17120 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17121 "\n\n\n\n\n",
17122 "After folding the third buffer, its text should not be displayed"
17123 );
17124
17125 multi_buffer_editor.update(cx, |editor, cx| {
17126 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17127 });
17128 assert_eq!(
17129 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17130 "\n\n\n\n4444\n5555\n6666\n\n",
17131 "After unfolding the second buffer, its text should be displayed"
17132 );
17133
17134 multi_buffer_editor.update(cx, |editor, cx| {
17135 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17136 });
17137 assert_eq!(
17138 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17139 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17140 "After unfolding the first buffer, its text should be displayed"
17141 );
17142
17143 multi_buffer_editor.update(cx, |editor, cx| {
17144 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17145 });
17146 assert_eq!(
17147 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17148 full_text,
17149 "After unfolding all buffers, all original text should be displayed"
17150 );
17151}
17152
17153#[gpui::test]
17154async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17155 init_test(cx, |_| {});
17156
17157 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17158
17159 let fs = FakeFs::new(cx.executor());
17160 fs.insert_tree(
17161 path!("/a"),
17162 json!({
17163 "main.rs": sample_text,
17164 }),
17165 )
17166 .await;
17167 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17168 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17169 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17170 let worktree = project.update(cx, |project, cx| {
17171 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17172 assert_eq!(worktrees.len(), 1);
17173 worktrees.pop().unwrap()
17174 });
17175 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17176
17177 let buffer_1 = project
17178 .update(cx, |project, cx| {
17179 project.open_buffer((worktree_id, "main.rs"), cx)
17180 })
17181 .await
17182 .unwrap();
17183
17184 let multi_buffer = cx.new(|cx| {
17185 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17186 multi_buffer.push_excerpts(
17187 buffer_1.clone(),
17188 [ExcerptRange::new(
17189 Point::new(0, 0)
17190 ..Point::new(
17191 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17192 0,
17193 ),
17194 )],
17195 cx,
17196 );
17197 multi_buffer
17198 });
17199 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17200 Editor::new(
17201 EditorMode::Full,
17202 multi_buffer,
17203 Some(project.clone()),
17204 window,
17205 cx,
17206 )
17207 });
17208
17209 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17210 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17211 enum TestHighlight {}
17212 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17213 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17214 editor.highlight_text::<TestHighlight>(
17215 vec![highlight_range.clone()],
17216 HighlightStyle::color(Hsla::green()),
17217 cx,
17218 );
17219 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17220 });
17221
17222 let full_text = format!("\n\n{sample_text}");
17223 assert_eq!(
17224 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17225 full_text,
17226 );
17227}
17228
17229#[gpui::test]
17230async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17231 init_test(cx, |_| {});
17232 cx.update(|cx| {
17233 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17234 "keymaps/default-linux.json",
17235 cx,
17236 )
17237 .unwrap();
17238 cx.bind_keys(default_key_bindings);
17239 });
17240
17241 let (editor, cx) = cx.add_window_view(|window, cx| {
17242 let multi_buffer = MultiBuffer::build_multi(
17243 [
17244 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17245 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17246 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17247 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17248 ],
17249 cx,
17250 );
17251 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17252
17253 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17254 // fold all but the second buffer, so that we test navigating between two
17255 // adjacent folded buffers, as well as folded buffers at the start and
17256 // end the multibuffer
17257 editor.fold_buffer(buffer_ids[0], cx);
17258 editor.fold_buffer(buffer_ids[2], cx);
17259 editor.fold_buffer(buffer_ids[3], cx);
17260
17261 editor
17262 });
17263 cx.simulate_resize(size(px(1000.), px(1000.)));
17264
17265 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17266 cx.assert_excerpts_with_selections(indoc! {"
17267 [EXCERPT]
17268 ˇ[FOLDED]
17269 [EXCERPT]
17270 a1
17271 b1
17272 [EXCERPT]
17273 [FOLDED]
17274 [EXCERPT]
17275 [FOLDED]
17276 "
17277 });
17278 cx.simulate_keystroke("down");
17279 cx.assert_excerpts_with_selections(indoc! {"
17280 [EXCERPT]
17281 [FOLDED]
17282 [EXCERPT]
17283 ˇa1
17284 b1
17285 [EXCERPT]
17286 [FOLDED]
17287 [EXCERPT]
17288 [FOLDED]
17289 "
17290 });
17291 cx.simulate_keystroke("down");
17292 cx.assert_excerpts_with_selections(indoc! {"
17293 [EXCERPT]
17294 [FOLDED]
17295 [EXCERPT]
17296 a1
17297 ˇb1
17298 [EXCERPT]
17299 [FOLDED]
17300 [EXCERPT]
17301 [FOLDED]
17302 "
17303 });
17304 cx.simulate_keystroke("down");
17305 cx.assert_excerpts_with_selections(indoc! {"
17306 [EXCERPT]
17307 [FOLDED]
17308 [EXCERPT]
17309 a1
17310 b1
17311 ˇ[EXCERPT]
17312 [FOLDED]
17313 [EXCERPT]
17314 [FOLDED]
17315 "
17316 });
17317 cx.simulate_keystroke("down");
17318 cx.assert_excerpts_with_selections(indoc! {"
17319 [EXCERPT]
17320 [FOLDED]
17321 [EXCERPT]
17322 a1
17323 b1
17324 [EXCERPT]
17325 ˇ[FOLDED]
17326 [EXCERPT]
17327 [FOLDED]
17328 "
17329 });
17330 for _ in 0..5 {
17331 cx.simulate_keystroke("down");
17332 cx.assert_excerpts_with_selections(indoc! {"
17333 [EXCERPT]
17334 [FOLDED]
17335 [EXCERPT]
17336 a1
17337 b1
17338 [EXCERPT]
17339 [FOLDED]
17340 [EXCERPT]
17341 ˇ[FOLDED]
17342 "
17343 });
17344 }
17345
17346 cx.simulate_keystroke("up");
17347 cx.assert_excerpts_with_selections(indoc! {"
17348 [EXCERPT]
17349 [FOLDED]
17350 [EXCERPT]
17351 a1
17352 b1
17353 [EXCERPT]
17354 ˇ[FOLDED]
17355 [EXCERPT]
17356 [FOLDED]
17357 "
17358 });
17359 cx.simulate_keystroke("up");
17360 cx.assert_excerpts_with_selections(indoc! {"
17361 [EXCERPT]
17362 [FOLDED]
17363 [EXCERPT]
17364 a1
17365 b1
17366 ˇ[EXCERPT]
17367 [FOLDED]
17368 [EXCERPT]
17369 [FOLDED]
17370 "
17371 });
17372 cx.simulate_keystroke("up");
17373 cx.assert_excerpts_with_selections(indoc! {"
17374 [EXCERPT]
17375 [FOLDED]
17376 [EXCERPT]
17377 a1
17378 ˇb1
17379 [EXCERPT]
17380 [FOLDED]
17381 [EXCERPT]
17382 [FOLDED]
17383 "
17384 });
17385 cx.simulate_keystroke("up");
17386 cx.assert_excerpts_with_selections(indoc! {"
17387 [EXCERPT]
17388 [FOLDED]
17389 [EXCERPT]
17390 ˇa1
17391 b1
17392 [EXCERPT]
17393 [FOLDED]
17394 [EXCERPT]
17395 [FOLDED]
17396 "
17397 });
17398 for _ in 0..5 {
17399 cx.simulate_keystroke("up");
17400 cx.assert_excerpts_with_selections(indoc! {"
17401 [EXCERPT]
17402 ˇ[FOLDED]
17403 [EXCERPT]
17404 a1
17405 b1
17406 [EXCERPT]
17407 [FOLDED]
17408 [EXCERPT]
17409 [FOLDED]
17410 "
17411 });
17412 }
17413}
17414
17415#[gpui::test]
17416async fn test_inline_completion_text(cx: &mut TestAppContext) {
17417 init_test(cx, |_| {});
17418
17419 // Simple insertion
17420 assert_highlighted_edits(
17421 "Hello, world!",
17422 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17423 true,
17424 cx,
17425 |highlighted_edits, cx| {
17426 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17427 assert_eq!(highlighted_edits.highlights.len(), 1);
17428 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17429 assert_eq!(
17430 highlighted_edits.highlights[0].1.background_color,
17431 Some(cx.theme().status().created_background)
17432 );
17433 },
17434 )
17435 .await;
17436
17437 // Replacement
17438 assert_highlighted_edits(
17439 "This is a test.",
17440 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17441 false,
17442 cx,
17443 |highlighted_edits, cx| {
17444 assert_eq!(highlighted_edits.text, "That is a test.");
17445 assert_eq!(highlighted_edits.highlights.len(), 1);
17446 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17447 assert_eq!(
17448 highlighted_edits.highlights[0].1.background_color,
17449 Some(cx.theme().status().created_background)
17450 );
17451 },
17452 )
17453 .await;
17454
17455 // Multiple edits
17456 assert_highlighted_edits(
17457 "Hello, world!",
17458 vec![
17459 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17460 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17461 ],
17462 false,
17463 cx,
17464 |highlighted_edits, cx| {
17465 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17466 assert_eq!(highlighted_edits.highlights.len(), 2);
17467 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17468 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17469 assert_eq!(
17470 highlighted_edits.highlights[0].1.background_color,
17471 Some(cx.theme().status().created_background)
17472 );
17473 assert_eq!(
17474 highlighted_edits.highlights[1].1.background_color,
17475 Some(cx.theme().status().created_background)
17476 );
17477 },
17478 )
17479 .await;
17480
17481 // Multiple lines with edits
17482 assert_highlighted_edits(
17483 "First line\nSecond line\nThird line\nFourth line",
17484 vec![
17485 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17486 (
17487 Point::new(2, 0)..Point::new(2, 10),
17488 "New third line".to_string(),
17489 ),
17490 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17491 ],
17492 false,
17493 cx,
17494 |highlighted_edits, cx| {
17495 assert_eq!(
17496 highlighted_edits.text,
17497 "Second modified\nNew third line\nFourth updated line"
17498 );
17499 assert_eq!(highlighted_edits.highlights.len(), 3);
17500 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17501 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17502 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17503 for highlight in &highlighted_edits.highlights {
17504 assert_eq!(
17505 highlight.1.background_color,
17506 Some(cx.theme().status().created_background)
17507 );
17508 }
17509 },
17510 )
17511 .await;
17512}
17513
17514#[gpui::test]
17515async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17516 init_test(cx, |_| {});
17517
17518 // Deletion
17519 assert_highlighted_edits(
17520 "Hello, world!",
17521 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17522 true,
17523 cx,
17524 |highlighted_edits, cx| {
17525 assert_eq!(highlighted_edits.text, "Hello, world!");
17526 assert_eq!(highlighted_edits.highlights.len(), 1);
17527 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17528 assert_eq!(
17529 highlighted_edits.highlights[0].1.background_color,
17530 Some(cx.theme().status().deleted_background)
17531 );
17532 },
17533 )
17534 .await;
17535
17536 // Insertion
17537 assert_highlighted_edits(
17538 "Hello, world!",
17539 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17540 true,
17541 cx,
17542 |highlighted_edits, cx| {
17543 assert_eq!(highlighted_edits.highlights.len(), 1);
17544 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17545 assert_eq!(
17546 highlighted_edits.highlights[0].1.background_color,
17547 Some(cx.theme().status().created_background)
17548 );
17549 },
17550 )
17551 .await;
17552}
17553
17554async fn assert_highlighted_edits(
17555 text: &str,
17556 edits: Vec<(Range<Point>, String)>,
17557 include_deletions: bool,
17558 cx: &mut TestAppContext,
17559 assertion_fn: impl Fn(HighlightedText, &App),
17560) {
17561 let window = cx.add_window(|window, cx| {
17562 let buffer = MultiBuffer::build_simple(text, cx);
17563 Editor::new(EditorMode::Full, buffer, None, window, cx)
17564 });
17565 let cx = &mut VisualTestContext::from_window(*window, cx);
17566
17567 let (buffer, snapshot) = window
17568 .update(cx, |editor, _window, cx| {
17569 (
17570 editor.buffer().clone(),
17571 editor.buffer().read(cx).snapshot(cx),
17572 )
17573 })
17574 .unwrap();
17575
17576 let edits = edits
17577 .into_iter()
17578 .map(|(range, edit)| {
17579 (
17580 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17581 edit,
17582 )
17583 })
17584 .collect::<Vec<_>>();
17585
17586 let text_anchor_edits = edits
17587 .clone()
17588 .into_iter()
17589 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17590 .collect::<Vec<_>>();
17591
17592 let edit_preview = window
17593 .update(cx, |_, _window, cx| {
17594 buffer
17595 .read(cx)
17596 .as_singleton()
17597 .unwrap()
17598 .read(cx)
17599 .preview_edits(text_anchor_edits.into(), cx)
17600 })
17601 .unwrap()
17602 .await;
17603
17604 cx.update(|_window, cx| {
17605 let highlighted_edits = inline_completion_edit_text(
17606 &snapshot.as_singleton().unwrap().2,
17607 &edits,
17608 &edit_preview,
17609 include_deletions,
17610 cx,
17611 );
17612 assertion_fn(highlighted_edits, cx)
17613 });
17614}
17615
17616#[track_caller]
17617fn assert_breakpoint(
17618 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17619 path: &Arc<Path>,
17620 expected: Vec<(u32, Breakpoint)>,
17621) {
17622 if expected.len() == 0usize {
17623 assert!(!breakpoints.contains_key(path), "{}", path.display());
17624 } else {
17625 let mut breakpoint = breakpoints
17626 .get(path)
17627 .unwrap()
17628 .into_iter()
17629 .map(|breakpoint| {
17630 (
17631 breakpoint.row,
17632 Breakpoint {
17633 message: breakpoint.message.clone(),
17634 state: breakpoint.state,
17635 condition: breakpoint.condition.clone(),
17636 hit_condition: breakpoint.hit_condition.clone(),
17637 },
17638 )
17639 })
17640 .collect::<Vec<_>>();
17641
17642 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17643
17644 assert_eq!(expected, breakpoint);
17645 }
17646}
17647
17648fn add_log_breakpoint_at_cursor(
17649 editor: &mut Editor,
17650 log_message: &str,
17651 window: &mut Window,
17652 cx: &mut Context<Editor>,
17653) {
17654 let (anchor, bp) = editor
17655 .breakpoint_at_cursor_head(window, cx)
17656 .unwrap_or_else(|| {
17657 let cursor_position: Point = editor.selections.newest(cx).head();
17658
17659 let breakpoint_position = editor
17660 .snapshot(window, cx)
17661 .display_snapshot
17662 .buffer_snapshot
17663 .anchor_before(Point::new(cursor_position.row, 0));
17664
17665 (breakpoint_position, Breakpoint::new_log(&log_message))
17666 });
17667
17668 editor.edit_breakpoint_at_anchor(
17669 anchor,
17670 bp,
17671 BreakpointEditAction::EditLogMessage(log_message.into()),
17672 cx,
17673 );
17674}
17675
17676#[gpui::test]
17677async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17678 init_test(cx, |_| {});
17679
17680 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17681 let fs = FakeFs::new(cx.executor());
17682 fs.insert_tree(
17683 path!("/a"),
17684 json!({
17685 "main.rs": sample_text,
17686 }),
17687 )
17688 .await;
17689 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17691 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17692
17693 let fs = FakeFs::new(cx.executor());
17694 fs.insert_tree(
17695 path!("/a"),
17696 json!({
17697 "main.rs": sample_text,
17698 }),
17699 )
17700 .await;
17701 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17702 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17703 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17704 let worktree_id = workspace
17705 .update(cx, |workspace, _window, cx| {
17706 workspace.project().update(cx, |project, cx| {
17707 project.worktrees(cx).next().unwrap().read(cx).id()
17708 })
17709 })
17710 .unwrap();
17711
17712 let buffer = project
17713 .update(cx, |project, cx| {
17714 project.open_buffer((worktree_id, "main.rs"), cx)
17715 })
17716 .await
17717 .unwrap();
17718
17719 let (editor, cx) = cx.add_window_view(|window, cx| {
17720 Editor::new(
17721 EditorMode::Full,
17722 MultiBuffer::build_from_buffer(buffer, cx),
17723 Some(project.clone()),
17724 window,
17725 cx,
17726 )
17727 });
17728
17729 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17730 let abs_path = project.read_with(cx, |project, cx| {
17731 project
17732 .absolute_path(&project_path, cx)
17733 .map(|path_buf| Arc::from(path_buf.to_owned()))
17734 .unwrap()
17735 });
17736
17737 // assert we can add breakpoint on the first line
17738 editor.update_in(cx, |editor, window, cx| {
17739 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17740 editor.move_to_end(&MoveToEnd, window, cx);
17741 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17742 });
17743
17744 let breakpoints = editor.update(cx, |editor, cx| {
17745 editor
17746 .breakpoint_store()
17747 .as_ref()
17748 .unwrap()
17749 .read(cx)
17750 .all_breakpoints(cx)
17751 .clone()
17752 });
17753
17754 assert_eq!(1, breakpoints.len());
17755 assert_breakpoint(
17756 &breakpoints,
17757 &abs_path,
17758 vec![
17759 (0, Breakpoint::new_standard()),
17760 (3, Breakpoint::new_standard()),
17761 ],
17762 );
17763
17764 editor.update_in(cx, |editor, window, cx| {
17765 editor.move_to_beginning(&MoveToBeginning, window, cx);
17766 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17767 });
17768
17769 let breakpoints = editor.update(cx, |editor, cx| {
17770 editor
17771 .breakpoint_store()
17772 .as_ref()
17773 .unwrap()
17774 .read(cx)
17775 .all_breakpoints(cx)
17776 .clone()
17777 });
17778
17779 assert_eq!(1, breakpoints.len());
17780 assert_breakpoint(
17781 &breakpoints,
17782 &abs_path,
17783 vec![(3, Breakpoint::new_standard())],
17784 );
17785
17786 editor.update_in(cx, |editor, window, cx| {
17787 editor.move_to_end(&MoveToEnd, window, cx);
17788 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17789 });
17790
17791 let breakpoints = editor.update(cx, |editor, cx| {
17792 editor
17793 .breakpoint_store()
17794 .as_ref()
17795 .unwrap()
17796 .read(cx)
17797 .all_breakpoints(cx)
17798 .clone()
17799 });
17800
17801 assert_eq!(0, breakpoints.len());
17802 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17803}
17804
17805#[gpui::test]
17806async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17807 init_test(cx, |_| {});
17808
17809 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17810
17811 let fs = FakeFs::new(cx.executor());
17812 fs.insert_tree(
17813 path!("/a"),
17814 json!({
17815 "main.rs": sample_text,
17816 }),
17817 )
17818 .await;
17819 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17820 let (workspace, cx) =
17821 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17822
17823 let worktree_id = workspace.update(cx, |workspace, cx| {
17824 workspace.project().update(cx, |project, cx| {
17825 project.worktrees(cx).next().unwrap().read(cx).id()
17826 })
17827 });
17828
17829 let buffer = project
17830 .update(cx, |project, cx| {
17831 project.open_buffer((worktree_id, "main.rs"), cx)
17832 })
17833 .await
17834 .unwrap();
17835
17836 let (editor, cx) = cx.add_window_view(|window, cx| {
17837 Editor::new(
17838 EditorMode::Full,
17839 MultiBuffer::build_from_buffer(buffer, cx),
17840 Some(project.clone()),
17841 window,
17842 cx,
17843 )
17844 });
17845
17846 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17847 let abs_path = project.read_with(cx, |project, cx| {
17848 project
17849 .absolute_path(&project_path, cx)
17850 .map(|path_buf| Arc::from(path_buf.to_owned()))
17851 .unwrap()
17852 });
17853
17854 editor.update_in(cx, |editor, window, cx| {
17855 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17856 });
17857
17858 let breakpoints = editor.update(cx, |editor, cx| {
17859 editor
17860 .breakpoint_store()
17861 .as_ref()
17862 .unwrap()
17863 .read(cx)
17864 .all_breakpoints(cx)
17865 .clone()
17866 });
17867
17868 assert_breakpoint(
17869 &breakpoints,
17870 &abs_path,
17871 vec![(0, Breakpoint::new_log("hello world"))],
17872 );
17873
17874 // Removing a log message from a log breakpoint should remove it
17875 editor.update_in(cx, |editor, window, cx| {
17876 add_log_breakpoint_at_cursor(editor, "", window, cx);
17877 });
17878
17879 let breakpoints = editor.update(cx, |editor, cx| {
17880 editor
17881 .breakpoint_store()
17882 .as_ref()
17883 .unwrap()
17884 .read(cx)
17885 .all_breakpoints(cx)
17886 .clone()
17887 });
17888
17889 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17890
17891 editor.update_in(cx, |editor, window, cx| {
17892 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17893 editor.move_to_end(&MoveToEnd, window, cx);
17894 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17895 // Not adding a log message to a standard breakpoint shouldn't remove it
17896 add_log_breakpoint_at_cursor(editor, "", window, cx);
17897 });
17898
17899 let breakpoints = editor.update(cx, |editor, cx| {
17900 editor
17901 .breakpoint_store()
17902 .as_ref()
17903 .unwrap()
17904 .read(cx)
17905 .all_breakpoints(cx)
17906 .clone()
17907 });
17908
17909 assert_breakpoint(
17910 &breakpoints,
17911 &abs_path,
17912 vec![
17913 (0, Breakpoint::new_standard()),
17914 (3, Breakpoint::new_standard()),
17915 ],
17916 );
17917
17918 editor.update_in(cx, |editor, window, cx| {
17919 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17920 });
17921
17922 let breakpoints = editor.update(cx, |editor, cx| {
17923 editor
17924 .breakpoint_store()
17925 .as_ref()
17926 .unwrap()
17927 .read(cx)
17928 .all_breakpoints(cx)
17929 .clone()
17930 });
17931
17932 assert_breakpoint(
17933 &breakpoints,
17934 &abs_path,
17935 vec![
17936 (0, Breakpoint::new_standard()),
17937 (3, Breakpoint::new_log("hello world")),
17938 ],
17939 );
17940
17941 editor.update_in(cx, |editor, window, cx| {
17942 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17943 });
17944
17945 let breakpoints = editor.update(cx, |editor, cx| {
17946 editor
17947 .breakpoint_store()
17948 .as_ref()
17949 .unwrap()
17950 .read(cx)
17951 .all_breakpoints(cx)
17952 .clone()
17953 });
17954
17955 assert_breakpoint(
17956 &breakpoints,
17957 &abs_path,
17958 vec![
17959 (0, Breakpoint::new_standard()),
17960 (3, Breakpoint::new_log("hello Earth!!")),
17961 ],
17962 );
17963}
17964
17965/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17966/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17967/// or when breakpoints were placed out of order. This tests for a regression too
17968#[gpui::test]
17969async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17970 init_test(cx, |_| {});
17971
17972 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17973 let fs = FakeFs::new(cx.executor());
17974 fs.insert_tree(
17975 path!("/a"),
17976 json!({
17977 "main.rs": sample_text,
17978 }),
17979 )
17980 .await;
17981 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17982 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17983 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17984
17985 let fs = FakeFs::new(cx.executor());
17986 fs.insert_tree(
17987 path!("/a"),
17988 json!({
17989 "main.rs": sample_text,
17990 }),
17991 )
17992 .await;
17993 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17994 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17995 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17996 let worktree_id = workspace
17997 .update(cx, |workspace, _window, cx| {
17998 workspace.project().update(cx, |project, cx| {
17999 project.worktrees(cx).next().unwrap().read(cx).id()
18000 })
18001 })
18002 .unwrap();
18003
18004 let buffer = project
18005 .update(cx, |project, cx| {
18006 project.open_buffer((worktree_id, "main.rs"), cx)
18007 })
18008 .await
18009 .unwrap();
18010
18011 let (editor, cx) = cx.add_window_view(|window, cx| {
18012 Editor::new(
18013 EditorMode::Full,
18014 MultiBuffer::build_from_buffer(buffer, cx),
18015 Some(project.clone()),
18016 window,
18017 cx,
18018 )
18019 });
18020
18021 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18022 let abs_path = project.read_with(cx, |project, cx| {
18023 project
18024 .absolute_path(&project_path, cx)
18025 .map(|path_buf| Arc::from(path_buf.to_owned()))
18026 .unwrap()
18027 });
18028
18029 // assert we can add breakpoint on the first line
18030 editor.update_in(cx, |editor, window, cx| {
18031 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18032 editor.move_to_end(&MoveToEnd, window, cx);
18033 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18034 editor.move_up(&MoveUp, window, cx);
18035 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18036 });
18037
18038 let breakpoints = editor.update(cx, |editor, cx| {
18039 editor
18040 .breakpoint_store()
18041 .as_ref()
18042 .unwrap()
18043 .read(cx)
18044 .all_breakpoints(cx)
18045 .clone()
18046 });
18047
18048 assert_eq!(1, breakpoints.len());
18049 assert_breakpoint(
18050 &breakpoints,
18051 &abs_path,
18052 vec![
18053 (0, Breakpoint::new_standard()),
18054 (2, Breakpoint::new_standard()),
18055 (3, Breakpoint::new_standard()),
18056 ],
18057 );
18058
18059 editor.update_in(cx, |editor, window, cx| {
18060 editor.move_to_beginning(&MoveToBeginning, window, cx);
18061 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18062 editor.move_to_end(&MoveToEnd, window, cx);
18063 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18064 // Disabling a breakpoint that doesn't exist should do nothing
18065 editor.move_up(&MoveUp, window, cx);
18066 editor.move_up(&MoveUp, window, cx);
18067 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18068 });
18069
18070 let breakpoints = editor.update(cx, |editor, cx| {
18071 editor
18072 .breakpoint_store()
18073 .as_ref()
18074 .unwrap()
18075 .read(cx)
18076 .all_breakpoints(cx)
18077 .clone()
18078 });
18079
18080 let disable_breakpoint = {
18081 let mut bp = Breakpoint::new_standard();
18082 bp.state = BreakpointState::Disabled;
18083 bp
18084 };
18085
18086 assert_eq!(1, breakpoints.len());
18087 assert_breakpoint(
18088 &breakpoints,
18089 &abs_path,
18090 vec![
18091 (0, disable_breakpoint.clone()),
18092 (2, Breakpoint::new_standard()),
18093 (3, disable_breakpoint.clone()),
18094 ],
18095 );
18096
18097 editor.update_in(cx, |editor, window, cx| {
18098 editor.move_to_beginning(&MoveToBeginning, window, cx);
18099 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18100 editor.move_to_end(&MoveToEnd, window, cx);
18101 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18102 editor.move_up(&MoveUp, window, cx);
18103 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18104 });
18105
18106 let breakpoints = editor.update(cx, |editor, cx| {
18107 editor
18108 .breakpoint_store()
18109 .as_ref()
18110 .unwrap()
18111 .read(cx)
18112 .all_breakpoints(cx)
18113 .clone()
18114 });
18115
18116 assert_eq!(1, breakpoints.len());
18117 assert_breakpoint(
18118 &breakpoints,
18119 &abs_path,
18120 vec![
18121 (0, Breakpoint::new_standard()),
18122 (2, disable_breakpoint),
18123 (3, Breakpoint::new_standard()),
18124 ],
18125 );
18126}
18127
18128#[gpui::test]
18129async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18130 init_test(cx, |_| {});
18131 let capabilities = lsp::ServerCapabilities {
18132 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18133 prepare_provider: Some(true),
18134 work_done_progress_options: Default::default(),
18135 })),
18136 ..Default::default()
18137 };
18138 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18139
18140 cx.set_state(indoc! {"
18141 struct Fˇoo {}
18142 "});
18143
18144 cx.update_editor(|editor, _, cx| {
18145 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18146 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18147 editor.highlight_background::<DocumentHighlightRead>(
18148 &[highlight_range],
18149 |c| c.editor_document_highlight_read_background,
18150 cx,
18151 );
18152 });
18153
18154 let mut prepare_rename_handler = cx
18155 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18156 move |_, _, _| async move {
18157 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18158 start: lsp::Position {
18159 line: 0,
18160 character: 7,
18161 },
18162 end: lsp::Position {
18163 line: 0,
18164 character: 10,
18165 },
18166 })))
18167 },
18168 );
18169 let prepare_rename_task = cx
18170 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18171 .expect("Prepare rename was not started");
18172 prepare_rename_handler.next().await.unwrap();
18173 prepare_rename_task.await.expect("Prepare rename failed");
18174
18175 let mut rename_handler =
18176 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18177 let edit = lsp::TextEdit {
18178 range: lsp::Range {
18179 start: lsp::Position {
18180 line: 0,
18181 character: 7,
18182 },
18183 end: lsp::Position {
18184 line: 0,
18185 character: 10,
18186 },
18187 },
18188 new_text: "FooRenamed".to_string(),
18189 };
18190 Ok(Some(lsp::WorkspaceEdit::new(
18191 // Specify the same edit twice
18192 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18193 )))
18194 });
18195 let rename_task = cx
18196 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18197 .expect("Confirm rename was not started");
18198 rename_handler.next().await.unwrap();
18199 rename_task.await.expect("Confirm rename failed");
18200 cx.run_until_parked();
18201
18202 // Despite two edits, only one is actually applied as those are identical
18203 cx.assert_editor_state(indoc! {"
18204 struct FooRenamedˇ {}
18205 "});
18206}
18207
18208#[gpui::test]
18209async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18210 init_test(cx, |_| {});
18211 // These capabilities indicate that the server does not support prepare rename.
18212 let capabilities = lsp::ServerCapabilities {
18213 rename_provider: Some(lsp::OneOf::Left(true)),
18214 ..Default::default()
18215 };
18216 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18217
18218 cx.set_state(indoc! {"
18219 struct Fˇoo {}
18220 "});
18221
18222 cx.update_editor(|editor, _window, cx| {
18223 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18224 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18225 editor.highlight_background::<DocumentHighlightRead>(
18226 &[highlight_range],
18227 |c| c.editor_document_highlight_read_background,
18228 cx,
18229 );
18230 });
18231
18232 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18233 .expect("Prepare rename was not started")
18234 .await
18235 .expect("Prepare rename failed");
18236
18237 let mut rename_handler =
18238 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18239 let edit = lsp::TextEdit {
18240 range: lsp::Range {
18241 start: lsp::Position {
18242 line: 0,
18243 character: 7,
18244 },
18245 end: lsp::Position {
18246 line: 0,
18247 character: 10,
18248 },
18249 },
18250 new_text: "FooRenamed".to_string(),
18251 };
18252 Ok(Some(lsp::WorkspaceEdit::new(
18253 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18254 )))
18255 });
18256 let rename_task = cx
18257 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18258 .expect("Confirm rename was not started");
18259 rename_handler.next().await.unwrap();
18260 rename_task.await.expect("Confirm rename failed");
18261 cx.run_until_parked();
18262
18263 // Correct range is renamed, as `surrounding_word` is used to find it.
18264 cx.assert_editor_state(indoc! {"
18265 struct FooRenamedˇ {}
18266 "});
18267}
18268
18269#[gpui::test]
18270async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18271 init_test(cx, |_| {});
18272 let mut cx = EditorTestContext::new(cx).await;
18273
18274 let language = Arc::new(
18275 Language::new(
18276 LanguageConfig::default(),
18277 Some(tree_sitter_html::LANGUAGE.into()),
18278 )
18279 .with_brackets_query(
18280 r#"
18281 ("<" @open "/>" @close)
18282 ("</" @open ">" @close)
18283 ("<" @open ">" @close)
18284 ("\"" @open "\"" @close)
18285 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18286 "#,
18287 )
18288 .unwrap(),
18289 );
18290 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18291
18292 cx.set_state(indoc! {"
18293 <span>ˇ</span>
18294 "});
18295 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18296 cx.assert_editor_state(indoc! {"
18297 <span>
18298 ˇ
18299 </span>
18300 "});
18301
18302 cx.set_state(indoc! {"
18303 <span><span></span>ˇ</span>
18304 "});
18305 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18306 cx.assert_editor_state(indoc! {"
18307 <span><span></span>
18308 ˇ</span>
18309 "});
18310
18311 cx.set_state(indoc! {"
18312 <span>ˇ
18313 </span>
18314 "});
18315 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18316 cx.assert_editor_state(indoc! {"
18317 <span>
18318 ˇ
18319 </span>
18320 "});
18321}
18322
18323#[gpui::test(iterations = 10)]
18324async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18325 init_test(cx, |_| {});
18326
18327 let fs = FakeFs::new(cx.executor());
18328 fs.insert_tree(
18329 path!("/dir"),
18330 json!({
18331 "a.ts": "a",
18332 }),
18333 )
18334 .await;
18335
18336 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18337 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18338 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18339
18340 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18341 language_registry.add(Arc::new(Language::new(
18342 LanguageConfig {
18343 name: "TypeScript".into(),
18344 matcher: LanguageMatcher {
18345 path_suffixes: vec!["ts".to_string()],
18346 ..Default::default()
18347 },
18348 ..Default::default()
18349 },
18350 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18351 )));
18352 let mut fake_language_servers = language_registry.register_fake_lsp(
18353 "TypeScript",
18354 FakeLspAdapter {
18355 capabilities: lsp::ServerCapabilities {
18356 code_lens_provider: Some(lsp::CodeLensOptions {
18357 resolve_provider: Some(true),
18358 }),
18359 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18360 commands: vec!["_the/command".to_string()],
18361 ..lsp::ExecuteCommandOptions::default()
18362 }),
18363 ..lsp::ServerCapabilities::default()
18364 },
18365 ..FakeLspAdapter::default()
18366 },
18367 );
18368
18369 let (buffer, _handle) = project
18370 .update(cx, |p, cx| {
18371 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18372 })
18373 .await
18374 .unwrap();
18375 cx.executor().run_until_parked();
18376
18377 let fake_server = fake_language_servers.next().await.unwrap();
18378
18379 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18380 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18381 drop(buffer_snapshot);
18382 let actions = cx
18383 .update_window(*workspace, |_, window, cx| {
18384 project.code_actions(&buffer, anchor..anchor, window, cx)
18385 })
18386 .unwrap();
18387
18388 fake_server
18389 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18390 Ok(Some(vec![
18391 lsp::CodeLens {
18392 range: lsp::Range::default(),
18393 command: Some(lsp::Command {
18394 title: "Code lens command".to_owned(),
18395 command: "_the/command".to_owned(),
18396 arguments: None,
18397 }),
18398 data: None,
18399 },
18400 lsp::CodeLens {
18401 range: lsp::Range::default(),
18402 command: Some(lsp::Command {
18403 title: "Command not in capabilities".to_owned(),
18404 command: "not in capabilities".to_owned(),
18405 arguments: None,
18406 }),
18407 data: None,
18408 },
18409 lsp::CodeLens {
18410 range: lsp::Range {
18411 start: lsp::Position {
18412 line: 1,
18413 character: 1,
18414 },
18415 end: lsp::Position {
18416 line: 1,
18417 character: 1,
18418 },
18419 },
18420 command: Some(lsp::Command {
18421 title: "Command not in range".to_owned(),
18422 command: "_the/command".to_owned(),
18423 arguments: None,
18424 }),
18425 data: None,
18426 },
18427 ]))
18428 })
18429 .next()
18430 .await;
18431
18432 let actions = actions.await.unwrap();
18433 assert_eq!(
18434 actions.len(),
18435 1,
18436 "Should have only one valid action for the 0..0 range"
18437 );
18438 let action = actions[0].clone();
18439 let apply = project.update(cx, |project, cx| {
18440 project.apply_code_action(buffer.clone(), action, true, cx)
18441 });
18442
18443 // Resolving the code action does not populate its edits. In absence of
18444 // edits, we must execute the given command.
18445 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18446 |mut lens, _| async move {
18447 let lens_command = lens.command.as_mut().expect("should have a command");
18448 assert_eq!(lens_command.title, "Code lens command");
18449 lens_command.arguments = Some(vec![json!("the-argument")]);
18450 Ok(lens)
18451 },
18452 );
18453
18454 // While executing the command, the language server sends the editor
18455 // a `workspaceEdit` request.
18456 fake_server
18457 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18458 let fake = fake_server.clone();
18459 move |params, _| {
18460 assert_eq!(params.command, "_the/command");
18461 let fake = fake.clone();
18462 async move {
18463 fake.server
18464 .request::<lsp::request::ApplyWorkspaceEdit>(
18465 lsp::ApplyWorkspaceEditParams {
18466 label: None,
18467 edit: lsp::WorkspaceEdit {
18468 changes: Some(
18469 [(
18470 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18471 vec![lsp::TextEdit {
18472 range: lsp::Range::new(
18473 lsp::Position::new(0, 0),
18474 lsp::Position::new(0, 0),
18475 ),
18476 new_text: "X".into(),
18477 }],
18478 )]
18479 .into_iter()
18480 .collect(),
18481 ),
18482 ..Default::default()
18483 },
18484 },
18485 )
18486 .await
18487 .unwrap();
18488 Ok(Some(json!(null)))
18489 }
18490 }
18491 })
18492 .next()
18493 .await;
18494
18495 // Applying the code lens command returns a project transaction containing the edits
18496 // sent by the language server in its `workspaceEdit` request.
18497 let transaction = apply.await.unwrap();
18498 assert!(transaction.0.contains_key(&buffer));
18499 buffer.update(cx, |buffer, cx| {
18500 assert_eq!(buffer.text(), "Xa");
18501 buffer.undo(cx);
18502 assert_eq!(buffer.text(), "a");
18503 });
18504}
18505
18506#[gpui::test]
18507async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18508 init_test(cx, |_| {});
18509
18510 let fs = FakeFs::new(cx.executor());
18511 let main_text = r#"fn main() {
18512println!("1");
18513println!("2");
18514println!("3");
18515println!("4");
18516println!("5");
18517}"#;
18518 let lib_text = "mod foo {}";
18519 fs.insert_tree(
18520 path!("/a"),
18521 json!({
18522 "lib.rs": lib_text,
18523 "main.rs": main_text,
18524 }),
18525 )
18526 .await;
18527
18528 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18529 let (workspace, cx) =
18530 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18531 let worktree_id = workspace.update(cx, |workspace, cx| {
18532 workspace.project().update(cx, |project, cx| {
18533 project.worktrees(cx).next().unwrap().read(cx).id()
18534 })
18535 });
18536
18537 let expected_ranges = vec![
18538 Point::new(0, 0)..Point::new(0, 0),
18539 Point::new(1, 0)..Point::new(1, 1),
18540 Point::new(2, 0)..Point::new(2, 2),
18541 Point::new(3, 0)..Point::new(3, 3),
18542 ];
18543
18544 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18545 let editor_1 = workspace
18546 .update_in(cx, |workspace, window, cx| {
18547 workspace.open_path(
18548 (worktree_id, "main.rs"),
18549 Some(pane_1.downgrade()),
18550 true,
18551 window,
18552 cx,
18553 )
18554 })
18555 .unwrap()
18556 .await
18557 .downcast::<Editor>()
18558 .unwrap();
18559 pane_1.update(cx, |pane, cx| {
18560 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18561 open_editor.update(cx, |editor, cx| {
18562 assert_eq!(
18563 editor.display_text(cx),
18564 main_text,
18565 "Original main.rs text on initial open",
18566 );
18567 assert_eq!(
18568 editor
18569 .selections
18570 .all::<Point>(cx)
18571 .into_iter()
18572 .map(|s| s.range())
18573 .collect::<Vec<_>>(),
18574 vec![Point::zero()..Point::zero()],
18575 "Default selections on initial open",
18576 );
18577 })
18578 });
18579 editor_1.update_in(cx, |editor, window, cx| {
18580 editor.change_selections(None, window, cx, |s| {
18581 s.select_ranges(expected_ranges.clone());
18582 });
18583 });
18584
18585 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18586 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18587 });
18588 let editor_2 = workspace
18589 .update_in(cx, |workspace, window, cx| {
18590 workspace.open_path(
18591 (worktree_id, "main.rs"),
18592 Some(pane_2.downgrade()),
18593 true,
18594 window,
18595 cx,
18596 )
18597 })
18598 .unwrap()
18599 .await
18600 .downcast::<Editor>()
18601 .unwrap();
18602 pane_2.update(cx, |pane, cx| {
18603 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18604 open_editor.update(cx, |editor, cx| {
18605 assert_eq!(
18606 editor.display_text(cx),
18607 main_text,
18608 "Original main.rs text on initial open in another panel",
18609 );
18610 assert_eq!(
18611 editor
18612 .selections
18613 .all::<Point>(cx)
18614 .into_iter()
18615 .map(|s| s.range())
18616 .collect::<Vec<_>>(),
18617 vec![Point::zero()..Point::zero()],
18618 "Default selections on initial open in another panel",
18619 );
18620 })
18621 });
18622
18623 editor_2.update_in(cx, |editor, window, cx| {
18624 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18625 });
18626
18627 let _other_editor_1 = workspace
18628 .update_in(cx, |workspace, window, cx| {
18629 workspace.open_path(
18630 (worktree_id, "lib.rs"),
18631 Some(pane_1.downgrade()),
18632 true,
18633 window,
18634 cx,
18635 )
18636 })
18637 .unwrap()
18638 .await
18639 .downcast::<Editor>()
18640 .unwrap();
18641 pane_1
18642 .update_in(cx, |pane, window, cx| {
18643 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18644 .unwrap()
18645 })
18646 .await
18647 .unwrap();
18648 drop(editor_1);
18649 pane_1.update(cx, |pane, cx| {
18650 pane.active_item()
18651 .unwrap()
18652 .downcast::<Editor>()
18653 .unwrap()
18654 .update(cx, |editor, cx| {
18655 assert_eq!(
18656 editor.display_text(cx),
18657 lib_text,
18658 "Other file should be open and active",
18659 );
18660 });
18661 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18662 });
18663
18664 let _other_editor_2 = workspace
18665 .update_in(cx, |workspace, window, cx| {
18666 workspace.open_path(
18667 (worktree_id, "lib.rs"),
18668 Some(pane_2.downgrade()),
18669 true,
18670 window,
18671 cx,
18672 )
18673 })
18674 .unwrap()
18675 .await
18676 .downcast::<Editor>()
18677 .unwrap();
18678 pane_2
18679 .update_in(cx, |pane, window, cx| {
18680 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18681 .unwrap()
18682 })
18683 .await
18684 .unwrap();
18685 drop(editor_2);
18686 pane_2.update(cx, |pane, cx| {
18687 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18688 open_editor.update(cx, |editor, cx| {
18689 assert_eq!(
18690 editor.display_text(cx),
18691 lib_text,
18692 "Other file should be open and active in another panel too",
18693 );
18694 });
18695 assert_eq!(
18696 pane.items().count(),
18697 1,
18698 "No other editors should be open in another pane",
18699 );
18700 });
18701
18702 let _editor_1_reopened = workspace
18703 .update_in(cx, |workspace, window, cx| {
18704 workspace.open_path(
18705 (worktree_id, "main.rs"),
18706 Some(pane_1.downgrade()),
18707 true,
18708 window,
18709 cx,
18710 )
18711 })
18712 .unwrap()
18713 .await
18714 .downcast::<Editor>()
18715 .unwrap();
18716 let _editor_2_reopened = workspace
18717 .update_in(cx, |workspace, window, cx| {
18718 workspace.open_path(
18719 (worktree_id, "main.rs"),
18720 Some(pane_2.downgrade()),
18721 true,
18722 window,
18723 cx,
18724 )
18725 })
18726 .unwrap()
18727 .await
18728 .downcast::<Editor>()
18729 .unwrap();
18730 pane_1.update(cx, |pane, cx| {
18731 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18732 open_editor.update(cx, |editor, cx| {
18733 assert_eq!(
18734 editor.display_text(cx),
18735 main_text,
18736 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18737 );
18738 assert_eq!(
18739 editor
18740 .selections
18741 .all::<Point>(cx)
18742 .into_iter()
18743 .map(|s| s.range())
18744 .collect::<Vec<_>>(),
18745 expected_ranges,
18746 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18747 );
18748 })
18749 });
18750 pane_2.update(cx, |pane, cx| {
18751 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18752 open_editor.update(cx, |editor, cx| {
18753 assert_eq!(
18754 editor.display_text(cx),
18755 r#"fn main() {
18756⋯rintln!("1");
18757⋯intln!("2");
18758⋯ntln!("3");
18759println!("4");
18760println!("5");
18761}"#,
18762 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18763 );
18764 assert_eq!(
18765 editor
18766 .selections
18767 .all::<Point>(cx)
18768 .into_iter()
18769 .map(|s| s.range())
18770 .collect::<Vec<_>>(),
18771 vec![Point::zero()..Point::zero()],
18772 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18773 );
18774 })
18775 });
18776}
18777
18778#[gpui::test]
18779async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18780 init_test(cx, |_| {});
18781
18782 let fs = FakeFs::new(cx.executor());
18783 let main_text = r#"fn main() {
18784println!("1");
18785println!("2");
18786println!("3");
18787println!("4");
18788println!("5");
18789}"#;
18790 let lib_text = "mod foo {}";
18791 fs.insert_tree(
18792 path!("/a"),
18793 json!({
18794 "lib.rs": lib_text,
18795 "main.rs": main_text,
18796 }),
18797 )
18798 .await;
18799
18800 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18801 let (workspace, cx) =
18802 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18803 let worktree_id = workspace.update(cx, |workspace, cx| {
18804 workspace.project().update(cx, |project, cx| {
18805 project.worktrees(cx).next().unwrap().read(cx).id()
18806 })
18807 });
18808
18809 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18810 let editor = workspace
18811 .update_in(cx, |workspace, window, cx| {
18812 workspace.open_path(
18813 (worktree_id, "main.rs"),
18814 Some(pane.downgrade()),
18815 true,
18816 window,
18817 cx,
18818 )
18819 })
18820 .unwrap()
18821 .await
18822 .downcast::<Editor>()
18823 .unwrap();
18824 pane.update(cx, |pane, cx| {
18825 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18826 open_editor.update(cx, |editor, cx| {
18827 assert_eq!(
18828 editor.display_text(cx),
18829 main_text,
18830 "Original main.rs text on initial open",
18831 );
18832 })
18833 });
18834 editor.update_in(cx, |editor, window, cx| {
18835 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18836 });
18837
18838 cx.update_global(|store: &mut SettingsStore, cx| {
18839 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18840 s.restore_on_file_reopen = Some(false);
18841 });
18842 });
18843 editor.update_in(cx, |editor, window, cx| {
18844 editor.fold_ranges(
18845 vec![
18846 Point::new(1, 0)..Point::new(1, 1),
18847 Point::new(2, 0)..Point::new(2, 2),
18848 Point::new(3, 0)..Point::new(3, 3),
18849 ],
18850 false,
18851 window,
18852 cx,
18853 );
18854 });
18855 pane.update_in(cx, |pane, window, cx| {
18856 pane.close_all_items(&CloseAllItems::default(), window, cx)
18857 .unwrap()
18858 })
18859 .await
18860 .unwrap();
18861 pane.update(cx, |pane, _| {
18862 assert!(pane.active_item().is_none());
18863 });
18864 cx.update_global(|store: &mut SettingsStore, cx| {
18865 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18866 s.restore_on_file_reopen = Some(true);
18867 });
18868 });
18869
18870 let _editor_reopened = workspace
18871 .update_in(cx, |workspace, window, cx| {
18872 workspace.open_path(
18873 (worktree_id, "main.rs"),
18874 Some(pane.downgrade()),
18875 true,
18876 window,
18877 cx,
18878 )
18879 })
18880 .unwrap()
18881 .await
18882 .downcast::<Editor>()
18883 .unwrap();
18884 pane.update(cx, |pane, cx| {
18885 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18886 open_editor.update(cx, |editor, cx| {
18887 assert_eq!(
18888 editor.display_text(cx),
18889 main_text,
18890 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18891 );
18892 })
18893 });
18894}
18895
18896fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18897 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18898 point..point
18899}
18900
18901fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18902 let (text, ranges) = marked_text_ranges(marked_text, true);
18903 assert_eq!(editor.text(cx), text);
18904 assert_eq!(
18905 editor.selections.ranges(cx),
18906 ranges,
18907 "Assert selections are {}",
18908 marked_text
18909 );
18910}
18911
18912pub fn handle_signature_help_request(
18913 cx: &mut EditorLspTestContext,
18914 mocked_response: lsp::SignatureHelp,
18915) -> impl Future<Output = ()> + use<> {
18916 let mut request =
18917 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18918 let mocked_response = mocked_response.clone();
18919 async move { Ok(Some(mocked_response)) }
18920 });
18921
18922 async move {
18923 request.next().await;
18924 }
18925}
18926
18927/// Handle completion request passing a marked string specifying where the completion
18928/// should be triggered from using '|' character, what range should be replaced, and what completions
18929/// should be returned using '<' and '>' to delimit the range.
18930///
18931/// Also see `handle_completion_request_with_insert_and_replace`.
18932#[track_caller]
18933pub fn handle_completion_request(
18934 cx: &mut EditorLspTestContext,
18935 marked_string: &str,
18936 completions: Vec<&'static str>,
18937 counter: Arc<AtomicUsize>,
18938) -> impl Future<Output = ()> {
18939 let complete_from_marker: TextRangeMarker = '|'.into();
18940 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18941 let (_, mut marked_ranges) = marked_text_ranges_by(
18942 marked_string,
18943 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18944 );
18945
18946 let complete_from_position =
18947 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18948 let replace_range =
18949 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18950
18951 let mut request =
18952 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18953 let completions = completions.clone();
18954 counter.fetch_add(1, atomic::Ordering::Release);
18955 async move {
18956 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18957 assert_eq!(
18958 params.text_document_position.position,
18959 complete_from_position
18960 );
18961 Ok(Some(lsp::CompletionResponse::Array(
18962 completions
18963 .iter()
18964 .map(|completion_text| lsp::CompletionItem {
18965 label: completion_text.to_string(),
18966 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18967 range: replace_range,
18968 new_text: completion_text.to_string(),
18969 })),
18970 ..Default::default()
18971 })
18972 .collect(),
18973 )))
18974 }
18975 });
18976
18977 async move {
18978 request.next().await;
18979 }
18980}
18981
18982/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
18983/// given instead, which also contains an `insert` range.
18984///
18985/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
18986/// that is, `replace_range.start..cursor_pos`.
18987pub fn handle_completion_request_with_insert_and_replace(
18988 cx: &mut EditorLspTestContext,
18989 marked_string: &str,
18990 completions: Vec<&'static str>,
18991 counter: Arc<AtomicUsize>,
18992) -> impl Future<Output = ()> {
18993 let complete_from_marker: TextRangeMarker = '|'.into();
18994 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18995 let (_, mut marked_ranges) = marked_text_ranges_by(
18996 marked_string,
18997 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18998 );
18999
19000 let complete_from_position =
19001 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19002 let replace_range =
19003 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19004
19005 let mut request =
19006 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19007 let completions = completions.clone();
19008 counter.fetch_add(1, atomic::Ordering::Release);
19009 async move {
19010 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19011 assert_eq!(
19012 params.text_document_position.position, complete_from_position,
19013 "marker `|` position doesn't match",
19014 );
19015 Ok(Some(lsp::CompletionResponse::Array(
19016 completions
19017 .iter()
19018 .map(|completion_text| lsp::CompletionItem {
19019 label: completion_text.to_string(),
19020 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19021 lsp::InsertReplaceEdit {
19022 insert: lsp::Range {
19023 start: replace_range.start,
19024 end: complete_from_position,
19025 },
19026 replace: replace_range,
19027 new_text: completion_text.to_string(),
19028 },
19029 )),
19030 ..Default::default()
19031 })
19032 .collect(),
19033 )))
19034 }
19035 });
19036
19037 async move {
19038 request.next().await;
19039 }
19040}
19041
19042fn handle_resolve_completion_request(
19043 cx: &mut EditorLspTestContext,
19044 edits: Option<Vec<(&'static str, &'static str)>>,
19045) -> impl Future<Output = ()> {
19046 let edits = edits.map(|edits| {
19047 edits
19048 .iter()
19049 .map(|(marked_string, new_text)| {
19050 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19051 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19052 lsp::TextEdit::new(replace_range, new_text.to_string())
19053 })
19054 .collect::<Vec<_>>()
19055 });
19056
19057 let mut request =
19058 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19059 let edits = edits.clone();
19060 async move {
19061 Ok(lsp::CompletionItem {
19062 additional_text_edits: edits,
19063 ..Default::default()
19064 })
19065 }
19066 });
19067
19068 async move {
19069 request.next().await;
19070 }
19071}
19072
19073pub(crate) fn update_test_language_settings(
19074 cx: &mut TestAppContext,
19075 f: impl Fn(&mut AllLanguageSettingsContent),
19076) {
19077 cx.update(|cx| {
19078 SettingsStore::update_global(cx, |store, cx| {
19079 store.update_user_settings::<AllLanguageSettings>(cx, f);
19080 });
19081 });
19082}
19083
19084pub(crate) fn update_test_project_settings(
19085 cx: &mut TestAppContext,
19086 f: impl Fn(&mut ProjectSettings),
19087) {
19088 cx.update(|cx| {
19089 SettingsStore::update_global(cx, |store, cx| {
19090 store.update_user_settings::<ProjectSettings>(cx, f);
19091 });
19092 });
19093}
19094
19095pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19096 cx.update(|cx| {
19097 assets::Assets.load_test_fonts(cx);
19098 let store = SettingsStore::test(cx);
19099 cx.set_global(store);
19100 theme::init(theme::LoadThemes::JustBase, cx);
19101 release_channel::init(SemanticVersion::default(), cx);
19102 client::init_settings(cx);
19103 language::init(cx);
19104 Project::init_settings(cx);
19105 workspace::init_settings(cx);
19106 crate::init(cx);
19107 });
19108
19109 update_test_language_settings(cx, f);
19110}
19111
19112#[track_caller]
19113fn assert_hunk_revert(
19114 not_reverted_text_with_selections: &str,
19115 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19116 expected_reverted_text_with_selections: &str,
19117 base_text: &str,
19118 cx: &mut EditorLspTestContext,
19119) {
19120 cx.set_state(not_reverted_text_with_selections);
19121 cx.set_head_text(base_text);
19122 cx.executor().run_until_parked();
19123
19124 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19125 let snapshot = editor.snapshot(window, cx);
19126 let reverted_hunk_statuses = snapshot
19127 .buffer_snapshot
19128 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19129 .map(|hunk| hunk.status().kind)
19130 .collect::<Vec<_>>();
19131
19132 editor.git_restore(&Default::default(), window, cx);
19133 reverted_hunk_statuses
19134 });
19135 cx.executor().run_until_parked();
19136 cx.assert_editor_state(expected_reverted_text_with_selections);
19137 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19138}