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(cx: &mut TestAppContext) {
2922 init_test(cx, |settings| {
2923 settings.defaults.tab_size = NonZeroU32::new(4)
2924 });
2925
2926 let language = Arc::new(
2927 Language::new(
2928 LanguageConfig::default(),
2929 Some(tree_sitter_rust::LANGUAGE.into()),
2930 )
2931 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2932 .unwrap(),
2933 );
2934
2935 let mut cx = EditorTestContext::new(cx).await;
2936 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2937 cx.set_state(indoc! {"
2938 fn a() {
2939 if b {
2940 \t ˇc
2941 }
2942 }
2943 "});
2944
2945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2946 cx.assert_editor_state(indoc! {"
2947 fn a() {
2948 if b {
2949 ˇc
2950 }
2951 }
2952 "});
2953}
2954
2955#[gpui::test]
2956async fn test_indent_outdent(cx: &mut TestAppContext) {
2957 init_test(cx, |settings| {
2958 settings.defaults.tab_size = NonZeroU32::new(4);
2959 });
2960
2961 let mut cx = EditorTestContext::new(cx).await;
2962
2963 cx.set_state(indoc! {"
2964 «oneˇ» «twoˇ»
2965 three
2966 four
2967 "});
2968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 «oneˇ» «twoˇ»
2971 three
2972 four
2973 "});
2974
2975 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2976 cx.assert_editor_state(indoc! {"
2977 «oneˇ» «twoˇ»
2978 three
2979 four
2980 "});
2981
2982 // select across line ending
2983 cx.set_state(indoc! {"
2984 one two
2985 t«hree
2986 ˇ» four
2987 "});
2988 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2989 cx.assert_editor_state(indoc! {"
2990 one two
2991 t«hree
2992 ˇ» four
2993 "});
2994
2995 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 t«hree
2999 ˇ» four
3000 "});
3001
3002 // Ensure that indenting/outdenting works when the cursor is at column 0.
3003 cx.set_state(indoc! {"
3004 one two
3005 ˇthree
3006 four
3007 "});
3008 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3009 cx.assert_editor_state(indoc! {"
3010 one two
3011 ˇthree
3012 four
3013 "});
3014
3015 cx.set_state(indoc! {"
3016 one two
3017 ˇ three
3018 four
3019 "});
3020 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 one two
3023 ˇthree
3024 four
3025 "});
3026}
3027
3028#[gpui::test]
3029async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3030 init_test(cx, |settings| {
3031 settings.defaults.hard_tabs = Some(true);
3032 });
3033
3034 let mut cx = EditorTestContext::new(cx).await;
3035
3036 // select two ranges on one line
3037 cx.set_state(indoc! {"
3038 «oneˇ» «twoˇ»
3039 three
3040 four
3041 "});
3042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3043 cx.assert_editor_state(indoc! {"
3044 \t«oneˇ» «twoˇ»
3045 three
3046 four
3047 "});
3048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 \t\t«oneˇ» «twoˇ»
3051 three
3052 four
3053 "});
3054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 \t«oneˇ» «twoˇ»
3057 three
3058 four
3059 "});
3060 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3061 cx.assert_editor_state(indoc! {"
3062 «oneˇ» «twoˇ»
3063 three
3064 four
3065 "});
3066
3067 // select across a line ending
3068 cx.set_state(indoc! {"
3069 one two
3070 t«hree
3071 ˇ»four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 one two
3076 \tt«hree
3077 ˇ»four
3078 "});
3079 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 one two
3082 \t\tt«hree
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 \tt«hree
3089 ˇ»four
3090 "});
3091 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 one two
3094 t«hree
3095 ˇ»four
3096 "});
3097
3098 // Ensure that indenting/outdenting works when the cursor is at column 0.
3099 cx.set_state(indoc! {"
3100 one two
3101 ˇthree
3102 four
3103 "});
3104 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 ˇthree
3108 four
3109 "});
3110 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 \tˇthree
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 ˇthree
3120 four
3121 "});
3122}
3123
3124#[gpui::test]
3125fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3126 init_test(cx, |settings| {
3127 settings.languages.extend([
3128 (
3129 "TOML".into(),
3130 LanguageSettingsContent {
3131 tab_size: NonZeroU32::new(2),
3132 ..Default::default()
3133 },
3134 ),
3135 (
3136 "Rust".into(),
3137 LanguageSettingsContent {
3138 tab_size: NonZeroU32::new(4),
3139 ..Default::default()
3140 },
3141 ),
3142 ]);
3143 });
3144
3145 let toml_language = Arc::new(Language::new(
3146 LanguageConfig {
3147 name: "TOML".into(),
3148 ..Default::default()
3149 },
3150 None,
3151 ));
3152 let rust_language = Arc::new(Language::new(
3153 LanguageConfig {
3154 name: "Rust".into(),
3155 ..Default::default()
3156 },
3157 None,
3158 ));
3159
3160 let toml_buffer =
3161 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3162 let rust_buffer =
3163 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3164 let multibuffer = cx.new(|cx| {
3165 let mut multibuffer = MultiBuffer::new(ReadWrite);
3166 multibuffer.push_excerpts(
3167 toml_buffer.clone(),
3168 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3169 cx,
3170 );
3171 multibuffer.push_excerpts(
3172 rust_buffer.clone(),
3173 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3174 cx,
3175 );
3176 multibuffer
3177 });
3178
3179 cx.add_window(|window, cx| {
3180 let mut editor = build_editor(multibuffer, window, cx);
3181
3182 assert_eq!(
3183 editor.text(cx),
3184 indoc! {"
3185 a = 1
3186 b = 2
3187
3188 const c: usize = 3;
3189 "}
3190 );
3191
3192 select_ranges(
3193 &mut editor,
3194 indoc! {"
3195 «aˇ» = 1
3196 b = 2
3197
3198 «const c:ˇ» usize = 3;
3199 "},
3200 window,
3201 cx,
3202 );
3203
3204 editor.tab(&Tab, window, cx);
3205 assert_text_with_selections(
3206 &mut editor,
3207 indoc! {"
3208 «aˇ» = 1
3209 b = 2
3210
3211 «const c:ˇ» usize = 3;
3212 "},
3213 cx,
3214 );
3215 editor.backtab(&Backtab, window, cx);
3216 assert_text_with_selections(
3217 &mut editor,
3218 indoc! {"
3219 «aˇ» = 1
3220 b = 2
3221
3222 «const c:ˇ» usize = 3;
3223 "},
3224 cx,
3225 );
3226
3227 editor
3228 });
3229}
3230
3231#[gpui::test]
3232async fn test_backspace(cx: &mut TestAppContext) {
3233 init_test(cx, |_| {});
3234
3235 let mut cx = EditorTestContext::new(cx).await;
3236
3237 // Basic backspace
3238 cx.set_state(indoc! {"
3239 onˇe two three
3240 fou«rˇ» five six
3241 seven «ˇeight nine
3242 »ten
3243 "});
3244 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3245 cx.assert_editor_state(indoc! {"
3246 oˇe two three
3247 fouˇ five six
3248 seven ˇten
3249 "});
3250
3251 // Test backspace inside and around indents
3252 cx.set_state(indoc! {"
3253 zero
3254 ˇone
3255 ˇtwo
3256 ˇ ˇ ˇ three
3257 ˇ ˇ four
3258 "});
3259 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3260 cx.assert_editor_state(indoc! {"
3261 zero
3262 ˇone
3263 ˇtwo
3264 ˇ threeˇ four
3265 "});
3266}
3267
3268#[gpui::test]
3269async fn test_delete(cx: &mut TestAppContext) {
3270 init_test(cx, |_| {});
3271
3272 let mut cx = EditorTestContext::new(cx).await;
3273 cx.set_state(indoc! {"
3274 onˇe two three
3275 fou«rˇ» five six
3276 seven «ˇeight nine
3277 »ten
3278 "});
3279 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3280 cx.assert_editor_state(indoc! {"
3281 onˇ two three
3282 fouˇ five six
3283 seven ˇten
3284 "});
3285}
3286
3287#[gpui::test]
3288fn test_delete_line(cx: &mut TestAppContext) {
3289 init_test(cx, |_| {});
3290
3291 let editor = cx.add_window(|window, cx| {
3292 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3293 build_editor(buffer, window, cx)
3294 });
3295 _ = editor.update(cx, |editor, window, cx| {
3296 editor.change_selections(None, window, cx, |s| {
3297 s.select_display_ranges([
3298 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3299 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3300 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3301 ])
3302 });
3303 editor.delete_line(&DeleteLine, window, cx);
3304 assert_eq!(editor.display_text(cx), "ghi");
3305 assert_eq!(
3306 editor.selections.display_ranges(cx),
3307 vec![
3308 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3309 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3310 ]
3311 );
3312 });
3313
3314 let editor = cx.add_window(|window, cx| {
3315 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3316 build_editor(buffer, window, cx)
3317 });
3318 _ = editor.update(cx, |editor, window, cx| {
3319 editor.change_selections(None, window, cx, |s| {
3320 s.select_display_ranges([
3321 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3322 ])
3323 });
3324 editor.delete_line(&DeleteLine, window, cx);
3325 assert_eq!(editor.display_text(cx), "ghi\n");
3326 assert_eq!(
3327 editor.selections.display_ranges(cx),
3328 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3329 );
3330 });
3331}
3332
3333#[gpui::test]
3334fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3335 init_test(cx, |_| {});
3336
3337 cx.add_window(|window, cx| {
3338 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3339 let mut editor = build_editor(buffer.clone(), window, cx);
3340 let buffer = buffer.read(cx).as_singleton().unwrap();
3341
3342 assert_eq!(
3343 editor.selections.ranges::<Point>(cx),
3344 &[Point::new(0, 0)..Point::new(0, 0)]
3345 );
3346
3347 // When on single line, replace newline at end by space
3348 editor.join_lines(&JoinLines, window, cx);
3349 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3350 assert_eq!(
3351 editor.selections.ranges::<Point>(cx),
3352 &[Point::new(0, 3)..Point::new(0, 3)]
3353 );
3354
3355 // When multiple lines are selected, remove newlines that are spanned by the selection
3356 editor.change_selections(None, window, cx, |s| {
3357 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3358 });
3359 editor.join_lines(&JoinLines, window, cx);
3360 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3361 assert_eq!(
3362 editor.selections.ranges::<Point>(cx),
3363 &[Point::new(0, 11)..Point::new(0, 11)]
3364 );
3365
3366 // Undo should be transactional
3367 editor.undo(&Undo, window, cx);
3368 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3369 assert_eq!(
3370 editor.selections.ranges::<Point>(cx),
3371 &[Point::new(0, 5)..Point::new(2, 2)]
3372 );
3373
3374 // When joining an empty line don't insert a space
3375 editor.change_selections(None, window, cx, |s| {
3376 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3377 });
3378 editor.join_lines(&JoinLines, window, cx);
3379 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3380 assert_eq!(
3381 editor.selections.ranges::<Point>(cx),
3382 [Point::new(2, 3)..Point::new(2, 3)]
3383 );
3384
3385 // We can remove trailing newlines
3386 editor.join_lines(&JoinLines, window, cx);
3387 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3388 assert_eq!(
3389 editor.selections.ranges::<Point>(cx),
3390 [Point::new(2, 3)..Point::new(2, 3)]
3391 );
3392
3393 // We don't blow up on the last line
3394 editor.join_lines(&JoinLines, window, cx);
3395 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3396 assert_eq!(
3397 editor.selections.ranges::<Point>(cx),
3398 [Point::new(2, 3)..Point::new(2, 3)]
3399 );
3400
3401 // reset to test indentation
3402 editor.buffer.update(cx, |buffer, cx| {
3403 buffer.edit(
3404 [
3405 (Point::new(1, 0)..Point::new(1, 2), " "),
3406 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3407 ],
3408 None,
3409 cx,
3410 )
3411 });
3412
3413 // We remove any leading spaces
3414 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3415 editor.change_selections(None, window, cx, |s| {
3416 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3417 });
3418 editor.join_lines(&JoinLines, window, cx);
3419 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3420
3421 // We don't insert a space for a line containing only spaces
3422 editor.join_lines(&JoinLines, window, cx);
3423 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3424
3425 // We ignore any leading tabs
3426 editor.join_lines(&JoinLines, window, cx);
3427 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3428
3429 editor
3430 });
3431}
3432
3433#[gpui::test]
3434fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3435 init_test(cx, |_| {});
3436
3437 cx.add_window(|window, cx| {
3438 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3439 let mut editor = build_editor(buffer.clone(), window, cx);
3440 let buffer = buffer.read(cx).as_singleton().unwrap();
3441
3442 editor.change_selections(None, window, cx, |s| {
3443 s.select_ranges([
3444 Point::new(0, 2)..Point::new(1, 1),
3445 Point::new(1, 2)..Point::new(1, 2),
3446 Point::new(3, 1)..Point::new(3, 2),
3447 ])
3448 });
3449
3450 editor.join_lines(&JoinLines, window, cx);
3451 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3452
3453 assert_eq!(
3454 editor.selections.ranges::<Point>(cx),
3455 [
3456 Point::new(0, 7)..Point::new(0, 7),
3457 Point::new(1, 3)..Point::new(1, 3)
3458 ]
3459 );
3460 editor
3461 });
3462}
3463
3464#[gpui::test]
3465async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3466 init_test(cx, |_| {});
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 let diff_base = r#"
3471 Line 0
3472 Line 1
3473 Line 2
3474 Line 3
3475 "#
3476 .unindent();
3477
3478 cx.set_state(
3479 &r#"
3480 ˇLine 0
3481 Line 1
3482 Line 2
3483 Line 3
3484 "#
3485 .unindent(),
3486 );
3487
3488 cx.set_head_text(&diff_base);
3489 executor.run_until_parked();
3490
3491 // Join lines
3492 cx.update_editor(|editor, window, cx| {
3493 editor.join_lines(&JoinLines, window, cx);
3494 });
3495 executor.run_until_parked();
3496
3497 cx.assert_editor_state(
3498 &r#"
3499 Line 0ˇ Line 1
3500 Line 2
3501 Line 3
3502 "#
3503 .unindent(),
3504 );
3505 // Join again
3506 cx.update_editor(|editor, window, cx| {
3507 editor.join_lines(&JoinLines, window, cx);
3508 });
3509 executor.run_until_parked();
3510
3511 cx.assert_editor_state(
3512 &r#"
3513 Line 0 Line 1ˇ Line 2
3514 Line 3
3515 "#
3516 .unindent(),
3517 );
3518}
3519
3520#[gpui::test]
3521async fn test_custom_newlines_cause_no_false_positive_diffs(
3522 executor: BackgroundExecutor,
3523 cx: &mut TestAppContext,
3524) {
3525 init_test(cx, |_| {});
3526 let mut cx = EditorTestContext::new(cx).await;
3527 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3528 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3529 executor.run_until_parked();
3530
3531 cx.update_editor(|editor, window, cx| {
3532 let snapshot = editor.snapshot(window, cx);
3533 assert_eq!(
3534 snapshot
3535 .buffer_snapshot
3536 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3537 .collect::<Vec<_>>(),
3538 Vec::new(),
3539 "Should not have any diffs for files with custom newlines"
3540 );
3541 });
3542}
3543
3544#[gpui::test]
3545async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3546 init_test(cx, |_| {});
3547
3548 let mut cx = EditorTestContext::new(cx).await;
3549
3550 // Test sort_lines_case_insensitive()
3551 cx.set_state(indoc! {"
3552 «z
3553 y
3554 x
3555 Z
3556 Y
3557 Xˇ»
3558 "});
3559 cx.update_editor(|e, window, cx| {
3560 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3561 });
3562 cx.assert_editor_state(indoc! {"
3563 «x
3564 X
3565 y
3566 Y
3567 z
3568 Zˇ»
3569 "});
3570
3571 // Test reverse_lines()
3572 cx.set_state(indoc! {"
3573 «5
3574 4
3575 3
3576 2
3577 1ˇ»
3578 "});
3579 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 «1
3582 2
3583 3
3584 4
3585 5ˇ»
3586 "});
3587
3588 // Skip testing shuffle_line()
3589
3590 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3591 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3592
3593 // Don't manipulate when cursor is on single line, but expand the selection
3594 cx.set_state(indoc! {"
3595 ddˇdd
3596 ccc
3597 bb
3598 a
3599 "});
3600 cx.update_editor(|e, window, cx| {
3601 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3602 });
3603 cx.assert_editor_state(indoc! {"
3604 «ddddˇ»
3605 ccc
3606 bb
3607 a
3608 "});
3609
3610 // Basic manipulate case
3611 // Start selection moves to column 0
3612 // End of selection shrinks to fit shorter line
3613 cx.set_state(indoc! {"
3614 dd«d
3615 ccc
3616 bb
3617 aaaaaˇ»
3618 "});
3619 cx.update_editor(|e, window, cx| {
3620 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3621 });
3622 cx.assert_editor_state(indoc! {"
3623 «aaaaa
3624 bb
3625 ccc
3626 dddˇ»
3627 "});
3628
3629 // Manipulate case with newlines
3630 cx.set_state(indoc! {"
3631 dd«d
3632 ccc
3633
3634 bb
3635 aaaaa
3636
3637 ˇ»
3638 "});
3639 cx.update_editor(|e, window, cx| {
3640 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3641 });
3642 cx.assert_editor_state(indoc! {"
3643 «
3644
3645 aaaaa
3646 bb
3647 ccc
3648 dddˇ»
3649
3650 "});
3651
3652 // Adding new line
3653 cx.set_state(indoc! {"
3654 aa«a
3655 bbˇ»b
3656 "});
3657 cx.update_editor(|e, window, cx| {
3658 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3659 });
3660 cx.assert_editor_state(indoc! {"
3661 «aaa
3662 bbb
3663 added_lineˇ»
3664 "});
3665
3666 // Removing line
3667 cx.set_state(indoc! {"
3668 aa«a
3669 bbbˇ»
3670 "});
3671 cx.update_editor(|e, window, cx| {
3672 e.manipulate_lines(window, cx, |lines| {
3673 lines.pop();
3674 })
3675 });
3676 cx.assert_editor_state(indoc! {"
3677 «aaaˇ»
3678 "});
3679
3680 // Removing all lines
3681 cx.set_state(indoc! {"
3682 aa«a
3683 bbbˇ»
3684 "});
3685 cx.update_editor(|e, window, cx| {
3686 e.manipulate_lines(window, cx, |lines| {
3687 lines.drain(..);
3688 })
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 ˇ
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Consider continuous selection as single selection
3702 cx.set_state(indoc! {"
3703 Aaa«aa
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «Aaaaa
3713 ccc
3714 bb
3715 aaaaaˇ»
3716 "});
3717
3718 cx.set_state(indoc! {"
3719 Aaa«aa
3720 cˇ»c«c
3721 bb
3722 aaaˇ»aa
3723 "});
3724 cx.update_editor(|e, window, cx| {
3725 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3726 });
3727 cx.assert_editor_state(indoc! {"
3728 «Aaaaa
3729 ccc
3730 bbˇ»
3731 "});
3732
3733 // Consider non continuous selection as distinct dedup operations
3734 cx.set_state(indoc! {"
3735 «aaaaa
3736 bb
3737 aaaaa
3738 aaaaaˇ»
3739
3740 aaa«aaˇ»
3741 "});
3742 cx.update_editor(|e, window, cx| {
3743 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3744 });
3745 cx.assert_editor_state(indoc! {"
3746 «aaaaa
3747 bbˇ»
3748
3749 «aaaaaˇ»
3750 "});
3751}
3752
3753#[gpui::test]
3754async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3755 init_test(cx, |_| {});
3756
3757 let mut cx = EditorTestContext::new(cx).await;
3758
3759 cx.set_state(indoc! {"
3760 «Aaa
3761 aAa
3762 Aaaˇ»
3763 "});
3764 cx.update_editor(|e, window, cx| {
3765 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3766 });
3767 cx.assert_editor_state(indoc! {"
3768 «Aaa
3769 aAaˇ»
3770 "});
3771
3772 cx.set_state(indoc! {"
3773 «Aaa
3774 aAa
3775 aaAˇ»
3776 "});
3777 cx.update_editor(|e, window, cx| {
3778 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3779 });
3780 cx.assert_editor_state(indoc! {"
3781 «Aaaˇ»
3782 "});
3783}
3784
3785#[gpui::test]
3786async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3787 init_test(cx, |_| {});
3788
3789 let mut cx = EditorTestContext::new(cx).await;
3790
3791 // Manipulate with multiple selections on a single line
3792 cx.set_state(indoc! {"
3793 dd«dd
3794 cˇ»c«c
3795 bb
3796 aaaˇ»aa
3797 "});
3798 cx.update_editor(|e, window, cx| {
3799 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3800 });
3801 cx.assert_editor_state(indoc! {"
3802 «aaaaa
3803 bb
3804 ccc
3805 ddddˇ»
3806 "});
3807
3808 // Manipulate with multiple disjoin selections
3809 cx.set_state(indoc! {"
3810 5«
3811 4
3812 3
3813 2
3814 1ˇ»
3815
3816 dd«dd
3817 ccc
3818 bb
3819 aaaˇ»aa
3820 "});
3821 cx.update_editor(|e, window, cx| {
3822 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3823 });
3824 cx.assert_editor_state(indoc! {"
3825 «1
3826 2
3827 3
3828 4
3829 5ˇ»
3830
3831 «aaaaa
3832 bb
3833 ccc
3834 ddddˇ»
3835 "});
3836
3837 // Adding lines on each selection
3838 cx.set_state(indoc! {"
3839 2«
3840 1ˇ»
3841
3842 bb«bb
3843 aaaˇ»aa
3844 "});
3845 cx.update_editor(|e, window, cx| {
3846 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3847 });
3848 cx.assert_editor_state(indoc! {"
3849 «2
3850 1
3851 added lineˇ»
3852
3853 «bbbb
3854 aaaaa
3855 added lineˇ»
3856 "});
3857
3858 // Removing lines on each selection
3859 cx.set_state(indoc! {"
3860 2«
3861 1ˇ»
3862
3863 bb«bb
3864 aaaˇ»aa
3865 "});
3866 cx.update_editor(|e, window, cx| {
3867 e.manipulate_lines(window, cx, |lines| {
3868 lines.pop();
3869 })
3870 });
3871 cx.assert_editor_state(indoc! {"
3872 «2ˇ»
3873
3874 «bbbbˇ»
3875 "});
3876}
3877
3878#[gpui::test]
3879async fn test_toggle_case(cx: &mut TestAppContext) {
3880 init_test(cx, |_| {});
3881
3882 let mut cx = EditorTestContext::new(cx).await;
3883
3884 // If all lower case -> upper case
3885 cx.set_state(indoc! {"
3886 «hello worldˇ»
3887 "});
3888 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 «HELLO WORLDˇ»
3891 "});
3892
3893 // If all upper case -> lower case
3894 cx.set_state(indoc! {"
3895 «HELLO WORLDˇ»
3896 "});
3897 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3898 cx.assert_editor_state(indoc! {"
3899 «hello worldˇ»
3900 "});
3901
3902 // If any upper case characters are identified -> lower case
3903 // This matches JetBrains IDEs
3904 cx.set_state(indoc! {"
3905 «hEllo worldˇ»
3906 "});
3907 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3908 cx.assert_editor_state(indoc! {"
3909 «hello worldˇ»
3910 "});
3911}
3912
3913#[gpui::test]
3914async fn test_manipulate_text(cx: &mut TestAppContext) {
3915 init_test(cx, |_| {});
3916
3917 let mut cx = EditorTestContext::new(cx).await;
3918
3919 // Test convert_to_upper_case()
3920 cx.set_state(indoc! {"
3921 «hello worldˇ»
3922 "});
3923 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3924 cx.assert_editor_state(indoc! {"
3925 «HELLO WORLDˇ»
3926 "});
3927
3928 // Test convert_to_lower_case()
3929 cx.set_state(indoc! {"
3930 «HELLO WORLDˇ»
3931 "});
3932 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3933 cx.assert_editor_state(indoc! {"
3934 «hello worldˇ»
3935 "});
3936
3937 // Test multiple line, single selection case
3938 cx.set_state(indoc! {"
3939 «The quick brown
3940 fox jumps over
3941 the lazy dogˇ»
3942 "});
3943 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3944 cx.assert_editor_state(indoc! {"
3945 «The Quick Brown
3946 Fox Jumps Over
3947 The Lazy Dogˇ»
3948 "});
3949
3950 // Test multiple line, single selection case
3951 cx.set_state(indoc! {"
3952 «The quick brown
3953 fox jumps over
3954 the lazy dogˇ»
3955 "});
3956 cx.update_editor(|e, window, cx| {
3957 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3958 });
3959 cx.assert_editor_state(indoc! {"
3960 «TheQuickBrown
3961 FoxJumpsOver
3962 TheLazyDogˇ»
3963 "});
3964
3965 // From here on out, test more complex cases of manipulate_text()
3966
3967 // Test no selection case - should affect words cursors are in
3968 // Cursor at beginning, middle, and end of word
3969 cx.set_state(indoc! {"
3970 ˇhello big beauˇtiful worldˇ
3971 "});
3972 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3973 cx.assert_editor_state(indoc! {"
3974 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3975 "});
3976
3977 // Test multiple selections on a single line and across multiple lines
3978 cx.set_state(indoc! {"
3979 «Theˇ» quick «brown
3980 foxˇ» jumps «overˇ»
3981 the «lazyˇ» dog
3982 "});
3983 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3984 cx.assert_editor_state(indoc! {"
3985 «THEˇ» quick «BROWN
3986 FOXˇ» jumps «OVERˇ»
3987 the «LAZYˇ» dog
3988 "});
3989
3990 // Test case where text length grows
3991 cx.set_state(indoc! {"
3992 «tschüߡ»
3993 "});
3994 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3995 cx.assert_editor_state(indoc! {"
3996 «TSCHÜSSˇ»
3997 "});
3998
3999 // Test to make sure we don't crash when text shrinks
4000 cx.set_state(indoc! {"
4001 aaa_bbbˇ
4002 "});
4003 cx.update_editor(|e, window, cx| {
4004 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4005 });
4006 cx.assert_editor_state(indoc! {"
4007 «aaaBbbˇ»
4008 "});
4009
4010 // Test to make sure we all aware of the fact that each word can grow and shrink
4011 // Final selections should be aware of this fact
4012 cx.set_state(indoc! {"
4013 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4014 "});
4015 cx.update_editor(|e, window, cx| {
4016 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4017 });
4018 cx.assert_editor_state(indoc! {"
4019 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4020 "});
4021
4022 cx.set_state(indoc! {"
4023 «hElLo, WoRld!ˇ»
4024 "});
4025 cx.update_editor(|e, window, cx| {
4026 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4027 });
4028 cx.assert_editor_state(indoc! {"
4029 «HeLlO, wOrLD!ˇ»
4030 "});
4031}
4032
4033#[gpui::test]
4034fn test_duplicate_line(cx: &mut TestAppContext) {
4035 init_test(cx, |_| {});
4036
4037 let editor = cx.add_window(|window, cx| {
4038 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4039 build_editor(buffer, window, cx)
4040 });
4041 _ = editor.update(cx, |editor, window, cx| {
4042 editor.change_selections(None, window, cx, |s| {
4043 s.select_display_ranges([
4044 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4045 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4046 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4047 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4048 ])
4049 });
4050 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4051 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4052 assert_eq!(
4053 editor.selections.display_ranges(cx),
4054 vec![
4055 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4056 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4057 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4058 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4059 ]
4060 );
4061 });
4062
4063 let editor = cx.add_window(|window, cx| {
4064 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4065 build_editor(buffer, window, cx)
4066 });
4067 _ = editor.update(cx, |editor, window, cx| {
4068 editor.change_selections(None, window, cx, |s| {
4069 s.select_display_ranges([
4070 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4071 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4072 ])
4073 });
4074 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4075 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4076 assert_eq!(
4077 editor.selections.display_ranges(cx),
4078 vec![
4079 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4080 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4081 ]
4082 );
4083 });
4084
4085 // With `move_upwards` the selections stay in place, except for
4086 // the lines inserted above them
4087 let editor = cx.add_window(|window, cx| {
4088 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4089 build_editor(buffer, window, cx)
4090 });
4091 _ = editor.update(cx, |editor, window, cx| {
4092 editor.change_selections(None, window, cx, |s| {
4093 s.select_display_ranges([
4094 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4095 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4096 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4097 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4098 ])
4099 });
4100 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4101 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4102 assert_eq!(
4103 editor.selections.display_ranges(cx),
4104 vec![
4105 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4106 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4107 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4108 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4109 ]
4110 );
4111 });
4112
4113 let editor = cx.add_window(|window, cx| {
4114 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4115 build_editor(buffer, window, cx)
4116 });
4117 _ = editor.update(cx, |editor, window, cx| {
4118 editor.change_selections(None, window, cx, |s| {
4119 s.select_display_ranges([
4120 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4121 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4122 ])
4123 });
4124 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4125 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4126 assert_eq!(
4127 editor.selections.display_ranges(cx),
4128 vec![
4129 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4130 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4131 ]
4132 );
4133 });
4134
4135 let editor = cx.add_window(|window, cx| {
4136 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4137 build_editor(buffer, window, cx)
4138 });
4139 _ = editor.update(cx, |editor, window, cx| {
4140 editor.change_selections(None, window, cx, |s| {
4141 s.select_display_ranges([
4142 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4143 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4144 ])
4145 });
4146 editor.duplicate_selection(&DuplicateSelection, window, cx);
4147 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4148 assert_eq!(
4149 editor.selections.display_ranges(cx),
4150 vec![
4151 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4152 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4153 ]
4154 );
4155 });
4156}
4157
4158#[gpui::test]
4159fn test_move_line_up_down(cx: &mut TestAppContext) {
4160 init_test(cx, |_| {});
4161
4162 let editor = cx.add_window(|window, cx| {
4163 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4164 build_editor(buffer, window, cx)
4165 });
4166 _ = editor.update(cx, |editor, window, cx| {
4167 editor.fold_creases(
4168 vec![
4169 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4170 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4171 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4172 ],
4173 true,
4174 window,
4175 cx,
4176 );
4177 editor.change_selections(None, window, cx, |s| {
4178 s.select_display_ranges([
4179 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4180 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4181 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4182 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4183 ])
4184 });
4185 assert_eq!(
4186 editor.display_text(cx),
4187 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4188 );
4189
4190 editor.move_line_up(&MoveLineUp, window, cx);
4191 assert_eq!(
4192 editor.display_text(cx),
4193 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4194 );
4195 assert_eq!(
4196 editor.selections.display_ranges(cx),
4197 vec![
4198 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4199 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4200 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4201 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4202 ]
4203 );
4204 });
4205
4206 _ = editor.update(cx, |editor, window, cx| {
4207 editor.move_line_down(&MoveLineDown, window, cx);
4208 assert_eq!(
4209 editor.display_text(cx),
4210 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4211 );
4212 assert_eq!(
4213 editor.selections.display_ranges(cx),
4214 vec![
4215 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4216 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4217 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4218 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4219 ]
4220 );
4221 });
4222
4223 _ = editor.update(cx, |editor, window, cx| {
4224 editor.move_line_down(&MoveLineDown, window, cx);
4225 assert_eq!(
4226 editor.display_text(cx),
4227 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4228 );
4229 assert_eq!(
4230 editor.selections.display_ranges(cx),
4231 vec![
4232 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4233 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4234 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4235 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4236 ]
4237 );
4238 });
4239
4240 _ = editor.update(cx, |editor, window, cx| {
4241 editor.move_line_up(&MoveLineUp, window, cx);
4242 assert_eq!(
4243 editor.display_text(cx),
4244 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4245 );
4246 assert_eq!(
4247 editor.selections.display_ranges(cx),
4248 vec![
4249 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4250 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4251 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4252 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4253 ]
4254 );
4255 });
4256}
4257
4258#[gpui::test]
4259fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4260 init_test(cx, |_| {});
4261
4262 let editor = cx.add_window(|window, cx| {
4263 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4264 build_editor(buffer, window, cx)
4265 });
4266 _ = editor.update(cx, |editor, window, cx| {
4267 let snapshot = editor.buffer.read(cx).snapshot(cx);
4268 editor.insert_blocks(
4269 [BlockProperties {
4270 style: BlockStyle::Fixed,
4271 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4272 height: Some(1),
4273 render: Arc::new(|_| div().into_any()),
4274 priority: 0,
4275 }],
4276 Some(Autoscroll::fit()),
4277 cx,
4278 );
4279 editor.change_selections(None, window, cx, |s| {
4280 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4281 });
4282 editor.move_line_down(&MoveLineDown, window, cx);
4283 });
4284}
4285
4286#[gpui::test]
4287async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4288 init_test(cx, |_| {});
4289
4290 let mut cx = EditorTestContext::new(cx).await;
4291 cx.set_state(
4292 &"
4293 ˇzero
4294 one
4295 two
4296 three
4297 four
4298 five
4299 "
4300 .unindent(),
4301 );
4302
4303 // Create a four-line block that replaces three lines of text.
4304 cx.update_editor(|editor, window, cx| {
4305 let snapshot = editor.snapshot(window, cx);
4306 let snapshot = &snapshot.buffer_snapshot;
4307 let placement = BlockPlacement::Replace(
4308 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4309 );
4310 editor.insert_blocks(
4311 [BlockProperties {
4312 placement,
4313 height: Some(4),
4314 style: BlockStyle::Sticky,
4315 render: Arc::new(|_| gpui::div().into_any_element()),
4316 priority: 0,
4317 }],
4318 None,
4319 cx,
4320 );
4321 });
4322
4323 // Move down so that the cursor touches the block.
4324 cx.update_editor(|editor, window, cx| {
4325 editor.move_down(&Default::default(), window, cx);
4326 });
4327 cx.assert_editor_state(
4328 &"
4329 zero
4330 «one
4331 two
4332 threeˇ»
4333 four
4334 five
4335 "
4336 .unindent(),
4337 );
4338
4339 // Move down past the block.
4340 cx.update_editor(|editor, window, cx| {
4341 editor.move_down(&Default::default(), window, cx);
4342 });
4343 cx.assert_editor_state(
4344 &"
4345 zero
4346 one
4347 two
4348 three
4349 ˇfour
4350 five
4351 "
4352 .unindent(),
4353 );
4354}
4355
4356#[gpui::test]
4357fn test_transpose(cx: &mut TestAppContext) {
4358 init_test(cx, |_| {});
4359
4360 _ = cx.add_window(|window, cx| {
4361 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4362 editor.set_style(EditorStyle::default(), window, cx);
4363 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4364 editor.transpose(&Default::default(), window, cx);
4365 assert_eq!(editor.text(cx), "bac");
4366 assert_eq!(editor.selections.ranges(cx), [2..2]);
4367
4368 editor.transpose(&Default::default(), window, cx);
4369 assert_eq!(editor.text(cx), "bca");
4370 assert_eq!(editor.selections.ranges(cx), [3..3]);
4371
4372 editor.transpose(&Default::default(), window, cx);
4373 assert_eq!(editor.text(cx), "bac");
4374 assert_eq!(editor.selections.ranges(cx), [3..3]);
4375
4376 editor
4377 });
4378
4379 _ = cx.add_window(|window, cx| {
4380 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4381 editor.set_style(EditorStyle::default(), window, cx);
4382 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4383 editor.transpose(&Default::default(), window, cx);
4384 assert_eq!(editor.text(cx), "acb\nde");
4385 assert_eq!(editor.selections.ranges(cx), [3..3]);
4386
4387 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4388 editor.transpose(&Default::default(), window, cx);
4389 assert_eq!(editor.text(cx), "acbd\ne");
4390 assert_eq!(editor.selections.ranges(cx), [5..5]);
4391
4392 editor.transpose(&Default::default(), window, cx);
4393 assert_eq!(editor.text(cx), "acbde\n");
4394 assert_eq!(editor.selections.ranges(cx), [6..6]);
4395
4396 editor.transpose(&Default::default(), window, cx);
4397 assert_eq!(editor.text(cx), "acbd\ne");
4398 assert_eq!(editor.selections.ranges(cx), [6..6]);
4399
4400 editor
4401 });
4402
4403 _ = cx.add_window(|window, cx| {
4404 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4405 editor.set_style(EditorStyle::default(), window, cx);
4406 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4407 editor.transpose(&Default::default(), window, cx);
4408 assert_eq!(editor.text(cx), "bacd\ne");
4409 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4410
4411 editor.transpose(&Default::default(), window, cx);
4412 assert_eq!(editor.text(cx), "bcade\n");
4413 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4414
4415 editor.transpose(&Default::default(), window, cx);
4416 assert_eq!(editor.text(cx), "bcda\ne");
4417 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4418
4419 editor.transpose(&Default::default(), window, cx);
4420 assert_eq!(editor.text(cx), "bcade\n");
4421 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4422
4423 editor.transpose(&Default::default(), window, cx);
4424 assert_eq!(editor.text(cx), "bcaed\n");
4425 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4426
4427 editor
4428 });
4429
4430 _ = cx.add_window(|window, cx| {
4431 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4432 editor.set_style(EditorStyle::default(), window, cx);
4433 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4434 editor.transpose(&Default::default(), window, cx);
4435 assert_eq!(editor.text(cx), "🏀🍐✋");
4436 assert_eq!(editor.selections.ranges(cx), [8..8]);
4437
4438 editor.transpose(&Default::default(), window, cx);
4439 assert_eq!(editor.text(cx), "🏀✋🍐");
4440 assert_eq!(editor.selections.ranges(cx), [11..11]);
4441
4442 editor.transpose(&Default::default(), window, cx);
4443 assert_eq!(editor.text(cx), "🏀🍐✋");
4444 assert_eq!(editor.selections.ranges(cx), [11..11]);
4445
4446 editor
4447 });
4448}
4449
4450#[gpui::test]
4451async fn test_rewrap(cx: &mut TestAppContext) {
4452 init_test(cx, |settings| {
4453 settings.languages.extend([
4454 (
4455 "Markdown".into(),
4456 LanguageSettingsContent {
4457 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4458 ..Default::default()
4459 },
4460 ),
4461 (
4462 "Plain Text".into(),
4463 LanguageSettingsContent {
4464 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4465 ..Default::default()
4466 },
4467 ),
4468 ])
4469 });
4470
4471 let mut cx = EditorTestContext::new(cx).await;
4472
4473 let language_with_c_comments = Arc::new(Language::new(
4474 LanguageConfig {
4475 line_comments: vec!["// ".into()],
4476 ..LanguageConfig::default()
4477 },
4478 None,
4479 ));
4480 let language_with_pound_comments = Arc::new(Language::new(
4481 LanguageConfig {
4482 line_comments: vec!["# ".into()],
4483 ..LanguageConfig::default()
4484 },
4485 None,
4486 ));
4487 let markdown_language = Arc::new(Language::new(
4488 LanguageConfig {
4489 name: "Markdown".into(),
4490 ..LanguageConfig::default()
4491 },
4492 None,
4493 ));
4494 let language_with_doc_comments = Arc::new(Language::new(
4495 LanguageConfig {
4496 line_comments: vec!["// ".into(), "/// ".into()],
4497 ..LanguageConfig::default()
4498 },
4499 Some(tree_sitter_rust::LANGUAGE.into()),
4500 ));
4501
4502 let plaintext_language = Arc::new(Language::new(
4503 LanguageConfig {
4504 name: "Plain Text".into(),
4505 ..LanguageConfig::default()
4506 },
4507 None,
4508 ));
4509
4510 assert_rewrap(
4511 indoc! {"
4512 // ˇ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.
4513 "},
4514 indoc! {"
4515 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4516 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4517 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4518 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4519 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4520 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4521 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4522 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4523 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4524 // porttitor id. Aliquam id accumsan eros.
4525 "},
4526 language_with_c_comments.clone(),
4527 &mut cx,
4528 );
4529
4530 // Test that rewrapping works inside of a selection
4531 assert_rewrap(
4532 indoc! {"
4533 «// 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.ˇ»
4534 "},
4535 indoc! {"
4536 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4537 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4538 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4539 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4540 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4541 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4542 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4543 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4544 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4545 // porttitor id. Aliquam id accumsan eros.ˇ»
4546 "},
4547 language_with_c_comments.clone(),
4548 &mut cx,
4549 );
4550
4551 // Test that cursors that expand to the same region are collapsed.
4552 assert_rewrap(
4553 indoc! {"
4554 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4555 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4556 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4557 // ˇ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.
4558 "},
4559 indoc! {"
4560 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4561 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4562 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4563 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4564 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4565 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4566 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4567 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4568 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4569 // porttitor id. Aliquam id accumsan eros.
4570 "},
4571 language_with_c_comments.clone(),
4572 &mut cx,
4573 );
4574
4575 // Test that non-contiguous selections are treated separately.
4576 assert_rewrap(
4577 indoc! {"
4578 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4579 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4580 //
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.
4588 //
4589 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4590 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4591 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4592 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4593 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4594 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4595 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4596 "},
4597 language_with_c_comments.clone(),
4598 &mut cx,
4599 );
4600
4601 // Test that different comment prefixes are supported.
4602 assert_rewrap(
4603 indoc! {"
4604 # ˇ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.
4605 "},
4606 indoc! {"
4607 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4608 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4609 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4610 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4611 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4612 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4613 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4614 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4615 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4616 # accumsan eros.
4617 "},
4618 language_with_pound_comments.clone(),
4619 &mut cx,
4620 );
4621
4622 // Test that rewrapping is ignored outside of comments in most languages.
4623 assert_rewrap(
4624 indoc! {"
4625 /// Adds two numbers.
4626 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4627 fn add(a: u32, b: u32) -> u32 {
4628 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ˇ
4629 }
4630 "},
4631 indoc! {"
4632 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4633 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4634 fn add(a: u32, b: u32) -> u32 {
4635 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ˇ
4636 }
4637 "},
4638 language_with_doc_comments.clone(),
4639 &mut cx,
4640 );
4641
4642 // Test that rewrapping works in Markdown and Plain Text languages.
4643 assert_rewrap(
4644 indoc! {"
4645 # Hello
4646
4647 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.
4648 "},
4649 indoc! {"
4650 # Hello
4651
4652 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4653 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4654 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4655 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4656 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4657 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4658 Integer sit amet scelerisque nisi.
4659 "},
4660 markdown_language,
4661 &mut cx,
4662 );
4663
4664 assert_rewrap(
4665 indoc! {"
4666 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.
4667 "},
4668 indoc! {"
4669 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4670 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4671 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4672 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4673 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4674 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4675 Integer sit amet scelerisque nisi.
4676 "},
4677 plaintext_language,
4678 &mut cx,
4679 );
4680
4681 // Test rewrapping unaligned comments in a selection.
4682 assert_rewrap(
4683 indoc! {"
4684 fn foo() {
4685 if true {
4686 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4687 // Praesent semper egestas tellus id dignissim.ˇ»
4688 do_something();
4689 } else {
4690 //
4691 }
4692 }
4693 "},
4694 indoc! {"
4695 fn foo() {
4696 if true {
4697 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4698 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4699 // egestas tellus id dignissim.ˇ»
4700 do_something();
4701 } else {
4702 //
4703 }
4704 }
4705 "},
4706 language_with_doc_comments.clone(),
4707 &mut cx,
4708 );
4709
4710 assert_rewrap(
4711 indoc! {"
4712 fn foo() {
4713 if true {
4714 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4715 // Praesent semper egestas tellus id dignissim.»
4716 do_something();
4717 } else {
4718 //
4719 }
4720
4721 }
4722 "},
4723 indoc! {"
4724 fn foo() {
4725 if true {
4726 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4727 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4728 // egestas tellus id dignissim.»
4729 do_something();
4730 } else {
4731 //
4732 }
4733
4734 }
4735 "},
4736 language_with_doc_comments.clone(),
4737 &mut cx,
4738 );
4739
4740 #[track_caller]
4741 fn assert_rewrap(
4742 unwrapped_text: &str,
4743 wrapped_text: &str,
4744 language: Arc<Language>,
4745 cx: &mut EditorTestContext,
4746 ) {
4747 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4748 cx.set_state(unwrapped_text);
4749 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4750 cx.assert_editor_state(wrapped_text);
4751 }
4752}
4753
4754#[gpui::test]
4755async fn test_hard_wrap(cx: &mut TestAppContext) {
4756 init_test(cx, |_| {});
4757 let mut cx = EditorTestContext::new(cx).await;
4758
4759 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4760 cx.update_editor(|editor, _, cx| {
4761 editor.set_hard_wrap(Some(14), cx);
4762 });
4763
4764 cx.set_state(indoc!(
4765 "
4766 one two three ˇ
4767 "
4768 ));
4769 cx.simulate_input("four");
4770 cx.run_until_parked();
4771
4772 cx.assert_editor_state(indoc!(
4773 "
4774 one two three
4775 fourˇ
4776 "
4777 ));
4778
4779 cx.update_editor(|editor, window, cx| {
4780 editor.newline(&Default::default(), window, cx);
4781 });
4782 cx.run_until_parked();
4783 cx.assert_editor_state(indoc!(
4784 "
4785 one two three
4786 four
4787 ˇ
4788 "
4789 ));
4790
4791 cx.simulate_input("five");
4792 cx.run_until_parked();
4793 cx.assert_editor_state(indoc!(
4794 "
4795 one two three
4796 four
4797 fiveˇ
4798 "
4799 ));
4800
4801 cx.update_editor(|editor, window, cx| {
4802 editor.newline(&Default::default(), window, cx);
4803 });
4804 cx.run_until_parked();
4805 cx.simulate_input("# ");
4806 cx.run_until_parked();
4807 cx.assert_editor_state(indoc!(
4808 "
4809 one two three
4810 four
4811 five
4812 # ˇ
4813 "
4814 ));
4815
4816 cx.update_editor(|editor, window, cx| {
4817 editor.newline(&Default::default(), window, cx);
4818 });
4819 cx.run_until_parked();
4820 cx.assert_editor_state(indoc!(
4821 "
4822 one two three
4823 four
4824 five
4825 #\x20
4826 #ˇ
4827 "
4828 ));
4829
4830 cx.simulate_input(" 6");
4831 cx.run_until_parked();
4832 cx.assert_editor_state(indoc!(
4833 "
4834 one two three
4835 four
4836 five
4837 #
4838 # 6ˇ
4839 "
4840 ));
4841}
4842
4843#[gpui::test]
4844async fn test_clipboard(cx: &mut TestAppContext) {
4845 init_test(cx, |_| {});
4846
4847 let mut cx = EditorTestContext::new(cx).await;
4848
4849 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4850 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4851 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4852
4853 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4854 cx.set_state("two ˇfour ˇsix ˇ");
4855 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4856 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4857
4858 // Paste again but with only two cursors. Since the number of cursors doesn't
4859 // match the number of slices in the clipboard, the entire clipboard text
4860 // is pasted at each cursor.
4861 cx.set_state("ˇtwo one✅ four three six five ˇ");
4862 cx.update_editor(|e, window, cx| {
4863 e.handle_input("( ", window, cx);
4864 e.paste(&Paste, window, cx);
4865 e.handle_input(") ", window, cx);
4866 });
4867 cx.assert_editor_state(
4868 &([
4869 "( one✅ ",
4870 "three ",
4871 "five ) ˇtwo one✅ four three six five ( one✅ ",
4872 "three ",
4873 "five ) ˇ",
4874 ]
4875 .join("\n")),
4876 );
4877
4878 // Cut with three selections, one of which is full-line.
4879 cx.set_state(indoc! {"
4880 1«2ˇ»3
4881 4ˇ567
4882 «8ˇ»9"});
4883 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4884 cx.assert_editor_state(indoc! {"
4885 1ˇ3
4886 ˇ9"});
4887
4888 // Paste with three selections, noticing how the copied selection that was full-line
4889 // gets inserted before the second cursor.
4890 cx.set_state(indoc! {"
4891 1ˇ3
4892 9ˇ
4893 «oˇ»ne"});
4894 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4895 cx.assert_editor_state(indoc! {"
4896 12ˇ3
4897 4567
4898 9ˇ
4899 8ˇne"});
4900
4901 // Copy with a single cursor only, which writes the whole line into the clipboard.
4902 cx.set_state(indoc! {"
4903 The quick brown
4904 fox juˇmps over
4905 the lazy dog"});
4906 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4907 assert_eq!(
4908 cx.read_from_clipboard()
4909 .and_then(|item| item.text().as_deref().map(str::to_string)),
4910 Some("fox jumps over\n".to_string())
4911 );
4912
4913 // Paste with three selections, noticing how the copied full-line selection is inserted
4914 // before the empty selections but replaces the selection that is non-empty.
4915 cx.set_state(indoc! {"
4916 Tˇhe quick brown
4917 «foˇ»x jumps over
4918 tˇhe lazy dog"});
4919 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4920 cx.assert_editor_state(indoc! {"
4921 fox jumps over
4922 Tˇhe quick brown
4923 fox jumps over
4924 ˇx jumps over
4925 fox jumps over
4926 tˇhe lazy dog"});
4927}
4928
4929#[gpui::test]
4930async fn test_copy_trim(cx: &mut TestAppContext) {
4931 init_test(cx, |_| {});
4932
4933 let mut cx = EditorTestContext::new(cx).await;
4934 cx.set_state(
4935 r#" «for selection in selections.iter() {
4936 let mut start = selection.start;
4937 let mut end = selection.end;
4938 let is_entire_line = selection.is_empty();
4939 if is_entire_line {
4940 start = Point::new(start.row, 0);ˇ»
4941 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4942 }
4943 "#,
4944 );
4945 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4946 assert_eq!(
4947 cx.read_from_clipboard()
4948 .and_then(|item| item.text().as_deref().map(str::to_string)),
4949 Some(
4950 "for selection in selections.iter() {
4951 let mut start = selection.start;
4952 let mut end = selection.end;
4953 let is_entire_line = selection.is_empty();
4954 if is_entire_line {
4955 start = Point::new(start.row, 0);"
4956 .to_string()
4957 ),
4958 "Regular copying preserves all indentation selected",
4959 );
4960 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4961 assert_eq!(
4962 cx.read_from_clipboard()
4963 .and_then(|item| item.text().as_deref().map(str::to_string)),
4964 Some(
4965 "for selection in selections.iter() {
4966let mut start = selection.start;
4967let mut end = selection.end;
4968let is_entire_line = selection.is_empty();
4969if is_entire_line {
4970 start = Point::new(start.row, 0);"
4971 .to_string()
4972 ),
4973 "Copying with stripping should strip all leading whitespaces"
4974 );
4975
4976 cx.set_state(
4977 r#" « for selection in selections.iter() {
4978 let mut start = selection.start;
4979 let mut end = selection.end;
4980 let is_entire_line = selection.is_empty();
4981 if is_entire_line {
4982 start = Point::new(start.row, 0);ˇ»
4983 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4984 }
4985 "#,
4986 );
4987 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4988 assert_eq!(
4989 cx.read_from_clipboard()
4990 .and_then(|item| item.text().as_deref().map(str::to_string)),
4991 Some(
4992 " for selection in selections.iter() {
4993 let mut start = selection.start;
4994 let mut end = selection.end;
4995 let is_entire_line = selection.is_empty();
4996 if is_entire_line {
4997 start = Point::new(start.row, 0);"
4998 .to_string()
4999 ),
5000 "Regular copying preserves all indentation selected",
5001 );
5002 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5003 assert_eq!(
5004 cx.read_from_clipboard()
5005 .and_then(|item| item.text().as_deref().map(str::to_string)),
5006 Some(
5007 "for selection in selections.iter() {
5008let mut start = selection.start;
5009let mut end = selection.end;
5010let is_entire_line = selection.is_empty();
5011if is_entire_line {
5012 start = Point::new(start.row, 0);"
5013 .to_string()
5014 ),
5015 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5016 );
5017
5018 cx.set_state(
5019 r#" «ˇ for selection in selections.iter() {
5020 let mut start = selection.start;
5021 let mut end = selection.end;
5022 let is_entire_line = selection.is_empty();
5023 if is_entire_line {
5024 start = Point::new(start.row, 0);»
5025 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5026 }
5027 "#,
5028 );
5029 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5030 assert_eq!(
5031 cx.read_from_clipboard()
5032 .and_then(|item| item.text().as_deref().map(str::to_string)),
5033 Some(
5034 " for selection in selections.iter() {
5035 let mut start = selection.start;
5036 let mut end = selection.end;
5037 let is_entire_line = selection.is_empty();
5038 if is_entire_line {
5039 start = Point::new(start.row, 0);"
5040 .to_string()
5041 ),
5042 "Regular copying for reverse selection works the same",
5043 );
5044 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5045 assert_eq!(
5046 cx.read_from_clipboard()
5047 .and_then(|item| item.text().as_deref().map(str::to_string)),
5048 Some(
5049 "for selection in selections.iter() {
5050let mut start = selection.start;
5051let mut end = selection.end;
5052let is_entire_line = selection.is_empty();
5053if is_entire_line {
5054 start = Point::new(start.row, 0);"
5055 .to_string()
5056 ),
5057 "Copying with stripping for reverse selection works the same"
5058 );
5059
5060 cx.set_state(
5061 r#" for selection «in selections.iter() {
5062 let mut start = selection.start;
5063 let mut end = selection.end;
5064 let is_entire_line = selection.is_empty();
5065 if is_entire_line {
5066 start = Point::new(start.row, 0);ˇ»
5067 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5068 }
5069 "#,
5070 );
5071 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5072 assert_eq!(
5073 cx.read_from_clipboard()
5074 .and_then(|item| item.text().as_deref().map(str::to_string)),
5075 Some(
5076 "in selections.iter() {
5077 let mut start = selection.start;
5078 let mut end = selection.end;
5079 let is_entire_line = selection.is_empty();
5080 if is_entire_line {
5081 start = Point::new(start.row, 0);"
5082 .to_string()
5083 ),
5084 "When selecting past the indent, the copying works as usual",
5085 );
5086 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5087 assert_eq!(
5088 cx.read_from_clipboard()
5089 .and_then(|item| item.text().as_deref().map(str::to_string)),
5090 Some(
5091 "in selections.iter() {
5092 let mut start = selection.start;
5093 let mut end = selection.end;
5094 let is_entire_line = selection.is_empty();
5095 if is_entire_line {
5096 start = Point::new(start.row, 0);"
5097 .to_string()
5098 ),
5099 "When selecting past the indent, nothing is trimmed"
5100 );
5101}
5102
5103#[gpui::test]
5104async fn test_paste_multiline(cx: &mut TestAppContext) {
5105 init_test(cx, |_| {});
5106
5107 let mut cx = EditorTestContext::new(cx).await;
5108 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5109
5110 // Cut an indented block, without the leading whitespace.
5111 cx.set_state(indoc! {"
5112 const a: B = (
5113 c(),
5114 «d(
5115 e,
5116 f
5117 )ˇ»
5118 );
5119 "});
5120 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5121 cx.assert_editor_state(indoc! {"
5122 const a: B = (
5123 c(),
5124 ˇ
5125 );
5126 "});
5127
5128 // Paste it at the same position.
5129 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5130 cx.assert_editor_state(indoc! {"
5131 const a: B = (
5132 c(),
5133 d(
5134 e,
5135 f
5136 )ˇ
5137 );
5138 "});
5139
5140 // Paste it at a line with a lower indent level.
5141 cx.set_state(indoc! {"
5142 ˇ
5143 const a: B = (
5144 c(),
5145 );
5146 "});
5147 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5148 cx.assert_editor_state(indoc! {"
5149 d(
5150 e,
5151 f
5152 )ˇ
5153 const a: B = (
5154 c(),
5155 );
5156 "});
5157
5158 // Cut an indented block, with the leading whitespace.
5159 cx.set_state(indoc! {"
5160 const a: B = (
5161 c(),
5162 « d(
5163 e,
5164 f
5165 )
5166 ˇ»);
5167 "});
5168 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5169 cx.assert_editor_state(indoc! {"
5170 const a: B = (
5171 c(),
5172 ˇ);
5173 "});
5174
5175 // Paste it at the same position.
5176 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5177 cx.assert_editor_state(indoc! {"
5178 const a: B = (
5179 c(),
5180 d(
5181 e,
5182 f
5183 )
5184 ˇ);
5185 "});
5186
5187 // Paste it at a line with a higher indent level.
5188 cx.set_state(indoc! {"
5189 const a: B = (
5190 c(),
5191 d(
5192 e,
5193 fˇ
5194 )
5195 );
5196 "});
5197 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5198 cx.assert_editor_state(indoc! {"
5199 const a: B = (
5200 c(),
5201 d(
5202 e,
5203 f d(
5204 e,
5205 f
5206 )
5207 ˇ
5208 )
5209 );
5210 "});
5211
5212 // Copy an indented block, starting mid-line
5213 cx.set_state(indoc! {"
5214 const a: B = (
5215 c(),
5216 somethin«g(
5217 e,
5218 f
5219 )ˇ»
5220 );
5221 "});
5222 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5223
5224 // Paste it on a line with a lower indent level
5225 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5226 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5227 cx.assert_editor_state(indoc! {"
5228 const a: B = (
5229 c(),
5230 something(
5231 e,
5232 f
5233 )
5234 );
5235 g(
5236 e,
5237 f
5238 )ˇ"});
5239}
5240
5241#[gpui::test]
5242async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5243 init_test(cx, |_| {});
5244
5245 cx.write_to_clipboard(ClipboardItem::new_string(
5246 " d(\n e\n );\n".into(),
5247 ));
5248
5249 let mut cx = EditorTestContext::new(cx).await;
5250 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5251
5252 cx.set_state(indoc! {"
5253 fn a() {
5254 b();
5255 if c() {
5256 ˇ
5257 }
5258 }
5259 "});
5260
5261 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5262 cx.assert_editor_state(indoc! {"
5263 fn a() {
5264 b();
5265 if c() {
5266 d(
5267 e
5268 );
5269 ˇ
5270 }
5271 }
5272 "});
5273
5274 cx.set_state(indoc! {"
5275 fn a() {
5276 b();
5277 ˇ
5278 }
5279 "});
5280
5281 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5282 cx.assert_editor_state(indoc! {"
5283 fn a() {
5284 b();
5285 d(
5286 e
5287 );
5288 ˇ
5289 }
5290 "});
5291}
5292
5293#[gpui::test]
5294fn test_select_all(cx: &mut TestAppContext) {
5295 init_test(cx, |_| {});
5296
5297 let editor = cx.add_window(|window, cx| {
5298 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5299 build_editor(buffer, window, cx)
5300 });
5301 _ = editor.update(cx, |editor, window, cx| {
5302 editor.select_all(&SelectAll, window, cx);
5303 assert_eq!(
5304 editor.selections.display_ranges(cx),
5305 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5306 );
5307 });
5308}
5309
5310#[gpui::test]
5311fn test_select_line(cx: &mut TestAppContext) {
5312 init_test(cx, |_| {});
5313
5314 let editor = cx.add_window(|window, cx| {
5315 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5316 build_editor(buffer, window, cx)
5317 });
5318 _ = editor.update(cx, |editor, window, cx| {
5319 editor.change_selections(None, window, cx, |s| {
5320 s.select_display_ranges([
5321 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5322 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5323 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5324 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5325 ])
5326 });
5327 editor.select_line(&SelectLine, window, cx);
5328 assert_eq!(
5329 editor.selections.display_ranges(cx),
5330 vec![
5331 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5332 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5333 ]
5334 );
5335 });
5336
5337 _ = editor.update(cx, |editor, window, cx| {
5338 editor.select_line(&SelectLine, window, cx);
5339 assert_eq!(
5340 editor.selections.display_ranges(cx),
5341 vec![
5342 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5343 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5344 ]
5345 );
5346 });
5347
5348 _ = editor.update(cx, |editor, window, cx| {
5349 editor.select_line(&SelectLine, window, cx);
5350 assert_eq!(
5351 editor.selections.display_ranges(cx),
5352 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5353 );
5354 });
5355}
5356
5357#[gpui::test]
5358async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5359 init_test(cx, |_| {});
5360 let mut cx = EditorTestContext::new(cx).await;
5361
5362 #[track_caller]
5363 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5364 cx.set_state(initial_state);
5365 cx.update_editor(|e, window, cx| {
5366 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5367 });
5368 cx.assert_editor_state(expected_state);
5369 }
5370
5371 // Selection starts and ends at the middle of lines, left-to-right
5372 test(
5373 &mut cx,
5374 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5375 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5376 );
5377 // Same thing, right-to-left
5378 test(
5379 &mut cx,
5380 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5381 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5382 );
5383
5384 // Whole buffer, left-to-right, last line *doesn't* end with newline
5385 test(
5386 &mut cx,
5387 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5388 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5389 );
5390 // Same thing, right-to-left
5391 test(
5392 &mut cx,
5393 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5394 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5395 );
5396
5397 // Whole buffer, left-to-right, last line ends with newline
5398 test(
5399 &mut cx,
5400 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5401 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5402 );
5403 // Same thing, right-to-left
5404 test(
5405 &mut cx,
5406 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5407 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5408 );
5409
5410 // Starts at the end of a line, ends at the start of another
5411 test(
5412 &mut cx,
5413 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5414 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5415 );
5416}
5417
5418#[gpui::test]
5419async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5420 init_test(cx, |_| {});
5421
5422 let editor = cx.add_window(|window, cx| {
5423 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5424 build_editor(buffer, window, cx)
5425 });
5426
5427 // setup
5428 _ = editor.update(cx, |editor, window, cx| {
5429 editor.fold_creases(
5430 vec![
5431 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5432 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5433 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5434 ],
5435 true,
5436 window,
5437 cx,
5438 );
5439 assert_eq!(
5440 editor.display_text(cx),
5441 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5442 );
5443 });
5444
5445 _ = editor.update(cx, |editor, window, cx| {
5446 editor.change_selections(None, window, cx, |s| {
5447 s.select_display_ranges([
5448 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5449 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5450 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5451 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5452 ])
5453 });
5454 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5455 assert_eq!(
5456 editor.display_text(cx),
5457 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5458 );
5459 });
5460 EditorTestContext::for_editor(editor, cx)
5461 .await
5462 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5463
5464 _ = editor.update(cx, |editor, window, cx| {
5465 editor.change_selections(None, window, cx, |s| {
5466 s.select_display_ranges([
5467 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5468 ])
5469 });
5470 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5471 assert_eq!(
5472 editor.display_text(cx),
5473 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5474 );
5475 assert_eq!(
5476 editor.selections.display_ranges(cx),
5477 [
5478 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5479 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5480 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5481 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5482 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5483 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5484 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5485 ]
5486 );
5487 });
5488 EditorTestContext::for_editor(editor, cx)
5489 .await
5490 .assert_editor_state(
5491 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5492 );
5493}
5494
5495#[gpui::test]
5496async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5497 init_test(cx, |_| {});
5498
5499 let mut cx = EditorTestContext::new(cx).await;
5500
5501 cx.set_state(indoc!(
5502 r#"abc
5503 defˇghi
5504
5505 jk
5506 nlmo
5507 "#
5508 ));
5509
5510 cx.update_editor(|editor, window, cx| {
5511 editor.add_selection_above(&Default::default(), window, cx);
5512 });
5513
5514 cx.assert_editor_state(indoc!(
5515 r#"abcˇ
5516 defˇghi
5517
5518 jk
5519 nlmo
5520 "#
5521 ));
5522
5523 cx.update_editor(|editor, window, cx| {
5524 editor.add_selection_above(&Default::default(), window, cx);
5525 });
5526
5527 cx.assert_editor_state(indoc!(
5528 r#"abcˇ
5529 defˇghi
5530
5531 jk
5532 nlmo
5533 "#
5534 ));
5535
5536 cx.update_editor(|editor, window, cx| {
5537 editor.add_selection_below(&Default::default(), window, cx);
5538 });
5539
5540 cx.assert_editor_state(indoc!(
5541 r#"abc
5542 defˇghi
5543
5544 jk
5545 nlmo
5546 "#
5547 ));
5548
5549 cx.update_editor(|editor, window, cx| {
5550 editor.undo_selection(&Default::default(), window, cx);
5551 });
5552
5553 cx.assert_editor_state(indoc!(
5554 r#"abcˇ
5555 defˇghi
5556
5557 jk
5558 nlmo
5559 "#
5560 ));
5561
5562 cx.update_editor(|editor, window, cx| {
5563 editor.redo_selection(&Default::default(), window, cx);
5564 });
5565
5566 cx.assert_editor_state(indoc!(
5567 r#"abc
5568 defˇghi
5569
5570 jk
5571 nlmo
5572 "#
5573 ));
5574
5575 cx.update_editor(|editor, window, cx| {
5576 editor.add_selection_below(&Default::default(), window, cx);
5577 });
5578
5579 cx.assert_editor_state(indoc!(
5580 r#"abc
5581 defˇghi
5582
5583 jk
5584 nlmˇo
5585 "#
5586 ));
5587
5588 cx.update_editor(|editor, window, cx| {
5589 editor.add_selection_below(&Default::default(), window, cx);
5590 });
5591
5592 cx.assert_editor_state(indoc!(
5593 r#"abc
5594 defˇghi
5595
5596 jk
5597 nlmˇo
5598 "#
5599 ));
5600
5601 // change selections
5602 cx.set_state(indoc!(
5603 r#"abc
5604 def«ˇg»hi
5605
5606 jk
5607 nlmo
5608 "#
5609 ));
5610
5611 cx.update_editor(|editor, window, cx| {
5612 editor.add_selection_below(&Default::default(), window, cx);
5613 });
5614
5615 cx.assert_editor_state(indoc!(
5616 r#"abc
5617 def«ˇg»hi
5618
5619 jk
5620 nlm«ˇo»
5621 "#
5622 ));
5623
5624 cx.update_editor(|editor, window, cx| {
5625 editor.add_selection_below(&Default::default(), window, cx);
5626 });
5627
5628 cx.assert_editor_state(indoc!(
5629 r#"abc
5630 def«ˇg»hi
5631
5632 jk
5633 nlm«ˇo»
5634 "#
5635 ));
5636
5637 cx.update_editor(|editor, window, cx| {
5638 editor.add_selection_above(&Default::default(), window, cx);
5639 });
5640
5641 cx.assert_editor_state(indoc!(
5642 r#"abc
5643 def«ˇg»hi
5644
5645 jk
5646 nlmo
5647 "#
5648 ));
5649
5650 cx.update_editor(|editor, window, cx| {
5651 editor.add_selection_above(&Default::default(), window, cx);
5652 });
5653
5654 cx.assert_editor_state(indoc!(
5655 r#"abc
5656 def«ˇg»hi
5657
5658 jk
5659 nlmo
5660 "#
5661 ));
5662
5663 // Change selections again
5664 cx.set_state(indoc!(
5665 r#"a«bc
5666 defgˇ»hi
5667
5668 jk
5669 nlmo
5670 "#
5671 ));
5672
5673 cx.update_editor(|editor, window, cx| {
5674 editor.add_selection_below(&Default::default(), window, cx);
5675 });
5676
5677 cx.assert_editor_state(indoc!(
5678 r#"a«bcˇ»
5679 d«efgˇ»hi
5680
5681 j«kˇ»
5682 nlmo
5683 "#
5684 ));
5685
5686 cx.update_editor(|editor, window, cx| {
5687 editor.add_selection_below(&Default::default(), window, cx);
5688 });
5689 cx.assert_editor_state(indoc!(
5690 r#"a«bcˇ»
5691 d«efgˇ»hi
5692
5693 j«kˇ»
5694 n«lmoˇ»
5695 "#
5696 ));
5697 cx.update_editor(|editor, window, cx| {
5698 editor.add_selection_above(&Default::default(), window, cx);
5699 });
5700
5701 cx.assert_editor_state(indoc!(
5702 r#"a«bcˇ»
5703 d«efgˇ»hi
5704
5705 j«kˇ»
5706 nlmo
5707 "#
5708 ));
5709
5710 // Change selections again
5711 cx.set_state(indoc!(
5712 r#"abc
5713 d«ˇefghi
5714
5715 jk
5716 nlm»o
5717 "#
5718 ));
5719
5720 cx.update_editor(|editor, window, cx| {
5721 editor.add_selection_above(&Default::default(), window, cx);
5722 });
5723
5724 cx.assert_editor_state(indoc!(
5725 r#"a«ˇbc»
5726 d«ˇef»ghi
5727
5728 j«ˇk»
5729 n«ˇlm»o
5730 "#
5731 ));
5732
5733 cx.update_editor(|editor, window, cx| {
5734 editor.add_selection_below(&Default::default(), window, cx);
5735 });
5736
5737 cx.assert_editor_state(indoc!(
5738 r#"abc
5739 d«ˇef»ghi
5740
5741 j«ˇk»
5742 n«ˇlm»o
5743 "#
5744 ));
5745}
5746
5747#[gpui::test]
5748async fn test_select_next(cx: &mut TestAppContext) {
5749 init_test(cx, |_| {});
5750
5751 let mut cx = EditorTestContext::new(cx).await;
5752 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5753
5754 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5755 .unwrap();
5756 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5757
5758 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5759 .unwrap();
5760 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5761
5762 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5763 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5764
5765 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5766 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5767
5768 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5769 .unwrap();
5770 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5771
5772 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5773 .unwrap();
5774 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5775}
5776
5777#[gpui::test]
5778async fn test_select_all_matches(cx: &mut TestAppContext) {
5779 init_test(cx, |_| {});
5780
5781 let mut cx = EditorTestContext::new(cx).await;
5782
5783 // Test caret-only selections
5784 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5785 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5786 .unwrap();
5787 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5788
5789 // Test left-to-right selections
5790 cx.set_state("abc\n«abcˇ»\nabc");
5791 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5792 .unwrap();
5793 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5794
5795 // Test right-to-left selections
5796 cx.set_state("abc\n«ˇabc»\nabc");
5797 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5798 .unwrap();
5799 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5800
5801 // Test selecting whitespace with caret selection
5802 cx.set_state("abc\nˇ abc\nabc");
5803 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5804 .unwrap();
5805 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5806
5807 // Test selecting whitespace with left-to-right selection
5808 cx.set_state("abc\n«ˇ »abc\nabc");
5809 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5810 .unwrap();
5811 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5812
5813 // Test no matches with right-to-left selection
5814 cx.set_state("abc\n« ˇ»abc\nabc");
5815 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5816 .unwrap();
5817 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5818}
5819
5820#[gpui::test]
5821async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5822 init_test(cx, |_| {});
5823
5824 let mut cx = EditorTestContext::new(cx).await;
5825 cx.set_state(
5826 r#"let foo = 2;
5827lˇet foo = 2;
5828let fooˇ = 2;
5829let foo = 2;
5830let foo = ˇ2;"#,
5831 );
5832
5833 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5834 .unwrap();
5835 cx.assert_editor_state(
5836 r#"let foo = 2;
5837«letˇ» foo = 2;
5838let «fooˇ» = 2;
5839let foo = 2;
5840let foo = «2ˇ»;"#,
5841 );
5842
5843 // noop for multiple selections with different contents
5844 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5845 .unwrap();
5846 cx.assert_editor_state(
5847 r#"let foo = 2;
5848«letˇ» foo = 2;
5849let «fooˇ» = 2;
5850let foo = 2;
5851let foo = «2ˇ»;"#,
5852 );
5853}
5854
5855#[gpui::test]
5856async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5857 init_test(cx, |_| {});
5858
5859 let mut cx =
5860 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5861
5862 cx.assert_editor_state(indoc! {"
5863 ˇbbb
5864 ccc
5865
5866 bbb
5867 ccc
5868 "});
5869 cx.dispatch_action(SelectPrevious::default());
5870 cx.assert_editor_state(indoc! {"
5871 «bbbˇ»
5872 ccc
5873
5874 bbb
5875 ccc
5876 "});
5877 cx.dispatch_action(SelectPrevious::default());
5878 cx.assert_editor_state(indoc! {"
5879 «bbbˇ»
5880 ccc
5881
5882 «bbbˇ»
5883 ccc
5884 "});
5885}
5886
5887#[gpui::test]
5888async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5889 init_test(cx, |_| {});
5890
5891 let mut cx = EditorTestContext::new(cx).await;
5892 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5893
5894 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5895 .unwrap();
5896 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5897
5898 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5899 .unwrap();
5900 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5901
5902 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5903 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5904
5905 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5906 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5907
5908 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5909 .unwrap();
5910 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5911
5912 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5913 .unwrap();
5914 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5915
5916 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5917 .unwrap();
5918 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5919}
5920
5921#[gpui::test]
5922async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5923 init_test(cx, |_| {});
5924
5925 let mut cx = EditorTestContext::new(cx).await;
5926 cx.set_state("aˇ");
5927
5928 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5929 .unwrap();
5930 cx.assert_editor_state("«aˇ»");
5931 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5932 .unwrap();
5933 cx.assert_editor_state("«aˇ»");
5934}
5935
5936#[gpui::test]
5937async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5938 init_test(cx, |_| {});
5939
5940 let mut cx = EditorTestContext::new(cx).await;
5941 cx.set_state(
5942 r#"let foo = 2;
5943lˇet foo = 2;
5944let fooˇ = 2;
5945let foo = 2;
5946let foo = ˇ2;"#,
5947 );
5948
5949 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5950 .unwrap();
5951 cx.assert_editor_state(
5952 r#"let foo = 2;
5953«letˇ» foo = 2;
5954let «fooˇ» = 2;
5955let foo = 2;
5956let foo = «2ˇ»;"#,
5957 );
5958
5959 // noop for multiple selections with different contents
5960 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5961 .unwrap();
5962 cx.assert_editor_state(
5963 r#"let foo = 2;
5964«letˇ» foo = 2;
5965let «fooˇ» = 2;
5966let foo = 2;
5967let foo = «2ˇ»;"#,
5968 );
5969}
5970
5971#[gpui::test]
5972async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5973 init_test(cx, |_| {});
5974
5975 let mut cx = EditorTestContext::new(cx).await;
5976 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5977
5978 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5979 .unwrap();
5980 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5981
5982 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5983 .unwrap();
5984 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5985
5986 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5987 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5988
5989 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5990 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5991
5992 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5993 .unwrap();
5994 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5995
5996 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5997 .unwrap();
5998 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5999}
6000
6001#[gpui::test]
6002async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6003 init_test(cx, |_| {});
6004
6005 let language = Arc::new(Language::new(
6006 LanguageConfig::default(),
6007 Some(tree_sitter_rust::LANGUAGE.into()),
6008 ));
6009
6010 let text = r#"
6011 use mod1::mod2::{mod3, mod4};
6012
6013 fn fn_1(param1: bool, param2: &str) {
6014 let var1 = "text";
6015 }
6016 "#
6017 .unindent();
6018
6019 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6020 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6021 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6022
6023 editor
6024 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6025 .await;
6026
6027 editor.update_in(cx, |editor, window, cx| {
6028 editor.change_selections(None, window, cx, |s| {
6029 s.select_display_ranges([
6030 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6031 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6032 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6033 ]);
6034 });
6035 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6036 });
6037 editor.update(cx, |editor, cx| {
6038 assert_text_with_selections(
6039 editor,
6040 indoc! {r#"
6041 use mod1::mod2::{mod3, «mod4ˇ»};
6042
6043 fn fn_1«ˇ(param1: bool, param2: &str)» {
6044 let var1 = "«ˇtext»";
6045 }
6046 "#},
6047 cx,
6048 );
6049 });
6050
6051 editor.update_in(cx, |editor, window, cx| {
6052 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6053 });
6054 editor.update(cx, |editor, cx| {
6055 assert_text_with_selections(
6056 editor,
6057 indoc! {r#"
6058 use mod1::mod2::«{mod3, mod4}ˇ»;
6059
6060 «ˇfn fn_1(param1: bool, param2: &str) {
6061 let var1 = "text";
6062 }»
6063 "#},
6064 cx,
6065 );
6066 });
6067
6068 editor.update_in(cx, |editor, window, cx| {
6069 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6070 });
6071 assert_eq!(
6072 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6073 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6074 );
6075
6076 // Trying to expand the selected syntax node one more time has no effect.
6077 editor.update_in(cx, |editor, window, cx| {
6078 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6079 });
6080 assert_eq!(
6081 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6082 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6083 );
6084
6085 editor.update_in(cx, |editor, window, cx| {
6086 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6087 });
6088 editor.update(cx, |editor, cx| {
6089 assert_text_with_selections(
6090 editor,
6091 indoc! {r#"
6092 use mod1::mod2::«{mod3, mod4}ˇ»;
6093
6094 «ˇfn fn_1(param1: bool, param2: &str) {
6095 let var1 = "text";
6096 }»
6097 "#},
6098 cx,
6099 );
6100 });
6101
6102 editor.update_in(cx, |editor, window, cx| {
6103 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6104 });
6105 editor.update(cx, |editor, cx| {
6106 assert_text_with_selections(
6107 editor,
6108 indoc! {r#"
6109 use mod1::mod2::{mod3, «mod4ˇ»};
6110
6111 fn fn_1«ˇ(param1: bool, param2: &str)» {
6112 let var1 = "«ˇtext»";
6113 }
6114 "#},
6115 cx,
6116 );
6117 });
6118
6119 editor.update_in(cx, |editor, window, cx| {
6120 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6121 });
6122 editor.update(cx, |editor, cx| {
6123 assert_text_with_selections(
6124 editor,
6125 indoc! {r#"
6126 use mod1::mod2::{mod3, mo«ˇ»d4};
6127
6128 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6129 let var1 = "te«ˇ»xt";
6130 }
6131 "#},
6132 cx,
6133 );
6134 });
6135
6136 // Trying to shrink the selected syntax node one more time has no effect.
6137 editor.update_in(cx, |editor, window, cx| {
6138 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6139 });
6140 editor.update_in(cx, |editor, _, cx| {
6141 assert_text_with_selections(
6142 editor,
6143 indoc! {r#"
6144 use mod1::mod2::{mod3, mo«ˇ»d4};
6145
6146 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6147 let var1 = "te«ˇ»xt";
6148 }
6149 "#},
6150 cx,
6151 );
6152 });
6153
6154 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6155 // a fold.
6156 editor.update_in(cx, |editor, window, cx| {
6157 editor.fold_creases(
6158 vec![
6159 Crease::simple(
6160 Point::new(0, 21)..Point::new(0, 24),
6161 FoldPlaceholder::test(),
6162 ),
6163 Crease::simple(
6164 Point::new(3, 20)..Point::new(3, 22),
6165 FoldPlaceholder::test(),
6166 ),
6167 ],
6168 true,
6169 window,
6170 cx,
6171 );
6172 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6173 });
6174 editor.update(cx, |editor, cx| {
6175 assert_text_with_selections(
6176 editor,
6177 indoc! {r#"
6178 use mod1::mod2::«{mod3, mod4}ˇ»;
6179
6180 fn fn_1«ˇ(param1: bool, param2: &str)» {
6181 «ˇlet var1 = "text";»
6182 }
6183 "#},
6184 cx,
6185 );
6186 });
6187}
6188
6189#[gpui::test]
6190async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6191 init_test(cx, |_| {});
6192
6193 let base_text = r#"
6194 impl A {
6195 // this is an uncommitted comment
6196
6197 fn b() {
6198 c();
6199 }
6200
6201 // this is another uncommitted comment
6202
6203 fn d() {
6204 // e
6205 // f
6206 }
6207 }
6208
6209 fn g() {
6210 // h
6211 }
6212 "#
6213 .unindent();
6214
6215 let text = r#"
6216 ˇimpl A {
6217
6218 fn b() {
6219 c();
6220 }
6221
6222 fn d() {
6223 // e
6224 // f
6225 }
6226 }
6227
6228 fn g() {
6229 // h
6230 }
6231 "#
6232 .unindent();
6233
6234 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6235 cx.set_state(&text);
6236 cx.set_head_text(&base_text);
6237 cx.update_editor(|editor, window, cx| {
6238 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6239 });
6240
6241 cx.assert_state_with_diff(
6242 "
6243 ˇimpl A {
6244 - // this is an uncommitted comment
6245
6246 fn b() {
6247 c();
6248 }
6249
6250 - // this is another uncommitted comment
6251 -
6252 fn d() {
6253 // e
6254 // f
6255 }
6256 }
6257
6258 fn g() {
6259 // h
6260 }
6261 "
6262 .unindent(),
6263 );
6264
6265 let expected_display_text = "
6266 impl A {
6267 // this is an uncommitted comment
6268
6269 fn b() {
6270 ⋯
6271 }
6272
6273 // this is another uncommitted comment
6274
6275 fn d() {
6276 ⋯
6277 }
6278 }
6279
6280 fn g() {
6281 ⋯
6282 }
6283 "
6284 .unindent();
6285
6286 cx.update_editor(|editor, window, cx| {
6287 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6288 assert_eq!(editor.display_text(cx), expected_display_text);
6289 });
6290}
6291
6292#[gpui::test]
6293async fn test_autoindent(cx: &mut TestAppContext) {
6294 init_test(cx, |_| {});
6295
6296 let language = Arc::new(
6297 Language::new(
6298 LanguageConfig {
6299 brackets: BracketPairConfig {
6300 pairs: vec![
6301 BracketPair {
6302 start: "{".to_string(),
6303 end: "}".to_string(),
6304 close: false,
6305 surround: false,
6306 newline: true,
6307 },
6308 BracketPair {
6309 start: "(".to_string(),
6310 end: ")".to_string(),
6311 close: false,
6312 surround: false,
6313 newline: true,
6314 },
6315 ],
6316 ..Default::default()
6317 },
6318 ..Default::default()
6319 },
6320 Some(tree_sitter_rust::LANGUAGE.into()),
6321 )
6322 .with_indents_query(
6323 r#"
6324 (_ "(" ")" @end) @indent
6325 (_ "{" "}" @end) @indent
6326 "#,
6327 )
6328 .unwrap(),
6329 );
6330
6331 let text = "fn a() {}";
6332
6333 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6334 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6335 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6336 editor
6337 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6338 .await;
6339
6340 editor.update_in(cx, |editor, window, cx| {
6341 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6342 editor.newline(&Newline, window, cx);
6343 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6344 assert_eq!(
6345 editor.selections.ranges(cx),
6346 &[
6347 Point::new(1, 4)..Point::new(1, 4),
6348 Point::new(3, 4)..Point::new(3, 4),
6349 Point::new(5, 0)..Point::new(5, 0)
6350 ]
6351 );
6352 });
6353}
6354
6355#[gpui::test]
6356async fn test_autoindent_selections(cx: &mut TestAppContext) {
6357 init_test(cx, |_| {});
6358
6359 {
6360 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6361 cx.set_state(indoc! {"
6362 impl A {
6363
6364 fn b() {}
6365
6366 «fn c() {
6367
6368 }ˇ»
6369 }
6370 "});
6371
6372 cx.update_editor(|editor, window, cx| {
6373 editor.autoindent(&Default::default(), window, cx);
6374 });
6375
6376 cx.assert_editor_state(indoc! {"
6377 impl A {
6378
6379 fn b() {}
6380
6381 «fn c() {
6382
6383 }ˇ»
6384 }
6385 "});
6386 }
6387
6388 {
6389 let mut cx = EditorTestContext::new_multibuffer(
6390 cx,
6391 [indoc! { "
6392 impl A {
6393 «
6394 // a
6395 fn b(){}
6396 »
6397 «
6398 }
6399 fn c(){}
6400 »
6401 "}],
6402 );
6403
6404 let buffer = cx.update_editor(|editor, _, cx| {
6405 let buffer = editor.buffer().update(cx, |buffer, _| {
6406 buffer.all_buffers().iter().next().unwrap().clone()
6407 });
6408 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6409 buffer
6410 });
6411
6412 cx.run_until_parked();
6413 cx.update_editor(|editor, window, cx| {
6414 editor.select_all(&Default::default(), window, cx);
6415 editor.autoindent(&Default::default(), window, cx)
6416 });
6417 cx.run_until_parked();
6418
6419 cx.update(|_, cx| {
6420 assert_eq!(
6421 buffer.read(cx).text(),
6422 indoc! { "
6423 impl A {
6424
6425 // a
6426 fn b(){}
6427
6428
6429 }
6430 fn c(){}
6431
6432 " }
6433 )
6434 });
6435 }
6436}
6437
6438#[gpui::test]
6439async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6440 init_test(cx, |_| {});
6441
6442 let mut cx = EditorTestContext::new(cx).await;
6443
6444 let language = Arc::new(Language::new(
6445 LanguageConfig {
6446 brackets: BracketPairConfig {
6447 pairs: vec![
6448 BracketPair {
6449 start: "{".to_string(),
6450 end: "}".to_string(),
6451 close: true,
6452 surround: true,
6453 newline: true,
6454 },
6455 BracketPair {
6456 start: "(".to_string(),
6457 end: ")".to_string(),
6458 close: true,
6459 surround: true,
6460 newline: true,
6461 },
6462 BracketPair {
6463 start: "/*".to_string(),
6464 end: " */".to_string(),
6465 close: true,
6466 surround: true,
6467 newline: true,
6468 },
6469 BracketPair {
6470 start: "[".to_string(),
6471 end: "]".to_string(),
6472 close: false,
6473 surround: false,
6474 newline: true,
6475 },
6476 BracketPair {
6477 start: "\"".to_string(),
6478 end: "\"".to_string(),
6479 close: true,
6480 surround: true,
6481 newline: false,
6482 },
6483 BracketPair {
6484 start: "<".to_string(),
6485 end: ">".to_string(),
6486 close: false,
6487 surround: true,
6488 newline: true,
6489 },
6490 ],
6491 ..Default::default()
6492 },
6493 autoclose_before: "})]".to_string(),
6494 ..Default::default()
6495 },
6496 Some(tree_sitter_rust::LANGUAGE.into()),
6497 ));
6498
6499 cx.language_registry().add(language.clone());
6500 cx.update_buffer(|buffer, cx| {
6501 buffer.set_language(Some(language), cx);
6502 });
6503
6504 cx.set_state(
6505 &r#"
6506 🏀ˇ
6507 εˇ
6508 ❤️ˇ
6509 "#
6510 .unindent(),
6511 );
6512
6513 // autoclose multiple nested brackets at multiple cursors
6514 cx.update_editor(|editor, window, cx| {
6515 editor.handle_input("{", window, cx);
6516 editor.handle_input("{", window, cx);
6517 editor.handle_input("{", window, cx);
6518 });
6519 cx.assert_editor_state(
6520 &"
6521 🏀{{{ˇ}}}
6522 ε{{{ˇ}}}
6523 ❤️{{{ˇ}}}
6524 "
6525 .unindent(),
6526 );
6527
6528 // insert a different closing bracket
6529 cx.update_editor(|editor, window, cx| {
6530 editor.handle_input(")", window, cx);
6531 });
6532 cx.assert_editor_state(
6533 &"
6534 🏀{{{)ˇ}}}
6535 ε{{{)ˇ}}}
6536 ❤️{{{)ˇ}}}
6537 "
6538 .unindent(),
6539 );
6540
6541 // skip over the auto-closed brackets when typing a closing bracket
6542 cx.update_editor(|editor, window, cx| {
6543 editor.move_right(&MoveRight, window, cx);
6544 editor.handle_input("}", window, cx);
6545 editor.handle_input("}", window, cx);
6546 editor.handle_input("}", window, cx);
6547 });
6548 cx.assert_editor_state(
6549 &"
6550 🏀{{{)}}}}ˇ
6551 ε{{{)}}}}ˇ
6552 ❤️{{{)}}}}ˇ
6553 "
6554 .unindent(),
6555 );
6556
6557 // autoclose multi-character pairs
6558 cx.set_state(
6559 &"
6560 ˇ
6561 ˇ
6562 "
6563 .unindent(),
6564 );
6565 cx.update_editor(|editor, window, cx| {
6566 editor.handle_input("/", window, cx);
6567 editor.handle_input("*", window, cx);
6568 });
6569 cx.assert_editor_state(
6570 &"
6571 /*ˇ */
6572 /*ˇ */
6573 "
6574 .unindent(),
6575 );
6576
6577 // one cursor autocloses a multi-character pair, one cursor
6578 // does not autoclose.
6579 cx.set_state(
6580 &"
6581 /ˇ
6582 ˇ
6583 "
6584 .unindent(),
6585 );
6586 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6587 cx.assert_editor_state(
6588 &"
6589 /*ˇ */
6590 *ˇ
6591 "
6592 .unindent(),
6593 );
6594
6595 // Don't autoclose if the next character isn't whitespace and isn't
6596 // listed in the language's "autoclose_before" section.
6597 cx.set_state("ˇa b");
6598 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6599 cx.assert_editor_state("{ˇa b");
6600
6601 // Don't autoclose if `close` is false for the bracket pair
6602 cx.set_state("ˇ");
6603 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6604 cx.assert_editor_state("[ˇ");
6605
6606 // Surround with brackets if text is selected
6607 cx.set_state("«aˇ» b");
6608 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6609 cx.assert_editor_state("{«aˇ»} b");
6610
6611 // Autoclose when not immediately after a word character
6612 cx.set_state("a ˇ");
6613 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6614 cx.assert_editor_state("a \"ˇ\"");
6615
6616 // Autoclose pair where the start and end characters are the same
6617 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6618 cx.assert_editor_state("a \"\"ˇ");
6619
6620 // Don't autoclose when immediately after a word character
6621 cx.set_state("aˇ");
6622 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6623 cx.assert_editor_state("a\"ˇ");
6624
6625 // Do autoclose when after a non-word character
6626 cx.set_state("{ˇ");
6627 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6628 cx.assert_editor_state("{\"ˇ\"");
6629
6630 // Non identical pairs autoclose regardless of preceding character
6631 cx.set_state("aˇ");
6632 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6633 cx.assert_editor_state("a{ˇ}");
6634
6635 // Don't autoclose pair if autoclose is disabled
6636 cx.set_state("ˇ");
6637 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6638 cx.assert_editor_state("<ˇ");
6639
6640 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6641 cx.set_state("«aˇ» b");
6642 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6643 cx.assert_editor_state("<«aˇ»> b");
6644}
6645
6646#[gpui::test]
6647async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6648 init_test(cx, |settings| {
6649 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6650 });
6651
6652 let mut cx = EditorTestContext::new(cx).await;
6653
6654 let language = Arc::new(Language::new(
6655 LanguageConfig {
6656 brackets: BracketPairConfig {
6657 pairs: vec![
6658 BracketPair {
6659 start: "{".to_string(),
6660 end: "}".to_string(),
6661 close: true,
6662 surround: true,
6663 newline: true,
6664 },
6665 BracketPair {
6666 start: "(".to_string(),
6667 end: ")".to_string(),
6668 close: true,
6669 surround: true,
6670 newline: true,
6671 },
6672 BracketPair {
6673 start: "[".to_string(),
6674 end: "]".to_string(),
6675 close: false,
6676 surround: false,
6677 newline: true,
6678 },
6679 ],
6680 ..Default::default()
6681 },
6682 autoclose_before: "})]".to_string(),
6683 ..Default::default()
6684 },
6685 Some(tree_sitter_rust::LANGUAGE.into()),
6686 ));
6687
6688 cx.language_registry().add(language.clone());
6689 cx.update_buffer(|buffer, cx| {
6690 buffer.set_language(Some(language), cx);
6691 });
6692
6693 cx.set_state(
6694 &"
6695 ˇ
6696 ˇ
6697 ˇ
6698 "
6699 .unindent(),
6700 );
6701
6702 // ensure only matching closing brackets are skipped over
6703 cx.update_editor(|editor, window, cx| {
6704 editor.handle_input("}", window, cx);
6705 editor.move_left(&MoveLeft, window, cx);
6706 editor.handle_input(")", window, cx);
6707 editor.move_left(&MoveLeft, window, cx);
6708 });
6709 cx.assert_editor_state(
6710 &"
6711 ˇ)}
6712 ˇ)}
6713 ˇ)}
6714 "
6715 .unindent(),
6716 );
6717
6718 // skip-over closing brackets at multiple cursors
6719 cx.update_editor(|editor, window, cx| {
6720 editor.handle_input(")", window, cx);
6721 editor.handle_input("}", window, cx);
6722 });
6723 cx.assert_editor_state(
6724 &"
6725 )}ˇ
6726 )}ˇ
6727 )}ˇ
6728 "
6729 .unindent(),
6730 );
6731
6732 // ignore non-close brackets
6733 cx.update_editor(|editor, window, cx| {
6734 editor.handle_input("]", window, cx);
6735 editor.move_left(&MoveLeft, window, cx);
6736 editor.handle_input("]", window, cx);
6737 });
6738 cx.assert_editor_state(
6739 &"
6740 )}]ˇ]
6741 )}]ˇ]
6742 )}]ˇ]
6743 "
6744 .unindent(),
6745 );
6746}
6747
6748#[gpui::test]
6749async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6750 init_test(cx, |_| {});
6751
6752 let mut cx = EditorTestContext::new(cx).await;
6753
6754 let html_language = Arc::new(
6755 Language::new(
6756 LanguageConfig {
6757 name: "HTML".into(),
6758 brackets: BracketPairConfig {
6759 pairs: vec![
6760 BracketPair {
6761 start: "<".into(),
6762 end: ">".into(),
6763 close: true,
6764 ..Default::default()
6765 },
6766 BracketPair {
6767 start: "{".into(),
6768 end: "}".into(),
6769 close: true,
6770 ..Default::default()
6771 },
6772 BracketPair {
6773 start: "(".into(),
6774 end: ")".into(),
6775 close: true,
6776 ..Default::default()
6777 },
6778 ],
6779 ..Default::default()
6780 },
6781 autoclose_before: "})]>".into(),
6782 ..Default::default()
6783 },
6784 Some(tree_sitter_html::LANGUAGE.into()),
6785 )
6786 .with_injection_query(
6787 r#"
6788 (script_element
6789 (raw_text) @injection.content
6790 (#set! injection.language "javascript"))
6791 "#,
6792 )
6793 .unwrap(),
6794 );
6795
6796 let javascript_language = Arc::new(Language::new(
6797 LanguageConfig {
6798 name: "JavaScript".into(),
6799 brackets: BracketPairConfig {
6800 pairs: vec![
6801 BracketPair {
6802 start: "/*".into(),
6803 end: " */".into(),
6804 close: true,
6805 ..Default::default()
6806 },
6807 BracketPair {
6808 start: "{".into(),
6809 end: "}".into(),
6810 close: true,
6811 ..Default::default()
6812 },
6813 BracketPair {
6814 start: "(".into(),
6815 end: ")".into(),
6816 close: true,
6817 ..Default::default()
6818 },
6819 ],
6820 ..Default::default()
6821 },
6822 autoclose_before: "})]>".into(),
6823 ..Default::default()
6824 },
6825 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6826 ));
6827
6828 cx.language_registry().add(html_language.clone());
6829 cx.language_registry().add(javascript_language.clone());
6830
6831 cx.update_buffer(|buffer, cx| {
6832 buffer.set_language(Some(html_language), cx);
6833 });
6834
6835 cx.set_state(
6836 &r#"
6837 <body>ˇ
6838 <script>
6839 var x = 1;ˇ
6840 </script>
6841 </body>ˇ
6842 "#
6843 .unindent(),
6844 );
6845
6846 // Precondition: different languages are active at different locations.
6847 cx.update_editor(|editor, window, cx| {
6848 let snapshot = editor.snapshot(window, cx);
6849 let cursors = editor.selections.ranges::<usize>(cx);
6850 let languages = cursors
6851 .iter()
6852 .map(|c| snapshot.language_at(c.start).unwrap().name())
6853 .collect::<Vec<_>>();
6854 assert_eq!(
6855 languages,
6856 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6857 );
6858 });
6859
6860 // Angle brackets autoclose in HTML, but not JavaScript.
6861 cx.update_editor(|editor, window, cx| {
6862 editor.handle_input("<", window, cx);
6863 editor.handle_input("a", window, cx);
6864 });
6865 cx.assert_editor_state(
6866 &r#"
6867 <body><aˇ>
6868 <script>
6869 var x = 1;<aˇ
6870 </script>
6871 </body><aˇ>
6872 "#
6873 .unindent(),
6874 );
6875
6876 // Curly braces and parens autoclose in both HTML and JavaScript.
6877 cx.update_editor(|editor, window, cx| {
6878 editor.handle_input(" b=", window, cx);
6879 editor.handle_input("{", window, cx);
6880 editor.handle_input("c", window, cx);
6881 editor.handle_input("(", window, cx);
6882 });
6883 cx.assert_editor_state(
6884 &r#"
6885 <body><a b={c(ˇ)}>
6886 <script>
6887 var x = 1;<a b={c(ˇ)}
6888 </script>
6889 </body><a b={c(ˇ)}>
6890 "#
6891 .unindent(),
6892 );
6893
6894 // Brackets that were already autoclosed are skipped.
6895 cx.update_editor(|editor, window, cx| {
6896 editor.handle_input(")", window, cx);
6897 editor.handle_input("d", window, cx);
6898 editor.handle_input("}", window, cx);
6899 });
6900 cx.assert_editor_state(
6901 &r#"
6902 <body><a b={c()d}ˇ>
6903 <script>
6904 var x = 1;<a b={c()d}ˇ
6905 </script>
6906 </body><a b={c()d}ˇ>
6907 "#
6908 .unindent(),
6909 );
6910 cx.update_editor(|editor, window, cx| {
6911 editor.handle_input(">", window, cx);
6912 });
6913 cx.assert_editor_state(
6914 &r#"
6915 <body><a b={c()d}>ˇ
6916 <script>
6917 var x = 1;<a b={c()d}>ˇ
6918 </script>
6919 </body><a b={c()d}>ˇ
6920 "#
6921 .unindent(),
6922 );
6923
6924 // Reset
6925 cx.set_state(
6926 &r#"
6927 <body>ˇ
6928 <script>
6929 var x = 1;ˇ
6930 </script>
6931 </body>ˇ
6932 "#
6933 .unindent(),
6934 );
6935
6936 cx.update_editor(|editor, window, cx| {
6937 editor.handle_input("<", window, cx);
6938 });
6939 cx.assert_editor_state(
6940 &r#"
6941 <body><ˇ>
6942 <script>
6943 var x = 1;<ˇ
6944 </script>
6945 </body><ˇ>
6946 "#
6947 .unindent(),
6948 );
6949
6950 // When backspacing, the closing angle brackets are removed.
6951 cx.update_editor(|editor, window, cx| {
6952 editor.backspace(&Backspace, window, cx);
6953 });
6954 cx.assert_editor_state(
6955 &r#"
6956 <body>ˇ
6957 <script>
6958 var x = 1;ˇ
6959 </script>
6960 </body>ˇ
6961 "#
6962 .unindent(),
6963 );
6964
6965 // Block comments autoclose in JavaScript, but not HTML.
6966 cx.update_editor(|editor, window, cx| {
6967 editor.handle_input("/", window, cx);
6968 editor.handle_input("*", window, cx);
6969 });
6970 cx.assert_editor_state(
6971 &r#"
6972 <body>/*ˇ
6973 <script>
6974 var x = 1;/*ˇ */
6975 </script>
6976 </body>/*ˇ
6977 "#
6978 .unindent(),
6979 );
6980}
6981
6982#[gpui::test]
6983async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6984 init_test(cx, |_| {});
6985
6986 let mut cx = EditorTestContext::new(cx).await;
6987
6988 let rust_language = Arc::new(
6989 Language::new(
6990 LanguageConfig {
6991 name: "Rust".into(),
6992 brackets: serde_json::from_value(json!([
6993 { "start": "{", "end": "}", "close": true, "newline": true },
6994 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6995 ]))
6996 .unwrap(),
6997 autoclose_before: "})]>".into(),
6998 ..Default::default()
6999 },
7000 Some(tree_sitter_rust::LANGUAGE.into()),
7001 )
7002 .with_override_query("(string_literal) @string")
7003 .unwrap(),
7004 );
7005
7006 cx.language_registry().add(rust_language.clone());
7007 cx.update_buffer(|buffer, cx| {
7008 buffer.set_language(Some(rust_language), cx);
7009 });
7010
7011 cx.set_state(
7012 &r#"
7013 let x = ˇ
7014 "#
7015 .unindent(),
7016 );
7017
7018 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7019 cx.update_editor(|editor, window, cx| {
7020 editor.handle_input("\"", window, cx);
7021 });
7022 cx.assert_editor_state(
7023 &r#"
7024 let x = "ˇ"
7025 "#
7026 .unindent(),
7027 );
7028
7029 // Inserting another quotation mark. The cursor moves across the existing
7030 // automatically-inserted quotation mark.
7031 cx.update_editor(|editor, window, cx| {
7032 editor.handle_input("\"", window, cx);
7033 });
7034 cx.assert_editor_state(
7035 &r#"
7036 let x = ""ˇ
7037 "#
7038 .unindent(),
7039 );
7040
7041 // Reset
7042 cx.set_state(
7043 &r#"
7044 let x = ˇ
7045 "#
7046 .unindent(),
7047 );
7048
7049 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7050 cx.update_editor(|editor, window, cx| {
7051 editor.handle_input("\"", window, cx);
7052 editor.handle_input(" ", window, cx);
7053 editor.move_left(&Default::default(), window, cx);
7054 editor.handle_input("\\", window, cx);
7055 editor.handle_input("\"", window, cx);
7056 });
7057 cx.assert_editor_state(
7058 &r#"
7059 let x = "\"ˇ "
7060 "#
7061 .unindent(),
7062 );
7063
7064 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7065 // mark. Nothing is inserted.
7066 cx.update_editor(|editor, window, cx| {
7067 editor.move_right(&Default::default(), window, cx);
7068 editor.handle_input("\"", window, cx);
7069 });
7070 cx.assert_editor_state(
7071 &r#"
7072 let x = "\" "ˇ
7073 "#
7074 .unindent(),
7075 );
7076}
7077
7078#[gpui::test]
7079async fn test_surround_with_pair(cx: &mut TestAppContext) {
7080 init_test(cx, |_| {});
7081
7082 let language = Arc::new(Language::new(
7083 LanguageConfig {
7084 brackets: BracketPairConfig {
7085 pairs: vec![
7086 BracketPair {
7087 start: "{".to_string(),
7088 end: "}".to_string(),
7089 close: true,
7090 surround: true,
7091 newline: true,
7092 },
7093 BracketPair {
7094 start: "/* ".to_string(),
7095 end: "*/".to_string(),
7096 close: true,
7097 surround: true,
7098 ..Default::default()
7099 },
7100 ],
7101 ..Default::default()
7102 },
7103 ..Default::default()
7104 },
7105 Some(tree_sitter_rust::LANGUAGE.into()),
7106 ));
7107
7108 let text = r#"
7109 a
7110 b
7111 c
7112 "#
7113 .unindent();
7114
7115 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7116 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7117 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7118 editor
7119 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7120 .await;
7121
7122 editor.update_in(cx, |editor, window, cx| {
7123 editor.change_selections(None, window, cx, |s| {
7124 s.select_display_ranges([
7125 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7126 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7127 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7128 ])
7129 });
7130
7131 editor.handle_input("{", window, cx);
7132 editor.handle_input("{", window, cx);
7133 editor.handle_input("{", window, cx);
7134 assert_eq!(
7135 editor.text(cx),
7136 "
7137 {{{a}}}
7138 {{{b}}}
7139 {{{c}}}
7140 "
7141 .unindent()
7142 );
7143 assert_eq!(
7144 editor.selections.display_ranges(cx),
7145 [
7146 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7147 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7148 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7149 ]
7150 );
7151
7152 editor.undo(&Undo, window, cx);
7153 editor.undo(&Undo, window, cx);
7154 editor.undo(&Undo, window, cx);
7155 assert_eq!(
7156 editor.text(cx),
7157 "
7158 a
7159 b
7160 c
7161 "
7162 .unindent()
7163 );
7164 assert_eq!(
7165 editor.selections.display_ranges(cx),
7166 [
7167 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7168 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7169 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7170 ]
7171 );
7172
7173 // Ensure inserting the first character of a multi-byte bracket pair
7174 // doesn't surround the selections with the bracket.
7175 editor.handle_input("/", window, cx);
7176 assert_eq!(
7177 editor.text(cx),
7178 "
7179 /
7180 /
7181 /
7182 "
7183 .unindent()
7184 );
7185 assert_eq!(
7186 editor.selections.display_ranges(cx),
7187 [
7188 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7189 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7190 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7191 ]
7192 );
7193
7194 editor.undo(&Undo, window, cx);
7195 assert_eq!(
7196 editor.text(cx),
7197 "
7198 a
7199 b
7200 c
7201 "
7202 .unindent()
7203 );
7204 assert_eq!(
7205 editor.selections.display_ranges(cx),
7206 [
7207 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7208 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7209 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7210 ]
7211 );
7212
7213 // Ensure inserting the last character of a multi-byte bracket pair
7214 // doesn't surround the selections with the bracket.
7215 editor.handle_input("*", window, cx);
7216 assert_eq!(
7217 editor.text(cx),
7218 "
7219 *
7220 *
7221 *
7222 "
7223 .unindent()
7224 );
7225 assert_eq!(
7226 editor.selections.display_ranges(cx),
7227 [
7228 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7229 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7230 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7231 ]
7232 );
7233 });
7234}
7235
7236#[gpui::test]
7237async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7238 init_test(cx, |_| {});
7239
7240 let language = Arc::new(Language::new(
7241 LanguageConfig {
7242 brackets: BracketPairConfig {
7243 pairs: vec![BracketPair {
7244 start: "{".to_string(),
7245 end: "}".to_string(),
7246 close: true,
7247 surround: true,
7248 newline: true,
7249 }],
7250 ..Default::default()
7251 },
7252 autoclose_before: "}".to_string(),
7253 ..Default::default()
7254 },
7255 Some(tree_sitter_rust::LANGUAGE.into()),
7256 ));
7257
7258 let text = r#"
7259 a
7260 b
7261 c
7262 "#
7263 .unindent();
7264
7265 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7266 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7267 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7268 editor
7269 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7270 .await;
7271
7272 editor.update_in(cx, |editor, window, cx| {
7273 editor.change_selections(None, window, cx, |s| {
7274 s.select_ranges([
7275 Point::new(0, 1)..Point::new(0, 1),
7276 Point::new(1, 1)..Point::new(1, 1),
7277 Point::new(2, 1)..Point::new(2, 1),
7278 ])
7279 });
7280
7281 editor.handle_input("{", window, cx);
7282 editor.handle_input("{", window, cx);
7283 editor.handle_input("_", window, cx);
7284 assert_eq!(
7285 editor.text(cx),
7286 "
7287 a{{_}}
7288 b{{_}}
7289 c{{_}}
7290 "
7291 .unindent()
7292 );
7293 assert_eq!(
7294 editor.selections.ranges::<Point>(cx),
7295 [
7296 Point::new(0, 4)..Point::new(0, 4),
7297 Point::new(1, 4)..Point::new(1, 4),
7298 Point::new(2, 4)..Point::new(2, 4)
7299 ]
7300 );
7301
7302 editor.backspace(&Default::default(), window, cx);
7303 editor.backspace(&Default::default(), window, cx);
7304 assert_eq!(
7305 editor.text(cx),
7306 "
7307 a{}
7308 b{}
7309 c{}
7310 "
7311 .unindent()
7312 );
7313 assert_eq!(
7314 editor.selections.ranges::<Point>(cx),
7315 [
7316 Point::new(0, 2)..Point::new(0, 2),
7317 Point::new(1, 2)..Point::new(1, 2),
7318 Point::new(2, 2)..Point::new(2, 2)
7319 ]
7320 );
7321
7322 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7323 assert_eq!(
7324 editor.text(cx),
7325 "
7326 a
7327 b
7328 c
7329 "
7330 .unindent()
7331 );
7332 assert_eq!(
7333 editor.selections.ranges::<Point>(cx),
7334 [
7335 Point::new(0, 1)..Point::new(0, 1),
7336 Point::new(1, 1)..Point::new(1, 1),
7337 Point::new(2, 1)..Point::new(2, 1)
7338 ]
7339 );
7340 });
7341}
7342
7343#[gpui::test]
7344async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7345 init_test(cx, |settings| {
7346 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7347 });
7348
7349 let mut cx = EditorTestContext::new(cx).await;
7350
7351 let language = Arc::new(Language::new(
7352 LanguageConfig {
7353 brackets: BracketPairConfig {
7354 pairs: vec![
7355 BracketPair {
7356 start: "{".to_string(),
7357 end: "}".to_string(),
7358 close: true,
7359 surround: true,
7360 newline: true,
7361 },
7362 BracketPair {
7363 start: "(".to_string(),
7364 end: ")".to_string(),
7365 close: true,
7366 surround: true,
7367 newline: true,
7368 },
7369 BracketPair {
7370 start: "[".to_string(),
7371 end: "]".to_string(),
7372 close: false,
7373 surround: true,
7374 newline: true,
7375 },
7376 ],
7377 ..Default::default()
7378 },
7379 autoclose_before: "})]".to_string(),
7380 ..Default::default()
7381 },
7382 Some(tree_sitter_rust::LANGUAGE.into()),
7383 ));
7384
7385 cx.language_registry().add(language.clone());
7386 cx.update_buffer(|buffer, cx| {
7387 buffer.set_language(Some(language), cx);
7388 });
7389
7390 cx.set_state(
7391 &"
7392 {(ˇ)}
7393 [[ˇ]]
7394 {(ˇ)}
7395 "
7396 .unindent(),
7397 );
7398
7399 cx.update_editor(|editor, window, cx| {
7400 editor.backspace(&Default::default(), window, cx);
7401 editor.backspace(&Default::default(), window, cx);
7402 });
7403
7404 cx.assert_editor_state(
7405 &"
7406 ˇ
7407 ˇ]]
7408 ˇ
7409 "
7410 .unindent(),
7411 );
7412
7413 cx.update_editor(|editor, window, cx| {
7414 editor.handle_input("{", window, cx);
7415 editor.handle_input("{", window, cx);
7416 editor.move_right(&MoveRight, window, cx);
7417 editor.move_right(&MoveRight, window, cx);
7418 editor.move_left(&MoveLeft, window, cx);
7419 editor.move_left(&MoveLeft, window, cx);
7420 editor.backspace(&Default::default(), window, cx);
7421 });
7422
7423 cx.assert_editor_state(
7424 &"
7425 {ˇ}
7426 {ˇ}]]
7427 {ˇ}
7428 "
7429 .unindent(),
7430 );
7431
7432 cx.update_editor(|editor, window, cx| {
7433 editor.backspace(&Default::default(), window, cx);
7434 });
7435
7436 cx.assert_editor_state(
7437 &"
7438 ˇ
7439 ˇ]]
7440 ˇ
7441 "
7442 .unindent(),
7443 );
7444}
7445
7446#[gpui::test]
7447async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7448 init_test(cx, |_| {});
7449
7450 let language = Arc::new(Language::new(
7451 LanguageConfig::default(),
7452 Some(tree_sitter_rust::LANGUAGE.into()),
7453 ));
7454
7455 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7456 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7457 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7458 editor
7459 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7460 .await;
7461
7462 editor.update_in(cx, |editor, window, cx| {
7463 editor.set_auto_replace_emoji_shortcode(true);
7464
7465 editor.handle_input("Hello ", window, cx);
7466 editor.handle_input(":wave", window, cx);
7467 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7468
7469 editor.handle_input(":", window, cx);
7470 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7471
7472 editor.handle_input(" :smile", window, cx);
7473 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7474
7475 editor.handle_input(":", window, cx);
7476 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7477
7478 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7479 editor.handle_input(":wave", window, cx);
7480 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7481
7482 editor.handle_input(":", window, cx);
7483 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7484
7485 editor.handle_input(":1", window, cx);
7486 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7487
7488 editor.handle_input(":", window, cx);
7489 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7490
7491 // Ensure shortcode does not get replaced when it is part of a word
7492 editor.handle_input(" Test:wave", window, cx);
7493 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7494
7495 editor.handle_input(":", window, cx);
7496 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7497
7498 editor.set_auto_replace_emoji_shortcode(false);
7499
7500 // Ensure shortcode does not get replaced when auto replace is off
7501 editor.handle_input(" :wave", window, cx);
7502 assert_eq!(
7503 editor.text(cx),
7504 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7505 );
7506
7507 editor.handle_input(":", window, cx);
7508 assert_eq!(
7509 editor.text(cx),
7510 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7511 );
7512 });
7513}
7514
7515#[gpui::test]
7516async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7517 init_test(cx, |_| {});
7518
7519 let (text, insertion_ranges) = marked_text_ranges(
7520 indoc! {"
7521 ˇ
7522 "},
7523 false,
7524 );
7525
7526 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7527 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7528
7529 _ = editor.update_in(cx, |editor, window, cx| {
7530 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7531
7532 editor
7533 .insert_snippet(&insertion_ranges, snippet, window, cx)
7534 .unwrap();
7535
7536 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7537 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7538 assert_eq!(editor.text(cx), expected_text);
7539 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7540 }
7541
7542 assert(
7543 editor,
7544 cx,
7545 indoc! {"
7546 type «» =•
7547 "},
7548 );
7549
7550 assert!(editor.context_menu_visible(), "There should be a matches");
7551 });
7552}
7553
7554#[gpui::test]
7555async fn test_snippets(cx: &mut TestAppContext) {
7556 init_test(cx, |_| {});
7557
7558 let (text, insertion_ranges) = marked_text_ranges(
7559 indoc! {"
7560 a.ˇ b
7561 a.ˇ b
7562 a.ˇ b
7563 "},
7564 false,
7565 );
7566
7567 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7568 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7569
7570 editor.update_in(cx, |editor, window, cx| {
7571 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7572
7573 editor
7574 .insert_snippet(&insertion_ranges, snippet, window, cx)
7575 .unwrap();
7576
7577 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7578 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7579 assert_eq!(editor.text(cx), expected_text);
7580 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7581 }
7582
7583 assert(
7584 editor,
7585 cx,
7586 indoc! {"
7587 a.f(«one», two, «three») b
7588 a.f(«one», two, «three») b
7589 a.f(«one», two, «three») b
7590 "},
7591 );
7592
7593 // Can't move earlier than the first tab stop
7594 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7595 assert(
7596 editor,
7597 cx,
7598 indoc! {"
7599 a.f(«one», two, «three») b
7600 a.f(«one», two, «three») b
7601 a.f(«one», two, «three») b
7602 "},
7603 );
7604
7605 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7606 assert(
7607 editor,
7608 cx,
7609 indoc! {"
7610 a.f(one, «two», three) b
7611 a.f(one, «two», three) b
7612 a.f(one, «two», three) b
7613 "},
7614 );
7615
7616 editor.move_to_prev_snippet_tabstop(window, cx);
7617 assert(
7618 editor,
7619 cx,
7620 indoc! {"
7621 a.f(«one», two, «three») b
7622 a.f(«one», two, «three») b
7623 a.f(«one», two, «three») b
7624 "},
7625 );
7626
7627 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7628 assert(
7629 editor,
7630 cx,
7631 indoc! {"
7632 a.f(one, «two», three) b
7633 a.f(one, «two», three) b
7634 a.f(one, «two», three) b
7635 "},
7636 );
7637 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7638 assert(
7639 editor,
7640 cx,
7641 indoc! {"
7642 a.f(one, two, three)ˇ b
7643 a.f(one, two, three)ˇ b
7644 a.f(one, two, three)ˇ b
7645 "},
7646 );
7647
7648 // As soon as the last tab stop is reached, snippet state is gone
7649 editor.move_to_prev_snippet_tabstop(window, cx);
7650 assert(
7651 editor,
7652 cx,
7653 indoc! {"
7654 a.f(one, two, three)ˇ b
7655 a.f(one, two, three)ˇ b
7656 a.f(one, two, three)ˇ b
7657 "},
7658 );
7659 });
7660}
7661
7662#[gpui::test]
7663async fn test_document_format_during_save(cx: &mut TestAppContext) {
7664 init_test(cx, |_| {});
7665
7666 let fs = FakeFs::new(cx.executor());
7667 fs.insert_file(path!("/file.rs"), Default::default()).await;
7668
7669 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7670
7671 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7672 language_registry.add(rust_lang());
7673 let mut fake_servers = language_registry.register_fake_lsp(
7674 "Rust",
7675 FakeLspAdapter {
7676 capabilities: lsp::ServerCapabilities {
7677 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7678 ..Default::default()
7679 },
7680 ..Default::default()
7681 },
7682 );
7683
7684 let buffer = project
7685 .update(cx, |project, cx| {
7686 project.open_local_buffer(path!("/file.rs"), cx)
7687 })
7688 .await
7689 .unwrap();
7690
7691 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7692 let (editor, cx) = cx.add_window_view(|window, cx| {
7693 build_editor_with_project(project.clone(), buffer, window, cx)
7694 });
7695 editor.update_in(cx, |editor, window, cx| {
7696 editor.set_text("one\ntwo\nthree\n", window, cx)
7697 });
7698 assert!(cx.read(|cx| editor.is_dirty(cx)));
7699
7700 cx.executor().start_waiting();
7701 let fake_server = fake_servers.next().await.unwrap();
7702
7703 let save = editor
7704 .update_in(cx, |editor, window, cx| {
7705 editor.save(true, project.clone(), window, cx)
7706 })
7707 .unwrap();
7708 fake_server
7709 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7710 assert_eq!(
7711 params.text_document.uri,
7712 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7713 );
7714 assert_eq!(params.options.tab_size, 4);
7715 Ok(Some(vec![lsp::TextEdit::new(
7716 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7717 ", ".to_string(),
7718 )]))
7719 })
7720 .next()
7721 .await;
7722 cx.executor().start_waiting();
7723 save.await;
7724
7725 assert_eq!(
7726 editor.update(cx, |editor, cx| editor.text(cx)),
7727 "one, two\nthree\n"
7728 );
7729 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7730
7731 editor.update_in(cx, |editor, window, cx| {
7732 editor.set_text("one\ntwo\nthree\n", window, cx)
7733 });
7734 assert!(cx.read(|cx| editor.is_dirty(cx)));
7735
7736 // Ensure we can still save even if formatting hangs.
7737 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7738 move |params, _| async move {
7739 assert_eq!(
7740 params.text_document.uri,
7741 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7742 );
7743 futures::future::pending::<()>().await;
7744 unreachable!()
7745 },
7746 );
7747 let save = editor
7748 .update_in(cx, |editor, window, cx| {
7749 editor.save(true, project.clone(), window, cx)
7750 })
7751 .unwrap();
7752 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7753 cx.executor().start_waiting();
7754 save.await;
7755 assert_eq!(
7756 editor.update(cx, |editor, cx| editor.text(cx)),
7757 "one\ntwo\nthree\n"
7758 );
7759 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7760
7761 // For non-dirty buffer, no formatting request should be sent
7762 let save = editor
7763 .update_in(cx, |editor, window, cx| {
7764 editor.save(true, project.clone(), window, cx)
7765 })
7766 .unwrap();
7767 let _pending_format_request = fake_server
7768 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7769 panic!("Should not be invoked on non-dirty buffer");
7770 })
7771 .next();
7772 cx.executor().start_waiting();
7773 save.await;
7774
7775 // Set rust language override and assert overridden tabsize is sent to language server
7776 update_test_language_settings(cx, |settings| {
7777 settings.languages.insert(
7778 "Rust".into(),
7779 LanguageSettingsContent {
7780 tab_size: NonZeroU32::new(8),
7781 ..Default::default()
7782 },
7783 );
7784 });
7785
7786 editor.update_in(cx, |editor, window, cx| {
7787 editor.set_text("somehting_new\n", window, cx)
7788 });
7789 assert!(cx.read(|cx| editor.is_dirty(cx)));
7790 let save = editor
7791 .update_in(cx, |editor, window, cx| {
7792 editor.save(true, project.clone(), window, cx)
7793 })
7794 .unwrap();
7795 fake_server
7796 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7797 assert_eq!(
7798 params.text_document.uri,
7799 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7800 );
7801 assert_eq!(params.options.tab_size, 8);
7802 Ok(Some(vec![]))
7803 })
7804 .next()
7805 .await;
7806 cx.executor().start_waiting();
7807 save.await;
7808}
7809
7810#[gpui::test]
7811async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7812 init_test(cx, |_| {});
7813
7814 let cols = 4;
7815 let rows = 10;
7816 let sample_text_1 = sample_text(rows, cols, 'a');
7817 assert_eq!(
7818 sample_text_1,
7819 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7820 );
7821 let sample_text_2 = sample_text(rows, cols, 'l');
7822 assert_eq!(
7823 sample_text_2,
7824 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7825 );
7826 let sample_text_3 = sample_text(rows, cols, 'v');
7827 assert_eq!(
7828 sample_text_3,
7829 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7830 );
7831
7832 let fs = FakeFs::new(cx.executor());
7833 fs.insert_tree(
7834 path!("/a"),
7835 json!({
7836 "main.rs": sample_text_1,
7837 "other.rs": sample_text_2,
7838 "lib.rs": sample_text_3,
7839 }),
7840 )
7841 .await;
7842
7843 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7844 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7845 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7846
7847 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7848 language_registry.add(rust_lang());
7849 let mut fake_servers = language_registry.register_fake_lsp(
7850 "Rust",
7851 FakeLspAdapter {
7852 capabilities: lsp::ServerCapabilities {
7853 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7854 ..Default::default()
7855 },
7856 ..Default::default()
7857 },
7858 );
7859
7860 let worktree = project.update(cx, |project, cx| {
7861 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7862 assert_eq!(worktrees.len(), 1);
7863 worktrees.pop().unwrap()
7864 });
7865 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7866
7867 let buffer_1 = project
7868 .update(cx, |project, cx| {
7869 project.open_buffer((worktree_id, "main.rs"), cx)
7870 })
7871 .await
7872 .unwrap();
7873 let buffer_2 = project
7874 .update(cx, |project, cx| {
7875 project.open_buffer((worktree_id, "other.rs"), cx)
7876 })
7877 .await
7878 .unwrap();
7879 let buffer_3 = project
7880 .update(cx, |project, cx| {
7881 project.open_buffer((worktree_id, "lib.rs"), cx)
7882 })
7883 .await
7884 .unwrap();
7885
7886 let multi_buffer = cx.new(|cx| {
7887 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7888 multi_buffer.push_excerpts(
7889 buffer_1.clone(),
7890 [
7891 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7892 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7893 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7894 ],
7895 cx,
7896 );
7897 multi_buffer.push_excerpts(
7898 buffer_2.clone(),
7899 [
7900 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7901 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7902 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7903 ],
7904 cx,
7905 );
7906 multi_buffer.push_excerpts(
7907 buffer_3.clone(),
7908 [
7909 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7910 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7911 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7912 ],
7913 cx,
7914 );
7915 multi_buffer
7916 });
7917 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7918 Editor::new(
7919 EditorMode::Full,
7920 multi_buffer,
7921 Some(project.clone()),
7922 window,
7923 cx,
7924 )
7925 });
7926
7927 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7928 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7929 s.select_ranges(Some(1..2))
7930 });
7931 editor.insert("|one|two|three|", window, cx);
7932 });
7933 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7934 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7935 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7936 s.select_ranges(Some(60..70))
7937 });
7938 editor.insert("|four|five|six|", window, cx);
7939 });
7940 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7941
7942 // First two buffers should be edited, but not the third one.
7943 assert_eq!(
7944 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7945 "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}",
7946 );
7947 buffer_1.update(cx, |buffer, _| {
7948 assert!(buffer.is_dirty());
7949 assert_eq!(
7950 buffer.text(),
7951 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7952 )
7953 });
7954 buffer_2.update(cx, |buffer, _| {
7955 assert!(buffer.is_dirty());
7956 assert_eq!(
7957 buffer.text(),
7958 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7959 )
7960 });
7961 buffer_3.update(cx, |buffer, _| {
7962 assert!(!buffer.is_dirty());
7963 assert_eq!(buffer.text(), sample_text_3,)
7964 });
7965 cx.executor().run_until_parked();
7966
7967 cx.executor().start_waiting();
7968 let save = multi_buffer_editor
7969 .update_in(cx, |editor, window, cx| {
7970 editor.save(true, project.clone(), window, cx)
7971 })
7972 .unwrap();
7973
7974 let fake_server = fake_servers.next().await.unwrap();
7975 fake_server
7976 .server
7977 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7978 Ok(Some(vec![lsp::TextEdit::new(
7979 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7980 format!("[{} formatted]", params.text_document.uri),
7981 )]))
7982 })
7983 .detach();
7984 save.await;
7985
7986 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7987 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7988 assert_eq!(
7989 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7990 uri!(
7991 "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}"
7992 ),
7993 );
7994 buffer_1.update(cx, |buffer, _| {
7995 assert!(!buffer.is_dirty());
7996 assert_eq!(
7997 buffer.text(),
7998 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7999 )
8000 });
8001 buffer_2.update(cx, |buffer, _| {
8002 assert!(!buffer.is_dirty());
8003 assert_eq!(
8004 buffer.text(),
8005 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8006 )
8007 });
8008 buffer_3.update(cx, |buffer, _| {
8009 assert!(!buffer.is_dirty());
8010 assert_eq!(buffer.text(), sample_text_3,)
8011 });
8012}
8013
8014#[gpui::test]
8015async fn test_range_format_during_save(cx: &mut TestAppContext) {
8016 init_test(cx, |_| {});
8017
8018 let fs = FakeFs::new(cx.executor());
8019 fs.insert_file(path!("/file.rs"), Default::default()).await;
8020
8021 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8022
8023 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8024 language_registry.add(rust_lang());
8025 let mut fake_servers = language_registry.register_fake_lsp(
8026 "Rust",
8027 FakeLspAdapter {
8028 capabilities: lsp::ServerCapabilities {
8029 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8030 ..Default::default()
8031 },
8032 ..Default::default()
8033 },
8034 );
8035
8036 let buffer = project
8037 .update(cx, |project, cx| {
8038 project.open_local_buffer(path!("/file.rs"), cx)
8039 })
8040 .await
8041 .unwrap();
8042
8043 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8044 let (editor, cx) = cx.add_window_view(|window, cx| {
8045 build_editor_with_project(project.clone(), buffer, window, cx)
8046 });
8047 editor.update_in(cx, |editor, window, cx| {
8048 editor.set_text("one\ntwo\nthree\n", window, cx)
8049 });
8050 assert!(cx.read(|cx| editor.is_dirty(cx)));
8051
8052 cx.executor().start_waiting();
8053 let fake_server = fake_servers.next().await.unwrap();
8054
8055 let save = editor
8056 .update_in(cx, |editor, window, cx| {
8057 editor.save(true, project.clone(), window, cx)
8058 })
8059 .unwrap();
8060 fake_server
8061 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8062 assert_eq!(
8063 params.text_document.uri,
8064 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8065 );
8066 assert_eq!(params.options.tab_size, 4);
8067 Ok(Some(vec![lsp::TextEdit::new(
8068 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8069 ", ".to_string(),
8070 )]))
8071 })
8072 .next()
8073 .await;
8074 cx.executor().start_waiting();
8075 save.await;
8076 assert_eq!(
8077 editor.update(cx, |editor, cx| editor.text(cx)),
8078 "one, two\nthree\n"
8079 );
8080 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8081
8082 editor.update_in(cx, |editor, window, cx| {
8083 editor.set_text("one\ntwo\nthree\n", window, cx)
8084 });
8085 assert!(cx.read(|cx| editor.is_dirty(cx)));
8086
8087 // Ensure we can still save even if formatting hangs.
8088 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8089 move |params, _| async move {
8090 assert_eq!(
8091 params.text_document.uri,
8092 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8093 );
8094 futures::future::pending::<()>().await;
8095 unreachable!()
8096 },
8097 );
8098 let save = editor
8099 .update_in(cx, |editor, window, cx| {
8100 editor.save(true, project.clone(), window, cx)
8101 })
8102 .unwrap();
8103 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8104 cx.executor().start_waiting();
8105 save.await;
8106 assert_eq!(
8107 editor.update(cx, |editor, cx| editor.text(cx)),
8108 "one\ntwo\nthree\n"
8109 );
8110 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8111
8112 // For non-dirty buffer, no formatting request should be sent
8113 let save = editor
8114 .update_in(cx, |editor, window, cx| {
8115 editor.save(true, project.clone(), window, cx)
8116 })
8117 .unwrap();
8118 let _pending_format_request = fake_server
8119 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8120 panic!("Should not be invoked on non-dirty buffer");
8121 })
8122 .next();
8123 cx.executor().start_waiting();
8124 save.await;
8125
8126 // Set Rust language override and assert overridden tabsize is sent to language server
8127 update_test_language_settings(cx, |settings| {
8128 settings.languages.insert(
8129 "Rust".into(),
8130 LanguageSettingsContent {
8131 tab_size: NonZeroU32::new(8),
8132 ..Default::default()
8133 },
8134 );
8135 });
8136
8137 editor.update_in(cx, |editor, window, cx| {
8138 editor.set_text("somehting_new\n", window, cx)
8139 });
8140 assert!(cx.read(|cx| editor.is_dirty(cx)));
8141 let save = editor
8142 .update_in(cx, |editor, window, cx| {
8143 editor.save(true, project.clone(), window, cx)
8144 })
8145 .unwrap();
8146 fake_server
8147 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8148 assert_eq!(
8149 params.text_document.uri,
8150 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8151 );
8152 assert_eq!(params.options.tab_size, 8);
8153 Ok(Some(vec![]))
8154 })
8155 .next()
8156 .await;
8157 cx.executor().start_waiting();
8158 save.await;
8159}
8160
8161#[gpui::test]
8162async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8163 init_test(cx, |settings| {
8164 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8165 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8166 ))
8167 });
8168
8169 let fs = FakeFs::new(cx.executor());
8170 fs.insert_file(path!("/file.rs"), Default::default()).await;
8171
8172 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8173
8174 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8175 language_registry.add(Arc::new(Language::new(
8176 LanguageConfig {
8177 name: "Rust".into(),
8178 matcher: LanguageMatcher {
8179 path_suffixes: vec!["rs".to_string()],
8180 ..Default::default()
8181 },
8182 ..LanguageConfig::default()
8183 },
8184 Some(tree_sitter_rust::LANGUAGE.into()),
8185 )));
8186 update_test_language_settings(cx, |settings| {
8187 // Enable Prettier formatting for the same buffer, and ensure
8188 // LSP is called instead of Prettier.
8189 settings.defaults.prettier = Some(PrettierSettings {
8190 allowed: true,
8191 ..PrettierSettings::default()
8192 });
8193 });
8194 let mut fake_servers = language_registry.register_fake_lsp(
8195 "Rust",
8196 FakeLspAdapter {
8197 capabilities: lsp::ServerCapabilities {
8198 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8199 ..Default::default()
8200 },
8201 ..Default::default()
8202 },
8203 );
8204
8205 let buffer = project
8206 .update(cx, |project, cx| {
8207 project.open_local_buffer(path!("/file.rs"), cx)
8208 })
8209 .await
8210 .unwrap();
8211
8212 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8213 let (editor, cx) = cx.add_window_view(|window, cx| {
8214 build_editor_with_project(project.clone(), buffer, window, cx)
8215 });
8216 editor.update_in(cx, |editor, window, cx| {
8217 editor.set_text("one\ntwo\nthree\n", window, cx)
8218 });
8219
8220 cx.executor().start_waiting();
8221 let fake_server = fake_servers.next().await.unwrap();
8222
8223 let format = editor
8224 .update_in(cx, |editor, window, cx| {
8225 editor.perform_format(
8226 project.clone(),
8227 FormatTrigger::Manual,
8228 FormatTarget::Buffers,
8229 window,
8230 cx,
8231 )
8232 })
8233 .unwrap();
8234 fake_server
8235 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8236 assert_eq!(
8237 params.text_document.uri,
8238 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8239 );
8240 assert_eq!(params.options.tab_size, 4);
8241 Ok(Some(vec![lsp::TextEdit::new(
8242 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8243 ", ".to_string(),
8244 )]))
8245 })
8246 .next()
8247 .await;
8248 cx.executor().start_waiting();
8249 format.await;
8250 assert_eq!(
8251 editor.update(cx, |editor, cx| editor.text(cx)),
8252 "one, two\nthree\n"
8253 );
8254
8255 editor.update_in(cx, |editor, window, cx| {
8256 editor.set_text("one\ntwo\nthree\n", window, cx)
8257 });
8258 // Ensure we don't lock if formatting hangs.
8259 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8260 move |params, _| async move {
8261 assert_eq!(
8262 params.text_document.uri,
8263 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8264 );
8265 futures::future::pending::<()>().await;
8266 unreachable!()
8267 },
8268 );
8269 let format = editor
8270 .update_in(cx, |editor, window, cx| {
8271 editor.perform_format(
8272 project,
8273 FormatTrigger::Manual,
8274 FormatTarget::Buffers,
8275 window,
8276 cx,
8277 )
8278 })
8279 .unwrap();
8280 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8281 cx.executor().start_waiting();
8282 format.await;
8283 assert_eq!(
8284 editor.update(cx, |editor, cx| editor.text(cx)),
8285 "one\ntwo\nthree\n"
8286 );
8287}
8288
8289#[gpui::test]
8290async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8291 init_test(cx, |settings| {
8292 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8293 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8294 ))
8295 });
8296
8297 let fs = FakeFs::new(cx.executor());
8298 fs.insert_file(path!("/file.ts"), Default::default()).await;
8299
8300 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8301
8302 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8303 language_registry.add(Arc::new(Language::new(
8304 LanguageConfig {
8305 name: "TypeScript".into(),
8306 matcher: LanguageMatcher {
8307 path_suffixes: vec!["ts".to_string()],
8308 ..Default::default()
8309 },
8310 ..LanguageConfig::default()
8311 },
8312 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8313 )));
8314 update_test_language_settings(cx, |settings| {
8315 settings.defaults.prettier = Some(PrettierSettings {
8316 allowed: true,
8317 ..PrettierSettings::default()
8318 });
8319 });
8320 let mut fake_servers = language_registry.register_fake_lsp(
8321 "TypeScript",
8322 FakeLspAdapter {
8323 capabilities: lsp::ServerCapabilities {
8324 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8325 ..Default::default()
8326 },
8327 ..Default::default()
8328 },
8329 );
8330
8331 let buffer = project
8332 .update(cx, |project, cx| {
8333 project.open_local_buffer(path!("/file.ts"), cx)
8334 })
8335 .await
8336 .unwrap();
8337
8338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8339 let (editor, cx) = cx.add_window_view(|window, cx| {
8340 build_editor_with_project(project.clone(), buffer, window, cx)
8341 });
8342 editor.update_in(cx, |editor, window, cx| {
8343 editor.set_text(
8344 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8345 window,
8346 cx,
8347 )
8348 });
8349
8350 cx.executor().start_waiting();
8351 let fake_server = fake_servers.next().await.unwrap();
8352
8353 let format = editor
8354 .update_in(cx, |editor, window, cx| {
8355 editor.perform_code_action_kind(
8356 project.clone(),
8357 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8358 window,
8359 cx,
8360 )
8361 })
8362 .unwrap();
8363 fake_server
8364 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8365 assert_eq!(
8366 params.text_document.uri,
8367 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8368 );
8369 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8370 lsp::CodeAction {
8371 title: "Organize Imports".to_string(),
8372 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8373 edit: Some(lsp::WorkspaceEdit {
8374 changes: Some(
8375 [(
8376 params.text_document.uri.clone(),
8377 vec![lsp::TextEdit::new(
8378 lsp::Range::new(
8379 lsp::Position::new(1, 0),
8380 lsp::Position::new(2, 0),
8381 ),
8382 "".to_string(),
8383 )],
8384 )]
8385 .into_iter()
8386 .collect(),
8387 ),
8388 ..Default::default()
8389 }),
8390 ..Default::default()
8391 },
8392 )]))
8393 })
8394 .next()
8395 .await;
8396 cx.executor().start_waiting();
8397 format.await;
8398 assert_eq!(
8399 editor.update(cx, |editor, cx| editor.text(cx)),
8400 "import { a } from 'module';\n\nconst x = a;\n"
8401 );
8402
8403 editor.update_in(cx, |editor, window, cx| {
8404 editor.set_text(
8405 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8406 window,
8407 cx,
8408 )
8409 });
8410 // Ensure we don't lock if code action hangs.
8411 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8412 move |params, _| async move {
8413 assert_eq!(
8414 params.text_document.uri,
8415 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8416 );
8417 futures::future::pending::<()>().await;
8418 unreachable!()
8419 },
8420 );
8421 let format = editor
8422 .update_in(cx, |editor, window, cx| {
8423 editor.perform_code_action_kind(
8424 project,
8425 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8426 window,
8427 cx,
8428 )
8429 })
8430 .unwrap();
8431 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8432 cx.executor().start_waiting();
8433 format.await;
8434 assert_eq!(
8435 editor.update(cx, |editor, cx| editor.text(cx)),
8436 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8437 );
8438}
8439
8440#[gpui::test]
8441async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8442 init_test(cx, |_| {});
8443
8444 let mut cx = EditorLspTestContext::new_rust(
8445 lsp::ServerCapabilities {
8446 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8447 ..Default::default()
8448 },
8449 cx,
8450 )
8451 .await;
8452
8453 cx.set_state(indoc! {"
8454 one.twoˇ
8455 "});
8456
8457 // The format request takes a long time. When it completes, it inserts
8458 // a newline and an indent before the `.`
8459 cx.lsp
8460 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8461 let executor = cx.background_executor().clone();
8462 async move {
8463 executor.timer(Duration::from_millis(100)).await;
8464 Ok(Some(vec![lsp::TextEdit {
8465 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8466 new_text: "\n ".into(),
8467 }]))
8468 }
8469 });
8470
8471 // Submit a format request.
8472 let format_1 = cx
8473 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8474 .unwrap();
8475 cx.executor().run_until_parked();
8476
8477 // Submit a second format request.
8478 let format_2 = cx
8479 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8480 .unwrap();
8481 cx.executor().run_until_parked();
8482
8483 // Wait for both format requests to complete
8484 cx.executor().advance_clock(Duration::from_millis(200));
8485 cx.executor().start_waiting();
8486 format_1.await.unwrap();
8487 cx.executor().start_waiting();
8488 format_2.await.unwrap();
8489
8490 // The formatting edits only happens once.
8491 cx.assert_editor_state(indoc! {"
8492 one
8493 .twoˇ
8494 "});
8495}
8496
8497#[gpui::test]
8498async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8499 init_test(cx, |settings| {
8500 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8501 });
8502
8503 let mut cx = EditorLspTestContext::new_rust(
8504 lsp::ServerCapabilities {
8505 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8506 ..Default::default()
8507 },
8508 cx,
8509 )
8510 .await;
8511
8512 // Set up a buffer white some trailing whitespace and no trailing newline.
8513 cx.set_state(
8514 &[
8515 "one ", //
8516 "twoˇ", //
8517 "three ", //
8518 "four", //
8519 ]
8520 .join("\n"),
8521 );
8522
8523 // Submit a format request.
8524 let format = cx
8525 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8526 .unwrap();
8527
8528 // Record which buffer changes have been sent to the language server
8529 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8530 cx.lsp
8531 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8532 let buffer_changes = buffer_changes.clone();
8533 move |params, _| {
8534 buffer_changes.lock().extend(
8535 params
8536 .content_changes
8537 .into_iter()
8538 .map(|e| (e.range.unwrap(), e.text)),
8539 );
8540 }
8541 });
8542
8543 // Handle formatting requests to the language server.
8544 cx.lsp
8545 .set_request_handler::<lsp::request::Formatting, _, _>({
8546 let buffer_changes = buffer_changes.clone();
8547 move |_, _| {
8548 // When formatting is requested, trailing whitespace has already been stripped,
8549 // and the trailing newline has already been added.
8550 assert_eq!(
8551 &buffer_changes.lock()[1..],
8552 &[
8553 (
8554 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8555 "".into()
8556 ),
8557 (
8558 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8559 "".into()
8560 ),
8561 (
8562 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8563 "\n".into()
8564 ),
8565 ]
8566 );
8567
8568 // Insert blank lines between each line of the buffer.
8569 async move {
8570 Ok(Some(vec![
8571 lsp::TextEdit {
8572 range: lsp::Range::new(
8573 lsp::Position::new(1, 0),
8574 lsp::Position::new(1, 0),
8575 ),
8576 new_text: "\n".into(),
8577 },
8578 lsp::TextEdit {
8579 range: lsp::Range::new(
8580 lsp::Position::new(2, 0),
8581 lsp::Position::new(2, 0),
8582 ),
8583 new_text: "\n".into(),
8584 },
8585 ]))
8586 }
8587 }
8588 });
8589
8590 // After formatting the buffer, the trailing whitespace is stripped,
8591 // a newline is appended, and the edits provided by the language server
8592 // have been applied.
8593 format.await.unwrap();
8594 cx.assert_editor_state(
8595 &[
8596 "one", //
8597 "", //
8598 "twoˇ", //
8599 "", //
8600 "three", //
8601 "four", //
8602 "", //
8603 ]
8604 .join("\n"),
8605 );
8606
8607 // Undoing the formatting undoes the trailing whitespace removal, the
8608 // trailing newline, and the LSP edits.
8609 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8610 cx.assert_editor_state(
8611 &[
8612 "one ", //
8613 "twoˇ", //
8614 "three ", //
8615 "four", //
8616 ]
8617 .join("\n"),
8618 );
8619}
8620
8621#[gpui::test]
8622async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8623 cx: &mut TestAppContext,
8624) {
8625 init_test(cx, |_| {});
8626
8627 cx.update(|cx| {
8628 cx.update_global::<SettingsStore, _>(|settings, cx| {
8629 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8630 settings.auto_signature_help = Some(true);
8631 });
8632 });
8633 });
8634
8635 let mut cx = EditorLspTestContext::new_rust(
8636 lsp::ServerCapabilities {
8637 signature_help_provider: Some(lsp::SignatureHelpOptions {
8638 ..Default::default()
8639 }),
8640 ..Default::default()
8641 },
8642 cx,
8643 )
8644 .await;
8645
8646 let language = Language::new(
8647 LanguageConfig {
8648 name: "Rust".into(),
8649 brackets: BracketPairConfig {
8650 pairs: vec![
8651 BracketPair {
8652 start: "{".to_string(),
8653 end: "}".to_string(),
8654 close: true,
8655 surround: true,
8656 newline: true,
8657 },
8658 BracketPair {
8659 start: "(".to_string(),
8660 end: ")".to_string(),
8661 close: true,
8662 surround: true,
8663 newline: true,
8664 },
8665 BracketPair {
8666 start: "/*".to_string(),
8667 end: " */".to_string(),
8668 close: true,
8669 surround: true,
8670 newline: true,
8671 },
8672 BracketPair {
8673 start: "[".to_string(),
8674 end: "]".to_string(),
8675 close: false,
8676 surround: false,
8677 newline: true,
8678 },
8679 BracketPair {
8680 start: "\"".to_string(),
8681 end: "\"".to_string(),
8682 close: true,
8683 surround: true,
8684 newline: false,
8685 },
8686 BracketPair {
8687 start: "<".to_string(),
8688 end: ">".to_string(),
8689 close: false,
8690 surround: true,
8691 newline: true,
8692 },
8693 ],
8694 ..Default::default()
8695 },
8696 autoclose_before: "})]".to_string(),
8697 ..Default::default()
8698 },
8699 Some(tree_sitter_rust::LANGUAGE.into()),
8700 );
8701 let language = Arc::new(language);
8702
8703 cx.language_registry().add(language.clone());
8704 cx.update_buffer(|buffer, cx| {
8705 buffer.set_language(Some(language), cx);
8706 });
8707
8708 cx.set_state(
8709 &r#"
8710 fn main() {
8711 sampleˇ
8712 }
8713 "#
8714 .unindent(),
8715 );
8716
8717 cx.update_editor(|editor, window, cx| {
8718 editor.handle_input("(", window, cx);
8719 });
8720 cx.assert_editor_state(
8721 &"
8722 fn main() {
8723 sample(ˇ)
8724 }
8725 "
8726 .unindent(),
8727 );
8728
8729 let mocked_response = lsp::SignatureHelp {
8730 signatures: vec![lsp::SignatureInformation {
8731 label: "fn sample(param1: u8, param2: u8)".to_string(),
8732 documentation: None,
8733 parameters: Some(vec![
8734 lsp::ParameterInformation {
8735 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8736 documentation: None,
8737 },
8738 lsp::ParameterInformation {
8739 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8740 documentation: None,
8741 },
8742 ]),
8743 active_parameter: None,
8744 }],
8745 active_signature: Some(0),
8746 active_parameter: Some(0),
8747 };
8748 handle_signature_help_request(&mut cx, mocked_response).await;
8749
8750 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8751 .await;
8752
8753 cx.editor(|editor, _, _| {
8754 let signature_help_state = editor.signature_help_state.popover().cloned();
8755 assert_eq!(
8756 signature_help_state.unwrap().label,
8757 "param1: u8, param2: u8"
8758 );
8759 });
8760}
8761
8762#[gpui::test]
8763async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8764 init_test(cx, |_| {});
8765
8766 cx.update(|cx| {
8767 cx.update_global::<SettingsStore, _>(|settings, cx| {
8768 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8769 settings.auto_signature_help = Some(false);
8770 settings.show_signature_help_after_edits = Some(false);
8771 });
8772 });
8773 });
8774
8775 let mut cx = EditorLspTestContext::new_rust(
8776 lsp::ServerCapabilities {
8777 signature_help_provider: Some(lsp::SignatureHelpOptions {
8778 ..Default::default()
8779 }),
8780 ..Default::default()
8781 },
8782 cx,
8783 )
8784 .await;
8785
8786 let language = Language::new(
8787 LanguageConfig {
8788 name: "Rust".into(),
8789 brackets: BracketPairConfig {
8790 pairs: vec![
8791 BracketPair {
8792 start: "{".to_string(),
8793 end: "}".to_string(),
8794 close: true,
8795 surround: true,
8796 newline: true,
8797 },
8798 BracketPair {
8799 start: "(".to_string(),
8800 end: ")".to_string(),
8801 close: true,
8802 surround: true,
8803 newline: true,
8804 },
8805 BracketPair {
8806 start: "/*".to_string(),
8807 end: " */".to_string(),
8808 close: true,
8809 surround: true,
8810 newline: true,
8811 },
8812 BracketPair {
8813 start: "[".to_string(),
8814 end: "]".to_string(),
8815 close: false,
8816 surround: false,
8817 newline: true,
8818 },
8819 BracketPair {
8820 start: "\"".to_string(),
8821 end: "\"".to_string(),
8822 close: true,
8823 surround: true,
8824 newline: false,
8825 },
8826 BracketPair {
8827 start: "<".to_string(),
8828 end: ">".to_string(),
8829 close: false,
8830 surround: true,
8831 newline: true,
8832 },
8833 ],
8834 ..Default::default()
8835 },
8836 autoclose_before: "})]".to_string(),
8837 ..Default::default()
8838 },
8839 Some(tree_sitter_rust::LANGUAGE.into()),
8840 );
8841 let language = Arc::new(language);
8842
8843 cx.language_registry().add(language.clone());
8844 cx.update_buffer(|buffer, cx| {
8845 buffer.set_language(Some(language), cx);
8846 });
8847
8848 // Ensure that signature_help is not called when no signature help is enabled.
8849 cx.set_state(
8850 &r#"
8851 fn main() {
8852 sampleˇ
8853 }
8854 "#
8855 .unindent(),
8856 );
8857 cx.update_editor(|editor, window, cx| {
8858 editor.handle_input("(", window, cx);
8859 });
8860 cx.assert_editor_state(
8861 &"
8862 fn main() {
8863 sample(ˇ)
8864 }
8865 "
8866 .unindent(),
8867 );
8868 cx.editor(|editor, _, _| {
8869 assert!(editor.signature_help_state.task().is_none());
8870 });
8871
8872 let mocked_response = lsp::SignatureHelp {
8873 signatures: vec![lsp::SignatureInformation {
8874 label: "fn sample(param1: u8, param2: u8)".to_string(),
8875 documentation: None,
8876 parameters: Some(vec![
8877 lsp::ParameterInformation {
8878 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8879 documentation: None,
8880 },
8881 lsp::ParameterInformation {
8882 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8883 documentation: None,
8884 },
8885 ]),
8886 active_parameter: None,
8887 }],
8888 active_signature: Some(0),
8889 active_parameter: Some(0),
8890 };
8891
8892 // Ensure that signature_help is called when enabled afte edits
8893 cx.update(|_, cx| {
8894 cx.update_global::<SettingsStore, _>(|settings, cx| {
8895 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8896 settings.auto_signature_help = Some(false);
8897 settings.show_signature_help_after_edits = Some(true);
8898 });
8899 });
8900 });
8901 cx.set_state(
8902 &r#"
8903 fn main() {
8904 sampleˇ
8905 }
8906 "#
8907 .unindent(),
8908 );
8909 cx.update_editor(|editor, window, cx| {
8910 editor.handle_input("(", window, cx);
8911 });
8912 cx.assert_editor_state(
8913 &"
8914 fn main() {
8915 sample(ˇ)
8916 }
8917 "
8918 .unindent(),
8919 );
8920 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8921 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8922 .await;
8923 cx.update_editor(|editor, _, _| {
8924 let signature_help_state = editor.signature_help_state.popover().cloned();
8925 assert!(signature_help_state.is_some());
8926 assert_eq!(
8927 signature_help_state.unwrap().label,
8928 "param1: u8, param2: u8"
8929 );
8930 editor.signature_help_state = SignatureHelpState::default();
8931 });
8932
8933 // Ensure that signature_help is called when auto signature help override is enabled
8934 cx.update(|_, cx| {
8935 cx.update_global::<SettingsStore, _>(|settings, cx| {
8936 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8937 settings.auto_signature_help = Some(true);
8938 settings.show_signature_help_after_edits = Some(false);
8939 });
8940 });
8941 });
8942 cx.set_state(
8943 &r#"
8944 fn main() {
8945 sampleˇ
8946 }
8947 "#
8948 .unindent(),
8949 );
8950 cx.update_editor(|editor, window, cx| {
8951 editor.handle_input("(", window, cx);
8952 });
8953 cx.assert_editor_state(
8954 &"
8955 fn main() {
8956 sample(ˇ)
8957 }
8958 "
8959 .unindent(),
8960 );
8961 handle_signature_help_request(&mut cx, mocked_response).await;
8962 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8963 .await;
8964 cx.editor(|editor, _, _| {
8965 let signature_help_state = editor.signature_help_state.popover().cloned();
8966 assert!(signature_help_state.is_some());
8967 assert_eq!(
8968 signature_help_state.unwrap().label,
8969 "param1: u8, param2: u8"
8970 );
8971 });
8972}
8973
8974#[gpui::test]
8975async fn test_signature_help(cx: &mut TestAppContext) {
8976 init_test(cx, |_| {});
8977 cx.update(|cx| {
8978 cx.update_global::<SettingsStore, _>(|settings, cx| {
8979 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8980 settings.auto_signature_help = Some(true);
8981 });
8982 });
8983 });
8984
8985 let mut cx = EditorLspTestContext::new_rust(
8986 lsp::ServerCapabilities {
8987 signature_help_provider: Some(lsp::SignatureHelpOptions {
8988 ..Default::default()
8989 }),
8990 ..Default::default()
8991 },
8992 cx,
8993 )
8994 .await;
8995
8996 // A test that directly calls `show_signature_help`
8997 cx.update_editor(|editor, window, cx| {
8998 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8999 });
9000
9001 let mocked_response = lsp::SignatureHelp {
9002 signatures: vec![lsp::SignatureInformation {
9003 label: "fn sample(param1: u8, param2: u8)".to_string(),
9004 documentation: None,
9005 parameters: Some(vec![
9006 lsp::ParameterInformation {
9007 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9008 documentation: None,
9009 },
9010 lsp::ParameterInformation {
9011 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9012 documentation: None,
9013 },
9014 ]),
9015 active_parameter: None,
9016 }],
9017 active_signature: Some(0),
9018 active_parameter: Some(0),
9019 };
9020 handle_signature_help_request(&mut cx, mocked_response).await;
9021
9022 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9023 .await;
9024
9025 cx.editor(|editor, _, _| {
9026 let signature_help_state = editor.signature_help_state.popover().cloned();
9027 assert!(signature_help_state.is_some());
9028 assert_eq!(
9029 signature_help_state.unwrap().label,
9030 "param1: u8, param2: u8"
9031 );
9032 });
9033
9034 // When exiting outside from inside the brackets, `signature_help` is closed.
9035 cx.set_state(indoc! {"
9036 fn main() {
9037 sample(ˇ);
9038 }
9039
9040 fn sample(param1: u8, param2: u8) {}
9041 "});
9042
9043 cx.update_editor(|editor, window, cx| {
9044 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9045 });
9046
9047 let mocked_response = lsp::SignatureHelp {
9048 signatures: Vec::new(),
9049 active_signature: None,
9050 active_parameter: None,
9051 };
9052 handle_signature_help_request(&mut cx, mocked_response).await;
9053
9054 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9055 .await;
9056
9057 cx.editor(|editor, _, _| {
9058 assert!(!editor.signature_help_state.is_shown());
9059 });
9060
9061 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9062 cx.set_state(indoc! {"
9063 fn main() {
9064 sample(ˇ);
9065 }
9066
9067 fn sample(param1: u8, param2: u8) {}
9068 "});
9069
9070 let mocked_response = lsp::SignatureHelp {
9071 signatures: vec![lsp::SignatureInformation {
9072 label: "fn sample(param1: u8, param2: u8)".to_string(),
9073 documentation: None,
9074 parameters: Some(vec![
9075 lsp::ParameterInformation {
9076 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9077 documentation: None,
9078 },
9079 lsp::ParameterInformation {
9080 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9081 documentation: None,
9082 },
9083 ]),
9084 active_parameter: None,
9085 }],
9086 active_signature: Some(0),
9087 active_parameter: Some(0),
9088 };
9089 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9090 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9091 .await;
9092 cx.editor(|editor, _, _| {
9093 assert!(editor.signature_help_state.is_shown());
9094 });
9095
9096 // Restore the popover with more parameter input
9097 cx.set_state(indoc! {"
9098 fn main() {
9099 sample(param1, param2ˇ);
9100 }
9101
9102 fn sample(param1: u8, param2: u8) {}
9103 "});
9104
9105 let mocked_response = lsp::SignatureHelp {
9106 signatures: vec![lsp::SignatureInformation {
9107 label: "fn sample(param1: u8, param2: u8)".to_string(),
9108 documentation: None,
9109 parameters: Some(vec![
9110 lsp::ParameterInformation {
9111 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9112 documentation: None,
9113 },
9114 lsp::ParameterInformation {
9115 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9116 documentation: None,
9117 },
9118 ]),
9119 active_parameter: None,
9120 }],
9121 active_signature: Some(0),
9122 active_parameter: Some(1),
9123 };
9124 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9125 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9126 .await;
9127
9128 // When selecting a range, the popover is gone.
9129 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9130 cx.update_editor(|editor, window, cx| {
9131 editor.change_selections(None, window, cx, |s| {
9132 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9133 })
9134 });
9135 cx.assert_editor_state(indoc! {"
9136 fn main() {
9137 sample(param1, «ˇparam2»);
9138 }
9139
9140 fn sample(param1: u8, param2: u8) {}
9141 "});
9142 cx.editor(|editor, _, _| {
9143 assert!(!editor.signature_help_state.is_shown());
9144 });
9145
9146 // When unselecting again, the popover is back if within the brackets.
9147 cx.update_editor(|editor, window, cx| {
9148 editor.change_selections(None, window, cx, |s| {
9149 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9150 })
9151 });
9152 cx.assert_editor_state(indoc! {"
9153 fn main() {
9154 sample(param1, ˇparam2);
9155 }
9156
9157 fn sample(param1: u8, param2: u8) {}
9158 "});
9159 handle_signature_help_request(&mut cx, mocked_response).await;
9160 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9161 .await;
9162 cx.editor(|editor, _, _| {
9163 assert!(editor.signature_help_state.is_shown());
9164 });
9165
9166 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9167 cx.update_editor(|editor, window, cx| {
9168 editor.change_selections(None, window, cx, |s| {
9169 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9170 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9171 })
9172 });
9173 cx.assert_editor_state(indoc! {"
9174 fn main() {
9175 sample(param1, ˇparam2);
9176 }
9177
9178 fn sample(param1: u8, param2: u8) {}
9179 "});
9180
9181 let mocked_response = lsp::SignatureHelp {
9182 signatures: vec![lsp::SignatureInformation {
9183 label: "fn sample(param1: u8, param2: u8)".to_string(),
9184 documentation: None,
9185 parameters: Some(vec![
9186 lsp::ParameterInformation {
9187 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9188 documentation: None,
9189 },
9190 lsp::ParameterInformation {
9191 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9192 documentation: None,
9193 },
9194 ]),
9195 active_parameter: None,
9196 }],
9197 active_signature: Some(0),
9198 active_parameter: Some(1),
9199 };
9200 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9201 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9202 .await;
9203 cx.update_editor(|editor, _, cx| {
9204 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9205 });
9206 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9207 .await;
9208 cx.update_editor(|editor, window, cx| {
9209 editor.change_selections(None, window, cx, |s| {
9210 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9211 })
9212 });
9213 cx.assert_editor_state(indoc! {"
9214 fn main() {
9215 sample(param1, «ˇparam2»);
9216 }
9217
9218 fn sample(param1: u8, param2: u8) {}
9219 "});
9220 cx.update_editor(|editor, window, cx| {
9221 editor.change_selections(None, window, cx, |s| {
9222 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9223 })
9224 });
9225 cx.assert_editor_state(indoc! {"
9226 fn main() {
9227 sample(param1, ˇparam2);
9228 }
9229
9230 fn sample(param1: u8, param2: u8) {}
9231 "});
9232 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9233 .await;
9234}
9235
9236#[gpui::test]
9237async fn test_completion_mode(cx: &mut TestAppContext) {
9238 init_test(cx, |_| {});
9239 let mut cx = EditorLspTestContext::new_rust(
9240 lsp::ServerCapabilities {
9241 completion_provider: Some(lsp::CompletionOptions {
9242 resolve_provider: Some(true),
9243 ..Default::default()
9244 }),
9245 ..Default::default()
9246 },
9247 cx,
9248 )
9249 .await;
9250
9251 struct Run {
9252 run_description: &'static str,
9253 initial_state: String,
9254 buffer_marked_text: String,
9255 completion_text: &'static str,
9256 expected_with_insert_mode: String,
9257 expected_with_replace_mode: String,
9258 expected_with_replace_subsequence_mode: String,
9259 expected_with_replace_suffix_mode: String,
9260 }
9261
9262 let runs = [
9263 Run {
9264 run_description: "Start of word matches completion text",
9265 initial_state: "before ediˇ after".into(),
9266 buffer_marked_text: "before <edi|> after".into(),
9267 completion_text: "editor",
9268 expected_with_insert_mode: "before editorˇ after".into(),
9269 expected_with_replace_mode: "before editorˇ after".into(),
9270 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9271 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9272 },
9273 Run {
9274 run_description: "Accept same text at the middle of the word",
9275 initial_state: "before ediˇtor after".into(),
9276 buffer_marked_text: "before <edi|tor> after".into(),
9277 completion_text: "editor",
9278 expected_with_insert_mode: "before editorˇtor after".into(),
9279 expected_with_replace_mode: "before ediˇtor after".into(),
9280 expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
9281 expected_with_replace_suffix_mode: "before ediˇtor after".into(),
9282 },
9283 Run {
9284 run_description: "End of word matches completion text -- cursor at end",
9285 initial_state: "before torˇ after".into(),
9286 buffer_marked_text: "before <tor|> after".into(),
9287 completion_text: "editor",
9288 expected_with_insert_mode: "before editorˇ after".into(),
9289 expected_with_replace_mode: "before editorˇ after".into(),
9290 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9291 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9292 },
9293 Run {
9294 run_description: "End of word matches completion text -- cursor at start",
9295 initial_state: "before ˇtor after".into(),
9296 buffer_marked_text: "before <|tor> after".into(),
9297 completion_text: "editor",
9298 expected_with_insert_mode: "before editorˇtor after".into(),
9299 expected_with_replace_mode: "before editorˇ after".into(),
9300 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9301 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9302 },
9303 Run {
9304 run_description: "Prepend text containing whitespace",
9305 initial_state: "pˇfield: bool".into(),
9306 buffer_marked_text: "<p|field>: bool".into(),
9307 completion_text: "pub ",
9308 expected_with_insert_mode: "pub ˇfield: bool".into(),
9309 expected_with_replace_mode: "pub ˇ: bool".into(),
9310 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9311 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9312 },
9313 Run {
9314 run_description: "Add element to start of list",
9315 initial_state: "[element_ˇelement_2]".into(),
9316 buffer_marked_text: "[<element_|element_2>]".into(),
9317 completion_text: "element_1",
9318 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9319 expected_with_replace_mode: "[element_1ˇ]".into(),
9320 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9321 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9322 },
9323 Run {
9324 run_description: "Add element to start of list -- first and second elements are equal",
9325 initial_state: "[elˇelement]".into(),
9326 buffer_marked_text: "[<el|element>]".into(),
9327 completion_text: "element",
9328 expected_with_insert_mode: "[elementˇelement]".into(),
9329 expected_with_replace_mode: "[elˇement]".into(),
9330 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9331 expected_with_replace_suffix_mode: "[elˇement]".into(),
9332 },
9333 Run {
9334 run_description: "Ends with matching suffix",
9335 initial_state: "SubˇError".into(),
9336 buffer_marked_text: "<Sub|Error>".into(),
9337 completion_text: "SubscriptionError",
9338 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
9339 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9340 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9341 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9342 },
9343 Run {
9344 run_description: "Suffix is a subsequence -- contiguous",
9345 initial_state: "SubˇErr".into(),
9346 buffer_marked_text: "<Sub|Err>".into(),
9347 completion_text: "SubscriptionError",
9348 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
9349 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9350 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9351 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9352 },
9353 Run {
9354 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9355 initial_state: "Suˇscrirr".into(),
9356 buffer_marked_text: "<Su|scrirr>".into(),
9357 completion_text: "SubscriptionError",
9358 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
9359 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9360 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9361 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9362 },
9363 Run {
9364 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9365 initial_state: "foo(indˇix)".into(),
9366 buffer_marked_text: "foo(<ind|ix>)".into(),
9367 completion_text: "node_index",
9368 expected_with_insert_mode: "foo(node_indexˇix)".into(),
9369 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9370 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9371 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9372 },
9373 ];
9374
9375 for run in runs {
9376 let run_variations = [
9377 (LspInsertMode::Insert, run.expected_with_insert_mode),
9378 (LspInsertMode::Replace, run.expected_with_replace_mode),
9379 (
9380 LspInsertMode::ReplaceSubsequence,
9381 run.expected_with_replace_subsequence_mode,
9382 ),
9383 (
9384 LspInsertMode::ReplaceSuffix,
9385 run.expected_with_replace_suffix_mode,
9386 ),
9387 ];
9388
9389 for (lsp_insert_mode, expected_text) in run_variations {
9390 eprintln!(
9391 "run = {:?}, mode = {lsp_insert_mode:.?}",
9392 run.run_description,
9393 );
9394
9395 update_test_language_settings(&mut cx, |settings| {
9396 settings.defaults.completions = Some(CompletionSettings {
9397 lsp_insert_mode,
9398 words: WordsCompletionMode::Disabled,
9399 lsp: true,
9400 lsp_fetch_timeout_ms: 0,
9401 });
9402 });
9403
9404 cx.set_state(&run.initial_state);
9405 cx.update_editor(|editor, window, cx| {
9406 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9407 });
9408
9409 let counter = Arc::new(AtomicUsize::new(0));
9410 handle_completion_request_with_insert_and_replace(
9411 &mut cx,
9412 &run.buffer_marked_text,
9413 vec![run.completion_text],
9414 counter.clone(),
9415 )
9416 .await;
9417 cx.condition(|editor, _| editor.context_menu_visible())
9418 .await;
9419 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9420
9421 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9422 editor
9423 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9424 .unwrap()
9425 });
9426 cx.assert_editor_state(&expected_text);
9427 handle_resolve_completion_request(&mut cx, None).await;
9428 apply_additional_edits.await.unwrap();
9429 }
9430 }
9431}
9432
9433#[gpui::test]
9434async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
9435 init_test(cx, |_| {});
9436 let mut cx = EditorLspTestContext::new_rust(
9437 lsp::ServerCapabilities {
9438 completion_provider: Some(lsp::CompletionOptions {
9439 resolve_provider: Some(true),
9440 ..Default::default()
9441 }),
9442 ..Default::default()
9443 },
9444 cx,
9445 )
9446 .await;
9447
9448 let initial_state = "SubˇError";
9449 let buffer_marked_text = "<Sub|Error>";
9450 let completion_text = "SubscriptionError";
9451 let expected_with_insert_mode = "SubscriptionErrorˇError";
9452 let expected_with_replace_mode = "SubscriptionErrorˇ";
9453
9454 update_test_language_settings(&mut cx, |settings| {
9455 settings.defaults.completions = Some(CompletionSettings {
9456 words: WordsCompletionMode::Disabled,
9457 // set the opposite here to ensure that the action is overriding the default behavior
9458 lsp_insert_mode: LspInsertMode::Insert,
9459 lsp: true,
9460 lsp_fetch_timeout_ms: 0,
9461 });
9462 });
9463
9464 cx.set_state(initial_state);
9465 cx.update_editor(|editor, window, cx| {
9466 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9467 });
9468
9469 let counter = Arc::new(AtomicUsize::new(0));
9470 handle_completion_request_with_insert_and_replace(
9471 &mut cx,
9472 &buffer_marked_text,
9473 vec![completion_text],
9474 counter.clone(),
9475 )
9476 .await;
9477 cx.condition(|editor, _| editor.context_menu_visible())
9478 .await;
9479 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9480
9481 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9482 editor
9483 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
9484 .unwrap()
9485 });
9486 cx.assert_editor_state(&expected_with_replace_mode);
9487 handle_resolve_completion_request(&mut cx, None).await;
9488 apply_additional_edits.await.unwrap();
9489
9490 update_test_language_settings(&mut cx, |settings| {
9491 settings.defaults.completions = Some(CompletionSettings {
9492 words: WordsCompletionMode::Disabled,
9493 // set the opposite here to ensure that the action is overriding the default behavior
9494 lsp_insert_mode: LspInsertMode::Replace,
9495 lsp: true,
9496 lsp_fetch_timeout_ms: 0,
9497 });
9498 });
9499
9500 cx.set_state(initial_state);
9501 cx.update_editor(|editor, window, cx| {
9502 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9503 });
9504 handle_completion_request_with_insert_and_replace(
9505 &mut cx,
9506 &buffer_marked_text,
9507 vec![completion_text],
9508 counter.clone(),
9509 )
9510 .await;
9511 cx.condition(|editor, _| editor.context_menu_visible())
9512 .await;
9513 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9514
9515 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9516 editor
9517 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
9518 .unwrap()
9519 });
9520 cx.assert_editor_state(&expected_with_insert_mode);
9521 handle_resolve_completion_request(&mut cx, None).await;
9522 apply_additional_edits.await.unwrap();
9523}
9524
9525#[gpui::test]
9526async fn test_completion(cx: &mut TestAppContext) {
9527 init_test(cx, |_| {});
9528
9529 let mut cx = EditorLspTestContext::new_rust(
9530 lsp::ServerCapabilities {
9531 completion_provider: Some(lsp::CompletionOptions {
9532 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9533 resolve_provider: Some(true),
9534 ..Default::default()
9535 }),
9536 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9537 ..Default::default()
9538 },
9539 cx,
9540 )
9541 .await;
9542 let counter = Arc::new(AtomicUsize::new(0));
9543
9544 cx.set_state(indoc! {"
9545 oneˇ
9546 two
9547 three
9548 "});
9549 cx.simulate_keystroke(".");
9550 handle_completion_request(
9551 &mut cx,
9552 indoc! {"
9553 one.|<>
9554 two
9555 three
9556 "},
9557 vec!["first_completion", "second_completion"],
9558 counter.clone(),
9559 )
9560 .await;
9561 cx.condition(|editor, _| editor.context_menu_visible())
9562 .await;
9563 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9564
9565 let _handler = handle_signature_help_request(
9566 &mut cx,
9567 lsp::SignatureHelp {
9568 signatures: vec![lsp::SignatureInformation {
9569 label: "test signature".to_string(),
9570 documentation: None,
9571 parameters: Some(vec![lsp::ParameterInformation {
9572 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9573 documentation: None,
9574 }]),
9575 active_parameter: None,
9576 }],
9577 active_signature: None,
9578 active_parameter: None,
9579 },
9580 );
9581 cx.update_editor(|editor, window, cx| {
9582 assert!(
9583 !editor.signature_help_state.is_shown(),
9584 "No signature help was called for"
9585 );
9586 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9587 });
9588 cx.run_until_parked();
9589 cx.update_editor(|editor, _, _| {
9590 assert!(
9591 !editor.signature_help_state.is_shown(),
9592 "No signature help should be shown when completions menu is open"
9593 );
9594 });
9595
9596 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9597 editor.context_menu_next(&Default::default(), window, cx);
9598 editor
9599 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9600 .unwrap()
9601 });
9602 cx.assert_editor_state(indoc! {"
9603 one.second_completionˇ
9604 two
9605 three
9606 "});
9607
9608 handle_resolve_completion_request(
9609 &mut cx,
9610 Some(vec![
9611 (
9612 //This overlaps with the primary completion edit which is
9613 //misbehavior from the LSP spec, test that we filter it out
9614 indoc! {"
9615 one.second_ˇcompletion
9616 two
9617 threeˇ
9618 "},
9619 "overlapping additional edit",
9620 ),
9621 (
9622 indoc! {"
9623 one.second_completion
9624 two
9625 threeˇ
9626 "},
9627 "\nadditional edit",
9628 ),
9629 ]),
9630 )
9631 .await;
9632 apply_additional_edits.await.unwrap();
9633 cx.assert_editor_state(indoc! {"
9634 one.second_completionˇ
9635 two
9636 three
9637 additional edit
9638 "});
9639
9640 cx.set_state(indoc! {"
9641 one.second_completion
9642 twoˇ
9643 threeˇ
9644 additional edit
9645 "});
9646 cx.simulate_keystroke(" ");
9647 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9648 cx.simulate_keystroke("s");
9649 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9650
9651 cx.assert_editor_state(indoc! {"
9652 one.second_completion
9653 two sˇ
9654 three sˇ
9655 additional edit
9656 "});
9657 handle_completion_request(
9658 &mut cx,
9659 indoc! {"
9660 one.second_completion
9661 two s
9662 three <s|>
9663 additional edit
9664 "},
9665 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9666 counter.clone(),
9667 )
9668 .await;
9669 cx.condition(|editor, _| editor.context_menu_visible())
9670 .await;
9671 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9672
9673 cx.simulate_keystroke("i");
9674
9675 handle_completion_request(
9676 &mut cx,
9677 indoc! {"
9678 one.second_completion
9679 two si
9680 three <si|>
9681 additional edit
9682 "},
9683 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9684 counter.clone(),
9685 )
9686 .await;
9687 cx.condition(|editor, _| editor.context_menu_visible())
9688 .await;
9689 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9690
9691 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9692 editor
9693 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9694 .unwrap()
9695 });
9696 cx.assert_editor_state(indoc! {"
9697 one.second_completion
9698 two sixth_completionˇ
9699 three sixth_completionˇ
9700 additional edit
9701 "});
9702
9703 apply_additional_edits.await.unwrap();
9704
9705 update_test_language_settings(&mut cx, |settings| {
9706 settings.defaults.show_completions_on_input = Some(false);
9707 });
9708 cx.set_state("editorˇ");
9709 cx.simulate_keystroke(".");
9710 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9711 cx.simulate_keystrokes("c l o");
9712 cx.assert_editor_state("editor.cloˇ");
9713 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9714 cx.update_editor(|editor, window, cx| {
9715 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9716 });
9717 handle_completion_request(
9718 &mut cx,
9719 "editor.<clo|>",
9720 vec!["close", "clobber"],
9721 counter.clone(),
9722 )
9723 .await;
9724 cx.condition(|editor, _| editor.context_menu_visible())
9725 .await;
9726 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9727
9728 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9729 editor
9730 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9731 .unwrap()
9732 });
9733 cx.assert_editor_state("editor.closeˇ");
9734 handle_resolve_completion_request(&mut cx, None).await;
9735 apply_additional_edits.await.unwrap();
9736}
9737
9738#[gpui::test]
9739async fn test_word_completion(cx: &mut TestAppContext) {
9740 let lsp_fetch_timeout_ms = 10;
9741 init_test(cx, |language_settings| {
9742 language_settings.defaults.completions = Some(CompletionSettings {
9743 words: WordsCompletionMode::Fallback,
9744 lsp: true,
9745 lsp_fetch_timeout_ms: 10,
9746 lsp_insert_mode: LspInsertMode::Insert,
9747 });
9748 });
9749
9750 let mut cx = EditorLspTestContext::new_rust(
9751 lsp::ServerCapabilities {
9752 completion_provider: Some(lsp::CompletionOptions {
9753 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9754 ..lsp::CompletionOptions::default()
9755 }),
9756 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9757 ..lsp::ServerCapabilities::default()
9758 },
9759 cx,
9760 )
9761 .await;
9762
9763 let throttle_completions = Arc::new(AtomicBool::new(false));
9764
9765 let lsp_throttle_completions = throttle_completions.clone();
9766 let _completion_requests_handler =
9767 cx.lsp
9768 .server
9769 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9770 let lsp_throttle_completions = lsp_throttle_completions.clone();
9771 let cx = cx.clone();
9772 async move {
9773 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9774 cx.background_executor()
9775 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9776 .await;
9777 }
9778 Ok(Some(lsp::CompletionResponse::Array(vec![
9779 lsp::CompletionItem {
9780 label: "first".into(),
9781 ..lsp::CompletionItem::default()
9782 },
9783 lsp::CompletionItem {
9784 label: "last".into(),
9785 ..lsp::CompletionItem::default()
9786 },
9787 ])))
9788 }
9789 });
9790
9791 cx.set_state(indoc! {"
9792 oneˇ
9793 two
9794 three
9795 "});
9796 cx.simulate_keystroke(".");
9797 cx.executor().run_until_parked();
9798 cx.condition(|editor, _| editor.context_menu_visible())
9799 .await;
9800 cx.update_editor(|editor, window, cx| {
9801 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9802 {
9803 assert_eq!(
9804 completion_menu_entries(&menu),
9805 &["first", "last"],
9806 "When LSP server is fast to reply, no fallback word completions are used"
9807 );
9808 } else {
9809 panic!("expected completion menu to be open");
9810 }
9811 editor.cancel(&Cancel, window, cx);
9812 });
9813 cx.executor().run_until_parked();
9814 cx.condition(|editor, _| !editor.context_menu_visible())
9815 .await;
9816
9817 throttle_completions.store(true, atomic::Ordering::Release);
9818 cx.simulate_keystroke(".");
9819 cx.executor()
9820 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9821 cx.executor().run_until_parked();
9822 cx.condition(|editor, _| editor.context_menu_visible())
9823 .await;
9824 cx.update_editor(|editor, _, _| {
9825 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9826 {
9827 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9828 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9829 } else {
9830 panic!("expected completion menu to be open");
9831 }
9832 });
9833}
9834
9835#[gpui::test]
9836async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9837 init_test(cx, |language_settings| {
9838 language_settings.defaults.completions = Some(CompletionSettings {
9839 words: WordsCompletionMode::Enabled,
9840 lsp: true,
9841 lsp_fetch_timeout_ms: 0,
9842 lsp_insert_mode: LspInsertMode::Insert,
9843 });
9844 });
9845
9846 let mut cx = EditorLspTestContext::new_rust(
9847 lsp::ServerCapabilities {
9848 completion_provider: Some(lsp::CompletionOptions {
9849 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9850 ..lsp::CompletionOptions::default()
9851 }),
9852 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9853 ..lsp::ServerCapabilities::default()
9854 },
9855 cx,
9856 )
9857 .await;
9858
9859 let _completion_requests_handler =
9860 cx.lsp
9861 .server
9862 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9863 Ok(Some(lsp::CompletionResponse::Array(vec![
9864 lsp::CompletionItem {
9865 label: "first".into(),
9866 ..lsp::CompletionItem::default()
9867 },
9868 lsp::CompletionItem {
9869 label: "last".into(),
9870 ..lsp::CompletionItem::default()
9871 },
9872 ])))
9873 });
9874
9875 cx.set_state(indoc! {"ˇ
9876 first
9877 last
9878 second
9879 "});
9880 cx.simulate_keystroke(".");
9881 cx.executor().run_until_parked();
9882 cx.condition(|editor, _| editor.context_menu_visible())
9883 .await;
9884 cx.update_editor(|editor, _, _| {
9885 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9886 {
9887 assert_eq!(
9888 completion_menu_entries(&menu),
9889 &["first", "last", "second"],
9890 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9891 );
9892 } else {
9893 panic!("expected completion menu to be open");
9894 }
9895 });
9896}
9897
9898#[gpui::test]
9899async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9900 init_test(cx, |language_settings| {
9901 language_settings.defaults.completions = Some(CompletionSettings {
9902 words: WordsCompletionMode::Disabled,
9903 lsp: true,
9904 lsp_fetch_timeout_ms: 0,
9905 lsp_insert_mode: LspInsertMode::Insert,
9906 });
9907 });
9908
9909 let mut cx = EditorLspTestContext::new_rust(
9910 lsp::ServerCapabilities {
9911 completion_provider: Some(lsp::CompletionOptions {
9912 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9913 ..lsp::CompletionOptions::default()
9914 }),
9915 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9916 ..lsp::ServerCapabilities::default()
9917 },
9918 cx,
9919 )
9920 .await;
9921
9922 let _completion_requests_handler =
9923 cx.lsp
9924 .server
9925 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9926 panic!("LSP completions should not be queried when dealing with word completions")
9927 });
9928
9929 cx.set_state(indoc! {"ˇ
9930 first
9931 last
9932 second
9933 "});
9934 cx.update_editor(|editor, window, cx| {
9935 editor.show_word_completions(&ShowWordCompletions, window, cx);
9936 });
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 "`ShowWordCompletions` action should show word completions"
9947 );
9948 } else {
9949 panic!("expected completion menu to be open");
9950 }
9951 });
9952
9953 cx.simulate_keystroke("l");
9954 cx.executor().run_until_parked();
9955 cx.condition(|editor, _| editor.context_menu_visible())
9956 .await;
9957 cx.update_editor(|editor, _, _| {
9958 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9959 {
9960 assert_eq!(
9961 completion_menu_entries(&menu),
9962 &["last"],
9963 "After showing word completions, further editing should filter them and not query the LSP"
9964 );
9965 } else {
9966 panic!("expected completion menu to be open");
9967 }
9968 });
9969}
9970
9971#[gpui::test]
9972async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9973 init_test(cx, |language_settings| {
9974 language_settings.defaults.completions = Some(CompletionSettings {
9975 words: WordsCompletionMode::Fallback,
9976 lsp: false,
9977 lsp_fetch_timeout_ms: 0,
9978 lsp_insert_mode: LspInsertMode::Insert,
9979 });
9980 });
9981
9982 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9983
9984 cx.set_state(indoc! {"ˇ
9985 0_usize
9986 let
9987 33
9988 4.5f32
9989 "});
9990 cx.update_editor(|editor, window, cx| {
9991 editor.show_completions(&ShowCompletions::default(), window, cx);
9992 });
9993 cx.executor().run_until_parked();
9994 cx.condition(|editor, _| editor.context_menu_visible())
9995 .await;
9996 cx.update_editor(|editor, window, cx| {
9997 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9998 {
9999 assert_eq!(
10000 completion_menu_entries(&menu),
10001 &["let"],
10002 "With no digits in the completion query, no digits should be in the word completions"
10003 );
10004 } else {
10005 panic!("expected completion menu to be open");
10006 }
10007 editor.cancel(&Cancel, window, cx);
10008 });
10009
10010 cx.set_state(indoc! {"3ˇ
10011 0_usize
10012 let
10013 3
10014 33.35f32
10015 "});
10016 cx.update_editor(|editor, window, cx| {
10017 editor.show_completions(&ShowCompletions::default(), window, cx);
10018 });
10019 cx.executor().run_until_parked();
10020 cx.condition(|editor, _| editor.context_menu_visible())
10021 .await;
10022 cx.update_editor(|editor, _, _| {
10023 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10024 {
10025 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
10026 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
10027 } else {
10028 panic!("expected completion menu to be open");
10029 }
10030 });
10031}
10032
10033fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
10034 let position = || lsp::Position {
10035 line: params.text_document_position.position.line,
10036 character: params.text_document_position.position.character,
10037 };
10038 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10039 range: lsp::Range {
10040 start: position(),
10041 end: position(),
10042 },
10043 new_text: text.to_string(),
10044 }))
10045}
10046
10047#[gpui::test]
10048async fn test_multiline_completion(cx: &mut TestAppContext) {
10049 init_test(cx, |_| {});
10050
10051 let fs = FakeFs::new(cx.executor());
10052 fs.insert_tree(
10053 path!("/a"),
10054 json!({
10055 "main.ts": "a",
10056 }),
10057 )
10058 .await;
10059
10060 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10061 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10062 let typescript_language = Arc::new(Language::new(
10063 LanguageConfig {
10064 name: "TypeScript".into(),
10065 matcher: LanguageMatcher {
10066 path_suffixes: vec!["ts".to_string()],
10067 ..LanguageMatcher::default()
10068 },
10069 line_comments: vec!["// ".into()],
10070 ..LanguageConfig::default()
10071 },
10072 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10073 ));
10074 language_registry.add(typescript_language.clone());
10075 let mut fake_servers = language_registry.register_fake_lsp(
10076 "TypeScript",
10077 FakeLspAdapter {
10078 capabilities: lsp::ServerCapabilities {
10079 completion_provider: Some(lsp::CompletionOptions {
10080 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10081 ..lsp::CompletionOptions::default()
10082 }),
10083 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10084 ..lsp::ServerCapabilities::default()
10085 },
10086 // Emulate vtsls label generation
10087 label_for_completion: Some(Box::new(|item, _| {
10088 let text = if let Some(description) = item
10089 .label_details
10090 .as_ref()
10091 .and_then(|label_details| label_details.description.as_ref())
10092 {
10093 format!("{} {}", item.label, description)
10094 } else if let Some(detail) = &item.detail {
10095 format!("{} {}", item.label, detail)
10096 } else {
10097 item.label.clone()
10098 };
10099 let len = text.len();
10100 Some(language::CodeLabel {
10101 text,
10102 runs: Vec::new(),
10103 filter_range: 0..len,
10104 })
10105 })),
10106 ..FakeLspAdapter::default()
10107 },
10108 );
10109 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10110 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10111 let worktree_id = workspace
10112 .update(cx, |workspace, _window, cx| {
10113 workspace.project().update(cx, |project, cx| {
10114 project.worktrees(cx).next().unwrap().read(cx).id()
10115 })
10116 })
10117 .unwrap();
10118 let _buffer = project
10119 .update(cx, |project, cx| {
10120 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
10121 })
10122 .await
10123 .unwrap();
10124 let editor = workspace
10125 .update(cx, |workspace, window, cx| {
10126 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10127 })
10128 .unwrap()
10129 .await
10130 .unwrap()
10131 .downcast::<Editor>()
10132 .unwrap();
10133 let fake_server = fake_servers.next().await.unwrap();
10134
10135 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10136 let multiline_label_2 = "a\nb\nc\n";
10137 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10138 let multiline_description = "d\ne\nf\n";
10139 let multiline_detail_2 = "g\nh\ni\n";
10140
10141 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10142 move |params, _| async move {
10143 Ok(Some(lsp::CompletionResponse::Array(vec![
10144 lsp::CompletionItem {
10145 label: multiline_label.to_string(),
10146 text_edit: gen_text_edit(¶ms, "new_text_1"),
10147 ..lsp::CompletionItem::default()
10148 },
10149 lsp::CompletionItem {
10150 label: "single line label 1".to_string(),
10151 detail: Some(multiline_detail.to_string()),
10152 text_edit: gen_text_edit(¶ms, "new_text_2"),
10153 ..lsp::CompletionItem::default()
10154 },
10155 lsp::CompletionItem {
10156 label: "single line label 2".to_string(),
10157 label_details: Some(lsp::CompletionItemLabelDetails {
10158 description: Some(multiline_description.to_string()),
10159 detail: None,
10160 }),
10161 text_edit: gen_text_edit(¶ms, "new_text_2"),
10162 ..lsp::CompletionItem::default()
10163 },
10164 lsp::CompletionItem {
10165 label: multiline_label_2.to_string(),
10166 detail: Some(multiline_detail_2.to_string()),
10167 text_edit: gen_text_edit(¶ms, "new_text_3"),
10168 ..lsp::CompletionItem::default()
10169 },
10170 lsp::CompletionItem {
10171 label: "Label with many spaces and \t but without newlines".to_string(),
10172 detail: Some(
10173 "Details with many spaces and \t but without newlines".to_string(),
10174 ),
10175 text_edit: gen_text_edit(¶ms, "new_text_4"),
10176 ..lsp::CompletionItem::default()
10177 },
10178 ])))
10179 },
10180 );
10181
10182 editor.update_in(cx, |editor, window, cx| {
10183 cx.focus_self(window);
10184 editor.move_to_end(&MoveToEnd, window, cx);
10185 editor.handle_input(".", window, cx);
10186 });
10187 cx.run_until_parked();
10188 completion_handle.next().await.unwrap();
10189
10190 editor.update(cx, |editor, _| {
10191 assert!(editor.context_menu_visible());
10192 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10193 {
10194 let completion_labels = menu
10195 .completions
10196 .borrow()
10197 .iter()
10198 .map(|c| c.label.text.clone())
10199 .collect::<Vec<_>>();
10200 assert_eq!(
10201 completion_labels,
10202 &[
10203 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10204 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10205 "single line label 2 d e f ",
10206 "a b c g h i ",
10207 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
10208 ],
10209 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
10210 );
10211
10212 for completion in menu
10213 .completions
10214 .borrow()
10215 .iter() {
10216 assert_eq!(
10217 completion.label.filter_range,
10218 0..completion.label.text.len(),
10219 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
10220 );
10221 }
10222 } else {
10223 panic!("expected completion menu to be open");
10224 }
10225 });
10226}
10227
10228#[gpui::test]
10229async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
10230 init_test(cx, |_| {});
10231 let mut cx = EditorLspTestContext::new_rust(
10232 lsp::ServerCapabilities {
10233 completion_provider: Some(lsp::CompletionOptions {
10234 trigger_characters: Some(vec![".".to_string()]),
10235 ..Default::default()
10236 }),
10237 ..Default::default()
10238 },
10239 cx,
10240 )
10241 .await;
10242 cx.lsp
10243 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10244 Ok(Some(lsp::CompletionResponse::Array(vec![
10245 lsp::CompletionItem {
10246 label: "first".into(),
10247 ..Default::default()
10248 },
10249 lsp::CompletionItem {
10250 label: "last".into(),
10251 ..Default::default()
10252 },
10253 ])))
10254 });
10255 cx.set_state("variableˇ");
10256 cx.simulate_keystroke(".");
10257 cx.executor().run_until_parked();
10258
10259 cx.update_editor(|editor, _, _| {
10260 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10261 {
10262 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
10263 } else {
10264 panic!("expected completion menu to be open");
10265 }
10266 });
10267
10268 cx.update_editor(|editor, window, cx| {
10269 editor.move_page_down(&MovePageDown::default(), window, cx);
10270 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10271 {
10272 assert!(
10273 menu.selected_item == 1,
10274 "expected PageDown to select the last item from the context menu"
10275 );
10276 } else {
10277 panic!("expected completion menu to stay open after PageDown");
10278 }
10279 });
10280
10281 cx.update_editor(|editor, window, cx| {
10282 editor.move_page_up(&MovePageUp::default(), window, cx);
10283 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10284 {
10285 assert!(
10286 menu.selected_item == 0,
10287 "expected PageUp to select the first item from the context menu"
10288 );
10289 } else {
10290 panic!("expected completion menu to stay open after PageUp");
10291 }
10292 });
10293}
10294
10295#[gpui::test]
10296async fn test_completion_sort(cx: &mut TestAppContext) {
10297 init_test(cx, |_| {});
10298 let mut cx = EditorLspTestContext::new_rust(
10299 lsp::ServerCapabilities {
10300 completion_provider: Some(lsp::CompletionOptions {
10301 trigger_characters: Some(vec![".".to_string()]),
10302 ..Default::default()
10303 }),
10304 ..Default::default()
10305 },
10306 cx,
10307 )
10308 .await;
10309 cx.lsp
10310 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10311 Ok(Some(lsp::CompletionResponse::Array(vec![
10312 lsp::CompletionItem {
10313 label: "Range".into(),
10314 sort_text: Some("a".into()),
10315 ..Default::default()
10316 },
10317 lsp::CompletionItem {
10318 label: "r".into(),
10319 sort_text: Some("b".into()),
10320 ..Default::default()
10321 },
10322 lsp::CompletionItem {
10323 label: "ret".into(),
10324 sort_text: Some("c".into()),
10325 ..Default::default()
10326 },
10327 lsp::CompletionItem {
10328 label: "return".into(),
10329 sort_text: Some("d".into()),
10330 ..Default::default()
10331 },
10332 lsp::CompletionItem {
10333 label: "slice".into(),
10334 sort_text: Some("d".into()),
10335 ..Default::default()
10336 },
10337 ])))
10338 });
10339 cx.set_state("rˇ");
10340 cx.executor().run_until_parked();
10341 cx.update_editor(|editor, window, cx| {
10342 editor.show_completions(
10343 &ShowCompletions {
10344 trigger: Some("r".into()),
10345 },
10346 window,
10347 cx,
10348 );
10349 });
10350 cx.executor().run_until_parked();
10351
10352 cx.update_editor(|editor, _, _| {
10353 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10354 {
10355 assert_eq!(
10356 completion_menu_entries(&menu),
10357 &["r", "ret", "Range", "return"]
10358 );
10359 } else {
10360 panic!("expected completion menu to be open");
10361 }
10362 });
10363}
10364
10365#[gpui::test]
10366async fn test_as_is_completions(cx: &mut TestAppContext) {
10367 init_test(cx, |_| {});
10368 let mut cx = EditorLspTestContext::new_rust(
10369 lsp::ServerCapabilities {
10370 completion_provider: Some(lsp::CompletionOptions {
10371 ..Default::default()
10372 }),
10373 ..Default::default()
10374 },
10375 cx,
10376 )
10377 .await;
10378 cx.lsp
10379 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10380 Ok(Some(lsp::CompletionResponse::Array(vec![
10381 lsp::CompletionItem {
10382 label: "unsafe".into(),
10383 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10384 range: lsp::Range {
10385 start: lsp::Position {
10386 line: 1,
10387 character: 2,
10388 },
10389 end: lsp::Position {
10390 line: 1,
10391 character: 3,
10392 },
10393 },
10394 new_text: "unsafe".to_string(),
10395 })),
10396 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
10397 ..Default::default()
10398 },
10399 ])))
10400 });
10401 cx.set_state("fn a() {}\n nˇ");
10402 cx.executor().run_until_parked();
10403 cx.update_editor(|editor, window, cx| {
10404 editor.show_completions(
10405 &ShowCompletions {
10406 trigger: Some("\n".into()),
10407 },
10408 window,
10409 cx,
10410 );
10411 });
10412 cx.executor().run_until_parked();
10413
10414 cx.update_editor(|editor, window, cx| {
10415 editor.confirm_completion(&Default::default(), window, cx)
10416 });
10417 cx.executor().run_until_parked();
10418 cx.assert_editor_state("fn a() {}\n unsafeˇ");
10419}
10420
10421#[gpui::test]
10422async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10423 init_test(cx, |_| {});
10424
10425 let mut cx = EditorLspTestContext::new_rust(
10426 lsp::ServerCapabilities {
10427 completion_provider: Some(lsp::CompletionOptions {
10428 trigger_characters: Some(vec![".".to_string()]),
10429 resolve_provider: Some(true),
10430 ..Default::default()
10431 }),
10432 ..Default::default()
10433 },
10434 cx,
10435 )
10436 .await;
10437
10438 cx.set_state("fn main() { let a = 2ˇ; }");
10439 cx.simulate_keystroke(".");
10440 let completion_item = lsp::CompletionItem {
10441 label: "Some".into(),
10442 kind: Some(lsp::CompletionItemKind::SNIPPET),
10443 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10444 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10445 kind: lsp::MarkupKind::Markdown,
10446 value: "```rust\nSome(2)\n```".to_string(),
10447 })),
10448 deprecated: Some(false),
10449 sort_text: Some("Some".to_string()),
10450 filter_text: Some("Some".to_string()),
10451 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10452 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10453 range: lsp::Range {
10454 start: lsp::Position {
10455 line: 0,
10456 character: 22,
10457 },
10458 end: lsp::Position {
10459 line: 0,
10460 character: 22,
10461 },
10462 },
10463 new_text: "Some(2)".to_string(),
10464 })),
10465 additional_text_edits: Some(vec![lsp::TextEdit {
10466 range: lsp::Range {
10467 start: lsp::Position {
10468 line: 0,
10469 character: 20,
10470 },
10471 end: lsp::Position {
10472 line: 0,
10473 character: 22,
10474 },
10475 },
10476 new_text: "".to_string(),
10477 }]),
10478 ..Default::default()
10479 };
10480
10481 let closure_completion_item = completion_item.clone();
10482 let counter = Arc::new(AtomicUsize::new(0));
10483 let counter_clone = counter.clone();
10484 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10485 let task_completion_item = closure_completion_item.clone();
10486 counter_clone.fetch_add(1, atomic::Ordering::Release);
10487 async move {
10488 Ok(Some(lsp::CompletionResponse::Array(vec![
10489 task_completion_item,
10490 ])))
10491 }
10492 });
10493
10494 cx.condition(|editor, _| editor.context_menu_visible())
10495 .await;
10496 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10497 assert!(request.next().await.is_some());
10498 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10499
10500 cx.simulate_keystrokes("S o m");
10501 cx.condition(|editor, _| editor.context_menu_visible())
10502 .await;
10503 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10504 assert!(request.next().await.is_some());
10505 assert!(request.next().await.is_some());
10506 assert!(request.next().await.is_some());
10507 request.close();
10508 assert!(request.next().await.is_none());
10509 assert_eq!(
10510 counter.load(atomic::Ordering::Acquire),
10511 4,
10512 "With the completions menu open, only one LSP request should happen per input"
10513 );
10514}
10515
10516#[gpui::test]
10517async fn test_toggle_comment(cx: &mut TestAppContext) {
10518 init_test(cx, |_| {});
10519 let mut cx = EditorTestContext::new(cx).await;
10520 let language = Arc::new(Language::new(
10521 LanguageConfig {
10522 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10523 ..Default::default()
10524 },
10525 Some(tree_sitter_rust::LANGUAGE.into()),
10526 ));
10527 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10528
10529 // If multiple selections intersect a line, the line is only toggled once.
10530 cx.set_state(indoc! {"
10531 fn a() {
10532 «//b();
10533 ˇ»// «c();
10534 //ˇ» d();
10535 }
10536 "});
10537
10538 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10539
10540 cx.assert_editor_state(indoc! {"
10541 fn a() {
10542 «b();
10543 c();
10544 ˇ» d();
10545 }
10546 "});
10547
10548 // The comment prefix is inserted at the same column for every line in a
10549 // selection.
10550 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10551
10552 cx.assert_editor_state(indoc! {"
10553 fn a() {
10554 // «b();
10555 // c();
10556 ˇ»// d();
10557 }
10558 "});
10559
10560 // If a selection ends at the beginning of a line, that line is not toggled.
10561 cx.set_selections_state(indoc! {"
10562 fn a() {
10563 // b();
10564 «// c();
10565 ˇ» // d();
10566 }
10567 "});
10568
10569 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10570
10571 cx.assert_editor_state(indoc! {"
10572 fn a() {
10573 // b();
10574 «c();
10575 ˇ» // d();
10576 }
10577 "});
10578
10579 // If a selection span a single line and is empty, the line is toggled.
10580 cx.set_state(indoc! {"
10581 fn a() {
10582 a();
10583 b();
10584 ˇ
10585 }
10586 "});
10587
10588 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10589
10590 cx.assert_editor_state(indoc! {"
10591 fn a() {
10592 a();
10593 b();
10594 //•ˇ
10595 }
10596 "});
10597
10598 // If a selection span multiple lines, empty lines are not toggled.
10599 cx.set_state(indoc! {"
10600 fn a() {
10601 «a();
10602
10603 c();ˇ»
10604 }
10605 "});
10606
10607 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10608
10609 cx.assert_editor_state(indoc! {"
10610 fn a() {
10611 // «a();
10612
10613 // c();ˇ»
10614 }
10615 "});
10616
10617 // If a selection includes multiple comment prefixes, all lines are uncommented.
10618 cx.set_state(indoc! {"
10619 fn a() {
10620 «// a();
10621 /// b();
10622 //! c();ˇ»
10623 }
10624 "});
10625
10626 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10627
10628 cx.assert_editor_state(indoc! {"
10629 fn a() {
10630 «a();
10631 b();
10632 c();ˇ»
10633 }
10634 "});
10635}
10636
10637#[gpui::test]
10638async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10639 init_test(cx, |_| {});
10640 let mut cx = EditorTestContext::new(cx).await;
10641 let language = Arc::new(Language::new(
10642 LanguageConfig {
10643 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10644 ..Default::default()
10645 },
10646 Some(tree_sitter_rust::LANGUAGE.into()),
10647 ));
10648 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10649
10650 let toggle_comments = &ToggleComments {
10651 advance_downwards: false,
10652 ignore_indent: true,
10653 };
10654
10655 // If multiple selections intersect a line, the line is only toggled once.
10656 cx.set_state(indoc! {"
10657 fn a() {
10658 // «b();
10659 // c();
10660 // ˇ» d();
10661 }
10662 "});
10663
10664 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10665
10666 cx.assert_editor_state(indoc! {"
10667 fn a() {
10668 «b();
10669 c();
10670 ˇ» d();
10671 }
10672 "});
10673
10674 // The comment prefix is inserted at the beginning of each line
10675 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10676
10677 cx.assert_editor_state(indoc! {"
10678 fn a() {
10679 // «b();
10680 // c();
10681 // ˇ» d();
10682 }
10683 "});
10684
10685 // If a selection ends at the beginning of a line, that line is not toggled.
10686 cx.set_selections_state(indoc! {"
10687 fn a() {
10688 // b();
10689 // «c();
10690 ˇ»// d();
10691 }
10692 "});
10693
10694 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10695
10696 cx.assert_editor_state(indoc! {"
10697 fn a() {
10698 // b();
10699 «c();
10700 ˇ»// d();
10701 }
10702 "});
10703
10704 // If a selection span a single line and is empty, the line is toggled.
10705 cx.set_state(indoc! {"
10706 fn a() {
10707 a();
10708 b();
10709 ˇ
10710 }
10711 "});
10712
10713 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10714
10715 cx.assert_editor_state(indoc! {"
10716 fn a() {
10717 a();
10718 b();
10719 //ˇ
10720 }
10721 "});
10722
10723 // If a selection span multiple lines, empty lines are not toggled.
10724 cx.set_state(indoc! {"
10725 fn a() {
10726 «a();
10727
10728 c();ˇ»
10729 }
10730 "});
10731
10732 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10733
10734 cx.assert_editor_state(indoc! {"
10735 fn a() {
10736 // «a();
10737
10738 // c();ˇ»
10739 }
10740 "});
10741
10742 // If a selection includes multiple comment prefixes, all lines are uncommented.
10743 cx.set_state(indoc! {"
10744 fn a() {
10745 // «a();
10746 /// b();
10747 //! c();ˇ»
10748 }
10749 "});
10750
10751 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10752
10753 cx.assert_editor_state(indoc! {"
10754 fn a() {
10755 «a();
10756 b();
10757 c();ˇ»
10758 }
10759 "});
10760}
10761
10762#[gpui::test]
10763async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10764 init_test(cx, |_| {});
10765
10766 let language = Arc::new(Language::new(
10767 LanguageConfig {
10768 line_comments: vec!["// ".into()],
10769 ..Default::default()
10770 },
10771 Some(tree_sitter_rust::LANGUAGE.into()),
10772 ));
10773
10774 let mut cx = EditorTestContext::new(cx).await;
10775
10776 cx.language_registry().add(language.clone());
10777 cx.update_buffer(|buffer, cx| {
10778 buffer.set_language(Some(language), cx);
10779 });
10780
10781 let toggle_comments = &ToggleComments {
10782 advance_downwards: true,
10783 ignore_indent: false,
10784 };
10785
10786 // Single cursor on one line -> advance
10787 // Cursor moves horizontally 3 characters as well on non-blank line
10788 cx.set_state(indoc!(
10789 "fn a() {
10790 ˇdog();
10791 cat();
10792 }"
10793 ));
10794 cx.update_editor(|editor, window, cx| {
10795 editor.toggle_comments(toggle_comments, window, cx);
10796 });
10797 cx.assert_editor_state(indoc!(
10798 "fn a() {
10799 // dog();
10800 catˇ();
10801 }"
10802 ));
10803
10804 // Single selection on one line -> don't advance
10805 cx.set_state(indoc!(
10806 "fn a() {
10807 «dog()ˇ»;
10808 cat();
10809 }"
10810 ));
10811 cx.update_editor(|editor, window, cx| {
10812 editor.toggle_comments(toggle_comments, window, cx);
10813 });
10814 cx.assert_editor_state(indoc!(
10815 "fn a() {
10816 // «dog()ˇ»;
10817 cat();
10818 }"
10819 ));
10820
10821 // Multiple cursors on one line -> advance
10822 cx.set_state(indoc!(
10823 "fn a() {
10824 ˇdˇog();
10825 cat();
10826 }"
10827 ));
10828 cx.update_editor(|editor, window, cx| {
10829 editor.toggle_comments(toggle_comments, window, cx);
10830 });
10831 cx.assert_editor_state(indoc!(
10832 "fn a() {
10833 // dog();
10834 catˇ(ˇ);
10835 }"
10836 ));
10837
10838 // Multiple cursors on one line, with selection -> don't advance
10839 cx.set_state(indoc!(
10840 "fn a() {
10841 ˇdˇog«()ˇ»;
10842 cat();
10843 }"
10844 ));
10845 cx.update_editor(|editor, window, cx| {
10846 editor.toggle_comments(toggle_comments, window, cx);
10847 });
10848 cx.assert_editor_state(indoc!(
10849 "fn a() {
10850 // ˇdˇog«()ˇ»;
10851 cat();
10852 }"
10853 ));
10854
10855 // Single cursor on one line -> advance
10856 // Cursor moves to column 0 on blank line
10857 cx.set_state(indoc!(
10858 "fn a() {
10859 ˇdog();
10860
10861 cat();
10862 }"
10863 ));
10864 cx.update_editor(|editor, window, cx| {
10865 editor.toggle_comments(toggle_comments, window, cx);
10866 });
10867 cx.assert_editor_state(indoc!(
10868 "fn a() {
10869 // dog();
10870 ˇ
10871 cat();
10872 }"
10873 ));
10874
10875 // Single cursor on one line -> advance
10876 // Cursor starts and ends at column 0
10877 cx.set_state(indoc!(
10878 "fn a() {
10879 ˇ dog();
10880 cat();
10881 }"
10882 ));
10883 cx.update_editor(|editor, window, cx| {
10884 editor.toggle_comments(toggle_comments, window, cx);
10885 });
10886 cx.assert_editor_state(indoc!(
10887 "fn a() {
10888 // dog();
10889 ˇ cat();
10890 }"
10891 ));
10892}
10893
10894#[gpui::test]
10895async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10896 init_test(cx, |_| {});
10897
10898 let mut cx = EditorTestContext::new(cx).await;
10899
10900 let html_language = Arc::new(
10901 Language::new(
10902 LanguageConfig {
10903 name: "HTML".into(),
10904 block_comment: Some(("<!-- ".into(), " -->".into())),
10905 ..Default::default()
10906 },
10907 Some(tree_sitter_html::LANGUAGE.into()),
10908 )
10909 .with_injection_query(
10910 r#"
10911 (script_element
10912 (raw_text) @injection.content
10913 (#set! injection.language "javascript"))
10914 "#,
10915 )
10916 .unwrap(),
10917 );
10918
10919 let javascript_language = Arc::new(Language::new(
10920 LanguageConfig {
10921 name: "JavaScript".into(),
10922 line_comments: vec!["// ".into()],
10923 ..Default::default()
10924 },
10925 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10926 ));
10927
10928 cx.language_registry().add(html_language.clone());
10929 cx.language_registry().add(javascript_language.clone());
10930 cx.update_buffer(|buffer, cx| {
10931 buffer.set_language(Some(html_language), cx);
10932 });
10933
10934 // Toggle comments for empty selections
10935 cx.set_state(
10936 &r#"
10937 <p>A</p>ˇ
10938 <p>B</p>ˇ
10939 <p>C</p>ˇ
10940 "#
10941 .unindent(),
10942 );
10943 cx.update_editor(|editor, window, cx| {
10944 editor.toggle_comments(&ToggleComments::default(), window, cx)
10945 });
10946 cx.assert_editor_state(
10947 &r#"
10948 <!-- <p>A</p>ˇ -->
10949 <!-- <p>B</p>ˇ -->
10950 <!-- <p>C</p>ˇ -->
10951 "#
10952 .unindent(),
10953 );
10954 cx.update_editor(|editor, window, cx| {
10955 editor.toggle_comments(&ToggleComments::default(), window, cx)
10956 });
10957 cx.assert_editor_state(
10958 &r#"
10959 <p>A</p>ˇ
10960 <p>B</p>ˇ
10961 <p>C</p>ˇ
10962 "#
10963 .unindent(),
10964 );
10965
10966 // Toggle comments for mixture of empty and non-empty selections, where
10967 // multiple selections occupy a given line.
10968 cx.set_state(
10969 &r#"
10970 <p>A«</p>
10971 <p>ˇ»B</p>ˇ
10972 <p>C«</p>
10973 <p>ˇ»D</p>ˇ
10974 "#
10975 .unindent(),
10976 );
10977
10978 cx.update_editor(|editor, window, cx| {
10979 editor.toggle_comments(&ToggleComments::default(), window, cx)
10980 });
10981 cx.assert_editor_state(
10982 &r#"
10983 <!-- <p>A«</p>
10984 <p>ˇ»B</p>ˇ -->
10985 <!-- <p>C«</p>
10986 <p>ˇ»D</p>ˇ -->
10987 "#
10988 .unindent(),
10989 );
10990 cx.update_editor(|editor, window, cx| {
10991 editor.toggle_comments(&ToggleComments::default(), window, cx)
10992 });
10993 cx.assert_editor_state(
10994 &r#"
10995 <p>A«</p>
10996 <p>ˇ»B</p>ˇ
10997 <p>C«</p>
10998 <p>ˇ»D</p>ˇ
10999 "#
11000 .unindent(),
11001 );
11002
11003 // Toggle comments when different languages are active for different
11004 // selections.
11005 cx.set_state(
11006 &r#"
11007 ˇ<script>
11008 ˇvar x = new Y();
11009 ˇ</script>
11010 "#
11011 .unindent(),
11012 );
11013 cx.executor().run_until_parked();
11014 cx.update_editor(|editor, window, cx| {
11015 editor.toggle_comments(&ToggleComments::default(), window, cx)
11016 });
11017 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11018 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11019 cx.assert_editor_state(
11020 &r#"
11021 <!-- ˇ<script> -->
11022 // ˇvar x = new Y();
11023 <!-- ˇ</script> -->
11024 "#
11025 .unindent(),
11026 );
11027}
11028
11029#[gpui::test]
11030fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11031 init_test(cx, |_| {});
11032
11033 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11034 let multibuffer = cx.new(|cx| {
11035 let mut multibuffer = MultiBuffer::new(ReadWrite);
11036 multibuffer.push_excerpts(
11037 buffer.clone(),
11038 [
11039 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
11040 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
11041 ],
11042 cx,
11043 );
11044 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
11045 multibuffer
11046 });
11047
11048 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11049 editor.update_in(cx, |editor, window, cx| {
11050 assert_eq!(editor.text(cx), "aaaa\nbbbb");
11051 editor.change_selections(None, window, cx, |s| {
11052 s.select_ranges([
11053 Point::new(0, 0)..Point::new(0, 0),
11054 Point::new(1, 0)..Point::new(1, 0),
11055 ])
11056 });
11057
11058 editor.handle_input("X", window, cx);
11059 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
11060 assert_eq!(
11061 editor.selections.ranges(cx),
11062 [
11063 Point::new(0, 1)..Point::new(0, 1),
11064 Point::new(1, 1)..Point::new(1, 1),
11065 ]
11066 );
11067
11068 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
11069 editor.change_selections(None, window, cx, |s| {
11070 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
11071 });
11072 editor.backspace(&Default::default(), window, cx);
11073 assert_eq!(editor.text(cx), "Xa\nbbb");
11074 assert_eq!(
11075 editor.selections.ranges(cx),
11076 [Point::new(1, 0)..Point::new(1, 0)]
11077 );
11078
11079 editor.change_selections(None, window, cx, |s| {
11080 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
11081 });
11082 editor.backspace(&Default::default(), window, cx);
11083 assert_eq!(editor.text(cx), "X\nbb");
11084 assert_eq!(
11085 editor.selections.ranges(cx),
11086 [Point::new(0, 1)..Point::new(0, 1)]
11087 );
11088 });
11089}
11090
11091#[gpui::test]
11092fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
11093 init_test(cx, |_| {});
11094
11095 let markers = vec![('[', ']').into(), ('(', ')').into()];
11096 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
11097 indoc! {"
11098 [aaaa
11099 (bbbb]
11100 cccc)",
11101 },
11102 markers.clone(),
11103 );
11104 let excerpt_ranges = markers.into_iter().map(|marker| {
11105 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
11106 ExcerptRange::new(context.clone())
11107 });
11108 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
11109 let multibuffer = cx.new(|cx| {
11110 let mut multibuffer = MultiBuffer::new(ReadWrite);
11111 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
11112 multibuffer
11113 });
11114
11115 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11116 editor.update_in(cx, |editor, window, cx| {
11117 let (expected_text, selection_ranges) = marked_text_ranges(
11118 indoc! {"
11119 aaaa
11120 bˇbbb
11121 bˇbbˇb
11122 cccc"
11123 },
11124 true,
11125 );
11126 assert_eq!(editor.text(cx), expected_text);
11127 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11128
11129 editor.handle_input("X", window, cx);
11130
11131 let (expected_text, expected_selections) = marked_text_ranges(
11132 indoc! {"
11133 aaaa
11134 bXˇbbXb
11135 bXˇbbXˇb
11136 cccc"
11137 },
11138 false,
11139 );
11140 assert_eq!(editor.text(cx), expected_text);
11141 assert_eq!(editor.selections.ranges(cx), expected_selections);
11142
11143 editor.newline(&Newline, window, cx);
11144 let (expected_text, expected_selections) = marked_text_ranges(
11145 indoc! {"
11146 aaaa
11147 bX
11148 ˇbbX
11149 b
11150 bX
11151 ˇbbX
11152 ˇb
11153 cccc"
11154 },
11155 false,
11156 );
11157 assert_eq!(editor.text(cx), expected_text);
11158 assert_eq!(editor.selections.ranges(cx), expected_selections);
11159 });
11160}
11161
11162#[gpui::test]
11163fn test_refresh_selections(cx: &mut TestAppContext) {
11164 init_test(cx, |_| {});
11165
11166 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11167 let mut excerpt1_id = None;
11168 let multibuffer = cx.new(|cx| {
11169 let mut multibuffer = MultiBuffer::new(ReadWrite);
11170 excerpt1_id = multibuffer
11171 .push_excerpts(
11172 buffer.clone(),
11173 [
11174 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11175 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11176 ],
11177 cx,
11178 )
11179 .into_iter()
11180 .next();
11181 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11182 multibuffer
11183 });
11184
11185 let editor = cx.add_window(|window, cx| {
11186 let mut editor = build_editor(multibuffer.clone(), window, cx);
11187 let snapshot = editor.snapshot(window, cx);
11188 editor.change_selections(None, window, cx, |s| {
11189 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11190 });
11191 editor.begin_selection(
11192 Point::new(2, 1).to_display_point(&snapshot),
11193 true,
11194 1,
11195 window,
11196 cx,
11197 );
11198 assert_eq!(
11199 editor.selections.ranges(cx),
11200 [
11201 Point::new(1, 3)..Point::new(1, 3),
11202 Point::new(2, 1)..Point::new(2, 1),
11203 ]
11204 );
11205 editor
11206 });
11207
11208 // Refreshing selections is a no-op when excerpts haven't changed.
11209 _ = editor.update(cx, |editor, window, cx| {
11210 editor.change_selections(None, window, cx, |s| s.refresh());
11211 assert_eq!(
11212 editor.selections.ranges(cx),
11213 [
11214 Point::new(1, 3)..Point::new(1, 3),
11215 Point::new(2, 1)..Point::new(2, 1),
11216 ]
11217 );
11218 });
11219
11220 multibuffer.update(cx, |multibuffer, cx| {
11221 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11222 });
11223 _ = editor.update(cx, |editor, window, cx| {
11224 // Removing an excerpt causes the first selection to become degenerate.
11225 assert_eq!(
11226 editor.selections.ranges(cx),
11227 [
11228 Point::new(0, 0)..Point::new(0, 0),
11229 Point::new(0, 1)..Point::new(0, 1)
11230 ]
11231 );
11232
11233 // Refreshing selections will relocate the first selection to the original buffer
11234 // location.
11235 editor.change_selections(None, window, cx, |s| s.refresh());
11236 assert_eq!(
11237 editor.selections.ranges(cx),
11238 [
11239 Point::new(0, 1)..Point::new(0, 1),
11240 Point::new(0, 3)..Point::new(0, 3)
11241 ]
11242 );
11243 assert!(editor.selections.pending_anchor().is_some());
11244 });
11245}
11246
11247#[gpui::test]
11248fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
11249 init_test(cx, |_| {});
11250
11251 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11252 let mut excerpt1_id = None;
11253 let multibuffer = cx.new(|cx| {
11254 let mut multibuffer = MultiBuffer::new(ReadWrite);
11255 excerpt1_id = multibuffer
11256 .push_excerpts(
11257 buffer.clone(),
11258 [
11259 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11260 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11261 ],
11262 cx,
11263 )
11264 .into_iter()
11265 .next();
11266 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11267 multibuffer
11268 });
11269
11270 let editor = cx.add_window(|window, cx| {
11271 let mut editor = build_editor(multibuffer.clone(), window, cx);
11272 let snapshot = editor.snapshot(window, cx);
11273 editor.begin_selection(
11274 Point::new(1, 3).to_display_point(&snapshot),
11275 false,
11276 1,
11277 window,
11278 cx,
11279 );
11280 assert_eq!(
11281 editor.selections.ranges(cx),
11282 [Point::new(1, 3)..Point::new(1, 3)]
11283 );
11284 editor
11285 });
11286
11287 multibuffer.update(cx, |multibuffer, cx| {
11288 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11289 });
11290 _ = editor.update(cx, |editor, window, cx| {
11291 assert_eq!(
11292 editor.selections.ranges(cx),
11293 [Point::new(0, 0)..Point::new(0, 0)]
11294 );
11295
11296 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
11297 editor.change_selections(None, window, cx, |s| s.refresh());
11298 assert_eq!(
11299 editor.selections.ranges(cx),
11300 [Point::new(0, 3)..Point::new(0, 3)]
11301 );
11302 assert!(editor.selections.pending_anchor().is_some());
11303 });
11304}
11305
11306#[gpui::test]
11307async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
11308 init_test(cx, |_| {});
11309
11310 let language = Arc::new(
11311 Language::new(
11312 LanguageConfig {
11313 brackets: BracketPairConfig {
11314 pairs: vec![
11315 BracketPair {
11316 start: "{".to_string(),
11317 end: "}".to_string(),
11318 close: true,
11319 surround: true,
11320 newline: true,
11321 },
11322 BracketPair {
11323 start: "/* ".to_string(),
11324 end: " */".to_string(),
11325 close: true,
11326 surround: true,
11327 newline: true,
11328 },
11329 ],
11330 ..Default::default()
11331 },
11332 ..Default::default()
11333 },
11334 Some(tree_sitter_rust::LANGUAGE.into()),
11335 )
11336 .with_indents_query("")
11337 .unwrap(),
11338 );
11339
11340 let text = concat!(
11341 "{ }\n", //
11342 " x\n", //
11343 " /* */\n", //
11344 "x\n", //
11345 "{{} }\n", //
11346 );
11347
11348 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11349 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11350 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11351 editor
11352 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11353 .await;
11354
11355 editor.update_in(cx, |editor, window, cx| {
11356 editor.change_selections(None, window, cx, |s| {
11357 s.select_display_ranges([
11358 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11359 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11360 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11361 ])
11362 });
11363 editor.newline(&Newline, window, cx);
11364
11365 assert_eq!(
11366 editor.buffer().read(cx).read(cx).text(),
11367 concat!(
11368 "{ \n", // Suppress rustfmt
11369 "\n", //
11370 "}\n", //
11371 " x\n", //
11372 " /* \n", //
11373 " \n", //
11374 " */\n", //
11375 "x\n", //
11376 "{{} \n", //
11377 "}\n", //
11378 )
11379 );
11380 });
11381}
11382
11383#[gpui::test]
11384fn test_highlighted_ranges(cx: &mut TestAppContext) {
11385 init_test(cx, |_| {});
11386
11387 let editor = cx.add_window(|window, cx| {
11388 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11389 build_editor(buffer.clone(), window, cx)
11390 });
11391
11392 _ = editor.update(cx, |editor, window, cx| {
11393 struct Type1;
11394 struct Type2;
11395
11396 let buffer = editor.buffer.read(cx).snapshot(cx);
11397
11398 let anchor_range =
11399 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11400
11401 editor.highlight_background::<Type1>(
11402 &[
11403 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11404 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11405 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11406 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11407 ],
11408 |_| Hsla::red(),
11409 cx,
11410 );
11411 editor.highlight_background::<Type2>(
11412 &[
11413 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11414 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11415 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11416 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11417 ],
11418 |_| Hsla::green(),
11419 cx,
11420 );
11421
11422 let snapshot = editor.snapshot(window, cx);
11423 let mut highlighted_ranges = editor.background_highlights_in_range(
11424 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11425 &snapshot,
11426 cx.theme().colors(),
11427 );
11428 // Enforce a consistent ordering based on color without relying on the ordering of the
11429 // highlight's `TypeId` which is non-executor.
11430 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11431 assert_eq!(
11432 highlighted_ranges,
11433 &[
11434 (
11435 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11436 Hsla::red(),
11437 ),
11438 (
11439 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11440 Hsla::red(),
11441 ),
11442 (
11443 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11444 Hsla::green(),
11445 ),
11446 (
11447 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11448 Hsla::green(),
11449 ),
11450 ]
11451 );
11452 assert_eq!(
11453 editor.background_highlights_in_range(
11454 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11455 &snapshot,
11456 cx.theme().colors(),
11457 ),
11458 &[(
11459 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11460 Hsla::red(),
11461 )]
11462 );
11463 });
11464}
11465
11466#[gpui::test]
11467async fn test_following(cx: &mut TestAppContext) {
11468 init_test(cx, |_| {});
11469
11470 let fs = FakeFs::new(cx.executor());
11471 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11472
11473 let buffer = project.update(cx, |project, cx| {
11474 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11475 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11476 });
11477 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11478 let follower = cx.update(|cx| {
11479 cx.open_window(
11480 WindowOptions {
11481 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11482 gpui::Point::new(px(0.), px(0.)),
11483 gpui::Point::new(px(10.), px(80.)),
11484 ))),
11485 ..Default::default()
11486 },
11487 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11488 )
11489 .unwrap()
11490 });
11491
11492 let is_still_following = Rc::new(RefCell::new(true));
11493 let follower_edit_event_count = Rc::new(RefCell::new(0));
11494 let pending_update = Rc::new(RefCell::new(None));
11495 let leader_entity = leader.root(cx).unwrap();
11496 let follower_entity = follower.root(cx).unwrap();
11497 _ = follower.update(cx, {
11498 let update = pending_update.clone();
11499 let is_still_following = is_still_following.clone();
11500 let follower_edit_event_count = follower_edit_event_count.clone();
11501 |_, window, cx| {
11502 cx.subscribe_in(
11503 &leader_entity,
11504 window,
11505 move |_, leader, event, window, cx| {
11506 leader.read(cx).add_event_to_update_proto(
11507 event,
11508 &mut update.borrow_mut(),
11509 window,
11510 cx,
11511 );
11512 },
11513 )
11514 .detach();
11515
11516 cx.subscribe_in(
11517 &follower_entity,
11518 window,
11519 move |_, _, event: &EditorEvent, _window, _cx| {
11520 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11521 *is_still_following.borrow_mut() = false;
11522 }
11523
11524 if let EditorEvent::BufferEdited = event {
11525 *follower_edit_event_count.borrow_mut() += 1;
11526 }
11527 },
11528 )
11529 .detach();
11530 }
11531 });
11532
11533 // Update the selections only
11534 _ = leader.update(cx, |leader, window, cx| {
11535 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11536 });
11537 follower
11538 .update(cx, |follower, window, cx| {
11539 follower.apply_update_proto(
11540 &project,
11541 pending_update.borrow_mut().take().unwrap(),
11542 window,
11543 cx,
11544 )
11545 })
11546 .unwrap()
11547 .await
11548 .unwrap();
11549 _ = follower.update(cx, |follower, _, cx| {
11550 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11551 });
11552 assert!(*is_still_following.borrow());
11553 assert_eq!(*follower_edit_event_count.borrow(), 0);
11554
11555 // Update the scroll position only
11556 _ = leader.update(cx, |leader, window, cx| {
11557 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11558 });
11559 follower
11560 .update(cx, |follower, window, cx| {
11561 follower.apply_update_proto(
11562 &project,
11563 pending_update.borrow_mut().take().unwrap(),
11564 window,
11565 cx,
11566 )
11567 })
11568 .unwrap()
11569 .await
11570 .unwrap();
11571 assert_eq!(
11572 follower
11573 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11574 .unwrap(),
11575 gpui::Point::new(1.5, 3.5)
11576 );
11577 assert!(*is_still_following.borrow());
11578 assert_eq!(*follower_edit_event_count.borrow(), 0);
11579
11580 // Update the selections and scroll position. The follower's scroll position is updated
11581 // via autoscroll, not via the leader's exact scroll position.
11582 _ = leader.update(cx, |leader, window, cx| {
11583 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11584 leader.request_autoscroll(Autoscroll::newest(), cx);
11585 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11586 });
11587 follower
11588 .update(cx, |follower, window, cx| {
11589 follower.apply_update_proto(
11590 &project,
11591 pending_update.borrow_mut().take().unwrap(),
11592 window,
11593 cx,
11594 )
11595 })
11596 .unwrap()
11597 .await
11598 .unwrap();
11599 _ = follower.update(cx, |follower, _, cx| {
11600 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11601 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11602 });
11603 assert!(*is_still_following.borrow());
11604
11605 // Creating a pending selection that precedes another selection
11606 _ = leader.update(cx, |leader, window, cx| {
11607 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11608 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11609 });
11610 follower
11611 .update(cx, |follower, window, cx| {
11612 follower.apply_update_proto(
11613 &project,
11614 pending_update.borrow_mut().take().unwrap(),
11615 window,
11616 cx,
11617 )
11618 })
11619 .unwrap()
11620 .await
11621 .unwrap();
11622 _ = follower.update(cx, |follower, _, cx| {
11623 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11624 });
11625 assert!(*is_still_following.borrow());
11626
11627 // Extend the pending selection so that it surrounds another selection
11628 _ = leader.update(cx, |leader, window, cx| {
11629 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11630 });
11631 follower
11632 .update(cx, |follower, window, cx| {
11633 follower.apply_update_proto(
11634 &project,
11635 pending_update.borrow_mut().take().unwrap(),
11636 window,
11637 cx,
11638 )
11639 })
11640 .unwrap()
11641 .await
11642 .unwrap();
11643 _ = follower.update(cx, |follower, _, cx| {
11644 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11645 });
11646
11647 // Scrolling locally breaks the follow
11648 _ = follower.update(cx, |follower, window, cx| {
11649 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11650 follower.set_scroll_anchor(
11651 ScrollAnchor {
11652 anchor: top_anchor,
11653 offset: gpui::Point::new(0.0, 0.5),
11654 },
11655 window,
11656 cx,
11657 );
11658 });
11659 assert!(!(*is_still_following.borrow()));
11660}
11661
11662#[gpui::test]
11663async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11664 init_test(cx, |_| {});
11665
11666 let fs = FakeFs::new(cx.executor());
11667 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11668 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11669 let pane = workspace
11670 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11671 .unwrap();
11672
11673 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11674
11675 let leader = pane.update_in(cx, |_, window, cx| {
11676 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11677 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11678 });
11679
11680 // Start following the editor when it has no excerpts.
11681 let mut state_message =
11682 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11683 let workspace_entity = workspace.root(cx).unwrap();
11684 let follower_1 = cx
11685 .update_window(*workspace.deref(), |_, window, cx| {
11686 Editor::from_state_proto(
11687 workspace_entity,
11688 ViewId {
11689 creator: Default::default(),
11690 id: 0,
11691 },
11692 &mut state_message,
11693 window,
11694 cx,
11695 )
11696 })
11697 .unwrap()
11698 .unwrap()
11699 .await
11700 .unwrap();
11701
11702 let update_message = Rc::new(RefCell::new(None));
11703 follower_1.update_in(cx, {
11704 let update = update_message.clone();
11705 |_, window, cx| {
11706 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11707 leader.read(cx).add_event_to_update_proto(
11708 event,
11709 &mut update.borrow_mut(),
11710 window,
11711 cx,
11712 );
11713 })
11714 .detach();
11715 }
11716 });
11717
11718 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11719 (
11720 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11721 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11722 )
11723 });
11724
11725 // Insert some excerpts.
11726 leader.update(cx, |leader, cx| {
11727 leader.buffer.update(cx, |multibuffer, cx| {
11728 let excerpt_ids = multibuffer.push_excerpts(
11729 buffer_1.clone(),
11730 [
11731 ExcerptRange::new(1..6),
11732 ExcerptRange::new(12..15),
11733 ExcerptRange::new(0..3),
11734 ],
11735 cx,
11736 );
11737 multibuffer.insert_excerpts_after(
11738 excerpt_ids[0],
11739 buffer_2.clone(),
11740 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
11741 cx,
11742 );
11743 });
11744 });
11745
11746 // Apply the update of adding the excerpts.
11747 follower_1
11748 .update_in(cx, |follower, window, cx| {
11749 follower.apply_update_proto(
11750 &project,
11751 update_message.borrow().clone().unwrap(),
11752 window,
11753 cx,
11754 )
11755 })
11756 .await
11757 .unwrap();
11758 assert_eq!(
11759 follower_1.update(cx, |editor, cx| editor.text(cx)),
11760 leader.update(cx, |editor, cx| editor.text(cx))
11761 );
11762 update_message.borrow_mut().take();
11763
11764 // Start following separately after it already has excerpts.
11765 let mut state_message =
11766 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11767 let workspace_entity = workspace.root(cx).unwrap();
11768 let follower_2 = cx
11769 .update_window(*workspace.deref(), |_, window, cx| {
11770 Editor::from_state_proto(
11771 workspace_entity,
11772 ViewId {
11773 creator: Default::default(),
11774 id: 0,
11775 },
11776 &mut state_message,
11777 window,
11778 cx,
11779 )
11780 })
11781 .unwrap()
11782 .unwrap()
11783 .await
11784 .unwrap();
11785 assert_eq!(
11786 follower_2.update(cx, |editor, cx| editor.text(cx)),
11787 leader.update(cx, |editor, cx| editor.text(cx))
11788 );
11789
11790 // Remove some excerpts.
11791 leader.update(cx, |leader, cx| {
11792 leader.buffer.update(cx, |multibuffer, cx| {
11793 let excerpt_ids = multibuffer.excerpt_ids();
11794 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11795 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11796 });
11797 });
11798
11799 // Apply the update of removing the excerpts.
11800 follower_1
11801 .update_in(cx, |follower, window, cx| {
11802 follower.apply_update_proto(
11803 &project,
11804 update_message.borrow().clone().unwrap(),
11805 window,
11806 cx,
11807 )
11808 })
11809 .await
11810 .unwrap();
11811 follower_2
11812 .update_in(cx, |follower, window, cx| {
11813 follower.apply_update_proto(
11814 &project,
11815 update_message.borrow().clone().unwrap(),
11816 window,
11817 cx,
11818 )
11819 })
11820 .await
11821 .unwrap();
11822 update_message.borrow_mut().take();
11823 assert_eq!(
11824 follower_1.update(cx, |editor, cx| editor.text(cx)),
11825 leader.update(cx, |editor, cx| editor.text(cx))
11826 );
11827}
11828
11829#[gpui::test]
11830async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11831 init_test(cx, |_| {});
11832
11833 let mut cx = EditorTestContext::new(cx).await;
11834 let lsp_store =
11835 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11836
11837 cx.set_state(indoc! {"
11838 ˇfn func(abc def: i32) -> u32 {
11839 }
11840 "});
11841
11842 cx.update(|_, cx| {
11843 lsp_store.update(cx, |lsp_store, cx| {
11844 lsp_store
11845 .update_diagnostics(
11846 LanguageServerId(0),
11847 lsp::PublishDiagnosticsParams {
11848 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11849 version: None,
11850 diagnostics: vec![
11851 lsp::Diagnostic {
11852 range: lsp::Range::new(
11853 lsp::Position::new(0, 11),
11854 lsp::Position::new(0, 12),
11855 ),
11856 severity: Some(lsp::DiagnosticSeverity::ERROR),
11857 ..Default::default()
11858 },
11859 lsp::Diagnostic {
11860 range: lsp::Range::new(
11861 lsp::Position::new(0, 12),
11862 lsp::Position::new(0, 15),
11863 ),
11864 severity: Some(lsp::DiagnosticSeverity::ERROR),
11865 ..Default::default()
11866 },
11867 lsp::Diagnostic {
11868 range: lsp::Range::new(
11869 lsp::Position::new(0, 25),
11870 lsp::Position::new(0, 28),
11871 ),
11872 severity: Some(lsp::DiagnosticSeverity::ERROR),
11873 ..Default::default()
11874 },
11875 ],
11876 },
11877 &[],
11878 cx,
11879 )
11880 .unwrap()
11881 });
11882 });
11883
11884 executor.run_until_parked();
11885
11886 cx.update_editor(|editor, window, cx| {
11887 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11888 });
11889
11890 cx.assert_editor_state(indoc! {"
11891 fn func(abc def: i32) -> ˇu32 {
11892 }
11893 "});
11894
11895 cx.update_editor(|editor, window, cx| {
11896 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11897 });
11898
11899 cx.assert_editor_state(indoc! {"
11900 fn func(abc ˇdef: i32) -> u32 {
11901 }
11902 "});
11903
11904 cx.update_editor(|editor, window, cx| {
11905 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11906 });
11907
11908 cx.assert_editor_state(indoc! {"
11909 fn func(abcˇ def: i32) -> u32 {
11910 }
11911 "});
11912
11913 cx.update_editor(|editor, window, cx| {
11914 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11915 });
11916
11917 cx.assert_editor_state(indoc! {"
11918 fn func(abc def: i32) -> ˇu32 {
11919 }
11920 "});
11921}
11922
11923#[gpui::test]
11924async fn cycle_through_same_place_diagnostics(
11925 executor: BackgroundExecutor,
11926 cx: &mut TestAppContext,
11927) {
11928 init_test(cx, |_| {});
11929
11930 let mut cx = EditorTestContext::new(cx).await;
11931 let lsp_store =
11932 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11933
11934 cx.set_state(indoc! {"
11935 ˇfn func(abc def: i32) -> u32 {
11936 }
11937 "});
11938
11939 cx.update(|_, cx| {
11940 lsp_store.update(cx, |lsp_store, cx| {
11941 lsp_store
11942 .update_diagnostics(
11943 LanguageServerId(0),
11944 lsp::PublishDiagnosticsParams {
11945 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11946 version: None,
11947 diagnostics: vec![
11948 lsp::Diagnostic {
11949 range: lsp::Range::new(
11950 lsp::Position::new(0, 11),
11951 lsp::Position::new(0, 12),
11952 ),
11953 severity: Some(lsp::DiagnosticSeverity::ERROR),
11954 ..Default::default()
11955 },
11956 lsp::Diagnostic {
11957 range: lsp::Range::new(
11958 lsp::Position::new(0, 12),
11959 lsp::Position::new(0, 15),
11960 ),
11961 severity: Some(lsp::DiagnosticSeverity::ERROR),
11962 ..Default::default()
11963 },
11964 lsp::Diagnostic {
11965 range: lsp::Range::new(
11966 lsp::Position::new(0, 12),
11967 lsp::Position::new(0, 15),
11968 ),
11969 severity: Some(lsp::DiagnosticSeverity::ERROR),
11970 ..Default::default()
11971 },
11972 lsp::Diagnostic {
11973 range: lsp::Range::new(
11974 lsp::Position::new(0, 25),
11975 lsp::Position::new(0, 28),
11976 ),
11977 severity: Some(lsp::DiagnosticSeverity::ERROR),
11978 ..Default::default()
11979 },
11980 ],
11981 },
11982 &[],
11983 cx,
11984 )
11985 .unwrap()
11986 });
11987 });
11988 executor.run_until_parked();
11989
11990 //// Backward
11991
11992 // Fourth diagnostic
11993 cx.update_editor(|editor, window, cx| {
11994 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11995 });
11996 cx.assert_editor_state(indoc! {"
11997 fn func(abc def: i32) -> ˇu32 {
11998 }
11999 "});
12000
12001 // Third diagnostic
12002 cx.update_editor(|editor, window, cx| {
12003 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12004 });
12005 cx.assert_editor_state(indoc! {"
12006 fn func(abc ˇdef: i32) -> u32 {
12007 }
12008 "});
12009
12010 // Second diagnostic, same place
12011 cx.update_editor(|editor, window, cx| {
12012 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12013 });
12014 cx.assert_editor_state(indoc! {"
12015 fn func(abc ˇdef: i32) -> u32 {
12016 }
12017 "});
12018
12019 // First diagnostic
12020 cx.update_editor(|editor, window, cx| {
12021 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12022 });
12023 cx.assert_editor_state(indoc! {"
12024 fn func(abcˇ def: i32) -> u32 {
12025 }
12026 "});
12027
12028 // Wrapped over, fourth diagnostic
12029 cx.update_editor(|editor, window, cx| {
12030 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12031 });
12032 cx.assert_editor_state(indoc! {"
12033 fn func(abc def: i32) -> ˇu32 {
12034 }
12035 "});
12036
12037 cx.update_editor(|editor, window, cx| {
12038 editor.move_to_beginning(&MoveToBeginning, window, cx);
12039 });
12040 cx.assert_editor_state(indoc! {"
12041 ˇfn func(abc def: i32) -> u32 {
12042 }
12043 "});
12044
12045 //// Forward
12046
12047 // First diagnostic
12048 cx.update_editor(|editor, window, cx| {
12049 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12050 });
12051 cx.assert_editor_state(indoc! {"
12052 fn func(abcˇ def: i32) -> u32 {
12053 }
12054 "});
12055
12056 // Second diagnostic
12057 cx.update_editor(|editor, window, cx| {
12058 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12059 });
12060 cx.assert_editor_state(indoc! {"
12061 fn func(abc ˇdef: i32) -> u32 {
12062 }
12063 "});
12064
12065 // Third diagnostic, same place
12066 cx.update_editor(|editor, window, cx| {
12067 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12068 });
12069 cx.assert_editor_state(indoc! {"
12070 fn func(abc ˇdef: i32) -> u32 {
12071 }
12072 "});
12073
12074 // Fourth diagnostic
12075 cx.update_editor(|editor, window, cx| {
12076 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12077 });
12078 cx.assert_editor_state(indoc! {"
12079 fn func(abc def: i32) -> ˇu32 {
12080 }
12081 "});
12082
12083 // Wrapped around, first diagnostic
12084 cx.update_editor(|editor, window, cx| {
12085 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12086 });
12087 cx.assert_editor_state(indoc! {"
12088 fn func(abcˇ def: i32) -> u32 {
12089 }
12090 "});
12091}
12092
12093#[gpui::test]
12094async fn active_diagnostics_dismiss_after_invalidation(
12095 executor: BackgroundExecutor,
12096 cx: &mut TestAppContext,
12097) {
12098 init_test(cx, |_| {});
12099
12100 let mut cx = EditorTestContext::new(cx).await;
12101 let lsp_store =
12102 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12103
12104 cx.set_state(indoc! {"
12105 ˇfn func(abc def: i32) -> u32 {
12106 }
12107 "});
12108
12109 let message = "Something's wrong!";
12110 cx.update(|_, cx| {
12111 lsp_store.update(cx, |lsp_store, cx| {
12112 lsp_store
12113 .update_diagnostics(
12114 LanguageServerId(0),
12115 lsp::PublishDiagnosticsParams {
12116 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12117 version: None,
12118 diagnostics: vec![lsp::Diagnostic {
12119 range: lsp::Range::new(
12120 lsp::Position::new(0, 11),
12121 lsp::Position::new(0, 12),
12122 ),
12123 severity: Some(lsp::DiagnosticSeverity::ERROR),
12124 message: message.to_string(),
12125 ..Default::default()
12126 }],
12127 },
12128 &[],
12129 cx,
12130 )
12131 .unwrap()
12132 });
12133 });
12134 executor.run_until_parked();
12135
12136 cx.update_editor(|editor, window, cx| {
12137 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12138 assert_eq!(
12139 editor
12140 .active_diagnostics
12141 .as_ref()
12142 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
12143 Some(message),
12144 "Should have a diagnostics group activated"
12145 );
12146 });
12147 cx.assert_editor_state(indoc! {"
12148 fn func(abcˇ def: i32) -> u32 {
12149 }
12150 "});
12151
12152 cx.update(|_, cx| {
12153 lsp_store.update(cx, |lsp_store, cx| {
12154 lsp_store
12155 .update_diagnostics(
12156 LanguageServerId(0),
12157 lsp::PublishDiagnosticsParams {
12158 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12159 version: None,
12160 diagnostics: Vec::new(),
12161 },
12162 &[],
12163 cx,
12164 )
12165 .unwrap()
12166 });
12167 });
12168 executor.run_until_parked();
12169 cx.update_editor(|editor, _, _| {
12170 assert_eq!(
12171 editor.active_diagnostics, None,
12172 "After no diagnostics set to the editor, no diagnostics should be active"
12173 );
12174 });
12175 cx.assert_editor_state(indoc! {"
12176 fn func(abcˇ def: i32) -> u32 {
12177 }
12178 "});
12179
12180 cx.update_editor(|editor, window, cx| {
12181 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12182 assert_eq!(
12183 editor.active_diagnostics, None,
12184 "Should be no diagnostics to go to and activate"
12185 );
12186 });
12187 cx.assert_editor_state(indoc! {"
12188 fn func(abcˇ def: i32) -> u32 {
12189 }
12190 "});
12191}
12192
12193#[gpui::test]
12194async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12195 init_test(cx, |_| {});
12196
12197 let mut cx = EditorTestContext::new(cx).await;
12198
12199 cx.set_state(indoc! {"
12200 fn func(abˇc def: i32) -> u32 {
12201 }
12202 "});
12203 let lsp_store =
12204 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12205
12206 cx.update(|_, cx| {
12207 lsp_store.update(cx, |lsp_store, cx| {
12208 lsp_store.update_diagnostics(
12209 LanguageServerId(0),
12210 lsp::PublishDiagnosticsParams {
12211 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12212 version: None,
12213 diagnostics: vec![lsp::Diagnostic {
12214 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12215 severity: Some(lsp::DiagnosticSeverity::ERROR),
12216 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12217 ..Default::default()
12218 }],
12219 },
12220 &[],
12221 cx,
12222 )
12223 })
12224 }).unwrap();
12225 cx.run_until_parked();
12226 cx.update_editor(|editor, window, cx| {
12227 hover_popover::hover(editor, &Default::default(), window, cx)
12228 });
12229 cx.run_until_parked();
12230 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12231}
12232
12233#[gpui::test]
12234async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12235 init_test(cx, |_| {});
12236
12237 let mut cx = EditorTestContext::new(cx).await;
12238
12239 let diff_base = r#"
12240 use some::mod;
12241
12242 const A: u32 = 42;
12243
12244 fn main() {
12245 println!("hello");
12246
12247 println!("world");
12248 }
12249 "#
12250 .unindent();
12251
12252 // Edits are modified, removed, modified, added
12253 cx.set_state(
12254 &r#"
12255 use some::modified;
12256
12257 ˇ
12258 fn main() {
12259 println!("hello there");
12260
12261 println!("around the");
12262 println!("world");
12263 }
12264 "#
12265 .unindent(),
12266 );
12267
12268 cx.set_head_text(&diff_base);
12269 executor.run_until_parked();
12270
12271 cx.update_editor(|editor, window, cx| {
12272 //Wrap around the bottom of the buffer
12273 for _ in 0..3 {
12274 editor.go_to_next_hunk(&GoToHunk, window, cx);
12275 }
12276 });
12277
12278 cx.assert_editor_state(
12279 &r#"
12280 ˇuse some::modified;
12281
12282
12283 fn main() {
12284 println!("hello there");
12285
12286 println!("around the");
12287 println!("world");
12288 }
12289 "#
12290 .unindent(),
12291 );
12292
12293 cx.update_editor(|editor, window, cx| {
12294 //Wrap around the top of the buffer
12295 for _ in 0..2 {
12296 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12297 }
12298 });
12299
12300 cx.assert_editor_state(
12301 &r#"
12302 use some::modified;
12303
12304
12305 fn main() {
12306 ˇ println!("hello there");
12307
12308 println!("around the");
12309 println!("world");
12310 }
12311 "#
12312 .unindent(),
12313 );
12314
12315 cx.update_editor(|editor, window, cx| {
12316 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12317 });
12318
12319 cx.assert_editor_state(
12320 &r#"
12321 use some::modified;
12322
12323 ˇ
12324 fn main() {
12325 println!("hello there");
12326
12327 println!("around the");
12328 println!("world");
12329 }
12330 "#
12331 .unindent(),
12332 );
12333
12334 cx.update_editor(|editor, window, cx| {
12335 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12336 });
12337
12338 cx.assert_editor_state(
12339 &r#"
12340 ˇuse some::modified;
12341
12342
12343 fn main() {
12344 println!("hello there");
12345
12346 println!("around the");
12347 println!("world");
12348 }
12349 "#
12350 .unindent(),
12351 );
12352
12353 cx.update_editor(|editor, window, cx| {
12354 for _ in 0..2 {
12355 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12356 }
12357 });
12358
12359 cx.assert_editor_state(
12360 &r#"
12361 use some::modified;
12362
12363
12364 fn main() {
12365 ˇ println!("hello there");
12366
12367 println!("around the");
12368 println!("world");
12369 }
12370 "#
12371 .unindent(),
12372 );
12373
12374 cx.update_editor(|editor, window, cx| {
12375 editor.fold(&Fold, window, cx);
12376 });
12377
12378 cx.update_editor(|editor, window, cx| {
12379 editor.go_to_next_hunk(&GoToHunk, window, cx);
12380 });
12381
12382 cx.assert_editor_state(
12383 &r#"
12384 ˇuse some::modified;
12385
12386
12387 fn main() {
12388 println!("hello there");
12389
12390 println!("around the");
12391 println!("world");
12392 }
12393 "#
12394 .unindent(),
12395 );
12396}
12397
12398#[test]
12399fn test_split_words() {
12400 fn split(text: &str) -> Vec<&str> {
12401 split_words(text).collect()
12402 }
12403
12404 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12405 assert_eq!(split("hello_world"), &["hello_", "world"]);
12406 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12407 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12408 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12409 assert_eq!(split("helloworld"), &["helloworld"]);
12410
12411 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12412}
12413
12414#[gpui::test]
12415async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12416 init_test(cx, |_| {});
12417
12418 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12419 let mut assert = |before, after| {
12420 let _state_context = cx.set_state(before);
12421 cx.run_until_parked();
12422 cx.update_editor(|editor, window, cx| {
12423 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12424 });
12425 cx.run_until_parked();
12426 cx.assert_editor_state(after);
12427 };
12428
12429 // Outside bracket jumps to outside of matching bracket
12430 assert("console.logˇ(var);", "console.log(var)ˇ;");
12431 assert("console.log(var)ˇ;", "console.logˇ(var);");
12432
12433 // Inside bracket jumps to inside of matching bracket
12434 assert("console.log(ˇvar);", "console.log(varˇ);");
12435 assert("console.log(varˇ);", "console.log(ˇvar);");
12436
12437 // When outside a bracket and inside, favor jumping to the inside bracket
12438 assert(
12439 "console.log('foo', [1, 2, 3]ˇ);",
12440 "console.log(ˇ'foo', [1, 2, 3]);",
12441 );
12442 assert(
12443 "console.log(ˇ'foo', [1, 2, 3]);",
12444 "console.log('foo', [1, 2, 3]ˇ);",
12445 );
12446
12447 // Bias forward if two options are equally likely
12448 assert(
12449 "let result = curried_fun()ˇ();",
12450 "let result = curried_fun()()ˇ;",
12451 );
12452
12453 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12454 assert(
12455 indoc! {"
12456 function test() {
12457 console.log('test')ˇ
12458 }"},
12459 indoc! {"
12460 function test() {
12461 console.logˇ('test')
12462 }"},
12463 );
12464}
12465
12466#[gpui::test]
12467async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12468 init_test(cx, |_| {});
12469
12470 let fs = FakeFs::new(cx.executor());
12471 fs.insert_tree(
12472 path!("/a"),
12473 json!({
12474 "main.rs": "fn main() { let a = 5; }",
12475 "other.rs": "// Test file",
12476 }),
12477 )
12478 .await;
12479 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12480
12481 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12482 language_registry.add(Arc::new(Language::new(
12483 LanguageConfig {
12484 name: "Rust".into(),
12485 matcher: LanguageMatcher {
12486 path_suffixes: vec!["rs".to_string()],
12487 ..Default::default()
12488 },
12489 brackets: BracketPairConfig {
12490 pairs: vec![BracketPair {
12491 start: "{".to_string(),
12492 end: "}".to_string(),
12493 close: true,
12494 surround: true,
12495 newline: true,
12496 }],
12497 disabled_scopes_by_bracket_ix: Vec::new(),
12498 },
12499 ..Default::default()
12500 },
12501 Some(tree_sitter_rust::LANGUAGE.into()),
12502 )));
12503 let mut fake_servers = language_registry.register_fake_lsp(
12504 "Rust",
12505 FakeLspAdapter {
12506 capabilities: lsp::ServerCapabilities {
12507 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12508 first_trigger_character: "{".to_string(),
12509 more_trigger_character: None,
12510 }),
12511 ..Default::default()
12512 },
12513 ..Default::default()
12514 },
12515 );
12516
12517 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12518
12519 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12520
12521 let worktree_id = workspace
12522 .update(cx, |workspace, _, cx| {
12523 workspace.project().update(cx, |project, cx| {
12524 project.worktrees(cx).next().unwrap().read(cx).id()
12525 })
12526 })
12527 .unwrap();
12528
12529 let buffer = project
12530 .update(cx, |project, cx| {
12531 project.open_local_buffer(path!("/a/main.rs"), cx)
12532 })
12533 .await
12534 .unwrap();
12535 let editor_handle = workspace
12536 .update(cx, |workspace, window, cx| {
12537 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12538 })
12539 .unwrap()
12540 .await
12541 .unwrap()
12542 .downcast::<Editor>()
12543 .unwrap();
12544
12545 cx.executor().start_waiting();
12546 let fake_server = fake_servers.next().await.unwrap();
12547
12548 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12549 |params, _| async move {
12550 assert_eq!(
12551 params.text_document_position.text_document.uri,
12552 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12553 );
12554 assert_eq!(
12555 params.text_document_position.position,
12556 lsp::Position::new(0, 21),
12557 );
12558
12559 Ok(Some(vec![lsp::TextEdit {
12560 new_text: "]".to_string(),
12561 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12562 }]))
12563 },
12564 );
12565
12566 editor_handle.update_in(cx, |editor, window, cx| {
12567 window.focus(&editor.focus_handle(cx));
12568 editor.change_selections(None, window, cx, |s| {
12569 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12570 });
12571 editor.handle_input("{", window, cx);
12572 });
12573
12574 cx.executor().run_until_parked();
12575
12576 buffer.update(cx, |buffer, _| {
12577 assert_eq!(
12578 buffer.text(),
12579 "fn main() { let a = {5}; }",
12580 "No extra braces from on type formatting should appear in the buffer"
12581 )
12582 });
12583}
12584
12585#[gpui::test]
12586async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12587 init_test(cx, |_| {});
12588
12589 let fs = FakeFs::new(cx.executor());
12590 fs.insert_tree(
12591 path!("/a"),
12592 json!({
12593 "main.rs": "fn main() { let a = 5; }",
12594 "other.rs": "// Test file",
12595 }),
12596 )
12597 .await;
12598
12599 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12600
12601 let server_restarts = Arc::new(AtomicUsize::new(0));
12602 let closure_restarts = Arc::clone(&server_restarts);
12603 let language_server_name = "test language server";
12604 let language_name: LanguageName = "Rust".into();
12605
12606 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12607 language_registry.add(Arc::new(Language::new(
12608 LanguageConfig {
12609 name: language_name.clone(),
12610 matcher: LanguageMatcher {
12611 path_suffixes: vec!["rs".to_string()],
12612 ..Default::default()
12613 },
12614 ..Default::default()
12615 },
12616 Some(tree_sitter_rust::LANGUAGE.into()),
12617 )));
12618 let mut fake_servers = language_registry.register_fake_lsp(
12619 "Rust",
12620 FakeLspAdapter {
12621 name: language_server_name,
12622 initialization_options: Some(json!({
12623 "testOptionValue": true
12624 })),
12625 initializer: Some(Box::new(move |fake_server| {
12626 let task_restarts = Arc::clone(&closure_restarts);
12627 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12628 task_restarts.fetch_add(1, atomic::Ordering::Release);
12629 futures::future::ready(Ok(()))
12630 });
12631 })),
12632 ..Default::default()
12633 },
12634 );
12635
12636 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12637 let _buffer = project
12638 .update(cx, |project, cx| {
12639 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12640 })
12641 .await
12642 .unwrap();
12643 let _fake_server = fake_servers.next().await.unwrap();
12644 update_test_language_settings(cx, |language_settings| {
12645 language_settings.languages.insert(
12646 language_name.clone(),
12647 LanguageSettingsContent {
12648 tab_size: NonZeroU32::new(8),
12649 ..Default::default()
12650 },
12651 );
12652 });
12653 cx.executor().run_until_parked();
12654 assert_eq!(
12655 server_restarts.load(atomic::Ordering::Acquire),
12656 0,
12657 "Should not restart LSP server on an unrelated change"
12658 );
12659
12660 update_test_project_settings(cx, |project_settings| {
12661 project_settings.lsp.insert(
12662 "Some other server name".into(),
12663 LspSettings {
12664 binary: None,
12665 settings: None,
12666 initialization_options: Some(json!({
12667 "some other init value": false
12668 })),
12669 enable_lsp_tasks: false,
12670 },
12671 );
12672 });
12673 cx.executor().run_until_parked();
12674 assert_eq!(
12675 server_restarts.load(atomic::Ordering::Acquire),
12676 0,
12677 "Should not restart LSP server on an unrelated LSP settings change"
12678 );
12679
12680 update_test_project_settings(cx, |project_settings| {
12681 project_settings.lsp.insert(
12682 language_server_name.into(),
12683 LspSettings {
12684 binary: None,
12685 settings: None,
12686 initialization_options: Some(json!({
12687 "anotherInitValue": false
12688 })),
12689 enable_lsp_tasks: false,
12690 },
12691 );
12692 });
12693 cx.executor().run_until_parked();
12694 assert_eq!(
12695 server_restarts.load(atomic::Ordering::Acquire),
12696 1,
12697 "Should restart LSP server on a related LSP settings change"
12698 );
12699
12700 update_test_project_settings(cx, |project_settings| {
12701 project_settings.lsp.insert(
12702 language_server_name.into(),
12703 LspSettings {
12704 binary: None,
12705 settings: None,
12706 initialization_options: Some(json!({
12707 "anotherInitValue": false
12708 })),
12709 enable_lsp_tasks: false,
12710 },
12711 );
12712 });
12713 cx.executor().run_until_parked();
12714 assert_eq!(
12715 server_restarts.load(atomic::Ordering::Acquire),
12716 1,
12717 "Should not restart LSP server on a related LSP settings change that is the same"
12718 );
12719
12720 update_test_project_settings(cx, |project_settings| {
12721 project_settings.lsp.insert(
12722 language_server_name.into(),
12723 LspSettings {
12724 binary: None,
12725 settings: None,
12726 initialization_options: None,
12727 enable_lsp_tasks: false,
12728 },
12729 );
12730 });
12731 cx.executor().run_until_parked();
12732 assert_eq!(
12733 server_restarts.load(atomic::Ordering::Acquire),
12734 2,
12735 "Should restart LSP server on another related LSP settings change"
12736 );
12737}
12738
12739#[gpui::test]
12740async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12741 init_test(cx, |_| {});
12742
12743 let mut cx = EditorLspTestContext::new_rust(
12744 lsp::ServerCapabilities {
12745 completion_provider: Some(lsp::CompletionOptions {
12746 trigger_characters: Some(vec![".".to_string()]),
12747 resolve_provider: Some(true),
12748 ..Default::default()
12749 }),
12750 ..Default::default()
12751 },
12752 cx,
12753 )
12754 .await;
12755
12756 cx.set_state("fn main() { let a = 2ˇ; }");
12757 cx.simulate_keystroke(".");
12758 let completion_item = lsp::CompletionItem {
12759 label: "some".into(),
12760 kind: Some(lsp::CompletionItemKind::SNIPPET),
12761 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12762 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12763 kind: lsp::MarkupKind::Markdown,
12764 value: "```rust\nSome(2)\n```".to_string(),
12765 })),
12766 deprecated: Some(false),
12767 sort_text: Some("fffffff2".to_string()),
12768 filter_text: Some("some".to_string()),
12769 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12770 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12771 range: lsp::Range {
12772 start: lsp::Position {
12773 line: 0,
12774 character: 22,
12775 },
12776 end: lsp::Position {
12777 line: 0,
12778 character: 22,
12779 },
12780 },
12781 new_text: "Some(2)".to_string(),
12782 })),
12783 additional_text_edits: Some(vec![lsp::TextEdit {
12784 range: lsp::Range {
12785 start: lsp::Position {
12786 line: 0,
12787 character: 20,
12788 },
12789 end: lsp::Position {
12790 line: 0,
12791 character: 22,
12792 },
12793 },
12794 new_text: "".to_string(),
12795 }]),
12796 ..Default::default()
12797 };
12798
12799 let closure_completion_item = completion_item.clone();
12800 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12801 let task_completion_item = closure_completion_item.clone();
12802 async move {
12803 Ok(Some(lsp::CompletionResponse::Array(vec![
12804 task_completion_item,
12805 ])))
12806 }
12807 });
12808
12809 request.next().await;
12810
12811 cx.condition(|editor, _| editor.context_menu_visible())
12812 .await;
12813 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12814 editor
12815 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12816 .unwrap()
12817 });
12818 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12819
12820 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12821 let task_completion_item = completion_item.clone();
12822 async move { Ok(task_completion_item) }
12823 })
12824 .next()
12825 .await
12826 .unwrap();
12827 apply_additional_edits.await.unwrap();
12828 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12829}
12830
12831#[gpui::test]
12832async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12833 init_test(cx, |_| {});
12834
12835 let mut cx = EditorLspTestContext::new_rust(
12836 lsp::ServerCapabilities {
12837 completion_provider: Some(lsp::CompletionOptions {
12838 trigger_characters: Some(vec![".".to_string()]),
12839 resolve_provider: Some(true),
12840 ..Default::default()
12841 }),
12842 ..Default::default()
12843 },
12844 cx,
12845 )
12846 .await;
12847
12848 cx.set_state("fn main() { let a = 2ˇ; }");
12849 cx.simulate_keystroke(".");
12850
12851 let item1 = lsp::CompletionItem {
12852 label: "method id()".to_string(),
12853 filter_text: Some("id".to_string()),
12854 detail: None,
12855 documentation: None,
12856 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12857 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12858 new_text: ".id".to_string(),
12859 })),
12860 ..lsp::CompletionItem::default()
12861 };
12862
12863 let item2 = lsp::CompletionItem {
12864 label: "other".to_string(),
12865 filter_text: Some("other".to_string()),
12866 detail: None,
12867 documentation: None,
12868 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12869 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12870 new_text: ".other".to_string(),
12871 })),
12872 ..lsp::CompletionItem::default()
12873 };
12874
12875 let item1 = item1.clone();
12876 cx.set_request_handler::<lsp::request::Completion, _, _>({
12877 let item1 = item1.clone();
12878 move |_, _, _| {
12879 let item1 = item1.clone();
12880 let item2 = item2.clone();
12881 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12882 }
12883 })
12884 .next()
12885 .await;
12886
12887 cx.condition(|editor, _| editor.context_menu_visible())
12888 .await;
12889 cx.update_editor(|editor, _, _| {
12890 let context_menu = editor.context_menu.borrow_mut();
12891 let context_menu = context_menu
12892 .as_ref()
12893 .expect("Should have the context menu deployed");
12894 match context_menu {
12895 CodeContextMenu::Completions(completions_menu) => {
12896 let completions = completions_menu.completions.borrow_mut();
12897 assert_eq!(
12898 completions
12899 .iter()
12900 .map(|completion| &completion.label.text)
12901 .collect::<Vec<_>>(),
12902 vec!["method id()", "other"]
12903 )
12904 }
12905 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12906 }
12907 });
12908
12909 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12910 let item1 = item1.clone();
12911 move |_, item_to_resolve, _| {
12912 let item1 = item1.clone();
12913 async move {
12914 if item1 == item_to_resolve {
12915 Ok(lsp::CompletionItem {
12916 label: "method id()".to_string(),
12917 filter_text: Some("id".to_string()),
12918 detail: Some("Now resolved!".to_string()),
12919 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12920 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12921 range: lsp::Range::new(
12922 lsp::Position::new(0, 22),
12923 lsp::Position::new(0, 22),
12924 ),
12925 new_text: ".id".to_string(),
12926 })),
12927 ..lsp::CompletionItem::default()
12928 })
12929 } else {
12930 Ok(item_to_resolve)
12931 }
12932 }
12933 }
12934 })
12935 .next()
12936 .await
12937 .unwrap();
12938 cx.run_until_parked();
12939
12940 cx.update_editor(|editor, window, cx| {
12941 editor.context_menu_next(&Default::default(), window, cx);
12942 });
12943
12944 cx.update_editor(|editor, _, _| {
12945 let context_menu = editor.context_menu.borrow_mut();
12946 let context_menu = context_menu
12947 .as_ref()
12948 .expect("Should have the context menu deployed");
12949 match context_menu {
12950 CodeContextMenu::Completions(completions_menu) => {
12951 let completions = completions_menu.completions.borrow_mut();
12952 assert_eq!(
12953 completions
12954 .iter()
12955 .map(|completion| &completion.label.text)
12956 .collect::<Vec<_>>(),
12957 vec!["method id() Now resolved!", "other"],
12958 "Should update first completion label, but not second as the filter text did not match."
12959 );
12960 }
12961 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12962 }
12963 });
12964}
12965
12966#[gpui::test]
12967async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12968 init_test(cx, |_| {});
12969
12970 let mut cx = EditorLspTestContext::new_rust(
12971 lsp::ServerCapabilities {
12972 completion_provider: Some(lsp::CompletionOptions {
12973 trigger_characters: Some(vec![".".to_string()]),
12974 resolve_provider: Some(true),
12975 ..Default::default()
12976 }),
12977 ..Default::default()
12978 },
12979 cx,
12980 )
12981 .await;
12982
12983 cx.set_state("fn main() { let a = 2ˇ; }");
12984 cx.simulate_keystroke(".");
12985
12986 let unresolved_item_1 = lsp::CompletionItem {
12987 label: "id".to_string(),
12988 filter_text: Some("id".to_string()),
12989 detail: None,
12990 documentation: None,
12991 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12992 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12993 new_text: ".id".to_string(),
12994 })),
12995 ..lsp::CompletionItem::default()
12996 };
12997 let resolved_item_1 = lsp::CompletionItem {
12998 additional_text_edits: Some(vec![lsp::TextEdit {
12999 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13000 new_text: "!!".to_string(),
13001 }]),
13002 ..unresolved_item_1.clone()
13003 };
13004 let unresolved_item_2 = lsp::CompletionItem {
13005 label: "other".to_string(),
13006 filter_text: Some("other".to_string()),
13007 detail: None,
13008 documentation: None,
13009 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13010 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13011 new_text: ".other".to_string(),
13012 })),
13013 ..lsp::CompletionItem::default()
13014 };
13015 let resolved_item_2 = lsp::CompletionItem {
13016 additional_text_edits: Some(vec![lsp::TextEdit {
13017 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13018 new_text: "??".to_string(),
13019 }]),
13020 ..unresolved_item_2.clone()
13021 };
13022
13023 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13024 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13025 cx.lsp
13026 .server
13027 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13028 let unresolved_item_1 = unresolved_item_1.clone();
13029 let resolved_item_1 = resolved_item_1.clone();
13030 let unresolved_item_2 = unresolved_item_2.clone();
13031 let resolved_item_2 = resolved_item_2.clone();
13032 let resolve_requests_1 = resolve_requests_1.clone();
13033 let resolve_requests_2 = resolve_requests_2.clone();
13034 move |unresolved_request, _| {
13035 let unresolved_item_1 = unresolved_item_1.clone();
13036 let resolved_item_1 = resolved_item_1.clone();
13037 let unresolved_item_2 = unresolved_item_2.clone();
13038 let resolved_item_2 = resolved_item_2.clone();
13039 let resolve_requests_1 = resolve_requests_1.clone();
13040 let resolve_requests_2 = resolve_requests_2.clone();
13041 async move {
13042 if unresolved_request == unresolved_item_1 {
13043 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13044 Ok(resolved_item_1.clone())
13045 } else if unresolved_request == unresolved_item_2 {
13046 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13047 Ok(resolved_item_2.clone())
13048 } else {
13049 panic!("Unexpected completion item {unresolved_request:?}")
13050 }
13051 }
13052 }
13053 })
13054 .detach();
13055
13056 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13057 let unresolved_item_1 = unresolved_item_1.clone();
13058 let unresolved_item_2 = unresolved_item_2.clone();
13059 async move {
13060 Ok(Some(lsp::CompletionResponse::Array(vec![
13061 unresolved_item_1,
13062 unresolved_item_2,
13063 ])))
13064 }
13065 })
13066 .next()
13067 .await;
13068
13069 cx.condition(|editor, _| editor.context_menu_visible())
13070 .await;
13071 cx.update_editor(|editor, _, _| {
13072 let context_menu = editor.context_menu.borrow_mut();
13073 let context_menu = context_menu
13074 .as_ref()
13075 .expect("Should have the context menu deployed");
13076 match context_menu {
13077 CodeContextMenu::Completions(completions_menu) => {
13078 let completions = completions_menu.completions.borrow_mut();
13079 assert_eq!(
13080 completions
13081 .iter()
13082 .map(|completion| &completion.label.text)
13083 .collect::<Vec<_>>(),
13084 vec!["id", "other"]
13085 )
13086 }
13087 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13088 }
13089 });
13090 cx.run_until_parked();
13091
13092 cx.update_editor(|editor, window, cx| {
13093 editor.context_menu_next(&ContextMenuNext, window, cx);
13094 });
13095 cx.run_until_parked();
13096 cx.update_editor(|editor, window, cx| {
13097 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13098 });
13099 cx.run_until_parked();
13100 cx.update_editor(|editor, window, cx| {
13101 editor.context_menu_next(&ContextMenuNext, window, cx);
13102 });
13103 cx.run_until_parked();
13104 cx.update_editor(|editor, window, cx| {
13105 editor
13106 .compose_completion(&ComposeCompletion::default(), window, cx)
13107 .expect("No task returned")
13108 })
13109 .await
13110 .expect("Completion failed");
13111 cx.run_until_parked();
13112
13113 cx.update_editor(|editor, _, cx| {
13114 assert_eq!(
13115 resolve_requests_1.load(atomic::Ordering::Acquire),
13116 1,
13117 "Should always resolve once despite multiple selections"
13118 );
13119 assert_eq!(
13120 resolve_requests_2.load(atomic::Ordering::Acquire),
13121 1,
13122 "Should always resolve once after multiple selections and applying the completion"
13123 );
13124 assert_eq!(
13125 editor.text(cx),
13126 "fn main() { let a = ??.other; }",
13127 "Should use resolved data when applying the completion"
13128 );
13129 });
13130}
13131
13132#[gpui::test]
13133async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13134 init_test(cx, |_| {});
13135
13136 let item_0 = lsp::CompletionItem {
13137 label: "abs".into(),
13138 insert_text: Some("abs".into()),
13139 data: Some(json!({ "very": "special"})),
13140 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13141 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13142 lsp::InsertReplaceEdit {
13143 new_text: "abs".to_string(),
13144 insert: lsp::Range::default(),
13145 replace: lsp::Range::default(),
13146 },
13147 )),
13148 ..lsp::CompletionItem::default()
13149 };
13150 let items = iter::once(item_0.clone())
13151 .chain((11..51).map(|i| lsp::CompletionItem {
13152 label: format!("item_{}", i),
13153 insert_text: Some(format!("item_{}", i)),
13154 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13155 ..lsp::CompletionItem::default()
13156 }))
13157 .collect::<Vec<_>>();
13158
13159 let default_commit_characters = vec!["?".to_string()];
13160 let default_data = json!({ "default": "data"});
13161 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13162 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13163 let default_edit_range = lsp::Range {
13164 start: lsp::Position {
13165 line: 0,
13166 character: 5,
13167 },
13168 end: lsp::Position {
13169 line: 0,
13170 character: 5,
13171 },
13172 };
13173
13174 let mut cx = EditorLspTestContext::new_rust(
13175 lsp::ServerCapabilities {
13176 completion_provider: Some(lsp::CompletionOptions {
13177 trigger_characters: Some(vec![".".to_string()]),
13178 resolve_provider: Some(true),
13179 ..Default::default()
13180 }),
13181 ..Default::default()
13182 },
13183 cx,
13184 )
13185 .await;
13186
13187 cx.set_state("fn main() { let a = 2ˇ; }");
13188 cx.simulate_keystroke(".");
13189
13190 let completion_data = default_data.clone();
13191 let completion_characters = default_commit_characters.clone();
13192 let completion_items = items.clone();
13193 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13194 let default_data = completion_data.clone();
13195 let default_commit_characters = completion_characters.clone();
13196 let items = completion_items.clone();
13197 async move {
13198 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13199 items,
13200 item_defaults: Some(lsp::CompletionListItemDefaults {
13201 data: Some(default_data.clone()),
13202 commit_characters: Some(default_commit_characters.clone()),
13203 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13204 default_edit_range,
13205 )),
13206 insert_text_format: Some(default_insert_text_format),
13207 insert_text_mode: Some(default_insert_text_mode),
13208 }),
13209 ..lsp::CompletionList::default()
13210 })))
13211 }
13212 })
13213 .next()
13214 .await;
13215
13216 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13217 cx.lsp
13218 .server
13219 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13220 let closure_resolved_items = resolved_items.clone();
13221 move |item_to_resolve, _| {
13222 let closure_resolved_items = closure_resolved_items.clone();
13223 async move {
13224 closure_resolved_items.lock().push(item_to_resolve.clone());
13225 Ok(item_to_resolve)
13226 }
13227 }
13228 })
13229 .detach();
13230
13231 cx.condition(|editor, _| editor.context_menu_visible())
13232 .await;
13233 cx.run_until_parked();
13234 cx.update_editor(|editor, _, _| {
13235 let menu = editor.context_menu.borrow_mut();
13236 match menu.as_ref().expect("should have the completions menu") {
13237 CodeContextMenu::Completions(completions_menu) => {
13238 assert_eq!(
13239 completions_menu
13240 .entries
13241 .borrow()
13242 .iter()
13243 .map(|mat| mat.string.clone())
13244 .collect::<Vec<String>>(),
13245 items
13246 .iter()
13247 .map(|completion| completion.label.clone())
13248 .collect::<Vec<String>>()
13249 );
13250 }
13251 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13252 }
13253 });
13254 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13255 // with 4 from the end.
13256 assert_eq!(
13257 *resolved_items.lock(),
13258 [&items[0..16], &items[items.len() - 4..items.len()]]
13259 .concat()
13260 .iter()
13261 .cloned()
13262 .map(|mut item| {
13263 if item.data.is_none() {
13264 item.data = Some(default_data.clone());
13265 }
13266 item
13267 })
13268 .collect::<Vec<lsp::CompletionItem>>(),
13269 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13270 );
13271 resolved_items.lock().clear();
13272
13273 cx.update_editor(|editor, window, cx| {
13274 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13275 });
13276 cx.run_until_parked();
13277 // Completions that have already been resolved are skipped.
13278 assert_eq!(
13279 *resolved_items.lock(),
13280 items[items.len() - 16..items.len() - 4]
13281 .iter()
13282 .cloned()
13283 .map(|mut item| {
13284 if item.data.is_none() {
13285 item.data = Some(default_data.clone());
13286 }
13287 item
13288 })
13289 .collect::<Vec<lsp::CompletionItem>>()
13290 );
13291 resolved_items.lock().clear();
13292}
13293
13294#[gpui::test]
13295async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13296 init_test(cx, |_| {});
13297
13298 let mut cx = EditorLspTestContext::new(
13299 Language::new(
13300 LanguageConfig {
13301 matcher: LanguageMatcher {
13302 path_suffixes: vec!["jsx".into()],
13303 ..Default::default()
13304 },
13305 overrides: [(
13306 "element".into(),
13307 LanguageConfigOverride {
13308 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13309 ..Default::default()
13310 },
13311 )]
13312 .into_iter()
13313 .collect(),
13314 ..Default::default()
13315 },
13316 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13317 )
13318 .with_override_query("(jsx_self_closing_element) @element")
13319 .unwrap(),
13320 lsp::ServerCapabilities {
13321 completion_provider: Some(lsp::CompletionOptions {
13322 trigger_characters: Some(vec![":".to_string()]),
13323 ..Default::default()
13324 }),
13325 ..Default::default()
13326 },
13327 cx,
13328 )
13329 .await;
13330
13331 cx.lsp
13332 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13333 Ok(Some(lsp::CompletionResponse::Array(vec![
13334 lsp::CompletionItem {
13335 label: "bg-blue".into(),
13336 ..Default::default()
13337 },
13338 lsp::CompletionItem {
13339 label: "bg-red".into(),
13340 ..Default::default()
13341 },
13342 lsp::CompletionItem {
13343 label: "bg-yellow".into(),
13344 ..Default::default()
13345 },
13346 ])))
13347 });
13348
13349 cx.set_state(r#"<p class="bgˇ" />"#);
13350
13351 // Trigger completion when typing a dash, because the dash is an extra
13352 // word character in the 'element' scope, which contains the cursor.
13353 cx.simulate_keystroke("-");
13354 cx.executor().run_until_parked();
13355 cx.update_editor(|editor, _, _| {
13356 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13357 {
13358 assert_eq!(
13359 completion_menu_entries(&menu),
13360 &["bg-red", "bg-blue", "bg-yellow"]
13361 );
13362 } else {
13363 panic!("expected completion menu to be open");
13364 }
13365 });
13366
13367 cx.simulate_keystroke("l");
13368 cx.executor().run_until_parked();
13369 cx.update_editor(|editor, _, _| {
13370 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13371 {
13372 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13373 } else {
13374 panic!("expected completion menu to be open");
13375 }
13376 });
13377
13378 // When filtering completions, consider the character after the '-' to
13379 // be the start of a subword.
13380 cx.set_state(r#"<p class="yelˇ" />"#);
13381 cx.simulate_keystroke("l");
13382 cx.executor().run_until_parked();
13383 cx.update_editor(|editor, _, _| {
13384 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13385 {
13386 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13387 } else {
13388 panic!("expected completion menu to be open");
13389 }
13390 });
13391}
13392
13393fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13394 let entries = menu.entries.borrow();
13395 entries.iter().map(|mat| mat.string.clone()).collect()
13396}
13397
13398#[gpui::test]
13399async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13400 init_test(cx, |settings| {
13401 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13402 FormatterList(vec![Formatter::Prettier].into()),
13403 ))
13404 });
13405
13406 let fs = FakeFs::new(cx.executor());
13407 fs.insert_file(path!("/file.ts"), Default::default()).await;
13408
13409 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13410 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13411
13412 language_registry.add(Arc::new(Language::new(
13413 LanguageConfig {
13414 name: "TypeScript".into(),
13415 matcher: LanguageMatcher {
13416 path_suffixes: vec!["ts".to_string()],
13417 ..Default::default()
13418 },
13419 ..Default::default()
13420 },
13421 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13422 )));
13423 update_test_language_settings(cx, |settings| {
13424 settings.defaults.prettier = Some(PrettierSettings {
13425 allowed: true,
13426 ..PrettierSettings::default()
13427 });
13428 });
13429
13430 let test_plugin = "test_plugin";
13431 let _ = language_registry.register_fake_lsp(
13432 "TypeScript",
13433 FakeLspAdapter {
13434 prettier_plugins: vec![test_plugin],
13435 ..Default::default()
13436 },
13437 );
13438
13439 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13440 let buffer = project
13441 .update(cx, |project, cx| {
13442 project.open_local_buffer(path!("/file.ts"), cx)
13443 })
13444 .await
13445 .unwrap();
13446
13447 let buffer_text = "one\ntwo\nthree\n";
13448 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13449 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13450 editor.update_in(cx, |editor, window, cx| {
13451 editor.set_text(buffer_text, window, cx)
13452 });
13453
13454 editor
13455 .update_in(cx, |editor, window, cx| {
13456 editor.perform_format(
13457 project.clone(),
13458 FormatTrigger::Manual,
13459 FormatTarget::Buffers,
13460 window,
13461 cx,
13462 )
13463 })
13464 .unwrap()
13465 .await;
13466 assert_eq!(
13467 editor.update(cx, |editor, cx| editor.text(cx)),
13468 buffer_text.to_string() + prettier_format_suffix,
13469 "Test prettier formatting was not applied to the original buffer text",
13470 );
13471
13472 update_test_language_settings(cx, |settings| {
13473 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13474 });
13475 let format = editor.update_in(cx, |editor, window, cx| {
13476 editor.perform_format(
13477 project.clone(),
13478 FormatTrigger::Manual,
13479 FormatTarget::Buffers,
13480 window,
13481 cx,
13482 )
13483 });
13484 format.await.unwrap();
13485 assert_eq!(
13486 editor.update(cx, |editor, cx| editor.text(cx)),
13487 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13488 "Autoformatting (via test prettier) was not applied to the original buffer text",
13489 );
13490}
13491
13492#[gpui::test]
13493async fn test_addition_reverts(cx: &mut TestAppContext) {
13494 init_test(cx, |_| {});
13495 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13496 let base_text = indoc! {r#"
13497 struct Row;
13498 struct Row1;
13499 struct Row2;
13500
13501 struct Row4;
13502 struct Row5;
13503 struct Row6;
13504
13505 struct Row8;
13506 struct Row9;
13507 struct Row10;"#};
13508
13509 // When addition hunks are not adjacent to carets, no hunk revert is performed
13510 assert_hunk_revert(
13511 indoc! {r#"struct Row;
13512 struct Row1;
13513 struct Row1.1;
13514 struct Row1.2;
13515 struct Row2;ˇ
13516
13517 struct Row4;
13518 struct Row5;
13519 struct Row6;
13520
13521 struct Row8;
13522 ˇstruct Row9;
13523 struct Row9.1;
13524 struct Row9.2;
13525 struct Row9.3;
13526 struct Row10;"#},
13527 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13528 indoc! {r#"struct Row;
13529 struct Row1;
13530 struct Row1.1;
13531 struct Row1.2;
13532 struct Row2;ˇ
13533
13534 struct Row4;
13535 struct Row5;
13536 struct Row6;
13537
13538 struct Row8;
13539 ˇstruct Row9;
13540 struct Row9.1;
13541 struct Row9.2;
13542 struct Row9.3;
13543 struct Row10;"#},
13544 base_text,
13545 &mut cx,
13546 );
13547 // Same for selections
13548 assert_hunk_revert(
13549 indoc! {r#"struct Row;
13550 struct Row1;
13551 struct Row2;
13552 struct Row2.1;
13553 struct Row2.2;
13554 «ˇ
13555 struct Row4;
13556 struct» Row5;
13557 «struct Row6;
13558 ˇ»
13559 struct Row9.1;
13560 struct Row9.2;
13561 struct Row9.3;
13562 struct Row8;
13563 struct Row9;
13564 struct Row10;"#},
13565 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13566 indoc! {r#"struct Row;
13567 struct Row1;
13568 struct Row2;
13569 struct Row2.1;
13570 struct Row2.2;
13571 «ˇ
13572 struct Row4;
13573 struct» Row5;
13574 «struct Row6;
13575 ˇ»
13576 struct Row9.1;
13577 struct Row9.2;
13578 struct Row9.3;
13579 struct Row8;
13580 struct Row9;
13581 struct Row10;"#},
13582 base_text,
13583 &mut cx,
13584 );
13585
13586 // When carets and selections intersect the addition hunks, those are reverted.
13587 // Adjacent carets got merged.
13588 assert_hunk_revert(
13589 indoc! {r#"struct Row;
13590 ˇ// something on the top
13591 struct Row1;
13592 struct Row2;
13593 struct Roˇw3.1;
13594 struct Row2.2;
13595 struct Row2.3;ˇ
13596
13597 struct Row4;
13598 struct ˇRow5.1;
13599 struct Row5.2;
13600 struct «Rowˇ»5.3;
13601 struct Row5;
13602 struct Row6;
13603 ˇ
13604 struct Row9.1;
13605 struct «Rowˇ»9.2;
13606 struct «ˇRow»9.3;
13607 struct Row8;
13608 struct Row9;
13609 «ˇ// something on bottom»
13610 struct Row10;"#},
13611 vec![
13612 DiffHunkStatusKind::Added,
13613 DiffHunkStatusKind::Added,
13614 DiffHunkStatusKind::Added,
13615 DiffHunkStatusKind::Added,
13616 DiffHunkStatusKind::Added,
13617 ],
13618 indoc! {r#"struct Row;
13619 ˇstruct Row1;
13620 struct Row2;
13621 ˇ
13622 struct Row4;
13623 ˇstruct Row5;
13624 struct Row6;
13625 ˇ
13626 ˇstruct Row8;
13627 struct Row9;
13628 ˇstruct Row10;"#},
13629 base_text,
13630 &mut cx,
13631 );
13632}
13633
13634#[gpui::test]
13635async fn test_modification_reverts(cx: &mut TestAppContext) {
13636 init_test(cx, |_| {});
13637 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13638 let base_text = indoc! {r#"
13639 struct Row;
13640 struct Row1;
13641 struct Row2;
13642
13643 struct Row4;
13644 struct Row5;
13645 struct Row6;
13646
13647 struct Row8;
13648 struct Row9;
13649 struct Row10;"#};
13650
13651 // Modification hunks behave the same as the addition ones.
13652 assert_hunk_revert(
13653 indoc! {r#"struct Row;
13654 struct Row1;
13655 struct Row33;
13656 ˇ
13657 struct Row4;
13658 struct Row5;
13659 struct Row6;
13660 ˇ
13661 struct Row99;
13662 struct Row9;
13663 struct Row10;"#},
13664 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13665 indoc! {r#"struct Row;
13666 struct Row1;
13667 struct Row33;
13668 ˇ
13669 struct Row4;
13670 struct Row5;
13671 struct Row6;
13672 ˇ
13673 struct Row99;
13674 struct Row9;
13675 struct Row10;"#},
13676 base_text,
13677 &mut cx,
13678 );
13679 assert_hunk_revert(
13680 indoc! {r#"struct Row;
13681 struct Row1;
13682 struct Row33;
13683 «ˇ
13684 struct Row4;
13685 struct» Row5;
13686 «struct Row6;
13687 ˇ»
13688 struct Row99;
13689 struct Row9;
13690 struct Row10;"#},
13691 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13692 indoc! {r#"struct Row;
13693 struct Row1;
13694 struct Row33;
13695 «ˇ
13696 struct Row4;
13697 struct» Row5;
13698 «struct Row6;
13699 ˇ»
13700 struct Row99;
13701 struct Row9;
13702 struct Row10;"#},
13703 base_text,
13704 &mut cx,
13705 );
13706
13707 assert_hunk_revert(
13708 indoc! {r#"ˇstruct Row1.1;
13709 struct Row1;
13710 «ˇstr»uct Row22;
13711
13712 struct ˇRow44;
13713 struct Row5;
13714 struct «Rˇ»ow66;ˇ
13715
13716 «struˇ»ct Row88;
13717 struct Row9;
13718 struct Row1011;ˇ"#},
13719 vec![
13720 DiffHunkStatusKind::Modified,
13721 DiffHunkStatusKind::Modified,
13722 DiffHunkStatusKind::Modified,
13723 DiffHunkStatusKind::Modified,
13724 DiffHunkStatusKind::Modified,
13725 DiffHunkStatusKind::Modified,
13726 ],
13727 indoc! {r#"struct Row;
13728 ˇstruct Row1;
13729 struct Row2;
13730 ˇ
13731 struct Row4;
13732 ˇstruct Row5;
13733 struct Row6;
13734 ˇ
13735 struct Row8;
13736 ˇstruct Row9;
13737 struct Row10;ˇ"#},
13738 base_text,
13739 &mut cx,
13740 );
13741}
13742
13743#[gpui::test]
13744async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13745 init_test(cx, |_| {});
13746 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13747 let base_text = indoc! {r#"
13748 one
13749
13750 two
13751 three
13752 "#};
13753
13754 cx.set_head_text(base_text);
13755 cx.set_state("\nˇ\n");
13756 cx.executor().run_until_parked();
13757 cx.update_editor(|editor, _window, cx| {
13758 editor.expand_selected_diff_hunks(cx);
13759 });
13760 cx.executor().run_until_parked();
13761 cx.update_editor(|editor, window, cx| {
13762 editor.backspace(&Default::default(), window, cx);
13763 });
13764 cx.run_until_parked();
13765 cx.assert_state_with_diff(
13766 indoc! {r#"
13767
13768 - two
13769 - threeˇ
13770 +
13771 "#}
13772 .to_string(),
13773 );
13774}
13775
13776#[gpui::test]
13777async fn test_deletion_reverts(cx: &mut TestAppContext) {
13778 init_test(cx, |_| {});
13779 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13780 let base_text = indoc! {r#"struct Row;
13781struct Row1;
13782struct Row2;
13783
13784struct Row4;
13785struct Row5;
13786struct Row6;
13787
13788struct Row8;
13789struct Row9;
13790struct Row10;"#};
13791
13792 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13793 assert_hunk_revert(
13794 indoc! {r#"struct Row;
13795 struct Row2;
13796
13797 ˇstruct Row4;
13798 struct Row5;
13799 struct Row6;
13800 ˇ
13801 struct Row8;
13802 struct Row10;"#},
13803 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13804 indoc! {r#"struct Row;
13805 struct Row2;
13806
13807 ˇstruct Row4;
13808 struct Row5;
13809 struct Row6;
13810 ˇ
13811 struct Row8;
13812 struct Row10;"#},
13813 base_text,
13814 &mut cx,
13815 );
13816 assert_hunk_revert(
13817 indoc! {r#"struct Row;
13818 struct Row2;
13819
13820 «ˇstruct Row4;
13821 struct» Row5;
13822 «struct Row6;
13823 ˇ»
13824 struct Row8;
13825 struct Row10;"#},
13826 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13827 indoc! {r#"struct Row;
13828 struct Row2;
13829
13830 «ˇstruct Row4;
13831 struct» Row5;
13832 «struct Row6;
13833 ˇ»
13834 struct Row8;
13835 struct Row10;"#},
13836 base_text,
13837 &mut cx,
13838 );
13839
13840 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13841 assert_hunk_revert(
13842 indoc! {r#"struct Row;
13843 ˇstruct Row2;
13844
13845 struct Row4;
13846 struct Row5;
13847 struct Row6;
13848
13849 struct Row8;ˇ
13850 struct Row10;"#},
13851 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13852 indoc! {r#"struct Row;
13853 struct Row1;
13854 ˇstruct Row2;
13855
13856 struct Row4;
13857 struct Row5;
13858 struct Row6;
13859
13860 struct Row8;ˇ
13861 struct Row9;
13862 struct Row10;"#},
13863 base_text,
13864 &mut cx,
13865 );
13866 assert_hunk_revert(
13867 indoc! {r#"struct Row;
13868 struct Row2«ˇ;
13869 struct Row4;
13870 struct» Row5;
13871 «struct Row6;
13872
13873 struct Row8;ˇ»
13874 struct Row10;"#},
13875 vec![
13876 DiffHunkStatusKind::Deleted,
13877 DiffHunkStatusKind::Deleted,
13878 DiffHunkStatusKind::Deleted,
13879 ],
13880 indoc! {r#"struct Row;
13881 struct Row1;
13882 struct Row2«ˇ;
13883
13884 struct Row4;
13885 struct» Row5;
13886 «struct Row6;
13887
13888 struct Row8;ˇ»
13889 struct Row9;
13890 struct Row10;"#},
13891 base_text,
13892 &mut cx,
13893 );
13894}
13895
13896#[gpui::test]
13897async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13898 init_test(cx, |_| {});
13899
13900 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13901 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13902 let base_text_3 =
13903 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13904
13905 let text_1 = edit_first_char_of_every_line(base_text_1);
13906 let text_2 = edit_first_char_of_every_line(base_text_2);
13907 let text_3 = edit_first_char_of_every_line(base_text_3);
13908
13909 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13910 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13911 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13912
13913 let multibuffer = cx.new(|cx| {
13914 let mut multibuffer = MultiBuffer::new(ReadWrite);
13915 multibuffer.push_excerpts(
13916 buffer_1.clone(),
13917 [
13918 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13919 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13920 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13921 ],
13922 cx,
13923 );
13924 multibuffer.push_excerpts(
13925 buffer_2.clone(),
13926 [
13927 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13928 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13929 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13930 ],
13931 cx,
13932 );
13933 multibuffer.push_excerpts(
13934 buffer_3.clone(),
13935 [
13936 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13937 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13938 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13939 ],
13940 cx,
13941 );
13942 multibuffer
13943 });
13944
13945 let fs = FakeFs::new(cx.executor());
13946 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13947 let (editor, cx) = cx
13948 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13949 editor.update_in(cx, |editor, _window, cx| {
13950 for (buffer, diff_base) in [
13951 (buffer_1.clone(), base_text_1),
13952 (buffer_2.clone(), base_text_2),
13953 (buffer_3.clone(), base_text_3),
13954 ] {
13955 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13956 editor
13957 .buffer
13958 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13959 }
13960 });
13961 cx.executor().run_until_parked();
13962
13963 editor.update_in(cx, |editor, window, cx| {
13964 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}");
13965 editor.select_all(&SelectAll, window, cx);
13966 editor.git_restore(&Default::default(), window, cx);
13967 });
13968 cx.executor().run_until_parked();
13969
13970 // When all ranges are selected, all buffer hunks are reverted.
13971 editor.update(cx, |editor, cx| {
13972 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");
13973 });
13974 buffer_1.update(cx, |buffer, _| {
13975 assert_eq!(buffer.text(), base_text_1);
13976 });
13977 buffer_2.update(cx, |buffer, _| {
13978 assert_eq!(buffer.text(), base_text_2);
13979 });
13980 buffer_3.update(cx, |buffer, _| {
13981 assert_eq!(buffer.text(), base_text_3);
13982 });
13983
13984 editor.update_in(cx, |editor, window, cx| {
13985 editor.undo(&Default::default(), window, cx);
13986 });
13987
13988 editor.update_in(cx, |editor, window, cx| {
13989 editor.change_selections(None, window, cx, |s| {
13990 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13991 });
13992 editor.git_restore(&Default::default(), window, cx);
13993 });
13994
13995 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13996 // but not affect buffer_2 and its related excerpts.
13997 editor.update(cx, |editor, cx| {
13998 assert_eq!(
13999 editor.text(cx),
14000 "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}"
14001 );
14002 });
14003 buffer_1.update(cx, |buffer, _| {
14004 assert_eq!(buffer.text(), base_text_1);
14005 });
14006 buffer_2.update(cx, |buffer, _| {
14007 assert_eq!(
14008 buffer.text(),
14009 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14010 );
14011 });
14012 buffer_3.update(cx, |buffer, _| {
14013 assert_eq!(
14014 buffer.text(),
14015 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14016 );
14017 });
14018
14019 fn edit_first_char_of_every_line(text: &str) -> String {
14020 text.split('\n')
14021 .map(|line| format!("X{}", &line[1..]))
14022 .collect::<Vec<_>>()
14023 .join("\n")
14024 }
14025}
14026
14027#[gpui::test]
14028async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14029 init_test(cx, |_| {});
14030
14031 let cols = 4;
14032 let rows = 10;
14033 let sample_text_1 = sample_text(rows, cols, 'a');
14034 assert_eq!(
14035 sample_text_1,
14036 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14037 );
14038 let sample_text_2 = sample_text(rows, cols, 'l');
14039 assert_eq!(
14040 sample_text_2,
14041 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14042 );
14043 let sample_text_3 = sample_text(rows, cols, 'v');
14044 assert_eq!(
14045 sample_text_3,
14046 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14047 );
14048
14049 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14050 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14051 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14052
14053 let multi_buffer = cx.new(|cx| {
14054 let mut multibuffer = MultiBuffer::new(ReadWrite);
14055 multibuffer.push_excerpts(
14056 buffer_1.clone(),
14057 [
14058 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14059 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14060 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14061 ],
14062 cx,
14063 );
14064 multibuffer.push_excerpts(
14065 buffer_2.clone(),
14066 [
14067 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14068 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14069 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14070 ],
14071 cx,
14072 );
14073 multibuffer.push_excerpts(
14074 buffer_3.clone(),
14075 [
14076 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14077 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14078 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14079 ],
14080 cx,
14081 );
14082 multibuffer
14083 });
14084
14085 let fs = FakeFs::new(cx.executor());
14086 fs.insert_tree(
14087 "/a",
14088 json!({
14089 "main.rs": sample_text_1,
14090 "other.rs": sample_text_2,
14091 "lib.rs": sample_text_3,
14092 }),
14093 )
14094 .await;
14095 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14096 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14097 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14098 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14099 Editor::new(
14100 EditorMode::Full,
14101 multi_buffer,
14102 Some(project.clone()),
14103 window,
14104 cx,
14105 )
14106 });
14107 let multibuffer_item_id = workspace
14108 .update(cx, |workspace, window, cx| {
14109 assert!(
14110 workspace.active_item(cx).is_none(),
14111 "active item should be None before the first item is added"
14112 );
14113 workspace.add_item_to_active_pane(
14114 Box::new(multi_buffer_editor.clone()),
14115 None,
14116 true,
14117 window,
14118 cx,
14119 );
14120 let active_item = workspace
14121 .active_item(cx)
14122 .expect("should have an active item after adding the multi buffer");
14123 assert!(
14124 !active_item.is_singleton(cx),
14125 "A multi buffer was expected to active after adding"
14126 );
14127 active_item.item_id()
14128 })
14129 .unwrap();
14130 cx.executor().run_until_parked();
14131
14132 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14133 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14134 s.select_ranges(Some(1..2))
14135 });
14136 editor.open_excerpts(&OpenExcerpts, window, cx);
14137 });
14138 cx.executor().run_until_parked();
14139 let first_item_id = workspace
14140 .update(cx, |workspace, window, cx| {
14141 let active_item = workspace
14142 .active_item(cx)
14143 .expect("should have an active item after navigating into the 1st buffer");
14144 let first_item_id = active_item.item_id();
14145 assert_ne!(
14146 first_item_id, multibuffer_item_id,
14147 "Should navigate into the 1st buffer and activate it"
14148 );
14149 assert!(
14150 active_item.is_singleton(cx),
14151 "New active item should be a singleton buffer"
14152 );
14153 assert_eq!(
14154 active_item
14155 .act_as::<Editor>(cx)
14156 .expect("should have navigated into an editor for the 1st buffer")
14157 .read(cx)
14158 .text(cx),
14159 sample_text_1
14160 );
14161
14162 workspace
14163 .go_back(workspace.active_pane().downgrade(), window, cx)
14164 .detach_and_log_err(cx);
14165
14166 first_item_id
14167 })
14168 .unwrap();
14169 cx.executor().run_until_parked();
14170 workspace
14171 .update(cx, |workspace, _, cx| {
14172 let active_item = workspace
14173 .active_item(cx)
14174 .expect("should have an active item after navigating back");
14175 assert_eq!(
14176 active_item.item_id(),
14177 multibuffer_item_id,
14178 "Should navigate back to the multi buffer"
14179 );
14180 assert!(!active_item.is_singleton(cx));
14181 })
14182 .unwrap();
14183
14184 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14185 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14186 s.select_ranges(Some(39..40))
14187 });
14188 editor.open_excerpts(&OpenExcerpts, window, cx);
14189 });
14190 cx.executor().run_until_parked();
14191 let second_item_id = workspace
14192 .update(cx, |workspace, window, cx| {
14193 let active_item = workspace
14194 .active_item(cx)
14195 .expect("should have an active item after navigating into the 2nd buffer");
14196 let second_item_id = active_item.item_id();
14197 assert_ne!(
14198 second_item_id, multibuffer_item_id,
14199 "Should navigate away from the multibuffer"
14200 );
14201 assert_ne!(
14202 second_item_id, first_item_id,
14203 "Should navigate into the 2nd 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")
14213 .read(cx)
14214 .text(cx),
14215 sample_text_2
14216 );
14217
14218 workspace
14219 .go_back(workspace.active_pane().downgrade(), window, cx)
14220 .detach_and_log_err(cx);
14221
14222 second_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 from the 2nd buffer");
14231 assert_eq!(
14232 active_item.item_id(),
14233 multibuffer_item_id,
14234 "Should navigate back from the 2nd buffer 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(70..70))
14243 });
14244 editor.open_excerpts(&OpenExcerpts, window, cx);
14245 });
14246 cx.executor().run_until_parked();
14247 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 3rd buffer");
14252 let third_item_id = active_item.item_id();
14253 assert_ne!(
14254 third_item_id, multibuffer_item_id,
14255 "Should navigate into the 3rd buffer and activate it"
14256 );
14257 assert_ne!(third_item_id, first_item_id);
14258 assert_ne!(third_item_id, second_item_id);
14259 assert!(
14260 active_item.is_singleton(cx),
14261 "New active item should be a singleton buffer"
14262 );
14263 assert_eq!(
14264 active_item
14265 .act_as::<Editor>(cx)
14266 .expect("should have navigated into an editor")
14267 .read(cx)
14268 .text(cx),
14269 sample_text_3
14270 );
14271
14272 workspace
14273 .go_back(workspace.active_pane().downgrade(), window, cx)
14274 .detach_and_log_err(cx);
14275 })
14276 .unwrap();
14277 cx.executor().run_until_parked();
14278 workspace
14279 .update(cx, |workspace, _, cx| {
14280 let active_item = workspace
14281 .active_item(cx)
14282 .expect("should have an active item after navigating back from the 3rd buffer");
14283 assert_eq!(
14284 active_item.item_id(),
14285 multibuffer_item_id,
14286 "Should navigate back from the 3rd buffer to the multi buffer"
14287 );
14288 assert!(!active_item.is_singleton(cx));
14289 })
14290 .unwrap();
14291}
14292
14293#[gpui::test]
14294async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14295 init_test(cx, |_| {});
14296
14297 let mut cx = EditorTestContext::new(cx).await;
14298
14299 let diff_base = r#"
14300 use some::mod;
14301
14302 const A: u32 = 42;
14303
14304 fn main() {
14305 println!("hello");
14306
14307 println!("world");
14308 }
14309 "#
14310 .unindent();
14311
14312 cx.set_state(
14313 &r#"
14314 use some::modified;
14315
14316 ˇ
14317 fn main() {
14318 println!("hello there");
14319
14320 println!("around the");
14321 println!("world");
14322 }
14323 "#
14324 .unindent(),
14325 );
14326
14327 cx.set_head_text(&diff_base);
14328 executor.run_until_parked();
14329
14330 cx.update_editor(|editor, window, cx| {
14331 editor.go_to_next_hunk(&GoToHunk, window, cx);
14332 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14333 });
14334 executor.run_until_parked();
14335 cx.assert_state_with_diff(
14336 r#"
14337 use some::modified;
14338
14339
14340 fn main() {
14341 - println!("hello");
14342 + ˇ println!("hello there");
14343
14344 println!("around the");
14345 println!("world");
14346 }
14347 "#
14348 .unindent(),
14349 );
14350
14351 cx.update_editor(|editor, window, cx| {
14352 for _ in 0..2 {
14353 editor.go_to_next_hunk(&GoToHunk, window, cx);
14354 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14355 }
14356 });
14357 executor.run_until_parked();
14358 cx.assert_state_with_diff(
14359 r#"
14360 - use some::mod;
14361 + ˇuse some::modified;
14362
14363
14364 fn main() {
14365 - println!("hello");
14366 + println!("hello there");
14367
14368 + println!("around the");
14369 println!("world");
14370 }
14371 "#
14372 .unindent(),
14373 );
14374
14375 cx.update_editor(|editor, window, cx| {
14376 editor.go_to_next_hunk(&GoToHunk, window, cx);
14377 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14378 });
14379 executor.run_until_parked();
14380 cx.assert_state_with_diff(
14381 r#"
14382 - use some::mod;
14383 + use some::modified;
14384
14385 - const A: u32 = 42;
14386 ˇ
14387 fn main() {
14388 - println!("hello");
14389 + println!("hello there");
14390
14391 + println!("around the");
14392 println!("world");
14393 }
14394 "#
14395 .unindent(),
14396 );
14397
14398 cx.update_editor(|editor, window, cx| {
14399 editor.cancel(&Cancel, window, cx);
14400 });
14401
14402 cx.assert_state_with_diff(
14403 r#"
14404 use some::modified;
14405
14406 ˇ
14407 fn main() {
14408 println!("hello there");
14409
14410 println!("around the");
14411 println!("world");
14412 }
14413 "#
14414 .unindent(),
14415 );
14416}
14417
14418#[gpui::test]
14419async fn test_diff_base_change_with_expanded_diff_hunks(
14420 executor: BackgroundExecutor,
14421 cx: &mut TestAppContext,
14422) {
14423 init_test(cx, |_| {});
14424
14425 let mut cx = EditorTestContext::new(cx).await;
14426
14427 let diff_base = r#"
14428 use some::mod1;
14429 use some::mod2;
14430
14431 const A: u32 = 42;
14432 const B: u32 = 42;
14433 const C: u32 = 42;
14434
14435 fn main() {
14436 println!("hello");
14437
14438 println!("world");
14439 }
14440 "#
14441 .unindent();
14442
14443 cx.set_state(
14444 &r#"
14445 use some::mod2;
14446
14447 const A: u32 = 42;
14448 const C: u32 = 42;
14449
14450 fn main(ˇ) {
14451 //println!("hello");
14452
14453 println!("world");
14454 //
14455 //
14456 }
14457 "#
14458 .unindent(),
14459 );
14460
14461 cx.set_head_text(&diff_base);
14462 executor.run_until_parked();
14463
14464 cx.update_editor(|editor, window, cx| {
14465 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14466 });
14467 executor.run_until_parked();
14468 cx.assert_state_with_diff(
14469 r#"
14470 - use some::mod1;
14471 use some::mod2;
14472
14473 const A: u32 = 42;
14474 - const B: u32 = 42;
14475 const C: u32 = 42;
14476
14477 fn main(ˇ) {
14478 - println!("hello");
14479 + //println!("hello");
14480
14481 println!("world");
14482 + //
14483 + //
14484 }
14485 "#
14486 .unindent(),
14487 );
14488
14489 cx.set_head_text("new diff base!");
14490 executor.run_until_parked();
14491 cx.assert_state_with_diff(
14492 r#"
14493 - new diff base!
14494 + use some::mod2;
14495 +
14496 + const A: u32 = 42;
14497 + const C: u32 = 42;
14498 +
14499 + fn main(ˇ) {
14500 + //println!("hello");
14501 +
14502 + println!("world");
14503 + //
14504 + //
14505 + }
14506 "#
14507 .unindent(),
14508 );
14509}
14510
14511#[gpui::test]
14512async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14513 init_test(cx, |_| {});
14514
14515 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14516 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14517 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14518 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14519 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14520 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14521
14522 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14523 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14524 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14525
14526 let multi_buffer = cx.new(|cx| {
14527 let mut multibuffer = MultiBuffer::new(ReadWrite);
14528 multibuffer.push_excerpts(
14529 buffer_1.clone(),
14530 [
14531 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14532 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14533 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14534 ],
14535 cx,
14536 );
14537 multibuffer.push_excerpts(
14538 buffer_2.clone(),
14539 [
14540 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14541 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14542 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14543 ],
14544 cx,
14545 );
14546 multibuffer.push_excerpts(
14547 buffer_3.clone(),
14548 [
14549 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14550 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14551 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14552 ],
14553 cx,
14554 );
14555 multibuffer
14556 });
14557
14558 let editor =
14559 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14560 editor
14561 .update(cx, |editor, _window, cx| {
14562 for (buffer, diff_base) in [
14563 (buffer_1.clone(), file_1_old),
14564 (buffer_2.clone(), file_2_old),
14565 (buffer_3.clone(), file_3_old),
14566 ] {
14567 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14568 editor
14569 .buffer
14570 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14571 }
14572 })
14573 .unwrap();
14574
14575 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14576 cx.run_until_parked();
14577
14578 cx.assert_editor_state(
14579 &"
14580 ˇaaa
14581 ccc
14582 ddd
14583
14584 ggg
14585 hhh
14586
14587
14588 lll
14589 mmm
14590 NNN
14591
14592 qqq
14593 rrr
14594
14595 uuu
14596 111
14597 222
14598 333
14599
14600 666
14601 777
14602
14603 000
14604 !!!"
14605 .unindent(),
14606 );
14607
14608 cx.update_editor(|editor, window, cx| {
14609 editor.select_all(&SelectAll, window, cx);
14610 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14611 });
14612 cx.executor().run_until_parked();
14613
14614 cx.assert_state_with_diff(
14615 "
14616 «aaa
14617 - bbb
14618 ccc
14619 ddd
14620
14621 ggg
14622 hhh
14623
14624
14625 lll
14626 mmm
14627 - nnn
14628 + NNN
14629
14630 qqq
14631 rrr
14632
14633 uuu
14634 111
14635 222
14636 333
14637
14638 + 666
14639 777
14640
14641 000
14642 !!!ˇ»"
14643 .unindent(),
14644 );
14645}
14646
14647#[gpui::test]
14648async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14649 init_test(cx, |_| {});
14650
14651 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14652 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14653
14654 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14655 let multi_buffer = cx.new(|cx| {
14656 let mut multibuffer = MultiBuffer::new(ReadWrite);
14657 multibuffer.push_excerpts(
14658 buffer.clone(),
14659 [
14660 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
14661 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
14662 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
14663 ],
14664 cx,
14665 );
14666 multibuffer
14667 });
14668
14669 let editor =
14670 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14671 editor
14672 .update(cx, |editor, _window, cx| {
14673 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14674 editor
14675 .buffer
14676 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14677 })
14678 .unwrap();
14679
14680 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14681 cx.run_until_parked();
14682
14683 cx.update_editor(|editor, window, cx| {
14684 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14685 });
14686 cx.executor().run_until_parked();
14687
14688 // When the start of a hunk coincides with the start of its excerpt,
14689 // the hunk is expanded. When the start of a a hunk is earlier than
14690 // the start of its excerpt, the hunk is not expanded.
14691 cx.assert_state_with_diff(
14692 "
14693 ˇaaa
14694 - bbb
14695 + BBB
14696
14697 - ddd
14698 - eee
14699 + DDD
14700 + EEE
14701 fff
14702
14703 iii
14704 "
14705 .unindent(),
14706 );
14707}
14708
14709#[gpui::test]
14710async fn test_edits_around_expanded_insertion_hunks(
14711 executor: BackgroundExecutor,
14712 cx: &mut TestAppContext,
14713) {
14714 init_test(cx, |_| {});
14715
14716 let mut cx = EditorTestContext::new(cx).await;
14717
14718 let diff_base = r#"
14719 use some::mod1;
14720 use some::mod2;
14721
14722 const A: u32 = 42;
14723
14724 fn main() {
14725 println!("hello");
14726
14727 println!("world");
14728 }
14729 "#
14730 .unindent();
14731 executor.run_until_parked();
14732 cx.set_state(
14733 &r#"
14734 use some::mod1;
14735 use some::mod2;
14736
14737 const A: u32 = 42;
14738 const B: u32 = 42;
14739 const C: u32 = 42;
14740 ˇ
14741
14742 fn main() {
14743 println!("hello");
14744
14745 println!("world");
14746 }
14747 "#
14748 .unindent(),
14749 );
14750
14751 cx.set_head_text(&diff_base);
14752 executor.run_until_parked();
14753
14754 cx.update_editor(|editor, window, cx| {
14755 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14756 });
14757 executor.run_until_parked();
14758
14759 cx.assert_state_with_diff(
14760 r#"
14761 use some::mod1;
14762 use some::mod2;
14763
14764 const A: u32 = 42;
14765 + const B: u32 = 42;
14766 + const C: u32 = 42;
14767 + ˇ
14768
14769 fn main() {
14770 println!("hello");
14771
14772 println!("world");
14773 }
14774 "#
14775 .unindent(),
14776 );
14777
14778 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14779 executor.run_until_parked();
14780
14781 cx.assert_state_with_diff(
14782 r#"
14783 use some::mod1;
14784 use some::mod2;
14785
14786 const A: u32 = 42;
14787 + const B: u32 = 42;
14788 + const C: u32 = 42;
14789 + const D: u32 = 42;
14790 + ˇ
14791
14792 fn main() {
14793 println!("hello");
14794
14795 println!("world");
14796 }
14797 "#
14798 .unindent(),
14799 );
14800
14801 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14802 executor.run_until_parked();
14803
14804 cx.assert_state_with_diff(
14805 r#"
14806 use some::mod1;
14807 use some::mod2;
14808
14809 const A: u32 = 42;
14810 + const B: u32 = 42;
14811 + const C: u32 = 42;
14812 + const D: u32 = 42;
14813 + const E: u32 = 42;
14814 + ˇ
14815
14816 fn main() {
14817 println!("hello");
14818
14819 println!("world");
14820 }
14821 "#
14822 .unindent(),
14823 );
14824
14825 cx.update_editor(|editor, window, cx| {
14826 editor.delete_line(&DeleteLine, window, cx);
14827 });
14828 executor.run_until_parked();
14829
14830 cx.assert_state_with_diff(
14831 r#"
14832 use some::mod1;
14833 use some::mod2;
14834
14835 const A: u32 = 42;
14836 + const B: u32 = 42;
14837 + const C: u32 = 42;
14838 + const D: u32 = 42;
14839 + const E: u32 = 42;
14840 ˇ
14841 fn main() {
14842 println!("hello");
14843
14844 println!("world");
14845 }
14846 "#
14847 .unindent(),
14848 );
14849
14850 cx.update_editor(|editor, window, cx| {
14851 editor.move_up(&MoveUp, window, cx);
14852 editor.delete_line(&DeleteLine, window, cx);
14853 editor.move_up(&MoveUp, window, cx);
14854 editor.delete_line(&DeleteLine, window, cx);
14855 editor.move_up(&MoveUp, window, cx);
14856 editor.delete_line(&DeleteLine, window, cx);
14857 });
14858 executor.run_until_parked();
14859 cx.assert_state_with_diff(
14860 r#"
14861 use some::mod1;
14862 use some::mod2;
14863
14864 const A: u32 = 42;
14865 + const B: u32 = 42;
14866 ˇ
14867 fn main() {
14868 println!("hello");
14869
14870 println!("world");
14871 }
14872 "#
14873 .unindent(),
14874 );
14875
14876 cx.update_editor(|editor, window, cx| {
14877 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14878 editor.delete_line(&DeleteLine, window, cx);
14879 });
14880 executor.run_until_parked();
14881 cx.assert_state_with_diff(
14882 r#"
14883 ˇ
14884 fn main() {
14885 println!("hello");
14886
14887 println!("world");
14888 }
14889 "#
14890 .unindent(),
14891 );
14892}
14893
14894#[gpui::test]
14895async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14896 init_test(cx, |_| {});
14897
14898 let mut cx = EditorTestContext::new(cx).await;
14899 cx.set_head_text(indoc! { "
14900 one
14901 two
14902 three
14903 four
14904 five
14905 "
14906 });
14907 cx.set_state(indoc! { "
14908 one
14909 ˇthree
14910 five
14911 "});
14912 cx.run_until_parked();
14913 cx.update_editor(|editor, window, cx| {
14914 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14915 });
14916 cx.assert_state_with_diff(
14917 indoc! { "
14918 one
14919 - two
14920 ˇthree
14921 - four
14922 five
14923 "}
14924 .to_string(),
14925 );
14926 cx.update_editor(|editor, window, cx| {
14927 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14928 });
14929
14930 cx.assert_state_with_diff(
14931 indoc! { "
14932 one
14933 ˇthree
14934 five
14935 "}
14936 .to_string(),
14937 );
14938
14939 cx.set_state(indoc! { "
14940 one
14941 ˇTWO
14942 three
14943 four
14944 five
14945 "});
14946 cx.run_until_parked();
14947 cx.update_editor(|editor, window, cx| {
14948 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14949 });
14950
14951 cx.assert_state_with_diff(
14952 indoc! { "
14953 one
14954 - two
14955 + ˇTWO
14956 three
14957 four
14958 five
14959 "}
14960 .to_string(),
14961 );
14962 cx.update_editor(|editor, window, cx| {
14963 editor.move_up(&Default::default(), window, cx);
14964 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14965 });
14966 cx.assert_state_with_diff(
14967 indoc! { "
14968 one
14969 ˇTWO
14970 three
14971 four
14972 five
14973 "}
14974 .to_string(),
14975 );
14976}
14977
14978#[gpui::test]
14979async fn test_edits_around_expanded_deletion_hunks(
14980 executor: BackgroundExecutor,
14981 cx: &mut TestAppContext,
14982) {
14983 init_test(cx, |_| {});
14984
14985 let mut cx = EditorTestContext::new(cx).await;
14986
14987 let diff_base = r#"
14988 use some::mod1;
14989 use some::mod2;
14990
14991 const A: u32 = 42;
14992 const B: u32 = 42;
14993 const C: u32 = 42;
14994
14995
14996 fn main() {
14997 println!("hello");
14998
14999 println!("world");
15000 }
15001 "#
15002 .unindent();
15003 executor.run_until_parked();
15004 cx.set_state(
15005 &r#"
15006 use some::mod1;
15007 use some::mod2;
15008
15009 ˇconst B: u32 = 42;
15010 const C: u32 = 42;
15011
15012
15013 fn main() {
15014 println!("hello");
15015
15016 println!("world");
15017 }
15018 "#
15019 .unindent(),
15020 );
15021
15022 cx.set_head_text(&diff_base);
15023 executor.run_until_parked();
15024
15025 cx.update_editor(|editor, window, cx| {
15026 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15027 });
15028 executor.run_until_parked();
15029
15030 cx.assert_state_with_diff(
15031 r#"
15032 use some::mod1;
15033 use some::mod2;
15034
15035 - const A: u32 = 42;
15036 ˇconst B: u32 = 42;
15037 const C: u32 = 42;
15038
15039
15040 fn main() {
15041 println!("hello");
15042
15043 println!("world");
15044 }
15045 "#
15046 .unindent(),
15047 );
15048
15049 cx.update_editor(|editor, window, cx| {
15050 editor.delete_line(&DeleteLine, window, cx);
15051 });
15052 executor.run_until_parked();
15053 cx.assert_state_with_diff(
15054 r#"
15055 use some::mod1;
15056 use some::mod2;
15057
15058 - const A: u32 = 42;
15059 - const B: u32 = 42;
15060 ˇconst C: u32 = 42;
15061
15062
15063 fn main() {
15064 println!("hello");
15065
15066 println!("world");
15067 }
15068 "#
15069 .unindent(),
15070 );
15071
15072 cx.update_editor(|editor, window, cx| {
15073 editor.delete_line(&DeleteLine, window, cx);
15074 });
15075 executor.run_until_parked();
15076 cx.assert_state_with_diff(
15077 r#"
15078 use some::mod1;
15079 use some::mod2;
15080
15081 - const A: u32 = 42;
15082 - const B: u32 = 42;
15083 - const C: u32 = 42;
15084 ˇ
15085
15086 fn main() {
15087 println!("hello");
15088
15089 println!("world");
15090 }
15091 "#
15092 .unindent(),
15093 );
15094
15095 cx.update_editor(|editor, window, cx| {
15096 editor.handle_input("replacement", window, cx);
15097 });
15098 executor.run_until_parked();
15099 cx.assert_state_with_diff(
15100 r#"
15101 use some::mod1;
15102 use some::mod2;
15103
15104 - const A: u32 = 42;
15105 - const B: u32 = 42;
15106 - const C: u32 = 42;
15107 -
15108 + replacementˇ
15109
15110 fn main() {
15111 println!("hello");
15112
15113 println!("world");
15114 }
15115 "#
15116 .unindent(),
15117 );
15118}
15119
15120#[gpui::test]
15121async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15122 init_test(cx, |_| {});
15123
15124 let mut cx = EditorTestContext::new(cx).await;
15125
15126 let base_text = r#"
15127 one
15128 two
15129 three
15130 four
15131 five
15132 "#
15133 .unindent();
15134 executor.run_until_parked();
15135 cx.set_state(
15136 &r#"
15137 one
15138 two
15139 fˇour
15140 five
15141 "#
15142 .unindent(),
15143 );
15144
15145 cx.set_head_text(&base_text);
15146 executor.run_until_parked();
15147
15148 cx.update_editor(|editor, window, cx| {
15149 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15150 });
15151 executor.run_until_parked();
15152
15153 cx.assert_state_with_diff(
15154 r#"
15155 one
15156 two
15157 - three
15158 fˇour
15159 five
15160 "#
15161 .unindent(),
15162 );
15163
15164 cx.update_editor(|editor, window, cx| {
15165 editor.backspace(&Backspace, window, cx);
15166 editor.backspace(&Backspace, window, cx);
15167 });
15168 executor.run_until_parked();
15169 cx.assert_state_with_diff(
15170 r#"
15171 one
15172 two
15173 - threeˇ
15174 - four
15175 + our
15176 five
15177 "#
15178 .unindent(),
15179 );
15180}
15181
15182#[gpui::test]
15183async fn test_edit_after_expanded_modification_hunk(
15184 executor: BackgroundExecutor,
15185 cx: &mut TestAppContext,
15186) {
15187 init_test(cx, |_| {});
15188
15189 let mut cx = EditorTestContext::new(cx).await;
15190
15191 let diff_base = r#"
15192 use some::mod1;
15193 use some::mod2;
15194
15195 const A: u32 = 42;
15196 const B: u32 = 42;
15197 const C: u32 = 42;
15198 const D: u32 = 42;
15199
15200
15201 fn main() {
15202 println!("hello");
15203
15204 println!("world");
15205 }"#
15206 .unindent();
15207
15208 cx.set_state(
15209 &r#"
15210 use some::mod1;
15211 use some::mod2;
15212
15213 const A: u32 = 42;
15214 const B: u32 = 42;
15215 const C: u32 = 43ˇ
15216 const D: u32 = 42;
15217
15218
15219 fn main() {
15220 println!("hello");
15221
15222 println!("world");
15223 }"#
15224 .unindent(),
15225 );
15226
15227 cx.set_head_text(&diff_base);
15228 executor.run_until_parked();
15229 cx.update_editor(|editor, window, cx| {
15230 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15231 });
15232 executor.run_until_parked();
15233
15234 cx.assert_state_with_diff(
15235 r#"
15236 use some::mod1;
15237 use some::mod2;
15238
15239 const A: u32 = 42;
15240 const B: u32 = 42;
15241 - const C: u32 = 42;
15242 + const C: u32 = 43ˇ
15243 const D: u32 = 42;
15244
15245
15246 fn main() {
15247 println!("hello");
15248
15249 println!("world");
15250 }"#
15251 .unindent(),
15252 );
15253
15254 cx.update_editor(|editor, window, cx| {
15255 editor.handle_input("\nnew_line\n", window, cx);
15256 });
15257 executor.run_until_parked();
15258
15259 cx.assert_state_with_diff(
15260 r#"
15261 use some::mod1;
15262 use some::mod2;
15263
15264 const A: u32 = 42;
15265 const B: u32 = 42;
15266 - const C: u32 = 42;
15267 + const C: u32 = 43
15268 + new_line
15269 + ˇ
15270 const D: u32 = 42;
15271
15272
15273 fn main() {
15274 println!("hello");
15275
15276 println!("world");
15277 }"#
15278 .unindent(),
15279 );
15280}
15281
15282#[gpui::test]
15283async fn test_stage_and_unstage_added_file_hunk(
15284 executor: BackgroundExecutor,
15285 cx: &mut TestAppContext,
15286) {
15287 init_test(cx, |_| {});
15288
15289 let mut cx = EditorTestContext::new(cx).await;
15290 cx.update_editor(|editor, _, cx| {
15291 editor.set_expand_all_diff_hunks(cx);
15292 });
15293
15294 let working_copy = r#"
15295 ˇfn main() {
15296 println!("hello, world!");
15297 }
15298 "#
15299 .unindent();
15300
15301 cx.set_state(&working_copy);
15302 executor.run_until_parked();
15303
15304 cx.assert_state_with_diff(
15305 r#"
15306 + ˇfn main() {
15307 + println!("hello, world!");
15308 + }
15309 "#
15310 .unindent(),
15311 );
15312 cx.assert_index_text(None);
15313
15314 cx.update_editor(|editor, window, cx| {
15315 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15316 });
15317 executor.run_until_parked();
15318 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15319 cx.assert_state_with_diff(
15320 r#"
15321 + ˇfn main() {
15322 + println!("hello, world!");
15323 + }
15324 "#
15325 .unindent(),
15326 );
15327
15328 cx.update_editor(|editor, window, cx| {
15329 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15330 });
15331 executor.run_until_parked();
15332 cx.assert_index_text(None);
15333}
15334
15335async fn setup_indent_guides_editor(
15336 text: &str,
15337 cx: &mut TestAppContext,
15338) -> (BufferId, EditorTestContext) {
15339 init_test(cx, |_| {});
15340
15341 let mut cx = EditorTestContext::new(cx).await;
15342
15343 let buffer_id = cx.update_editor(|editor, window, cx| {
15344 editor.set_text(text, window, cx);
15345 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15346
15347 buffer_ids[0]
15348 });
15349
15350 (buffer_id, cx)
15351}
15352
15353fn assert_indent_guides(
15354 range: Range<u32>,
15355 expected: Vec<IndentGuide>,
15356 active_indices: Option<Vec<usize>>,
15357 cx: &mut EditorTestContext,
15358) {
15359 let indent_guides = cx.update_editor(|editor, window, cx| {
15360 let snapshot = editor.snapshot(window, cx).display_snapshot;
15361 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15362 editor,
15363 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15364 true,
15365 &snapshot,
15366 cx,
15367 );
15368
15369 indent_guides.sort_by(|a, b| {
15370 a.depth.cmp(&b.depth).then(
15371 a.start_row
15372 .cmp(&b.start_row)
15373 .then(a.end_row.cmp(&b.end_row)),
15374 )
15375 });
15376 indent_guides
15377 });
15378
15379 if let Some(expected) = active_indices {
15380 let active_indices = cx.update_editor(|editor, window, cx| {
15381 let snapshot = editor.snapshot(window, cx).display_snapshot;
15382 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15383 });
15384
15385 assert_eq!(
15386 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15387 expected,
15388 "Active indent guide indices do not match"
15389 );
15390 }
15391
15392 assert_eq!(indent_guides, expected, "Indent guides do not match");
15393}
15394
15395fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15396 IndentGuide {
15397 buffer_id,
15398 start_row: MultiBufferRow(start_row),
15399 end_row: MultiBufferRow(end_row),
15400 depth,
15401 tab_size: 4,
15402 settings: IndentGuideSettings {
15403 enabled: true,
15404 line_width: 1,
15405 active_line_width: 1,
15406 ..Default::default()
15407 },
15408 }
15409}
15410
15411#[gpui::test]
15412async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15413 let (buffer_id, mut cx) = setup_indent_guides_editor(
15414 &"
15415 fn main() {
15416 let a = 1;
15417 }"
15418 .unindent(),
15419 cx,
15420 )
15421 .await;
15422
15423 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15424}
15425
15426#[gpui::test]
15427async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15428 let (buffer_id, mut cx) = setup_indent_guides_editor(
15429 &"
15430 fn main() {
15431 let a = 1;
15432 let b = 2;
15433 }"
15434 .unindent(),
15435 cx,
15436 )
15437 .await;
15438
15439 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15440}
15441
15442#[gpui::test]
15443async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15444 let (buffer_id, mut cx) = setup_indent_guides_editor(
15445 &"
15446 fn main() {
15447 let a = 1;
15448 if a == 3 {
15449 let b = 2;
15450 } else {
15451 let c = 3;
15452 }
15453 }"
15454 .unindent(),
15455 cx,
15456 )
15457 .await;
15458
15459 assert_indent_guides(
15460 0..8,
15461 vec![
15462 indent_guide(buffer_id, 1, 6, 0),
15463 indent_guide(buffer_id, 3, 3, 1),
15464 indent_guide(buffer_id, 5, 5, 1),
15465 ],
15466 None,
15467 &mut cx,
15468 );
15469}
15470
15471#[gpui::test]
15472async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15473 let (buffer_id, mut cx) = setup_indent_guides_editor(
15474 &"
15475 fn main() {
15476 let a = 1;
15477 let b = 2;
15478 let c = 3;
15479 }"
15480 .unindent(),
15481 cx,
15482 )
15483 .await;
15484
15485 assert_indent_guides(
15486 0..5,
15487 vec![
15488 indent_guide(buffer_id, 1, 3, 0),
15489 indent_guide(buffer_id, 2, 2, 1),
15490 ],
15491 None,
15492 &mut cx,
15493 );
15494}
15495
15496#[gpui::test]
15497async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15498 let (buffer_id, mut cx) = setup_indent_guides_editor(
15499 &"
15500 fn main() {
15501 let a = 1;
15502
15503 let c = 3;
15504 }"
15505 .unindent(),
15506 cx,
15507 )
15508 .await;
15509
15510 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15511}
15512
15513#[gpui::test]
15514async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15515 let (buffer_id, mut cx) = setup_indent_guides_editor(
15516 &"
15517 fn main() {
15518 let a = 1;
15519
15520 let c = 3;
15521
15522 if a == 3 {
15523 let b = 2;
15524 } else {
15525 let c = 3;
15526 }
15527 }"
15528 .unindent(),
15529 cx,
15530 )
15531 .await;
15532
15533 assert_indent_guides(
15534 0..11,
15535 vec![
15536 indent_guide(buffer_id, 1, 9, 0),
15537 indent_guide(buffer_id, 6, 6, 1),
15538 indent_guide(buffer_id, 8, 8, 1),
15539 ],
15540 None,
15541 &mut cx,
15542 );
15543}
15544
15545#[gpui::test]
15546async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15547 let (buffer_id, mut cx) = setup_indent_guides_editor(
15548 &"
15549 fn main() {
15550 let a = 1;
15551
15552 let c = 3;
15553
15554 if a == 3 {
15555 let b = 2;
15556 } else {
15557 let c = 3;
15558 }
15559 }"
15560 .unindent(),
15561 cx,
15562 )
15563 .await;
15564
15565 assert_indent_guides(
15566 1..11,
15567 vec![
15568 indent_guide(buffer_id, 1, 9, 0),
15569 indent_guide(buffer_id, 6, 6, 1),
15570 indent_guide(buffer_id, 8, 8, 1),
15571 ],
15572 None,
15573 &mut cx,
15574 );
15575}
15576
15577#[gpui::test]
15578async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15579 let (buffer_id, mut cx) = setup_indent_guides_editor(
15580 &"
15581 fn main() {
15582 let a = 1;
15583
15584 let c = 3;
15585
15586 if a == 3 {
15587 let b = 2;
15588 } else {
15589 let c = 3;
15590 }
15591 }"
15592 .unindent(),
15593 cx,
15594 )
15595 .await;
15596
15597 assert_indent_guides(
15598 1..10,
15599 vec![
15600 indent_guide(buffer_id, 1, 9, 0),
15601 indent_guide(buffer_id, 6, 6, 1),
15602 indent_guide(buffer_id, 8, 8, 1),
15603 ],
15604 None,
15605 &mut cx,
15606 );
15607}
15608
15609#[gpui::test]
15610async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15611 let (buffer_id, mut cx) = setup_indent_guides_editor(
15612 &"
15613 block1
15614 block2
15615 block3
15616 block4
15617 block2
15618 block1
15619 block1"
15620 .unindent(),
15621 cx,
15622 )
15623 .await;
15624
15625 assert_indent_guides(
15626 1..10,
15627 vec![
15628 indent_guide(buffer_id, 1, 4, 0),
15629 indent_guide(buffer_id, 2, 3, 1),
15630 indent_guide(buffer_id, 3, 3, 2),
15631 ],
15632 None,
15633 &mut cx,
15634 );
15635}
15636
15637#[gpui::test]
15638async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15639 let (buffer_id, mut cx) = setup_indent_guides_editor(
15640 &"
15641 block1
15642 block2
15643 block3
15644
15645 block1
15646 block1"
15647 .unindent(),
15648 cx,
15649 )
15650 .await;
15651
15652 assert_indent_guides(
15653 0..6,
15654 vec![
15655 indent_guide(buffer_id, 1, 2, 0),
15656 indent_guide(buffer_id, 2, 2, 1),
15657 ],
15658 None,
15659 &mut cx,
15660 );
15661}
15662
15663#[gpui::test]
15664async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15665 let (buffer_id, mut cx) = setup_indent_guides_editor(
15666 &"
15667 block1
15668
15669
15670
15671 block2
15672 "
15673 .unindent(),
15674 cx,
15675 )
15676 .await;
15677
15678 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15679}
15680
15681#[gpui::test]
15682async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15683 let (buffer_id, mut cx) = setup_indent_guides_editor(
15684 &"
15685 def a:
15686 \tb = 3
15687 \tif True:
15688 \t\tc = 4
15689 \t\td = 5
15690 \tprint(b)
15691 "
15692 .unindent(),
15693 cx,
15694 )
15695 .await;
15696
15697 assert_indent_guides(
15698 0..6,
15699 vec![
15700 indent_guide(buffer_id, 1, 6, 0),
15701 indent_guide(buffer_id, 3, 4, 1),
15702 ],
15703 None,
15704 &mut cx,
15705 );
15706}
15707
15708#[gpui::test]
15709async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15710 let (buffer_id, mut cx) = setup_indent_guides_editor(
15711 &"
15712 fn main() {
15713 let a = 1;
15714 }"
15715 .unindent(),
15716 cx,
15717 )
15718 .await;
15719
15720 cx.update_editor(|editor, window, cx| {
15721 editor.change_selections(None, window, cx, |s| {
15722 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15723 });
15724 });
15725
15726 assert_indent_guides(
15727 0..3,
15728 vec![indent_guide(buffer_id, 1, 1, 0)],
15729 Some(vec![0]),
15730 &mut cx,
15731 );
15732}
15733
15734#[gpui::test]
15735async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15736 let (buffer_id, mut cx) = setup_indent_guides_editor(
15737 &"
15738 fn main() {
15739 if 1 == 2 {
15740 let a = 1;
15741 }
15742 }"
15743 .unindent(),
15744 cx,
15745 )
15746 .await;
15747
15748 cx.update_editor(|editor, window, cx| {
15749 editor.change_selections(None, window, cx, |s| {
15750 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15751 });
15752 });
15753
15754 assert_indent_guides(
15755 0..4,
15756 vec![
15757 indent_guide(buffer_id, 1, 3, 0),
15758 indent_guide(buffer_id, 2, 2, 1),
15759 ],
15760 Some(vec![1]),
15761 &mut cx,
15762 );
15763
15764 cx.update_editor(|editor, window, cx| {
15765 editor.change_selections(None, window, cx, |s| {
15766 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15767 });
15768 });
15769
15770 assert_indent_guides(
15771 0..4,
15772 vec![
15773 indent_guide(buffer_id, 1, 3, 0),
15774 indent_guide(buffer_id, 2, 2, 1),
15775 ],
15776 Some(vec![1]),
15777 &mut cx,
15778 );
15779
15780 cx.update_editor(|editor, window, cx| {
15781 editor.change_selections(None, window, cx, |s| {
15782 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15783 });
15784 });
15785
15786 assert_indent_guides(
15787 0..4,
15788 vec![
15789 indent_guide(buffer_id, 1, 3, 0),
15790 indent_guide(buffer_id, 2, 2, 1),
15791 ],
15792 Some(vec![0]),
15793 &mut cx,
15794 );
15795}
15796
15797#[gpui::test]
15798async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15799 let (buffer_id, mut cx) = setup_indent_guides_editor(
15800 &"
15801 fn main() {
15802 let a = 1;
15803
15804 let b = 2;
15805 }"
15806 .unindent(),
15807 cx,
15808 )
15809 .await;
15810
15811 cx.update_editor(|editor, window, cx| {
15812 editor.change_selections(None, window, cx, |s| {
15813 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15814 });
15815 });
15816
15817 assert_indent_guides(
15818 0..5,
15819 vec![indent_guide(buffer_id, 1, 3, 0)],
15820 Some(vec![0]),
15821 &mut cx,
15822 );
15823}
15824
15825#[gpui::test]
15826async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15827 let (buffer_id, mut cx) = setup_indent_guides_editor(
15828 &"
15829 def m:
15830 a = 1
15831 pass"
15832 .unindent(),
15833 cx,
15834 )
15835 .await;
15836
15837 cx.update_editor(|editor, window, cx| {
15838 editor.change_selections(None, window, cx, |s| {
15839 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15840 });
15841 });
15842
15843 assert_indent_guides(
15844 0..3,
15845 vec![indent_guide(buffer_id, 1, 2, 0)],
15846 Some(vec![0]),
15847 &mut cx,
15848 );
15849}
15850
15851#[gpui::test]
15852async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15853 init_test(cx, |_| {});
15854 let mut cx = EditorTestContext::new(cx).await;
15855 let text = indoc! {
15856 "
15857 impl A {
15858 fn b() {
15859 0;
15860 3;
15861 5;
15862 6;
15863 7;
15864 }
15865 }
15866 "
15867 };
15868 let base_text = indoc! {
15869 "
15870 impl A {
15871 fn b() {
15872 0;
15873 1;
15874 2;
15875 3;
15876 4;
15877 }
15878 fn c() {
15879 5;
15880 6;
15881 7;
15882 }
15883 }
15884 "
15885 };
15886
15887 cx.update_editor(|editor, window, cx| {
15888 editor.set_text(text, window, cx);
15889
15890 editor.buffer().update(cx, |multibuffer, cx| {
15891 let buffer = multibuffer.as_singleton().unwrap();
15892 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15893
15894 multibuffer.set_all_diff_hunks_expanded(cx);
15895 multibuffer.add_diff(diff, cx);
15896
15897 buffer.read(cx).remote_id()
15898 })
15899 });
15900 cx.run_until_parked();
15901
15902 cx.assert_state_with_diff(
15903 indoc! { "
15904 impl A {
15905 fn b() {
15906 0;
15907 - 1;
15908 - 2;
15909 3;
15910 - 4;
15911 - }
15912 - fn c() {
15913 5;
15914 6;
15915 7;
15916 }
15917 }
15918 ˇ"
15919 }
15920 .to_string(),
15921 );
15922
15923 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15924 editor
15925 .snapshot(window, cx)
15926 .buffer_snapshot
15927 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15928 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15929 .collect::<Vec<_>>()
15930 });
15931 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15932 assert_eq!(
15933 actual_guides,
15934 vec![
15935 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15936 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15937 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15938 ]
15939 );
15940}
15941
15942#[gpui::test]
15943async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15944 init_test(cx, |_| {});
15945 let mut cx = EditorTestContext::new(cx).await;
15946
15947 let diff_base = r#"
15948 a
15949 b
15950 c
15951 "#
15952 .unindent();
15953
15954 cx.set_state(
15955 &r#"
15956 ˇA
15957 b
15958 C
15959 "#
15960 .unindent(),
15961 );
15962 cx.set_head_text(&diff_base);
15963 cx.update_editor(|editor, window, cx| {
15964 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15965 });
15966 executor.run_until_parked();
15967
15968 let both_hunks_expanded = r#"
15969 - a
15970 + ˇA
15971 b
15972 - c
15973 + C
15974 "#
15975 .unindent();
15976
15977 cx.assert_state_with_diff(both_hunks_expanded.clone());
15978
15979 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15980 let snapshot = editor.snapshot(window, cx);
15981 let hunks = editor
15982 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15983 .collect::<Vec<_>>();
15984 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15985 let buffer_id = hunks[0].buffer_id;
15986 hunks
15987 .into_iter()
15988 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15989 .collect::<Vec<_>>()
15990 });
15991 assert_eq!(hunk_ranges.len(), 2);
15992
15993 cx.update_editor(|editor, _, cx| {
15994 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15995 });
15996 executor.run_until_parked();
15997
15998 let second_hunk_expanded = r#"
15999 ˇA
16000 b
16001 - c
16002 + C
16003 "#
16004 .unindent();
16005
16006 cx.assert_state_with_diff(second_hunk_expanded);
16007
16008 cx.update_editor(|editor, _, cx| {
16009 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16010 });
16011 executor.run_until_parked();
16012
16013 cx.assert_state_with_diff(both_hunks_expanded.clone());
16014
16015 cx.update_editor(|editor, _, cx| {
16016 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16017 });
16018 executor.run_until_parked();
16019
16020 let first_hunk_expanded = r#"
16021 - a
16022 + ˇA
16023 b
16024 C
16025 "#
16026 .unindent();
16027
16028 cx.assert_state_with_diff(first_hunk_expanded);
16029
16030 cx.update_editor(|editor, _, cx| {
16031 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16032 });
16033 executor.run_until_parked();
16034
16035 cx.assert_state_with_diff(both_hunks_expanded);
16036
16037 cx.set_state(
16038 &r#"
16039 ˇA
16040 b
16041 "#
16042 .unindent(),
16043 );
16044 cx.run_until_parked();
16045
16046 // TODO this cursor position seems bad
16047 cx.assert_state_with_diff(
16048 r#"
16049 - ˇa
16050 + A
16051 b
16052 "#
16053 .unindent(),
16054 );
16055
16056 cx.update_editor(|editor, window, cx| {
16057 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16058 });
16059
16060 cx.assert_state_with_diff(
16061 r#"
16062 - ˇa
16063 + A
16064 b
16065 - c
16066 "#
16067 .unindent(),
16068 );
16069
16070 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16071 let snapshot = editor.snapshot(window, cx);
16072 let hunks = editor
16073 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16074 .collect::<Vec<_>>();
16075 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16076 let buffer_id = hunks[0].buffer_id;
16077 hunks
16078 .into_iter()
16079 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16080 .collect::<Vec<_>>()
16081 });
16082 assert_eq!(hunk_ranges.len(), 2);
16083
16084 cx.update_editor(|editor, _, cx| {
16085 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16086 });
16087 executor.run_until_parked();
16088
16089 cx.assert_state_with_diff(
16090 r#"
16091 - ˇa
16092 + A
16093 b
16094 "#
16095 .unindent(),
16096 );
16097}
16098
16099#[gpui::test]
16100async fn test_toggle_deletion_hunk_at_start_of_file(
16101 executor: BackgroundExecutor,
16102 cx: &mut TestAppContext,
16103) {
16104 init_test(cx, |_| {});
16105 let mut cx = EditorTestContext::new(cx).await;
16106
16107 let diff_base = r#"
16108 a
16109 b
16110 c
16111 "#
16112 .unindent();
16113
16114 cx.set_state(
16115 &r#"
16116 ˇb
16117 c
16118 "#
16119 .unindent(),
16120 );
16121 cx.set_head_text(&diff_base);
16122 cx.update_editor(|editor, window, cx| {
16123 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16124 });
16125 executor.run_until_parked();
16126
16127 let hunk_expanded = r#"
16128 - a
16129 ˇb
16130 c
16131 "#
16132 .unindent();
16133
16134 cx.assert_state_with_diff(hunk_expanded.clone());
16135
16136 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16137 let snapshot = editor.snapshot(window, cx);
16138 let hunks = editor
16139 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16140 .collect::<Vec<_>>();
16141 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16142 let buffer_id = hunks[0].buffer_id;
16143 hunks
16144 .into_iter()
16145 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16146 .collect::<Vec<_>>()
16147 });
16148 assert_eq!(hunk_ranges.len(), 1);
16149
16150 cx.update_editor(|editor, _, cx| {
16151 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16152 });
16153 executor.run_until_parked();
16154
16155 let hunk_collapsed = r#"
16156 ˇb
16157 c
16158 "#
16159 .unindent();
16160
16161 cx.assert_state_with_diff(hunk_collapsed);
16162
16163 cx.update_editor(|editor, _, cx| {
16164 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16165 });
16166 executor.run_until_parked();
16167
16168 cx.assert_state_with_diff(hunk_expanded.clone());
16169}
16170
16171#[gpui::test]
16172async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16173 init_test(cx, |_| {});
16174
16175 let fs = FakeFs::new(cx.executor());
16176 fs.insert_tree(
16177 path!("/test"),
16178 json!({
16179 ".git": {},
16180 "file-1": "ONE\n",
16181 "file-2": "TWO\n",
16182 "file-3": "THREE\n",
16183 }),
16184 )
16185 .await;
16186
16187 fs.set_head_for_repo(
16188 path!("/test/.git").as_ref(),
16189 &[
16190 ("file-1".into(), "one\n".into()),
16191 ("file-2".into(), "two\n".into()),
16192 ("file-3".into(), "three\n".into()),
16193 ],
16194 );
16195
16196 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16197 let mut buffers = vec![];
16198 for i in 1..=3 {
16199 let buffer = project
16200 .update(cx, |project, cx| {
16201 let path = format!(path!("/test/file-{}"), i);
16202 project.open_local_buffer(path, cx)
16203 })
16204 .await
16205 .unwrap();
16206 buffers.push(buffer);
16207 }
16208
16209 let multibuffer = cx.new(|cx| {
16210 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16211 multibuffer.set_all_diff_hunks_expanded(cx);
16212 for buffer in &buffers {
16213 let snapshot = buffer.read(cx).snapshot();
16214 multibuffer.set_excerpts_for_path(
16215 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16216 buffer.clone(),
16217 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16218 DEFAULT_MULTIBUFFER_CONTEXT,
16219 cx,
16220 );
16221 }
16222 multibuffer
16223 });
16224
16225 let editor = cx.add_window(|window, cx| {
16226 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16227 });
16228 cx.run_until_parked();
16229
16230 let snapshot = editor
16231 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16232 .unwrap();
16233 let hunks = snapshot
16234 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16235 .map(|hunk| match hunk {
16236 DisplayDiffHunk::Unfolded {
16237 display_row_range, ..
16238 } => display_row_range,
16239 DisplayDiffHunk::Folded { .. } => unreachable!(),
16240 })
16241 .collect::<Vec<_>>();
16242 assert_eq!(
16243 hunks,
16244 [
16245 DisplayRow(2)..DisplayRow(4),
16246 DisplayRow(7)..DisplayRow(9),
16247 DisplayRow(12)..DisplayRow(14),
16248 ]
16249 );
16250}
16251
16252#[gpui::test]
16253async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16254 init_test(cx, |_| {});
16255
16256 let mut cx = EditorTestContext::new(cx).await;
16257 cx.set_head_text(indoc! { "
16258 one
16259 two
16260 three
16261 four
16262 five
16263 "
16264 });
16265 cx.set_index_text(indoc! { "
16266 one
16267 two
16268 three
16269 four
16270 five
16271 "
16272 });
16273 cx.set_state(indoc! {"
16274 one
16275 TWO
16276 ˇTHREE
16277 FOUR
16278 five
16279 "});
16280 cx.run_until_parked();
16281 cx.update_editor(|editor, window, cx| {
16282 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16283 });
16284 cx.run_until_parked();
16285 cx.assert_index_text(Some(indoc! {"
16286 one
16287 TWO
16288 THREE
16289 FOUR
16290 five
16291 "}));
16292 cx.set_state(indoc! { "
16293 one
16294 TWO
16295 ˇTHREE-HUNDRED
16296 FOUR
16297 five
16298 "});
16299 cx.run_until_parked();
16300 cx.update_editor(|editor, window, cx| {
16301 let snapshot = editor.snapshot(window, cx);
16302 let hunks = editor
16303 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16304 .collect::<Vec<_>>();
16305 assert_eq!(hunks.len(), 1);
16306 assert_eq!(
16307 hunks[0].status(),
16308 DiffHunkStatus {
16309 kind: DiffHunkStatusKind::Modified,
16310 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16311 }
16312 );
16313
16314 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16315 });
16316 cx.run_until_parked();
16317 cx.assert_index_text(Some(indoc! {"
16318 one
16319 TWO
16320 THREE-HUNDRED
16321 FOUR
16322 five
16323 "}));
16324}
16325
16326#[gpui::test]
16327fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16328 init_test(cx, |_| {});
16329
16330 let editor = cx.add_window(|window, cx| {
16331 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16332 build_editor(buffer, window, cx)
16333 });
16334
16335 let render_args = Arc::new(Mutex::new(None));
16336 let snapshot = editor
16337 .update(cx, |editor, window, cx| {
16338 let snapshot = editor.buffer().read(cx).snapshot(cx);
16339 let range =
16340 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16341
16342 struct RenderArgs {
16343 row: MultiBufferRow,
16344 folded: bool,
16345 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16346 }
16347
16348 let crease = Crease::inline(
16349 range,
16350 FoldPlaceholder::test(),
16351 {
16352 let toggle_callback = render_args.clone();
16353 move |row, folded, callback, _window, _cx| {
16354 *toggle_callback.lock() = Some(RenderArgs {
16355 row,
16356 folded,
16357 callback,
16358 });
16359 div()
16360 }
16361 },
16362 |_row, _folded, _window, _cx| div(),
16363 );
16364
16365 editor.insert_creases(Some(crease), cx);
16366 let snapshot = editor.snapshot(window, cx);
16367 let _div = snapshot.render_crease_toggle(
16368 MultiBufferRow(1),
16369 false,
16370 cx.entity().clone(),
16371 window,
16372 cx,
16373 );
16374 snapshot
16375 })
16376 .unwrap();
16377
16378 let render_args = render_args.lock().take().unwrap();
16379 assert_eq!(render_args.row, MultiBufferRow(1));
16380 assert!(!render_args.folded);
16381 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16382
16383 cx.update_window(*editor, |_, window, cx| {
16384 (render_args.callback)(true, window, cx)
16385 })
16386 .unwrap();
16387 let snapshot = editor
16388 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16389 .unwrap();
16390 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16391
16392 cx.update_window(*editor, |_, window, cx| {
16393 (render_args.callback)(false, window, cx)
16394 })
16395 .unwrap();
16396 let snapshot = editor
16397 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16398 .unwrap();
16399 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16400}
16401
16402#[gpui::test]
16403async fn test_input_text(cx: &mut TestAppContext) {
16404 init_test(cx, |_| {});
16405 let mut cx = EditorTestContext::new(cx).await;
16406
16407 cx.set_state(
16408 &r#"ˇone
16409 two
16410
16411 three
16412 fourˇ
16413 five
16414
16415 siˇx"#
16416 .unindent(),
16417 );
16418
16419 cx.dispatch_action(HandleInput(String::new()));
16420 cx.assert_editor_state(
16421 &r#"ˇone
16422 two
16423
16424 three
16425 fourˇ
16426 five
16427
16428 siˇx"#
16429 .unindent(),
16430 );
16431
16432 cx.dispatch_action(HandleInput("AAAA".to_string()));
16433 cx.assert_editor_state(
16434 &r#"AAAAˇone
16435 two
16436
16437 three
16438 fourAAAAˇ
16439 five
16440
16441 siAAAAˇx"#
16442 .unindent(),
16443 );
16444}
16445
16446#[gpui::test]
16447async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16448 init_test(cx, |_| {});
16449
16450 let mut cx = EditorTestContext::new(cx).await;
16451 cx.set_state(
16452 r#"let foo = 1;
16453let foo = 2;
16454let foo = 3;
16455let fooˇ = 4;
16456let foo = 5;
16457let foo = 6;
16458let foo = 7;
16459let foo = 8;
16460let foo = 9;
16461let foo = 10;
16462let foo = 11;
16463let foo = 12;
16464let foo = 13;
16465let foo = 14;
16466let foo = 15;"#,
16467 );
16468
16469 cx.update_editor(|e, window, cx| {
16470 assert_eq!(
16471 e.next_scroll_position,
16472 NextScrollCursorCenterTopBottom::Center,
16473 "Default next scroll direction is center",
16474 );
16475
16476 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16477 assert_eq!(
16478 e.next_scroll_position,
16479 NextScrollCursorCenterTopBottom::Top,
16480 "After center, next scroll direction should be top",
16481 );
16482
16483 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16484 assert_eq!(
16485 e.next_scroll_position,
16486 NextScrollCursorCenterTopBottom::Bottom,
16487 "After top, next scroll direction should be bottom",
16488 );
16489
16490 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16491 assert_eq!(
16492 e.next_scroll_position,
16493 NextScrollCursorCenterTopBottom::Center,
16494 "After bottom, scrolling should start over",
16495 );
16496
16497 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16498 assert_eq!(
16499 e.next_scroll_position,
16500 NextScrollCursorCenterTopBottom::Top,
16501 "Scrolling continues if retriggered fast enough"
16502 );
16503 });
16504
16505 cx.executor()
16506 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16507 cx.executor().run_until_parked();
16508 cx.update_editor(|e, _, _| {
16509 assert_eq!(
16510 e.next_scroll_position,
16511 NextScrollCursorCenterTopBottom::Center,
16512 "If scrolling is not triggered fast enough, it should reset"
16513 );
16514 });
16515}
16516
16517#[gpui::test]
16518async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16519 init_test(cx, |_| {});
16520 let mut cx = EditorLspTestContext::new_rust(
16521 lsp::ServerCapabilities {
16522 definition_provider: Some(lsp::OneOf::Left(true)),
16523 references_provider: Some(lsp::OneOf::Left(true)),
16524 ..lsp::ServerCapabilities::default()
16525 },
16526 cx,
16527 )
16528 .await;
16529
16530 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16531 let go_to_definition = cx
16532 .lsp
16533 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16534 move |params, _| async move {
16535 if empty_go_to_definition {
16536 Ok(None)
16537 } else {
16538 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16539 uri: params.text_document_position_params.text_document.uri,
16540 range: lsp::Range::new(
16541 lsp::Position::new(4, 3),
16542 lsp::Position::new(4, 6),
16543 ),
16544 })))
16545 }
16546 },
16547 );
16548 let references = cx
16549 .lsp
16550 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16551 Ok(Some(vec![lsp::Location {
16552 uri: params.text_document_position.text_document.uri,
16553 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16554 }]))
16555 });
16556 (go_to_definition, references)
16557 };
16558
16559 cx.set_state(
16560 &r#"fn one() {
16561 let mut a = ˇtwo();
16562 }
16563
16564 fn two() {}"#
16565 .unindent(),
16566 );
16567 set_up_lsp_handlers(false, &mut cx);
16568 let navigated = cx
16569 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16570 .await
16571 .expect("Failed to navigate to definition");
16572 assert_eq!(
16573 navigated,
16574 Navigated::Yes,
16575 "Should have navigated to definition from the GetDefinition response"
16576 );
16577 cx.assert_editor_state(
16578 &r#"fn one() {
16579 let mut a = two();
16580 }
16581
16582 fn «twoˇ»() {}"#
16583 .unindent(),
16584 );
16585
16586 let editors = cx.update_workspace(|workspace, _, cx| {
16587 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16588 });
16589 cx.update_editor(|_, _, test_editor_cx| {
16590 assert_eq!(
16591 editors.len(),
16592 1,
16593 "Initially, only one, test, editor should be open in the workspace"
16594 );
16595 assert_eq!(
16596 test_editor_cx.entity(),
16597 editors.last().expect("Asserted len is 1").clone()
16598 );
16599 });
16600
16601 set_up_lsp_handlers(true, &mut cx);
16602 let navigated = cx
16603 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16604 .await
16605 .expect("Failed to navigate to lookup references");
16606 assert_eq!(
16607 navigated,
16608 Navigated::Yes,
16609 "Should have navigated to references as a fallback after empty GoToDefinition response"
16610 );
16611 // We should not change the selections in the existing file,
16612 // if opening another milti buffer with the references
16613 cx.assert_editor_state(
16614 &r#"fn one() {
16615 let mut a = two();
16616 }
16617
16618 fn «twoˇ»() {}"#
16619 .unindent(),
16620 );
16621 let editors = cx.update_workspace(|workspace, _, cx| {
16622 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16623 });
16624 cx.update_editor(|_, _, test_editor_cx| {
16625 assert_eq!(
16626 editors.len(),
16627 2,
16628 "After falling back to references search, we open a new editor with the results"
16629 );
16630 let references_fallback_text = editors
16631 .into_iter()
16632 .find(|new_editor| *new_editor != test_editor_cx.entity())
16633 .expect("Should have one non-test editor now")
16634 .read(test_editor_cx)
16635 .text(test_editor_cx);
16636 assert_eq!(
16637 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16638 "Should use the range from the references response and not the GoToDefinition one"
16639 );
16640 });
16641}
16642
16643#[gpui::test]
16644async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16645 init_test(cx, |_| {});
16646 cx.update(|cx| {
16647 let mut editor_settings = EditorSettings::get_global(cx).clone();
16648 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16649 EditorSettings::override_global(editor_settings, cx);
16650 });
16651 let mut cx = EditorLspTestContext::new_rust(
16652 lsp::ServerCapabilities {
16653 definition_provider: Some(lsp::OneOf::Left(true)),
16654 references_provider: Some(lsp::OneOf::Left(true)),
16655 ..lsp::ServerCapabilities::default()
16656 },
16657 cx,
16658 )
16659 .await;
16660 let original_state = r#"fn one() {
16661 let mut a = ˇtwo();
16662 }
16663
16664 fn two() {}"#
16665 .unindent();
16666 cx.set_state(&original_state);
16667
16668 let mut go_to_definition = cx
16669 .lsp
16670 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16671 move |_, _| async move { Ok(None) },
16672 );
16673 let _references = cx
16674 .lsp
16675 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16676 panic!("Should not call for references with no go to definition fallback")
16677 });
16678
16679 let navigated = cx
16680 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16681 .await
16682 .expect("Failed to navigate to lookup references");
16683 go_to_definition
16684 .next()
16685 .await
16686 .expect("Should have called the go_to_definition handler");
16687
16688 assert_eq!(
16689 navigated,
16690 Navigated::No,
16691 "Should have navigated to references as a fallback after empty GoToDefinition response"
16692 );
16693 cx.assert_editor_state(&original_state);
16694 let editors = cx.update_workspace(|workspace, _, cx| {
16695 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16696 });
16697 cx.update_editor(|_, _, _| {
16698 assert_eq!(
16699 editors.len(),
16700 1,
16701 "After unsuccessful fallback, no other editor should have been opened"
16702 );
16703 });
16704}
16705
16706#[gpui::test]
16707async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16708 init_test(cx, |_| {});
16709
16710 let language = Arc::new(Language::new(
16711 LanguageConfig::default(),
16712 Some(tree_sitter_rust::LANGUAGE.into()),
16713 ));
16714
16715 let text = r#"
16716 #[cfg(test)]
16717 mod tests() {
16718 #[test]
16719 fn runnable_1() {
16720 let a = 1;
16721 }
16722
16723 #[test]
16724 fn runnable_2() {
16725 let a = 1;
16726 let b = 2;
16727 }
16728 }
16729 "#
16730 .unindent();
16731
16732 let fs = FakeFs::new(cx.executor());
16733 fs.insert_file("/file.rs", Default::default()).await;
16734
16735 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16736 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16737 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16738 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16739 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16740
16741 let editor = cx.new_window_entity(|window, cx| {
16742 Editor::new(
16743 EditorMode::Full,
16744 multi_buffer,
16745 Some(project.clone()),
16746 window,
16747 cx,
16748 )
16749 });
16750
16751 editor.update_in(cx, |editor, window, cx| {
16752 let snapshot = editor.buffer().read(cx).snapshot(cx);
16753 editor.tasks.insert(
16754 (buffer.read(cx).remote_id(), 3),
16755 RunnableTasks {
16756 templates: vec![],
16757 offset: snapshot.anchor_before(43),
16758 column: 0,
16759 extra_variables: HashMap::default(),
16760 context_range: BufferOffset(43)..BufferOffset(85),
16761 },
16762 );
16763 editor.tasks.insert(
16764 (buffer.read(cx).remote_id(), 8),
16765 RunnableTasks {
16766 templates: vec![],
16767 offset: snapshot.anchor_before(86),
16768 column: 0,
16769 extra_variables: HashMap::default(),
16770 context_range: BufferOffset(86)..BufferOffset(191),
16771 },
16772 );
16773
16774 // Test finding task when cursor is inside function body
16775 editor.change_selections(None, window, cx, |s| {
16776 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16777 });
16778 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16779 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16780
16781 // Test finding task when cursor is on function name
16782 editor.change_selections(None, window, cx, |s| {
16783 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16784 });
16785 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16786 assert_eq!(row, 8, "Should find task when cursor is on function name");
16787 });
16788}
16789
16790#[gpui::test]
16791async fn test_folding_buffers(cx: &mut TestAppContext) {
16792 init_test(cx, |_| {});
16793
16794 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16795 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16796 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16797
16798 let fs = FakeFs::new(cx.executor());
16799 fs.insert_tree(
16800 path!("/a"),
16801 json!({
16802 "first.rs": sample_text_1,
16803 "second.rs": sample_text_2,
16804 "third.rs": sample_text_3,
16805 }),
16806 )
16807 .await;
16808 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16810 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16811 let worktree = project.update(cx, |project, cx| {
16812 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16813 assert_eq!(worktrees.len(), 1);
16814 worktrees.pop().unwrap()
16815 });
16816 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16817
16818 let buffer_1 = project
16819 .update(cx, |project, cx| {
16820 project.open_buffer((worktree_id, "first.rs"), cx)
16821 })
16822 .await
16823 .unwrap();
16824 let buffer_2 = project
16825 .update(cx, |project, cx| {
16826 project.open_buffer((worktree_id, "second.rs"), cx)
16827 })
16828 .await
16829 .unwrap();
16830 let buffer_3 = project
16831 .update(cx, |project, cx| {
16832 project.open_buffer((worktree_id, "third.rs"), cx)
16833 })
16834 .await
16835 .unwrap();
16836
16837 let multi_buffer = cx.new(|cx| {
16838 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16839 multi_buffer.push_excerpts(
16840 buffer_1.clone(),
16841 [
16842 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16843 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16844 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16845 ],
16846 cx,
16847 );
16848 multi_buffer.push_excerpts(
16849 buffer_2.clone(),
16850 [
16851 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16852 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16853 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16854 ],
16855 cx,
16856 );
16857 multi_buffer.push_excerpts(
16858 buffer_3.clone(),
16859 [
16860 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16861 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16862 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16863 ],
16864 cx,
16865 );
16866 multi_buffer
16867 });
16868 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16869 Editor::new(
16870 EditorMode::Full,
16871 multi_buffer.clone(),
16872 Some(project.clone()),
16873 window,
16874 cx,
16875 )
16876 });
16877
16878 assert_eq!(
16879 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16880 "\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",
16881 );
16882
16883 multi_buffer_editor.update(cx, |editor, cx| {
16884 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16885 });
16886 assert_eq!(
16887 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16888 "\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",
16889 "After folding the first buffer, its text should not be displayed"
16890 );
16891
16892 multi_buffer_editor.update(cx, |editor, cx| {
16893 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16894 });
16895 assert_eq!(
16896 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16897 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16898 "After folding the second buffer, its text should not be displayed"
16899 );
16900
16901 multi_buffer_editor.update(cx, |editor, cx| {
16902 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16903 });
16904 assert_eq!(
16905 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16906 "\n\n\n\n\n",
16907 "After folding the third buffer, its text should not be displayed"
16908 );
16909
16910 // Emulate selection inside the fold logic, that should work
16911 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16912 editor
16913 .snapshot(window, cx)
16914 .next_line_boundary(Point::new(0, 4));
16915 });
16916
16917 multi_buffer_editor.update(cx, |editor, cx| {
16918 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16919 });
16920 assert_eq!(
16921 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16922 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16923 "After unfolding the second buffer, its text should be displayed"
16924 );
16925
16926 // Typing inside of buffer 1 causes that buffer to be unfolded.
16927 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16928 assert_eq!(
16929 multi_buffer
16930 .read(cx)
16931 .snapshot(cx)
16932 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16933 .collect::<String>(),
16934 "bbbb"
16935 );
16936 editor.change_selections(None, window, cx, |selections| {
16937 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16938 });
16939 editor.handle_input("B", window, cx);
16940 });
16941
16942 assert_eq!(
16943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16944 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16945 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16946 );
16947
16948 multi_buffer_editor.update(cx, |editor, cx| {
16949 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16950 });
16951 assert_eq!(
16952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16953 "\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",
16954 "After unfolding the all buffers, all original text should be displayed"
16955 );
16956}
16957
16958#[gpui::test]
16959async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16960 init_test(cx, |_| {});
16961
16962 let sample_text_1 = "1111\n2222\n3333".to_string();
16963 let sample_text_2 = "4444\n5555\n6666".to_string();
16964 let sample_text_3 = "7777\n8888\n9999".to_string();
16965
16966 let fs = FakeFs::new(cx.executor());
16967 fs.insert_tree(
16968 path!("/a"),
16969 json!({
16970 "first.rs": sample_text_1,
16971 "second.rs": sample_text_2,
16972 "third.rs": sample_text_3,
16973 }),
16974 )
16975 .await;
16976 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16977 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16978 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16979 let worktree = project.update(cx, |project, cx| {
16980 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16981 assert_eq!(worktrees.len(), 1);
16982 worktrees.pop().unwrap()
16983 });
16984 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16985
16986 let buffer_1 = project
16987 .update(cx, |project, cx| {
16988 project.open_buffer((worktree_id, "first.rs"), cx)
16989 })
16990 .await
16991 .unwrap();
16992 let buffer_2 = project
16993 .update(cx, |project, cx| {
16994 project.open_buffer((worktree_id, "second.rs"), cx)
16995 })
16996 .await
16997 .unwrap();
16998 let buffer_3 = project
16999 .update(cx, |project, cx| {
17000 project.open_buffer((worktree_id, "third.rs"), cx)
17001 })
17002 .await
17003 .unwrap();
17004
17005 let multi_buffer = cx.new(|cx| {
17006 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17007 multi_buffer.push_excerpts(
17008 buffer_1.clone(),
17009 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17010 cx,
17011 );
17012 multi_buffer.push_excerpts(
17013 buffer_2.clone(),
17014 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17015 cx,
17016 );
17017 multi_buffer.push_excerpts(
17018 buffer_3.clone(),
17019 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17020 cx,
17021 );
17022 multi_buffer
17023 });
17024
17025 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17026 Editor::new(
17027 EditorMode::Full,
17028 multi_buffer,
17029 Some(project.clone()),
17030 window,
17031 cx,
17032 )
17033 });
17034
17035 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17036 assert_eq!(
17037 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17038 full_text,
17039 );
17040
17041 multi_buffer_editor.update(cx, |editor, cx| {
17042 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17043 });
17044 assert_eq!(
17045 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17046 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17047 "After folding the first buffer, its text should not be displayed"
17048 );
17049
17050 multi_buffer_editor.update(cx, |editor, cx| {
17051 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17052 });
17053
17054 assert_eq!(
17055 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17056 "\n\n\n\n\n\n7777\n8888\n9999",
17057 "After folding the second buffer, its text should not be displayed"
17058 );
17059
17060 multi_buffer_editor.update(cx, |editor, cx| {
17061 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17062 });
17063 assert_eq!(
17064 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17065 "\n\n\n\n\n",
17066 "After folding the third buffer, its text should not be displayed"
17067 );
17068
17069 multi_buffer_editor.update(cx, |editor, cx| {
17070 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17071 });
17072 assert_eq!(
17073 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17074 "\n\n\n\n4444\n5555\n6666\n\n",
17075 "After unfolding the second buffer, its text should be displayed"
17076 );
17077
17078 multi_buffer_editor.update(cx, |editor, cx| {
17079 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17080 });
17081 assert_eq!(
17082 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17083 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17084 "After unfolding the first buffer, its text should be displayed"
17085 );
17086
17087 multi_buffer_editor.update(cx, |editor, cx| {
17088 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17089 });
17090 assert_eq!(
17091 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17092 full_text,
17093 "After unfolding all buffers, all original text should be displayed"
17094 );
17095}
17096
17097#[gpui::test]
17098async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17099 init_test(cx, |_| {});
17100
17101 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17102
17103 let fs = FakeFs::new(cx.executor());
17104 fs.insert_tree(
17105 path!("/a"),
17106 json!({
17107 "main.rs": sample_text,
17108 }),
17109 )
17110 .await;
17111 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17112 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17113 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17114 let worktree = project.update(cx, |project, cx| {
17115 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17116 assert_eq!(worktrees.len(), 1);
17117 worktrees.pop().unwrap()
17118 });
17119 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17120
17121 let buffer_1 = project
17122 .update(cx, |project, cx| {
17123 project.open_buffer((worktree_id, "main.rs"), cx)
17124 })
17125 .await
17126 .unwrap();
17127
17128 let multi_buffer = cx.new(|cx| {
17129 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17130 multi_buffer.push_excerpts(
17131 buffer_1.clone(),
17132 [ExcerptRange::new(
17133 Point::new(0, 0)
17134 ..Point::new(
17135 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17136 0,
17137 ),
17138 )],
17139 cx,
17140 );
17141 multi_buffer
17142 });
17143 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17144 Editor::new(
17145 EditorMode::Full,
17146 multi_buffer,
17147 Some(project.clone()),
17148 window,
17149 cx,
17150 )
17151 });
17152
17153 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17154 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17155 enum TestHighlight {}
17156 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17157 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17158 editor.highlight_text::<TestHighlight>(
17159 vec![highlight_range.clone()],
17160 HighlightStyle::color(Hsla::green()),
17161 cx,
17162 );
17163 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17164 });
17165
17166 let full_text = format!("\n\n{sample_text}");
17167 assert_eq!(
17168 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17169 full_text,
17170 );
17171}
17172
17173#[gpui::test]
17174async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17175 init_test(cx, |_| {});
17176 cx.update(|cx| {
17177 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17178 "keymaps/default-linux.json",
17179 cx,
17180 )
17181 .unwrap();
17182 cx.bind_keys(default_key_bindings);
17183 });
17184
17185 let (editor, cx) = cx.add_window_view(|window, cx| {
17186 let multi_buffer = MultiBuffer::build_multi(
17187 [
17188 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17189 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17190 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17191 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17192 ],
17193 cx,
17194 );
17195 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17196
17197 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17198 // fold all but the second buffer, so that we test navigating between two
17199 // adjacent folded buffers, as well as folded buffers at the start and
17200 // end the multibuffer
17201 editor.fold_buffer(buffer_ids[0], cx);
17202 editor.fold_buffer(buffer_ids[2], cx);
17203 editor.fold_buffer(buffer_ids[3], cx);
17204
17205 editor
17206 });
17207 cx.simulate_resize(size(px(1000.), px(1000.)));
17208
17209 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17210 cx.assert_excerpts_with_selections(indoc! {"
17211 [EXCERPT]
17212 ˇ[FOLDED]
17213 [EXCERPT]
17214 a1
17215 b1
17216 [EXCERPT]
17217 [FOLDED]
17218 [EXCERPT]
17219 [FOLDED]
17220 "
17221 });
17222 cx.simulate_keystroke("down");
17223 cx.assert_excerpts_with_selections(indoc! {"
17224 [EXCERPT]
17225 [FOLDED]
17226 [EXCERPT]
17227 ˇa1
17228 b1
17229 [EXCERPT]
17230 [FOLDED]
17231 [EXCERPT]
17232 [FOLDED]
17233 "
17234 });
17235 cx.simulate_keystroke("down");
17236 cx.assert_excerpts_with_selections(indoc! {"
17237 [EXCERPT]
17238 [FOLDED]
17239 [EXCERPT]
17240 a1
17241 ˇb1
17242 [EXCERPT]
17243 [FOLDED]
17244 [EXCERPT]
17245 [FOLDED]
17246 "
17247 });
17248 cx.simulate_keystroke("down");
17249 cx.assert_excerpts_with_selections(indoc! {"
17250 [EXCERPT]
17251 [FOLDED]
17252 [EXCERPT]
17253 a1
17254 b1
17255 ˇ[EXCERPT]
17256 [FOLDED]
17257 [EXCERPT]
17258 [FOLDED]
17259 "
17260 });
17261 cx.simulate_keystroke("down");
17262 cx.assert_excerpts_with_selections(indoc! {"
17263 [EXCERPT]
17264 [FOLDED]
17265 [EXCERPT]
17266 a1
17267 b1
17268 [EXCERPT]
17269 ˇ[FOLDED]
17270 [EXCERPT]
17271 [FOLDED]
17272 "
17273 });
17274 for _ in 0..5 {
17275 cx.simulate_keystroke("down");
17276 cx.assert_excerpts_with_selections(indoc! {"
17277 [EXCERPT]
17278 [FOLDED]
17279 [EXCERPT]
17280 a1
17281 b1
17282 [EXCERPT]
17283 [FOLDED]
17284 [EXCERPT]
17285 ˇ[FOLDED]
17286 "
17287 });
17288 }
17289
17290 cx.simulate_keystroke("up");
17291 cx.assert_excerpts_with_selections(indoc! {"
17292 [EXCERPT]
17293 [FOLDED]
17294 [EXCERPT]
17295 a1
17296 b1
17297 [EXCERPT]
17298 ˇ[FOLDED]
17299 [EXCERPT]
17300 [FOLDED]
17301 "
17302 });
17303 cx.simulate_keystroke("up");
17304 cx.assert_excerpts_with_selections(indoc! {"
17305 [EXCERPT]
17306 [FOLDED]
17307 [EXCERPT]
17308 a1
17309 b1
17310 ˇ[EXCERPT]
17311 [FOLDED]
17312 [EXCERPT]
17313 [FOLDED]
17314 "
17315 });
17316 cx.simulate_keystroke("up");
17317 cx.assert_excerpts_with_selections(indoc! {"
17318 [EXCERPT]
17319 [FOLDED]
17320 [EXCERPT]
17321 a1
17322 ˇb1
17323 [EXCERPT]
17324 [FOLDED]
17325 [EXCERPT]
17326 [FOLDED]
17327 "
17328 });
17329 cx.simulate_keystroke("up");
17330 cx.assert_excerpts_with_selections(indoc! {"
17331 [EXCERPT]
17332 [FOLDED]
17333 [EXCERPT]
17334 ˇa1
17335 b1
17336 [EXCERPT]
17337 [FOLDED]
17338 [EXCERPT]
17339 [FOLDED]
17340 "
17341 });
17342 for _ in 0..5 {
17343 cx.simulate_keystroke("up");
17344 cx.assert_excerpts_with_selections(indoc! {"
17345 [EXCERPT]
17346 ˇ[FOLDED]
17347 [EXCERPT]
17348 a1
17349 b1
17350 [EXCERPT]
17351 [FOLDED]
17352 [EXCERPT]
17353 [FOLDED]
17354 "
17355 });
17356 }
17357}
17358
17359#[gpui::test]
17360async fn test_inline_completion_text(cx: &mut TestAppContext) {
17361 init_test(cx, |_| {});
17362
17363 // Simple insertion
17364 assert_highlighted_edits(
17365 "Hello, world!",
17366 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17367 true,
17368 cx,
17369 |highlighted_edits, cx| {
17370 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17371 assert_eq!(highlighted_edits.highlights.len(), 1);
17372 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17373 assert_eq!(
17374 highlighted_edits.highlights[0].1.background_color,
17375 Some(cx.theme().status().created_background)
17376 );
17377 },
17378 )
17379 .await;
17380
17381 // Replacement
17382 assert_highlighted_edits(
17383 "This is a test.",
17384 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17385 false,
17386 cx,
17387 |highlighted_edits, cx| {
17388 assert_eq!(highlighted_edits.text, "That is a test.");
17389 assert_eq!(highlighted_edits.highlights.len(), 1);
17390 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17391 assert_eq!(
17392 highlighted_edits.highlights[0].1.background_color,
17393 Some(cx.theme().status().created_background)
17394 );
17395 },
17396 )
17397 .await;
17398
17399 // Multiple edits
17400 assert_highlighted_edits(
17401 "Hello, world!",
17402 vec![
17403 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17404 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17405 ],
17406 false,
17407 cx,
17408 |highlighted_edits, cx| {
17409 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17410 assert_eq!(highlighted_edits.highlights.len(), 2);
17411 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17412 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17413 assert_eq!(
17414 highlighted_edits.highlights[0].1.background_color,
17415 Some(cx.theme().status().created_background)
17416 );
17417 assert_eq!(
17418 highlighted_edits.highlights[1].1.background_color,
17419 Some(cx.theme().status().created_background)
17420 );
17421 },
17422 )
17423 .await;
17424
17425 // Multiple lines with edits
17426 assert_highlighted_edits(
17427 "First line\nSecond line\nThird line\nFourth line",
17428 vec![
17429 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17430 (
17431 Point::new(2, 0)..Point::new(2, 10),
17432 "New third line".to_string(),
17433 ),
17434 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17435 ],
17436 false,
17437 cx,
17438 |highlighted_edits, cx| {
17439 assert_eq!(
17440 highlighted_edits.text,
17441 "Second modified\nNew third line\nFourth updated line"
17442 );
17443 assert_eq!(highlighted_edits.highlights.len(), 3);
17444 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17445 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17446 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17447 for highlight in &highlighted_edits.highlights {
17448 assert_eq!(
17449 highlight.1.background_color,
17450 Some(cx.theme().status().created_background)
17451 );
17452 }
17453 },
17454 )
17455 .await;
17456}
17457
17458#[gpui::test]
17459async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17460 init_test(cx, |_| {});
17461
17462 // Deletion
17463 assert_highlighted_edits(
17464 "Hello, world!",
17465 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17466 true,
17467 cx,
17468 |highlighted_edits, cx| {
17469 assert_eq!(highlighted_edits.text, "Hello, world!");
17470 assert_eq!(highlighted_edits.highlights.len(), 1);
17471 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17472 assert_eq!(
17473 highlighted_edits.highlights[0].1.background_color,
17474 Some(cx.theme().status().deleted_background)
17475 );
17476 },
17477 )
17478 .await;
17479
17480 // Insertion
17481 assert_highlighted_edits(
17482 "Hello, world!",
17483 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17484 true,
17485 cx,
17486 |highlighted_edits, cx| {
17487 assert_eq!(highlighted_edits.highlights.len(), 1);
17488 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17489 assert_eq!(
17490 highlighted_edits.highlights[0].1.background_color,
17491 Some(cx.theme().status().created_background)
17492 );
17493 },
17494 )
17495 .await;
17496}
17497
17498async fn assert_highlighted_edits(
17499 text: &str,
17500 edits: Vec<(Range<Point>, String)>,
17501 include_deletions: bool,
17502 cx: &mut TestAppContext,
17503 assertion_fn: impl Fn(HighlightedText, &App),
17504) {
17505 let window = cx.add_window(|window, cx| {
17506 let buffer = MultiBuffer::build_simple(text, cx);
17507 Editor::new(EditorMode::Full, buffer, None, window, cx)
17508 });
17509 let cx = &mut VisualTestContext::from_window(*window, cx);
17510
17511 let (buffer, snapshot) = window
17512 .update(cx, |editor, _window, cx| {
17513 (
17514 editor.buffer().clone(),
17515 editor.buffer().read(cx).snapshot(cx),
17516 )
17517 })
17518 .unwrap();
17519
17520 let edits = edits
17521 .into_iter()
17522 .map(|(range, edit)| {
17523 (
17524 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17525 edit,
17526 )
17527 })
17528 .collect::<Vec<_>>();
17529
17530 let text_anchor_edits = edits
17531 .clone()
17532 .into_iter()
17533 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17534 .collect::<Vec<_>>();
17535
17536 let edit_preview = window
17537 .update(cx, |_, _window, cx| {
17538 buffer
17539 .read(cx)
17540 .as_singleton()
17541 .unwrap()
17542 .read(cx)
17543 .preview_edits(text_anchor_edits.into(), cx)
17544 })
17545 .unwrap()
17546 .await;
17547
17548 cx.update(|_window, cx| {
17549 let highlighted_edits = inline_completion_edit_text(
17550 &snapshot.as_singleton().unwrap().2,
17551 &edits,
17552 &edit_preview,
17553 include_deletions,
17554 cx,
17555 );
17556 assertion_fn(highlighted_edits, cx)
17557 });
17558}
17559
17560#[track_caller]
17561fn assert_breakpoint(
17562 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17563 path: &Arc<Path>,
17564 expected: Vec<(u32, Breakpoint)>,
17565) {
17566 if expected.len() == 0usize {
17567 assert!(!breakpoints.contains_key(path), "{}", path.display());
17568 } else {
17569 let mut breakpoint = breakpoints
17570 .get(path)
17571 .unwrap()
17572 .into_iter()
17573 .map(|breakpoint| {
17574 (
17575 breakpoint.row,
17576 Breakpoint {
17577 message: breakpoint.message.clone(),
17578 state: breakpoint.state,
17579 condition: breakpoint.condition.clone(),
17580 hit_condition: breakpoint.hit_condition.clone(),
17581 },
17582 )
17583 })
17584 .collect::<Vec<_>>();
17585
17586 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17587
17588 assert_eq!(expected, breakpoint);
17589 }
17590}
17591
17592fn add_log_breakpoint_at_cursor(
17593 editor: &mut Editor,
17594 log_message: &str,
17595 window: &mut Window,
17596 cx: &mut Context<Editor>,
17597) {
17598 let (anchor, bp) = editor
17599 .breakpoint_at_cursor_head(window, cx)
17600 .unwrap_or_else(|| {
17601 let cursor_position: Point = editor.selections.newest(cx).head();
17602
17603 let breakpoint_position = editor
17604 .snapshot(window, cx)
17605 .display_snapshot
17606 .buffer_snapshot
17607 .anchor_before(Point::new(cursor_position.row, 0));
17608
17609 (breakpoint_position, Breakpoint::new_log(&log_message))
17610 });
17611
17612 editor.edit_breakpoint_at_anchor(
17613 anchor,
17614 bp,
17615 BreakpointEditAction::EditLogMessage(log_message.into()),
17616 cx,
17617 );
17618}
17619
17620#[gpui::test]
17621async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17622 init_test(cx, |_| {});
17623
17624 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17625 let fs = FakeFs::new(cx.executor());
17626 fs.insert_tree(
17627 path!("/a"),
17628 json!({
17629 "main.rs": sample_text,
17630 }),
17631 )
17632 .await;
17633 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17634 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17635 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17636
17637 let fs = FakeFs::new(cx.executor());
17638 fs.insert_tree(
17639 path!("/a"),
17640 json!({
17641 "main.rs": sample_text,
17642 }),
17643 )
17644 .await;
17645 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17646 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17647 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17648 let worktree_id = workspace
17649 .update(cx, |workspace, _window, cx| {
17650 workspace.project().update(cx, |project, cx| {
17651 project.worktrees(cx).next().unwrap().read(cx).id()
17652 })
17653 })
17654 .unwrap();
17655
17656 let buffer = project
17657 .update(cx, |project, cx| {
17658 project.open_buffer((worktree_id, "main.rs"), cx)
17659 })
17660 .await
17661 .unwrap();
17662
17663 let (editor, cx) = cx.add_window_view(|window, cx| {
17664 Editor::new(
17665 EditorMode::Full,
17666 MultiBuffer::build_from_buffer(buffer, cx),
17667 Some(project.clone()),
17668 window,
17669 cx,
17670 )
17671 });
17672
17673 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17674 let abs_path = project.read_with(cx, |project, cx| {
17675 project
17676 .absolute_path(&project_path, cx)
17677 .map(|path_buf| Arc::from(path_buf.to_owned()))
17678 .unwrap()
17679 });
17680
17681 // assert we can add breakpoint on the first line
17682 editor.update_in(cx, |editor, window, cx| {
17683 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17684 editor.move_to_end(&MoveToEnd, window, cx);
17685 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17686 });
17687
17688 let breakpoints = editor.update(cx, |editor, cx| {
17689 editor
17690 .breakpoint_store()
17691 .as_ref()
17692 .unwrap()
17693 .read(cx)
17694 .all_breakpoints(cx)
17695 .clone()
17696 });
17697
17698 assert_eq!(1, breakpoints.len());
17699 assert_breakpoint(
17700 &breakpoints,
17701 &abs_path,
17702 vec![
17703 (0, Breakpoint::new_standard()),
17704 (3, Breakpoint::new_standard()),
17705 ],
17706 );
17707
17708 editor.update_in(cx, |editor, window, cx| {
17709 editor.move_to_beginning(&MoveToBeginning, window, cx);
17710 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17711 });
17712
17713 let breakpoints = editor.update(cx, |editor, cx| {
17714 editor
17715 .breakpoint_store()
17716 .as_ref()
17717 .unwrap()
17718 .read(cx)
17719 .all_breakpoints(cx)
17720 .clone()
17721 });
17722
17723 assert_eq!(1, breakpoints.len());
17724 assert_breakpoint(
17725 &breakpoints,
17726 &abs_path,
17727 vec![(3, Breakpoint::new_standard())],
17728 );
17729
17730 editor.update_in(cx, |editor, window, cx| {
17731 editor.move_to_end(&MoveToEnd, window, cx);
17732 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17733 });
17734
17735 let breakpoints = editor.update(cx, |editor, cx| {
17736 editor
17737 .breakpoint_store()
17738 .as_ref()
17739 .unwrap()
17740 .read(cx)
17741 .all_breakpoints(cx)
17742 .clone()
17743 });
17744
17745 assert_eq!(0, breakpoints.len());
17746 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17747}
17748
17749#[gpui::test]
17750async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17751 init_test(cx, |_| {});
17752
17753 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17754
17755 let fs = FakeFs::new(cx.executor());
17756 fs.insert_tree(
17757 path!("/a"),
17758 json!({
17759 "main.rs": sample_text,
17760 }),
17761 )
17762 .await;
17763 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17764 let (workspace, cx) =
17765 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17766
17767 let worktree_id = workspace.update(cx, |workspace, cx| {
17768 workspace.project().update(cx, |project, cx| {
17769 project.worktrees(cx).next().unwrap().read(cx).id()
17770 })
17771 });
17772
17773 let buffer = project
17774 .update(cx, |project, cx| {
17775 project.open_buffer((worktree_id, "main.rs"), cx)
17776 })
17777 .await
17778 .unwrap();
17779
17780 let (editor, cx) = cx.add_window_view(|window, cx| {
17781 Editor::new(
17782 EditorMode::Full,
17783 MultiBuffer::build_from_buffer(buffer, cx),
17784 Some(project.clone()),
17785 window,
17786 cx,
17787 )
17788 });
17789
17790 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17791 let abs_path = project.read_with(cx, |project, cx| {
17792 project
17793 .absolute_path(&project_path, cx)
17794 .map(|path_buf| Arc::from(path_buf.to_owned()))
17795 .unwrap()
17796 });
17797
17798 editor.update_in(cx, |editor, window, cx| {
17799 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17800 });
17801
17802 let breakpoints = editor.update(cx, |editor, cx| {
17803 editor
17804 .breakpoint_store()
17805 .as_ref()
17806 .unwrap()
17807 .read(cx)
17808 .all_breakpoints(cx)
17809 .clone()
17810 });
17811
17812 assert_breakpoint(
17813 &breakpoints,
17814 &abs_path,
17815 vec![(0, Breakpoint::new_log("hello world"))],
17816 );
17817
17818 // Removing a log message from a log breakpoint should remove it
17819 editor.update_in(cx, |editor, window, cx| {
17820 add_log_breakpoint_at_cursor(editor, "", window, cx);
17821 });
17822
17823 let breakpoints = editor.update(cx, |editor, cx| {
17824 editor
17825 .breakpoint_store()
17826 .as_ref()
17827 .unwrap()
17828 .read(cx)
17829 .all_breakpoints(cx)
17830 .clone()
17831 });
17832
17833 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17834
17835 editor.update_in(cx, |editor, window, cx| {
17836 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17837 editor.move_to_end(&MoveToEnd, window, cx);
17838 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17839 // Not adding a log message to a standard breakpoint shouldn't remove it
17840 add_log_breakpoint_at_cursor(editor, "", window, cx);
17841 });
17842
17843 let breakpoints = editor.update(cx, |editor, cx| {
17844 editor
17845 .breakpoint_store()
17846 .as_ref()
17847 .unwrap()
17848 .read(cx)
17849 .all_breakpoints(cx)
17850 .clone()
17851 });
17852
17853 assert_breakpoint(
17854 &breakpoints,
17855 &abs_path,
17856 vec![
17857 (0, Breakpoint::new_standard()),
17858 (3, Breakpoint::new_standard()),
17859 ],
17860 );
17861
17862 editor.update_in(cx, |editor, window, cx| {
17863 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17864 });
17865
17866 let breakpoints = editor.update(cx, |editor, cx| {
17867 editor
17868 .breakpoint_store()
17869 .as_ref()
17870 .unwrap()
17871 .read(cx)
17872 .all_breakpoints(cx)
17873 .clone()
17874 });
17875
17876 assert_breakpoint(
17877 &breakpoints,
17878 &abs_path,
17879 vec![
17880 (0, Breakpoint::new_standard()),
17881 (3, Breakpoint::new_log("hello world")),
17882 ],
17883 );
17884
17885 editor.update_in(cx, |editor, window, cx| {
17886 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17887 });
17888
17889 let breakpoints = editor.update(cx, |editor, cx| {
17890 editor
17891 .breakpoint_store()
17892 .as_ref()
17893 .unwrap()
17894 .read(cx)
17895 .all_breakpoints(cx)
17896 .clone()
17897 });
17898
17899 assert_breakpoint(
17900 &breakpoints,
17901 &abs_path,
17902 vec![
17903 (0, Breakpoint::new_standard()),
17904 (3, Breakpoint::new_log("hello Earth!!")),
17905 ],
17906 );
17907}
17908
17909/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17910/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17911/// or when breakpoints were placed out of order. This tests for a regression too
17912#[gpui::test]
17913async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17914 init_test(cx, |_| {});
17915
17916 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17917 let fs = FakeFs::new(cx.executor());
17918 fs.insert_tree(
17919 path!("/a"),
17920 json!({
17921 "main.rs": sample_text,
17922 }),
17923 )
17924 .await;
17925 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17926 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17927 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17928
17929 let fs = FakeFs::new(cx.executor());
17930 fs.insert_tree(
17931 path!("/a"),
17932 json!({
17933 "main.rs": sample_text,
17934 }),
17935 )
17936 .await;
17937 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17938 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17939 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17940 let worktree_id = workspace
17941 .update(cx, |workspace, _window, cx| {
17942 workspace.project().update(cx, |project, cx| {
17943 project.worktrees(cx).next().unwrap().read(cx).id()
17944 })
17945 })
17946 .unwrap();
17947
17948 let buffer = project
17949 .update(cx, |project, cx| {
17950 project.open_buffer((worktree_id, "main.rs"), cx)
17951 })
17952 .await
17953 .unwrap();
17954
17955 let (editor, cx) = cx.add_window_view(|window, cx| {
17956 Editor::new(
17957 EditorMode::Full,
17958 MultiBuffer::build_from_buffer(buffer, cx),
17959 Some(project.clone()),
17960 window,
17961 cx,
17962 )
17963 });
17964
17965 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17966 let abs_path = project.read_with(cx, |project, cx| {
17967 project
17968 .absolute_path(&project_path, cx)
17969 .map(|path_buf| Arc::from(path_buf.to_owned()))
17970 .unwrap()
17971 });
17972
17973 // assert we can add breakpoint on the first line
17974 editor.update_in(cx, |editor, window, cx| {
17975 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17976 editor.move_to_end(&MoveToEnd, window, cx);
17977 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17978 editor.move_up(&MoveUp, window, cx);
17979 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17980 });
17981
17982 let breakpoints = editor.update(cx, |editor, cx| {
17983 editor
17984 .breakpoint_store()
17985 .as_ref()
17986 .unwrap()
17987 .read(cx)
17988 .all_breakpoints(cx)
17989 .clone()
17990 });
17991
17992 assert_eq!(1, breakpoints.len());
17993 assert_breakpoint(
17994 &breakpoints,
17995 &abs_path,
17996 vec![
17997 (0, Breakpoint::new_standard()),
17998 (2, Breakpoint::new_standard()),
17999 (3, Breakpoint::new_standard()),
18000 ],
18001 );
18002
18003 editor.update_in(cx, |editor, window, cx| {
18004 editor.move_to_beginning(&MoveToBeginning, window, cx);
18005 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18006 editor.move_to_end(&MoveToEnd, window, cx);
18007 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18008 // Disabling a breakpoint that doesn't exist should do nothing
18009 editor.move_up(&MoveUp, window, cx);
18010 editor.move_up(&MoveUp, window, cx);
18011 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18012 });
18013
18014 let breakpoints = editor.update(cx, |editor, cx| {
18015 editor
18016 .breakpoint_store()
18017 .as_ref()
18018 .unwrap()
18019 .read(cx)
18020 .all_breakpoints(cx)
18021 .clone()
18022 });
18023
18024 let disable_breakpoint = {
18025 let mut bp = Breakpoint::new_standard();
18026 bp.state = BreakpointState::Disabled;
18027 bp
18028 };
18029
18030 assert_eq!(1, breakpoints.len());
18031 assert_breakpoint(
18032 &breakpoints,
18033 &abs_path,
18034 vec![
18035 (0, disable_breakpoint.clone()),
18036 (2, Breakpoint::new_standard()),
18037 (3, disable_breakpoint.clone()),
18038 ],
18039 );
18040
18041 editor.update_in(cx, |editor, window, cx| {
18042 editor.move_to_beginning(&MoveToBeginning, window, cx);
18043 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18044 editor.move_to_end(&MoveToEnd, window, cx);
18045 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18046 editor.move_up(&MoveUp, window, cx);
18047 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18048 });
18049
18050 let breakpoints = editor.update(cx, |editor, cx| {
18051 editor
18052 .breakpoint_store()
18053 .as_ref()
18054 .unwrap()
18055 .read(cx)
18056 .all_breakpoints(cx)
18057 .clone()
18058 });
18059
18060 assert_eq!(1, breakpoints.len());
18061 assert_breakpoint(
18062 &breakpoints,
18063 &abs_path,
18064 vec![
18065 (0, Breakpoint::new_standard()),
18066 (2, disable_breakpoint),
18067 (3, Breakpoint::new_standard()),
18068 ],
18069 );
18070}
18071
18072#[gpui::test]
18073async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18074 init_test(cx, |_| {});
18075 let capabilities = lsp::ServerCapabilities {
18076 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18077 prepare_provider: Some(true),
18078 work_done_progress_options: Default::default(),
18079 })),
18080 ..Default::default()
18081 };
18082 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18083
18084 cx.set_state(indoc! {"
18085 struct Fˇoo {}
18086 "});
18087
18088 cx.update_editor(|editor, _, cx| {
18089 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18090 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18091 editor.highlight_background::<DocumentHighlightRead>(
18092 &[highlight_range],
18093 |c| c.editor_document_highlight_read_background,
18094 cx,
18095 );
18096 });
18097
18098 let mut prepare_rename_handler = cx
18099 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18100 move |_, _, _| async move {
18101 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18102 start: lsp::Position {
18103 line: 0,
18104 character: 7,
18105 },
18106 end: lsp::Position {
18107 line: 0,
18108 character: 10,
18109 },
18110 })))
18111 },
18112 );
18113 let prepare_rename_task = cx
18114 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18115 .expect("Prepare rename was not started");
18116 prepare_rename_handler.next().await.unwrap();
18117 prepare_rename_task.await.expect("Prepare rename failed");
18118
18119 let mut rename_handler =
18120 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18121 let edit = lsp::TextEdit {
18122 range: lsp::Range {
18123 start: lsp::Position {
18124 line: 0,
18125 character: 7,
18126 },
18127 end: lsp::Position {
18128 line: 0,
18129 character: 10,
18130 },
18131 },
18132 new_text: "FooRenamed".to_string(),
18133 };
18134 Ok(Some(lsp::WorkspaceEdit::new(
18135 // Specify the same edit twice
18136 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18137 )))
18138 });
18139 let rename_task = cx
18140 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18141 .expect("Confirm rename was not started");
18142 rename_handler.next().await.unwrap();
18143 rename_task.await.expect("Confirm rename failed");
18144 cx.run_until_parked();
18145
18146 // Despite two edits, only one is actually applied as those are identical
18147 cx.assert_editor_state(indoc! {"
18148 struct FooRenamedˇ {}
18149 "});
18150}
18151
18152#[gpui::test]
18153async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18154 init_test(cx, |_| {});
18155 // These capabilities indicate that the server does not support prepare rename.
18156 let capabilities = lsp::ServerCapabilities {
18157 rename_provider: Some(lsp::OneOf::Left(true)),
18158 ..Default::default()
18159 };
18160 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18161
18162 cx.set_state(indoc! {"
18163 struct Fˇoo {}
18164 "});
18165
18166 cx.update_editor(|editor, _window, cx| {
18167 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18168 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18169 editor.highlight_background::<DocumentHighlightRead>(
18170 &[highlight_range],
18171 |c| c.editor_document_highlight_read_background,
18172 cx,
18173 );
18174 });
18175
18176 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18177 .expect("Prepare rename was not started")
18178 .await
18179 .expect("Prepare rename failed");
18180
18181 let mut rename_handler =
18182 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18183 let edit = lsp::TextEdit {
18184 range: lsp::Range {
18185 start: lsp::Position {
18186 line: 0,
18187 character: 7,
18188 },
18189 end: lsp::Position {
18190 line: 0,
18191 character: 10,
18192 },
18193 },
18194 new_text: "FooRenamed".to_string(),
18195 };
18196 Ok(Some(lsp::WorkspaceEdit::new(
18197 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18198 )))
18199 });
18200 let rename_task = cx
18201 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18202 .expect("Confirm rename was not started");
18203 rename_handler.next().await.unwrap();
18204 rename_task.await.expect("Confirm rename failed");
18205 cx.run_until_parked();
18206
18207 // Correct range is renamed, as `surrounding_word` is used to find it.
18208 cx.assert_editor_state(indoc! {"
18209 struct FooRenamedˇ {}
18210 "});
18211}
18212
18213#[gpui::test]
18214async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18215 init_test(cx, |_| {});
18216 let mut cx = EditorTestContext::new(cx).await;
18217
18218 let language = Arc::new(
18219 Language::new(
18220 LanguageConfig::default(),
18221 Some(tree_sitter_html::LANGUAGE.into()),
18222 )
18223 .with_brackets_query(
18224 r#"
18225 ("<" @open "/>" @close)
18226 ("</" @open ">" @close)
18227 ("<" @open ">" @close)
18228 ("\"" @open "\"" @close)
18229 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18230 "#,
18231 )
18232 .unwrap(),
18233 );
18234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18235
18236 cx.set_state(indoc! {"
18237 <span>ˇ</span>
18238 "});
18239 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18240 cx.assert_editor_state(indoc! {"
18241 <span>
18242 ˇ
18243 </span>
18244 "});
18245
18246 cx.set_state(indoc! {"
18247 <span><span></span>ˇ</span>
18248 "});
18249 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18250 cx.assert_editor_state(indoc! {"
18251 <span><span></span>
18252 ˇ</span>
18253 "});
18254
18255 cx.set_state(indoc! {"
18256 <span>ˇ
18257 </span>
18258 "});
18259 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18260 cx.assert_editor_state(indoc! {"
18261 <span>
18262 ˇ
18263 </span>
18264 "});
18265}
18266
18267#[gpui::test(iterations = 10)]
18268async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18269 init_test(cx, |_| {});
18270
18271 let fs = FakeFs::new(cx.executor());
18272 fs.insert_tree(
18273 path!("/dir"),
18274 json!({
18275 "a.ts": "a",
18276 }),
18277 )
18278 .await;
18279
18280 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18281 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18282 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18283
18284 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18285 language_registry.add(Arc::new(Language::new(
18286 LanguageConfig {
18287 name: "TypeScript".into(),
18288 matcher: LanguageMatcher {
18289 path_suffixes: vec!["ts".to_string()],
18290 ..Default::default()
18291 },
18292 ..Default::default()
18293 },
18294 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18295 )));
18296 let mut fake_language_servers = language_registry.register_fake_lsp(
18297 "TypeScript",
18298 FakeLspAdapter {
18299 capabilities: lsp::ServerCapabilities {
18300 code_lens_provider: Some(lsp::CodeLensOptions {
18301 resolve_provider: Some(true),
18302 }),
18303 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18304 commands: vec!["_the/command".to_string()],
18305 ..lsp::ExecuteCommandOptions::default()
18306 }),
18307 ..lsp::ServerCapabilities::default()
18308 },
18309 ..FakeLspAdapter::default()
18310 },
18311 );
18312
18313 let (buffer, _handle) = project
18314 .update(cx, |p, cx| {
18315 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18316 })
18317 .await
18318 .unwrap();
18319 cx.executor().run_until_parked();
18320
18321 let fake_server = fake_language_servers.next().await.unwrap();
18322
18323 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18324 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18325 drop(buffer_snapshot);
18326 let actions = cx
18327 .update_window(*workspace, |_, window, cx| {
18328 project.code_actions(&buffer, anchor..anchor, window, cx)
18329 })
18330 .unwrap();
18331
18332 fake_server
18333 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18334 Ok(Some(vec![
18335 lsp::CodeLens {
18336 range: lsp::Range::default(),
18337 command: Some(lsp::Command {
18338 title: "Code lens command".to_owned(),
18339 command: "_the/command".to_owned(),
18340 arguments: None,
18341 }),
18342 data: None,
18343 },
18344 lsp::CodeLens {
18345 range: lsp::Range::default(),
18346 command: Some(lsp::Command {
18347 title: "Command not in capabilities".to_owned(),
18348 command: "not in capabilities".to_owned(),
18349 arguments: None,
18350 }),
18351 data: None,
18352 },
18353 lsp::CodeLens {
18354 range: lsp::Range {
18355 start: lsp::Position {
18356 line: 1,
18357 character: 1,
18358 },
18359 end: lsp::Position {
18360 line: 1,
18361 character: 1,
18362 },
18363 },
18364 command: Some(lsp::Command {
18365 title: "Command not in range".to_owned(),
18366 command: "_the/command".to_owned(),
18367 arguments: None,
18368 }),
18369 data: None,
18370 },
18371 ]))
18372 })
18373 .next()
18374 .await;
18375
18376 let actions = actions.await.unwrap();
18377 assert_eq!(
18378 actions.len(),
18379 1,
18380 "Should have only one valid action for the 0..0 range"
18381 );
18382 let action = actions[0].clone();
18383 let apply = project.update(cx, |project, cx| {
18384 project.apply_code_action(buffer.clone(), action, true, cx)
18385 });
18386
18387 // Resolving the code action does not populate its edits. In absence of
18388 // edits, we must execute the given command.
18389 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18390 |mut lens, _| async move {
18391 let lens_command = lens.command.as_mut().expect("should have a command");
18392 assert_eq!(lens_command.title, "Code lens command");
18393 lens_command.arguments = Some(vec![json!("the-argument")]);
18394 Ok(lens)
18395 },
18396 );
18397
18398 // While executing the command, the language server sends the editor
18399 // a `workspaceEdit` request.
18400 fake_server
18401 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18402 let fake = fake_server.clone();
18403 move |params, _| {
18404 assert_eq!(params.command, "_the/command");
18405 let fake = fake.clone();
18406 async move {
18407 fake.server
18408 .request::<lsp::request::ApplyWorkspaceEdit>(
18409 lsp::ApplyWorkspaceEditParams {
18410 label: None,
18411 edit: lsp::WorkspaceEdit {
18412 changes: Some(
18413 [(
18414 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18415 vec![lsp::TextEdit {
18416 range: lsp::Range::new(
18417 lsp::Position::new(0, 0),
18418 lsp::Position::new(0, 0),
18419 ),
18420 new_text: "X".into(),
18421 }],
18422 )]
18423 .into_iter()
18424 .collect(),
18425 ),
18426 ..Default::default()
18427 },
18428 },
18429 )
18430 .await
18431 .unwrap();
18432 Ok(Some(json!(null)))
18433 }
18434 }
18435 })
18436 .next()
18437 .await;
18438
18439 // Applying the code lens command returns a project transaction containing the edits
18440 // sent by the language server in its `workspaceEdit` request.
18441 let transaction = apply.await.unwrap();
18442 assert!(transaction.0.contains_key(&buffer));
18443 buffer.update(cx, |buffer, cx| {
18444 assert_eq!(buffer.text(), "Xa");
18445 buffer.undo(cx);
18446 assert_eq!(buffer.text(), "a");
18447 });
18448}
18449
18450#[gpui::test]
18451async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18452 init_test(cx, |_| {});
18453
18454 let fs = FakeFs::new(cx.executor());
18455 let main_text = r#"fn main() {
18456println!("1");
18457println!("2");
18458println!("3");
18459println!("4");
18460println!("5");
18461}"#;
18462 let lib_text = "mod foo {}";
18463 fs.insert_tree(
18464 path!("/a"),
18465 json!({
18466 "lib.rs": lib_text,
18467 "main.rs": main_text,
18468 }),
18469 )
18470 .await;
18471
18472 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18473 let (workspace, cx) =
18474 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18475 let worktree_id = workspace.update(cx, |workspace, cx| {
18476 workspace.project().update(cx, |project, cx| {
18477 project.worktrees(cx).next().unwrap().read(cx).id()
18478 })
18479 });
18480
18481 let expected_ranges = vec![
18482 Point::new(0, 0)..Point::new(0, 0),
18483 Point::new(1, 0)..Point::new(1, 1),
18484 Point::new(2, 0)..Point::new(2, 2),
18485 Point::new(3, 0)..Point::new(3, 3),
18486 ];
18487
18488 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18489 let editor_1 = workspace
18490 .update_in(cx, |workspace, window, cx| {
18491 workspace.open_path(
18492 (worktree_id, "main.rs"),
18493 Some(pane_1.downgrade()),
18494 true,
18495 window,
18496 cx,
18497 )
18498 })
18499 .unwrap()
18500 .await
18501 .downcast::<Editor>()
18502 .unwrap();
18503 pane_1.update(cx, |pane, cx| {
18504 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18505 open_editor.update(cx, |editor, cx| {
18506 assert_eq!(
18507 editor.display_text(cx),
18508 main_text,
18509 "Original main.rs text on initial open",
18510 );
18511 assert_eq!(
18512 editor
18513 .selections
18514 .all::<Point>(cx)
18515 .into_iter()
18516 .map(|s| s.range())
18517 .collect::<Vec<_>>(),
18518 vec![Point::zero()..Point::zero()],
18519 "Default selections on initial open",
18520 );
18521 })
18522 });
18523 editor_1.update_in(cx, |editor, window, cx| {
18524 editor.change_selections(None, window, cx, |s| {
18525 s.select_ranges(expected_ranges.clone());
18526 });
18527 });
18528
18529 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18530 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18531 });
18532 let editor_2 = workspace
18533 .update_in(cx, |workspace, window, cx| {
18534 workspace.open_path(
18535 (worktree_id, "main.rs"),
18536 Some(pane_2.downgrade()),
18537 true,
18538 window,
18539 cx,
18540 )
18541 })
18542 .unwrap()
18543 .await
18544 .downcast::<Editor>()
18545 .unwrap();
18546 pane_2.update(cx, |pane, cx| {
18547 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18548 open_editor.update(cx, |editor, cx| {
18549 assert_eq!(
18550 editor.display_text(cx),
18551 main_text,
18552 "Original main.rs text on initial open in another panel",
18553 );
18554 assert_eq!(
18555 editor
18556 .selections
18557 .all::<Point>(cx)
18558 .into_iter()
18559 .map(|s| s.range())
18560 .collect::<Vec<_>>(),
18561 vec![Point::zero()..Point::zero()],
18562 "Default selections on initial open in another panel",
18563 );
18564 })
18565 });
18566
18567 editor_2.update_in(cx, |editor, window, cx| {
18568 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18569 });
18570
18571 let _other_editor_1 = workspace
18572 .update_in(cx, |workspace, window, cx| {
18573 workspace.open_path(
18574 (worktree_id, "lib.rs"),
18575 Some(pane_1.downgrade()),
18576 true,
18577 window,
18578 cx,
18579 )
18580 })
18581 .unwrap()
18582 .await
18583 .downcast::<Editor>()
18584 .unwrap();
18585 pane_1
18586 .update_in(cx, |pane, window, cx| {
18587 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18588 .unwrap()
18589 })
18590 .await
18591 .unwrap();
18592 drop(editor_1);
18593 pane_1.update(cx, |pane, cx| {
18594 pane.active_item()
18595 .unwrap()
18596 .downcast::<Editor>()
18597 .unwrap()
18598 .update(cx, |editor, cx| {
18599 assert_eq!(
18600 editor.display_text(cx),
18601 lib_text,
18602 "Other file should be open and active",
18603 );
18604 });
18605 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18606 });
18607
18608 let _other_editor_2 = workspace
18609 .update_in(cx, |workspace, window, cx| {
18610 workspace.open_path(
18611 (worktree_id, "lib.rs"),
18612 Some(pane_2.downgrade()),
18613 true,
18614 window,
18615 cx,
18616 )
18617 })
18618 .unwrap()
18619 .await
18620 .downcast::<Editor>()
18621 .unwrap();
18622 pane_2
18623 .update_in(cx, |pane, window, cx| {
18624 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18625 .unwrap()
18626 })
18627 .await
18628 .unwrap();
18629 drop(editor_2);
18630 pane_2.update(cx, |pane, cx| {
18631 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18632 open_editor.update(cx, |editor, cx| {
18633 assert_eq!(
18634 editor.display_text(cx),
18635 lib_text,
18636 "Other file should be open and active in another panel too",
18637 );
18638 });
18639 assert_eq!(
18640 pane.items().count(),
18641 1,
18642 "No other editors should be open in another pane",
18643 );
18644 });
18645
18646 let _editor_1_reopened = workspace
18647 .update_in(cx, |workspace, window, cx| {
18648 workspace.open_path(
18649 (worktree_id, "main.rs"),
18650 Some(pane_1.downgrade()),
18651 true,
18652 window,
18653 cx,
18654 )
18655 })
18656 .unwrap()
18657 .await
18658 .downcast::<Editor>()
18659 .unwrap();
18660 let _editor_2_reopened = workspace
18661 .update_in(cx, |workspace, window, cx| {
18662 workspace.open_path(
18663 (worktree_id, "main.rs"),
18664 Some(pane_2.downgrade()),
18665 true,
18666 window,
18667 cx,
18668 )
18669 })
18670 .unwrap()
18671 .await
18672 .downcast::<Editor>()
18673 .unwrap();
18674 pane_1.update(cx, |pane, cx| {
18675 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18676 open_editor.update(cx, |editor, cx| {
18677 assert_eq!(
18678 editor.display_text(cx),
18679 main_text,
18680 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18681 );
18682 assert_eq!(
18683 editor
18684 .selections
18685 .all::<Point>(cx)
18686 .into_iter()
18687 .map(|s| s.range())
18688 .collect::<Vec<_>>(),
18689 expected_ranges,
18690 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18691 );
18692 })
18693 });
18694 pane_2.update(cx, |pane, cx| {
18695 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18696 open_editor.update(cx, |editor, cx| {
18697 assert_eq!(
18698 editor.display_text(cx),
18699 r#"fn main() {
18700⋯rintln!("1");
18701⋯intln!("2");
18702⋯ntln!("3");
18703println!("4");
18704println!("5");
18705}"#,
18706 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18707 );
18708 assert_eq!(
18709 editor
18710 .selections
18711 .all::<Point>(cx)
18712 .into_iter()
18713 .map(|s| s.range())
18714 .collect::<Vec<_>>(),
18715 vec![Point::zero()..Point::zero()],
18716 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18717 );
18718 })
18719 });
18720}
18721
18722#[gpui::test]
18723async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18724 init_test(cx, |_| {});
18725
18726 let fs = FakeFs::new(cx.executor());
18727 let main_text = r#"fn main() {
18728println!("1");
18729println!("2");
18730println!("3");
18731println!("4");
18732println!("5");
18733}"#;
18734 let lib_text = "mod foo {}";
18735 fs.insert_tree(
18736 path!("/a"),
18737 json!({
18738 "lib.rs": lib_text,
18739 "main.rs": main_text,
18740 }),
18741 )
18742 .await;
18743
18744 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18745 let (workspace, cx) =
18746 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18747 let worktree_id = workspace.update(cx, |workspace, cx| {
18748 workspace.project().update(cx, |project, cx| {
18749 project.worktrees(cx).next().unwrap().read(cx).id()
18750 })
18751 });
18752
18753 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18754 let editor = workspace
18755 .update_in(cx, |workspace, window, cx| {
18756 workspace.open_path(
18757 (worktree_id, "main.rs"),
18758 Some(pane.downgrade()),
18759 true,
18760 window,
18761 cx,
18762 )
18763 })
18764 .unwrap()
18765 .await
18766 .downcast::<Editor>()
18767 .unwrap();
18768 pane.update(cx, |pane, cx| {
18769 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18770 open_editor.update(cx, |editor, cx| {
18771 assert_eq!(
18772 editor.display_text(cx),
18773 main_text,
18774 "Original main.rs text on initial open",
18775 );
18776 })
18777 });
18778 editor.update_in(cx, |editor, window, cx| {
18779 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18780 });
18781
18782 cx.update_global(|store: &mut SettingsStore, cx| {
18783 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18784 s.restore_on_file_reopen = Some(false);
18785 });
18786 });
18787 editor.update_in(cx, |editor, window, cx| {
18788 editor.fold_ranges(
18789 vec![
18790 Point::new(1, 0)..Point::new(1, 1),
18791 Point::new(2, 0)..Point::new(2, 2),
18792 Point::new(3, 0)..Point::new(3, 3),
18793 ],
18794 false,
18795 window,
18796 cx,
18797 );
18798 });
18799 pane.update_in(cx, |pane, window, cx| {
18800 pane.close_all_items(&CloseAllItems::default(), window, cx)
18801 .unwrap()
18802 })
18803 .await
18804 .unwrap();
18805 pane.update(cx, |pane, _| {
18806 assert!(pane.active_item().is_none());
18807 });
18808 cx.update_global(|store: &mut SettingsStore, cx| {
18809 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18810 s.restore_on_file_reopen = Some(true);
18811 });
18812 });
18813
18814 let _editor_reopened = workspace
18815 .update_in(cx, |workspace, window, cx| {
18816 workspace.open_path(
18817 (worktree_id, "main.rs"),
18818 Some(pane.downgrade()),
18819 true,
18820 window,
18821 cx,
18822 )
18823 })
18824 .unwrap()
18825 .await
18826 .downcast::<Editor>()
18827 .unwrap();
18828 pane.update(cx, |pane, cx| {
18829 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18830 open_editor.update(cx, |editor, cx| {
18831 assert_eq!(
18832 editor.display_text(cx),
18833 main_text,
18834 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18835 );
18836 })
18837 });
18838}
18839
18840fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18841 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18842 point..point
18843}
18844
18845fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18846 let (text, ranges) = marked_text_ranges(marked_text, true);
18847 assert_eq!(editor.text(cx), text);
18848 assert_eq!(
18849 editor.selections.ranges(cx),
18850 ranges,
18851 "Assert selections are {}",
18852 marked_text
18853 );
18854}
18855
18856pub fn handle_signature_help_request(
18857 cx: &mut EditorLspTestContext,
18858 mocked_response: lsp::SignatureHelp,
18859) -> impl Future<Output = ()> + use<> {
18860 let mut request =
18861 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18862 let mocked_response = mocked_response.clone();
18863 async move { Ok(Some(mocked_response)) }
18864 });
18865
18866 async move {
18867 request.next().await;
18868 }
18869}
18870
18871/// Handle completion request passing a marked string specifying where the completion
18872/// should be triggered from using '|' character, what range should be replaced, and what completions
18873/// should be returned using '<' and '>' to delimit the range.
18874///
18875/// Also see `handle_completion_request_with_insert_and_replace`.
18876#[track_caller]
18877pub fn handle_completion_request(
18878 cx: &mut EditorLspTestContext,
18879 marked_string: &str,
18880 completions: Vec<&'static str>,
18881 counter: Arc<AtomicUsize>,
18882) -> impl Future<Output = ()> {
18883 let complete_from_marker: TextRangeMarker = '|'.into();
18884 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18885 let (_, mut marked_ranges) = marked_text_ranges_by(
18886 marked_string,
18887 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18888 );
18889
18890 let complete_from_position =
18891 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18892 let replace_range =
18893 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18894
18895 let mut request =
18896 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18897 let completions = completions.clone();
18898 counter.fetch_add(1, atomic::Ordering::Release);
18899 async move {
18900 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18901 assert_eq!(
18902 params.text_document_position.position,
18903 complete_from_position
18904 );
18905 Ok(Some(lsp::CompletionResponse::Array(
18906 completions
18907 .iter()
18908 .map(|completion_text| lsp::CompletionItem {
18909 label: completion_text.to_string(),
18910 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18911 range: replace_range,
18912 new_text: completion_text.to_string(),
18913 })),
18914 ..Default::default()
18915 })
18916 .collect(),
18917 )))
18918 }
18919 });
18920
18921 async move {
18922 request.next().await;
18923 }
18924}
18925
18926/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
18927/// given instead, which also contains an `insert` range.
18928///
18929/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
18930/// that is, `replace_range.start..cursor_pos`.
18931pub fn handle_completion_request_with_insert_and_replace(
18932 cx: &mut EditorLspTestContext,
18933 marked_string: &str,
18934 completions: Vec<&'static str>,
18935 counter: Arc<AtomicUsize>,
18936) -> impl Future<Output = ()> {
18937 let complete_from_marker: TextRangeMarker = '|'.into();
18938 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18939 let (_, mut marked_ranges) = marked_text_ranges_by(
18940 marked_string,
18941 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18942 );
18943
18944 let complete_from_position =
18945 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18946 let replace_range =
18947 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18948
18949 let mut request =
18950 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18951 let completions = completions.clone();
18952 counter.fetch_add(1, atomic::Ordering::Release);
18953 async move {
18954 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18955 assert_eq!(
18956 params.text_document_position.position, complete_from_position,
18957 "marker `|` position doesn't match",
18958 );
18959 Ok(Some(lsp::CompletionResponse::Array(
18960 completions
18961 .iter()
18962 .map(|completion_text| lsp::CompletionItem {
18963 label: completion_text.to_string(),
18964 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18965 lsp::InsertReplaceEdit {
18966 insert: lsp::Range {
18967 start: replace_range.start,
18968 end: complete_from_position,
18969 },
18970 replace: replace_range,
18971 new_text: completion_text.to_string(),
18972 },
18973 )),
18974 ..Default::default()
18975 })
18976 .collect(),
18977 )))
18978 }
18979 });
18980
18981 async move {
18982 request.next().await;
18983 }
18984}
18985
18986fn handle_resolve_completion_request(
18987 cx: &mut EditorLspTestContext,
18988 edits: Option<Vec<(&'static str, &'static str)>>,
18989) -> impl Future<Output = ()> {
18990 let edits = edits.map(|edits| {
18991 edits
18992 .iter()
18993 .map(|(marked_string, new_text)| {
18994 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18995 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18996 lsp::TextEdit::new(replace_range, new_text.to_string())
18997 })
18998 .collect::<Vec<_>>()
18999 });
19000
19001 let mut request =
19002 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19003 let edits = edits.clone();
19004 async move {
19005 Ok(lsp::CompletionItem {
19006 additional_text_edits: edits,
19007 ..Default::default()
19008 })
19009 }
19010 });
19011
19012 async move {
19013 request.next().await;
19014 }
19015}
19016
19017pub(crate) fn update_test_language_settings(
19018 cx: &mut TestAppContext,
19019 f: impl Fn(&mut AllLanguageSettingsContent),
19020) {
19021 cx.update(|cx| {
19022 SettingsStore::update_global(cx, |store, cx| {
19023 store.update_user_settings::<AllLanguageSettings>(cx, f);
19024 });
19025 });
19026}
19027
19028pub(crate) fn update_test_project_settings(
19029 cx: &mut TestAppContext,
19030 f: impl Fn(&mut ProjectSettings),
19031) {
19032 cx.update(|cx| {
19033 SettingsStore::update_global(cx, |store, cx| {
19034 store.update_user_settings::<ProjectSettings>(cx, f);
19035 });
19036 });
19037}
19038
19039pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19040 cx.update(|cx| {
19041 assets::Assets.load_test_fonts(cx);
19042 let store = SettingsStore::test(cx);
19043 cx.set_global(store);
19044 theme::init(theme::LoadThemes::JustBase, cx);
19045 release_channel::init(SemanticVersion::default(), cx);
19046 client::init_settings(cx);
19047 language::init(cx);
19048 Project::init_settings(cx);
19049 workspace::init_settings(cx);
19050 crate::init(cx);
19051 });
19052
19053 update_test_language_settings(cx, f);
19054}
19055
19056#[track_caller]
19057fn assert_hunk_revert(
19058 not_reverted_text_with_selections: &str,
19059 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19060 expected_reverted_text_with_selections: &str,
19061 base_text: &str,
19062 cx: &mut EditorLspTestContext,
19063) {
19064 cx.set_state(not_reverted_text_with_selections);
19065 cx.set_head_text(base_text);
19066 cx.executor().run_until_parked();
19067
19068 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19069 let snapshot = editor.snapshot(window, cx);
19070 let reverted_hunk_statuses = snapshot
19071 .buffer_snapshot
19072 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19073 .map(|hunk| hunk.status().kind)
19074 .collect::<Vec<_>>();
19075
19076 editor.git_restore(&Default::default(), window, cx);
19077 reverted_hunk_statuses
19078 });
19079 cx.executor().run_until_parked();
19080 cx.assert_editor_state(expected_reverted_text_with_selections);
19081 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19082}