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#[gpui::test]
1320fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1321 init_test(cx, |_| {});
1322
1323 let editor = cx.add_window(|window, cx| {
1324 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1325 build_editor(buffer.clone(), window, cx)
1326 });
1327
1328 assert_eq!('🟥'.len_utf8(), 4);
1329 assert_eq!('α'.len_utf8(), 2);
1330
1331 _ = editor.update(cx, |editor, window, cx| {
1332 editor.fold_creases(
1333 vec![
1334 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1335 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1336 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1337 ],
1338 true,
1339 window,
1340 cx,
1341 );
1342 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1343
1344 editor.move_right(&MoveRight, window, cx);
1345 assert_eq!(
1346 editor.selections.display_ranges(cx),
1347 &[empty_range(0, "🟥".len())]
1348 );
1349 editor.move_right(&MoveRight, window, cx);
1350 assert_eq!(
1351 editor.selections.display_ranges(cx),
1352 &[empty_range(0, "🟥🟧".len())]
1353 );
1354 editor.move_right(&MoveRight, window, cx);
1355 assert_eq!(
1356 editor.selections.display_ranges(cx),
1357 &[empty_range(0, "🟥🟧⋯".len())]
1358 );
1359
1360 editor.move_down(&MoveDown, window, cx);
1361 assert_eq!(
1362 editor.selections.display_ranges(cx),
1363 &[empty_range(1, "ab⋯e".len())]
1364 );
1365 editor.move_left(&MoveLeft, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(1, "ab⋯".len())]
1369 );
1370 editor.move_left(&MoveLeft, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(1, "ab".len())]
1374 );
1375 editor.move_left(&MoveLeft, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(1, "a".len())]
1379 );
1380
1381 editor.move_down(&MoveDown, window, cx);
1382 assert_eq!(
1383 editor.selections.display_ranges(cx),
1384 &[empty_range(2, "α".len())]
1385 );
1386 editor.move_right(&MoveRight, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(2, "αβ".len())]
1390 );
1391 editor.move_right(&MoveRight, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(2, "αβ⋯".len())]
1395 );
1396 editor.move_right(&MoveRight, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(2, "αβ⋯ε".len())]
1400 );
1401
1402 editor.move_up(&MoveUp, window, cx);
1403 assert_eq!(
1404 editor.selections.display_ranges(cx),
1405 &[empty_range(1, "ab⋯e".len())]
1406 );
1407 editor.move_down(&MoveDown, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(2, "αβ⋯ε".len())]
1411 );
1412 editor.move_up(&MoveUp, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(1, "ab⋯e".len())]
1416 );
1417
1418 editor.move_up(&MoveUp, window, cx);
1419 assert_eq!(
1420 editor.selections.display_ranges(cx),
1421 &[empty_range(0, "🟥🟧".len())]
1422 );
1423 editor.move_left(&MoveLeft, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(0, "🟥".len())]
1427 );
1428 editor.move_left(&MoveLeft, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[empty_range(0, "".len())]
1432 );
1433 });
1434}
1435
1436#[gpui::test]
1437fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1438 init_test(cx, |_| {});
1439
1440 let editor = cx.add_window(|window, cx| {
1441 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1442 build_editor(buffer.clone(), window, cx)
1443 });
1444 _ = editor.update(cx, |editor, window, cx| {
1445 editor.change_selections(None, window, cx, |s| {
1446 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1447 });
1448
1449 // moving above start of document should move selection to start of document,
1450 // but the next move down should still be at the original goal_x
1451 editor.move_up(&MoveUp, window, cx);
1452 assert_eq!(
1453 editor.selections.display_ranges(cx),
1454 &[empty_range(0, "".len())]
1455 );
1456
1457 editor.move_down(&MoveDown, window, cx);
1458 assert_eq!(
1459 editor.selections.display_ranges(cx),
1460 &[empty_range(1, "abcd".len())]
1461 );
1462
1463 editor.move_down(&MoveDown, window, cx);
1464 assert_eq!(
1465 editor.selections.display_ranges(cx),
1466 &[empty_range(2, "αβγ".len())]
1467 );
1468
1469 editor.move_down(&MoveDown, window, cx);
1470 assert_eq!(
1471 editor.selections.display_ranges(cx),
1472 &[empty_range(3, "abcd".len())]
1473 );
1474
1475 editor.move_down(&MoveDown, window, cx);
1476 assert_eq!(
1477 editor.selections.display_ranges(cx),
1478 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1479 );
1480
1481 // moving past end of document should not change goal_x
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(5, "".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(5, "".len())]
1492 );
1493
1494 editor.move_up(&MoveUp, window, cx);
1495 assert_eq!(
1496 editor.selections.display_ranges(cx),
1497 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1498 );
1499
1500 editor.move_up(&MoveUp, window, cx);
1501 assert_eq!(
1502 editor.selections.display_ranges(cx),
1503 &[empty_range(3, "abcd".len())]
1504 );
1505
1506 editor.move_up(&MoveUp, window, cx);
1507 assert_eq!(
1508 editor.selections.display_ranges(cx),
1509 &[empty_range(2, "αβγ".len())]
1510 );
1511 });
1512}
1513
1514#[gpui::test]
1515fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1516 init_test(cx, |_| {});
1517 let move_to_beg = MoveToBeginningOfLine {
1518 stop_at_soft_wraps: true,
1519 stop_at_indent: true,
1520 };
1521
1522 let delete_to_beg = DeleteToBeginningOfLine {
1523 stop_at_indent: false,
1524 };
1525
1526 let move_to_end = MoveToEndOfLine {
1527 stop_at_soft_wraps: true,
1528 };
1529
1530 let editor = cx.add_window(|window, cx| {
1531 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1532 build_editor(buffer, window, cx)
1533 });
1534 _ = editor.update(cx, |editor, window, cx| {
1535 editor.change_selections(None, window, cx, |s| {
1536 s.select_display_ranges([
1537 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1538 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1539 ]);
1540 });
1541 });
1542
1543 _ = editor.update(cx, |editor, window, cx| {
1544 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1545 assert_eq!(
1546 editor.selections.display_ranges(cx),
1547 &[
1548 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1549 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1550 ]
1551 );
1552 });
1553
1554 _ = editor.update(cx, |editor, window, cx| {
1555 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[
1559 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1560 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1561 ]
1562 );
1563 });
1564
1565 _ = editor.update(cx, |editor, window, cx| {
1566 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1567 assert_eq!(
1568 editor.selections.display_ranges(cx),
1569 &[
1570 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1571 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1572 ]
1573 );
1574 });
1575
1576 _ = editor.update(cx, |editor, window, cx| {
1577 editor.move_to_end_of_line(&move_to_end, window, cx);
1578 assert_eq!(
1579 editor.selections.display_ranges(cx),
1580 &[
1581 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1582 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1583 ]
1584 );
1585 });
1586
1587 // Moving to the end of line again is a no-op.
1588 _ = editor.update(cx, |editor, window, cx| {
1589 editor.move_to_end_of_line(&move_to_end, window, cx);
1590 assert_eq!(
1591 editor.selections.display_ranges(cx),
1592 &[
1593 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1594 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1595 ]
1596 );
1597 });
1598
1599 _ = editor.update(cx, |editor, window, cx| {
1600 editor.move_left(&MoveLeft, window, cx);
1601 editor.select_to_beginning_of_line(
1602 &SelectToBeginningOfLine {
1603 stop_at_soft_wraps: true,
1604 stop_at_indent: true,
1605 },
1606 window,
1607 cx,
1608 );
1609 assert_eq!(
1610 editor.selections.display_ranges(cx),
1611 &[
1612 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1613 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1614 ]
1615 );
1616 });
1617
1618 _ = editor.update(cx, |editor, window, cx| {
1619 editor.select_to_beginning_of_line(
1620 &SelectToBeginningOfLine {
1621 stop_at_soft_wraps: true,
1622 stop_at_indent: true,
1623 },
1624 window,
1625 cx,
1626 );
1627 assert_eq!(
1628 editor.selections.display_ranges(cx),
1629 &[
1630 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1631 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1632 ]
1633 );
1634 });
1635
1636 _ = editor.update(cx, |editor, window, cx| {
1637 editor.select_to_beginning_of_line(
1638 &SelectToBeginningOfLine {
1639 stop_at_soft_wraps: true,
1640 stop_at_indent: true,
1641 },
1642 window,
1643 cx,
1644 );
1645 assert_eq!(
1646 editor.selections.display_ranges(cx),
1647 &[
1648 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1649 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1650 ]
1651 );
1652 });
1653
1654 _ = editor.update(cx, |editor, window, cx| {
1655 editor.select_to_end_of_line(
1656 &SelectToEndOfLine {
1657 stop_at_soft_wraps: true,
1658 },
1659 window,
1660 cx,
1661 );
1662 assert_eq!(
1663 editor.selections.display_ranges(cx),
1664 &[
1665 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1666 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1667 ]
1668 );
1669 });
1670
1671 _ = editor.update(cx, |editor, window, cx| {
1672 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1673 assert_eq!(editor.display_text(cx), "ab\n de");
1674 assert_eq!(
1675 editor.selections.display_ranges(cx),
1676 &[
1677 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1678 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1679 ]
1680 );
1681 });
1682
1683 _ = editor.update(cx, |editor, window, cx| {
1684 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1685 assert_eq!(editor.display_text(cx), "\n");
1686 assert_eq!(
1687 editor.selections.display_ranges(cx),
1688 &[
1689 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1691 ]
1692 );
1693 });
1694}
1695
1696#[gpui::test]
1697fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1698 init_test(cx, |_| {});
1699 let move_to_beg = MoveToBeginningOfLine {
1700 stop_at_soft_wraps: false,
1701 stop_at_indent: false,
1702 };
1703
1704 let move_to_end = MoveToEndOfLine {
1705 stop_at_soft_wraps: false,
1706 };
1707
1708 let editor = cx.add_window(|window, cx| {
1709 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1710 build_editor(buffer, window, cx)
1711 });
1712
1713 _ = editor.update(cx, |editor, window, cx| {
1714 editor.set_wrap_width(Some(140.0.into()), cx);
1715
1716 // We expect the following lines after wrapping
1717 // ```
1718 // thequickbrownfox
1719 // jumpedoverthelazydo
1720 // gs
1721 // ```
1722 // The final `gs` was soft-wrapped onto a new line.
1723 assert_eq!(
1724 "thequickbrownfox\njumpedoverthelaz\nydogs",
1725 editor.display_text(cx),
1726 );
1727
1728 // First, let's assert behavior on the first line, that was not soft-wrapped.
1729 // Start the cursor at the `k` on the first line
1730 editor.change_selections(None, window, cx, |s| {
1731 s.select_display_ranges([
1732 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1733 ]);
1734 });
1735
1736 // Moving to the beginning of the line should put us at the beginning of the line.
1737 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1738 assert_eq!(
1739 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1740 editor.selections.display_ranges(cx)
1741 );
1742
1743 // Moving to the end of the line should put us at the end of the line.
1744 editor.move_to_end_of_line(&move_to_end, window, cx);
1745 assert_eq!(
1746 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1747 editor.selections.display_ranges(cx)
1748 );
1749
1750 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1751 // Start the cursor at the last line (`y` that was wrapped to a new line)
1752 editor.change_selections(None, window, cx, |s| {
1753 s.select_display_ranges([
1754 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1755 ]);
1756 });
1757
1758 // Moving to the beginning of the line should put us at the start of the second line of
1759 // display text, i.e., the `j`.
1760 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1761 assert_eq!(
1762 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1763 editor.selections.display_ranges(cx)
1764 );
1765
1766 // Moving to the beginning of the line again should be a no-op.
1767 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1768 assert_eq!(
1769 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1770 editor.selections.display_ranges(cx)
1771 );
1772
1773 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1774 // next display line.
1775 editor.move_to_end_of_line(&move_to_end, window, cx);
1776 assert_eq!(
1777 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1778 editor.selections.display_ranges(cx)
1779 );
1780
1781 // Moving to the end of the line again should be a no-op.
1782 editor.move_to_end_of_line(&move_to_end, window, cx);
1783 assert_eq!(
1784 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1785 editor.selections.display_ranges(cx)
1786 );
1787 });
1788}
1789
1790#[gpui::test]
1791fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1792 init_test(cx, |_| {});
1793
1794 let move_to_beg = MoveToBeginningOfLine {
1795 stop_at_soft_wraps: true,
1796 stop_at_indent: true,
1797 };
1798
1799 let select_to_beg = SelectToBeginningOfLine {
1800 stop_at_soft_wraps: true,
1801 stop_at_indent: true,
1802 };
1803
1804 let delete_to_beg = DeleteToBeginningOfLine {
1805 stop_at_indent: true,
1806 };
1807
1808 let move_to_end = MoveToEndOfLine {
1809 stop_at_soft_wraps: false,
1810 };
1811
1812 let editor = cx.add_window(|window, cx| {
1813 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1814 build_editor(buffer, window, cx)
1815 });
1816
1817 _ = editor.update(cx, |editor, window, cx| {
1818 editor.change_selections(None, window, cx, |s| {
1819 s.select_display_ranges([
1820 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1822 ]);
1823 });
1824
1825 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1826 // and the second cursor at the first non-whitespace character in the line.
1827 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1828 assert_eq!(
1829 editor.selections.display_ranges(cx),
1830 &[
1831 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1832 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1833 ]
1834 );
1835
1836 // Moving to the beginning of the line again should be a no-op for the first cursor,
1837 // and should move the second cursor to the beginning of the line.
1838 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1839 assert_eq!(
1840 editor.selections.display_ranges(cx),
1841 &[
1842 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1843 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1844 ]
1845 );
1846
1847 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1848 // and should move the second cursor back to the first non-whitespace character in the line.
1849 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1850 assert_eq!(
1851 editor.selections.display_ranges(cx),
1852 &[
1853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1854 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1855 ]
1856 );
1857
1858 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1859 // and to the first non-whitespace character in the line for the second cursor.
1860 editor.move_to_end_of_line(&move_to_end, window, cx);
1861 editor.move_left(&MoveLeft, window, cx);
1862 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1872 // and should select to the beginning of the line for the second cursor.
1873 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1874 assert_eq!(
1875 editor.selections.display_ranges(cx),
1876 &[
1877 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1878 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1879 ]
1880 );
1881
1882 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1883 // and should delete to the first non-whitespace character in the line for the second cursor.
1884 editor.move_to_end_of_line(&move_to_end, window, cx);
1885 editor.move_left(&MoveLeft, window, cx);
1886 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1887 assert_eq!(editor.text(cx), "c\n f");
1888 });
1889}
1890
1891#[gpui::test]
1892fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1893 init_test(cx, |_| {});
1894
1895 let editor = cx.add_window(|window, cx| {
1896 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1897 build_editor(buffer, window, cx)
1898 });
1899 _ = editor.update(cx, |editor, window, cx| {
1900 editor.change_selections(None, window, cx, |s| {
1901 s.select_display_ranges([
1902 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1903 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1904 ])
1905 });
1906
1907 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1908 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1909
1910 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1911 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1912
1913 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1914 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1915
1916 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1917 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1918
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1921
1922 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1923 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1924
1925 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1926 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1929 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1930
1931 editor.move_right(&MoveRight, window, cx);
1932 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1933 assert_selection_ranges(
1934 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1935 editor,
1936 cx,
1937 );
1938
1939 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1940 assert_selection_ranges(
1941 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1942 editor,
1943 cx,
1944 );
1945
1946 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1947 assert_selection_ranges(
1948 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1949 editor,
1950 cx,
1951 );
1952 });
1953}
1954
1955#[gpui::test]
1956fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1957 init_test(cx, |_| {});
1958
1959 let editor = cx.add_window(|window, cx| {
1960 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1961 build_editor(buffer, window, cx)
1962 });
1963
1964 _ = editor.update(cx, |editor, window, cx| {
1965 editor.set_wrap_width(Some(140.0.into()), cx);
1966 assert_eq!(
1967 editor.display_text(cx),
1968 "use one::{\n two::three::\n four::five\n};"
1969 );
1970
1971 editor.change_selections(None, window, cx, |s| {
1972 s.select_display_ranges([
1973 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1974 ]);
1975 });
1976
1977 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1978 assert_eq!(
1979 editor.selections.display_ranges(cx),
1980 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1981 );
1982
1983 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1984 assert_eq!(
1985 editor.selections.display_ranges(cx),
1986 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1987 );
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1999 );
2000
2001 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2011 );
2012 });
2013}
2014
2015#[gpui::test]
2016async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2017 init_test(cx, |_| {});
2018 let mut cx = EditorTestContext::new(cx).await;
2019
2020 let line_height = cx.editor(|editor, window, _| {
2021 editor
2022 .style()
2023 .unwrap()
2024 .text
2025 .line_height_in_pixels(window.rem_size())
2026 });
2027 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2028
2029 cx.set_state(
2030 &r#"ˇone
2031 two
2032
2033 three
2034 fourˇ
2035 five
2036
2037 six"#
2038 .unindent(),
2039 );
2040
2041 cx.update_editor(|editor, window, cx| {
2042 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2043 });
2044 cx.assert_editor_state(
2045 &r#"one
2046 two
2047 ˇ
2048 three
2049 four
2050 five
2051 ˇ
2052 six"#
2053 .unindent(),
2054 );
2055
2056 cx.update_editor(|editor, window, cx| {
2057 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2058 });
2059 cx.assert_editor_state(
2060 &r#"one
2061 two
2062
2063 three
2064 four
2065 five
2066 ˇ
2067 sixˇ"#
2068 .unindent(),
2069 );
2070
2071 cx.update_editor(|editor, window, cx| {
2072 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2073 });
2074 cx.assert_editor_state(
2075 &r#"one
2076 two
2077
2078 three
2079 four
2080 five
2081
2082 sixˇ"#
2083 .unindent(),
2084 );
2085
2086 cx.update_editor(|editor, window, cx| {
2087 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2088 });
2089 cx.assert_editor_state(
2090 &r#"one
2091 two
2092
2093 three
2094 four
2095 five
2096 ˇ
2097 six"#
2098 .unindent(),
2099 );
2100
2101 cx.update_editor(|editor, window, cx| {
2102 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2103 });
2104 cx.assert_editor_state(
2105 &r#"one
2106 two
2107 ˇ
2108 three
2109 four
2110 five
2111
2112 six"#
2113 .unindent(),
2114 );
2115
2116 cx.update_editor(|editor, window, cx| {
2117 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2118 });
2119 cx.assert_editor_state(
2120 &r#"ˇone
2121 two
2122
2123 three
2124 four
2125 five
2126
2127 six"#
2128 .unindent(),
2129 );
2130}
2131
2132#[gpui::test]
2133async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2134 init_test(cx, |_| {});
2135 let mut cx = EditorTestContext::new(cx).await;
2136 let line_height = cx.editor(|editor, window, _| {
2137 editor
2138 .style()
2139 .unwrap()
2140 .text
2141 .line_height_in_pixels(window.rem_size())
2142 });
2143 let window = cx.window;
2144 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2145
2146 cx.set_state(
2147 r#"ˇone
2148 two
2149 three
2150 four
2151 five
2152 six
2153 seven
2154 eight
2155 nine
2156 ten
2157 "#,
2158 );
2159
2160 cx.update_editor(|editor, window, cx| {
2161 assert_eq!(
2162 editor.snapshot(window, cx).scroll_position(),
2163 gpui::Point::new(0., 0.)
2164 );
2165 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2166 assert_eq!(
2167 editor.snapshot(window, cx).scroll_position(),
2168 gpui::Point::new(0., 3.)
2169 );
2170 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2171 assert_eq!(
2172 editor.snapshot(window, cx).scroll_position(),
2173 gpui::Point::new(0., 6.)
2174 );
2175 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2176 assert_eq!(
2177 editor.snapshot(window, cx).scroll_position(),
2178 gpui::Point::new(0., 3.)
2179 );
2180
2181 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2182 assert_eq!(
2183 editor.snapshot(window, cx).scroll_position(),
2184 gpui::Point::new(0., 1.)
2185 );
2186 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2187 assert_eq!(
2188 editor.snapshot(window, cx).scroll_position(),
2189 gpui::Point::new(0., 3.)
2190 );
2191 });
2192}
2193
2194#[gpui::test]
2195async fn test_autoscroll(cx: &mut TestAppContext) {
2196 init_test(cx, |_| {});
2197 let mut cx = EditorTestContext::new(cx).await;
2198
2199 let line_height = cx.update_editor(|editor, window, cx| {
2200 editor.set_vertical_scroll_margin(2, cx);
2201 editor
2202 .style()
2203 .unwrap()
2204 .text
2205 .line_height_in_pixels(window.rem_size())
2206 });
2207 let window = cx.window;
2208 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2209
2210 cx.set_state(
2211 r#"ˇone
2212 two
2213 three
2214 four
2215 five
2216 six
2217 seven
2218 eight
2219 nine
2220 ten
2221 "#,
2222 );
2223 cx.update_editor(|editor, window, cx| {
2224 assert_eq!(
2225 editor.snapshot(window, cx).scroll_position(),
2226 gpui::Point::new(0., 0.0)
2227 );
2228 });
2229
2230 // Add a cursor below the visible area. Since both cursors cannot fit
2231 // on screen, the editor autoscrolls to reveal the newest cursor, and
2232 // allows the vertical scroll margin below that cursor.
2233 cx.update_editor(|editor, window, cx| {
2234 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2235 selections.select_ranges([
2236 Point::new(0, 0)..Point::new(0, 0),
2237 Point::new(6, 0)..Point::new(6, 0),
2238 ]);
2239 })
2240 });
2241 cx.update_editor(|editor, window, cx| {
2242 assert_eq!(
2243 editor.snapshot(window, cx).scroll_position(),
2244 gpui::Point::new(0., 3.0)
2245 );
2246 });
2247
2248 // Move down. The editor cursor scrolls down to track the newest cursor.
2249 cx.update_editor(|editor, window, cx| {
2250 editor.move_down(&Default::default(), window, cx);
2251 });
2252 cx.update_editor(|editor, window, cx| {
2253 assert_eq!(
2254 editor.snapshot(window, cx).scroll_position(),
2255 gpui::Point::new(0., 4.0)
2256 );
2257 });
2258
2259 // Add a cursor above the visible area. Since both cursors fit on screen,
2260 // the editor scrolls to show both.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2263 selections.select_ranges([
2264 Point::new(1, 0)..Point::new(1, 0),
2265 Point::new(6, 0)..Point::new(6, 0),
2266 ]);
2267 })
2268 });
2269 cx.update_editor(|editor, window, cx| {
2270 assert_eq!(
2271 editor.snapshot(window, cx).scroll_position(),
2272 gpui::Point::new(0., 1.0)
2273 );
2274 });
2275}
2276
2277#[gpui::test]
2278async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2279 init_test(cx, |_| {});
2280 let mut cx = EditorTestContext::new(cx).await;
2281
2282 let line_height = cx.editor(|editor, window, _cx| {
2283 editor
2284 .style()
2285 .unwrap()
2286 .text
2287 .line_height_in_pixels(window.rem_size())
2288 });
2289 let window = cx.window;
2290 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2291 cx.set_state(
2292 &r#"
2293 ˇone
2294 two
2295 threeˇ
2296 four
2297 five
2298 six
2299 seven
2300 eight
2301 nine
2302 ten
2303 "#
2304 .unindent(),
2305 );
2306
2307 cx.update_editor(|editor, window, cx| {
2308 editor.move_page_down(&MovePageDown::default(), window, cx)
2309 });
2310 cx.assert_editor_state(
2311 &r#"
2312 one
2313 two
2314 three
2315 ˇfour
2316 five
2317 sixˇ
2318 seven
2319 eight
2320 nine
2321 ten
2322 "#
2323 .unindent(),
2324 );
2325
2326 cx.update_editor(|editor, window, cx| {
2327 editor.move_page_down(&MovePageDown::default(), window, cx)
2328 });
2329 cx.assert_editor_state(
2330 &r#"
2331 one
2332 two
2333 three
2334 four
2335 five
2336 six
2337 ˇseven
2338 eight
2339 nineˇ
2340 ten
2341 "#
2342 .unindent(),
2343 );
2344
2345 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2346 cx.assert_editor_state(
2347 &r#"
2348 one
2349 two
2350 three
2351 ˇfour
2352 five
2353 sixˇ
2354 seven
2355 eight
2356 nine
2357 ten
2358 "#
2359 .unindent(),
2360 );
2361
2362 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2363 cx.assert_editor_state(
2364 &r#"
2365 ˇone
2366 two
2367 threeˇ
2368 four
2369 five
2370 six
2371 seven
2372 eight
2373 nine
2374 ten
2375 "#
2376 .unindent(),
2377 );
2378
2379 // Test select collapsing
2380 cx.update_editor(|editor, window, cx| {
2381 editor.move_page_down(&MovePageDown::default(), window, cx);
2382 editor.move_page_down(&MovePageDown::default(), window, cx);
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 });
2385 cx.assert_editor_state(
2386 &r#"
2387 one
2388 two
2389 three
2390 four
2391 five
2392 six
2393 seven
2394 eight
2395 nine
2396 ˇten
2397 ˇ"#
2398 .unindent(),
2399 );
2400}
2401
2402#[gpui::test]
2403async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2404 init_test(cx, |_| {});
2405 let mut cx = EditorTestContext::new(cx).await;
2406 cx.set_state("one «two threeˇ» four");
2407 cx.update_editor(|editor, window, cx| {
2408 editor.delete_to_beginning_of_line(
2409 &DeleteToBeginningOfLine {
2410 stop_at_indent: false,
2411 },
2412 window,
2413 cx,
2414 );
2415 assert_eq!(editor.text(cx), " four");
2416 });
2417}
2418
2419#[gpui::test]
2420fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2421 init_test(cx, |_| {});
2422
2423 let editor = cx.add_window(|window, cx| {
2424 let buffer = MultiBuffer::build_simple("one two three four", cx);
2425 build_editor(buffer.clone(), window, cx)
2426 });
2427
2428 _ = editor.update(cx, |editor, window, cx| {
2429 editor.change_selections(None, window, cx, |s| {
2430 s.select_display_ranges([
2431 // an empty selection - the preceding word fragment is deleted
2432 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2433 // characters selected - they are deleted
2434 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2435 ])
2436 });
2437 editor.delete_to_previous_word_start(
2438 &DeleteToPreviousWordStart {
2439 ignore_newlines: false,
2440 },
2441 window,
2442 cx,
2443 );
2444 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2445 });
2446
2447 _ = editor.update(cx, |editor, window, cx| {
2448 editor.change_selections(None, window, cx, |s| {
2449 s.select_display_ranges([
2450 // an empty selection - the following word fragment is deleted
2451 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2452 // characters selected - they are deleted
2453 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2454 ])
2455 });
2456 editor.delete_to_next_word_end(
2457 &DeleteToNextWordEnd {
2458 ignore_newlines: false,
2459 },
2460 window,
2461 cx,
2462 );
2463 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2464 });
2465}
2466
2467#[gpui::test]
2468fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2469 init_test(cx, |_| {});
2470
2471 let editor = cx.add_window(|window, cx| {
2472 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2473 build_editor(buffer.clone(), window, cx)
2474 });
2475 let del_to_prev_word_start = DeleteToPreviousWordStart {
2476 ignore_newlines: false,
2477 };
2478 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2479 ignore_newlines: true,
2480 };
2481
2482 _ = editor.update(cx, |editor, window, cx| {
2483 editor.change_selections(None, window, cx, |s| {
2484 s.select_display_ranges([
2485 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2486 ])
2487 });
2488 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2489 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
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");
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\n");
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");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
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(), "");
2500 });
2501}
2502
2503#[gpui::test]
2504fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2505 init_test(cx, |_| {});
2506
2507 let editor = cx.add_window(|window, cx| {
2508 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2509 build_editor(buffer.clone(), window, cx)
2510 });
2511 let del_to_next_word_end = DeleteToNextWordEnd {
2512 ignore_newlines: false,
2513 };
2514 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2515 ignore_newlines: true,
2516 };
2517
2518 _ = editor.update(cx, |editor, window, cx| {
2519 editor.change_selections(None, window, cx, |s| {
2520 s.select_display_ranges([
2521 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2522 ])
2523 });
2524 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2525 assert_eq!(
2526 editor.buffer.read(cx).read(cx).text(),
2527 "one\n two\nthree\n four"
2528 );
2529 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2530 assert_eq!(
2531 editor.buffer.read(cx).read(cx).text(),
2532 "\n two\nthree\n four"
2533 );
2534 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2535 assert_eq!(
2536 editor.buffer.read(cx).read(cx).text(),
2537 "two\nthree\n four"
2538 );
2539 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2540 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2541 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\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(), "");
2545 });
2546}
2547
2548#[gpui::test]
2549fn test_newline(cx: &mut TestAppContext) {
2550 init_test(cx, |_| {});
2551
2552 let editor = cx.add_window(|window, cx| {
2553 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2554 build_editor(buffer.clone(), window, cx)
2555 });
2556
2557 _ = editor.update(cx, |editor, window, cx| {
2558 editor.change_selections(None, window, cx, |s| {
2559 s.select_display_ranges([
2560 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2561 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2562 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2563 ])
2564 });
2565
2566 editor.newline(&Newline, window, cx);
2567 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2568 });
2569}
2570
2571#[gpui::test]
2572fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2573 init_test(cx, |_| {});
2574
2575 let editor = cx.add_window(|window, cx| {
2576 let buffer = MultiBuffer::build_simple(
2577 "
2578 a
2579 b(
2580 X
2581 )
2582 c(
2583 X
2584 )
2585 "
2586 .unindent()
2587 .as_str(),
2588 cx,
2589 );
2590 let mut editor = build_editor(buffer.clone(), window, cx);
2591 editor.change_selections(None, window, cx, |s| {
2592 s.select_ranges([
2593 Point::new(2, 4)..Point::new(2, 5),
2594 Point::new(5, 4)..Point::new(5, 5),
2595 ])
2596 });
2597 editor
2598 });
2599
2600 _ = editor.update(cx, |editor, window, cx| {
2601 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2602 editor.buffer.update(cx, |buffer, cx| {
2603 buffer.edit(
2604 [
2605 (Point::new(1, 2)..Point::new(3, 0), ""),
2606 (Point::new(4, 2)..Point::new(6, 0), ""),
2607 ],
2608 None,
2609 cx,
2610 );
2611 assert_eq!(
2612 buffer.read(cx).text(),
2613 "
2614 a
2615 b()
2616 c()
2617 "
2618 .unindent()
2619 );
2620 });
2621 assert_eq!(
2622 editor.selections.ranges(cx),
2623 &[
2624 Point::new(1, 2)..Point::new(1, 2),
2625 Point::new(2, 2)..Point::new(2, 2),
2626 ],
2627 );
2628
2629 editor.newline(&Newline, window, cx);
2630 assert_eq!(
2631 editor.text(cx),
2632 "
2633 a
2634 b(
2635 )
2636 c(
2637 )
2638 "
2639 .unindent()
2640 );
2641
2642 // The selections are moved after the inserted newlines
2643 assert_eq!(
2644 editor.selections.ranges(cx),
2645 &[
2646 Point::new(2, 0)..Point::new(2, 0),
2647 Point::new(4, 0)..Point::new(4, 0),
2648 ],
2649 );
2650 });
2651}
2652
2653#[gpui::test]
2654async fn test_newline_above(cx: &mut TestAppContext) {
2655 init_test(cx, |settings| {
2656 settings.defaults.tab_size = NonZeroU32::new(4)
2657 });
2658
2659 let language = Arc::new(
2660 Language::new(
2661 LanguageConfig::default(),
2662 Some(tree_sitter_rust::LANGUAGE.into()),
2663 )
2664 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2665 .unwrap(),
2666 );
2667
2668 let mut cx = EditorTestContext::new(cx).await;
2669 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2670 cx.set_state(indoc! {"
2671 const a: ˇA = (
2672 (ˇ
2673 «const_functionˇ»(ˇ),
2674 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2675 )ˇ
2676 ˇ);ˇ
2677 "});
2678
2679 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2680 cx.assert_editor_state(indoc! {"
2681 ˇ
2682 const a: A = (
2683 ˇ
2684 (
2685 ˇ
2686 ˇ
2687 const_function(),
2688 ˇ
2689 ˇ
2690 ˇ
2691 ˇ
2692 something_else,
2693 ˇ
2694 )
2695 ˇ
2696 ˇ
2697 );
2698 "});
2699}
2700
2701#[gpui::test]
2702async fn test_newline_below(cx: &mut TestAppContext) {
2703 init_test(cx, |settings| {
2704 settings.defaults.tab_size = NonZeroU32::new(4)
2705 });
2706
2707 let language = Arc::new(
2708 Language::new(
2709 LanguageConfig::default(),
2710 Some(tree_sitter_rust::LANGUAGE.into()),
2711 )
2712 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2713 .unwrap(),
2714 );
2715
2716 let mut cx = EditorTestContext::new(cx).await;
2717 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2718 cx.set_state(indoc! {"
2719 const a: ˇA = (
2720 (ˇ
2721 «const_functionˇ»(ˇ),
2722 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2723 )ˇ
2724 ˇ);ˇ
2725 "});
2726
2727 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2728 cx.assert_editor_state(indoc! {"
2729 const a: A = (
2730 ˇ
2731 (
2732 ˇ
2733 const_function(),
2734 ˇ
2735 ˇ
2736 something_else,
2737 ˇ
2738 ˇ
2739 ˇ
2740 ˇ
2741 )
2742 ˇ
2743 );
2744 ˇ
2745 ˇ
2746 "});
2747}
2748
2749#[gpui::test]
2750async fn test_newline_comments(cx: &mut TestAppContext) {
2751 init_test(cx, |settings| {
2752 settings.defaults.tab_size = NonZeroU32::new(4)
2753 });
2754
2755 let language = Arc::new(Language::new(
2756 LanguageConfig {
2757 line_comments: vec!["//".into()],
2758 ..LanguageConfig::default()
2759 },
2760 None,
2761 ));
2762 {
2763 let mut cx = EditorTestContext::new(cx).await;
2764 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2765 cx.set_state(indoc! {"
2766 // Fooˇ
2767 "});
2768
2769 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2770 cx.assert_editor_state(indoc! {"
2771 // Foo
2772 //ˇ
2773 "});
2774 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2775 cx.set_state(indoc! {"
2776 ˇ// Foo
2777 "});
2778 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2779 cx.assert_editor_state(indoc! {"
2780
2781 ˇ// Foo
2782 "});
2783 }
2784 // Ensure that comment continuations can be disabled.
2785 update_test_language_settings(cx, |settings| {
2786 settings.defaults.extend_comment_on_newline = Some(false);
2787 });
2788 let mut cx = EditorTestContext::new(cx).await;
2789 cx.set_state(indoc! {"
2790 // Fooˇ
2791 "});
2792 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2793 cx.assert_editor_state(indoc! {"
2794 // Foo
2795 ˇ
2796 "});
2797}
2798
2799#[gpui::test]
2800fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2801 init_test(cx, |_| {});
2802
2803 let editor = cx.add_window(|window, cx| {
2804 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2805 let mut editor = build_editor(buffer.clone(), window, cx);
2806 editor.change_selections(None, window, cx, |s| {
2807 s.select_ranges([3..4, 11..12, 19..20])
2808 });
2809 editor
2810 });
2811
2812 _ = editor.update(cx, |editor, window, cx| {
2813 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2814 editor.buffer.update(cx, |buffer, cx| {
2815 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2816 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2817 });
2818 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2819
2820 editor.insert("Z", window, cx);
2821 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2822
2823 // The selections are moved after the inserted characters
2824 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2825 });
2826}
2827
2828#[gpui::test]
2829async fn test_tab(cx: &mut TestAppContext) {
2830 init_test(cx, |settings| {
2831 settings.defaults.tab_size = NonZeroU32::new(3)
2832 });
2833
2834 let mut cx = EditorTestContext::new(cx).await;
2835 cx.set_state(indoc! {"
2836 ˇabˇc
2837 ˇ🏀ˇ🏀ˇefg
2838 dˇ
2839 "});
2840 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2841 cx.assert_editor_state(indoc! {"
2842 ˇab ˇc
2843 ˇ🏀 ˇ🏀 ˇefg
2844 d ˇ
2845 "});
2846
2847 cx.set_state(indoc! {"
2848 a
2849 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2850 "});
2851 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2852 cx.assert_editor_state(indoc! {"
2853 a
2854 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2855 "});
2856}
2857
2858#[gpui::test]
2859async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2860 init_test(cx, |_| {});
2861
2862 let mut cx = EditorTestContext::new(cx).await;
2863 let language = Arc::new(
2864 Language::new(
2865 LanguageConfig::default(),
2866 Some(tree_sitter_rust::LANGUAGE.into()),
2867 )
2868 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2869 .unwrap(),
2870 );
2871 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2872
2873 // cursors that are already at the suggested indent level insert
2874 // a soft tab. cursors that are to the left of the suggested indent
2875 // auto-indent their line.
2876 cx.set_state(indoc! {"
2877 ˇ
2878 const a: B = (
2879 c(
2880 d(
2881 ˇ
2882 )
2883 ˇ
2884 ˇ )
2885 );
2886 "});
2887 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2888 cx.assert_editor_state(indoc! {"
2889 ˇ
2890 const a: B = (
2891 c(
2892 d(
2893 ˇ
2894 )
2895 ˇ
2896 ˇ)
2897 );
2898 "});
2899
2900 // handle auto-indent when there are multiple cursors on the same line
2901 cx.set_state(indoc! {"
2902 const a: B = (
2903 c(
2904 ˇ ˇ
2905 ˇ )
2906 );
2907 "});
2908 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2909 cx.assert_editor_state(indoc! {"
2910 const a: B = (
2911 c(
2912 ˇ
2913 ˇ)
2914 );
2915 "});
2916}
2917
2918#[gpui::test]
2919async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
2920 init_test(cx, |settings| {
2921 settings.defaults.tab_size = NonZeroU32::new(3)
2922 });
2923
2924 let mut cx = EditorTestContext::new(cx).await;
2925 cx.set_state(indoc! {"
2926 ˇ
2927 \t ˇ
2928 \t ˇ
2929 \t ˇ
2930 \t \t\t \t \t\t \t\t \t \t ˇ
2931 "});
2932
2933 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2934 cx.assert_editor_state(indoc! {"
2935 ˇ
2936 \t ˇ
2937 \t ˇ
2938 \t ˇ
2939 \t \t\t \t \t\t \t\t \t \t ˇ
2940 "});
2941}
2942
2943#[gpui::test]
2944async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
2945 init_test(cx, |settings| {
2946 settings.defaults.tab_size = NonZeroU32::new(4)
2947 });
2948
2949 let language = Arc::new(
2950 Language::new(
2951 LanguageConfig::default(),
2952 Some(tree_sitter_rust::LANGUAGE.into()),
2953 )
2954 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2955 .unwrap(),
2956 );
2957
2958 let mut cx = EditorTestContext::new(cx).await;
2959 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2960 cx.set_state(indoc! {"
2961 fn a() {
2962 if b {
2963 \t ˇc
2964 }
2965 }
2966 "});
2967
2968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 fn a() {
2971 if b {
2972 ˇc
2973 }
2974 }
2975 "});
2976}
2977
2978#[gpui::test]
2979async fn test_indent_outdent(cx: &mut TestAppContext) {
2980 init_test(cx, |settings| {
2981 settings.defaults.tab_size = NonZeroU32::new(4);
2982 });
2983
2984 let mut cx = EditorTestContext::new(cx).await;
2985
2986 cx.set_state(indoc! {"
2987 «oneˇ» «twoˇ»
2988 three
2989 four
2990 "});
2991 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 «oneˇ» «twoˇ»
2994 three
2995 four
2996 "});
2997
2998 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2999 cx.assert_editor_state(indoc! {"
3000 «oneˇ» «twoˇ»
3001 three
3002 four
3003 "});
3004
3005 // select across line ending
3006 cx.set_state(indoc! {"
3007 one two
3008 t«hree
3009 ˇ» four
3010 "});
3011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 one two
3014 t«hree
3015 ˇ» four
3016 "});
3017
3018 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3019 cx.assert_editor_state(indoc! {"
3020 one two
3021 t«hree
3022 ˇ» four
3023 "});
3024
3025 // Ensure that indenting/outdenting works when the cursor is at column 0.
3026 cx.set_state(indoc! {"
3027 one two
3028 ˇthree
3029 four
3030 "});
3031 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3032 cx.assert_editor_state(indoc! {"
3033 one two
3034 ˇthree
3035 four
3036 "});
3037
3038 cx.set_state(indoc! {"
3039 one two
3040 ˇ three
3041 four
3042 "});
3043 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3044 cx.assert_editor_state(indoc! {"
3045 one two
3046 ˇthree
3047 four
3048 "});
3049}
3050
3051#[gpui::test]
3052async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3053 init_test(cx, |settings| {
3054 settings.defaults.hard_tabs = Some(true);
3055 });
3056
3057 let mut cx = EditorTestContext::new(cx).await;
3058
3059 // select two ranges on one line
3060 cx.set_state(indoc! {"
3061 «oneˇ» «twoˇ»
3062 three
3063 four
3064 "});
3065 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3066 cx.assert_editor_state(indoc! {"
3067 \t«oneˇ» «twoˇ»
3068 three
3069 four
3070 "});
3071 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3072 cx.assert_editor_state(indoc! {"
3073 \t\t«oneˇ» «twoˇ»
3074 three
3075 four
3076 "});
3077 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3078 cx.assert_editor_state(indoc! {"
3079 \t«oneˇ» «twoˇ»
3080 three
3081 four
3082 "});
3083 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3084 cx.assert_editor_state(indoc! {"
3085 «oneˇ» «twoˇ»
3086 three
3087 four
3088 "});
3089
3090 // select across a line ending
3091 cx.set_state(indoc! {"
3092 one two
3093 t«hree
3094 ˇ»four
3095 "});
3096 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3097 cx.assert_editor_state(indoc! {"
3098 one two
3099 \tt«hree
3100 ˇ»four
3101 "});
3102 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3103 cx.assert_editor_state(indoc! {"
3104 one two
3105 \t\tt«hree
3106 ˇ»four
3107 "});
3108 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3109 cx.assert_editor_state(indoc! {"
3110 one two
3111 \tt«hree
3112 ˇ»four
3113 "});
3114 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3115 cx.assert_editor_state(indoc! {"
3116 one two
3117 t«hree
3118 ˇ»four
3119 "});
3120
3121 // Ensure that indenting/outdenting works when the cursor is at column 0.
3122 cx.set_state(indoc! {"
3123 one two
3124 ˇthree
3125 four
3126 "});
3127 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 one two
3130 ˇthree
3131 four
3132 "});
3133 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3134 cx.assert_editor_state(indoc! {"
3135 one two
3136 \tˇthree
3137 four
3138 "});
3139 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3140 cx.assert_editor_state(indoc! {"
3141 one two
3142 ˇthree
3143 four
3144 "});
3145}
3146
3147#[gpui::test]
3148fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3149 init_test(cx, |settings| {
3150 settings.languages.extend([
3151 (
3152 "TOML".into(),
3153 LanguageSettingsContent {
3154 tab_size: NonZeroU32::new(2),
3155 ..Default::default()
3156 },
3157 ),
3158 (
3159 "Rust".into(),
3160 LanguageSettingsContent {
3161 tab_size: NonZeroU32::new(4),
3162 ..Default::default()
3163 },
3164 ),
3165 ]);
3166 });
3167
3168 let toml_language = Arc::new(Language::new(
3169 LanguageConfig {
3170 name: "TOML".into(),
3171 ..Default::default()
3172 },
3173 None,
3174 ));
3175 let rust_language = Arc::new(Language::new(
3176 LanguageConfig {
3177 name: "Rust".into(),
3178 ..Default::default()
3179 },
3180 None,
3181 ));
3182
3183 let toml_buffer =
3184 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3185 let rust_buffer =
3186 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3187 let multibuffer = cx.new(|cx| {
3188 let mut multibuffer = MultiBuffer::new(ReadWrite);
3189 multibuffer.push_excerpts(
3190 toml_buffer.clone(),
3191 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3192 cx,
3193 );
3194 multibuffer.push_excerpts(
3195 rust_buffer.clone(),
3196 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3197 cx,
3198 );
3199 multibuffer
3200 });
3201
3202 cx.add_window(|window, cx| {
3203 let mut editor = build_editor(multibuffer, window, cx);
3204
3205 assert_eq!(
3206 editor.text(cx),
3207 indoc! {"
3208 a = 1
3209 b = 2
3210
3211 const c: usize = 3;
3212 "}
3213 );
3214
3215 select_ranges(
3216 &mut editor,
3217 indoc! {"
3218 «aˇ» = 1
3219 b = 2
3220
3221 «const c:ˇ» usize = 3;
3222 "},
3223 window,
3224 cx,
3225 );
3226
3227 editor.tab(&Tab, window, cx);
3228 assert_text_with_selections(
3229 &mut editor,
3230 indoc! {"
3231 «aˇ» = 1
3232 b = 2
3233
3234 «const c:ˇ» usize = 3;
3235 "},
3236 cx,
3237 );
3238 editor.backtab(&Backtab, window, cx);
3239 assert_text_with_selections(
3240 &mut editor,
3241 indoc! {"
3242 «aˇ» = 1
3243 b = 2
3244
3245 «const c:ˇ» usize = 3;
3246 "},
3247 cx,
3248 );
3249
3250 editor
3251 });
3252}
3253
3254#[gpui::test]
3255async fn test_backspace(cx: &mut TestAppContext) {
3256 init_test(cx, |_| {});
3257
3258 let mut cx = EditorTestContext::new(cx).await;
3259
3260 // Basic backspace
3261 cx.set_state(indoc! {"
3262 onˇe two three
3263 fou«rˇ» five six
3264 seven «ˇeight nine
3265 »ten
3266 "});
3267 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 oˇe two three
3270 fouˇ five six
3271 seven ˇten
3272 "});
3273
3274 // Test backspace inside and around indents
3275 cx.set_state(indoc! {"
3276 zero
3277 ˇone
3278 ˇtwo
3279 ˇ ˇ ˇ three
3280 ˇ ˇ four
3281 "});
3282 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3283 cx.assert_editor_state(indoc! {"
3284 zero
3285 ˇone
3286 ˇtwo
3287 ˇ threeˇ four
3288 "});
3289}
3290
3291#[gpui::test]
3292async fn test_delete(cx: &mut TestAppContext) {
3293 init_test(cx, |_| {});
3294
3295 let mut cx = EditorTestContext::new(cx).await;
3296 cx.set_state(indoc! {"
3297 onˇe two three
3298 fou«rˇ» five six
3299 seven «ˇeight nine
3300 »ten
3301 "});
3302 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3303 cx.assert_editor_state(indoc! {"
3304 onˇ two three
3305 fouˇ five six
3306 seven ˇten
3307 "});
3308}
3309
3310#[gpui::test]
3311fn test_delete_line(cx: &mut TestAppContext) {
3312 init_test(cx, |_| {});
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(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3322 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3323 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3324 ])
3325 });
3326 editor.delete_line(&DeleteLine, window, cx);
3327 assert_eq!(editor.display_text(cx), "ghi");
3328 assert_eq!(
3329 editor.selections.display_ranges(cx),
3330 vec![
3331 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3332 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3333 ]
3334 );
3335 });
3336
3337 let editor = cx.add_window(|window, cx| {
3338 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3339 build_editor(buffer, window, cx)
3340 });
3341 _ = editor.update(cx, |editor, window, cx| {
3342 editor.change_selections(None, window, cx, |s| {
3343 s.select_display_ranges([
3344 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3345 ])
3346 });
3347 editor.delete_line(&DeleteLine, window, cx);
3348 assert_eq!(editor.display_text(cx), "ghi\n");
3349 assert_eq!(
3350 editor.selections.display_ranges(cx),
3351 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3352 );
3353 });
3354}
3355
3356#[gpui::test]
3357fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3358 init_test(cx, |_| {});
3359
3360 cx.add_window(|window, cx| {
3361 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3362 let mut editor = build_editor(buffer.clone(), window, cx);
3363 let buffer = buffer.read(cx).as_singleton().unwrap();
3364
3365 assert_eq!(
3366 editor.selections.ranges::<Point>(cx),
3367 &[Point::new(0, 0)..Point::new(0, 0)]
3368 );
3369
3370 // When on single line, replace newline at end by space
3371 editor.join_lines(&JoinLines, window, cx);
3372 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3373 assert_eq!(
3374 editor.selections.ranges::<Point>(cx),
3375 &[Point::new(0, 3)..Point::new(0, 3)]
3376 );
3377
3378 // When multiple lines are selected, remove newlines that are spanned by the selection
3379 editor.change_selections(None, window, cx, |s| {
3380 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3381 });
3382 editor.join_lines(&JoinLines, window, cx);
3383 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3384 assert_eq!(
3385 editor.selections.ranges::<Point>(cx),
3386 &[Point::new(0, 11)..Point::new(0, 11)]
3387 );
3388
3389 // Undo should be transactional
3390 editor.undo(&Undo, window, cx);
3391 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3392 assert_eq!(
3393 editor.selections.ranges::<Point>(cx),
3394 &[Point::new(0, 5)..Point::new(2, 2)]
3395 );
3396
3397 // When joining an empty line don't insert a space
3398 editor.change_selections(None, window, cx, |s| {
3399 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3400 });
3401 editor.join_lines(&JoinLines, window, cx);
3402 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3403 assert_eq!(
3404 editor.selections.ranges::<Point>(cx),
3405 [Point::new(2, 3)..Point::new(2, 3)]
3406 );
3407
3408 // We can remove trailing newlines
3409 editor.join_lines(&JoinLines, window, cx);
3410 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3411 assert_eq!(
3412 editor.selections.ranges::<Point>(cx),
3413 [Point::new(2, 3)..Point::new(2, 3)]
3414 );
3415
3416 // We don't blow up on the last line
3417 editor.join_lines(&JoinLines, window, cx);
3418 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3419 assert_eq!(
3420 editor.selections.ranges::<Point>(cx),
3421 [Point::new(2, 3)..Point::new(2, 3)]
3422 );
3423
3424 // reset to test indentation
3425 editor.buffer.update(cx, |buffer, cx| {
3426 buffer.edit(
3427 [
3428 (Point::new(1, 0)..Point::new(1, 2), " "),
3429 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3430 ],
3431 None,
3432 cx,
3433 )
3434 });
3435
3436 // We remove any leading spaces
3437 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3438 editor.change_selections(None, window, cx, |s| {
3439 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3440 });
3441 editor.join_lines(&JoinLines, window, cx);
3442 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3443
3444 // We don't insert a space for a line containing only spaces
3445 editor.join_lines(&JoinLines, window, cx);
3446 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3447
3448 // We ignore any leading tabs
3449 editor.join_lines(&JoinLines, window, cx);
3450 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3451
3452 editor
3453 });
3454}
3455
3456#[gpui::test]
3457fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3458 init_test(cx, |_| {});
3459
3460 cx.add_window(|window, cx| {
3461 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3462 let mut editor = build_editor(buffer.clone(), window, cx);
3463 let buffer = buffer.read(cx).as_singleton().unwrap();
3464
3465 editor.change_selections(None, window, cx, |s| {
3466 s.select_ranges([
3467 Point::new(0, 2)..Point::new(1, 1),
3468 Point::new(1, 2)..Point::new(1, 2),
3469 Point::new(3, 1)..Point::new(3, 2),
3470 ])
3471 });
3472
3473 editor.join_lines(&JoinLines, window, cx);
3474 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3475
3476 assert_eq!(
3477 editor.selections.ranges::<Point>(cx),
3478 [
3479 Point::new(0, 7)..Point::new(0, 7),
3480 Point::new(1, 3)..Point::new(1, 3)
3481 ]
3482 );
3483 editor
3484 });
3485}
3486
3487#[gpui::test]
3488async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3489 init_test(cx, |_| {});
3490
3491 let mut cx = EditorTestContext::new(cx).await;
3492
3493 let diff_base = r#"
3494 Line 0
3495 Line 1
3496 Line 2
3497 Line 3
3498 "#
3499 .unindent();
3500
3501 cx.set_state(
3502 &r#"
3503 ˇLine 0
3504 Line 1
3505 Line 2
3506 Line 3
3507 "#
3508 .unindent(),
3509 );
3510
3511 cx.set_head_text(&diff_base);
3512 executor.run_until_parked();
3513
3514 // Join lines
3515 cx.update_editor(|editor, window, cx| {
3516 editor.join_lines(&JoinLines, window, cx);
3517 });
3518 executor.run_until_parked();
3519
3520 cx.assert_editor_state(
3521 &r#"
3522 Line 0ˇ Line 1
3523 Line 2
3524 Line 3
3525 "#
3526 .unindent(),
3527 );
3528 // Join again
3529 cx.update_editor(|editor, window, cx| {
3530 editor.join_lines(&JoinLines, window, cx);
3531 });
3532 executor.run_until_parked();
3533
3534 cx.assert_editor_state(
3535 &r#"
3536 Line 0 Line 1ˇ Line 2
3537 Line 3
3538 "#
3539 .unindent(),
3540 );
3541}
3542
3543#[gpui::test]
3544async fn test_custom_newlines_cause_no_false_positive_diffs(
3545 executor: BackgroundExecutor,
3546 cx: &mut TestAppContext,
3547) {
3548 init_test(cx, |_| {});
3549 let mut cx = EditorTestContext::new(cx).await;
3550 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3551 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3552 executor.run_until_parked();
3553
3554 cx.update_editor(|editor, window, cx| {
3555 let snapshot = editor.snapshot(window, cx);
3556 assert_eq!(
3557 snapshot
3558 .buffer_snapshot
3559 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3560 .collect::<Vec<_>>(),
3561 Vec::new(),
3562 "Should not have any diffs for files with custom newlines"
3563 );
3564 });
3565}
3566
3567#[gpui::test]
3568async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3569 init_test(cx, |_| {});
3570
3571 let mut cx = EditorTestContext::new(cx).await;
3572
3573 // Test sort_lines_case_insensitive()
3574 cx.set_state(indoc! {"
3575 «z
3576 y
3577 x
3578 Z
3579 Y
3580 Xˇ»
3581 "});
3582 cx.update_editor(|e, window, cx| {
3583 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3584 });
3585 cx.assert_editor_state(indoc! {"
3586 «x
3587 X
3588 y
3589 Y
3590 z
3591 Zˇ»
3592 "});
3593
3594 // Test reverse_lines()
3595 cx.set_state(indoc! {"
3596 «5
3597 4
3598 3
3599 2
3600 1ˇ»
3601 "});
3602 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3603 cx.assert_editor_state(indoc! {"
3604 «1
3605 2
3606 3
3607 4
3608 5ˇ»
3609 "});
3610
3611 // Skip testing shuffle_line()
3612
3613 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3614 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3615
3616 // Don't manipulate when cursor is on single line, but expand the selection
3617 cx.set_state(indoc! {"
3618 ddˇdd
3619 ccc
3620 bb
3621 a
3622 "});
3623 cx.update_editor(|e, window, cx| {
3624 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3625 });
3626 cx.assert_editor_state(indoc! {"
3627 «ddddˇ»
3628 ccc
3629 bb
3630 a
3631 "});
3632
3633 // Basic manipulate case
3634 // Start selection moves to column 0
3635 // End of selection shrinks to fit shorter line
3636 cx.set_state(indoc! {"
3637 dd«d
3638 ccc
3639 bb
3640 aaaaaˇ»
3641 "});
3642 cx.update_editor(|e, window, cx| {
3643 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3644 });
3645 cx.assert_editor_state(indoc! {"
3646 «aaaaa
3647 bb
3648 ccc
3649 dddˇ»
3650 "});
3651
3652 // Manipulate case with newlines
3653 cx.set_state(indoc! {"
3654 dd«d
3655 ccc
3656
3657 bb
3658 aaaaa
3659
3660 ˇ»
3661 "});
3662 cx.update_editor(|e, window, cx| {
3663 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3664 });
3665 cx.assert_editor_state(indoc! {"
3666 «
3667
3668 aaaaa
3669 bb
3670 ccc
3671 dddˇ»
3672
3673 "});
3674
3675 // Adding new line
3676 cx.set_state(indoc! {"
3677 aa«a
3678 bbˇ»b
3679 "});
3680 cx.update_editor(|e, window, cx| {
3681 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3682 });
3683 cx.assert_editor_state(indoc! {"
3684 «aaa
3685 bbb
3686 added_lineˇ»
3687 "});
3688
3689 // Removing line
3690 cx.set_state(indoc! {"
3691 aa«a
3692 bbbˇ»
3693 "});
3694 cx.update_editor(|e, window, cx| {
3695 e.manipulate_lines(window, cx, |lines| {
3696 lines.pop();
3697 })
3698 });
3699 cx.assert_editor_state(indoc! {"
3700 «aaaˇ»
3701 "});
3702
3703 // Removing all lines
3704 cx.set_state(indoc! {"
3705 aa«a
3706 bbbˇ»
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.manipulate_lines(window, cx, |lines| {
3710 lines.drain(..);
3711 })
3712 });
3713 cx.assert_editor_state(indoc! {"
3714 ˇ
3715 "});
3716}
3717
3718#[gpui::test]
3719async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3720 init_test(cx, |_| {});
3721
3722 let mut cx = EditorTestContext::new(cx).await;
3723
3724 // Consider continuous selection as single selection
3725 cx.set_state(indoc! {"
3726 Aaa«aa
3727 cˇ»c«c
3728 bb
3729 aaaˇ»aa
3730 "});
3731 cx.update_editor(|e, window, cx| {
3732 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3733 });
3734 cx.assert_editor_state(indoc! {"
3735 «Aaaaa
3736 ccc
3737 bb
3738 aaaaaˇ»
3739 "});
3740
3741 cx.set_state(indoc! {"
3742 Aaa«aa
3743 cˇ»c«c
3744 bb
3745 aaaˇ»aa
3746 "});
3747 cx.update_editor(|e, window, cx| {
3748 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3749 });
3750 cx.assert_editor_state(indoc! {"
3751 «Aaaaa
3752 ccc
3753 bbˇ»
3754 "});
3755
3756 // Consider non continuous selection as distinct dedup operations
3757 cx.set_state(indoc! {"
3758 «aaaaa
3759 bb
3760 aaaaa
3761 aaaaaˇ»
3762
3763 aaa«aaˇ»
3764 "});
3765 cx.update_editor(|e, window, cx| {
3766 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3767 });
3768 cx.assert_editor_state(indoc! {"
3769 «aaaaa
3770 bbˇ»
3771
3772 «aaaaaˇ»
3773 "});
3774}
3775
3776#[gpui::test]
3777async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3778 init_test(cx, |_| {});
3779
3780 let mut cx = EditorTestContext::new(cx).await;
3781
3782 cx.set_state(indoc! {"
3783 «Aaa
3784 aAa
3785 Aaaˇ»
3786 "});
3787 cx.update_editor(|e, window, cx| {
3788 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3789 });
3790 cx.assert_editor_state(indoc! {"
3791 «Aaa
3792 aAaˇ»
3793 "});
3794
3795 cx.set_state(indoc! {"
3796 «Aaa
3797 aAa
3798 aaAˇ»
3799 "});
3800 cx.update_editor(|e, window, cx| {
3801 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3802 });
3803 cx.assert_editor_state(indoc! {"
3804 «Aaaˇ»
3805 "});
3806}
3807
3808#[gpui::test]
3809async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3810 init_test(cx, |_| {});
3811
3812 let mut cx = EditorTestContext::new(cx).await;
3813
3814 // Manipulate with multiple selections on a single line
3815 cx.set_state(indoc! {"
3816 dd«dd
3817 cˇ»c«c
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 «aaaaa
3826 bb
3827 ccc
3828 ddddˇ»
3829 "});
3830
3831 // Manipulate with multiple disjoin selections
3832 cx.set_state(indoc! {"
3833 5«
3834 4
3835 3
3836 2
3837 1ˇ»
3838
3839 dd«dd
3840 ccc
3841 bb
3842 aaaˇ»aa
3843 "});
3844 cx.update_editor(|e, window, cx| {
3845 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3846 });
3847 cx.assert_editor_state(indoc! {"
3848 «1
3849 2
3850 3
3851 4
3852 5ˇ»
3853
3854 «aaaaa
3855 bb
3856 ccc
3857 ddddˇ»
3858 "});
3859
3860 // Adding lines on each selection
3861 cx.set_state(indoc! {"
3862 2«
3863 1ˇ»
3864
3865 bb«bb
3866 aaaˇ»aa
3867 "});
3868 cx.update_editor(|e, window, cx| {
3869 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3870 });
3871 cx.assert_editor_state(indoc! {"
3872 «2
3873 1
3874 added lineˇ»
3875
3876 «bbbb
3877 aaaaa
3878 added lineˇ»
3879 "});
3880
3881 // Removing lines on each selection
3882 cx.set_state(indoc! {"
3883 2«
3884 1ˇ»
3885
3886 bb«bb
3887 aaaˇ»aa
3888 "});
3889 cx.update_editor(|e, window, cx| {
3890 e.manipulate_lines(window, cx, |lines| {
3891 lines.pop();
3892 })
3893 });
3894 cx.assert_editor_state(indoc! {"
3895 «2ˇ»
3896
3897 «bbbbˇ»
3898 "});
3899}
3900
3901#[gpui::test]
3902async fn test_toggle_case(cx: &mut TestAppContext) {
3903 init_test(cx, |_| {});
3904
3905 let mut cx = EditorTestContext::new(cx).await;
3906
3907 // If all lower case -> upper case
3908 cx.set_state(indoc! {"
3909 «hello worldˇ»
3910 "});
3911 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3912 cx.assert_editor_state(indoc! {"
3913 «HELLO WORLDˇ»
3914 "});
3915
3916 // If all upper case -> lower case
3917 cx.set_state(indoc! {"
3918 «HELLO WORLDˇ»
3919 "});
3920 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3921 cx.assert_editor_state(indoc! {"
3922 «hello worldˇ»
3923 "});
3924
3925 // If any upper case characters are identified -> lower case
3926 // This matches JetBrains IDEs
3927 cx.set_state(indoc! {"
3928 «hEllo worldˇ»
3929 "});
3930 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3931 cx.assert_editor_state(indoc! {"
3932 «hello worldˇ»
3933 "});
3934}
3935
3936#[gpui::test]
3937async fn test_manipulate_text(cx: &mut TestAppContext) {
3938 init_test(cx, |_| {});
3939
3940 let mut cx = EditorTestContext::new(cx).await;
3941
3942 // Test convert_to_upper_case()
3943 cx.set_state(indoc! {"
3944 «hello worldˇ»
3945 "});
3946 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3947 cx.assert_editor_state(indoc! {"
3948 «HELLO WORLDˇ»
3949 "});
3950
3951 // Test convert_to_lower_case()
3952 cx.set_state(indoc! {"
3953 «HELLO WORLDˇ»
3954 "});
3955 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3956 cx.assert_editor_state(indoc! {"
3957 «hello worldˇ»
3958 "});
3959
3960 // Test multiple line, single selection case
3961 cx.set_state(indoc! {"
3962 «The quick brown
3963 fox jumps over
3964 the lazy dogˇ»
3965 "});
3966 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3967 cx.assert_editor_state(indoc! {"
3968 «The Quick Brown
3969 Fox Jumps Over
3970 The Lazy Dogˇ»
3971 "});
3972
3973 // Test multiple line, single selection case
3974 cx.set_state(indoc! {"
3975 «The quick brown
3976 fox jumps over
3977 the lazy dogˇ»
3978 "});
3979 cx.update_editor(|e, window, cx| {
3980 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3981 });
3982 cx.assert_editor_state(indoc! {"
3983 «TheQuickBrown
3984 FoxJumpsOver
3985 TheLazyDogˇ»
3986 "});
3987
3988 // From here on out, test more complex cases of manipulate_text()
3989
3990 // Test no selection case - should affect words cursors are in
3991 // Cursor at beginning, middle, and end of word
3992 cx.set_state(indoc! {"
3993 ˇhello big beauˇtiful worldˇ
3994 "});
3995 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3996 cx.assert_editor_state(indoc! {"
3997 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3998 "});
3999
4000 // Test multiple selections on a single line and across multiple lines
4001 cx.set_state(indoc! {"
4002 «Theˇ» quick «brown
4003 foxˇ» jumps «overˇ»
4004 the «lazyˇ» dog
4005 "});
4006 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4007 cx.assert_editor_state(indoc! {"
4008 «THEˇ» quick «BROWN
4009 FOXˇ» jumps «OVERˇ»
4010 the «LAZYˇ» dog
4011 "});
4012
4013 // Test case where text length grows
4014 cx.set_state(indoc! {"
4015 «tschüߡ»
4016 "});
4017 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 «TSCHÜSSˇ»
4020 "});
4021
4022 // Test to make sure we don't crash when text shrinks
4023 cx.set_state(indoc! {"
4024 aaa_bbbˇ
4025 "});
4026 cx.update_editor(|e, window, cx| {
4027 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4028 });
4029 cx.assert_editor_state(indoc! {"
4030 «aaaBbbˇ»
4031 "});
4032
4033 // Test to make sure we all aware of the fact that each word can grow and shrink
4034 // Final selections should be aware of this fact
4035 cx.set_state(indoc! {"
4036 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4037 "});
4038 cx.update_editor(|e, window, cx| {
4039 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4040 });
4041 cx.assert_editor_state(indoc! {"
4042 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4043 "});
4044
4045 cx.set_state(indoc! {"
4046 «hElLo, WoRld!ˇ»
4047 "});
4048 cx.update_editor(|e, window, cx| {
4049 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4050 });
4051 cx.assert_editor_state(indoc! {"
4052 «HeLlO, wOrLD!ˇ»
4053 "});
4054}
4055
4056#[gpui::test]
4057fn test_duplicate_line(cx: &mut TestAppContext) {
4058 init_test(cx, |_| {});
4059
4060 let editor = cx.add_window(|window, cx| {
4061 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4062 build_editor(buffer, window, cx)
4063 });
4064 _ = editor.update(cx, |editor, window, cx| {
4065 editor.change_selections(None, window, cx, |s| {
4066 s.select_display_ranges([
4067 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4068 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4069 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4070 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4071 ])
4072 });
4073 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4074 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4075 assert_eq!(
4076 editor.selections.display_ranges(cx),
4077 vec![
4078 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4079 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4080 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4081 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4082 ]
4083 );
4084 });
4085
4086 let editor = cx.add_window(|window, cx| {
4087 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4088 build_editor(buffer, window, cx)
4089 });
4090 _ = editor.update(cx, |editor, window, cx| {
4091 editor.change_selections(None, window, cx, |s| {
4092 s.select_display_ranges([
4093 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4094 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4095 ])
4096 });
4097 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4098 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4099 assert_eq!(
4100 editor.selections.display_ranges(cx),
4101 vec![
4102 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4103 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4104 ]
4105 );
4106 });
4107
4108 // With `move_upwards` the selections stay in place, except for
4109 // the lines inserted above them
4110 let editor = cx.add_window(|window, cx| {
4111 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4112 build_editor(buffer, window, cx)
4113 });
4114 _ = editor.update(cx, |editor, window, cx| {
4115 editor.change_selections(None, window, cx, |s| {
4116 s.select_display_ranges([
4117 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4118 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4119 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4120 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4121 ])
4122 });
4123 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4124 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4125 assert_eq!(
4126 editor.selections.display_ranges(cx),
4127 vec![
4128 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4129 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4130 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4131 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4132 ]
4133 );
4134 });
4135
4136 let editor = cx.add_window(|window, cx| {
4137 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4138 build_editor(buffer, window, cx)
4139 });
4140 _ = editor.update(cx, |editor, window, cx| {
4141 editor.change_selections(None, window, cx, |s| {
4142 s.select_display_ranges([
4143 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4144 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4145 ])
4146 });
4147 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4148 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4149 assert_eq!(
4150 editor.selections.display_ranges(cx),
4151 vec![
4152 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4153 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4154 ]
4155 );
4156 });
4157
4158 let editor = cx.add_window(|window, cx| {
4159 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4160 build_editor(buffer, window, cx)
4161 });
4162 _ = editor.update(cx, |editor, window, cx| {
4163 editor.change_selections(None, window, cx, |s| {
4164 s.select_display_ranges([
4165 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4166 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4167 ])
4168 });
4169 editor.duplicate_selection(&DuplicateSelection, window, cx);
4170 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4171 assert_eq!(
4172 editor.selections.display_ranges(cx),
4173 vec![
4174 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4175 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4176 ]
4177 );
4178 });
4179}
4180
4181#[gpui::test]
4182fn test_move_line_up_down(cx: &mut TestAppContext) {
4183 init_test(cx, |_| {});
4184
4185 let editor = cx.add_window(|window, cx| {
4186 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4187 build_editor(buffer, window, cx)
4188 });
4189 _ = editor.update(cx, |editor, window, cx| {
4190 editor.fold_creases(
4191 vec![
4192 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4193 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4194 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4195 ],
4196 true,
4197 window,
4198 cx,
4199 );
4200 editor.change_selections(None, window, cx, |s| {
4201 s.select_display_ranges([
4202 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4203 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4204 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4205 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4206 ])
4207 });
4208 assert_eq!(
4209 editor.display_text(cx),
4210 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4211 );
4212
4213 editor.move_line_up(&MoveLineUp, window, cx);
4214 assert_eq!(
4215 editor.display_text(cx),
4216 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4217 );
4218 assert_eq!(
4219 editor.selections.display_ranges(cx),
4220 vec![
4221 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4222 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4223 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4224 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4225 ]
4226 );
4227 });
4228
4229 _ = editor.update(cx, |editor, window, cx| {
4230 editor.move_line_down(&MoveLineDown, window, cx);
4231 assert_eq!(
4232 editor.display_text(cx),
4233 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4234 );
4235 assert_eq!(
4236 editor.selections.display_ranges(cx),
4237 vec![
4238 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4239 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4240 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4241 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4242 ]
4243 );
4244 });
4245
4246 _ = editor.update(cx, |editor, window, cx| {
4247 editor.move_line_down(&MoveLineDown, window, cx);
4248 assert_eq!(
4249 editor.display_text(cx),
4250 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4251 );
4252 assert_eq!(
4253 editor.selections.display_ranges(cx),
4254 vec![
4255 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4256 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4257 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4258 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4259 ]
4260 );
4261 });
4262
4263 _ = editor.update(cx, |editor, window, cx| {
4264 editor.move_line_up(&MoveLineUp, window, cx);
4265 assert_eq!(
4266 editor.display_text(cx),
4267 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4268 );
4269 assert_eq!(
4270 editor.selections.display_ranges(cx),
4271 vec![
4272 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4273 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4274 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4275 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4276 ]
4277 );
4278 });
4279}
4280
4281#[gpui::test]
4282fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4283 init_test(cx, |_| {});
4284
4285 let editor = cx.add_window(|window, cx| {
4286 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4287 build_editor(buffer, window, cx)
4288 });
4289 _ = editor.update(cx, |editor, window, cx| {
4290 let snapshot = editor.buffer.read(cx).snapshot(cx);
4291 editor.insert_blocks(
4292 [BlockProperties {
4293 style: BlockStyle::Fixed,
4294 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4295 height: Some(1),
4296 render: Arc::new(|_| div().into_any()),
4297 priority: 0,
4298 }],
4299 Some(Autoscroll::fit()),
4300 cx,
4301 );
4302 editor.change_selections(None, window, cx, |s| {
4303 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4304 });
4305 editor.move_line_down(&MoveLineDown, window, cx);
4306 });
4307}
4308
4309#[gpui::test]
4310async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4311 init_test(cx, |_| {});
4312
4313 let mut cx = EditorTestContext::new(cx).await;
4314 cx.set_state(
4315 &"
4316 ˇzero
4317 one
4318 two
4319 three
4320 four
4321 five
4322 "
4323 .unindent(),
4324 );
4325
4326 // Create a four-line block that replaces three lines of text.
4327 cx.update_editor(|editor, window, cx| {
4328 let snapshot = editor.snapshot(window, cx);
4329 let snapshot = &snapshot.buffer_snapshot;
4330 let placement = BlockPlacement::Replace(
4331 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4332 );
4333 editor.insert_blocks(
4334 [BlockProperties {
4335 placement,
4336 height: Some(4),
4337 style: BlockStyle::Sticky,
4338 render: Arc::new(|_| gpui::div().into_any_element()),
4339 priority: 0,
4340 }],
4341 None,
4342 cx,
4343 );
4344 });
4345
4346 // Move down so that the cursor touches the block.
4347 cx.update_editor(|editor, window, cx| {
4348 editor.move_down(&Default::default(), window, cx);
4349 });
4350 cx.assert_editor_state(
4351 &"
4352 zero
4353 «one
4354 two
4355 threeˇ»
4356 four
4357 five
4358 "
4359 .unindent(),
4360 );
4361
4362 // Move down past the block.
4363 cx.update_editor(|editor, window, cx| {
4364 editor.move_down(&Default::default(), window, cx);
4365 });
4366 cx.assert_editor_state(
4367 &"
4368 zero
4369 one
4370 two
4371 three
4372 ˇfour
4373 five
4374 "
4375 .unindent(),
4376 );
4377}
4378
4379#[gpui::test]
4380fn test_transpose(cx: &mut TestAppContext) {
4381 init_test(cx, |_| {});
4382
4383 _ = cx.add_window(|window, cx| {
4384 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4385 editor.set_style(EditorStyle::default(), window, cx);
4386 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4387 editor.transpose(&Default::default(), window, cx);
4388 assert_eq!(editor.text(cx), "bac");
4389 assert_eq!(editor.selections.ranges(cx), [2..2]);
4390
4391 editor.transpose(&Default::default(), window, cx);
4392 assert_eq!(editor.text(cx), "bca");
4393 assert_eq!(editor.selections.ranges(cx), [3..3]);
4394
4395 editor.transpose(&Default::default(), window, cx);
4396 assert_eq!(editor.text(cx), "bac");
4397 assert_eq!(editor.selections.ranges(cx), [3..3]);
4398
4399 editor
4400 });
4401
4402 _ = cx.add_window(|window, cx| {
4403 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4404 editor.set_style(EditorStyle::default(), window, cx);
4405 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4406 editor.transpose(&Default::default(), window, cx);
4407 assert_eq!(editor.text(cx), "acb\nde");
4408 assert_eq!(editor.selections.ranges(cx), [3..3]);
4409
4410 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4411 editor.transpose(&Default::default(), window, cx);
4412 assert_eq!(editor.text(cx), "acbd\ne");
4413 assert_eq!(editor.selections.ranges(cx), [5..5]);
4414
4415 editor.transpose(&Default::default(), window, cx);
4416 assert_eq!(editor.text(cx), "acbde\n");
4417 assert_eq!(editor.selections.ranges(cx), [6..6]);
4418
4419 editor.transpose(&Default::default(), window, cx);
4420 assert_eq!(editor.text(cx), "acbd\ne");
4421 assert_eq!(editor.selections.ranges(cx), [6..6]);
4422
4423 editor
4424 });
4425
4426 _ = cx.add_window(|window, cx| {
4427 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4428 editor.set_style(EditorStyle::default(), window, cx);
4429 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4430 editor.transpose(&Default::default(), window, cx);
4431 assert_eq!(editor.text(cx), "bacd\ne");
4432 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4433
4434 editor.transpose(&Default::default(), window, cx);
4435 assert_eq!(editor.text(cx), "bcade\n");
4436 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4437
4438 editor.transpose(&Default::default(), window, cx);
4439 assert_eq!(editor.text(cx), "bcda\ne");
4440 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4441
4442 editor.transpose(&Default::default(), window, cx);
4443 assert_eq!(editor.text(cx), "bcade\n");
4444 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4445
4446 editor.transpose(&Default::default(), window, cx);
4447 assert_eq!(editor.text(cx), "bcaed\n");
4448 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4449
4450 editor
4451 });
4452
4453 _ = cx.add_window(|window, cx| {
4454 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4455 editor.set_style(EditorStyle::default(), window, cx);
4456 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4457 editor.transpose(&Default::default(), window, cx);
4458 assert_eq!(editor.text(cx), "🏀🍐✋");
4459 assert_eq!(editor.selections.ranges(cx), [8..8]);
4460
4461 editor.transpose(&Default::default(), window, cx);
4462 assert_eq!(editor.text(cx), "🏀✋🍐");
4463 assert_eq!(editor.selections.ranges(cx), [11..11]);
4464
4465 editor.transpose(&Default::default(), window, cx);
4466 assert_eq!(editor.text(cx), "🏀🍐✋");
4467 assert_eq!(editor.selections.ranges(cx), [11..11]);
4468
4469 editor
4470 });
4471}
4472
4473#[gpui::test]
4474async fn test_rewrap(cx: &mut TestAppContext) {
4475 init_test(cx, |settings| {
4476 settings.languages.extend([
4477 (
4478 "Markdown".into(),
4479 LanguageSettingsContent {
4480 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4481 ..Default::default()
4482 },
4483 ),
4484 (
4485 "Plain Text".into(),
4486 LanguageSettingsContent {
4487 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4488 ..Default::default()
4489 },
4490 ),
4491 ])
4492 });
4493
4494 let mut cx = EditorTestContext::new(cx).await;
4495
4496 let language_with_c_comments = Arc::new(Language::new(
4497 LanguageConfig {
4498 line_comments: vec!["// ".into()],
4499 ..LanguageConfig::default()
4500 },
4501 None,
4502 ));
4503 let language_with_pound_comments = Arc::new(Language::new(
4504 LanguageConfig {
4505 line_comments: vec!["# ".into()],
4506 ..LanguageConfig::default()
4507 },
4508 None,
4509 ));
4510 let markdown_language = Arc::new(Language::new(
4511 LanguageConfig {
4512 name: "Markdown".into(),
4513 ..LanguageConfig::default()
4514 },
4515 None,
4516 ));
4517 let language_with_doc_comments = Arc::new(Language::new(
4518 LanguageConfig {
4519 line_comments: vec!["// ".into(), "/// ".into()],
4520 ..LanguageConfig::default()
4521 },
4522 Some(tree_sitter_rust::LANGUAGE.into()),
4523 ));
4524
4525 let plaintext_language = Arc::new(Language::new(
4526 LanguageConfig {
4527 name: "Plain Text".into(),
4528 ..LanguageConfig::default()
4529 },
4530 None,
4531 ));
4532
4533 assert_rewrap(
4534 indoc! {"
4535 // ˇ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.
4536 "},
4537 indoc! {"
4538 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4539 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4540 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4541 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4542 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4543 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4544 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4545 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4546 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4547 // porttitor id. Aliquam id accumsan eros.
4548 "},
4549 language_with_c_comments.clone(),
4550 &mut cx,
4551 );
4552
4553 // Test that rewrapping works inside of a selection
4554 assert_rewrap(
4555 indoc! {"
4556 «// 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.ˇ»
4557 "},
4558 indoc! {"
4559 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4560 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4561 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4562 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4563 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4564 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4565 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4566 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4567 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4568 // porttitor id. Aliquam id accumsan eros.ˇ»
4569 "},
4570 language_with_c_comments.clone(),
4571 &mut cx,
4572 );
4573
4574 // Test that cursors that expand to the same region are collapsed.
4575 assert_rewrap(
4576 indoc! {"
4577 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4578 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4579 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4580 // ˇ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.
4581 "},
4582 indoc! {"
4583 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4584 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4585 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4586 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4587 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4588 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4589 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4590 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4591 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4592 // porttitor id. Aliquam id accumsan eros.
4593 "},
4594 language_with_c_comments.clone(),
4595 &mut cx,
4596 );
4597
4598 // Test that non-contiguous selections are treated separately.
4599 assert_rewrap(
4600 indoc! {"
4601 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4602 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4603 //
4604 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4605 // ˇ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.
4606 "},
4607 indoc! {"
4608 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4609 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4610 // auctor, eu lacinia sapien scelerisque.
4611 //
4612 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4613 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4614 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4615 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4616 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4617 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4618 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4619 "},
4620 language_with_c_comments.clone(),
4621 &mut cx,
4622 );
4623
4624 // Test that different comment prefixes are supported.
4625 assert_rewrap(
4626 indoc! {"
4627 # ˇ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.
4628 "},
4629 indoc! {"
4630 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4631 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4632 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4633 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4634 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4635 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4636 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4637 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4638 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4639 # accumsan eros.
4640 "},
4641 language_with_pound_comments.clone(),
4642 &mut cx,
4643 );
4644
4645 // Test that rewrapping is ignored outside of comments in most languages.
4646 assert_rewrap(
4647 indoc! {"
4648 /// Adds two numbers.
4649 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4650 fn add(a: u32, b: u32) -> u32 {
4651 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ˇ
4652 }
4653 "},
4654 indoc! {"
4655 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4656 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4657 fn add(a: u32, b: u32) -> u32 {
4658 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ˇ
4659 }
4660 "},
4661 language_with_doc_comments.clone(),
4662 &mut cx,
4663 );
4664
4665 // Test that rewrapping works in Markdown and Plain Text languages.
4666 assert_rewrap(
4667 indoc! {"
4668 # Hello
4669
4670 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.
4671 "},
4672 indoc! {"
4673 # Hello
4674
4675 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4676 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4677 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4678 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4679 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4680 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4681 Integer sit amet scelerisque nisi.
4682 "},
4683 markdown_language,
4684 &mut cx,
4685 );
4686
4687 assert_rewrap(
4688 indoc! {"
4689 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.
4690 "},
4691 indoc! {"
4692 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4693 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4694 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4695 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4696 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4697 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4698 Integer sit amet scelerisque nisi.
4699 "},
4700 plaintext_language,
4701 &mut cx,
4702 );
4703
4704 // Test rewrapping unaligned comments in a selection.
4705 assert_rewrap(
4706 indoc! {"
4707 fn foo() {
4708 if true {
4709 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4710 // Praesent semper egestas tellus id dignissim.ˇ»
4711 do_something();
4712 } else {
4713 //
4714 }
4715 }
4716 "},
4717 indoc! {"
4718 fn foo() {
4719 if true {
4720 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4721 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4722 // egestas tellus id dignissim.ˇ»
4723 do_something();
4724 } else {
4725 //
4726 }
4727 }
4728 "},
4729 language_with_doc_comments.clone(),
4730 &mut cx,
4731 );
4732
4733 assert_rewrap(
4734 indoc! {"
4735 fn foo() {
4736 if true {
4737 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4738 // Praesent semper egestas tellus id dignissim.»
4739 do_something();
4740 } else {
4741 //
4742 }
4743
4744 }
4745 "},
4746 indoc! {"
4747 fn foo() {
4748 if true {
4749 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4750 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4751 // egestas tellus id dignissim.»
4752 do_something();
4753 } else {
4754 //
4755 }
4756
4757 }
4758 "},
4759 language_with_doc_comments.clone(),
4760 &mut cx,
4761 );
4762
4763 #[track_caller]
4764 fn assert_rewrap(
4765 unwrapped_text: &str,
4766 wrapped_text: &str,
4767 language: Arc<Language>,
4768 cx: &mut EditorTestContext,
4769 ) {
4770 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4771 cx.set_state(unwrapped_text);
4772 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4773 cx.assert_editor_state(wrapped_text);
4774 }
4775}
4776
4777#[gpui::test]
4778async fn test_hard_wrap(cx: &mut TestAppContext) {
4779 init_test(cx, |_| {});
4780 let mut cx = EditorTestContext::new(cx).await;
4781
4782 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4783 cx.update_editor(|editor, _, cx| {
4784 editor.set_hard_wrap(Some(14), cx);
4785 });
4786
4787 cx.set_state(indoc!(
4788 "
4789 one two three ˇ
4790 "
4791 ));
4792 cx.simulate_input("four");
4793 cx.run_until_parked();
4794
4795 cx.assert_editor_state(indoc!(
4796 "
4797 one two three
4798 fourˇ
4799 "
4800 ));
4801
4802 cx.update_editor(|editor, window, cx| {
4803 editor.newline(&Default::default(), window, cx);
4804 });
4805 cx.run_until_parked();
4806 cx.assert_editor_state(indoc!(
4807 "
4808 one two three
4809 four
4810 ˇ
4811 "
4812 ));
4813
4814 cx.simulate_input("five");
4815 cx.run_until_parked();
4816 cx.assert_editor_state(indoc!(
4817 "
4818 one two three
4819 four
4820 fiveˇ
4821 "
4822 ));
4823
4824 cx.update_editor(|editor, window, cx| {
4825 editor.newline(&Default::default(), window, cx);
4826 });
4827 cx.run_until_parked();
4828 cx.simulate_input("# ");
4829 cx.run_until_parked();
4830 cx.assert_editor_state(indoc!(
4831 "
4832 one two three
4833 four
4834 five
4835 # ˇ
4836 "
4837 ));
4838
4839 cx.update_editor(|editor, window, cx| {
4840 editor.newline(&Default::default(), window, cx);
4841 });
4842 cx.run_until_parked();
4843 cx.assert_editor_state(indoc!(
4844 "
4845 one two three
4846 four
4847 five
4848 #\x20
4849 #ˇ
4850 "
4851 ));
4852
4853 cx.simulate_input(" 6");
4854 cx.run_until_parked();
4855 cx.assert_editor_state(indoc!(
4856 "
4857 one two three
4858 four
4859 five
4860 #
4861 # 6ˇ
4862 "
4863 ));
4864}
4865
4866#[gpui::test]
4867async fn test_clipboard(cx: &mut TestAppContext) {
4868 init_test(cx, |_| {});
4869
4870 let mut cx = EditorTestContext::new(cx).await;
4871
4872 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4873 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4874 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4875
4876 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4877 cx.set_state("two ˇfour ˇsix ˇ");
4878 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4879 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4880
4881 // Paste again but with only two cursors. Since the number of cursors doesn't
4882 // match the number of slices in the clipboard, the entire clipboard text
4883 // is pasted at each cursor.
4884 cx.set_state("ˇtwo one✅ four three six five ˇ");
4885 cx.update_editor(|e, window, cx| {
4886 e.handle_input("( ", window, cx);
4887 e.paste(&Paste, window, cx);
4888 e.handle_input(") ", window, cx);
4889 });
4890 cx.assert_editor_state(
4891 &([
4892 "( one✅ ",
4893 "three ",
4894 "five ) ˇtwo one✅ four three six five ( one✅ ",
4895 "three ",
4896 "five ) ˇ",
4897 ]
4898 .join("\n")),
4899 );
4900
4901 // Cut with three selections, one of which is full-line.
4902 cx.set_state(indoc! {"
4903 1«2ˇ»3
4904 4ˇ567
4905 «8ˇ»9"});
4906 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4907 cx.assert_editor_state(indoc! {"
4908 1ˇ3
4909 ˇ9"});
4910
4911 // Paste with three selections, noticing how the copied selection that was full-line
4912 // gets inserted before the second cursor.
4913 cx.set_state(indoc! {"
4914 1ˇ3
4915 9ˇ
4916 «oˇ»ne"});
4917 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4918 cx.assert_editor_state(indoc! {"
4919 12ˇ3
4920 4567
4921 9ˇ
4922 8ˇne"});
4923
4924 // Copy with a single cursor only, which writes the whole line into the clipboard.
4925 cx.set_state(indoc! {"
4926 The quick brown
4927 fox juˇmps over
4928 the lazy dog"});
4929 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4930 assert_eq!(
4931 cx.read_from_clipboard()
4932 .and_then(|item| item.text().as_deref().map(str::to_string)),
4933 Some("fox jumps over\n".to_string())
4934 );
4935
4936 // Paste with three selections, noticing how the copied full-line selection is inserted
4937 // before the empty selections but replaces the selection that is non-empty.
4938 cx.set_state(indoc! {"
4939 Tˇhe quick brown
4940 «foˇ»x jumps over
4941 tˇhe lazy dog"});
4942 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4943 cx.assert_editor_state(indoc! {"
4944 fox jumps over
4945 Tˇhe quick brown
4946 fox jumps over
4947 ˇx jumps over
4948 fox jumps over
4949 tˇhe lazy dog"});
4950}
4951
4952#[gpui::test]
4953async fn test_copy_trim(cx: &mut TestAppContext) {
4954 init_test(cx, |_| {});
4955
4956 let mut cx = EditorTestContext::new(cx).await;
4957 cx.set_state(
4958 r#" «for selection in selections.iter() {
4959 let mut start = selection.start;
4960 let mut end = selection.end;
4961 let is_entire_line = selection.is_empty();
4962 if is_entire_line {
4963 start = Point::new(start.row, 0);ˇ»
4964 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4965 }
4966 "#,
4967 );
4968 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4969 assert_eq!(
4970 cx.read_from_clipboard()
4971 .and_then(|item| item.text().as_deref().map(str::to_string)),
4972 Some(
4973 "for selection in selections.iter() {
4974 let mut start = selection.start;
4975 let mut end = selection.end;
4976 let is_entire_line = selection.is_empty();
4977 if is_entire_line {
4978 start = Point::new(start.row, 0);"
4979 .to_string()
4980 ),
4981 "Regular copying preserves all indentation selected",
4982 );
4983 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4984 assert_eq!(
4985 cx.read_from_clipboard()
4986 .and_then(|item| item.text().as_deref().map(str::to_string)),
4987 Some(
4988 "for selection in selections.iter() {
4989let mut start = selection.start;
4990let mut end = selection.end;
4991let is_entire_line = selection.is_empty();
4992if is_entire_line {
4993 start = Point::new(start.row, 0);"
4994 .to_string()
4995 ),
4996 "Copying with stripping should strip all leading whitespaces"
4997 );
4998
4999 cx.set_state(
5000 r#" « for selection in selections.iter() {
5001 let mut start = selection.start;
5002 let mut end = selection.end;
5003 let is_entire_line = selection.is_empty();
5004 if is_entire_line {
5005 start = Point::new(start.row, 0);ˇ»
5006 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5007 }
5008 "#,
5009 );
5010 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5011 assert_eq!(
5012 cx.read_from_clipboard()
5013 .and_then(|item| item.text().as_deref().map(str::to_string)),
5014 Some(
5015 " for selection in selections.iter() {
5016 let mut start = selection.start;
5017 let mut end = selection.end;
5018 let is_entire_line = selection.is_empty();
5019 if is_entire_line {
5020 start = Point::new(start.row, 0);"
5021 .to_string()
5022 ),
5023 "Regular copying preserves all indentation selected",
5024 );
5025 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5026 assert_eq!(
5027 cx.read_from_clipboard()
5028 .and_then(|item| item.text().as_deref().map(str::to_string)),
5029 Some(
5030 "for selection in selections.iter() {
5031let mut start = selection.start;
5032let mut end = selection.end;
5033let is_entire_line = selection.is_empty();
5034if is_entire_line {
5035 start = Point::new(start.row, 0);"
5036 .to_string()
5037 ),
5038 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5039 );
5040
5041 cx.set_state(
5042 r#" «ˇ for selection in selections.iter() {
5043 let mut start = selection.start;
5044 let mut end = selection.end;
5045 let is_entire_line = selection.is_empty();
5046 if is_entire_line {
5047 start = Point::new(start.row, 0);»
5048 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5049 }
5050 "#,
5051 );
5052 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5053 assert_eq!(
5054 cx.read_from_clipboard()
5055 .and_then(|item| item.text().as_deref().map(str::to_string)),
5056 Some(
5057 " for selection in selections.iter() {
5058 let mut start = selection.start;
5059 let mut end = selection.end;
5060 let is_entire_line = selection.is_empty();
5061 if is_entire_line {
5062 start = Point::new(start.row, 0);"
5063 .to_string()
5064 ),
5065 "Regular copying for reverse selection works the same",
5066 );
5067 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5068 assert_eq!(
5069 cx.read_from_clipboard()
5070 .and_then(|item| item.text().as_deref().map(str::to_string)),
5071 Some(
5072 "for selection in selections.iter() {
5073let mut start = selection.start;
5074let mut end = selection.end;
5075let is_entire_line = selection.is_empty();
5076if is_entire_line {
5077 start = Point::new(start.row, 0);"
5078 .to_string()
5079 ),
5080 "Copying with stripping for reverse selection works the same"
5081 );
5082
5083 cx.set_state(
5084 r#" for selection «in selections.iter() {
5085 let mut start = selection.start;
5086 let mut end = selection.end;
5087 let is_entire_line = selection.is_empty();
5088 if is_entire_line {
5089 start = Point::new(start.row, 0);ˇ»
5090 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5091 }
5092 "#,
5093 );
5094 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5095 assert_eq!(
5096 cx.read_from_clipboard()
5097 .and_then(|item| item.text().as_deref().map(str::to_string)),
5098 Some(
5099 "in selections.iter() {
5100 let mut start = selection.start;
5101 let mut end = selection.end;
5102 let is_entire_line = selection.is_empty();
5103 if is_entire_line {
5104 start = Point::new(start.row, 0);"
5105 .to_string()
5106 ),
5107 "When selecting past the indent, the copying works as usual",
5108 );
5109 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5110 assert_eq!(
5111 cx.read_from_clipboard()
5112 .and_then(|item| item.text().as_deref().map(str::to_string)),
5113 Some(
5114 "in selections.iter() {
5115 let mut start = selection.start;
5116 let mut end = selection.end;
5117 let is_entire_line = selection.is_empty();
5118 if is_entire_line {
5119 start = Point::new(start.row, 0);"
5120 .to_string()
5121 ),
5122 "When selecting past the indent, nothing is trimmed"
5123 );
5124}
5125
5126#[gpui::test]
5127async fn test_paste_multiline(cx: &mut TestAppContext) {
5128 init_test(cx, |_| {});
5129
5130 let mut cx = EditorTestContext::new(cx).await;
5131 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5132
5133 // Cut an indented block, without the leading whitespace.
5134 cx.set_state(indoc! {"
5135 const a: B = (
5136 c(),
5137 «d(
5138 e,
5139 f
5140 )ˇ»
5141 );
5142 "});
5143 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5144 cx.assert_editor_state(indoc! {"
5145 const a: B = (
5146 c(),
5147 ˇ
5148 );
5149 "});
5150
5151 // Paste it at the same position.
5152 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5153 cx.assert_editor_state(indoc! {"
5154 const a: B = (
5155 c(),
5156 d(
5157 e,
5158 f
5159 )ˇ
5160 );
5161 "});
5162
5163 // Paste it at a line with a lower indent level.
5164 cx.set_state(indoc! {"
5165 ˇ
5166 const a: B = (
5167 c(),
5168 );
5169 "});
5170 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5171 cx.assert_editor_state(indoc! {"
5172 d(
5173 e,
5174 f
5175 )ˇ
5176 const a: B = (
5177 c(),
5178 );
5179 "});
5180
5181 // Cut an indented block, with the leading whitespace.
5182 cx.set_state(indoc! {"
5183 const a: B = (
5184 c(),
5185 « d(
5186 e,
5187 f
5188 )
5189 ˇ»);
5190 "});
5191 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5192 cx.assert_editor_state(indoc! {"
5193 const a: B = (
5194 c(),
5195 ˇ);
5196 "});
5197
5198 // Paste it at the same position.
5199 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5200 cx.assert_editor_state(indoc! {"
5201 const a: B = (
5202 c(),
5203 d(
5204 e,
5205 f
5206 )
5207 ˇ);
5208 "});
5209
5210 // Paste it at a line with a higher indent level.
5211 cx.set_state(indoc! {"
5212 const a: B = (
5213 c(),
5214 d(
5215 e,
5216 fˇ
5217 )
5218 );
5219 "});
5220 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5221 cx.assert_editor_state(indoc! {"
5222 const a: B = (
5223 c(),
5224 d(
5225 e,
5226 f d(
5227 e,
5228 f
5229 )
5230 ˇ
5231 )
5232 );
5233 "});
5234
5235 // Copy an indented block, starting mid-line
5236 cx.set_state(indoc! {"
5237 const a: B = (
5238 c(),
5239 somethin«g(
5240 e,
5241 f
5242 )ˇ»
5243 );
5244 "});
5245 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5246
5247 // Paste it on a line with a lower indent level
5248 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5249 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5250 cx.assert_editor_state(indoc! {"
5251 const a: B = (
5252 c(),
5253 something(
5254 e,
5255 f
5256 )
5257 );
5258 g(
5259 e,
5260 f
5261 )ˇ"});
5262}
5263
5264#[gpui::test]
5265async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5266 init_test(cx, |_| {});
5267
5268 cx.write_to_clipboard(ClipboardItem::new_string(
5269 " d(\n e\n );\n".into(),
5270 ));
5271
5272 let mut cx = EditorTestContext::new(cx).await;
5273 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5274
5275 cx.set_state(indoc! {"
5276 fn a() {
5277 b();
5278 if c() {
5279 ˇ
5280 }
5281 }
5282 "});
5283
5284 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5285 cx.assert_editor_state(indoc! {"
5286 fn a() {
5287 b();
5288 if c() {
5289 d(
5290 e
5291 );
5292 ˇ
5293 }
5294 }
5295 "});
5296
5297 cx.set_state(indoc! {"
5298 fn a() {
5299 b();
5300 ˇ
5301 }
5302 "});
5303
5304 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5305 cx.assert_editor_state(indoc! {"
5306 fn a() {
5307 b();
5308 d(
5309 e
5310 );
5311 ˇ
5312 }
5313 "});
5314}
5315
5316#[gpui::test]
5317fn test_select_all(cx: &mut TestAppContext) {
5318 init_test(cx, |_| {});
5319
5320 let editor = cx.add_window(|window, cx| {
5321 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5322 build_editor(buffer, window, cx)
5323 });
5324 _ = editor.update(cx, |editor, window, cx| {
5325 editor.select_all(&SelectAll, window, cx);
5326 assert_eq!(
5327 editor.selections.display_ranges(cx),
5328 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5329 );
5330 });
5331}
5332
5333#[gpui::test]
5334fn test_select_line(cx: &mut TestAppContext) {
5335 init_test(cx, |_| {});
5336
5337 let editor = cx.add_window(|window, cx| {
5338 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5339 build_editor(buffer, window, cx)
5340 });
5341 _ = editor.update(cx, |editor, window, cx| {
5342 editor.change_selections(None, window, cx, |s| {
5343 s.select_display_ranges([
5344 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5345 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5346 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5347 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5348 ])
5349 });
5350 editor.select_line(&SelectLine, window, cx);
5351 assert_eq!(
5352 editor.selections.display_ranges(cx),
5353 vec![
5354 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5355 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5356 ]
5357 );
5358 });
5359
5360 _ = editor.update(cx, |editor, window, cx| {
5361 editor.select_line(&SelectLine, window, cx);
5362 assert_eq!(
5363 editor.selections.display_ranges(cx),
5364 vec![
5365 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5366 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5367 ]
5368 );
5369 });
5370
5371 _ = editor.update(cx, |editor, window, cx| {
5372 editor.select_line(&SelectLine, window, cx);
5373 assert_eq!(
5374 editor.selections.display_ranges(cx),
5375 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5376 );
5377 });
5378}
5379
5380#[gpui::test]
5381async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5382 init_test(cx, |_| {});
5383 let mut cx = EditorTestContext::new(cx).await;
5384
5385 #[track_caller]
5386 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5387 cx.set_state(initial_state);
5388 cx.update_editor(|e, window, cx| {
5389 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5390 });
5391 cx.assert_editor_state(expected_state);
5392 }
5393
5394 // Selection starts and ends at the middle of lines, left-to-right
5395 test(
5396 &mut cx,
5397 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5398 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5399 );
5400 // Same thing, right-to-left
5401 test(
5402 &mut cx,
5403 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5404 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5405 );
5406
5407 // Whole buffer, left-to-right, last line *doesn't* end with newline
5408 test(
5409 &mut cx,
5410 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5411 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5412 );
5413 // Same thing, right-to-left
5414 test(
5415 &mut cx,
5416 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5417 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5418 );
5419
5420 // Whole buffer, left-to-right, last line ends with newline
5421 test(
5422 &mut cx,
5423 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5424 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5425 );
5426 // Same thing, right-to-left
5427 test(
5428 &mut cx,
5429 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5430 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5431 );
5432
5433 // Starts at the end of a line, ends at the start of another
5434 test(
5435 &mut cx,
5436 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5437 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5438 );
5439}
5440
5441#[gpui::test]
5442async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5443 init_test(cx, |_| {});
5444
5445 let editor = cx.add_window(|window, cx| {
5446 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5447 build_editor(buffer, window, cx)
5448 });
5449
5450 // setup
5451 _ = editor.update(cx, |editor, window, cx| {
5452 editor.fold_creases(
5453 vec![
5454 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5455 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5456 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5457 ],
5458 true,
5459 window,
5460 cx,
5461 );
5462 assert_eq!(
5463 editor.display_text(cx),
5464 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5465 );
5466 });
5467
5468 _ = editor.update(cx, |editor, window, cx| {
5469 editor.change_selections(None, window, cx, |s| {
5470 s.select_display_ranges([
5471 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5472 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5473 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5474 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5475 ])
5476 });
5477 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5478 assert_eq!(
5479 editor.display_text(cx),
5480 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5481 );
5482 });
5483 EditorTestContext::for_editor(editor, cx)
5484 .await
5485 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5486
5487 _ = editor.update(cx, |editor, window, cx| {
5488 editor.change_selections(None, window, cx, |s| {
5489 s.select_display_ranges([
5490 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5491 ])
5492 });
5493 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5494 assert_eq!(
5495 editor.display_text(cx),
5496 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5497 );
5498 assert_eq!(
5499 editor.selections.display_ranges(cx),
5500 [
5501 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5502 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5503 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5504 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5505 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5506 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5507 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5508 ]
5509 );
5510 });
5511 EditorTestContext::for_editor(editor, cx)
5512 .await
5513 .assert_editor_state(
5514 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5515 );
5516}
5517
5518#[gpui::test]
5519async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5520 init_test(cx, |_| {});
5521
5522 let mut cx = EditorTestContext::new(cx).await;
5523
5524 cx.set_state(indoc!(
5525 r#"abc
5526 defˇghi
5527
5528 jk
5529 nlmo
5530 "#
5531 ));
5532
5533 cx.update_editor(|editor, window, cx| {
5534 editor.add_selection_above(&Default::default(), window, cx);
5535 });
5536
5537 cx.assert_editor_state(indoc!(
5538 r#"abcˇ
5539 defˇghi
5540
5541 jk
5542 nlmo
5543 "#
5544 ));
5545
5546 cx.update_editor(|editor, window, cx| {
5547 editor.add_selection_above(&Default::default(), window, cx);
5548 });
5549
5550 cx.assert_editor_state(indoc!(
5551 r#"abcˇ
5552 defˇghi
5553
5554 jk
5555 nlmo
5556 "#
5557 ));
5558
5559 cx.update_editor(|editor, window, cx| {
5560 editor.add_selection_below(&Default::default(), window, cx);
5561 });
5562
5563 cx.assert_editor_state(indoc!(
5564 r#"abc
5565 defˇghi
5566
5567 jk
5568 nlmo
5569 "#
5570 ));
5571
5572 cx.update_editor(|editor, window, cx| {
5573 editor.undo_selection(&Default::default(), window, cx);
5574 });
5575
5576 cx.assert_editor_state(indoc!(
5577 r#"abcˇ
5578 defˇghi
5579
5580 jk
5581 nlmo
5582 "#
5583 ));
5584
5585 cx.update_editor(|editor, window, cx| {
5586 editor.redo_selection(&Default::default(), window, cx);
5587 });
5588
5589 cx.assert_editor_state(indoc!(
5590 r#"abc
5591 defˇghi
5592
5593 jk
5594 nlmo
5595 "#
5596 ));
5597
5598 cx.update_editor(|editor, window, cx| {
5599 editor.add_selection_below(&Default::default(), window, cx);
5600 });
5601
5602 cx.assert_editor_state(indoc!(
5603 r#"abc
5604 defˇghi
5605
5606 jk
5607 nlmˇo
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ˇghi
5618
5619 jk
5620 nlmˇo
5621 "#
5622 ));
5623
5624 // change selections
5625 cx.set_state(indoc!(
5626 r#"abc
5627 def«ˇg»hi
5628
5629 jk
5630 nlmo
5631 "#
5632 ));
5633
5634 cx.update_editor(|editor, window, cx| {
5635 editor.add_selection_below(&Default::default(), window, cx);
5636 });
5637
5638 cx.assert_editor_state(indoc!(
5639 r#"abc
5640 def«ˇg»hi
5641
5642 jk
5643 nlm«ˇo»
5644 "#
5645 ));
5646
5647 cx.update_editor(|editor, window, cx| {
5648 editor.add_selection_below(&Default::default(), window, cx);
5649 });
5650
5651 cx.assert_editor_state(indoc!(
5652 r#"abc
5653 def«ˇg»hi
5654
5655 jk
5656 nlm«ˇo»
5657 "#
5658 ));
5659
5660 cx.update_editor(|editor, window, cx| {
5661 editor.add_selection_above(&Default::default(), window, cx);
5662 });
5663
5664 cx.assert_editor_state(indoc!(
5665 r#"abc
5666 def«ˇg»hi
5667
5668 jk
5669 nlmo
5670 "#
5671 ));
5672
5673 cx.update_editor(|editor, window, cx| {
5674 editor.add_selection_above(&Default::default(), window, cx);
5675 });
5676
5677 cx.assert_editor_state(indoc!(
5678 r#"abc
5679 def«ˇg»hi
5680
5681 jk
5682 nlmo
5683 "#
5684 ));
5685
5686 // Change selections again
5687 cx.set_state(indoc!(
5688 r#"a«bc
5689 defgˇ»hi
5690
5691 jk
5692 nlmo
5693 "#
5694 ));
5695
5696 cx.update_editor(|editor, window, cx| {
5697 editor.add_selection_below(&Default::default(), window, cx);
5698 });
5699
5700 cx.assert_editor_state(indoc!(
5701 r#"a«bcˇ»
5702 d«efgˇ»hi
5703
5704 j«kˇ»
5705 nlmo
5706 "#
5707 ));
5708
5709 cx.update_editor(|editor, window, cx| {
5710 editor.add_selection_below(&Default::default(), window, cx);
5711 });
5712 cx.assert_editor_state(indoc!(
5713 r#"a«bcˇ»
5714 d«efgˇ»hi
5715
5716 j«kˇ»
5717 n«lmoˇ»
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«efgˇ»hi
5727
5728 j«kˇ»
5729 nlmo
5730 "#
5731 ));
5732
5733 // Change selections again
5734 cx.set_state(indoc!(
5735 r#"abc
5736 d«ˇefghi
5737
5738 jk
5739 nlm»o
5740 "#
5741 ));
5742
5743 cx.update_editor(|editor, window, cx| {
5744 editor.add_selection_above(&Default::default(), window, cx);
5745 });
5746
5747 cx.assert_editor_state(indoc!(
5748 r#"a«ˇbc»
5749 d«ˇef»ghi
5750
5751 j«ˇk»
5752 n«ˇlm»o
5753 "#
5754 ));
5755
5756 cx.update_editor(|editor, window, cx| {
5757 editor.add_selection_below(&Default::default(), window, cx);
5758 });
5759
5760 cx.assert_editor_state(indoc!(
5761 r#"abc
5762 d«ˇef»ghi
5763
5764 j«ˇk»
5765 n«ˇlm»o
5766 "#
5767 ));
5768}
5769
5770#[gpui::test]
5771async fn test_select_next(cx: &mut TestAppContext) {
5772 init_test(cx, |_| {});
5773
5774 let mut cx = EditorTestContext::new(cx).await;
5775 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5776
5777 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5778 .unwrap();
5779 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5780
5781 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5782 .unwrap();
5783 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5784
5785 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5786 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5787
5788 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5789 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5790
5791 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5792 .unwrap();
5793 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5794
5795 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5796 .unwrap();
5797 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5798}
5799
5800#[gpui::test]
5801async fn test_select_all_matches(cx: &mut TestAppContext) {
5802 init_test(cx, |_| {});
5803
5804 let mut cx = EditorTestContext::new(cx).await;
5805
5806 // Test caret-only selections
5807 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5808 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5809 .unwrap();
5810 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5811
5812 // Test left-to-right selections
5813 cx.set_state("abc\n«abcˇ»\nabc");
5814 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5815 .unwrap();
5816 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5817
5818 // Test right-to-left selections
5819 cx.set_state("abc\n«ˇabc»\nabc");
5820 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5821 .unwrap();
5822 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5823
5824 // Test selecting whitespace with caret selection
5825 cx.set_state("abc\nˇ abc\nabc");
5826 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5827 .unwrap();
5828 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5829
5830 // Test selecting whitespace with left-to-right selection
5831 cx.set_state("abc\n«ˇ »abc\nabc");
5832 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5833 .unwrap();
5834 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5835
5836 // Test no matches with right-to-left selection
5837 cx.set_state("abc\n« ˇ»abc\nabc");
5838 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5839 .unwrap();
5840 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5841}
5842
5843#[gpui::test]
5844async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
5845 init_test(cx, |_| {});
5846
5847 let mut cx = EditorTestContext::new(cx).await;
5848
5849 let large_body_1 = "\nd".repeat(200);
5850 let large_body_2 = "\ne".repeat(200);
5851
5852 cx.set_state(&format!(
5853 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
5854 ));
5855 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
5856 let scroll_position = editor.scroll_position(cx);
5857 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
5858 scroll_position
5859 });
5860
5861 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5862 .unwrap();
5863 cx.assert_editor_state(&format!(
5864 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
5865 ));
5866 let scroll_position_after_selection =
5867 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
5868 assert_eq!(
5869 initial_scroll_position, scroll_position_after_selection,
5870 "Scroll position should not change after selecting all matches"
5871 );
5872}
5873
5874#[gpui::test]
5875async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
5876 init_test(cx, |_| {});
5877
5878 let mut cx = EditorLspTestContext::new_rust(
5879 lsp::ServerCapabilities {
5880 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5881 ..Default::default()
5882 },
5883 cx,
5884 )
5885 .await;
5886
5887 cx.set_state(indoc! {"
5888 line 1
5889 line 2
5890 linˇe 3
5891 line 4
5892 line 5
5893 "});
5894
5895 // Make an edit
5896 cx.update_editor(|editor, window, cx| {
5897 editor.handle_input("X", window, cx);
5898 });
5899
5900 // Move cursor to a different position
5901 cx.update_editor(|editor, window, cx| {
5902 editor.change_selections(None, window, cx, |s| {
5903 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
5904 });
5905 });
5906
5907 cx.assert_editor_state(indoc! {"
5908 line 1
5909 line 2
5910 linXe 3
5911 line 4
5912 liˇne 5
5913 "});
5914
5915 cx.lsp
5916 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
5917 Ok(Some(vec![lsp::TextEdit::new(
5918 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
5919 "PREFIX ".to_string(),
5920 )]))
5921 });
5922
5923 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
5924 .unwrap()
5925 .await
5926 .unwrap();
5927
5928 cx.assert_editor_state(indoc! {"
5929 PREFIX line 1
5930 line 2
5931 linXe 3
5932 line 4
5933 liˇne 5
5934 "});
5935
5936 // Undo formatting
5937 cx.update_editor(|editor, window, cx| {
5938 editor.undo(&Default::default(), window, cx);
5939 });
5940
5941 // Verify cursor moved back to position after edit
5942 cx.assert_editor_state(indoc! {"
5943 line 1
5944 line 2
5945 linXˇe 3
5946 line 4
5947 line 5
5948 "});
5949}
5950
5951#[gpui::test]
5952async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5953 init_test(cx, |_| {});
5954
5955 let mut cx = EditorTestContext::new(cx).await;
5956 cx.set_state(
5957 r#"let foo = 2;
5958lˇet foo = 2;
5959let fooˇ = 2;
5960let foo = 2;
5961let foo = ˇ2;"#,
5962 );
5963
5964 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5965 .unwrap();
5966 cx.assert_editor_state(
5967 r#"let foo = 2;
5968«letˇ» foo = 2;
5969let «fooˇ» = 2;
5970let foo = 2;
5971let foo = «2ˇ»;"#,
5972 );
5973
5974 // noop for multiple selections with different contents
5975 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5976 .unwrap();
5977 cx.assert_editor_state(
5978 r#"let foo = 2;
5979«letˇ» foo = 2;
5980let «fooˇ» = 2;
5981let foo = 2;
5982let foo = «2ˇ»;"#,
5983 );
5984}
5985
5986#[gpui::test]
5987async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5988 init_test(cx, |_| {});
5989
5990 let mut cx =
5991 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5992
5993 cx.assert_editor_state(indoc! {"
5994 ˇbbb
5995 ccc
5996
5997 bbb
5998 ccc
5999 "});
6000 cx.dispatch_action(SelectPrevious::default());
6001 cx.assert_editor_state(indoc! {"
6002 «bbbˇ»
6003 ccc
6004
6005 bbb
6006 ccc
6007 "});
6008 cx.dispatch_action(SelectPrevious::default());
6009 cx.assert_editor_state(indoc! {"
6010 «bbbˇ»
6011 ccc
6012
6013 «bbbˇ»
6014 ccc
6015 "});
6016}
6017
6018#[gpui::test]
6019async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6020 init_test(cx, |_| {});
6021
6022 let mut cx = EditorTestContext::new(cx).await;
6023 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6024
6025 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6026 .unwrap();
6027 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6028
6029 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6030 .unwrap();
6031 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6032
6033 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6034 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6035
6036 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6037 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6038
6039 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6040 .unwrap();
6041 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6042
6043 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6044 .unwrap();
6045 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
6046
6047 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6048 .unwrap();
6049 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6050}
6051
6052#[gpui::test]
6053async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6054 init_test(cx, |_| {});
6055
6056 let mut cx = EditorTestContext::new(cx).await;
6057 cx.set_state("aˇ");
6058
6059 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6060 .unwrap();
6061 cx.assert_editor_state("«aˇ»");
6062 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6063 .unwrap();
6064 cx.assert_editor_state("«aˇ»");
6065}
6066
6067#[gpui::test]
6068async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6069 init_test(cx, |_| {});
6070
6071 let mut cx = EditorTestContext::new(cx).await;
6072 cx.set_state(
6073 r#"let foo = 2;
6074lˇet foo = 2;
6075let fooˇ = 2;
6076let foo = 2;
6077let foo = ˇ2;"#,
6078 );
6079
6080 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6081 .unwrap();
6082 cx.assert_editor_state(
6083 r#"let foo = 2;
6084«letˇ» foo = 2;
6085let «fooˇ» = 2;
6086let foo = 2;
6087let foo = «2ˇ»;"#,
6088 );
6089
6090 // noop for multiple selections with different contents
6091 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6092 .unwrap();
6093 cx.assert_editor_state(
6094 r#"let foo = 2;
6095«letˇ» foo = 2;
6096let «fooˇ» = 2;
6097let foo = 2;
6098let foo = «2ˇ»;"#,
6099 );
6100}
6101
6102#[gpui::test]
6103async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6104 init_test(cx, |_| {});
6105
6106 let mut cx = EditorTestContext::new(cx).await;
6107 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6108
6109 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6110 .unwrap();
6111 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6112
6113 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6114 .unwrap();
6115 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6116
6117 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6118 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6119
6120 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6121 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6122
6123 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6124 .unwrap();
6125 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
6126
6127 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6128 .unwrap();
6129 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6130}
6131
6132#[gpui::test]
6133async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6134 init_test(cx, |_| {});
6135
6136 let language = Arc::new(Language::new(
6137 LanguageConfig::default(),
6138 Some(tree_sitter_rust::LANGUAGE.into()),
6139 ));
6140
6141 let text = r#"
6142 use mod1::mod2::{mod3, mod4};
6143
6144 fn fn_1(param1: bool, param2: &str) {
6145 let var1 = "text";
6146 }
6147 "#
6148 .unindent();
6149
6150 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6151 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6152 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6153
6154 editor
6155 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6156 .await;
6157
6158 editor.update_in(cx, |editor, window, cx| {
6159 editor.change_selections(None, window, cx, |s| {
6160 s.select_display_ranges([
6161 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6162 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6163 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6164 ]);
6165 });
6166 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6167 });
6168 editor.update(cx, |editor, cx| {
6169 assert_text_with_selections(
6170 editor,
6171 indoc! {r#"
6172 use mod1::mod2::{mod3, «mod4ˇ»};
6173
6174 fn fn_1«ˇ(param1: bool, param2: &str)» {
6175 let var1 = "«ˇtext»";
6176 }
6177 "#},
6178 cx,
6179 );
6180 });
6181
6182 editor.update_in(cx, |editor, window, cx| {
6183 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6184 });
6185 editor.update(cx, |editor, cx| {
6186 assert_text_with_selections(
6187 editor,
6188 indoc! {r#"
6189 use mod1::mod2::«{mod3, mod4}ˇ»;
6190
6191 «ˇfn fn_1(param1: bool, param2: &str) {
6192 let var1 = "text";
6193 }»
6194 "#},
6195 cx,
6196 );
6197 });
6198
6199 editor.update_in(cx, |editor, window, cx| {
6200 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6201 });
6202 assert_eq!(
6203 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6204 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6205 );
6206
6207 // Trying to expand the selected syntax node one more time has no effect.
6208 editor.update_in(cx, |editor, window, cx| {
6209 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6210 });
6211 assert_eq!(
6212 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6213 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6214 );
6215
6216 editor.update_in(cx, |editor, window, cx| {
6217 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6218 });
6219 editor.update(cx, |editor, cx| {
6220 assert_text_with_selections(
6221 editor,
6222 indoc! {r#"
6223 use mod1::mod2::«{mod3, mod4}ˇ»;
6224
6225 «ˇfn fn_1(param1: bool, param2: &str) {
6226 let var1 = "text";
6227 }»
6228 "#},
6229 cx,
6230 );
6231 });
6232
6233 editor.update_in(cx, |editor, window, cx| {
6234 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6235 });
6236 editor.update(cx, |editor, cx| {
6237 assert_text_with_selections(
6238 editor,
6239 indoc! {r#"
6240 use mod1::mod2::{mod3, «mod4ˇ»};
6241
6242 fn fn_1«ˇ(param1: bool, param2: &str)» {
6243 let var1 = "«ˇtext»";
6244 }
6245 "#},
6246 cx,
6247 );
6248 });
6249
6250 editor.update_in(cx, |editor, window, cx| {
6251 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6252 });
6253 editor.update(cx, |editor, cx| {
6254 assert_text_with_selections(
6255 editor,
6256 indoc! {r#"
6257 use mod1::mod2::{mod3, mo«ˇ»d4};
6258
6259 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6260 let var1 = "te«ˇ»xt";
6261 }
6262 "#},
6263 cx,
6264 );
6265 });
6266
6267 // Trying to shrink the selected syntax node one more time has no effect.
6268 editor.update_in(cx, |editor, window, cx| {
6269 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6270 });
6271 editor.update_in(cx, |editor, _, cx| {
6272 assert_text_with_selections(
6273 editor,
6274 indoc! {r#"
6275 use mod1::mod2::{mod3, mo«ˇ»d4};
6276
6277 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6278 let var1 = "te«ˇ»xt";
6279 }
6280 "#},
6281 cx,
6282 );
6283 });
6284
6285 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6286 // a fold.
6287 editor.update_in(cx, |editor, window, cx| {
6288 editor.fold_creases(
6289 vec![
6290 Crease::simple(
6291 Point::new(0, 21)..Point::new(0, 24),
6292 FoldPlaceholder::test(),
6293 ),
6294 Crease::simple(
6295 Point::new(3, 20)..Point::new(3, 22),
6296 FoldPlaceholder::test(),
6297 ),
6298 ],
6299 true,
6300 window,
6301 cx,
6302 );
6303 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6304 });
6305 editor.update(cx, |editor, cx| {
6306 assert_text_with_selections(
6307 editor,
6308 indoc! {r#"
6309 use mod1::mod2::«{mod3, mod4}ˇ»;
6310
6311 fn fn_1«ˇ(param1: bool, param2: &str)» {
6312 «ˇlet var1 = "text";»
6313 }
6314 "#},
6315 cx,
6316 );
6317 });
6318}
6319
6320#[gpui::test]
6321async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6322 init_test(cx, |_| {});
6323
6324 let base_text = r#"
6325 impl A {
6326 // this is an uncommitted comment
6327
6328 fn b() {
6329 c();
6330 }
6331
6332 // this is another uncommitted comment
6333
6334 fn d() {
6335 // e
6336 // f
6337 }
6338 }
6339
6340 fn g() {
6341 // h
6342 }
6343 "#
6344 .unindent();
6345
6346 let text = r#"
6347 ˇimpl A {
6348
6349 fn b() {
6350 c();
6351 }
6352
6353 fn d() {
6354 // e
6355 // f
6356 }
6357 }
6358
6359 fn g() {
6360 // h
6361 }
6362 "#
6363 .unindent();
6364
6365 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6366 cx.set_state(&text);
6367 cx.set_head_text(&base_text);
6368 cx.update_editor(|editor, window, cx| {
6369 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6370 });
6371
6372 cx.assert_state_with_diff(
6373 "
6374 ˇimpl A {
6375 - // this is an uncommitted comment
6376
6377 fn b() {
6378 c();
6379 }
6380
6381 - // this is another uncommitted comment
6382 -
6383 fn d() {
6384 // e
6385 // f
6386 }
6387 }
6388
6389 fn g() {
6390 // h
6391 }
6392 "
6393 .unindent(),
6394 );
6395
6396 let expected_display_text = "
6397 impl A {
6398 // this is an uncommitted comment
6399
6400 fn b() {
6401 ⋯
6402 }
6403
6404 // this is another uncommitted comment
6405
6406 fn d() {
6407 ⋯
6408 }
6409 }
6410
6411 fn g() {
6412 ⋯
6413 }
6414 "
6415 .unindent();
6416
6417 cx.update_editor(|editor, window, cx| {
6418 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6419 assert_eq!(editor.display_text(cx), expected_display_text);
6420 });
6421}
6422
6423#[gpui::test]
6424async fn test_autoindent(cx: &mut TestAppContext) {
6425 init_test(cx, |_| {});
6426
6427 let language = Arc::new(
6428 Language::new(
6429 LanguageConfig {
6430 brackets: BracketPairConfig {
6431 pairs: vec![
6432 BracketPair {
6433 start: "{".to_string(),
6434 end: "}".to_string(),
6435 close: false,
6436 surround: false,
6437 newline: true,
6438 },
6439 BracketPair {
6440 start: "(".to_string(),
6441 end: ")".to_string(),
6442 close: false,
6443 surround: false,
6444 newline: true,
6445 },
6446 ],
6447 ..Default::default()
6448 },
6449 ..Default::default()
6450 },
6451 Some(tree_sitter_rust::LANGUAGE.into()),
6452 )
6453 .with_indents_query(
6454 r#"
6455 (_ "(" ")" @end) @indent
6456 (_ "{" "}" @end) @indent
6457 "#,
6458 )
6459 .unwrap(),
6460 );
6461
6462 let text = "fn a() {}";
6463
6464 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6465 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6466 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6467 editor
6468 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6469 .await;
6470
6471 editor.update_in(cx, |editor, window, cx| {
6472 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6473 editor.newline(&Newline, window, cx);
6474 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6475 assert_eq!(
6476 editor.selections.ranges(cx),
6477 &[
6478 Point::new(1, 4)..Point::new(1, 4),
6479 Point::new(3, 4)..Point::new(3, 4),
6480 Point::new(5, 0)..Point::new(5, 0)
6481 ]
6482 );
6483 });
6484}
6485
6486#[gpui::test]
6487async fn test_autoindent_selections(cx: &mut TestAppContext) {
6488 init_test(cx, |_| {});
6489
6490 {
6491 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6492 cx.set_state(indoc! {"
6493 impl A {
6494
6495 fn b() {}
6496
6497 «fn c() {
6498
6499 }ˇ»
6500 }
6501 "});
6502
6503 cx.update_editor(|editor, window, cx| {
6504 editor.autoindent(&Default::default(), window, cx);
6505 });
6506
6507 cx.assert_editor_state(indoc! {"
6508 impl A {
6509
6510 fn b() {}
6511
6512 «fn c() {
6513
6514 }ˇ»
6515 }
6516 "});
6517 }
6518
6519 {
6520 let mut cx = EditorTestContext::new_multibuffer(
6521 cx,
6522 [indoc! { "
6523 impl A {
6524 «
6525 // a
6526 fn b(){}
6527 »
6528 «
6529 }
6530 fn c(){}
6531 »
6532 "}],
6533 );
6534
6535 let buffer = cx.update_editor(|editor, _, cx| {
6536 let buffer = editor.buffer().update(cx, |buffer, _| {
6537 buffer.all_buffers().iter().next().unwrap().clone()
6538 });
6539 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6540 buffer
6541 });
6542
6543 cx.run_until_parked();
6544 cx.update_editor(|editor, window, cx| {
6545 editor.select_all(&Default::default(), window, cx);
6546 editor.autoindent(&Default::default(), window, cx)
6547 });
6548 cx.run_until_parked();
6549
6550 cx.update(|_, cx| {
6551 assert_eq!(
6552 buffer.read(cx).text(),
6553 indoc! { "
6554 impl A {
6555
6556 // a
6557 fn b(){}
6558
6559
6560 }
6561 fn c(){}
6562
6563 " }
6564 )
6565 });
6566 }
6567}
6568
6569#[gpui::test]
6570async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6571 init_test(cx, |_| {});
6572
6573 let mut cx = EditorTestContext::new(cx).await;
6574
6575 let language = Arc::new(Language::new(
6576 LanguageConfig {
6577 brackets: BracketPairConfig {
6578 pairs: vec![
6579 BracketPair {
6580 start: "{".to_string(),
6581 end: "}".to_string(),
6582 close: true,
6583 surround: true,
6584 newline: true,
6585 },
6586 BracketPair {
6587 start: "(".to_string(),
6588 end: ")".to_string(),
6589 close: true,
6590 surround: true,
6591 newline: true,
6592 },
6593 BracketPair {
6594 start: "/*".to_string(),
6595 end: " */".to_string(),
6596 close: true,
6597 surround: true,
6598 newline: true,
6599 },
6600 BracketPair {
6601 start: "[".to_string(),
6602 end: "]".to_string(),
6603 close: false,
6604 surround: false,
6605 newline: true,
6606 },
6607 BracketPair {
6608 start: "\"".to_string(),
6609 end: "\"".to_string(),
6610 close: true,
6611 surround: true,
6612 newline: false,
6613 },
6614 BracketPair {
6615 start: "<".to_string(),
6616 end: ">".to_string(),
6617 close: false,
6618 surround: true,
6619 newline: true,
6620 },
6621 ],
6622 ..Default::default()
6623 },
6624 autoclose_before: "})]".to_string(),
6625 ..Default::default()
6626 },
6627 Some(tree_sitter_rust::LANGUAGE.into()),
6628 ));
6629
6630 cx.language_registry().add(language.clone());
6631 cx.update_buffer(|buffer, cx| {
6632 buffer.set_language(Some(language), cx);
6633 });
6634
6635 cx.set_state(
6636 &r#"
6637 🏀ˇ
6638 εˇ
6639 ❤️ˇ
6640 "#
6641 .unindent(),
6642 );
6643
6644 // autoclose multiple nested brackets at multiple cursors
6645 cx.update_editor(|editor, window, cx| {
6646 editor.handle_input("{", window, cx);
6647 editor.handle_input("{", window, cx);
6648 editor.handle_input("{", window, cx);
6649 });
6650 cx.assert_editor_state(
6651 &"
6652 🏀{{{ˇ}}}
6653 ε{{{ˇ}}}
6654 ❤️{{{ˇ}}}
6655 "
6656 .unindent(),
6657 );
6658
6659 // insert a different closing bracket
6660 cx.update_editor(|editor, window, cx| {
6661 editor.handle_input(")", window, cx);
6662 });
6663 cx.assert_editor_state(
6664 &"
6665 🏀{{{)ˇ}}}
6666 ε{{{)ˇ}}}
6667 ❤️{{{)ˇ}}}
6668 "
6669 .unindent(),
6670 );
6671
6672 // skip over the auto-closed brackets when typing a closing bracket
6673 cx.update_editor(|editor, window, cx| {
6674 editor.move_right(&MoveRight, window, cx);
6675 editor.handle_input("}", window, cx);
6676 editor.handle_input("}", window, cx);
6677 editor.handle_input("}", window, cx);
6678 });
6679 cx.assert_editor_state(
6680 &"
6681 🏀{{{)}}}}ˇ
6682 ε{{{)}}}}ˇ
6683 ❤️{{{)}}}}ˇ
6684 "
6685 .unindent(),
6686 );
6687
6688 // autoclose multi-character pairs
6689 cx.set_state(
6690 &"
6691 ˇ
6692 ˇ
6693 "
6694 .unindent(),
6695 );
6696 cx.update_editor(|editor, window, cx| {
6697 editor.handle_input("/", window, cx);
6698 editor.handle_input("*", window, cx);
6699 });
6700 cx.assert_editor_state(
6701 &"
6702 /*ˇ */
6703 /*ˇ */
6704 "
6705 .unindent(),
6706 );
6707
6708 // one cursor autocloses a multi-character pair, one cursor
6709 // does not autoclose.
6710 cx.set_state(
6711 &"
6712 /ˇ
6713 ˇ
6714 "
6715 .unindent(),
6716 );
6717 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6718 cx.assert_editor_state(
6719 &"
6720 /*ˇ */
6721 *ˇ
6722 "
6723 .unindent(),
6724 );
6725
6726 // Don't autoclose if the next character isn't whitespace and isn't
6727 // listed in the language's "autoclose_before" section.
6728 cx.set_state("ˇa b");
6729 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6730 cx.assert_editor_state("{ˇa b");
6731
6732 // Don't autoclose if `close` is false for the bracket pair
6733 cx.set_state("ˇ");
6734 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6735 cx.assert_editor_state("[ˇ");
6736
6737 // Surround with brackets if text is selected
6738 cx.set_state("«aˇ» b");
6739 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6740 cx.assert_editor_state("{«aˇ»} b");
6741
6742 // Autoclose when not immediately after a word character
6743 cx.set_state("a ˇ");
6744 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6745 cx.assert_editor_state("a \"ˇ\"");
6746
6747 // Autoclose pair where the start and end characters are the same
6748 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6749 cx.assert_editor_state("a \"\"ˇ");
6750
6751 // Don't autoclose when immediately after a word character
6752 cx.set_state("aˇ");
6753 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6754 cx.assert_editor_state("a\"ˇ");
6755
6756 // Do autoclose when after a non-word character
6757 cx.set_state("{ˇ");
6758 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6759 cx.assert_editor_state("{\"ˇ\"");
6760
6761 // Non identical pairs autoclose regardless of preceding character
6762 cx.set_state("aˇ");
6763 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6764 cx.assert_editor_state("a{ˇ}");
6765
6766 // Don't autoclose pair if autoclose is disabled
6767 cx.set_state("ˇ");
6768 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6769 cx.assert_editor_state("<ˇ");
6770
6771 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6772 cx.set_state("«aˇ» b");
6773 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6774 cx.assert_editor_state("<«aˇ»> b");
6775}
6776
6777#[gpui::test]
6778async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6779 init_test(cx, |settings| {
6780 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6781 });
6782
6783 let mut cx = EditorTestContext::new(cx).await;
6784
6785 let language = Arc::new(Language::new(
6786 LanguageConfig {
6787 brackets: BracketPairConfig {
6788 pairs: vec![
6789 BracketPair {
6790 start: "{".to_string(),
6791 end: "}".to_string(),
6792 close: true,
6793 surround: true,
6794 newline: true,
6795 },
6796 BracketPair {
6797 start: "(".to_string(),
6798 end: ")".to_string(),
6799 close: true,
6800 surround: true,
6801 newline: true,
6802 },
6803 BracketPair {
6804 start: "[".to_string(),
6805 end: "]".to_string(),
6806 close: false,
6807 surround: false,
6808 newline: true,
6809 },
6810 ],
6811 ..Default::default()
6812 },
6813 autoclose_before: "})]".to_string(),
6814 ..Default::default()
6815 },
6816 Some(tree_sitter_rust::LANGUAGE.into()),
6817 ));
6818
6819 cx.language_registry().add(language.clone());
6820 cx.update_buffer(|buffer, cx| {
6821 buffer.set_language(Some(language), cx);
6822 });
6823
6824 cx.set_state(
6825 &"
6826 ˇ
6827 ˇ
6828 ˇ
6829 "
6830 .unindent(),
6831 );
6832
6833 // ensure only matching closing brackets are skipped over
6834 cx.update_editor(|editor, window, cx| {
6835 editor.handle_input("}", window, cx);
6836 editor.move_left(&MoveLeft, window, cx);
6837 editor.handle_input(")", window, cx);
6838 editor.move_left(&MoveLeft, window, cx);
6839 });
6840 cx.assert_editor_state(
6841 &"
6842 ˇ)}
6843 ˇ)}
6844 ˇ)}
6845 "
6846 .unindent(),
6847 );
6848
6849 // skip-over closing brackets at multiple cursors
6850 cx.update_editor(|editor, window, cx| {
6851 editor.handle_input(")", window, cx);
6852 editor.handle_input("}", window, cx);
6853 });
6854 cx.assert_editor_state(
6855 &"
6856 )}ˇ
6857 )}ˇ
6858 )}ˇ
6859 "
6860 .unindent(),
6861 );
6862
6863 // ignore non-close brackets
6864 cx.update_editor(|editor, window, cx| {
6865 editor.handle_input("]", window, cx);
6866 editor.move_left(&MoveLeft, window, cx);
6867 editor.handle_input("]", window, cx);
6868 });
6869 cx.assert_editor_state(
6870 &"
6871 )}]ˇ]
6872 )}]ˇ]
6873 )}]ˇ]
6874 "
6875 .unindent(),
6876 );
6877}
6878
6879#[gpui::test]
6880async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6881 init_test(cx, |_| {});
6882
6883 let mut cx = EditorTestContext::new(cx).await;
6884
6885 let html_language = Arc::new(
6886 Language::new(
6887 LanguageConfig {
6888 name: "HTML".into(),
6889 brackets: BracketPairConfig {
6890 pairs: vec![
6891 BracketPair {
6892 start: "<".into(),
6893 end: ">".into(),
6894 close: true,
6895 ..Default::default()
6896 },
6897 BracketPair {
6898 start: "{".into(),
6899 end: "}".into(),
6900 close: true,
6901 ..Default::default()
6902 },
6903 BracketPair {
6904 start: "(".into(),
6905 end: ")".into(),
6906 close: true,
6907 ..Default::default()
6908 },
6909 ],
6910 ..Default::default()
6911 },
6912 autoclose_before: "})]>".into(),
6913 ..Default::default()
6914 },
6915 Some(tree_sitter_html::LANGUAGE.into()),
6916 )
6917 .with_injection_query(
6918 r#"
6919 (script_element
6920 (raw_text) @injection.content
6921 (#set! injection.language "javascript"))
6922 "#,
6923 )
6924 .unwrap(),
6925 );
6926
6927 let javascript_language = Arc::new(Language::new(
6928 LanguageConfig {
6929 name: "JavaScript".into(),
6930 brackets: BracketPairConfig {
6931 pairs: vec![
6932 BracketPair {
6933 start: "/*".into(),
6934 end: " */".into(),
6935 close: true,
6936 ..Default::default()
6937 },
6938 BracketPair {
6939 start: "{".into(),
6940 end: "}".into(),
6941 close: true,
6942 ..Default::default()
6943 },
6944 BracketPair {
6945 start: "(".into(),
6946 end: ")".into(),
6947 close: true,
6948 ..Default::default()
6949 },
6950 ],
6951 ..Default::default()
6952 },
6953 autoclose_before: "})]>".into(),
6954 ..Default::default()
6955 },
6956 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6957 ));
6958
6959 cx.language_registry().add(html_language.clone());
6960 cx.language_registry().add(javascript_language.clone());
6961
6962 cx.update_buffer(|buffer, cx| {
6963 buffer.set_language(Some(html_language), cx);
6964 });
6965
6966 cx.set_state(
6967 &r#"
6968 <body>ˇ
6969 <script>
6970 var x = 1;ˇ
6971 </script>
6972 </body>ˇ
6973 "#
6974 .unindent(),
6975 );
6976
6977 // Precondition: different languages are active at different locations.
6978 cx.update_editor(|editor, window, cx| {
6979 let snapshot = editor.snapshot(window, cx);
6980 let cursors = editor.selections.ranges::<usize>(cx);
6981 let languages = cursors
6982 .iter()
6983 .map(|c| snapshot.language_at(c.start).unwrap().name())
6984 .collect::<Vec<_>>();
6985 assert_eq!(
6986 languages,
6987 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6988 );
6989 });
6990
6991 // Angle brackets autoclose in HTML, but not JavaScript.
6992 cx.update_editor(|editor, window, cx| {
6993 editor.handle_input("<", window, cx);
6994 editor.handle_input("a", window, cx);
6995 });
6996 cx.assert_editor_state(
6997 &r#"
6998 <body><aˇ>
6999 <script>
7000 var x = 1;<aˇ
7001 </script>
7002 </body><aˇ>
7003 "#
7004 .unindent(),
7005 );
7006
7007 // Curly braces and parens autoclose in both HTML and JavaScript.
7008 cx.update_editor(|editor, window, cx| {
7009 editor.handle_input(" b=", window, cx);
7010 editor.handle_input("{", window, cx);
7011 editor.handle_input("c", window, cx);
7012 editor.handle_input("(", window, cx);
7013 });
7014 cx.assert_editor_state(
7015 &r#"
7016 <body><a b={c(ˇ)}>
7017 <script>
7018 var x = 1;<a b={c(ˇ)}
7019 </script>
7020 </body><a b={c(ˇ)}>
7021 "#
7022 .unindent(),
7023 );
7024
7025 // Brackets that were already autoclosed are skipped.
7026 cx.update_editor(|editor, window, cx| {
7027 editor.handle_input(")", window, cx);
7028 editor.handle_input("d", window, cx);
7029 editor.handle_input("}", window, cx);
7030 });
7031 cx.assert_editor_state(
7032 &r#"
7033 <body><a b={c()d}ˇ>
7034 <script>
7035 var x = 1;<a b={c()d}ˇ
7036 </script>
7037 </body><a b={c()d}ˇ>
7038 "#
7039 .unindent(),
7040 );
7041 cx.update_editor(|editor, window, cx| {
7042 editor.handle_input(">", window, cx);
7043 });
7044 cx.assert_editor_state(
7045 &r#"
7046 <body><a b={c()d}>ˇ
7047 <script>
7048 var x = 1;<a b={c()d}>ˇ
7049 </script>
7050 </body><a b={c()d}>ˇ
7051 "#
7052 .unindent(),
7053 );
7054
7055 // Reset
7056 cx.set_state(
7057 &r#"
7058 <body>ˇ
7059 <script>
7060 var x = 1;ˇ
7061 </script>
7062 </body>ˇ
7063 "#
7064 .unindent(),
7065 );
7066
7067 cx.update_editor(|editor, window, cx| {
7068 editor.handle_input("<", window, cx);
7069 });
7070 cx.assert_editor_state(
7071 &r#"
7072 <body><ˇ>
7073 <script>
7074 var x = 1;<ˇ
7075 </script>
7076 </body><ˇ>
7077 "#
7078 .unindent(),
7079 );
7080
7081 // When backspacing, the closing angle brackets are removed.
7082 cx.update_editor(|editor, window, cx| {
7083 editor.backspace(&Backspace, window, cx);
7084 });
7085 cx.assert_editor_state(
7086 &r#"
7087 <body>ˇ
7088 <script>
7089 var x = 1;ˇ
7090 </script>
7091 </body>ˇ
7092 "#
7093 .unindent(),
7094 );
7095
7096 // Block comments autoclose in JavaScript, but not HTML.
7097 cx.update_editor(|editor, window, cx| {
7098 editor.handle_input("/", window, cx);
7099 editor.handle_input("*", window, cx);
7100 });
7101 cx.assert_editor_state(
7102 &r#"
7103 <body>/*ˇ
7104 <script>
7105 var x = 1;/*ˇ */
7106 </script>
7107 </body>/*ˇ
7108 "#
7109 .unindent(),
7110 );
7111}
7112
7113#[gpui::test]
7114async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7115 init_test(cx, |_| {});
7116
7117 let mut cx = EditorTestContext::new(cx).await;
7118
7119 let rust_language = Arc::new(
7120 Language::new(
7121 LanguageConfig {
7122 name: "Rust".into(),
7123 brackets: serde_json::from_value(json!([
7124 { "start": "{", "end": "}", "close": true, "newline": true },
7125 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7126 ]))
7127 .unwrap(),
7128 autoclose_before: "})]>".into(),
7129 ..Default::default()
7130 },
7131 Some(tree_sitter_rust::LANGUAGE.into()),
7132 )
7133 .with_override_query("(string_literal) @string")
7134 .unwrap(),
7135 );
7136
7137 cx.language_registry().add(rust_language.clone());
7138 cx.update_buffer(|buffer, cx| {
7139 buffer.set_language(Some(rust_language), cx);
7140 });
7141
7142 cx.set_state(
7143 &r#"
7144 let x = ˇ
7145 "#
7146 .unindent(),
7147 );
7148
7149 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7150 cx.update_editor(|editor, window, cx| {
7151 editor.handle_input("\"", window, cx);
7152 });
7153 cx.assert_editor_state(
7154 &r#"
7155 let x = "ˇ"
7156 "#
7157 .unindent(),
7158 );
7159
7160 // Inserting another quotation mark. The cursor moves across the existing
7161 // automatically-inserted quotation mark.
7162 cx.update_editor(|editor, window, cx| {
7163 editor.handle_input("\"", window, cx);
7164 });
7165 cx.assert_editor_state(
7166 &r#"
7167 let x = ""ˇ
7168 "#
7169 .unindent(),
7170 );
7171
7172 // Reset
7173 cx.set_state(
7174 &r#"
7175 let x = ˇ
7176 "#
7177 .unindent(),
7178 );
7179
7180 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7181 cx.update_editor(|editor, window, cx| {
7182 editor.handle_input("\"", window, cx);
7183 editor.handle_input(" ", window, cx);
7184 editor.move_left(&Default::default(), window, cx);
7185 editor.handle_input("\\", window, cx);
7186 editor.handle_input("\"", window, cx);
7187 });
7188 cx.assert_editor_state(
7189 &r#"
7190 let x = "\"ˇ "
7191 "#
7192 .unindent(),
7193 );
7194
7195 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7196 // mark. Nothing is inserted.
7197 cx.update_editor(|editor, window, cx| {
7198 editor.move_right(&Default::default(), window, cx);
7199 editor.handle_input("\"", window, cx);
7200 });
7201 cx.assert_editor_state(
7202 &r#"
7203 let x = "\" "ˇ
7204 "#
7205 .unindent(),
7206 );
7207}
7208
7209#[gpui::test]
7210async fn test_surround_with_pair(cx: &mut TestAppContext) {
7211 init_test(cx, |_| {});
7212
7213 let language = Arc::new(Language::new(
7214 LanguageConfig {
7215 brackets: BracketPairConfig {
7216 pairs: vec![
7217 BracketPair {
7218 start: "{".to_string(),
7219 end: "}".to_string(),
7220 close: true,
7221 surround: true,
7222 newline: true,
7223 },
7224 BracketPair {
7225 start: "/* ".to_string(),
7226 end: "*/".to_string(),
7227 close: true,
7228 surround: true,
7229 ..Default::default()
7230 },
7231 ],
7232 ..Default::default()
7233 },
7234 ..Default::default()
7235 },
7236 Some(tree_sitter_rust::LANGUAGE.into()),
7237 ));
7238
7239 let text = r#"
7240 a
7241 b
7242 c
7243 "#
7244 .unindent();
7245
7246 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7247 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7248 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7249 editor
7250 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7251 .await;
7252
7253 editor.update_in(cx, |editor, window, cx| {
7254 editor.change_selections(None, window, cx, |s| {
7255 s.select_display_ranges([
7256 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7257 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7258 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7259 ])
7260 });
7261
7262 editor.handle_input("{", window, cx);
7263 editor.handle_input("{", window, cx);
7264 editor.handle_input("{", window, cx);
7265 assert_eq!(
7266 editor.text(cx),
7267 "
7268 {{{a}}}
7269 {{{b}}}
7270 {{{c}}}
7271 "
7272 .unindent()
7273 );
7274 assert_eq!(
7275 editor.selections.display_ranges(cx),
7276 [
7277 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7278 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7279 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7280 ]
7281 );
7282
7283 editor.undo(&Undo, window, cx);
7284 editor.undo(&Undo, window, cx);
7285 editor.undo(&Undo, window, cx);
7286 assert_eq!(
7287 editor.text(cx),
7288 "
7289 a
7290 b
7291 c
7292 "
7293 .unindent()
7294 );
7295 assert_eq!(
7296 editor.selections.display_ranges(cx),
7297 [
7298 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7299 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7300 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7301 ]
7302 );
7303
7304 // Ensure inserting the first character of a multi-byte bracket pair
7305 // doesn't surround the selections with the bracket.
7306 editor.handle_input("/", window, cx);
7307 assert_eq!(
7308 editor.text(cx),
7309 "
7310 /
7311 /
7312 /
7313 "
7314 .unindent()
7315 );
7316 assert_eq!(
7317 editor.selections.display_ranges(cx),
7318 [
7319 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7320 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7321 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7322 ]
7323 );
7324
7325 editor.undo(&Undo, window, cx);
7326 assert_eq!(
7327 editor.text(cx),
7328 "
7329 a
7330 b
7331 c
7332 "
7333 .unindent()
7334 );
7335 assert_eq!(
7336 editor.selections.display_ranges(cx),
7337 [
7338 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7339 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7340 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7341 ]
7342 );
7343
7344 // Ensure inserting the last character of a multi-byte bracket pair
7345 // doesn't surround the selections with the bracket.
7346 editor.handle_input("*", window, cx);
7347 assert_eq!(
7348 editor.text(cx),
7349 "
7350 *
7351 *
7352 *
7353 "
7354 .unindent()
7355 );
7356 assert_eq!(
7357 editor.selections.display_ranges(cx),
7358 [
7359 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7360 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7361 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7362 ]
7363 );
7364 });
7365}
7366
7367#[gpui::test]
7368async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7369 init_test(cx, |_| {});
7370
7371 let language = Arc::new(Language::new(
7372 LanguageConfig {
7373 brackets: BracketPairConfig {
7374 pairs: vec![BracketPair {
7375 start: "{".to_string(),
7376 end: "}".to_string(),
7377 close: true,
7378 surround: true,
7379 newline: true,
7380 }],
7381 ..Default::default()
7382 },
7383 autoclose_before: "}".to_string(),
7384 ..Default::default()
7385 },
7386 Some(tree_sitter_rust::LANGUAGE.into()),
7387 ));
7388
7389 let text = r#"
7390 a
7391 b
7392 c
7393 "#
7394 .unindent();
7395
7396 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7397 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7398 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7399 editor
7400 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7401 .await;
7402
7403 editor.update_in(cx, |editor, window, cx| {
7404 editor.change_selections(None, window, cx, |s| {
7405 s.select_ranges([
7406 Point::new(0, 1)..Point::new(0, 1),
7407 Point::new(1, 1)..Point::new(1, 1),
7408 Point::new(2, 1)..Point::new(2, 1),
7409 ])
7410 });
7411
7412 editor.handle_input("{", window, cx);
7413 editor.handle_input("{", window, cx);
7414 editor.handle_input("_", window, cx);
7415 assert_eq!(
7416 editor.text(cx),
7417 "
7418 a{{_}}
7419 b{{_}}
7420 c{{_}}
7421 "
7422 .unindent()
7423 );
7424 assert_eq!(
7425 editor.selections.ranges::<Point>(cx),
7426 [
7427 Point::new(0, 4)..Point::new(0, 4),
7428 Point::new(1, 4)..Point::new(1, 4),
7429 Point::new(2, 4)..Point::new(2, 4)
7430 ]
7431 );
7432
7433 editor.backspace(&Default::default(), window, cx);
7434 editor.backspace(&Default::default(), window, cx);
7435 assert_eq!(
7436 editor.text(cx),
7437 "
7438 a{}
7439 b{}
7440 c{}
7441 "
7442 .unindent()
7443 );
7444 assert_eq!(
7445 editor.selections.ranges::<Point>(cx),
7446 [
7447 Point::new(0, 2)..Point::new(0, 2),
7448 Point::new(1, 2)..Point::new(1, 2),
7449 Point::new(2, 2)..Point::new(2, 2)
7450 ]
7451 );
7452
7453 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7454 assert_eq!(
7455 editor.text(cx),
7456 "
7457 a
7458 b
7459 c
7460 "
7461 .unindent()
7462 );
7463 assert_eq!(
7464 editor.selections.ranges::<Point>(cx),
7465 [
7466 Point::new(0, 1)..Point::new(0, 1),
7467 Point::new(1, 1)..Point::new(1, 1),
7468 Point::new(2, 1)..Point::new(2, 1)
7469 ]
7470 );
7471 });
7472}
7473
7474#[gpui::test]
7475async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7476 init_test(cx, |settings| {
7477 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7478 });
7479
7480 let mut cx = EditorTestContext::new(cx).await;
7481
7482 let language = Arc::new(Language::new(
7483 LanguageConfig {
7484 brackets: BracketPairConfig {
7485 pairs: vec![
7486 BracketPair {
7487 start: "{".to_string(),
7488 end: "}".to_string(),
7489 close: true,
7490 surround: true,
7491 newline: true,
7492 },
7493 BracketPair {
7494 start: "(".to_string(),
7495 end: ")".to_string(),
7496 close: true,
7497 surround: true,
7498 newline: true,
7499 },
7500 BracketPair {
7501 start: "[".to_string(),
7502 end: "]".to_string(),
7503 close: false,
7504 surround: true,
7505 newline: true,
7506 },
7507 ],
7508 ..Default::default()
7509 },
7510 autoclose_before: "})]".to_string(),
7511 ..Default::default()
7512 },
7513 Some(tree_sitter_rust::LANGUAGE.into()),
7514 ));
7515
7516 cx.language_registry().add(language.clone());
7517 cx.update_buffer(|buffer, cx| {
7518 buffer.set_language(Some(language), cx);
7519 });
7520
7521 cx.set_state(
7522 &"
7523 {(ˇ)}
7524 [[ˇ]]
7525 {(ˇ)}
7526 "
7527 .unindent(),
7528 );
7529
7530 cx.update_editor(|editor, window, cx| {
7531 editor.backspace(&Default::default(), window, cx);
7532 editor.backspace(&Default::default(), window, cx);
7533 });
7534
7535 cx.assert_editor_state(
7536 &"
7537 ˇ
7538 ˇ]]
7539 ˇ
7540 "
7541 .unindent(),
7542 );
7543
7544 cx.update_editor(|editor, window, cx| {
7545 editor.handle_input("{", window, cx);
7546 editor.handle_input("{", window, cx);
7547 editor.move_right(&MoveRight, window, cx);
7548 editor.move_right(&MoveRight, window, cx);
7549 editor.move_left(&MoveLeft, window, cx);
7550 editor.move_left(&MoveLeft, window, cx);
7551 editor.backspace(&Default::default(), window, cx);
7552 });
7553
7554 cx.assert_editor_state(
7555 &"
7556 {ˇ}
7557 {ˇ}]]
7558 {ˇ}
7559 "
7560 .unindent(),
7561 );
7562
7563 cx.update_editor(|editor, window, cx| {
7564 editor.backspace(&Default::default(), window, cx);
7565 });
7566
7567 cx.assert_editor_state(
7568 &"
7569 ˇ
7570 ˇ]]
7571 ˇ
7572 "
7573 .unindent(),
7574 );
7575}
7576
7577#[gpui::test]
7578async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7579 init_test(cx, |_| {});
7580
7581 let language = Arc::new(Language::new(
7582 LanguageConfig::default(),
7583 Some(tree_sitter_rust::LANGUAGE.into()),
7584 ));
7585
7586 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7587 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7588 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7589 editor
7590 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7591 .await;
7592
7593 editor.update_in(cx, |editor, window, cx| {
7594 editor.set_auto_replace_emoji_shortcode(true);
7595
7596 editor.handle_input("Hello ", window, cx);
7597 editor.handle_input(":wave", window, cx);
7598 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7599
7600 editor.handle_input(":", window, cx);
7601 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7602
7603 editor.handle_input(" :smile", window, cx);
7604 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7605
7606 editor.handle_input(":", window, cx);
7607 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7608
7609 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7610 editor.handle_input(":wave", window, cx);
7611 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7612
7613 editor.handle_input(":", window, cx);
7614 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7615
7616 editor.handle_input(":1", window, cx);
7617 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7618
7619 editor.handle_input(":", window, cx);
7620 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7621
7622 // Ensure shortcode does not get replaced when it is part of a word
7623 editor.handle_input(" Test:wave", window, cx);
7624 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7625
7626 editor.handle_input(":", window, cx);
7627 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7628
7629 editor.set_auto_replace_emoji_shortcode(false);
7630
7631 // Ensure shortcode does not get replaced when auto replace is off
7632 editor.handle_input(" :wave", window, cx);
7633 assert_eq!(
7634 editor.text(cx),
7635 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7636 );
7637
7638 editor.handle_input(":", window, cx);
7639 assert_eq!(
7640 editor.text(cx),
7641 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7642 );
7643 });
7644}
7645
7646#[gpui::test]
7647async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7648 init_test(cx, |_| {});
7649
7650 let (text, insertion_ranges) = marked_text_ranges(
7651 indoc! {"
7652 ˇ
7653 "},
7654 false,
7655 );
7656
7657 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7658 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7659
7660 _ = editor.update_in(cx, |editor, window, cx| {
7661 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7662
7663 editor
7664 .insert_snippet(&insertion_ranges, snippet, window, cx)
7665 .unwrap();
7666
7667 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7668 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7669 assert_eq!(editor.text(cx), expected_text);
7670 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7671 }
7672
7673 assert(
7674 editor,
7675 cx,
7676 indoc! {"
7677 type «» =•
7678 "},
7679 );
7680
7681 assert!(editor.context_menu_visible(), "There should be a matches");
7682 });
7683}
7684
7685#[gpui::test]
7686async fn test_snippets(cx: &mut TestAppContext) {
7687 init_test(cx, |_| {});
7688
7689 let (text, insertion_ranges) = marked_text_ranges(
7690 indoc! {"
7691 a.ˇ b
7692 a.ˇ b
7693 a.ˇ b
7694 "},
7695 false,
7696 );
7697
7698 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7699 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7700
7701 editor.update_in(cx, |editor, window, cx| {
7702 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7703
7704 editor
7705 .insert_snippet(&insertion_ranges, snippet, window, cx)
7706 .unwrap();
7707
7708 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7709 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7710 assert_eq!(editor.text(cx), expected_text);
7711 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7712 }
7713
7714 assert(
7715 editor,
7716 cx,
7717 indoc! {"
7718 a.f(«one», two, «three») b
7719 a.f(«one», two, «three») b
7720 a.f(«one», two, «three») b
7721 "},
7722 );
7723
7724 // Can't move earlier than the first tab stop
7725 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7726 assert(
7727 editor,
7728 cx,
7729 indoc! {"
7730 a.f(«one», two, «three») b
7731 a.f(«one», two, «three») b
7732 a.f(«one», two, «three») b
7733 "},
7734 );
7735
7736 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7737 assert(
7738 editor,
7739 cx,
7740 indoc! {"
7741 a.f(one, «two», three) b
7742 a.f(one, «two», three) b
7743 a.f(one, «two», three) b
7744 "},
7745 );
7746
7747 editor.move_to_prev_snippet_tabstop(window, cx);
7748 assert(
7749 editor,
7750 cx,
7751 indoc! {"
7752 a.f(«one», two, «three») b
7753 a.f(«one», two, «three») b
7754 a.f(«one», two, «three») b
7755 "},
7756 );
7757
7758 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7759 assert(
7760 editor,
7761 cx,
7762 indoc! {"
7763 a.f(one, «two», three) b
7764 a.f(one, «two», three) b
7765 a.f(one, «two», three) b
7766 "},
7767 );
7768 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7769 assert(
7770 editor,
7771 cx,
7772 indoc! {"
7773 a.f(one, two, three)ˇ b
7774 a.f(one, two, three)ˇ b
7775 a.f(one, two, three)ˇ b
7776 "},
7777 );
7778
7779 // As soon as the last tab stop is reached, snippet state is gone
7780 editor.move_to_prev_snippet_tabstop(window, cx);
7781 assert(
7782 editor,
7783 cx,
7784 indoc! {"
7785 a.f(one, two, three)ˇ b
7786 a.f(one, two, three)ˇ b
7787 a.f(one, two, three)ˇ b
7788 "},
7789 );
7790 });
7791}
7792
7793#[gpui::test]
7794async fn test_document_format_during_save(cx: &mut TestAppContext) {
7795 init_test(cx, |_| {});
7796
7797 let fs = FakeFs::new(cx.executor());
7798 fs.insert_file(path!("/file.rs"), Default::default()).await;
7799
7800 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7801
7802 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7803 language_registry.add(rust_lang());
7804 let mut fake_servers = language_registry.register_fake_lsp(
7805 "Rust",
7806 FakeLspAdapter {
7807 capabilities: lsp::ServerCapabilities {
7808 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7809 ..Default::default()
7810 },
7811 ..Default::default()
7812 },
7813 );
7814
7815 let buffer = project
7816 .update(cx, |project, cx| {
7817 project.open_local_buffer(path!("/file.rs"), cx)
7818 })
7819 .await
7820 .unwrap();
7821
7822 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7823 let (editor, cx) = cx.add_window_view(|window, cx| {
7824 build_editor_with_project(project.clone(), buffer, window, cx)
7825 });
7826 editor.update_in(cx, |editor, window, cx| {
7827 editor.set_text("one\ntwo\nthree\n", window, cx)
7828 });
7829 assert!(cx.read(|cx| editor.is_dirty(cx)));
7830
7831 cx.executor().start_waiting();
7832 let fake_server = fake_servers.next().await.unwrap();
7833
7834 {
7835 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7836 move |params, _| async move {
7837 assert_eq!(
7838 params.text_document.uri,
7839 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7840 );
7841 assert_eq!(params.options.tab_size, 4);
7842 Ok(Some(vec![lsp::TextEdit::new(
7843 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7844 ", ".to_string(),
7845 )]))
7846 },
7847 );
7848 let save = editor
7849 .update_in(cx, |editor, window, cx| {
7850 editor.save(true, project.clone(), window, cx)
7851 })
7852 .unwrap();
7853 cx.executor().start_waiting();
7854 save.await;
7855
7856 assert_eq!(
7857 editor.update(cx, |editor, cx| editor.text(cx)),
7858 "one, two\nthree\n"
7859 );
7860 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7861 }
7862
7863 {
7864 editor.update_in(cx, |editor, window, cx| {
7865 editor.set_text("one\ntwo\nthree\n", window, cx)
7866 });
7867 assert!(cx.read(|cx| editor.is_dirty(cx)));
7868
7869 // Ensure we can still save even if formatting hangs.
7870 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7871 move |params, _| async move {
7872 assert_eq!(
7873 params.text_document.uri,
7874 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7875 );
7876 futures::future::pending::<()>().await;
7877 unreachable!()
7878 },
7879 );
7880 let save = editor
7881 .update_in(cx, |editor, window, cx| {
7882 editor.save(true, project.clone(), window, cx)
7883 })
7884 .unwrap();
7885 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7886 cx.executor().start_waiting();
7887 save.await;
7888 assert_eq!(
7889 editor.update(cx, |editor, cx| editor.text(cx)),
7890 "one\ntwo\nthree\n"
7891 );
7892 }
7893
7894 // For non-dirty buffer, no formatting request should be sent
7895 {
7896 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7897
7898 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7899 panic!("Should not be invoked on non-dirty buffer");
7900 });
7901 let save = editor
7902 .update_in(cx, |editor, window, cx| {
7903 editor.save(true, project.clone(), window, cx)
7904 })
7905 .unwrap();
7906 cx.executor().start_waiting();
7907 save.await;
7908 }
7909
7910 // Set rust language override and assert overridden tabsize is sent to language server
7911 update_test_language_settings(cx, |settings| {
7912 settings.languages.insert(
7913 "Rust".into(),
7914 LanguageSettingsContent {
7915 tab_size: NonZeroU32::new(8),
7916 ..Default::default()
7917 },
7918 );
7919 });
7920
7921 {
7922 editor.update_in(cx, |editor, window, cx| {
7923 editor.set_text("somehting_new\n", window, cx)
7924 });
7925 assert!(cx.read(|cx| editor.is_dirty(cx)));
7926 let _formatting_request_signal = fake_server
7927 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7928 assert_eq!(
7929 params.text_document.uri,
7930 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7931 );
7932 assert_eq!(params.options.tab_size, 8);
7933 Ok(Some(vec![]))
7934 });
7935 let save = editor
7936 .update_in(cx, |editor, window, cx| {
7937 editor.save(true, project.clone(), window, cx)
7938 })
7939 .unwrap();
7940 cx.executor().start_waiting();
7941 save.await;
7942 }
7943}
7944
7945#[gpui::test]
7946async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7947 init_test(cx, |_| {});
7948
7949 let cols = 4;
7950 let rows = 10;
7951 let sample_text_1 = sample_text(rows, cols, 'a');
7952 assert_eq!(
7953 sample_text_1,
7954 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7955 );
7956 let sample_text_2 = sample_text(rows, cols, 'l');
7957 assert_eq!(
7958 sample_text_2,
7959 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7960 );
7961 let sample_text_3 = sample_text(rows, cols, 'v');
7962 assert_eq!(
7963 sample_text_3,
7964 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7965 );
7966
7967 let fs = FakeFs::new(cx.executor());
7968 fs.insert_tree(
7969 path!("/a"),
7970 json!({
7971 "main.rs": sample_text_1,
7972 "other.rs": sample_text_2,
7973 "lib.rs": sample_text_3,
7974 }),
7975 )
7976 .await;
7977
7978 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7979 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7980 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7981
7982 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7983 language_registry.add(rust_lang());
7984 let mut fake_servers = language_registry.register_fake_lsp(
7985 "Rust",
7986 FakeLspAdapter {
7987 capabilities: lsp::ServerCapabilities {
7988 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7989 ..Default::default()
7990 },
7991 ..Default::default()
7992 },
7993 );
7994
7995 let worktree = project.update(cx, |project, cx| {
7996 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7997 assert_eq!(worktrees.len(), 1);
7998 worktrees.pop().unwrap()
7999 });
8000 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8001
8002 let buffer_1 = project
8003 .update(cx, |project, cx| {
8004 project.open_buffer((worktree_id, "main.rs"), cx)
8005 })
8006 .await
8007 .unwrap();
8008 let buffer_2 = project
8009 .update(cx, |project, cx| {
8010 project.open_buffer((worktree_id, "other.rs"), cx)
8011 })
8012 .await
8013 .unwrap();
8014 let buffer_3 = project
8015 .update(cx, |project, cx| {
8016 project.open_buffer((worktree_id, "lib.rs"), cx)
8017 })
8018 .await
8019 .unwrap();
8020
8021 let multi_buffer = cx.new(|cx| {
8022 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8023 multi_buffer.push_excerpts(
8024 buffer_1.clone(),
8025 [
8026 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8027 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8028 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8029 ],
8030 cx,
8031 );
8032 multi_buffer.push_excerpts(
8033 buffer_2.clone(),
8034 [
8035 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8036 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8037 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8038 ],
8039 cx,
8040 );
8041 multi_buffer.push_excerpts(
8042 buffer_3.clone(),
8043 [
8044 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8045 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8046 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8047 ],
8048 cx,
8049 );
8050 multi_buffer
8051 });
8052 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8053 Editor::new(
8054 EditorMode::full(),
8055 multi_buffer,
8056 Some(project.clone()),
8057 window,
8058 cx,
8059 )
8060 });
8061
8062 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8063 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8064 s.select_ranges(Some(1..2))
8065 });
8066 editor.insert("|one|two|three|", window, cx);
8067 });
8068 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8069 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8070 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8071 s.select_ranges(Some(60..70))
8072 });
8073 editor.insert("|four|five|six|", window, cx);
8074 });
8075 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8076
8077 // First two buffers should be edited, but not the third one.
8078 assert_eq!(
8079 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8080 "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}",
8081 );
8082 buffer_1.update(cx, |buffer, _| {
8083 assert!(buffer.is_dirty());
8084 assert_eq!(
8085 buffer.text(),
8086 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8087 )
8088 });
8089 buffer_2.update(cx, |buffer, _| {
8090 assert!(buffer.is_dirty());
8091 assert_eq!(
8092 buffer.text(),
8093 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8094 )
8095 });
8096 buffer_3.update(cx, |buffer, _| {
8097 assert!(!buffer.is_dirty());
8098 assert_eq!(buffer.text(), sample_text_3,)
8099 });
8100 cx.executor().run_until_parked();
8101
8102 cx.executor().start_waiting();
8103 let save = multi_buffer_editor
8104 .update_in(cx, |editor, window, cx| {
8105 editor.save(true, project.clone(), window, cx)
8106 })
8107 .unwrap();
8108
8109 let fake_server = fake_servers.next().await.unwrap();
8110 fake_server
8111 .server
8112 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8113 Ok(Some(vec![lsp::TextEdit::new(
8114 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8115 format!("[{} formatted]", params.text_document.uri),
8116 )]))
8117 })
8118 .detach();
8119 save.await;
8120
8121 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8122 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8123 assert_eq!(
8124 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8125 uri!(
8126 "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}"
8127 ),
8128 );
8129 buffer_1.update(cx, |buffer, _| {
8130 assert!(!buffer.is_dirty());
8131 assert_eq!(
8132 buffer.text(),
8133 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8134 )
8135 });
8136 buffer_2.update(cx, |buffer, _| {
8137 assert!(!buffer.is_dirty());
8138 assert_eq!(
8139 buffer.text(),
8140 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8141 )
8142 });
8143 buffer_3.update(cx, |buffer, _| {
8144 assert!(!buffer.is_dirty());
8145 assert_eq!(buffer.text(), sample_text_3,)
8146 });
8147}
8148
8149#[gpui::test]
8150async fn test_range_format_during_save(cx: &mut TestAppContext) {
8151 init_test(cx, |_| {});
8152
8153 let fs = FakeFs::new(cx.executor());
8154 fs.insert_file(path!("/file.rs"), Default::default()).await;
8155
8156 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8157
8158 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8159 language_registry.add(rust_lang());
8160 let mut fake_servers = language_registry.register_fake_lsp(
8161 "Rust",
8162 FakeLspAdapter {
8163 capabilities: lsp::ServerCapabilities {
8164 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8165 ..Default::default()
8166 },
8167 ..Default::default()
8168 },
8169 );
8170
8171 let buffer = project
8172 .update(cx, |project, cx| {
8173 project.open_local_buffer(path!("/file.rs"), cx)
8174 })
8175 .await
8176 .unwrap();
8177
8178 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8179 let (editor, cx) = cx.add_window_view(|window, cx| {
8180 build_editor_with_project(project.clone(), buffer, window, cx)
8181 });
8182 editor.update_in(cx, |editor, window, cx| {
8183 editor.set_text("one\ntwo\nthree\n", window, cx)
8184 });
8185 assert!(cx.read(|cx| editor.is_dirty(cx)));
8186
8187 cx.executor().start_waiting();
8188 let fake_server = fake_servers.next().await.unwrap();
8189
8190 let save = editor
8191 .update_in(cx, |editor, window, cx| {
8192 editor.save(true, project.clone(), window, cx)
8193 })
8194 .unwrap();
8195 fake_server
8196 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8197 assert_eq!(
8198 params.text_document.uri,
8199 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8200 );
8201 assert_eq!(params.options.tab_size, 4);
8202 Ok(Some(vec![lsp::TextEdit::new(
8203 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8204 ", ".to_string(),
8205 )]))
8206 })
8207 .next()
8208 .await;
8209 cx.executor().start_waiting();
8210 save.await;
8211 assert_eq!(
8212 editor.update(cx, |editor, cx| editor.text(cx)),
8213 "one, two\nthree\n"
8214 );
8215 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8216
8217 editor.update_in(cx, |editor, window, cx| {
8218 editor.set_text("one\ntwo\nthree\n", window, cx)
8219 });
8220 assert!(cx.read(|cx| editor.is_dirty(cx)));
8221
8222 // Ensure we can still save even if formatting hangs.
8223 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8224 move |params, _| async move {
8225 assert_eq!(
8226 params.text_document.uri,
8227 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8228 );
8229 futures::future::pending::<()>().await;
8230 unreachable!()
8231 },
8232 );
8233 let save = editor
8234 .update_in(cx, |editor, window, cx| {
8235 editor.save(true, project.clone(), window, cx)
8236 })
8237 .unwrap();
8238 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8239 cx.executor().start_waiting();
8240 save.await;
8241 assert_eq!(
8242 editor.update(cx, |editor, cx| editor.text(cx)),
8243 "one\ntwo\nthree\n"
8244 );
8245 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8246
8247 // For non-dirty buffer, no formatting request should be sent
8248 let save = editor
8249 .update_in(cx, |editor, window, cx| {
8250 editor.save(true, project.clone(), window, cx)
8251 })
8252 .unwrap();
8253 let _pending_format_request = fake_server
8254 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8255 panic!("Should not be invoked on non-dirty buffer");
8256 })
8257 .next();
8258 cx.executor().start_waiting();
8259 save.await;
8260
8261 // Set Rust language override and assert overridden tabsize is sent to language server
8262 update_test_language_settings(cx, |settings| {
8263 settings.languages.insert(
8264 "Rust".into(),
8265 LanguageSettingsContent {
8266 tab_size: NonZeroU32::new(8),
8267 ..Default::default()
8268 },
8269 );
8270 });
8271
8272 editor.update_in(cx, |editor, window, cx| {
8273 editor.set_text("somehting_new\n", window, cx)
8274 });
8275 assert!(cx.read(|cx| editor.is_dirty(cx)));
8276 let save = editor
8277 .update_in(cx, |editor, window, cx| {
8278 editor.save(true, project.clone(), window, cx)
8279 })
8280 .unwrap();
8281 fake_server
8282 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8283 assert_eq!(
8284 params.text_document.uri,
8285 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8286 );
8287 assert_eq!(params.options.tab_size, 8);
8288 Ok(Some(vec![]))
8289 })
8290 .next()
8291 .await;
8292 cx.executor().start_waiting();
8293 save.await;
8294}
8295
8296#[gpui::test]
8297async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8298 init_test(cx, |settings| {
8299 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8300 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8301 ))
8302 });
8303
8304 let fs = FakeFs::new(cx.executor());
8305 fs.insert_file(path!("/file.rs"), Default::default()).await;
8306
8307 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8308
8309 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8310 language_registry.add(Arc::new(Language::new(
8311 LanguageConfig {
8312 name: "Rust".into(),
8313 matcher: LanguageMatcher {
8314 path_suffixes: vec!["rs".to_string()],
8315 ..Default::default()
8316 },
8317 ..LanguageConfig::default()
8318 },
8319 Some(tree_sitter_rust::LANGUAGE.into()),
8320 )));
8321 update_test_language_settings(cx, |settings| {
8322 // Enable Prettier formatting for the same buffer, and ensure
8323 // LSP is called instead of Prettier.
8324 settings.defaults.prettier = Some(PrettierSettings {
8325 allowed: true,
8326 ..PrettierSettings::default()
8327 });
8328 });
8329 let mut fake_servers = language_registry.register_fake_lsp(
8330 "Rust",
8331 FakeLspAdapter {
8332 capabilities: lsp::ServerCapabilities {
8333 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8334 ..Default::default()
8335 },
8336 ..Default::default()
8337 },
8338 );
8339
8340 let buffer = project
8341 .update(cx, |project, cx| {
8342 project.open_local_buffer(path!("/file.rs"), cx)
8343 })
8344 .await
8345 .unwrap();
8346
8347 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8348 let (editor, cx) = cx.add_window_view(|window, cx| {
8349 build_editor_with_project(project.clone(), buffer, window, cx)
8350 });
8351 editor.update_in(cx, |editor, window, cx| {
8352 editor.set_text("one\ntwo\nthree\n", window, cx)
8353 });
8354
8355 cx.executor().start_waiting();
8356 let fake_server = fake_servers.next().await.unwrap();
8357
8358 let format = editor
8359 .update_in(cx, |editor, window, cx| {
8360 editor.perform_format(
8361 project.clone(),
8362 FormatTrigger::Manual,
8363 FormatTarget::Buffers,
8364 window,
8365 cx,
8366 )
8367 })
8368 .unwrap();
8369 fake_server
8370 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8371 assert_eq!(
8372 params.text_document.uri,
8373 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8374 );
8375 assert_eq!(params.options.tab_size, 4);
8376 Ok(Some(vec![lsp::TextEdit::new(
8377 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8378 ", ".to_string(),
8379 )]))
8380 })
8381 .next()
8382 .await;
8383 cx.executor().start_waiting();
8384 format.await;
8385 assert_eq!(
8386 editor.update(cx, |editor, cx| editor.text(cx)),
8387 "one, two\nthree\n"
8388 );
8389
8390 editor.update_in(cx, |editor, window, cx| {
8391 editor.set_text("one\ntwo\nthree\n", window, cx)
8392 });
8393 // Ensure we don't lock if formatting hangs.
8394 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8395 move |params, _| async move {
8396 assert_eq!(
8397 params.text_document.uri,
8398 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8399 );
8400 futures::future::pending::<()>().await;
8401 unreachable!()
8402 },
8403 );
8404 let format = editor
8405 .update_in(cx, |editor, window, cx| {
8406 editor.perform_format(
8407 project,
8408 FormatTrigger::Manual,
8409 FormatTarget::Buffers,
8410 window,
8411 cx,
8412 )
8413 })
8414 .unwrap();
8415 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8416 cx.executor().start_waiting();
8417 format.await;
8418 assert_eq!(
8419 editor.update(cx, |editor, cx| editor.text(cx)),
8420 "one\ntwo\nthree\n"
8421 );
8422}
8423
8424#[gpui::test]
8425async fn test_multiple_formatters(cx: &mut TestAppContext) {
8426 init_test(cx, |settings| {
8427 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8428 settings.defaults.formatter =
8429 Some(language_settings::SelectedFormatter::List(FormatterList(
8430 vec![
8431 Formatter::LanguageServer { name: None },
8432 Formatter::CodeActions(
8433 [
8434 ("code-action-1".into(), true),
8435 ("code-action-2".into(), true),
8436 ]
8437 .into_iter()
8438 .collect(),
8439 ),
8440 ]
8441 .into(),
8442 )))
8443 });
8444
8445 let fs = FakeFs::new(cx.executor());
8446 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8447 .await;
8448
8449 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8450 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8451 language_registry.add(rust_lang());
8452
8453 let mut fake_servers = language_registry.register_fake_lsp(
8454 "Rust",
8455 FakeLspAdapter {
8456 capabilities: lsp::ServerCapabilities {
8457 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8458 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8459 commands: vec!["the-command-for-code-action-1".into()],
8460 ..Default::default()
8461 }),
8462 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8463 ..Default::default()
8464 },
8465 ..Default::default()
8466 },
8467 );
8468
8469 let buffer = project
8470 .update(cx, |project, cx| {
8471 project.open_local_buffer(path!("/file.rs"), cx)
8472 })
8473 .await
8474 .unwrap();
8475
8476 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8477 let (editor, cx) = cx.add_window_view(|window, cx| {
8478 build_editor_with_project(project.clone(), buffer, window, cx)
8479 });
8480
8481 cx.executor().start_waiting();
8482
8483 let fake_server = fake_servers.next().await.unwrap();
8484 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8485 move |_params, _| async move {
8486 Ok(Some(vec![lsp::TextEdit::new(
8487 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8488 "applied-formatting\n".to_string(),
8489 )]))
8490 },
8491 );
8492 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8493 move |params, _| async move {
8494 assert_eq!(
8495 params.context.only,
8496 Some(vec!["code-action-1".into(), "code-action-2".into()])
8497 );
8498 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8499 Ok(Some(vec![
8500 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8501 kind: Some("code-action-1".into()),
8502 edit: Some(lsp::WorkspaceEdit::new(
8503 [(
8504 uri.clone(),
8505 vec![lsp::TextEdit::new(
8506 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8507 "applied-code-action-1-edit\n".to_string(),
8508 )],
8509 )]
8510 .into_iter()
8511 .collect(),
8512 )),
8513 command: Some(lsp::Command {
8514 command: "the-command-for-code-action-1".into(),
8515 ..Default::default()
8516 }),
8517 ..Default::default()
8518 }),
8519 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8520 kind: Some("code-action-2".into()),
8521 edit: Some(lsp::WorkspaceEdit::new(
8522 [(
8523 uri.clone(),
8524 vec![lsp::TextEdit::new(
8525 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8526 "applied-code-action-2-edit\n".to_string(),
8527 )],
8528 )]
8529 .into_iter()
8530 .collect(),
8531 )),
8532 ..Default::default()
8533 }),
8534 ]))
8535 },
8536 );
8537
8538 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8539 move |params, _| async move { Ok(params) }
8540 });
8541
8542 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8543 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8544 let fake = fake_server.clone();
8545 let lock = command_lock.clone();
8546 move |params, _| {
8547 assert_eq!(params.command, "the-command-for-code-action-1");
8548 let fake = fake.clone();
8549 let lock = lock.clone();
8550 async move {
8551 lock.lock().await;
8552 fake.server
8553 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8554 label: None,
8555 edit: lsp::WorkspaceEdit {
8556 changes: Some(
8557 [(
8558 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8559 vec![lsp::TextEdit {
8560 range: lsp::Range::new(
8561 lsp::Position::new(0, 0),
8562 lsp::Position::new(0, 0),
8563 ),
8564 new_text: "applied-code-action-1-command\n".into(),
8565 }],
8566 )]
8567 .into_iter()
8568 .collect(),
8569 ),
8570 ..Default::default()
8571 },
8572 })
8573 .await
8574 .unwrap();
8575 Ok(Some(json!(null)))
8576 }
8577 }
8578 });
8579
8580 cx.executor().start_waiting();
8581 editor
8582 .update_in(cx, |editor, window, cx| {
8583 editor.perform_format(
8584 project.clone(),
8585 FormatTrigger::Manual,
8586 FormatTarget::Buffers,
8587 window,
8588 cx,
8589 )
8590 })
8591 .unwrap()
8592 .await;
8593 editor.update(cx, |editor, cx| {
8594 assert_eq!(
8595 editor.text(cx),
8596 r#"
8597 applied-code-action-2-edit
8598 applied-code-action-1-command
8599 applied-code-action-1-edit
8600 applied-formatting
8601 one
8602 two
8603 three
8604 "#
8605 .unindent()
8606 );
8607 });
8608
8609 editor.update_in(cx, |editor, window, cx| {
8610 editor.undo(&Default::default(), window, cx);
8611 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8612 });
8613
8614 // Perform a manual edit while waiting for an LSP command
8615 // that's being run as part of a formatting code action.
8616 let lock_guard = command_lock.lock().await;
8617 let format = editor
8618 .update_in(cx, |editor, window, cx| {
8619 editor.perform_format(
8620 project.clone(),
8621 FormatTrigger::Manual,
8622 FormatTarget::Buffers,
8623 window,
8624 cx,
8625 )
8626 })
8627 .unwrap();
8628 cx.run_until_parked();
8629 editor.update(cx, |editor, cx| {
8630 assert_eq!(
8631 editor.text(cx),
8632 r#"
8633 applied-code-action-1-edit
8634 applied-formatting
8635 one
8636 two
8637 three
8638 "#
8639 .unindent()
8640 );
8641
8642 editor.buffer.update(cx, |buffer, cx| {
8643 let ix = buffer.len(cx);
8644 buffer.edit([(ix..ix, "edited\n")], None, cx);
8645 });
8646 });
8647
8648 // Allow the LSP command to proceed. Because the buffer was edited,
8649 // the second code action will not be run.
8650 drop(lock_guard);
8651 format.await;
8652 editor.update_in(cx, |editor, window, cx| {
8653 assert_eq!(
8654 editor.text(cx),
8655 r#"
8656 applied-code-action-1-command
8657 applied-code-action-1-edit
8658 applied-formatting
8659 one
8660 two
8661 three
8662 edited
8663 "#
8664 .unindent()
8665 );
8666
8667 // The manual edit is undone first, because it is the last thing the user did
8668 // (even though the command completed afterwards).
8669 editor.undo(&Default::default(), window, cx);
8670 assert_eq!(
8671 editor.text(cx),
8672 r#"
8673 applied-code-action-1-command
8674 applied-code-action-1-edit
8675 applied-formatting
8676 one
8677 two
8678 three
8679 "#
8680 .unindent()
8681 );
8682
8683 // All the formatting (including the command, which completed after the manual edit)
8684 // is undone together.
8685 editor.undo(&Default::default(), window, cx);
8686 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8687 });
8688}
8689
8690#[gpui::test]
8691async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8692 init_test(cx, |settings| {
8693 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8694 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8695 ))
8696 });
8697
8698 let fs = FakeFs::new(cx.executor());
8699 fs.insert_file(path!("/file.ts"), Default::default()).await;
8700
8701 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8702
8703 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8704 language_registry.add(Arc::new(Language::new(
8705 LanguageConfig {
8706 name: "TypeScript".into(),
8707 matcher: LanguageMatcher {
8708 path_suffixes: vec!["ts".to_string()],
8709 ..Default::default()
8710 },
8711 ..LanguageConfig::default()
8712 },
8713 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8714 )));
8715 update_test_language_settings(cx, |settings| {
8716 settings.defaults.prettier = Some(PrettierSettings {
8717 allowed: true,
8718 ..PrettierSettings::default()
8719 });
8720 });
8721 let mut fake_servers = language_registry.register_fake_lsp(
8722 "TypeScript",
8723 FakeLspAdapter {
8724 capabilities: lsp::ServerCapabilities {
8725 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8726 ..Default::default()
8727 },
8728 ..Default::default()
8729 },
8730 );
8731
8732 let buffer = project
8733 .update(cx, |project, cx| {
8734 project.open_local_buffer(path!("/file.ts"), cx)
8735 })
8736 .await
8737 .unwrap();
8738
8739 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8740 let (editor, cx) = cx.add_window_view(|window, cx| {
8741 build_editor_with_project(project.clone(), buffer, window, cx)
8742 });
8743 editor.update_in(cx, |editor, window, cx| {
8744 editor.set_text(
8745 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8746 window,
8747 cx,
8748 )
8749 });
8750
8751 cx.executor().start_waiting();
8752 let fake_server = fake_servers.next().await.unwrap();
8753
8754 let format = editor
8755 .update_in(cx, |editor, window, cx| {
8756 editor.perform_code_action_kind(
8757 project.clone(),
8758 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8759 window,
8760 cx,
8761 )
8762 })
8763 .unwrap();
8764 fake_server
8765 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8766 assert_eq!(
8767 params.text_document.uri,
8768 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8769 );
8770 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8771 lsp::CodeAction {
8772 title: "Organize Imports".to_string(),
8773 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8774 edit: Some(lsp::WorkspaceEdit {
8775 changes: Some(
8776 [(
8777 params.text_document.uri.clone(),
8778 vec![lsp::TextEdit::new(
8779 lsp::Range::new(
8780 lsp::Position::new(1, 0),
8781 lsp::Position::new(2, 0),
8782 ),
8783 "".to_string(),
8784 )],
8785 )]
8786 .into_iter()
8787 .collect(),
8788 ),
8789 ..Default::default()
8790 }),
8791 ..Default::default()
8792 },
8793 )]))
8794 })
8795 .next()
8796 .await;
8797 cx.executor().start_waiting();
8798 format.await;
8799 assert_eq!(
8800 editor.update(cx, |editor, cx| editor.text(cx)),
8801 "import { a } from 'module';\n\nconst x = a;\n"
8802 );
8803
8804 editor.update_in(cx, |editor, window, cx| {
8805 editor.set_text(
8806 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8807 window,
8808 cx,
8809 )
8810 });
8811 // Ensure we don't lock if code action hangs.
8812 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8813 move |params, _| async move {
8814 assert_eq!(
8815 params.text_document.uri,
8816 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8817 );
8818 futures::future::pending::<()>().await;
8819 unreachable!()
8820 },
8821 );
8822 let format = editor
8823 .update_in(cx, |editor, window, cx| {
8824 editor.perform_code_action_kind(
8825 project,
8826 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8827 window,
8828 cx,
8829 )
8830 })
8831 .unwrap();
8832 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8833 cx.executor().start_waiting();
8834 format.await;
8835 assert_eq!(
8836 editor.update(cx, |editor, cx| editor.text(cx)),
8837 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8838 );
8839}
8840
8841#[gpui::test]
8842async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8843 init_test(cx, |_| {});
8844
8845 let mut cx = EditorLspTestContext::new_rust(
8846 lsp::ServerCapabilities {
8847 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8848 ..Default::default()
8849 },
8850 cx,
8851 )
8852 .await;
8853
8854 cx.set_state(indoc! {"
8855 one.twoˇ
8856 "});
8857
8858 // The format request takes a long time. When it completes, it inserts
8859 // a newline and an indent before the `.`
8860 cx.lsp
8861 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8862 let executor = cx.background_executor().clone();
8863 async move {
8864 executor.timer(Duration::from_millis(100)).await;
8865 Ok(Some(vec![lsp::TextEdit {
8866 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8867 new_text: "\n ".into(),
8868 }]))
8869 }
8870 });
8871
8872 // Submit a format request.
8873 let format_1 = cx
8874 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8875 .unwrap();
8876 cx.executor().run_until_parked();
8877
8878 // Submit a second format request.
8879 let format_2 = cx
8880 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8881 .unwrap();
8882 cx.executor().run_until_parked();
8883
8884 // Wait for both format requests to complete
8885 cx.executor().advance_clock(Duration::from_millis(200));
8886 cx.executor().start_waiting();
8887 format_1.await.unwrap();
8888 cx.executor().start_waiting();
8889 format_2.await.unwrap();
8890
8891 // The formatting edits only happens once.
8892 cx.assert_editor_state(indoc! {"
8893 one
8894 .twoˇ
8895 "});
8896}
8897
8898#[gpui::test]
8899async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8900 init_test(cx, |settings| {
8901 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8902 });
8903
8904 let mut cx = EditorLspTestContext::new_rust(
8905 lsp::ServerCapabilities {
8906 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8907 ..Default::default()
8908 },
8909 cx,
8910 )
8911 .await;
8912
8913 // Set up a buffer white some trailing whitespace and no trailing newline.
8914 cx.set_state(
8915 &[
8916 "one ", //
8917 "twoˇ", //
8918 "three ", //
8919 "four", //
8920 ]
8921 .join("\n"),
8922 );
8923
8924 // Submit a format request.
8925 let format = cx
8926 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8927 .unwrap();
8928
8929 // Record which buffer changes have been sent to the language server
8930 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8931 cx.lsp
8932 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8933 let buffer_changes = buffer_changes.clone();
8934 move |params, _| {
8935 buffer_changes.lock().extend(
8936 params
8937 .content_changes
8938 .into_iter()
8939 .map(|e| (e.range.unwrap(), e.text)),
8940 );
8941 }
8942 });
8943
8944 // Handle formatting requests to the language server.
8945 cx.lsp
8946 .set_request_handler::<lsp::request::Formatting, _, _>({
8947 let buffer_changes = buffer_changes.clone();
8948 move |_, _| {
8949 // When formatting is requested, trailing whitespace has already been stripped,
8950 // and the trailing newline has already been added.
8951 assert_eq!(
8952 &buffer_changes.lock()[1..],
8953 &[
8954 (
8955 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8956 "".into()
8957 ),
8958 (
8959 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8960 "".into()
8961 ),
8962 (
8963 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8964 "\n".into()
8965 ),
8966 ]
8967 );
8968
8969 // Insert blank lines between each line of the buffer.
8970 async move {
8971 Ok(Some(vec![
8972 lsp::TextEdit {
8973 range: lsp::Range::new(
8974 lsp::Position::new(1, 0),
8975 lsp::Position::new(1, 0),
8976 ),
8977 new_text: "\n".into(),
8978 },
8979 lsp::TextEdit {
8980 range: lsp::Range::new(
8981 lsp::Position::new(2, 0),
8982 lsp::Position::new(2, 0),
8983 ),
8984 new_text: "\n".into(),
8985 },
8986 ]))
8987 }
8988 }
8989 });
8990
8991 // After formatting the buffer, the trailing whitespace is stripped,
8992 // a newline is appended, and the edits provided by the language server
8993 // have been applied.
8994 format.await.unwrap();
8995 cx.assert_editor_state(
8996 &[
8997 "one", //
8998 "", //
8999 "twoˇ", //
9000 "", //
9001 "three", //
9002 "four", //
9003 "", //
9004 ]
9005 .join("\n"),
9006 );
9007
9008 // Undoing the formatting undoes the trailing whitespace removal, the
9009 // trailing newline, and the LSP edits.
9010 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9011 cx.assert_editor_state(
9012 &[
9013 "one ", //
9014 "twoˇ", //
9015 "three ", //
9016 "four", //
9017 ]
9018 .join("\n"),
9019 );
9020}
9021
9022#[gpui::test]
9023async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9024 cx: &mut TestAppContext,
9025) {
9026 init_test(cx, |_| {});
9027
9028 cx.update(|cx| {
9029 cx.update_global::<SettingsStore, _>(|settings, cx| {
9030 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9031 settings.auto_signature_help = Some(true);
9032 });
9033 });
9034 });
9035
9036 let mut cx = EditorLspTestContext::new_rust(
9037 lsp::ServerCapabilities {
9038 signature_help_provider: Some(lsp::SignatureHelpOptions {
9039 ..Default::default()
9040 }),
9041 ..Default::default()
9042 },
9043 cx,
9044 )
9045 .await;
9046
9047 let language = Language::new(
9048 LanguageConfig {
9049 name: "Rust".into(),
9050 brackets: BracketPairConfig {
9051 pairs: vec![
9052 BracketPair {
9053 start: "{".to_string(),
9054 end: "}".to_string(),
9055 close: true,
9056 surround: true,
9057 newline: true,
9058 },
9059 BracketPair {
9060 start: "(".to_string(),
9061 end: ")".to_string(),
9062 close: true,
9063 surround: true,
9064 newline: true,
9065 },
9066 BracketPair {
9067 start: "/*".to_string(),
9068 end: " */".to_string(),
9069 close: true,
9070 surround: true,
9071 newline: true,
9072 },
9073 BracketPair {
9074 start: "[".to_string(),
9075 end: "]".to_string(),
9076 close: false,
9077 surround: false,
9078 newline: true,
9079 },
9080 BracketPair {
9081 start: "\"".to_string(),
9082 end: "\"".to_string(),
9083 close: true,
9084 surround: true,
9085 newline: false,
9086 },
9087 BracketPair {
9088 start: "<".to_string(),
9089 end: ">".to_string(),
9090 close: false,
9091 surround: true,
9092 newline: true,
9093 },
9094 ],
9095 ..Default::default()
9096 },
9097 autoclose_before: "})]".to_string(),
9098 ..Default::default()
9099 },
9100 Some(tree_sitter_rust::LANGUAGE.into()),
9101 );
9102 let language = Arc::new(language);
9103
9104 cx.language_registry().add(language.clone());
9105 cx.update_buffer(|buffer, cx| {
9106 buffer.set_language(Some(language), cx);
9107 });
9108
9109 cx.set_state(
9110 &r#"
9111 fn main() {
9112 sampleˇ
9113 }
9114 "#
9115 .unindent(),
9116 );
9117
9118 cx.update_editor(|editor, window, cx| {
9119 editor.handle_input("(", window, cx);
9120 });
9121 cx.assert_editor_state(
9122 &"
9123 fn main() {
9124 sample(ˇ)
9125 }
9126 "
9127 .unindent(),
9128 );
9129
9130 let mocked_response = lsp::SignatureHelp {
9131 signatures: vec![lsp::SignatureInformation {
9132 label: "fn sample(param1: u8, param2: u8)".to_string(),
9133 documentation: None,
9134 parameters: Some(vec![
9135 lsp::ParameterInformation {
9136 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9137 documentation: None,
9138 },
9139 lsp::ParameterInformation {
9140 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9141 documentation: None,
9142 },
9143 ]),
9144 active_parameter: None,
9145 }],
9146 active_signature: Some(0),
9147 active_parameter: Some(0),
9148 };
9149 handle_signature_help_request(&mut cx, mocked_response).await;
9150
9151 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9152 .await;
9153
9154 cx.editor(|editor, _, _| {
9155 let signature_help_state = editor.signature_help_state.popover().cloned();
9156 assert_eq!(
9157 signature_help_state.unwrap().label,
9158 "param1: u8, param2: u8"
9159 );
9160 });
9161}
9162
9163#[gpui::test]
9164async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9165 init_test(cx, |_| {});
9166
9167 cx.update(|cx| {
9168 cx.update_global::<SettingsStore, _>(|settings, cx| {
9169 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9170 settings.auto_signature_help = Some(false);
9171 settings.show_signature_help_after_edits = Some(false);
9172 });
9173 });
9174 });
9175
9176 let mut cx = EditorLspTestContext::new_rust(
9177 lsp::ServerCapabilities {
9178 signature_help_provider: Some(lsp::SignatureHelpOptions {
9179 ..Default::default()
9180 }),
9181 ..Default::default()
9182 },
9183 cx,
9184 )
9185 .await;
9186
9187 let language = Language::new(
9188 LanguageConfig {
9189 name: "Rust".into(),
9190 brackets: BracketPairConfig {
9191 pairs: vec![
9192 BracketPair {
9193 start: "{".to_string(),
9194 end: "}".to_string(),
9195 close: true,
9196 surround: true,
9197 newline: true,
9198 },
9199 BracketPair {
9200 start: "(".to_string(),
9201 end: ")".to_string(),
9202 close: true,
9203 surround: true,
9204 newline: true,
9205 },
9206 BracketPair {
9207 start: "/*".to_string(),
9208 end: " */".to_string(),
9209 close: true,
9210 surround: true,
9211 newline: true,
9212 },
9213 BracketPair {
9214 start: "[".to_string(),
9215 end: "]".to_string(),
9216 close: false,
9217 surround: false,
9218 newline: true,
9219 },
9220 BracketPair {
9221 start: "\"".to_string(),
9222 end: "\"".to_string(),
9223 close: true,
9224 surround: true,
9225 newline: false,
9226 },
9227 BracketPair {
9228 start: "<".to_string(),
9229 end: ">".to_string(),
9230 close: false,
9231 surround: true,
9232 newline: true,
9233 },
9234 ],
9235 ..Default::default()
9236 },
9237 autoclose_before: "})]".to_string(),
9238 ..Default::default()
9239 },
9240 Some(tree_sitter_rust::LANGUAGE.into()),
9241 );
9242 let language = Arc::new(language);
9243
9244 cx.language_registry().add(language.clone());
9245 cx.update_buffer(|buffer, cx| {
9246 buffer.set_language(Some(language), cx);
9247 });
9248
9249 // Ensure that signature_help is not called when no signature help is enabled.
9250 cx.set_state(
9251 &r#"
9252 fn main() {
9253 sampleˇ
9254 }
9255 "#
9256 .unindent(),
9257 );
9258 cx.update_editor(|editor, window, cx| {
9259 editor.handle_input("(", window, cx);
9260 });
9261 cx.assert_editor_state(
9262 &"
9263 fn main() {
9264 sample(ˇ)
9265 }
9266 "
9267 .unindent(),
9268 );
9269 cx.editor(|editor, _, _| {
9270 assert!(editor.signature_help_state.task().is_none());
9271 });
9272
9273 let mocked_response = lsp::SignatureHelp {
9274 signatures: vec![lsp::SignatureInformation {
9275 label: "fn sample(param1: u8, param2: u8)".to_string(),
9276 documentation: None,
9277 parameters: Some(vec![
9278 lsp::ParameterInformation {
9279 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9280 documentation: None,
9281 },
9282 lsp::ParameterInformation {
9283 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9284 documentation: None,
9285 },
9286 ]),
9287 active_parameter: None,
9288 }],
9289 active_signature: Some(0),
9290 active_parameter: Some(0),
9291 };
9292
9293 // Ensure that signature_help is called when enabled afte edits
9294 cx.update(|_, cx| {
9295 cx.update_global::<SettingsStore, _>(|settings, cx| {
9296 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9297 settings.auto_signature_help = Some(false);
9298 settings.show_signature_help_after_edits = Some(true);
9299 });
9300 });
9301 });
9302 cx.set_state(
9303 &r#"
9304 fn main() {
9305 sampleˇ
9306 }
9307 "#
9308 .unindent(),
9309 );
9310 cx.update_editor(|editor, window, cx| {
9311 editor.handle_input("(", window, cx);
9312 });
9313 cx.assert_editor_state(
9314 &"
9315 fn main() {
9316 sample(ˇ)
9317 }
9318 "
9319 .unindent(),
9320 );
9321 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9322 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9323 .await;
9324 cx.update_editor(|editor, _, _| {
9325 let signature_help_state = editor.signature_help_state.popover().cloned();
9326 assert!(signature_help_state.is_some());
9327 assert_eq!(
9328 signature_help_state.unwrap().label,
9329 "param1: u8, param2: u8"
9330 );
9331 editor.signature_help_state = SignatureHelpState::default();
9332 });
9333
9334 // Ensure that signature_help is called when auto signature help override is enabled
9335 cx.update(|_, cx| {
9336 cx.update_global::<SettingsStore, _>(|settings, cx| {
9337 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9338 settings.auto_signature_help = Some(true);
9339 settings.show_signature_help_after_edits = Some(false);
9340 });
9341 });
9342 });
9343 cx.set_state(
9344 &r#"
9345 fn main() {
9346 sampleˇ
9347 }
9348 "#
9349 .unindent(),
9350 );
9351 cx.update_editor(|editor, window, cx| {
9352 editor.handle_input("(", window, cx);
9353 });
9354 cx.assert_editor_state(
9355 &"
9356 fn main() {
9357 sample(ˇ)
9358 }
9359 "
9360 .unindent(),
9361 );
9362 handle_signature_help_request(&mut cx, mocked_response).await;
9363 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9364 .await;
9365 cx.editor(|editor, _, _| {
9366 let signature_help_state = editor.signature_help_state.popover().cloned();
9367 assert!(signature_help_state.is_some());
9368 assert_eq!(
9369 signature_help_state.unwrap().label,
9370 "param1: u8, param2: u8"
9371 );
9372 });
9373}
9374
9375#[gpui::test]
9376async fn test_signature_help(cx: &mut TestAppContext) {
9377 init_test(cx, |_| {});
9378 cx.update(|cx| {
9379 cx.update_global::<SettingsStore, _>(|settings, cx| {
9380 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9381 settings.auto_signature_help = Some(true);
9382 });
9383 });
9384 });
9385
9386 let mut cx = EditorLspTestContext::new_rust(
9387 lsp::ServerCapabilities {
9388 signature_help_provider: Some(lsp::SignatureHelpOptions {
9389 ..Default::default()
9390 }),
9391 ..Default::default()
9392 },
9393 cx,
9394 )
9395 .await;
9396
9397 // A test that directly calls `show_signature_help`
9398 cx.update_editor(|editor, window, cx| {
9399 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9400 });
9401
9402 let mocked_response = lsp::SignatureHelp {
9403 signatures: vec![lsp::SignatureInformation {
9404 label: "fn sample(param1: u8, param2: u8)".to_string(),
9405 documentation: None,
9406 parameters: Some(vec![
9407 lsp::ParameterInformation {
9408 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9409 documentation: None,
9410 },
9411 lsp::ParameterInformation {
9412 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9413 documentation: None,
9414 },
9415 ]),
9416 active_parameter: None,
9417 }],
9418 active_signature: Some(0),
9419 active_parameter: Some(0),
9420 };
9421 handle_signature_help_request(&mut cx, mocked_response).await;
9422
9423 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9424 .await;
9425
9426 cx.editor(|editor, _, _| {
9427 let signature_help_state = editor.signature_help_state.popover().cloned();
9428 assert!(signature_help_state.is_some());
9429 assert_eq!(
9430 signature_help_state.unwrap().label,
9431 "param1: u8, param2: u8"
9432 );
9433 });
9434
9435 // When exiting outside from inside the brackets, `signature_help` is closed.
9436 cx.set_state(indoc! {"
9437 fn main() {
9438 sample(ˇ);
9439 }
9440
9441 fn sample(param1: u8, param2: u8) {}
9442 "});
9443
9444 cx.update_editor(|editor, window, cx| {
9445 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9446 });
9447
9448 let mocked_response = lsp::SignatureHelp {
9449 signatures: Vec::new(),
9450 active_signature: None,
9451 active_parameter: None,
9452 };
9453 handle_signature_help_request(&mut cx, mocked_response).await;
9454
9455 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9456 .await;
9457
9458 cx.editor(|editor, _, _| {
9459 assert!(!editor.signature_help_state.is_shown());
9460 });
9461
9462 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9463 cx.set_state(indoc! {"
9464 fn main() {
9465 sample(ˇ);
9466 }
9467
9468 fn sample(param1: u8, param2: u8) {}
9469 "});
9470
9471 let mocked_response = lsp::SignatureHelp {
9472 signatures: vec![lsp::SignatureInformation {
9473 label: "fn sample(param1: u8, param2: u8)".to_string(),
9474 documentation: None,
9475 parameters: Some(vec![
9476 lsp::ParameterInformation {
9477 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9478 documentation: None,
9479 },
9480 lsp::ParameterInformation {
9481 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9482 documentation: None,
9483 },
9484 ]),
9485 active_parameter: None,
9486 }],
9487 active_signature: Some(0),
9488 active_parameter: Some(0),
9489 };
9490 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9491 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9492 .await;
9493 cx.editor(|editor, _, _| {
9494 assert!(editor.signature_help_state.is_shown());
9495 });
9496
9497 // Restore the popover with more parameter input
9498 cx.set_state(indoc! {"
9499 fn main() {
9500 sample(param1, param2ˇ);
9501 }
9502
9503 fn sample(param1: u8, param2: u8) {}
9504 "});
9505
9506 let mocked_response = lsp::SignatureHelp {
9507 signatures: vec![lsp::SignatureInformation {
9508 label: "fn sample(param1: u8, param2: u8)".to_string(),
9509 documentation: None,
9510 parameters: Some(vec![
9511 lsp::ParameterInformation {
9512 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9513 documentation: None,
9514 },
9515 lsp::ParameterInformation {
9516 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9517 documentation: None,
9518 },
9519 ]),
9520 active_parameter: None,
9521 }],
9522 active_signature: Some(0),
9523 active_parameter: Some(1),
9524 };
9525 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9526 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9527 .await;
9528
9529 // When selecting a range, the popover is gone.
9530 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9531 cx.update_editor(|editor, window, cx| {
9532 editor.change_selections(None, window, cx, |s| {
9533 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9534 })
9535 });
9536 cx.assert_editor_state(indoc! {"
9537 fn main() {
9538 sample(param1, «ˇparam2»);
9539 }
9540
9541 fn sample(param1: u8, param2: u8) {}
9542 "});
9543 cx.editor(|editor, _, _| {
9544 assert!(!editor.signature_help_state.is_shown());
9545 });
9546
9547 // When unselecting again, the popover is back if within the brackets.
9548 cx.update_editor(|editor, window, cx| {
9549 editor.change_selections(None, window, cx, |s| {
9550 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9551 })
9552 });
9553 cx.assert_editor_state(indoc! {"
9554 fn main() {
9555 sample(param1, ˇparam2);
9556 }
9557
9558 fn sample(param1: u8, param2: u8) {}
9559 "});
9560 handle_signature_help_request(&mut cx, mocked_response).await;
9561 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9562 .await;
9563 cx.editor(|editor, _, _| {
9564 assert!(editor.signature_help_state.is_shown());
9565 });
9566
9567 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9568 cx.update_editor(|editor, window, cx| {
9569 editor.change_selections(None, window, cx, |s| {
9570 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9571 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9572 })
9573 });
9574 cx.assert_editor_state(indoc! {"
9575 fn main() {
9576 sample(param1, ˇparam2);
9577 }
9578
9579 fn sample(param1: u8, param2: u8) {}
9580 "});
9581
9582 let mocked_response = lsp::SignatureHelp {
9583 signatures: vec![lsp::SignatureInformation {
9584 label: "fn sample(param1: u8, param2: u8)".to_string(),
9585 documentation: None,
9586 parameters: Some(vec![
9587 lsp::ParameterInformation {
9588 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9589 documentation: None,
9590 },
9591 lsp::ParameterInformation {
9592 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9593 documentation: None,
9594 },
9595 ]),
9596 active_parameter: None,
9597 }],
9598 active_signature: Some(0),
9599 active_parameter: Some(1),
9600 };
9601 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9602 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9603 .await;
9604 cx.update_editor(|editor, _, cx| {
9605 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9606 });
9607 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9608 .await;
9609 cx.update_editor(|editor, window, cx| {
9610 editor.change_selections(None, window, cx, |s| {
9611 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9612 })
9613 });
9614 cx.assert_editor_state(indoc! {"
9615 fn main() {
9616 sample(param1, «ˇparam2»);
9617 }
9618
9619 fn sample(param1: u8, param2: u8) {}
9620 "});
9621 cx.update_editor(|editor, window, cx| {
9622 editor.change_selections(None, window, cx, |s| {
9623 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9624 })
9625 });
9626 cx.assert_editor_state(indoc! {"
9627 fn main() {
9628 sample(param1, ˇparam2);
9629 }
9630
9631 fn sample(param1: u8, param2: u8) {}
9632 "});
9633 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9634 .await;
9635}
9636
9637#[gpui::test]
9638async fn test_completion_mode(cx: &mut TestAppContext) {
9639 init_test(cx, |_| {});
9640 let mut cx = EditorLspTestContext::new_rust(
9641 lsp::ServerCapabilities {
9642 completion_provider: Some(lsp::CompletionOptions {
9643 resolve_provider: Some(true),
9644 ..Default::default()
9645 }),
9646 ..Default::default()
9647 },
9648 cx,
9649 )
9650 .await;
9651
9652 struct Run {
9653 run_description: &'static str,
9654 initial_state: String,
9655 buffer_marked_text: String,
9656 completion_text: &'static str,
9657 expected_with_insert_mode: String,
9658 expected_with_replace_mode: String,
9659 expected_with_replace_subsequence_mode: String,
9660 expected_with_replace_suffix_mode: String,
9661 }
9662
9663 let runs = [
9664 Run {
9665 run_description: "Start of word matches completion text",
9666 initial_state: "before ediˇ after".into(),
9667 buffer_marked_text: "before <edi|> after".into(),
9668 completion_text: "editor",
9669 expected_with_insert_mode: "before editorˇ after".into(),
9670 expected_with_replace_mode: "before editorˇ after".into(),
9671 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9672 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9673 },
9674 Run {
9675 run_description: "Accept same text at the middle of the word",
9676 initial_state: "before ediˇtor after".into(),
9677 buffer_marked_text: "before <edi|tor> after".into(),
9678 completion_text: "editor",
9679 expected_with_insert_mode: "before editorˇtor after".into(),
9680 expected_with_replace_mode: "before editorˇ after".into(),
9681 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9682 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9683 },
9684 Run {
9685 run_description: "End of word matches completion text -- cursor at end",
9686 initial_state: "before torˇ after".into(),
9687 buffer_marked_text: "before <tor|> after".into(),
9688 completion_text: "editor",
9689 expected_with_insert_mode: "before editorˇ after".into(),
9690 expected_with_replace_mode: "before editorˇ after".into(),
9691 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9692 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9693 },
9694 Run {
9695 run_description: "End of word matches completion text -- cursor at start",
9696 initial_state: "before ˇtor after".into(),
9697 buffer_marked_text: "before <|tor> after".into(),
9698 completion_text: "editor",
9699 expected_with_insert_mode: "before editorˇtor after".into(),
9700 expected_with_replace_mode: "before editorˇ after".into(),
9701 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9702 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9703 },
9704 Run {
9705 run_description: "Prepend text containing whitespace",
9706 initial_state: "pˇfield: bool".into(),
9707 buffer_marked_text: "<p|field>: bool".into(),
9708 completion_text: "pub ",
9709 expected_with_insert_mode: "pub ˇfield: bool".into(),
9710 expected_with_replace_mode: "pub ˇ: bool".into(),
9711 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9712 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9713 },
9714 Run {
9715 run_description: "Add element to start of list",
9716 initial_state: "[element_ˇelement_2]".into(),
9717 buffer_marked_text: "[<element_|element_2>]".into(),
9718 completion_text: "element_1",
9719 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9720 expected_with_replace_mode: "[element_1ˇ]".into(),
9721 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9722 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9723 },
9724 Run {
9725 run_description: "Add element to start of list -- first and second elements are equal",
9726 initial_state: "[elˇelement]".into(),
9727 buffer_marked_text: "[<el|element>]".into(),
9728 completion_text: "element",
9729 expected_with_insert_mode: "[elementˇelement]".into(),
9730 expected_with_replace_mode: "[elementˇ]".into(),
9731 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9732 expected_with_replace_suffix_mode: "[elementˇ]".into(),
9733 },
9734 Run {
9735 run_description: "Ends with matching suffix",
9736 initial_state: "SubˇError".into(),
9737 buffer_marked_text: "<Sub|Error>".into(),
9738 completion_text: "SubscriptionError",
9739 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
9740 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9741 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9742 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9743 },
9744 Run {
9745 run_description: "Suffix is a subsequence -- contiguous",
9746 initial_state: "SubˇErr".into(),
9747 buffer_marked_text: "<Sub|Err>".into(),
9748 completion_text: "SubscriptionError",
9749 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
9750 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9751 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9752 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9753 },
9754 Run {
9755 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9756 initial_state: "Suˇscrirr".into(),
9757 buffer_marked_text: "<Su|scrirr>".into(),
9758 completion_text: "SubscriptionError",
9759 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
9760 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9761 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9762 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9763 },
9764 Run {
9765 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9766 initial_state: "foo(indˇix)".into(),
9767 buffer_marked_text: "foo(<ind|ix>)".into(),
9768 completion_text: "node_index",
9769 expected_with_insert_mode: "foo(node_indexˇix)".into(),
9770 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9771 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9772 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9773 },
9774 ];
9775
9776 for run in runs {
9777 let run_variations = [
9778 (LspInsertMode::Insert, run.expected_with_insert_mode),
9779 (LspInsertMode::Replace, run.expected_with_replace_mode),
9780 (
9781 LspInsertMode::ReplaceSubsequence,
9782 run.expected_with_replace_subsequence_mode,
9783 ),
9784 (
9785 LspInsertMode::ReplaceSuffix,
9786 run.expected_with_replace_suffix_mode,
9787 ),
9788 ];
9789
9790 for (lsp_insert_mode, expected_text) in run_variations {
9791 eprintln!(
9792 "run = {:?}, mode = {lsp_insert_mode:.?}",
9793 run.run_description,
9794 );
9795
9796 update_test_language_settings(&mut cx, |settings| {
9797 settings.defaults.completions = Some(CompletionSettings {
9798 lsp_insert_mode,
9799 words: WordsCompletionMode::Disabled,
9800 lsp: true,
9801 lsp_fetch_timeout_ms: 0,
9802 });
9803 });
9804
9805 cx.set_state(&run.initial_state);
9806 cx.update_editor(|editor, window, cx| {
9807 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9808 });
9809
9810 let counter = Arc::new(AtomicUsize::new(0));
9811 handle_completion_request_with_insert_and_replace(
9812 &mut cx,
9813 &run.buffer_marked_text,
9814 vec![run.completion_text],
9815 counter.clone(),
9816 )
9817 .await;
9818 cx.condition(|editor, _| editor.context_menu_visible())
9819 .await;
9820 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9821
9822 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9823 editor
9824 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9825 .unwrap()
9826 });
9827 cx.assert_editor_state(&expected_text);
9828 handle_resolve_completion_request(&mut cx, None).await;
9829 apply_additional_edits.await.unwrap();
9830 }
9831 }
9832}
9833
9834#[gpui::test]
9835async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
9836 init_test(cx, |_| {});
9837 let mut cx = EditorLspTestContext::new_rust(
9838 lsp::ServerCapabilities {
9839 completion_provider: Some(lsp::CompletionOptions {
9840 resolve_provider: Some(true),
9841 ..Default::default()
9842 }),
9843 ..Default::default()
9844 },
9845 cx,
9846 )
9847 .await;
9848
9849 let initial_state = "SubˇError";
9850 let buffer_marked_text = "<Sub|Error>";
9851 let completion_text = "SubscriptionError";
9852 let expected_with_insert_mode = "SubscriptionErrorˇError";
9853 let expected_with_replace_mode = "SubscriptionErrorˇ";
9854
9855 update_test_language_settings(&mut cx, |settings| {
9856 settings.defaults.completions = Some(CompletionSettings {
9857 words: WordsCompletionMode::Disabled,
9858 // set the opposite here to ensure that the action is overriding the default behavior
9859 lsp_insert_mode: LspInsertMode::Insert,
9860 lsp: true,
9861 lsp_fetch_timeout_ms: 0,
9862 });
9863 });
9864
9865 cx.set_state(initial_state);
9866 cx.update_editor(|editor, window, cx| {
9867 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9868 });
9869
9870 let counter = Arc::new(AtomicUsize::new(0));
9871 handle_completion_request_with_insert_and_replace(
9872 &mut cx,
9873 &buffer_marked_text,
9874 vec![completion_text],
9875 counter.clone(),
9876 )
9877 .await;
9878 cx.condition(|editor, _| editor.context_menu_visible())
9879 .await;
9880 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9881
9882 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9883 editor
9884 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
9885 .unwrap()
9886 });
9887 cx.assert_editor_state(&expected_with_replace_mode);
9888 handle_resolve_completion_request(&mut cx, None).await;
9889 apply_additional_edits.await.unwrap();
9890
9891 update_test_language_settings(&mut cx, |settings| {
9892 settings.defaults.completions = Some(CompletionSettings {
9893 words: WordsCompletionMode::Disabled,
9894 // set the opposite here to ensure that the action is overriding the default behavior
9895 lsp_insert_mode: LspInsertMode::Replace,
9896 lsp: true,
9897 lsp_fetch_timeout_ms: 0,
9898 });
9899 });
9900
9901 cx.set_state(initial_state);
9902 cx.update_editor(|editor, window, cx| {
9903 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9904 });
9905 handle_completion_request_with_insert_and_replace(
9906 &mut cx,
9907 &buffer_marked_text,
9908 vec![completion_text],
9909 counter.clone(),
9910 )
9911 .await;
9912 cx.condition(|editor, _| editor.context_menu_visible())
9913 .await;
9914 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9915
9916 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9917 editor
9918 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
9919 .unwrap()
9920 });
9921 cx.assert_editor_state(&expected_with_insert_mode);
9922 handle_resolve_completion_request(&mut cx, None).await;
9923 apply_additional_edits.await.unwrap();
9924}
9925
9926#[gpui::test]
9927async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
9928 init_test(cx, |_| {});
9929 let mut cx = EditorLspTestContext::new_rust(
9930 lsp::ServerCapabilities {
9931 completion_provider: Some(lsp::CompletionOptions {
9932 resolve_provider: Some(true),
9933 ..Default::default()
9934 }),
9935 ..Default::default()
9936 },
9937 cx,
9938 )
9939 .await;
9940
9941 // scenario: surrounding text matches completion text
9942 let completion_text = "to_offset";
9943 let initial_state = indoc! {"
9944 1. buf.to_offˇsuffix
9945 2. buf.to_offˇsuf
9946 3. buf.to_offˇfix
9947 4. buf.to_offˇ
9948 5. into_offˇensive
9949 6. ˇsuffix
9950 7. let ˇ //
9951 8. aaˇzz
9952 9. buf.to_off«zzzzzˇ»suffix
9953 10. buf.«ˇzzzzz»suffix
9954 11. to_off«ˇzzzzz»
9955
9956 buf.to_offˇsuffix // newest cursor
9957 "};
9958 let completion_marked_buffer = indoc! {"
9959 1. buf.to_offsuffix
9960 2. buf.to_offsuf
9961 3. buf.to_offfix
9962 4. buf.to_off
9963 5. into_offensive
9964 6. suffix
9965 7. let //
9966 8. aazz
9967 9. buf.to_offzzzzzsuffix
9968 10. buf.zzzzzsuffix
9969 11. to_offzzzzz
9970
9971 buf.<to_off|suffix> // newest cursor
9972 "};
9973 let expected = indoc! {"
9974 1. buf.to_offsetˇ
9975 2. buf.to_offsetˇsuf
9976 3. buf.to_offsetˇfix
9977 4. buf.to_offsetˇ
9978 5. into_offsetˇensive
9979 6. to_offsetˇsuffix
9980 7. let to_offsetˇ //
9981 8. aato_offsetˇzz
9982 9. buf.to_offsetˇ
9983 10. buf.to_offsetˇsuffix
9984 11. to_offsetˇ
9985
9986 buf.to_offsetˇ // newest cursor
9987 "};
9988 cx.set_state(initial_state);
9989 cx.update_editor(|editor, window, cx| {
9990 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9991 });
9992 handle_completion_request_with_insert_and_replace(
9993 &mut cx,
9994 completion_marked_buffer,
9995 vec![completion_text],
9996 Arc::new(AtomicUsize::new(0)),
9997 )
9998 .await;
9999 cx.condition(|editor, _| editor.context_menu_visible())
10000 .await;
10001 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10002 editor
10003 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10004 .unwrap()
10005 });
10006 cx.assert_editor_state(expected);
10007 handle_resolve_completion_request(&mut cx, None).await;
10008 apply_additional_edits.await.unwrap();
10009
10010 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10011 let completion_text = "foo_and_bar";
10012 let initial_state = indoc! {"
10013 1. ooanbˇ
10014 2. zooanbˇ
10015 3. ooanbˇz
10016 4. zooanbˇz
10017 5. ooanˇ
10018 6. oanbˇ
10019
10020 ooanbˇ
10021 "};
10022 let completion_marked_buffer = indoc! {"
10023 1. ooanb
10024 2. zooanb
10025 3. ooanbz
10026 4. zooanbz
10027 5. ooan
10028 6. oanb
10029
10030 <ooanb|>
10031 "};
10032 let expected = indoc! {"
10033 1. foo_and_barˇ
10034 2. zfoo_and_barˇ
10035 3. foo_and_barˇz
10036 4. zfoo_and_barˇz
10037 5. ooanfoo_and_barˇ
10038 6. oanbfoo_and_barˇ
10039
10040 foo_and_barˇ
10041 "};
10042 cx.set_state(initial_state);
10043 cx.update_editor(|editor, window, cx| {
10044 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10045 });
10046 handle_completion_request_with_insert_and_replace(
10047 &mut cx,
10048 completion_marked_buffer,
10049 vec![completion_text],
10050 Arc::new(AtomicUsize::new(0)),
10051 )
10052 .await;
10053 cx.condition(|editor, _| editor.context_menu_visible())
10054 .await;
10055 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10056 editor
10057 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10058 .unwrap()
10059 });
10060 cx.assert_editor_state(expected);
10061 handle_resolve_completion_request(&mut cx, None).await;
10062 apply_additional_edits.await.unwrap();
10063
10064 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10065 // (expects the same as if it was inserted at the end)
10066 let completion_text = "foo_and_bar";
10067 let initial_state = indoc! {"
10068 1. ooˇanb
10069 2. zooˇanb
10070 3. ooˇanbz
10071 4. zooˇanbz
10072
10073 ooˇanb
10074 "};
10075 let completion_marked_buffer = indoc! {"
10076 1. ooanb
10077 2. zooanb
10078 3. ooanbz
10079 4. zooanbz
10080
10081 <oo|anb>
10082 "};
10083 let expected = indoc! {"
10084 1. foo_and_barˇ
10085 2. zfoo_and_barˇ
10086 3. foo_and_barˇz
10087 4. zfoo_and_barˇz
10088
10089 foo_and_barˇ
10090 "};
10091 cx.set_state(initial_state);
10092 cx.update_editor(|editor, window, cx| {
10093 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10094 });
10095 handle_completion_request_with_insert_and_replace(
10096 &mut cx,
10097 completion_marked_buffer,
10098 vec![completion_text],
10099 Arc::new(AtomicUsize::new(0)),
10100 )
10101 .await;
10102 cx.condition(|editor, _| editor.context_menu_visible())
10103 .await;
10104 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10105 editor
10106 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10107 .unwrap()
10108 });
10109 cx.assert_editor_state(expected);
10110 handle_resolve_completion_request(&mut cx, None).await;
10111 apply_additional_edits.await.unwrap();
10112}
10113
10114// This used to crash
10115#[gpui::test]
10116async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10117 init_test(cx, |_| {});
10118
10119 let buffer_text = indoc! {"
10120 fn main() {
10121 10.satu;
10122
10123 //
10124 // separate cursors so they open in different excerpts (manually reproducible)
10125 //
10126
10127 10.satu20;
10128 }
10129 "};
10130 let multibuffer_text_with_selections = indoc! {"
10131 fn main() {
10132 10.satuˇ;
10133
10134 //
10135
10136 //
10137
10138 10.satuˇ20;
10139 }
10140 "};
10141 let expected_multibuffer = indoc! {"
10142 fn main() {
10143 10.saturating_sub()ˇ;
10144
10145 //
10146
10147 //
10148
10149 10.saturating_sub()ˇ;
10150 }
10151 "};
10152
10153 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10154 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10155
10156 let fs = FakeFs::new(cx.executor());
10157 fs.insert_tree(
10158 path!("/a"),
10159 json!({
10160 "main.rs": buffer_text,
10161 }),
10162 )
10163 .await;
10164
10165 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10166 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10167 language_registry.add(rust_lang());
10168 let mut fake_servers = language_registry.register_fake_lsp(
10169 "Rust",
10170 FakeLspAdapter {
10171 capabilities: lsp::ServerCapabilities {
10172 completion_provider: Some(lsp::CompletionOptions {
10173 resolve_provider: None,
10174 ..lsp::CompletionOptions::default()
10175 }),
10176 ..lsp::ServerCapabilities::default()
10177 },
10178 ..FakeLspAdapter::default()
10179 },
10180 );
10181 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10182 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10183 let buffer = project
10184 .update(cx, |project, cx| {
10185 project.open_local_buffer(path!("/a/main.rs"), cx)
10186 })
10187 .await
10188 .unwrap();
10189
10190 let multi_buffer = cx.new(|cx| {
10191 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10192 multi_buffer.push_excerpts(
10193 buffer.clone(),
10194 [ExcerptRange::new(0..first_excerpt_end)],
10195 cx,
10196 );
10197 multi_buffer.push_excerpts(
10198 buffer.clone(),
10199 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10200 cx,
10201 );
10202 multi_buffer
10203 });
10204
10205 let editor = workspace
10206 .update(cx, |_, window, cx| {
10207 cx.new(|cx| {
10208 Editor::new(
10209 EditorMode::Full {
10210 scale_ui_elements_with_buffer_font_size: false,
10211 show_active_line_background: false,
10212 },
10213 multi_buffer.clone(),
10214 Some(project.clone()),
10215 window,
10216 cx,
10217 )
10218 })
10219 })
10220 .unwrap();
10221
10222 let pane = workspace
10223 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10224 .unwrap();
10225 pane.update_in(cx, |pane, window, cx| {
10226 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10227 });
10228
10229 let fake_server = fake_servers.next().await.unwrap();
10230
10231 editor.update_in(cx, |editor, window, cx| {
10232 editor.change_selections(None, window, cx, |s| {
10233 s.select_ranges([
10234 Point::new(1, 11)..Point::new(1, 11),
10235 Point::new(7, 11)..Point::new(7, 11),
10236 ])
10237 });
10238
10239 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10240 });
10241
10242 editor.update_in(cx, |editor, window, cx| {
10243 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10244 });
10245
10246 fake_server
10247 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10248 let completion_item = lsp::CompletionItem {
10249 label: "saturating_sub()".into(),
10250 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10251 lsp::InsertReplaceEdit {
10252 new_text: "saturating_sub()".to_owned(),
10253 insert: lsp::Range::new(
10254 lsp::Position::new(7, 7),
10255 lsp::Position::new(7, 11),
10256 ),
10257 replace: lsp::Range::new(
10258 lsp::Position::new(7, 7),
10259 lsp::Position::new(7, 13),
10260 ),
10261 },
10262 )),
10263 ..lsp::CompletionItem::default()
10264 };
10265
10266 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10267 })
10268 .next()
10269 .await
10270 .unwrap();
10271
10272 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10273 .await;
10274
10275 editor
10276 .update_in(cx, |editor, window, cx| {
10277 editor
10278 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10279 .unwrap()
10280 })
10281 .await
10282 .unwrap();
10283
10284 editor.update(cx, |editor, cx| {
10285 assert_text_with_selections(editor, expected_multibuffer, cx);
10286 })
10287}
10288
10289#[gpui::test]
10290async fn test_completion(cx: &mut TestAppContext) {
10291 init_test(cx, |_| {});
10292
10293 let mut cx = EditorLspTestContext::new_rust(
10294 lsp::ServerCapabilities {
10295 completion_provider: Some(lsp::CompletionOptions {
10296 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10297 resolve_provider: Some(true),
10298 ..Default::default()
10299 }),
10300 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10301 ..Default::default()
10302 },
10303 cx,
10304 )
10305 .await;
10306 let counter = Arc::new(AtomicUsize::new(0));
10307
10308 cx.set_state(indoc! {"
10309 oneˇ
10310 two
10311 three
10312 "});
10313 cx.simulate_keystroke(".");
10314 handle_completion_request(
10315 &mut cx,
10316 indoc! {"
10317 one.|<>
10318 two
10319 three
10320 "},
10321 vec!["first_completion", "second_completion"],
10322 counter.clone(),
10323 )
10324 .await;
10325 cx.condition(|editor, _| editor.context_menu_visible())
10326 .await;
10327 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10328
10329 let _handler = handle_signature_help_request(
10330 &mut cx,
10331 lsp::SignatureHelp {
10332 signatures: vec![lsp::SignatureInformation {
10333 label: "test signature".to_string(),
10334 documentation: None,
10335 parameters: Some(vec![lsp::ParameterInformation {
10336 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10337 documentation: None,
10338 }]),
10339 active_parameter: None,
10340 }],
10341 active_signature: None,
10342 active_parameter: None,
10343 },
10344 );
10345 cx.update_editor(|editor, window, cx| {
10346 assert!(
10347 !editor.signature_help_state.is_shown(),
10348 "No signature help was called for"
10349 );
10350 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10351 });
10352 cx.run_until_parked();
10353 cx.update_editor(|editor, _, _| {
10354 assert!(
10355 !editor.signature_help_state.is_shown(),
10356 "No signature help should be shown when completions menu is open"
10357 );
10358 });
10359
10360 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10361 editor.context_menu_next(&Default::default(), window, cx);
10362 editor
10363 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10364 .unwrap()
10365 });
10366 cx.assert_editor_state(indoc! {"
10367 one.second_completionˇ
10368 two
10369 three
10370 "});
10371
10372 handle_resolve_completion_request(
10373 &mut cx,
10374 Some(vec![
10375 (
10376 //This overlaps with the primary completion edit which is
10377 //misbehavior from the LSP spec, test that we filter it out
10378 indoc! {"
10379 one.second_ˇcompletion
10380 two
10381 threeˇ
10382 "},
10383 "overlapping additional edit",
10384 ),
10385 (
10386 indoc! {"
10387 one.second_completion
10388 two
10389 threeˇ
10390 "},
10391 "\nadditional edit",
10392 ),
10393 ]),
10394 )
10395 .await;
10396 apply_additional_edits.await.unwrap();
10397 cx.assert_editor_state(indoc! {"
10398 one.second_completionˇ
10399 two
10400 three
10401 additional edit
10402 "});
10403
10404 cx.set_state(indoc! {"
10405 one.second_completion
10406 twoˇ
10407 threeˇ
10408 additional edit
10409 "});
10410 cx.simulate_keystroke(" ");
10411 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10412 cx.simulate_keystroke("s");
10413 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10414
10415 cx.assert_editor_state(indoc! {"
10416 one.second_completion
10417 two sˇ
10418 three sˇ
10419 additional edit
10420 "});
10421 handle_completion_request(
10422 &mut cx,
10423 indoc! {"
10424 one.second_completion
10425 two s
10426 three <s|>
10427 additional edit
10428 "},
10429 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10430 counter.clone(),
10431 )
10432 .await;
10433 cx.condition(|editor, _| editor.context_menu_visible())
10434 .await;
10435 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10436
10437 cx.simulate_keystroke("i");
10438
10439 handle_completion_request(
10440 &mut cx,
10441 indoc! {"
10442 one.second_completion
10443 two si
10444 three <si|>
10445 additional edit
10446 "},
10447 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10448 counter.clone(),
10449 )
10450 .await;
10451 cx.condition(|editor, _| editor.context_menu_visible())
10452 .await;
10453 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10454
10455 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10456 editor
10457 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10458 .unwrap()
10459 });
10460 cx.assert_editor_state(indoc! {"
10461 one.second_completion
10462 two sixth_completionˇ
10463 three sixth_completionˇ
10464 additional edit
10465 "});
10466
10467 apply_additional_edits.await.unwrap();
10468
10469 update_test_language_settings(&mut cx, |settings| {
10470 settings.defaults.show_completions_on_input = Some(false);
10471 });
10472 cx.set_state("editorˇ");
10473 cx.simulate_keystroke(".");
10474 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10475 cx.simulate_keystrokes("c l o");
10476 cx.assert_editor_state("editor.cloˇ");
10477 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10478 cx.update_editor(|editor, window, cx| {
10479 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10480 });
10481 handle_completion_request(
10482 &mut cx,
10483 "editor.<clo|>",
10484 vec!["close", "clobber"],
10485 counter.clone(),
10486 )
10487 .await;
10488 cx.condition(|editor, _| editor.context_menu_visible())
10489 .await;
10490 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10491
10492 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10493 editor
10494 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10495 .unwrap()
10496 });
10497 cx.assert_editor_state("editor.closeˇ");
10498 handle_resolve_completion_request(&mut cx, None).await;
10499 apply_additional_edits.await.unwrap();
10500}
10501
10502#[gpui::test]
10503async fn test_word_completion(cx: &mut TestAppContext) {
10504 let lsp_fetch_timeout_ms = 10;
10505 init_test(cx, |language_settings| {
10506 language_settings.defaults.completions = Some(CompletionSettings {
10507 words: WordsCompletionMode::Fallback,
10508 lsp: true,
10509 lsp_fetch_timeout_ms: 10,
10510 lsp_insert_mode: LspInsertMode::Insert,
10511 });
10512 });
10513
10514 let mut cx = EditorLspTestContext::new_rust(
10515 lsp::ServerCapabilities {
10516 completion_provider: Some(lsp::CompletionOptions {
10517 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10518 ..lsp::CompletionOptions::default()
10519 }),
10520 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10521 ..lsp::ServerCapabilities::default()
10522 },
10523 cx,
10524 )
10525 .await;
10526
10527 let throttle_completions = Arc::new(AtomicBool::new(false));
10528
10529 let lsp_throttle_completions = throttle_completions.clone();
10530 let _completion_requests_handler =
10531 cx.lsp
10532 .server
10533 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10534 let lsp_throttle_completions = lsp_throttle_completions.clone();
10535 let cx = cx.clone();
10536 async move {
10537 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10538 cx.background_executor()
10539 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10540 .await;
10541 }
10542 Ok(Some(lsp::CompletionResponse::Array(vec![
10543 lsp::CompletionItem {
10544 label: "first".into(),
10545 ..lsp::CompletionItem::default()
10546 },
10547 lsp::CompletionItem {
10548 label: "last".into(),
10549 ..lsp::CompletionItem::default()
10550 },
10551 ])))
10552 }
10553 });
10554
10555 cx.set_state(indoc! {"
10556 oneˇ
10557 two
10558 three
10559 "});
10560 cx.simulate_keystroke(".");
10561 cx.executor().run_until_parked();
10562 cx.condition(|editor, _| editor.context_menu_visible())
10563 .await;
10564 cx.update_editor(|editor, window, cx| {
10565 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10566 {
10567 assert_eq!(
10568 completion_menu_entries(&menu),
10569 &["first", "last"],
10570 "When LSP server is fast to reply, no fallback word completions are used"
10571 );
10572 } else {
10573 panic!("expected completion menu to be open");
10574 }
10575 editor.cancel(&Cancel, window, cx);
10576 });
10577 cx.executor().run_until_parked();
10578 cx.condition(|editor, _| !editor.context_menu_visible())
10579 .await;
10580
10581 throttle_completions.store(true, atomic::Ordering::Release);
10582 cx.simulate_keystroke(".");
10583 cx.executor()
10584 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
10585 cx.executor().run_until_parked();
10586 cx.condition(|editor, _| editor.context_menu_visible())
10587 .await;
10588 cx.update_editor(|editor, _, _| {
10589 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10590 {
10591 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
10592 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10593 } else {
10594 panic!("expected completion menu to be open");
10595 }
10596 });
10597}
10598
10599#[gpui::test]
10600async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10601 init_test(cx, |language_settings| {
10602 language_settings.defaults.completions = Some(CompletionSettings {
10603 words: WordsCompletionMode::Enabled,
10604 lsp: true,
10605 lsp_fetch_timeout_ms: 0,
10606 lsp_insert_mode: LspInsertMode::Insert,
10607 });
10608 });
10609
10610 let mut cx = EditorLspTestContext::new_rust(
10611 lsp::ServerCapabilities {
10612 completion_provider: Some(lsp::CompletionOptions {
10613 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10614 ..lsp::CompletionOptions::default()
10615 }),
10616 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10617 ..lsp::ServerCapabilities::default()
10618 },
10619 cx,
10620 )
10621 .await;
10622
10623 let _completion_requests_handler =
10624 cx.lsp
10625 .server
10626 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10627 Ok(Some(lsp::CompletionResponse::Array(vec![
10628 lsp::CompletionItem {
10629 label: "first".into(),
10630 ..lsp::CompletionItem::default()
10631 },
10632 lsp::CompletionItem {
10633 label: "last".into(),
10634 ..lsp::CompletionItem::default()
10635 },
10636 ])))
10637 });
10638
10639 cx.set_state(indoc! {"ˇ
10640 first
10641 last
10642 second
10643 "});
10644 cx.simulate_keystroke(".");
10645 cx.executor().run_until_parked();
10646 cx.condition(|editor, _| editor.context_menu_visible())
10647 .await;
10648 cx.update_editor(|editor, _, _| {
10649 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10650 {
10651 assert_eq!(
10652 completion_menu_entries(&menu),
10653 &["first", "last", "second"],
10654 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10655 );
10656 } else {
10657 panic!("expected completion menu to be open");
10658 }
10659 });
10660}
10661
10662#[gpui::test]
10663async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10664 init_test(cx, |language_settings| {
10665 language_settings.defaults.completions = Some(CompletionSettings {
10666 words: WordsCompletionMode::Disabled,
10667 lsp: true,
10668 lsp_fetch_timeout_ms: 0,
10669 lsp_insert_mode: LspInsertMode::Insert,
10670 });
10671 });
10672
10673 let mut cx = EditorLspTestContext::new_rust(
10674 lsp::ServerCapabilities {
10675 completion_provider: Some(lsp::CompletionOptions {
10676 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10677 ..lsp::CompletionOptions::default()
10678 }),
10679 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10680 ..lsp::ServerCapabilities::default()
10681 },
10682 cx,
10683 )
10684 .await;
10685
10686 let _completion_requests_handler =
10687 cx.lsp
10688 .server
10689 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10690 panic!("LSP completions should not be queried when dealing with word completions")
10691 });
10692
10693 cx.set_state(indoc! {"ˇ
10694 first
10695 last
10696 second
10697 "});
10698 cx.update_editor(|editor, window, cx| {
10699 editor.show_word_completions(&ShowWordCompletions, window, cx);
10700 });
10701 cx.executor().run_until_parked();
10702 cx.condition(|editor, _| editor.context_menu_visible())
10703 .await;
10704 cx.update_editor(|editor, _, _| {
10705 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10706 {
10707 assert_eq!(
10708 completion_menu_entries(&menu),
10709 &["first", "last", "second"],
10710 "`ShowWordCompletions` action should show word completions"
10711 );
10712 } else {
10713 panic!("expected completion menu to be open");
10714 }
10715 });
10716
10717 cx.simulate_keystroke("l");
10718 cx.executor().run_until_parked();
10719 cx.condition(|editor, _| editor.context_menu_visible())
10720 .await;
10721 cx.update_editor(|editor, _, _| {
10722 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10723 {
10724 assert_eq!(
10725 completion_menu_entries(&menu),
10726 &["last"],
10727 "After showing word completions, further editing should filter them and not query the LSP"
10728 );
10729 } else {
10730 panic!("expected completion menu to be open");
10731 }
10732 });
10733}
10734
10735#[gpui::test]
10736async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
10737 init_test(cx, |language_settings| {
10738 language_settings.defaults.completions = Some(CompletionSettings {
10739 words: WordsCompletionMode::Fallback,
10740 lsp: false,
10741 lsp_fetch_timeout_ms: 0,
10742 lsp_insert_mode: LspInsertMode::Insert,
10743 });
10744 });
10745
10746 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
10747
10748 cx.set_state(indoc! {"ˇ
10749 0_usize
10750 let
10751 33
10752 4.5f32
10753 "});
10754 cx.update_editor(|editor, window, cx| {
10755 editor.show_completions(&ShowCompletions::default(), window, cx);
10756 });
10757 cx.executor().run_until_parked();
10758 cx.condition(|editor, _| editor.context_menu_visible())
10759 .await;
10760 cx.update_editor(|editor, window, cx| {
10761 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10762 {
10763 assert_eq!(
10764 completion_menu_entries(&menu),
10765 &["let"],
10766 "With no digits in the completion query, no digits should be in the word completions"
10767 );
10768 } else {
10769 panic!("expected completion menu to be open");
10770 }
10771 editor.cancel(&Cancel, window, cx);
10772 });
10773
10774 cx.set_state(indoc! {"3ˇ
10775 0_usize
10776 let
10777 3
10778 33.35f32
10779 "});
10780 cx.update_editor(|editor, window, cx| {
10781 editor.show_completions(&ShowCompletions::default(), window, cx);
10782 });
10783 cx.executor().run_until_parked();
10784 cx.condition(|editor, _| editor.context_menu_visible())
10785 .await;
10786 cx.update_editor(|editor, _, _| {
10787 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10788 {
10789 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
10790 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
10791 } else {
10792 panic!("expected completion menu to be open");
10793 }
10794 });
10795}
10796
10797fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
10798 let position = || lsp::Position {
10799 line: params.text_document_position.position.line,
10800 character: params.text_document_position.position.character,
10801 };
10802 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10803 range: lsp::Range {
10804 start: position(),
10805 end: position(),
10806 },
10807 new_text: text.to_string(),
10808 }))
10809}
10810
10811#[gpui::test]
10812async fn test_multiline_completion(cx: &mut TestAppContext) {
10813 init_test(cx, |_| {});
10814
10815 let fs = FakeFs::new(cx.executor());
10816 fs.insert_tree(
10817 path!("/a"),
10818 json!({
10819 "main.ts": "a",
10820 }),
10821 )
10822 .await;
10823
10824 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10825 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10826 let typescript_language = Arc::new(Language::new(
10827 LanguageConfig {
10828 name: "TypeScript".into(),
10829 matcher: LanguageMatcher {
10830 path_suffixes: vec!["ts".to_string()],
10831 ..LanguageMatcher::default()
10832 },
10833 line_comments: vec!["// ".into()],
10834 ..LanguageConfig::default()
10835 },
10836 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10837 ));
10838 language_registry.add(typescript_language.clone());
10839 let mut fake_servers = language_registry.register_fake_lsp(
10840 "TypeScript",
10841 FakeLspAdapter {
10842 capabilities: lsp::ServerCapabilities {
10843 completion_provider: Some(lsp::CompletionOptions {
10844 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10845 ..lsp::CompletionOptions::default()
10846 }),
10847 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10848 ..lsp::ServerCapabilities::default()
10849 },
10850 // Emulate vtsls label generation
10851 label_for_completion: Some(Box::new(|item, _| {
10852 let text = if let Some(description) = item
10853 .label_details
10854 .as_ref()
10855 .and_then(|label_details| label_details.description.as_ref())
10856 {
10857 format!("{} {}", item.label, description)
10858 } else if let Some(detail) = &item.detail {
10859 format!("{} {}", item.label, detail)
10860 } else {
10861 item.label.clone()
10862 };
10863 let len = text.len();
10864 Some(language::CodeLabel {
10865 text,
10866 runs: Vec::new(),
10867 filter_range: 0..len,
10868 })
10869 })),
10870 ..FakeLspAdapter::default()
10871 },
10872 );
10873 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10874 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10875 let worktree_id = workspace
10876 .update(cx, |workspace, _window, cx| {
10877 workspace.project().update(cx, |project, cx| {
10878 project.worktrees(cx).next().unwrap().read(cx).id()
10879 })
10880 })
10881 .unwrap();
10882 let _buffer = project
10883 .update(cx, |project, cx| {
10884 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
10885 })
10886 .await
10887 .unwrap();
10888 let editor = workspace
10889 .update(cx, |workspace, window, cx| {
10890 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10891 })
10892 .unwrap()
10893 .await
10894 .unwrap()
10895 .downcast::<Editor>()
10896 .unwrap();
10897 let fake_server = fake_servers.next().await.unwrap();
10898
10899 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10900 let multiline_label_2 = "a\nb\nc\n";
10901 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10902 let multiline_description = "d\ne\nf\n";
10903 let multiline_detail_2 = "g\nh\ni\n";
10904
10905 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10906 move |params, _| async move {
10907 Ok(Some(lsp::CompletionResponse::Array(vec![
10908 lsp::CompletionItem {
10909 label: multiline_label.to_string(),
10910 text_edit: gen_text_edit(¶ms, "new_text_1"),
10911 ..lsp::CompletionItem::default()
10912 },
10913 lsp::CompletionItem {
10914 label: "single line label 1".to_string(),
10915 detail: Some(multiline_detail.to_string()),
10916 text_edit: gen_text_edit(¶ms, "new_text_2"),
10917 ..lsp::CompletionItem::default()
10918 },
10919 lsp::CompletionItem {
10920 label: "single line label 2".to_string(),
10921 label_details: Some(lsp::CompletionItemLabelDetails {
10922 description: Some(multiline_description.to_string()),
10923 detail: None,
10924 }),
10925 text_edit: gen_text_edit(¶ms, "new_text_2"),
10926 ..lsp::CompletionItem::default()
10927 },
10928 lsp::CompletionItem {
10929 label: multiline_label_2.to_string(),
10930 detail: Some(multiline_detail_2.to_string()),
10931 text_edit: gen_text_edit(¶ms, "new_text_3"),
10932 ..lsp::CompletionItem::default()
10933 },
10934 lsp::CompletionItem {
10935 label: "Label with many spaces and \t but without newlines".to_string(),
10936 detail: Some(
10937 "Details with many spaces and \t but without newlines".to_string(),
10938 ),
10939 text_edit: gen_text_edit(¶ms, "new_text_4"),
10940 ..lsp::CompletionItem::default()
10941 },
10942 ])))
10943 },
10944 );
10945
10946 editor.update_in(cx, |editor, window, cx| {
10947 cx.focus_self(window);
10948 editor.move_to_end(&MoveToEnd, window, cx);
10949 editor.handle_input(".", window, cx);
10950 });
10951 cx.run_until_parked();
10952 completion_handle.next().await.unwrap();
10953
10954 editor.update(cx, |editor, _| {
10955 assert!(editor.context_menu_visible());
10956 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10957 {
10958 let completion_labels = menu
10959 .completions
10960 .borrow()
10961 .iter()
10962 .map(|c| c.label.text.clone())
10963 .collect::<Vec<_>>();
10964 assert_eq!(
10965 completion_labels,
10966 &[
10967 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10968 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10969 "single line label 2 d e f ",
10970 "a b c g h i ",
10971 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
10972 ],
10973 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
10974 );
10975
10976 for completion in menu
10977 .completions
10978 .borrow()
10979 .iter() {
10980 assert_eq!(
10981 completion.label.filter_range,
10982 0..completion.label.text.len(),
10983 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
10984 );
10985 }
10986 } else {
10987 panic!("expected completion menu to be open");
10988 }
10989 });
10990}
10991
10992#[gpui::test]
10993async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
10994 init_test(cx, |_| {});
10995 let mut cx = EditorLspTestContext::new_rust(
10996 lsp::ServerCapabilities {
10997 completion_provider: Some(lsp::CompletionOptions {
10998 trigger_characters: Some(vec![".".to_string()]),
10999 ..Default::default()
11000 }),
11001 ..Default::default()
11002 },
11003 cx,
11004 )
11005 .await;
11006 cx.lsp
11007 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11008 Ok(Some(lsp::CompletionResponse::Array(vec![
11009 lsp::CompletionItem {
11010 label: "first".into(),
11011 ..Default::default()
11012 },
11013 lsp::CompletionItem {
11014 label: "last".into(),
11015 ..Default::default()
11016 },
11017 ])))
11018 });
11019 cx.set_state("variableˇ");
11020 cx.simulate_keystroke(".");
11021 cx.executor().run_until_parked();
11022
11023 cx.update_editor(|editor, _, _| {
11024 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11025 {
11026 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11027 } else {
11028 panic!("expected completion menu to be open");
11029 }
11030 });
11031
11032 cx.update_editor(|editor, window, cx| {
11033 editor.move_page_down(&MovePageDown::default(), window, cx);
11034 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11035 {
11036 assert!(
11037 menu.selected_item == 1,
11038 "expected PageDown to select the last item from the context menu"
11039 );
11040 } else {
11041 panic!("expected completion menu to stay open after PageDown");
11042 }
11043 });
11044
11045 cx.update_editor(|editor, window, cx| {
11046 editor.move_page_up(&MovePageUp::default(), window, cx);
11047 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11048 {
11049 assert!(
11050 menu.selected_item == 0,
11051 "expected PageUp to select the first item from the context menu"
11052 );
11053 } else {
11054 panic!("expected completion menu to stay open after PageUp");
11055 }
11056 });
11057}
11058
11059#[gpui::test]
11060async fn test_completion_sort(cx: &mut TestAppContext) {
11061 init_test(cx, |_| {});
11062 let mut cx = EditorLspTestContext::new_rust(
11063 lsp::ServerCapabilities {
11064 completion_provider: Some(lsp::CompletionOptions {
11065 trigger_characters: Some(vec![".".to_string()]),
11066 ..Default::default()
11067 }),
11068 ..Default::default()
11069 },
11070 cx,
11071 )
11072 .await;
11073 cx.lsp
11074 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11075 Ok(Some(lsp::CompletionResponse::Array(vec![
11076 lsp::CompletionItem {
11077 label: "Range".into(),
11078 sort_text: Some("a".into()),
11079 ..Default::default()
11080 },
11081 lsp::CompletionItem {
11082 label: "r".into(),
11083 sort_text: Some("b".into()),
11084 ..Default::default()
11085 },
11086 lsp::CompletionItem {
11087 label: "ret".into(),
11088 sort_text: Some("c".into()),
11089 ..Default::default()
11090 },
11091 lsp::CompletionItem {
11092 label: "return".into(),
11093 sort_text: Some("d".into()),
11094 ..Default::default()
11095 },
11096 lsp::CompletionItem {
11097 label: "slice".into(),
11098 sort_text: Some("d".into()),
11099 ..Default::default()
11100 },
11101 ])))
11102 });
11103 cx.set_state("rˇ");
11104 cx.executor().run_until_parked();
11105 cx.update_editor(|editor, window, cx| {
11106 editor.show_completions(
11107 &ShowCompletions {
11108 trigger: Some("r".into()),
11109 },
11110 window,
11111 cx,
11112 );
11113 });
11114 cx.executor().run_until_parked();
11115
11116 cx.update_editor(|editor, _, _| {
11117 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11118 {
11119 assert_eq!(
11120 completion_menu_entries(&menu),
11121 &["r", "ret", "Range", "return"]
11122 );
11123 } else {
11124 panic!("expected completion menu to be open");
11125 }
11126 });
11127}
11128
11129#[gpui::test]
11130async fn test_as_is_completions(cx: &mut TestAppContext) {
11131 init_test(cx, |_| {});
11132 let mut cx = EditorLspTestContext::new_rust(
11133 lsp::ServerCapabilities {
11134 completion_provider: Some(lsp::CompletionOptions {
11135 ..Default::default()
11136 }),
11137 ..Default::default()
11138 },
11139 cx,
11140 )
11141 .await;
11142 cx.lsp
11143 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11144 Ok(Some(lsp::CompletionResponse::Array(vec![
11145 lsp::CompletionItem {
11146 label: "unsafe".into(),
11147 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11148 range: lsp::Range {
11149 start: lsp::Position {
11150 line: 1,
11151 character: 2,
11152 },
11153 end: lsp::Position {
11154 line: 1,
11155 character: 3,
11156 },
11157 },
11158 new_text: "unsafe".to_string(),
11159 })),
11160 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11161 ..Default::default()
11162 },
11163 ])))
11164 });
11165 cx.set_state("fn a() {}\n nˇ");
11166 cx.executor().run_until_parked();
11167 cx.update_editor(|editor, window, cx| {
11168 editor.show_completions(
11169 &ShowCompletions {
11170 trigger: Some("\n".into()),
11171 },
11172 window,
11173 cx,
11174 );
11175 });
11176 cx.executor().run_until_parked();
11177
11178 cx.update_editor(|editor, window, cx| {
11179 editor.confirm_completion(&Default::default(), window, cx)
11180 });
11181 cx.executor().run_until_parked();
11182 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11183}
11184
11185#[gpui::test]
11186async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11187 init_test(cx, |_| {});
11188
11189 let mut cx = EditorLspTestContext::new_rust(
11190 lsp::ServerCapabilities {
11191 completion_provider: Some(lsp::CompletionOptions {
11192 trigger_characters: Some(vec![".".to_string()]),
11193 resolve_provider: Some(true),
11194 ..Default::default()
11195 }),
11196 ..Default::default()
11197 },
11198 cx,
11199 )
11200 .await;
11201
11202 cx.set_state("fn main() { let a = 2ˇ; }");
11203 cx.simulate_keystroke(".");
11204 let completion_item = lsp::CompletionItem {
11205 label: "Some".into(),
11206 kind: Some(lsp::CompletionItemKind::SNIPPET),
11207 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11208 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11209 kind: lsp::MarkupKind::Markdown,
11210 value: "```rust\nSome(2)\n```".to_string(),
11211 })),
11212 deprecated: Some(false),
11213 sort_text: Some("Some".to_string()),
11214 filter_text: Some("Some".to_string()),
11215 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11216 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11217 range: lsp::Range {
11218 start: lsp::Position {
11219 line: 0,
11220 character: 22,
11221 },
11222 end: lsp::Position {
11223 line: 0,
11224 character: 22,
11225 },
11226 },
11227 new_text: "Some(2)".to_string(),
11228 })),
11229 additional_text_edits: Some(vec![lsp::TextEdit {
11230 range: lsp::Range {
11231 start: lsp::Position {
11232 line: 0,
11233 character: 20,
11234 },
11235 end: lsp::Position {
11236 line: 0,
11237 character: 22,
11238 },
11239 },
11240 new_text: "".to_string(),
11241 }]),
11242 ..Default::default()
11243 };
11244
11245 let closure_completion_item = completion_item.clone();
11246 let counter = Arc::new(AtomicUsize::new(0));
11247 let counter_clone = counter.clone();
11248 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11249 let task_completion_item = closure_completion_item.clone();
11250 counter_clone.fetch_add(1, atomic::Ordering::Release);
11251 async move {
11252 Ok(Some(lsp::CompletionResponse::Array(vec![
11253 task_completion_item,
11254 ])))
11255 }
11256 });
11257
11258 cx.condition(|editor, _| editor.context_menu_visible())
11259 .await;
11260 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11261 assert!(request.next().await.is_some());
11262 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11263
11264 cx.simulate_keystrokes("S o m");
11265 cx.condition(|editor, _| editor.context_menu_visible())
11266 .await;
11267 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11268 assert!(request.next().await.is_some());
11269 assert!(request.next().await.is_some());
11270 assert!(request.next().await.is_some());
11271 request.close();
11272 assert!(request.next().await.is_none());
11273 assert_eq!(
11274 counter.load(atomic::Ordering::Acquire),
11275 4,
11276 "With the completions menu open, only one LSP request should happen per input"
11277 );
11278}
11279
11280#[gpui::test]
11281async fn test_toggle_comment(cx: &mut TestAppContext) {
11282 init_test(cx, |_| {});
11283 let mut cx = EditorTestContext::new(cx).await;
11284 let language = Arc::new(Language::new(
11285 LanguageConfig {
11286 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11287 ..Default::default()
11288 },
11289 Some(tree_sitter_rust::LANGUAGE.into()),
11290 ));
11291 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11292
11293 // If multiple selections intersect a line, the line is only toggled once.
11294 cx.set_state(indoc! {"
11295 fn a() {
11296 «//b();
11297 ˇ»// «c();
11298 //ˇ» d();
11299 }
11300 "});
11301
11302 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11303
11304 cx.assert_editor_state(indoc! {"
11305 fn a() {
11306 «b();
11307 c();
11308 ˇ» d();
11309 }
11310 "});
11311
11312 // The comment prefix is inserted at the same column for every line in a
11313 // selection.
11314 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11315
11316 cx.assert_editor_state(indoc! {"
11317 fn a() {
11318 // «b();
11319 // c();
11320 ˇ»// d();
11321 }
11322 "});
11323
11324 // If a selection ends at the beginning of a line, that line is not toggled.
11325 cx.set_selections_state(indoc! {"
11326 fn a() {
11327 // b();
11328 «// c();
11329 ˇ» // d();
11330 }
11331 "});
11332
11333 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11334
11335 cx.assert_editor_state(indoc! {"
11336 fn a() {
11337 // b();
11338 «c();
11339 ˇ» // d();
11340 }
11341 "});
11342
11343 // If a selection span a single line and is empty, the line is toggled.
11344 cx.set_state(indoc! {"
11345 fn a() {
11346 a();
11347 b();
11348 ˇ
11349 }
11350 "});
11351
11352 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11353
11354 cx.assert_editor_state(indoc! {"
11355 fn a() {
11356 a();
11357 b();
11358 //•ˇ
11359 }
11360 "});
11361
11362 // If a selection span multiple lines, empty lines are not toggled.
11363 cx.set_state(indoc! {"
11364 fn a() {
11365 «a();
11366
11367 c();ˇ»
11368 }
11369 "});
11370
11371 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11372
11373 cx.assert_editor_state(indoc! {"
11374 fn a() {
11375 // «a();
11376
11377 // c();ˇ»
11378 }
11379 "});
11380
11381 // If a selection includes multiple comment prefixes, all lines are uncommented.
11382 cx.set_state(indoc! {"
11383 fn a() {
11384 «// a();
11385 /// b();
11386 //! c();ˇ»
11387 }
11388 "});
11389
11390 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11391
11392 cx.assert_editor_state(indoc! {"
11393 fn a() {
11394 «a();
11395 b();
11396 c();ˇ»
11397 }
11398 "});
11399}
11400
11401#[gpui::test]
11402async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11403 init_test(cx, |_| {});
11404 let mut cx = EditorTestContext::new(cx).await;
11405 let language = Arc::new(Language::new(
11406 LanguageConfig {
11407 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11408 ..Default::default()
11409 },
11410 Some(tree_sitter_rust::LANGUAGE.into()),
11411 ));
11412 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11413
11414 let toggle_comments = &ToggleComments {
11415 advance_downwards: false,
11416 ignore_indent: true,
11417 };
11418
11419 // If multiple selections intersect a line, the line is only toggled once.
11420 cx.set_state(indoc! {"
11421 fn a() {
11422 // «b();
11423 // c();
11424 // ˇ» d();
11425 }
11426 "});
11427
11428 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11429
11430 cx.assert_editor_state(indoc! {"
11431 fn a() {
11432 «b();
11433 c();
11434 ˇ» d();
11435 }
11436 "});
11437
11438 // The comment prefix is inserted at the beginning of each line
11439 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11440
11441 cx.assert_editor_state(indoc! {"
11442 fn a() {
11443 // «b();
11444 // c();
11445 // ˇ» d();
11446 }
11447 "});
11448
11449 // If a selection ends at the beginning of a line, that line is not toggled.
11450 cx.set_selections_state(indoc! {"
11451 fn a() {
11452 // b();
11453 // «c();
11454 ˇ»// d();
11455 }
11456 "});
11457
11458 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11459
11460 cx.assert_editor_state(indoc! {"
11461 fn a() {
11462 // b();
11463 «c();
11464 ˇ»// d();
11465 }
11466 "});
11467
11468 // If a selection span a single line and is empty, the line is toggled.
11469 cx.set_state(indoc! {"
11470 fn a() {
11471 a();
11472 b();
11473 ˇ
11474 }
11475 "});
11476
11477 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11478
11479 cx.assert_editor_state(indoc! {"
11480 fn a() {
11481 a();
11482 b();
11483 //ˇ
11484 }
11485 "});
11486
11487 // If a selection span multiple lines, empty lines are not toggled.
11488 cx.set_state(indoc! {"
11489 fn a() {
11490 «a();
11491
11492 c();ˇ»
11493 }
11494 "});
11495
11496 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11497
11498 cx.assert_editor_state(indoc! {"
11499 fn a() {
11500 // «a();
11501
11502 // c();ˇ»
11503 }
11504 "});
11505
11506 // If a selection includes multiple comment prefixes, all lines are uncommented.
11507 cx.set_state(indoc! {"
11508 fn a() {
11509 // «a();
11510 /// b();
11511 //! c();ˇ»
11512 }
11513 "});
11514
11515 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11516
11517 cx.assert_editor_state(indoc! {"
11518 fn a() {
11519 «a();
11520 b();
11521 c();ˇ»
11522 }
11523 "});
11524}
11525
11526#[gpui::test]
11527async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11528 init_test(cx, |_| {});
11529
11530 let language = Arc::new(Language::new(
11531 LanguageConfig {
11532 line_comments: vec!["// ".into()],
11533 ..Default::default()
11534 },
11535 Some(tree_sitter_rust::LANGUAGE.into()),
11536 ));
11537
11538 let mut cx = EditorTestContext::new(cx).await;
11539
11540 cx.language_registry().add(language.clone());
11541 cx.update_buffer(|buffer, cx| {
11542 buffer.set_language(Some(language), cx);
11543 });
11544
11545 let toggle_comments = &ToggleComments {
11546 advance_downwards: true,
11547 ignore_indent: false,
11548 };
11549
11550 // Single cursor on one line -> advance
11551 // Cursor moves horizontally 3 characters as well on non-blank line
11552 cx.set_state(indoc!(
11553 "fn a() {
11554 ˇdog();
11555 cat();
11556 }"
11557 ));
11558 cx.update_editor(|editor, window, cx| {
11559 editor.toggle_comments(toggle_comments, window, cx);
11560 });
11561 cx.assert_editor_state(indoc!(
11562 "fn a() {
11563 // dog();
11564 catˇ();
11565 }"
11566 ));
11567
11568 // Single selection on one line -> don't advance
11569 cx.set_state(indoc!(
11570 "fn a() {
11571 «dog()ˇ»;
11572 cat();
11573 }"
11574 ));
11575 cx.update_editor(|editor, window, cx| {
11576 editor.toggle_comments(toggle_comments, window, cx);
11577 });
11578 cx.assert_editor_state(indoc!(
11579 "fn a() {
11580 // «dog()ˇ»;
11581 cat();
11582 }"
11583 ));
11584
11585 // Multiple cursors on one line -> advance
11586 cx.set_state(indoc!(
11587 "fn a() {
11588 ˇdˇog();
11589 cat();
11590 }"
11591 ));
11592 cx.update_editor(|editor, window, cx| {
11593 editor.toggle_comments(toggle_comments, window, cx);
11594 });
11595 cx.assert_editor_state(indoc!(
11596 "fn a() {
11597 // dog();
11598 catˇ(ˇ);
11599 }"
11600 ));
11601
11602 // Multiple cursors on one line, with selection -> don't advance
11603 cx.set_state(indoc!(
11604 "fn a() {
11605 ˇdˇog«()ˇ»;
11606 cat();
11607 }"
11608 ));
11609 cx.update_editor(|editor, window, cx| {
11610 editor.toggle_comments(toggle_comments, window, cx);
11611 });
11612 cx.assert_editor_state(indoc!(
11613 "fn a() {
11614 // ˇdˇog«()ˇ»;
11615 cat();
11616 }"
11617 ));
11618
11619 // Single cursor on one line -> advance
11620 // Cursor moves to column 0 on blank line
11621 cx.set_state(indoc!(
11622 "fn a() {
11623 ˇdog();
11624
11625 cat();
11626 }"
11627 ));
11628 cx.update_editor(|editor, window, cx| {
11629 editor.toggle_comments(toggle_comments, window, cx);
11630 });
11631 cx.assert_editor_state(indoc!(
11632 "fn a() {
11633 // dog();
11634 ˇ
11635 cat();
11636 }"
11637 ));
11638
11639 // Single cursor on one line -> advance
11640 // Cursor starts and ends at column 0
11641 cx.set_state(indoc!(
11642 "fn a() {
11643 ˇ dog();
11644 cat();
11645 }"
11646 ));
11647 cx.update_editor(|editor, window, cx| {
11648 editor.toggle_comments(toggle_comments, window, cx);
11649 });
11650 cx.assert_editor_state(indoc!(
11651 "fn a() {
11652 // dog();
11653 ˇ cat();
11654 }"
11655 ));
11656}
11657
11658#[gpui::test]
11659async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11660 init_test(cx, |_| {});
11661
11662 let mut cx = EditorTestContext::new(cx).await;
11663
11664 let html_language = Arc::new(
11665 Language::new(
11666 LanguageConfig {
11667 name: "HTML".into(),
11668 block_comment: Some(("<!-- ".into(), " -->".into())),
11669 ..Default::default()
11670 },
11671 Some(tree_sitter_html::LANGUAGE.into()),
11672 )
11673 .with_injection_query(
11674 r#"
11675 (script_element
11676 (raw_text) @injection.content
11677 (#set! injection.language "javascript"))
11678 "#,
11679 )
11680 .unwrap(),
11681 );
11682
11683 let javascript_language = Arc::new(Language::new(
11684 LanguageConfig {
11685 name: "JavaScript".into(),
11686 line_comments: vec!["// ".into()],
11687 ..Default::default()
11688 },
11689 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11690 ));
11691
11692 cx.language_registry().add(html_language.clone());
11693 cx.language_registry().add(javascript_language.clone());
11694 cx.update_buffer(|buffer, cx| {
11695 buffer.set_language(Some(html_language), cx);
11696 });
11697
11698 // Toggle comments for empty selections
11699 cx.set_state(
11700 &r#"
11701 <p>A</p>ˇ
11702 <p>B</p>ˇ
11703 <p>C</p>ˇ
11704 "#
11705 .unindent(),
11706 );
11707 cx.update_editor(|editor, window, cx| {
11708 editor.toggle_comments(&ToggleComments::default(), window, cx)
11709 });
11710 cx.assert_editor_state(
11711 &r#"
11712 <!-- <p>A</p>ˇ -->
11713 <!-- <p>B</p>ˇ -->
11714 <!-- <p>C</p>ˇ -->
11715 "#
11716 .unindent(),
11717 );
11718 cx.update_editor(|editor, window, cx| {
11719 editor.toggle_comments(&ToggleComments::default(), window, cx)
11720 });
11721 cx.assert_editor_state(
11722 &r#"
11723 <p>A</p>ˇ
11724 <p>B</p>ˇ
11725 <p>C</p>ˇ
11726 "#
11727 .unindent(),
11728 );
11729
11730 // Toggle comments for mixture of empty and non-empty selections, where
11731 // multiple selections occupy a given line.
11732 cx.set_state(
11733 &r#"
11734 <p>A«</p>
11735 <p>ˇ»B</p>ˇ
11736 <p>C«</p>
11737 <p>ˇ»D</p>ˇ
11738 "#
11739 .unindent(),
11740 );
11741
11742 cx.update_editor(|editor, window, cx| {
11743 editor.toggle_comments(&ToggleComments::default(), window, cx)
11744 });
11745 cx.assert_editor_state(
11746 &r#"
11747 <!-- <p>A«</p>
11748 <p>ˇ»B</p>ˇ -->
11749 <!-- <p>C«</p>
11750 <p>ˇ»D</p>ˇ -->
11751 "#
11752 .unindent(),
11753 );
11754 cx.update_editor(|editor, window, cx| {
11755 editor.toggle_comments(&ToggleComments::default(), window, cx)
11756 });
11757 cx.assert_editor_state(
11758 &r#"
11759 <p>A«</p>
11760 <p>ˇ»B</p>ˇ
11761 <p>C«</p>
11762 <p>ˇ»D</p>ˇ
11763 "#
11764 .unindent(),
11765 );
11766
11767 // Toggle comments when different languages are active for different
11768 // selections.
11769 cx.set_state(
11770 &r#"
11771 ˇ<script>
11772 ˇvar x = new Y();
11773 ˇ</script>
11774 "#
11775 .unindent(),
11776 );
11777 cx.executor().run_until_parked();
11778 cx.update_editor(|editor, window, cx| {
11779 editor.toggle_comments(&ToggleComments::default(), window, cx)
11780 });
11781 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11782 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11783 cx.assert_editor_state(
11784 &r#"
11785 <!-- ˇ<script> -->
11786 // ˇvar x = new Y();
11787 <!-- ˇ</script> -->
11788 "#
11789 .unindent(),
11790 );
11791}
11792
11793#[gpui::test]
11794fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11795 init_test(cx, |_| {});
11796
11797 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11798 let multibuffer = cx.new(|cx| {
11799 let mut multibuffer = MultiBuffer::new(ReadWrite);
11800 multibuffer.push_excerpts(
11801 buffer.clone(),
11802 [
11803 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
11804 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
11805 ],
11806 cx,
11807 );
11808 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
11809 multibuffer
11810 });
11811
11812 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11813 editor.update_in(cx, |editor, window, cx| {
11814 assert_eq!(editor.text(cx), "aaaa\nbbbb");
11815 editor.change_selections(None, window, cx, |s| {
11816 s.select_ranges([
11817 Point::new(0, 0)..Point::new(0, 0),
11818 Point::new(1, 0)..Point::new(1, 0),
11819 ])
11820 });
11821
11822 editor.handle_input("X", window, cx);
11823 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
11824 assert_eq!(
11825 editor.selections.ranges(cx),
11826 [
11827 Point::new(0, 1)..Point::new(0, 1),
11828 Point::new(1, 1)..Point::new(1, 1),
11829 ]
11830 );
11831
11832 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
11833 editor.change_selections(None, window, cx, |s| {
11834 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
11835 });
11836 editor.backspace(&Default::default(), window, cx);
11837 assert_eq!(editor.text(cx), "Xa\nbbb");
11838 assert_eq!(
11839 editor.selections.ranges(cx),
11840 [Point::new(1, 0)..Point::new(1, 0)]
11841 );
11842
11843 editor.change_selections(None, window, cx, |s| {
11844 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
11845 });
11846 editor.backspace(&Default::default(), window, cx);
11847 assert_eq!(editor.text(cx), "X\nbb");
11848 assert_eq!(
11849 editor.selections.ranges(cx),
11850 [Point::new(0, 1)..Point::new(0, 1)]
11851 );
11852 });
11853}
11854
11855#[gpui::test]
11856fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
11857 init_test(cx, |_| {});
11858
11859 let markers = vec![('[', ']').into(), ('(', ')').into()];
11860 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
11861 indoc! {"
11862 [aaaa
11863 (bbbb]
11864 cccc)",
11865 },
11866 markers.clone(),
11867 );
11868 let excerpt_ranges = markers.into_iter().map(|marker| {
11869 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
11870 ExcerptRange::new(context.clone())
11871 });
11872 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
11873 let multibuffer = cx.new(|cx| {
11874 let mut multibuffer = MultiBuffer::new(ReadWrite);
11875 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
11876 multibuffer
11877 });
11878
11879 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11880 editor.update_in(cx, |editor, window, cx| {
11881 let (expected_text, selection_ranges) = marked_text_ranges(
11882 indoc! {"
11883 aaaa
11884 bˇbbb
11885 bˇbbˇb
11886 cccc"
11887 },
11888 true,
11889 );
11890 assert_eq!(editor.text(cx), expected_text);
11891 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11892
11893 editor.handle_input("X", window, cx);
11894
11895 let (expected_text, expected_selections) = marked_text_ranges(
11896 indoc! {"
11897 aaaa
11898 bXˇbbXb
11899 bXˇbbXˇb
11900 cccc"
11901 },
11902 false,
11903 );
11904 assert_eq!(editor.text(cx), expected_text);
11905 assert_eq!(editor.selections.ranges(cx), expected_selections);
11906
11907 editor.newline(&Newline, window, cx);
11908 let (expected_text, expected_selections) = marked_text_ranges(
11909 indoc! {"
11910 aaaa
11911 bX
11912 ˇbbX
11913 b
11914 bX
11915 ˇbbX
11916 ˇb
11917 cccc"
11918 },
11919 false,
11920 );
11921 assert_eq!(editor.text(cx), expected_text);
11922 assert_eq!(editor.selections.ranges(cx), expected_selections);
11923 });
11924}
11925
11926#[gpui::test]
11927fn test_refresh_selections(cx: &mut TestAppContext) {
11928 init_test(cx, |_| {});
11929
11930 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11931 let mut excerpt1_id = None;
11932 let multibuffer = cx.new(|cx| {
11933 let mut multibuffer = MultiBuffer::new(ReadWrite);
11934 excerpt1_id = multibuffer
11935 .push_excerpts(
11936 buffer.clone(),
11937 [
11938 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11939 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11940 ],
11941 cx,
11942 )
11943 .into_iter()
11944 .next();
11945 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11946 multibuffer
11947 });
11948
11949 let editor = cx.add_window(|window, cx| {
11950 let mut editor = build_editor(multibuffer.clone(), window, cx);
11951 let snapshot = editor.snapshot(window, cx);
11952 editor.change_selections(None, window, cx, |s| {
11953 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11954 });
11955 editor.begin_selection(
11956 Point::new(2, 1).to_display_point(&snapshot),
11957 true,
11958 1,
11959 window,
11960 cx,
11961 );
11962 assert_eq!(
11963 editor.selections.ranges(cx),
11964 [
11965 Point::new(1, 3)..Point::new(1, 3),
11966 Point::new(2, 1)..Point::new(2, 1),
11967 ]
11968 );
11969 editor
11970 });
11971
11972 // Refreshing selections is a no-op when excerpts haven't changed.
11973 _ = editor.update(cx, |editor, window, cx| {
11974 editor.change_selections(None, window, cx, |s| s.refresh());
11975 assert_eq!(
11976 editor.selections.ranges(cx),
11977 [
11978 Point::new(1, 3)..Point::new(1, 3),
11979 Point::new(2, 1)..Point::new(2, 1),
11980 ]
11981 );
11982 });
11983
11984 multibuffer.update(cx, |multibuffer, cx| {
11985 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11986 });
11987 _ = editor.update(cx, |editor, window, cx| {
11988 // Removing an excerpt causes the first selection to become degenerate.
11989 assert_eq!(
11990 editor.selections.ranges(cx),
11991 [
11992 Point::new(0, 0)..Point::new(0, 0),
11993 Point::new(0, 1)..Point::new(0, 1)
11994 ]
11995 );
11996
11997 // Refreshing selections will relocate the first selection to the original buffer
11998 // location.
11999 editor.change_selections(None, window, cx, |s| s.refresh());
12000 assert_eq!(
12001 editor.selections.ranges(cx),
12002 [
12003 Point::new(0, 1)..Point::new(0, 1),
12004 Point::new(0, 3)..Point::new(0, 3)
12005 ]
12006 );
12007 assert!(editor.selections.pending_anchor().is_some());
12008 });
12009}
12010
12011#[gpui::test]
12012fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12013 init_test(cx, |_| {});
12014
12015 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12016 let mut excerpt1_id = None;
12017 let multibuffer = cx.new(|cx| {
12018 let mut multibuffer = MultiBuffer::new(ReadWrite);
12019 excerpt1_id = multibuffer
12020 .push_excerpts(
12021 buffer.clone(),
12022 [
12023 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12024 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12025 ],
12026 cx,
12027 )
12028 .into_iter()
12029 .next();
12030 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12031 multibuffer
12032 });
12033
12034 let editor = cx.add_window(|window, cx| {
12035 let mut editor = build_editor(multibuffer.clone(), window, cx);
12036 let snapshot = editor.snapshot(window, cx);
12037 editor.begin_selection(
12038 Point::new(1, 3).to_display_point(&snapshot),
12039 false,
12040 1,
12041 window,
12042 cx,
12043 );
12044 assert_eq!(
12045 editor.selections.ranges(cx),
12046 [Point::new(1, 3)..Point::new(1, 3)]
12047 );
12048 editor
12049 });
12050
12051 multibuffer.update(cx, |multibuffer, cx| {
12052 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12053 });
12054 _ = editor.update(cx, |editor, window, cx| {
12055 assert_eq!(
12056 editor.selections.ranges(cx),
12057 [Point::new(0, 0)..Point::new(0, 0)]
12058 );
12059
12060 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12061 editor.change_selections(None, window, cx, |s| s.refresh());
12062 assert_eq!(
12063 editor.selections.ranges(cx),
12064 [Point::new(0, 3)..Point::new(0, 3)]
12065 );
12066 assert!(editor.selections.pending_anchor().is_some());
12067 });
12068}
12069
12070#[gpui::test]
12071async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12072 init_test(cx, |_| {});
12073
12074 let language = Arc::new(
12075 Language::new(
12076 LanguageConfig {
12077 brackets: BracketPairConfig {
12078 pairs: vec![
12079 BracketPair {
12080 start: "{".to_string(),
12081 end: "}".to_string(),
12082 close: true,
12083 surround: true,
12084 newline: true,
12085 },
12086 BracketPair {
12087 start: "/* ".to_string(),
12088 end: " */".to_string(),
12089 close: true,
12090 surround: true,
12091 newline: true,
12092 },
12093 ],
12094 ..Default::default()
12095 },
12096 ..Default::default()
12097 },
12098 Some(tree_sitter_rust::LANGUAGE.into()),
12099 )
12100 .with_indents_query("")
12101 .unwrap(),
12102 );
12103
12104 let text = concat!(
12105 "{ }\n", //
12106 " x\n", //
12107 " /* */\n", //
12108 "x\n", //
12109 "{{} }\n", //
12110 );
12111
12112 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12113 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12114 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12115 editor
12116 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12117 .await;
12118
12119 editor.update_in(cx, |editor, window, cx| {
12120 editor.change_selections(None, window, cx, |s| {
12121 s.select_display_ranges([
12122 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12123 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12124 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12125 ])
12126 });
12127 editor.newline(&Newline, window, cx);
12128
12129 assert_eq!(
12130 editor.buffer().read(cx).read(cx).text(),
12131 concat!(
12132 "{ \n", // Suppress rustfmt
12133 "\n", //
12134 "}\n", //
12135 " x\n", //
12136 " /* \n", //
12137 " \n", //
12138 " */\n", //
12139 "x\n", //
12140 "{{} \n", //
12141 "}\n", //
12142 )
12143 );
12144 });
12145}
12146
12147#[gpui::test]
12148fn test_highlighted_ranges(cx: &mut TestAppContext) {
12149 init_test(cx, |_| {});
12150
12151 let editor = cx.add_window(|window, cx| {
12152 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12153 build_editor(buffer.clone(), window, cx)
12154 });
12155
12156 _ = editor.update(cx, |editor, window, cx| {
12157 struct Type1;
12158 struct Type2;
12159
12160 let buffer = editor.buffer.read(cx).snapshot(cx);
12161
12162 let anchor_range =
12163 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12164
12165 editor.highlight_background::<Type1>(
12166 &[
12167 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12168 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12169 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12170 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12171 ],
12172 |_| Hsla::red(),
12173 cx,
12174 );
12175 editor.highlight_background::<Type2>(
12176 &[
12177 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12178 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12179 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12180 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12181 ],
12182 |_| Hsla::green(),
12183 cx,
12184 );
12185
12186 let snapshot = editor.snapshot(window, cx);
12187 let mut highlighted_ranges = editor.background_highlights_in_range(
12188 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12189 &snapshot,
12190 cx.theme().colors(),
12191 );
12192 // Enforce a consistent ordering based on color without relying on the ordering of the
12193 // highlight's `TypeId` which is non-executor.
12194 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12195 assert_eq!(
12196 highlighted_ranges,
12197 &[
12198 (
12199 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12200 Hsla::red(),
12201 ),
12202 (
12203 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12204 Hsla::red(),
12205 ),
12206 (
12207 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12208 Hsla::green(),
12209 ),
12210 (
12211 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12212 Hsla::green(),
12213 ),
12214 ]
12215 );
12216 assert_eq!(
12217 editor.background_highlights_in_range(
12218 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12219 &snapshot,
12220 cx.theme().colors(),
12221 ),
12222 &[(
12223 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12224 Hsla::red(),
12225 )]
12226 );
12227 });
12228}
12229
12230#[gpui::test]
12231async fn test_following(cx: &mut TestAppContext) {
12232 init_test(cx, |_| {});
12233
12234 let fs = FakeFs::new(cx.executor());
12235 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12236
12237 let buffer = project.update(cx, |project, cx| {
12238 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12239 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12240 });
12241 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12242 let follower = cx.update(|cx| {
12243 cx.open_window(
12244 WindowOptions {
12245 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12246 gpui::Point::new(px(0.), px(0.)),
12247 gpui::Point::new(px(10.), px(80.)),
12248 ))),
12249 ..Default::default()
12250 },
12251 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12252 )
12253 .unwrap()
12254 });
12255
12256 let is_still_following = Rc::new(RefCell::new(true));
12257 let follower_edit_event_count = Rc::new(RefCell::new(0));
12258 let pending_update = Rc::new(RefCell::new(None));
12259 let leader_entity = leader.root(cx).unwrap();
12260 let follower_entity = follower.root(cx).unwrap();
12261 _ = follower.update(cx, {
12262 let update = pending_update.clone();
12263 let is_still_following = is_still_following.clone();
12264 let follower_edit_event_count = follower_edit_event_count.clone();
12265 |_, window, cx| {
12266 cx.subscribe_in(
12267 &leader_entity,
12268 window,
12269 move |_, leader, event, window, cx| {
12270 leader.read(cx).add_event_to_update_proto(
12271 event,
12272 &mut update.borrow_mut(),
12273 window,
12274 cx,
12275 );
12276 },
12277 )
12278 .detach();
12279
12280 cx.subscribe_in(
12281 &follower_entity,
12282 window,
12283 move |_, _, event: &EditorEvent, _window, _cx| {
12284 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12285 *is_still_following.borrow_mut() = false;
12286 }
12287
12288 if let EditorEvent::BufferEdited = event {
12289 *follower_edit_event_count.borrow_mut() += 1;
12290 }
12291 },
12292 )
12293 .detach();
12294 }
12295 });
12296
12297 // Update the selections only
12298 _ = leader.update(cx, |leader, window, cx| {
12299 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12300 });
12301 follower
12302 .update(cx, |follower, window, cx| {
12303 follower.apply_update_proto(
12304 &project,
12305 pending_update.borrow_mut().take().unwrap(),
12306 window,
12307 cx,
12308 )
12309 })
12310 .unwrap()
12311 .await
12312 .unwrap();
12313 _ = follower.update(cx, |follower, _, cx| {
12314 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12315 });
12316 assert!(*is_still_following.borrow());
12317 assert_eq!(*follower_edit_event_count.borrow(), 0);
12318
12319 // Update the scroll position only
12320 _ = leader.update(cx, |leader, window, cx| {
12321 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12322 });
12323 follower
12324 .update(cx, |follower, window, cx| {
12325 follower.apply_update_proto(
12326 &project,
12327 pending_update.borrow_mut().take().unwrap(),
12328 window,
12329 cx,
12330 )
12331 })
12332 .unwrap()
12333 .await
12334 .unwrap();
12335 assert_eq!(
12336 follower
12337 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12338 .unwrap(),
12339 gpui::Point::new(1.5, 3.5)
12340 );
12341 assert!(*is_still_following.borrow());
12342 assert_eq!(*follower_edit_event_count.borrow(), 0);
12343
12344 // Update the selections and scroll position. The follower's scroll position is updated
12345 // via autoscroll, not via the leader's exact scroll position.
12346 _ = leader.update(cx, |leader, window, cx| {
12347 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12348 leader.request_autoscroll(Autoscroll::newest(), cx);
12349 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12350 });
12351 follower
12352 .update(cx, |follower, window, cx| {
12353 follower.apply_update_proto(
12354 &project,
12355 pending_update.borrow_mut().take().unwrap(),
12356 window,
12357 cx,
12358 )
12359 })
12360 .unwrap()
12361 .await
12362 .unwrap();
12363 _ = follower.update(cx, |follower, _, cx| {
12364 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12365 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12366 });
12367 assert!(*is_still_following.borrow());
12368
12369 // Creating a pending selection that precedes another selection
12370 _ = leader.update(cx, |leader, window, cx| {
12371 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12372 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12373 });
12374 follower
12375 .update(cx, |follower, window, cx| {
12376 follower.apply_update_proto(
12377 &project,
12378 pending_update.borrow_mut().take().unwrap(),
12379 window,
12380 cx,
12381 )
12382 })
12383 .unwrap()
12384 .await
12385 .unwrap();
12386 _ = follower.update(cx, |follower, _, cx| {
12387 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12388 });
12389 assert!(*is_still_following.borrow());
12390
12391 // Extend the pending selection so that it surrounds another selection
12392 _ = leader.update(cx, |leader, window, cx| {
12393 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12394 });
12395 follower
12396 .update(cx, |follower, window, cx| {
12397 follower.apply_update_proto(
12398 &project,
12399 pending_update.borrow_mut().take().unwrap(),
12400 window,
12401 cx,
12402 )
12403 })
12404 .unwrap()
12405 .await
12406 .unwrap();
12407 _ = follower.update(cx, |follower, _, cx| {
12408 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12409 });
12410
12411 // Scrolling locally breaks the follow
12412 _ = follower.update(cx, |follower, window, cx| {
12413 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12414 follower.set_scroll_anchor(
12415 ScrollAnchor {
12416 anchor: top_anchor,
12417 offset: gpui::Point::new(0.0, 0.5),
12418 },
12419 window,
12420 cx,
12421 );
12422 });
12423 assert!(!(*is_still_following.borrow()));
12424}
12425
12426#[gpui::test]
12427async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12428 init_test(cx, |_| {});
12429
12430 let fs = FakeFs::new(cx.executor());
12431 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12432 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12433 let pane = workspace
12434 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12435 .unwrap();
12436
12437 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12438
12439 let leader = pane.update_in(cx, |_, window, cx| {
12440 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12441 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12442 });
12443
12444 // Start following the editor when it has no excerpts.
12445 let mut state_message =
12446 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12447 let workspace_entity = workspace.root(cx).unwrap();
12448 let follower_1 = cx
12449 .update_window(*workspace.deref(), |_, window, cx| {
12450 Editor::from_state_proto(
12451 workspace_entity,
12452 ViewId {
12453 creator: Default::default(),
12454 id: 0,
12455 },
12456 &mut state_message,
12457 window,
12458 cx,
12459 )
12460 })
12461 .unwrap()
12462 .unwrap()
12463 .await
12464 .unwrap();
12465
12466 let update_message = Rc::new(RefCell::new(None));
12467 follower_1.update_in(cx, {
12468 let update = update_message.clone();
12469 |_, window, cx| {
12470 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12471 leader.read(cx).add_event_to_update_proto(
12472 event,
12473 &mut update.borrow_mut(),
12474 window,
12475 cx,
12476 );
12477 })
12478 .detach();
12479 }
12480 });
12481
12482 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12483 (
12484 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12485 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12486 )
12487 });
12488
12489 // Insert some excerpts.
12490 leader.update(cx, |leader, cx| {
12491 leader.buffer.update(cx, |multibuffer, cx| {
12492 let excerpt_ids = multibuffer.push_excerpts(
12493 buffer_1.clone(),
12494 [
12495 ExcerptRange::new(1..6),
12496 ExcerptRange::new(12..15),
12497 ExcerptRange::new(0..3),
12498 ],
12499 cx,
12500 );
12501 multibuffer.insert_excerpts_after(
12502 excerpt_ids[0],
12503 buffer_2.clone(),
12504 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
12505 cx,
12506 );
12507 });
12508 });
12509
12510 // Apply the update of adding the excerpts.
12511 follower_1
12512 .update_in(cx, |follower, window, cx| {
12513 follower.apply_update_proto(
12514 &project,
12515 update_message.borrow().clone().unwrap(),
12516 window,
12517 cx,
12518 )
12519 })
12520 .await
12521 .unwrap();
12522 assert_eq!(
12523 follower_1.update(cx, |editor, cx| editor.text(cx)),
12524 leader.update(cx, |editor, cx| editor.text(cx))
12525 );
12526 update_message.borrow_mut().take();
12527
12528 // Start following separately after it already has excerpts.
12529 let mut state_message =
12530 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12531 let workspace_entity = workspace.root(cx).unwrap();
12532 let follower_2 = cx
12533 .update_window(*workspace.deref(), |_, window, cx| {
12534 Editor::from_state_proto(
12535 workspace_entity,
12536 ViewId {
12537 creator: Default::default(),
12538 id: 0,
12539 },
12540 &mut state_message,
12541 window,
12542 cx,
12543 )
12544 })
12545 .unwrap()
12546 .unwrap()
12547 .await
12548 .unwrap();
12549 assert_eq!(
12550 follower_2.update(cx, |editor, cx| editor.text(cx)),
12551 leader.update(cx, |editor, cx| editor.text(cx))
12552 );
12553
12554 // Remove some excerpts.
12555 leader.update(cx, |leader, cx| {
12556 leader.buffer.update(cx, |multibuffer, cx| {
12557 let excerpt_ids = multibuffer.excerpt_ids();
12558 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12559 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12560 });
12561 });
12562
12563 // Apply the update of removing the excerpts.
12564 follower_1
12565 .update_in(cx, |follower, window, cx| {
12566 follower.apply_update_proto(
12567 &project,
12568 update_message.borrow().clone().unwrap(),
12569 window,
12570 cx,
12571 )
12572 })
12573 .await
12574 .unwrap();
12575 follower_2
12576 .update_in(cx, |follower, window, cx| {
12577 follower.apply_update_proto(
12578 &project,
12579 update_message.borrow().clone().unwrap(),
12580 window,
12581 cx,
12582 )
12583 })
12584 .await
12585 .unwrap();
12586 update_message.borrow_mut().take();
12587 assert_eq!(
12588 follower_1.update(cx, |editor, cx| editor.text(cx)),
12589 leader.update(cx, |editor, cx| editor.text(cx))
12590 );
12591}
12592
12593#[gpui::test]
12594async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12595 init_test(cx, |_| {});
12596
12597 let mut cx = EditorTestContext::new(cx).await;
12598 let lsp_store =
12599 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12600
12601 cx.set_state(indoc! {"
12602 ˇfn func(abc def: i32) -> u32 {
12603 }
12604 "});
12605
12606 cx.update(|_, cx| {
12607 lsp_store.update(cx, |lsp_store, cx| {
12608 lsp_store
12609 .update_diagnostics(
12610 LanguageServerId(0),
12611 lsp::PublishDiagnosticsParams {
12612 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12613 version: None,
12614 diagnostics: vec![
12615 lsp::Diagnostic {
12616 range: lsp::Range::new(
12617 lsp::Position::new(0, 11),
12618 lsp::Position::new(0, 12),
12619 ),
12620 severity: Some(lsp::DiagnosticSeverity::ERROR),
12621 ..Default::default()
12622 },
12623 lsp::Diagnostic {
12624 range: lsp::Range::new(
12625 lsp::Position::new(0, 12),
12626 lsp::Position::new(0, 15),
12627 ),
12628 severity: Some(lsp::DiagnosticSeverity::ERROR),
12629 ..Default::default()
12630 },
12631 lsp::Diagnostic {
12632 range: lsp::Range::new(
12633 lsp::Position::new(0, 25),
12634 lsp::Position::new(0, 28),
12635 ),
12636 severity: Some(lsp::DiagnosticSeverity::ERROR),
12637 ..Default::default()
12638 },
12639 ],
12640 },
12641 &[],
12642 cx,
12643 )
12644 .unwrap()
12645 });
12646 });
12647
12648 executor.run_until_parked();
12649
12650 cx.update_editor(|editor, window, cx| {
12651 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12652 });
12653
12654 cx.assert_editor_state(indoc! {"
12655 fn func(abc def: i32) -> ˇu32 {
12656 }
12657 "});
12658
12659 cx.update_editor(|editor, window, cx| {
12660 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12661 });
12662
12663 cx.assert_editor_state(indoc! {"
12664 fn func(abc ˇdef: i32) -> u32 {
12665 }
12666 "});
12667
12668 cx.update_editor(|editor, window, cx| {
12669 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12670 });
12671
12672 cx.assert_editor_state(indoc! {"
12673 fn func(abcˇ def: i32) -> u32 {
12674 }
12675 "});
12676
12677 cx.update_editor(|editor, window, cx| {
12678 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12679 });
12680
12681 cx.assert_editor_state(indoc! {"
12682 fn func(abc def: i32) -> ˇu32 {
12683 }
12684 "});
12685}
12686
12687#[gpui::test]
12688async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12689 init_test(cx, |_| {});
12690
12691 let mut cx = EditorTestContext::new(cx).await;
12692
12693 cx.set_state(indoc! {"
12694 fn func(abˇc def: i32) -> u32 {
12695 }
12696 "});
12697 let lsp_store =
12698 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12699
12700 cx.update(|_, cx| {
12701 lsp_store.update(cx, |lsp_store, cx| {
12702 lsp_store.update_diagnostics(
12703 LanguageServerId(0),
12704 lsp::PublishDiagnosticsParams {
12705 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12706 version: None,
12707 diagnostics: vec![lsp::Diagnostic {
12708 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12709 severity: Some(lsp::DiagnosticSeverity::ERROR),
12710 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12711 ..Default::default()
12712 }],
12713 },
12714 &[],
12715 cx,
12716 )
12717 })
12718 }).unwrap();
12719 cx.run_until_parked();
12720 cx.update_editor(|editor, window, cx| {
12721 hover_popover::hover(editor, &Default::default(), window, cx)
12722 });
12723 cx.run_until_parked();
12724 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12725}
12726
12727#[gpui::test]
12728async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12729 init_test(cx, |_| {});
12730
12731 let mut cx = EditorTestContext::new(cx).await;
12732
12733 let diff_base = r#"
12734 use some::mod;
12735
12736 const A: u32 = 42;
12737
12738 fn main() {
12739 println!("hello");
12740
12741 println!("world");
12742 }
12743 "#
12744 .unindent();
12745
12746 // Edits are modified, removed, modified, added
12747 cx.set_state(
12748 &r#"
12749 use some::modified;
12750
12751 ˇ
12752 fn main() {
12753 println!("hello there");
12754
12755 println!("around the");
12756 println!("world");
12757 }
12758 "#
12759 .unindent(),
12760 );
12761
12762 cx.set_head_text(&diff_base);
12763 executor.run_until_parked();
12764
12765 cx.update_editor(|editor, window, cx| {
12766 //Wrap around the bottom of the buffer
12767 for _ in 0..3 {
12768 editor.go_to_next_hunk(&GoToHunk, window, cx);
12769 }
12770 });
12771
12772 cx.assert_editor_state(
12773 &r#"
12774 ˇuse some::modified;
12775
12776
12777 fn main() {
12778 println!("hello there");
12779
12780 println!("around the");
12781 println!("world");
12782 }
12783 "#
12784 .unindent(),
12785 );
12786
12787 cx.update_editor(|editor, window, cx| {
12788 //Wrap around the top of the buffer
12789 for _ in 0..2 {
12790 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12791 }
12792 });
12793
12794 cx.assert_editor_state(
12795 &r#"
12796 use some::modified;
12797
12798
12799 fn main() {
12800 ˇ println!("hello there");
12801
12802 println!("around the");
12803 println!("world");
12804 }
12805 "#
12806 .unindent(),
12807 );
12808
12809 cx.update_editor(|editor, window, cx| {
12810 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12811 });
12812
12813 cx.assert_editor_state(
12814 &r#"
12815 use some::modified;
12816
12817 ˇ
12818 fn main() {
12819 println!("hello there");
12820
12821 println!("around the");
12822 println!("world");
12823 }
12824 "#
12825 .unindent(),
12826 );
12827
12828 cx.update_editor(|editor, window, cx| {
12829 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12830 });
12831
12832 cx.assert_editor_state(
12833 &r#"
12834 ˇuse some::modified;
12835
12836
12837 fn main() {
12838 println!("hello there");
12839
12840 println!("around the");
12841 println!("world");
12842 }
12843 "#
12844 .unindent(),
12845 );
12846
12847 cx.update_editor(|editor, window, cx| {
12848 for _ in 0..2 {
12849 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12850 }
12851 });
12852
12853 cx.assert_editor_state(
12854 &r#"
12855 use some::modified;
12856
12857
12858 fn main() {
12859 ˇ println!("hello there");
12860
12861 println!("around the");
12862 println!("world");
12863 }
12864 "#
12865 .unindent(),
12866 );
12867
12868 cx.update_editor(|editor, window, cx| {
12869 editor.fold(&Fold, window, cx);
12870 });
12871
12872 cx.update_editor(|editor, window, cx| {
12873 editor.go_to_next_hunk(&GoToHunk, window, cx);
12874 });
12875
12876 cx.assert_editor_state(
12877 &r#"
12878 ˇuse some::modified;
12879
12880
12881 fn main() {
12882 println!("hello there");
12883
12884 println!("around the");
12885 println!("world");
12886 }
12887 "#
12888 .unindent(),
12889 );
12890}
12891
12892#[test]
12893fn test_split_words() {
12894 fn split(text: &str) -> Vec<&str> {
12895 split_words(text).collect()
12896 }
12897
12898 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12899 assert_eq!(split("hello_world"), &["hello_", "world"]);
12900 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12901 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12902 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12903 assert_eq!(split("helloworld"), &["helloworld"]);
12904
12905 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12906}
12907
12908#[gpui::test]
12909async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12910 init_test(cx, |_| {});
12911
12912 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12913 let mut assert = |before, after| {
12914 let _state_context = cx.set_state(before);
12915 cx.run_until_parked();
12916 cx.update_editor(|editor, window, cx| {
12917 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12918 });
12919 cx.run_until_parked();
12920 cx.assert_editor_state(after);
12921 };
12922
12923 // Outside bracket jumps to outside of matching bracket
12924 assert("console.logˇ(var);", "console.log(var)ˇ;");
12925 assert("console.log(var)ˇ;", "console.logˇ(var);");
12926
12927 // Inside bracket jumps to inside of matching bracket
12928 assert("console.log(ˇvar);", "console.log(varˇ);");
12929 assert("console.log(varˇ);", "console.log(ˇvar);");
12930
12931 // When outside a bracket and inside, favor jumping to the inside bracket
12932 assert(
12933 "console.log('foo', [1, 2, 3]ˇ);",
12934 "console.log(ˇ'foo', [1, 2, 3]);",
12935 );
12936 assert(
12937 "console.log(ˇ'foo', [1, 2, 3]);",
12938 "console.log('foo', [1, 2, 3]ˇ);",
12939 );
12940
12941 // Bias forward if two options are equally likely
12942 assert(
12943 "let result = curried_fun()ˇ();",
12944 "let result = curried_fun()()ˇ;",
12945 );
12946
12947 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12948 assert(
12949 indoc! {"
12950 function test() {
12951 console.log('test')ˇ
12952 }"},
12953 indoc! {"
12954 function test() {
12955 console.logˇ('test')
12956 }"},
12957 );
12958}
12959
12960#[gpui::test]
12961async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12962 init_test(cx, |_| {});
12963
12964 let fs = FakeFs::new(cx.executor());
12965 fs.insert_tree(
12966 path!("/a"),
12967 json!({
12968 "main.rs": "fn main() { let a = 5; }",
12969 "other.rs": "// Test file",
12970 }),
12971 )
12972 .await;
12973 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12974
12975 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12976 language_registry.add(Arc::new(Language::new(
12977 LanguageConfig {
12978 name: "Rust".into(),
12979 matcher: LanguageMatcher {
12980 path_suffixes: vec!["rs".to_string()],
12981 ..Default::default()
12982 },
12983 brackets: BracketPairConfig {
12984 pairs: vec![BracketPair {
12985 start: "{".to_string(),
12986 end: "}".to_string(),
12987 close: true,
12988 surround: true,
12989 newline: true,
12990 }],
12991 disabled_scopes_by_bracket_ix: Vec::new(),
12992 },
12993 ..Default::default()
12994 },
12995 Some(tree_sitter_rust::LANGUAGE.into()),
12996 )));
12997 let mut fake_servers = language_registry.register_fake_lsp(
12998 "Rust",
12999 FakeLspAdapter {
13000 capabilities: lsp::ServerCapabilities {
13001 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13002 first_trigger_character: "{".to_string(),
13003 more_trigger_character: None,
13004 }),
13005 ..Default::default()
13006 },
13007 ..Default::default()
13008 },
13009 );
13010
13011 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13012
13013 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13014
13015 let worktree_id = workspace
13016 .update(cx, |workspace, _, cx| {
13017 workspace.project().update(cx, |project, cx| {
13018 project.worktrees(cx).next().unwrap().read(cx).id()
13019 })
13020 })
13021 .unwrap();
13022
13023 let buffer = project
13024 .update(cx, |project, cx| {
13025 project.open_local_buffer(path!("/a/main.rs"), cx)
13026 })
13027 .await
13028 .unwrap();
13029 let editor_handle = workspace
13030 .update(cx, |workspace, window, cx| {
13031 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13032 })
13033 .unwrap()
13034 .await
13035 .unwrap()
13036 .downcast::<Editor>()
13037 .unwrap();
13038
13039 cx.executor().start_waiting();
13040 let fake_server = fake_servers.next().await.unwrap();
13041
13042 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13043 |params, _| async move {
13044 assert_eq!(
13045 params.text_document_position.text_document.uri,
13046 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13047 );
13048 assert_eq!(
13049 params.text_document_position.position,
13050 lsp::Position::new(0, 21),
13051 );
13052
13053 Ok(Some(vec![lsp::TextEdit {
13054 new_text: "]".to_string(),
13055 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13056 }]))
13057 },
13058 );
13059
13060 editor_handle.update_in(cx, |editor, window, cx| {
13061 window.focus(&editor.focus_handle(cx));
13062 editor.change_selections(None, window, cx, |s| {
13063 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13064 });
13065 editor.handle_input("{", window, cx);
13066 });
13067
13068 cx.executor().run_until_parked();
13069
13070 buffer.update(cx, |buffer, _| {
13071 assert_eq!(
13072 buffer.text(),
13073 "fn main() { let a = {5}; }",
13074 "No extra braces from on type formatting should appear in the buffer"
13075 )
13076 });
13077}
13078
13079#[gpui::test]
13080async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13081 init_test(cx, |_| {});
13082
13083 let fs = FakeFs::new(cx.executor());
13084 fs.insert_tree(
13085 path!("/a"),
13086 json!({
13087 "main.rs": "fn main() { let a = 5; }",
13088 "other.rs": "// Test file",
13089 }),
13090 )
13091 .await;
13092
13093 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13094
13095 let server_restarts = Arc::new(AtomicUsize::new(0));
13096 let closure_restarts = Arc::clone(&server_restarts);
13097 let language_server_name = "test language server";
13098 let language_name: LanguageName = "Rust".into();
13099
13100 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13101 language_registry.add(Arc::new(Language::new(
13102 LanguageConfig {
13103 name: language_name.clone(),
13104 matcher: LanguageMatcher {
13105 path_suffixes: vec!["rs".to_string()],
13106 ..Default::default()
13107 },
13108 ..Default::default()
13109 },
13110 Some(tree_sitter_rust::LANGUAGE.into()),
13111 )));
13112 let mut fake_servers = language_registry.register_fake_lsp(
13113 "Rust",
13114 FakeLspAdapter {
13115 name: language_server_name,
13116 initialization_options: Some(json!({
13117 "testOptionValue": true
13118 })),
13119 initializer: Some(Box::new(move |fake_server| {
13120 let task_restarts = Arc::clone(&closure_restarts);
13121 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13122 task_restarts.fetch_add(1, atomic::Ordering::Release);
13123 futures::future::ready(Ok(()))
13124 });
13125 })),
13126 ..Default::default()
13127 },
13128 );
13129
13130 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13131 let _buffer = project
13132 .update(cx, |project, cx| {
13133 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13134 })
13135 .await
13136 .unwrap();
13137 let _fake_server = fake_servers.next().await.unwrap();
13138 update_test_language_settings(cx, |language_settings| {
13139 language_settings.languages.insert(
13140 language_name.clone(),
13141 LanguageSettingsContent {
13142 tab_size: NonZeroU32::new(8),
13143 ..Default::default()
13144 },
13145 );
13146 });
13147 cx.executor().run_until_parked();
13148 assert_eq!(
13149 server_restarts.load(atomic::Ordering::Acquire),
13150 0,
13151 "Should not restart LSP server on an unrelated change"
13152 );
13153
13154 update_test_project_settings(cx, |project_settings| {
13155 project_settings.lsp.insert(
13156 "Some other server name".into(),
13157 LspSettings {
13158 binary: None,
13159 settings: None,
13160 initialization_options: Some(json!({
13161 "some other init value": false
13162 })),
13163 enable_lsp_tasks: false,
13164 },
13165 );
13166 });
13167 cx.executor().run_until_parked();
13168 assert_eq!(
13169 server_restarts.load(atomic::Ordering::Acquire),
13170 0,
13171 "Should not restart LSP server on an unrelated LSP settings change"
13172 );
13173
13174 update_test_project_settings(cx, |project_settings| {
13175 project_settings.lsp.insert(
13176 language_server_name.into(),
13177 LspSettings {
13178 binary: None,
13179 settings: None,
13180 initialization_options: Some(json!({
13181 "anotherInitValue": false
13182 })),
13183 enable_lsp_tasks: false,
13184 },
13185 );
13186 });
13187 cx.executor().run_until_parked();
13188 assert_eq!(
13189 server_restarts.load(atomic::Ordering::Acquire),
13190 1,
13191 "Should restart LSP server on a related LSP settings change"
13192 );
13193
13194 update_test_project_settings(cx, |project_settings| {
13195 project_settings.lsp.insert(
13196 language_server_name.into(),
13197 LspSettings {
13198 binary: None,
13199 settings: None,
13200 initialization_options: Some(json!({
13201 "anotherInitValue": false
13202 })),
13203 enable_lsp_tasks: false,
13204 },
13205 );
13206 });
13207 cx.executor().run_until_parked();
13208 assert_eq!(
13209 server_restarts.load(atomic::Ordering::Acquire),
13210 1,
13211 "Should not restart LSP server on a related LSP settings change that is the same"
13212 );
13213
13214 update_test_project_settings(cx, |project_settings| {
13215 project_settings.lsp.insert(
13216 language_server_name.into(),
13217 LspSettings {
13218 binary: None,
13219 settings: None,
13220 initialization_options: None,
13221 enable_lsp_tasks: false,
13222 },
13223 );
13224 });
13225 cx.executor().run_until_parked();
13226 assert_eq!(
13227 server_restarts.load(atomic::Ordering::Acquire),
13228 2,
13229 "Should restart LSP server on another related LSP settings change"
13230 );
13231}
13232
13233#[gpui::test]
13234async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13235 init_test(cx, |_| {});
13236
13237 let mut cx = EditorLspTestContext::new_rust(
13238 lsp::ServerCapabilities {
13239 completion_provider: Some(lsp::CompletionOptions {
13240 trigger_characters: Some(vec![".".to_string()]),
13241 resolve_provider: Some(true),
13242 ..Default::default()
13243 }),
13244 ..Default::default()
13245 },
13246 cx,
13247 )
13248 .await;
13249
13250 cx.set_state("fn main() { let a = 2ˇ; }");
13251 cx.simulate_keystroke(".");
13252 let completion_item = lsp::CompletionItem {
13253 label: "some".into(),
13254 kind: Some(lsp::CompletionItemKind::SNIPPET),
13255 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13256 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13257 kind: lsp::MarkupKind::Markdown,
13258 value: "```rust\nSome(2)\n```".to_string(),
13259 })),
13260 deprecated: Some(false),
13261 sort_text: Some("fffffff2".to_string()),
13262 filter_text: Some("some".to_string()),
13263 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13264 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13265 range: lsp::Range {
13266 start: lsp::Position {
13267 line: 0,
13268 character: 22,
13269 },
13270 end: lsp::Position {
13271 line: 0,
13272 character: 22,
13273 },
13274 },
13275 new_text: "Some(2)".to_string(),
13276 })),
13277 additional_text_edits: Some(vec![lsp::TextEdit {
13278 range: lsp::Range {
13279 start: lsp::Position {
13280 line: 0,
13281 character: 20,
13282 },
13283 end: lsp::Position {
13284 line: 0,
13285 character: 22,
13286 },
13287 },
13288 new_text: "".to_string(),
13289 }]),
13290 ..Default::default()
13291 };
13292
13293 let closure_completion_item = completion_item.clone();
13294 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13295 let task_completion_item = closure_completion_item.clone();
13296 async move {
13297 Ok(Some(lsp::CompletionResponse::Array(vec![
13298 task_completion_item,
13299 ])))
13300 }
13301 });
13302
13303 request.next().await;
13304
13305 cx.condition(|editor, _| editor.context_menu_visible())
13306 .await;
13307 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13308 editor
13309 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13310 .unwrap()
13311 });
13312 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13313
13314 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13315 let task_completion_item = completion_item.clone();
13316 async move { Ok(task_completion_item) }
13317 })
13318 .next()
13319 .await
13320 .unwrap();
13321 apply_additional_edits.await.unwrap();
13322 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13323}
13324
13325#[gpui::test]
13326async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13327 init_test(cx, |_| {});
13328
13329 let mut cx = EditorLspTestContext::new_rust(
13330 lsp::ServerCapabilities {
13331 completion_provider: Some(lsp::CompletionOptions {
13332 trigger_characters: Some(vec![".".to_string()]),
13333 resolve_provider: Some(true),
13334 ..Default::default()
13335 }),
13336 ..Default::default()
13337 },
13338 cx,
13339 )
13340 .await;
13341
13342 cx.set_state("fn main() { let a = 2ˇ; }");
13343 cx.simulate_keystroke(".");
13344
13345 let item1 = lsp::CompletionItem {
13346 label: "method id()".to_string(),
13347 filter_text: Some("id".to_string()),
13348 detail: None,
13349 documentation: None,
13350 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13351 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13352 new_text: ".id".to_string(),
13353 })),
13354 ..lsp::CompletionItem::default()
13355 };
13356
13357 let item2 = lsp::CompletionItem {
13358 label: "other".to_string(),
13359 filter_text: Some("other".to_string()),
13360 detail: None,
13361 documentation: None,
13362 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13363 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13364 new_text: ".other".to_string(),
13365 })),
13366 ..lsp::CompletionItem::default()
13367 };
13368
13369 let item1 = item1.clone();
13370 cx.set_request_handler::<lsp::request::Completion, _, _>({
13371 let item1 = item1.clone();
13372 move |_, _, _| {
13373 let item1 = item1.clone();
13374 let item2 = item2.clone();
13375 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13376 }
13377 })
13378 .next()
13379 .await;
13380
13381 cx.condition(|editor, _| editor.context_menu_visible())
13382 .await;
13383 cx.update_editor(|editor, _, _| {
13384 let context_menu = editor.context_menu.borrow_mut();
13385 let context_menu = context_menu
13386 .as_ref()
13387 .expect("Should have the context menu deployed");
13388 match context_menu {
13389 CodeContextMenu::Completions(completions_menu) => {
13390 let completions = completions_menu.completions.borrow_mut();
13391 assert_eq!(
13392 completions
13393 .iter()
13394 .map(|completion| &completion.label.text)
13395 .collect::<Vec<_>>(),
13396 vec!["method id()", "other"]
13397 )
13398 }
13399 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13400 }
13401 });
13402
13403 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13404 let item1 = item1.clone();
13405 move |_, item_to_resolve, _| {
13406 let item1 = item1.clone();
13407 async move {
13408 if item1 == item_to_resolve {
13409 Ok(lsp::CompletionItem {
13410 label: "method id()".to_string(),
13411 filter_text: Some("id".to_string()),
13412 detail: Some("Now resolved!".to_string()),
13413 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13414 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13415 range: lsp::Range::new(
13416 lsp::Position::new(0, 22),
13417 lsp::Position::new(0, 22),
13418 ),
13419 new_text: ".id".to_string(),
13420 })),
13421 ..lsp::CompletionItem::default()
13422 })
13423 } else {
13424 Ok(item_to_resolve)
13425 }
13426 }
13427 }
13428 })
13429 .next()
13430 .await
13431 .unwrap();
13432 cx.run_until_parked();
13433
13434 cx.update_editor(|editor, window, cx| {
13435 editor.context_menu_next(&Default::default(), window, cx);
13436 });
13437
13438 cx.update_editor(|editor, _, _| {
13439 let context_menu = editor.context_menu.borrow_mut();
13440 let context_menu = context_menu
13441 .as_ref()
13442 .expect("Should have the context menu deployed");
13443 match context_menu {
13444 CodeContextMenu::Completions(completions_menu) => {
13445 let completions = completions_menu.completions.borrow_mut();
13446 assert_eq!(
13447 completions
13448 .iter()
13449 .map(|completion| &completion.label.text)
13450 .collect::<Vec<_>>(),
13451 vec!["method id() Now resolved!", "other"],
13452 "Should update first completion label, but not second as the filter text did not match."
13453 );
13454 }
13455 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13456 }
13457 });
13458}
13459
13460#[gpui::test]
13461async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13462 init_test(cx, |_| {});
13463
13464 let mut cx = EditorLspTestContext::new_rust(
13465 lsp::ServerCapabilities {
13466 completion_provider: Some(lsp::CompletionOptions {
13467 trigger_characters: Some(vec![".".to_string()]),
13468 resolve_provider: Some(true),
13469 ..Default::default()
13470 }),
13471 ..Default::default()
13472 },
13473 cx,
13474 )
13475 .await;
13476
13477 cx.set_state("fn main() { let a = 2ˇ; }");
13478 cx.simulate_keystroke(".");
13479
13480 let unresolved_item_1 = lsp::CompletionItem {
13481 label: "id".to_string(),
13482 filter_text: Some("id".to_string()),
13483 detail: None,
13484 documentation: None,
13485 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13486 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13487 new_text: ".id".to_string(),
13488 })),
13489 ..lsp::CompletionItem::default()
13490 };
13491 let resolved_item_1 = lsp::CompletionItem {
13492 additional_text_edits: Some(vec![lsp::TextEdit {
13493 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13494 new_text: "!!".to_string(),
13495 }]),
13496 ..unresolved_item_1.clone()
13497 };
13498 let unresolved_item_2 = lsp::CompletionItem {
13499 label: "other".to_string(),
13500 filter_text: Some("other".to_string()),
13501 detail: None,
13502 documentation: None,
13503 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13504 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13505 new_text: ".other".to_string(),
13506 })),
13507 ..lsp::CompletionItem::default()
13508 };
13509 let resolved_item_2 = lsp::CompletionItem {
13510 additional_text_edits: Some(vec![lsp::TextEdit {
13511 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13512 new_text: "??".to_string(),
13513 }]),
13514 ..unresolved_item_2.clone()
13515 };
13516
13517 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13518 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13519 cx.lsp
13520 .server
13521 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13522 let unresolved_item_1 = unresolved_item_1.clone();
13523 let resolved_item_1 = resolved_item_1.clone();
13524 let unresolved_item_2 = unresolved_item_2.clone();
13525 let resolved_item_2 = resolved_item_2.clone();
13526 let resolve_requests_1 = resolve_requests_1.clone();
13527 let resolve_requests_2 = resolve_requests_2.clone();
13528 move |unresolved_request, _| {
13529 let unresolved_item_1 = unresolved_item_1.clone();
13530 let resolved_item_1 = resolved_item_1.clone();
13531 let unresolved_item_2 = unresolved_item_2.clone();
13532 let resolved_item_2 = resolved_item_2.clone();
13533 let resolve_requests_1 = resolve_requests_1.clone();
13534 let resolve_requests_2 = resolve_requests_2.clone();
13535 async move {
13536 if unresolved_request == unresolved_item_1 {
13537 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13538 Ok(resolved_item_1.clone())
13539 } else if unresolved_request == unresolved_item_2 {
13540 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13541 Ok(resolved_item_2.clone())
13542 } else {
13543 panic!("Unexpected completion item {unresolved_request:?}")
13544 }
13545 }
13546 }
13547 })
13548 .detach();
13549
13550 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13551 let unresolved_item_1 = unresolved_item_1.clone();
13552 let unresolved_item_2 = unresolved_item_2.clone();
13553 async move {
13554 Ok(Some(lsp::CompletionResponse::Array(vec![
13555 unresolved_item_1,
13556 unresolved_item_2,
13557 ])))
13558 }
13559 })
13560 .next()
13561 .await;
13562
13563 cx.condition(|editor, _| editor.context_menu_visible())
13564 .await;
13565 cx.update_editor(|editor, _, _| {
13566 let context_menu = editor.context_menu.borrow_mut();
13567 let context_menu = context_menu
13568 .as_ref()
13569 .expect("Should have the context menu deployed");
13570 match context_menu {
13571 CodeContextMenu::Completions(completions_menu) => {
13572 let completions = completions_menu.completions.borrow_mut();
13573 assert_eq!(
13574 completions
13575 .iter()
13576 .map(|completion| &completion.label.text)
13577 .collect::<Vec<_>>(),
13578 vec!["id", "other"]
13579 )
13580 }
13581 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13582 }
13583 });
13584 cx.run_until_parked();
13585
13586 cx.update_editor(|editor, window, cx| {
13587 editor.context_menu_next(&ContextMenuNext, window, cx);
13588 });
13589 cx.run_until_parked();
13590 cx.update_editor(|editor, window, cx| {
13591 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13592 });
13593 cx.run_until_parked();
13594 cx.update_editor(|editor, window, cx| {
13595 editor.context_menu_next(&ContextMenuNext, window, cx);
13596 });
13597 cx.run_until_parked();
13598 cx.update_editor(|editor, window, cx| {
13599 editor
13600 .compose_completion(&ComposeCompletion::default(), window, cx)
13601 .expect("No task returned")
13602 })
13603 .await
13604 .expect("Completion failed");
13605 cx.run_until_parked();
13606
13607 cx.update_editor(|editor, _, cx| {
13608 assert_eq!(
13609 resolve_requests_1.load(atomic::Ordering::Acquire),
13610 1,
13611 "Should always resolve once despite multiple selections"
13612 );
13613 assert_eq!(
13614 resolve_requests_2.load(atomic::Ordering::Acquire),
13615 1,
13616 "Should always resolve once after multiple selections and applying the completion"
13617 );
13618 assert_eq!(
13619 editor.text(cx),
13620 "fn main() { let a = ??.other; }",
13621 "Should use resolved data when applying the completion"
13622 );
13623 });
13624}
13625
13626#[gpui::test]
13627async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13628 init_test(cx, |_| {});
13629
13630 let item_0 = lsp::CompletionItem {
13631 label: "abs".into(),
13632 insert_text: Some("abs".into()),
13633 data: Some(json!({ "very": "special"})),
13634 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13635 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13636 lsp::InsertReplaceEdit {
13637 new_text: "abs".to_string(),
13638 insert: lsp::Range::default(),
13639 replace: lsp::Range::default(),
13640 },
13641 )),
13642 ..lsp::CompletionItem::default()
13643 };
13644 let items = iter::once(item_0.clone())
13645 .chain((11..51).map(|i| lsp::CompletionItem {
13646 label: format!("item_{}", i),
13647 insert_text: Some(format!("item_{}", i)),
13648 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13649 ..lsp::CompletionItem::default()
13650 }))
13651 .collect::<Vec<_>>();
13652
13653 let default_commit_characters = vec!["?".to_string()];
13654 let default_data = json!({ "default": "data"});
13655 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13656 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13657 let default_edit_range = lsp::Range {
13658 start: lsp::Position {
13659 line: 0,
13660 character: 5,
13661 },
13662 end: lsp::Position {
13663 line: 0,
13664 character: 5,
13665 },
13666 };
13667
13668 let mut cx = EditorLspTestContext::new_rust(
13669 lsp::ServerCapabilities {
13670 completion_provider: Some(lsp::CompletionOptions {
13671 trigger_characters: Some(vec![".".to_string()]),
13672 resolve_provider: Some(true),
13673 ..Default::default()
13674 }),
13675 ..Default::default()
13676 },
13677 cx,
13678 )
13679 .await;
13680
13681 cx.set_state("fn main() { let a = 2ˇ; }");
13682 cx.simulate_keystroke(".");
13683
13684 let completion_data = default_data.clone();
13685 let completion_characters = default_commit_characters.clone();
13686 let completion_items = items.clone();
13687 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13688 let default_data = completion_data.clone();
13689 let default_commit_characters = completion_characters.clone();
13690 let items = completion_items.clone();
13691 async move {
13692 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13693 items,
13694 item_defaults: Some(lsp::CompletionListItemDefaults {
13695 data: Some(default_data.clone()),
13696 commit_characters: Some(default_commit_characters.clone()),
13697 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13698 default_edit_range,
13699 )),
13700 insert_text_format: Some(default_insert_text_format),
13701 insert_text_mode: Some(default_insert_text_mode),
13702 }),
13703 ..lsp::CompletionList::default()
13704 })))
13705 }
13706 })
13707 .next()
13708 .await;
13709
13710 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13711 cx.lsp
13712 .server
13713 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13714 let closure_resolved_items = resolved_items.clone();
13715 move |item_to_resolve, _| {
13716 let closure_resolved_items = closure_resolved_items.clone();
13717 async move {
13718 closure_resolved_items.lock().push(item_to_resolve.clone());
13719 Ok(item_to_resolve)
13720 }
13721 }
13722 })
13723 .detach();
13724
13725 cx.condition(|editor, _| editor.context_menu_visible())
13726 .await;
13727 cx.run_until_parked();
13728 cx.update_editor(|editor, _, _| {
13729 let menu = editor.context_menu.borrow_mut();
13730 match menu.as_ref().expect("should have the completions menu") {
13731 CodeContextMenu::Completions(completions_menu) => {
13732 assert_eq!(
13733 completions_menu
13734 .entries
13735 .borrow()
13736 .iter()
13737 .map(|mat| mat.string.clone())
13738 .collect::<Vec<String>>(),
13739 items
13740 .iter()
13741 .map(|completion| completion.label.clone())
13742 .collect::<Vec<String>>()
13743 );
13744 }
13745 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13746 }
13747 });
13748 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13749 // with 4 from the end.
13750 assert_eq!(
13751 *resolved_items.lock(),
13752 [&items[0..16], &items[items.len() - 4..items.len()]]
13753 .concat()
13754 .iter()
13755 .cloned()
13756 .map(|mut item| {
13757 if item.data.is_none() {
13758 item.data = Some(default_data.clone());
13759 }
13760 item
13761 })
13762 .collect::<Vec<lsp::CompletionItem>>(),
13763 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13764 );
13765 resolved_items.lock().clear();
13766
13767 cx.update_editor(|editor, window, cx| {
13768 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13769 });
13770 cx.run_until_parked();
13771 // Completions that have already been resolved are skipped.
13772 assert_eq!(
13773 *resolved_items.lock(),
13774 items[items.len() - 16..items.len() - 4]
13775 .iter()
13776 .cloned()
13777 .map(|mut item| {
13778 if item.data.is_none() {
13779 item.data = Some(default_data.clone());
13780 }
13781 item
13782 })
13783 .collect::<Vec<lsp::CompletionItem>>()
13784 );
13785 resolved_items.lock().clear();
13786}
13787
13788#[gpui::test]
13789async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13790 init_test(cx, |_| {});
13791
13792 let mut cx = EditorLspTestContext::new(
13793 Language::new(
13794 LanguageConfig {
13795 matcher: LanguageMatcher {
13796 path_suffixes: vec!["jsx".into()],
13797 ..Default::default()
13798 },
13799 overrides: [(
13800 "element".into(),
13801 LanguageConfigOverride {
13802 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13803 ..Default::default()
13804 },
13805 )]
13806 .into_iter()
13807 .collect(),
13808 ..Default::default()
13809 },
13810 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13811 )
13812 .with_override_query("(jsx_self_closing_element) @element")
13813 .unwrap(),
13814 lsp::ServerCapabilities {
13815 completion_provider: Some(lsp::CompletionOptions {
13816 trigger_characters: Some(vec![":".to_string()]),
13817 ..Default::default()
13818 }),
13819 ..Default::default()
13820 },
13821 cx,
13822 )
13823 .await;
13824
13825 cx.lsp
13826 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13827 Ok(Some(lsp::CompletionResponse::Array(vec![
13828 lsp::CompletionItem {
13829 label: "bg-blue".into(),
13830 ..Default::default()
13831 },
13832 lsp::CompletionItem {
13833 label: "bg-red".into(),
13834 ..Default::default()
13835 },
13836 lsp::CompletionItem {
13837 label: "bg-yellow".into(),
13838 ..Default::default()
13839 },
13840 ])))
13841 });
13842
13843 cx.set_state(r#"<p class="bgˇ" />"#);
13844
13845 // Trigger completion when typing a dash, because the dash is an extra
13846 // word character in the 'element' scope, which contains the cursor.
13847 cx.simulate_keystroke("-");
13848 cx.executor().run_until_parked();
13849 cx.update_editor(|editor, _, _| {
13850 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13851 {
13852 assert_eq!(
13853 completion_menu_entries(&menu),
13854 &["bg-red", "bg-blue", "bg-yellow"]
13855 );
13856 } else {
13857 panic!("expected completion menu to be open");
13858 }
13859 });
13860
13861 cx.simulate_keystroke("l");
13862 cx.executor().run_until_parked();
13863 cx.update_editor(|editor, _, _| {
13864 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13865 {
13866 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13867 } else {
13868 panic!("expected completion menu to be open");
13869 }
13870 });
13871
13872 // When filtering completions, consider the character after the '-' to
13873 // be the start of a subword.
13874 cx.set_state(r#"<p class="yelˇ" />"#);
13875 cx.simulate_keystroke("l");
13876 cx.executor().run_until_parked();
13877 cx.update_editor(|editor, _, _| {
13878 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13879 {
13880 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13881 } else {
13882 panic!("expected completion menu to be open");
13883 }
13884 });
13885}
13886
13887fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13888 let entries = menu.entries.borrow();
13889 entries.iter().map(|mat| mat.string.clone()).collect()
13890}
13891
13892#[gpui::test]
13893async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13894 init_test(cx, |settings| {
13895 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13896 FormatterList(vec![Formatter::Prettier].into()),
13897 ))
13898 });
13899
13900 let fs = FakeFs::new(cx.executor());
13901 fs.insert_file(path!("/file.ts"), Default::default()).await;
13902
13903 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13904 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13905
13906 language_registry.add(Arc::new(Language::new(
13907 LanguageConfig {
13908 name: "TypeScript".into(),
13909 matcher: LanguageMatcher {
13910 path_suffixes: vec!["ts".to_string()],
13911 ..Default::default()
13912 },
13913 ..Default::default()
13914 },
13915 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13916 )));
13917 update_test_language_settings(cx, |settings| {
13918 settings.defaults.prettier = Some(PrettierSettings {
13919 allowed: true,
13920 ..PrettierSettings::default()
13921 });
13922 });
13923
13924 let test_plugin = "test_plugin";
13925 let _ = language_registry.register_fake_lsp(
13926 "TypeScript",
13927 FakeLspAdapter {
13928 prettier_plugins: vec![test_plugin],
13929 ..Default::default()
13930 },
13931 );
13932
13933 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13934 let buffer = project
13935 .update(cx, |project, cx| {
13936 project.open_local_buffer(path!("/file.ts"), cx)
13937 })
13938 .await
13939 .unwrap();
13940
13941 let buffer_text = "one\ntwo\nthree\n";
13942 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13943 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13944 editor.update_in(cx, |editor, window, cx| {
13945 editor.set_text(buffer_text, window, cx)
13946 });
13947
13948 editor
13949 .update_in(cx, |editor, window, cx| {
13950 editor.perform_format(
13951 project.clone(),
13952 FormatTrigger::Manual,
13953 FormatTarget::Buffers,
13954 window,
13955 cx,
13956 )
13957 })
13958 .unwrap()
13959 .await;
13960 assert_eq!(
13961 editor.update(cx, |editor, cx| editor.text(cx)),
13962 buffer_text.to_string() + prettier_format_suffix,
13963 "Test prettier formatting was not applied to the original buffer text",
13964 );
13965
13966 update_test_language_settings(cx, |settings| {
13967 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13968 });
13969 let format = editor.update_in(cx, |editor, window, cx| {
13970 editor.perform_format(
13971 project.clone(),
13972 FormatTrigger::Manual,
13973 FormatTarget::Buffers,
13974 window,
13975 cx,
13976 )
13977 });
13978 format.await.unwrap();
13979 assert_eq!(
13980 editor.update(cx, |editor, cx| editor.text(cx)),
13981 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13982 "Autoformatting (via test prettier) was not applied to the original buffer text",
13983 );
13984}
13985
13986#[gpui::test]
13987async fn test_addition_reverts(cx: &mut TestAppContext) {
13988 init_test(cx, |_| {});
13989 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13990 let base_text = indoc! {r#"
13991 struct Row;
13992 struct Row1;
13993 struct Row2;
13994
13995 struct Row4;
13996 struct Row5;
13997 struct Row6;
13998
13999 struct Row8;
14000 struct Row9;
14001 struct Row10;"#};
14002
14003 // When addition hunks are not adjacent to carets, no hunk revert is performed
14004 assert_hunk_revert(
14005 indoc! {r#"struct Row;
14006 struct Row1;
14007 struct Row1.1;
14008 struct Row1.2;
14009 struct Row2;ˇ
14010
14011 struct Row4;
14012 struct Row5;
14013 struct Row6;
14014
14015 struct Row8;
14016 ˇstruct Row9;
14017 struct Row9.1;
14018 struct Row9.2;
14019 struct Row9.3;
14020 struct Row10;"#},
14021 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14022 indoc! {r#"struct Row;
14023 struct Row1;
14024 struct Row1.1;
14025 struct Row1.2;
14026 struct Row2;ˇ
14027
14028 struct Row4;
14029 struct Row5;
14030 struct Row6;
14031
14032 struct Row8;
14033 ˇstruct Row9;
14034 struct Row9.1;
14035 struct Row9.2;
14036 struct Row9.3;
14037 struct Row10;"#},
14038 base_text,
14039 &mut cx,
14040 );
14041 // Same for selections
14042 assert_hunk_revert(
14043 indoc! {r#"struct Row;
14044 struct Row1;
14045 struct Row2;
14046 struct Row2.1;
14047 struct Row2.2;
14048 «ˇ
14049 struct Row4;
14050 struct» Row5;
14051 «struct Row6;
14052 ˇ»
14053 struct Row9.1;
14054 struct Row9.2;
14055 struct Row9.3;
14056 struct Row8;
14057 struct Row9;
14058 struct Row10;"#},
14059 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14060 indoc! {r#"struct Row;
14061 struct Row1;
14062 struct Row2;
14063 struct Row2.1;
14064 struct Row2.2;
14065 «ˇ
14066 struct Row4;
14067 struct» Row5;
14068 «struct Row6;
14069 ˇ»
14070 struct Row9.1;
14071 struct Row9.2;
14072 struct Row9.3;
14073 struct Row8;
14074 struct Row9;
14075 struct Row10;"#},
14076 base_text,
14077 &mut cx,
14078 );
14079
14080 // When carets and selections intersect the addition hunks, those are reverted.
14081 // Adjacent carets got merged.
14082 assert_hunk_revert(
14083 indoc! {r#"struct Row;
14084 ˇ// something on the top
14085 struct Row1;
14086 struct Row2;
14087 struct Roˇw3.1;
14088 struct Row2.2;
14089 struct Row2.3;ˇ
14090
14091 struct Row4;
14092 struct ˇRow5.1;
14093 struct Row5.2;
14094 struct «Rowˇ»5.3;
14095 struct Row5;
14096 struct Row6;
14097 ˇ
14098 struct Row9.1;
14099 struct «Rowˇ»9.2;
14100 struct «ˇRow»9.3;
14101 struct Row8;
14102 struct Row9;
14103 «ˇ// something on bottom»
14104 struct Row10;"#},
14105 vec![
14106 DiffHunkStatusKind::Added,
14107 DiffHunkStatusKind::Added,
14108 DiffHunkStatusKind::Added,
14109 DiffHunkStatusKind::Added,
14110 DiffHunkStatusKind::Added,
14111 ],
14112 indoc! {r#"struct Row;
14113 ˇstruct Row1;
14114 struct Row2;
14115 ˇ
14116 struct Row4;
14117 ˇstruct Row5;
14118 struct Row6;
14119 ˇ
14120 ˇstruct Row8;
14121 struct Row9;
14122 ˇstruct Row10;"#},
14123 base_text,
14124 &mut cx,
14125 );
14126}
14127
14128#[gpui::test]
14129async fn test_modification_reverts(cx: &mut TestAppContext) {
14130 init_test(cx, |_| {});
14131 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14132 let base_text = indoc! {r#"
14133 struct Row;
14134 struct Row1;
14135 struct Row2;
14136
14137 struct Row4;
14138 struct Row5;
14139 struct Row6;
14140
14141 struct Row8;
14142 struct Row9;
14143 struct Row10;"#};
14144
14145 // Modification hunks behave the same as the addition ones.
14146 assert_hunk_revert(
14147 indoc! {r#"struct Row;
14148 struct Row1;
14149 struct Row33;
14150 ˇ
14151 struct Row4;
14152 struct Row5;
14153 struct Row6;
14154 ˇ
14155 struct Row99;
14156 struct Row9;
14157 struct Row10;"#},
14158 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14159 indoc! {r#"struct Row;
14160 struct Row1;
14161 struct Row33;
14162 ˇ
14163 struct Row4;
14164 struct Row5;
14165 struct Row6;
14166 ˇ
14167 struct Row99;
14168 struct Row9;
14169 struct Row10;"#},
14170 base_text,
14171 &mut cx,
14172 );
14173 assert_hunk_revert(
14174 indoc! {r#"struct Row;
14175 struct Row1;
14176 struct Row33;
14177 «ˇ
14178 struct Row4;
14179 struct» Row5;
14180 «struct Row6;
14181 ˇ»
14182 struct Row99;
14183 struct Row9;
14184 struct Row10;"#},
14185 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14186 indoc! {r#"struct Row;
14187 struct Row1;
14188 struct Row33;
14189 «ˇ
14190 struct Row4;
14191 struct» Row5;
14192 «struct Row6;
14193 ˇ»
14194 struct Row99;
14195 struct Row9;
14196 struct Row10;"#},
14197 base_text,
14198 &mut cx,
14199 );
14200
14201 assert_hunk_revert(
14202 indoc! {r#"ˇstruct Row1.1;
14203 struct Row1;
14204 «ˇstr»uct Row22;
14205
14206 struct ˇRow44;
14207 struct Row5;
14208 struct «Rˇ»ow66;ˇ
14209
14210 «struˇ»ct Row88;
14211 struct Row9;
14212 struct Row1011;ˇ"#},
14213 vec![
14214 DiffHunkStatusKind::Modified,
14215 DiffHunkStatusKind::Modified,
14216 DiffHunkStatusKind::Modified,
14217 DiffHunkStatusKind::Modified,
14218 DiffHunkStatusKind::Modified,
14219 DiffHunkStatusKind::Modified,
14220 ],
14221 indoc! {r#"struct Row;
14222 ˇstruct Row1;
14223 struct Row2;
14224 ˇ
14225 struct Row4;
14226 ˇstruct Row5;
14227 struct Row6;
14228 ˇ
14229 struct Row8;
14230 ˇstruct Row9;
14231 struct Row10;ˇ"#},
14232 base_text,
14233 &mut cx,
14234 );
14235}
14236
14237#[gpui::test]
14238async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14239 init_test(cx, |_| {});
14240 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14241 let base_text = indoc! {r#"
14242 one
14243
14244 two
14245 three
14246 "#};
14247
14248 cx.set_head_text(base_text);
14249 cx.set_state("\nˇ\n");
14250 cx.executor().run_until_parked();
14251 cx.update_editor(|editor, _window, cx| {
14252 editor.expand_selected_diff_hunks(cx);
14253 });
14254 cx.executor().run_until_parked();
14255 cx.update_editor(|editor, window, cx| {
14256 editor.backspace(&Default::default(), window, cx);
14257 });
14258 cx.run_until_parked();
14259 cx.assert_state_with_diff(
14260 indoc! {r#"
14261
14262 - two
14263 - threeˇ
14264 +
14265 "#}
14266 .to_string(),
14267 );
14268}
14269
14270#[gpui::test]
14271async fn test_deletion_reverts(cx: &mut TestAppContext) {
14272 init_test(cx, |_| {});
14273 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14274 let base_text = indoc! {r#"struct Row;
14275struct Row1;
14276struct Row2;
14277
14278struct Row4;
14279struct Row5;
14280struct Row6;
14281
14282struct Row8;
14283struct Row9;
14284struct Row10;"#};
14285
14286 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14287 assert_hunk_revert(
14288 indoc! {r#"struct Row;
14289 struct Row2;
14290
14291 ˇstruct Row4;
14292 struct Row5;
14293 struct Row6;
14294 ˇ
14295 struct Row8;
14296 struct Row10;"#},
14297 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14298 indoc! {r#"struct Row;
14299 struct Row2;
14300
14301 ˇstruct Row4;
14302 struct Row5;
14303 struct Row6;
14304 ˇ
14305 struct Row8;
14306 struct Row10;"#},
14307 base_text,
14308 &mut cx,
14309 );
14310 assert_hunk_revert(
14311 indoc! {r#"struct Row;
14312 struct Row2;
14313
14314 «ˇstruct Row4;
14315 struct» Row5;
14316 «struct Row6;
14317 ˇ»
14318 struct Row8;
14319 struct Row10;"#},
14320 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14321 indoc! {r#"struct Row;
14322 struct Row2;
14323
14324 «ˇstruct Row4;
14325 struct» Row5;
14326 «struct Row6;
14327 ˇ»
14328 struct Row8;
14329 struct Row10;"#},
14330 base_text,
14331 &mut cx,
14332 );
14333
14334 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14335 assert_hunk_revert(
14336 indoc! {r#"struct Row;
14337 ˇstruct Row2;
14338
14339 struct Row4;
14340 struct Row5;
14341 struct Row6;
14342
14343 struct Row8;ˇ
14344 struct Row10;"#},
14345 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14346 indoc! {r#"struct Row;
14347 struct Row1;
14348 ˇstruct Row2;
14349
14350 struct Row4;
14351 struct Row5;
14352 struct Row6;
14353
14354 struct Row8;ˇ
14355 struct Row9;
14356 struct Row10;"#},
14357 base_text,
14358 &mut cx,
14359 );
14360 assert_hunk_revert(
14361 indoc! {r#"struct Row;
14362 struct Row2«ˇ;
14363 struct Row4;
14364 struct» Row5;
14365 «struct Row6;
14366
14367 struct Row8;ˇ»
14368 struct Row10;"#},
14369 vec![
14370 DiffHunkStatusKind::Deleted,
14371 DiffHunkStatusKind::Deleted,
14372 DiffHunkStatusKind::Deleted,
14373 ],
14374 indoc! {r#"struct Row;
14375 struct Row1;
14376 struct Row2«ˇ;
14377
14378 struct Row4;
14379 struct» Row5;
14380 «struct Row6;
14381
14382 struct Row8;ˇ»
14383 struct Row9;
14384 struct Row10;"#},
14385 base_text,
14386 &mut cx,
14387 );
14388}
14389
14390#[gpui::test]
14391async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14392 init_test(cx, |_| {});
14393
14394 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14395 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14396 let base_text_3 =
14397 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14398
14399 let text_1 = edit_first_char_of_every_line(base_text_1);
14400 let text_2 = edit_first_char_of_every_line(base_text_2);
14401 let text_3 = edit_first_char_of_every_line(base_text_3);
14402
14403 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14404 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14405 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14406
14407 let multibuffer = cx.new(|cx| {
14408 let mut multibuffer = MultiBuffer::new(ReadWrite);
14409 multibuffer.push_excerpts(
14410 buffer_1.clone(),
14411 [
14412 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14413 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14414 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14415 ],
14416 cx,
14417 );
14418 multibuffer.push_excerpts(
14419 buffer_2.clone(),
14420 [
14421 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14422 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14423 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14424 ],
14425 cx,
14426 );
14427 multibuffer.push_excerpts(
14428 buffer_3.clone(),
14429 [
14430 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14431 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14432 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14433 ],
14434 cx,
14435 );
14436 multibuffer
14437 });
14438
14439 let fs = FakeFs::new(cx.executor());
14440 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14441 let (editor, cx) = cx
14442 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14443 editor.update_in(cx, |editor, _window, cx| {
14444 for (buffer, diff_base) in [
14445 (buffer_1.clone(), base_text_1),
14446 (buffer_2.clone(), base_text_2),
14447 (buffer_3.clone(), base_text_3),
14448 ] {
14449 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14450 editor
14451 .buffer
14452 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14453 }
14454 });
14455 cx.executor().run_until_parked();
14456
14457 editor.update_in(cx, |editor, window, cx| {
14458 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}");
14459 editor.select_all(&SelectAll, window, cx);
14460 editor.git_restore(&Default::default(), window, cx);
14461 });
14462 cx.executor().run_until_parked();
14463
14464 // When all ranges are selected, all buffer hunks are reverted.
14465 editor.update(cx, |editor, cx| {
14466 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");
14467 });
14468 buffer_1.update(cx, |buffer, _| {
14469 assert_eq!(buffer.text(), base_text_1);
14470 });
14471 buffer_2.update(cx, |buffer, _| {
14472 assert_eq!(buffer.text(), base_text_2);
14473 });
14474 buffer_3.update(cx, |buffer, _| {
14475 assert_eq!(buffer.text(), base_text_3);
14476 });
14477
14478 editor.update_in(cx, |editor, window, cx| {
14479 editor.undo(&Default::default(), window, cx);
14480 });
14481
14482 editor.update_in(cx, |editor, window, cx| {
14483 editor.change_selections(None, window, cx, |s| {
14484 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14485 });
14486 editor.git_restore(&Default::default(), window, cx);
14487 });
14488
14489 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14490 // but not affect buffer_2 and its related excerpts.
14491 editor.update(cx, |editor, cx| {
14492 assert_eq!(
14493 editor.text(cx),
14494 "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}"
14495 );
14496 });
14497 buffer_1.update(cx, |buffer, _| {
14498 assert_eq!(buffer.text(), base_text_1);
14499 });
14500 buffer_2.update(cx, |buffer, _| {
14501 assert_eq!(
14502 buffer.text(),
14503 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14504 );
14505 });
14506 buffer_3.update(cx, |buffer, _| {
14507 assert_eq!(
14508 buffer.text(),
14509 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14510 );
14511 });
14512
14513 fn edit_first_char_of_every_line(text: &str) -> String {
14514 text.split('\n')
14515 .map(|line| format!("X{}", &line[1..]))
14516 .collect::<Vec<_>>()
14517 .join("\n")
14518 }
14519}
14520
14521#[gpui::test]
14522async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14523 init_test(cx, |_| {});
14524
14525 let cols = 4;
14526 let rows = 10;
14527 let sample_text_1 = sample_text(rows, cols, 'a');
14528 assert_eq!(
14529 sample_text_1,
14530 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14531 );
14532 let sample_text_2 = sample_text(rows, cols, 'l');
14533 assert_eq!(
14534 sample_text_2,
14535 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14536 );
14537 let sample_text_3 = sample_text(rows, cols, 'v');
14538 assert_eq!(
14539 sample_text_3,
14540 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14541 );
14542
14543 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14544 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14545 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14546
14547 let multi_buffer = cx.new(|cx| {
14548 let mut multibuffer = MultiBuffer::new(ReadWrite);
14549 multibuffer.push_excerpts(
14550 buffer_1.clone(),
14551 [
14552 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14553 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14554 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14555 ],
14556 cx,
14557 );
14558 multibuffer.push_excerpts(
14559 buffer_2.clone(),
14560 [
14561 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14562 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14563 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14564 ],
14565 cx,
14566 );
14567 multibuffer.push_excerpts(
14568 buffer_3.clone(),
14569 [
14570 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14571 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14572 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14573 ],
14574 cx,
14575 );
14576 multibuffer
14577 });
14578
14579 let fs = FakeFs::new(cx.executor());
14580 fs.insert_tree(
14581 "/a",
14582 json!({
14583 "main.rs": sample_text_1,
14584 "other.rs": sample_text_2,
14585 "lib.rs": sample_text_3,
14586 }),
14587 )
14588 .await;
14589 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14590 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14591 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14592 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14593 Editor::new(
14594 EditorMode::full(),
14595 multi_buffer,
14596 Some(project.clone()),
14597 window,
14598 cx,
14599 )
14600 });
14601 let multibuffer_item_id = workspace
14602 .update(cx, |workspace, window, cx| {
14603 assert!(
14604 workspace.active_item(cx).is_none(),
14605 "active item should be None before the first item is added"
14606 );
14607 workspace.add_item_to_active_pane(
14608 Box::new(multi_buffer_editor.clone()),
14609 None,
14610 true,
14611 window,
14612 cx,
14613 );
14614 let active_item = workspace
14615 .active_item(cx)
14616 .expect("should have an active item after adding the multi buffer");
14617 assert!(
14618 !active_item.is_singleton(cx),
14619 "A multi buffer was expected to active after adding"
14620 );
14621 active_item.item_id()
14622 })
14623 .unwrap();
14624 cx.executor().run_until_parked();
14625
14626 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14627 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14628 s.select_ranges(Some(1..2))
14629 });
14630 editor.open_excerpts(&OpenExcerpts, window, cx);
14631 });
14632 cx.executor().run_until_parked();
14633 let first_item_id = workspace
14634 .update(cx, |workspace, window, cx| {
14635 let active_item = workspace
14636 .active_item(cx)
14637 .expect("should have an active item after navigating into the 1st buffer");
14638 let first_item_id = active_item.item_id();
14639 assert_ne!(
14640 first_item_id, multibuffer_item_id,
14641 "Should navigate into the 1st buffer and activate it"
14642 );
14643 assert!(
14644 active_item.is_singleton(cx),
14645 "New active item should be a singleton buffer"
14646 );
14647 assert_eq!(
14648 active_item
14649 .act_as::<Editor>(cx)
14650 .expect("should have navigated into an editor for the 1st buffer")
14651 .read(cx)
14652 .text(cx),
14653 sample_text_1
14654 );
14655
14656 workspace
14657 .go_back(workspace.active_pane().downgrade(), window, cx)
14658 .detach_and_log_err(cx);
14659
14660 first_item_id
14661 })
14662 .unwrap();
14663 cx.executor().run_until_parked();
14664 workspace
14665 .update(cx, |workspace, _, cx| {
14666 let active_item = workspace
14667 .active_item(cx)
14668 .expect("should have an active item after navigating back");
14669 assert_eq!(
14670 active_item.item_id(),
14671 multibuffer_item_id,
14672 "Should navigate back to the multi buffer"
14673 );
14674 assert!(!active_item.is_singleton(cx));
14675 })
14676 .unwrap();
14677
14678 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14679 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14680 s.select_ranges(Some(39..40))
14681 });
14682 editor.open_excerpts(&OpenExcerpts, window, cx);
14683 });
14684 cx.executor().run_until_parked();
14685 let second_item_id = workspace
14686 .update(cx, |workspace, window, cx| {
14687 let active_item = workspace
14688 .active_item(cx)
14689 .expect("should have an active item after navigating into the 2nd buffer");
14690 let second_item_id = active_item.item_id();
14691 assert_ne!(
14692 second_item_id, multibuffer_item_id,
14693 "Should navigate away from the multibuffer"
14694 );
14695 assert_ne!(
14696 second_item_id, first_item_id,
14697 "Should navigate into the 2nd buffer and activate it"
14698 );
14699 assert!(
14700 active_item.is_singleton(cx),
14701 "New active item should be a singleton buffer"
14702 );
14703 assert_eq!(
14704 active_item
14705 .act_as::<Editor>(cx)
14706 .expect("should have navigated into an editor")
14707 .read(cx)
14708 .text(cx),
14709 sample_text_2
14710 );
14711
14712 workspace
14713 .go_back(workspace.active_pane().downgrade(), window, cx)
14714 .detach_and_log_err(cx);
14715
14716 second_item_id
14717 })
14718 .unwrap();
14719 cx.executor().run_until_parked();
14720 workspace
14721 .update(cx, |workspace, _, cx| {
14722 let active_item = workspace
14723 .active_item(cx)
14724 .expect("should have an active item after navigating back from the 2nd buffer");
14725 assert_eq!(
14726 active_item.item_id(),
14727 multibuffer_item_id,
14728 "Should navigate back from the 2nd buffer to the multi buffer"
14729 );
14730 assert!(!active_item.is_singleton(cx));
14731 })
14732 .unwrap();
14733
14734 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14735 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14736 s.select_ranges(Some(70..70))
14737 });
14738 editor.open_excerpts(&OpenExcerpts, window, cx);
14739 });
14740 cx.executor().run_until_parked();
14741 workspace
14742 .update(cx, |workspace, window, cx| {
14743 let active_item = workspace
14744 .active_item(cx)
14745 .expect("should have an active item after navigating into the 3rd buffer");
14746 let third_item_id = active_item.item_id();
14747 assert_ne!(
14748 third_item_id, multibuffer_item_id,
14749 "Should navigate into the 3rd buffer and activate it"
14750 );
14751 assert_ne!(third_item_id, first_item_id);
14752 assert_ne!(third_item_id, second_item_id);
14753 assert!(
14754 active_item.is_singleton(cx),
14755 "New active item should be a singleton buffer"
14756 );
14757 assert_eq!(
14758 active_item
14759 .act_as::<Editor>(cx)
14760 .expect("should have navigated into an editor")
14761 .read(cx)
14762 .text(cx),
14763 sample_text_3
14764 );
14765
14766 workspace
14767 .go_back(workspace.active_pane().downgrade(), window, cx)
14768 .detach_and_log_err(cx);
14769 })
14770 .unwrap();
14771 cx.executor().run_until_parked();
14772 workspace
14773 .update(cx, |workspace, _, cx| {
14774 let active_item = workspace
14775 .active_item(cx)
14776 .expect("should have an active item after navigating back from the 3rd buffer");
14777 assert_eq!(
14778 active_item.item_id(),
14779 multibuffer_item_id,
14780 "Should navigate back from the 3rd buffer to the multi buffer"
14781 );
14782 assert!(!active_item.is_singleton(cx));
14783 })
14784 .unwrap();
14785}
14786
14787#[gpui::test]
14788async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14789 init_test(cx, |_| {});
14790
14791 let mut cx = EditorTestContext::new(cx).await;
14792
14793 let diff_base = r#"
14794 use some::mod;
14795
14796 const A: u32 = 42;
14797
14798 fn main() {
14799 println!("hello");
14800
14801 println!("world");
14802 }
14803 "#
14804 .unindent();
14805
14806 cx.set_state(
14807 &r#"
14808 use some::modified;
14809
14810 ˇ
14811 fn main() {
14812 println!("hello there");
14813
14814 println!("around the");
14815 println!("world");
14816 }
14817 "#
14818 .unindent(),
14819 );
14820
14821 cx.set_head_text(&diff_base);
14822 executor.run_until_parked();
14823
14824 cx.update_editor(|editor, window, cx| {
14825 editor.go_to_next_hunk(&GoToHunk, window, cx);
14826 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14827 });
14828 executor.run_until_parked();
14829 cx.assert_state_with_diff(
14830 r#"
14831 use some::modified;
14832
14833
14834 fn main() {
14835 - println!("hello");
14836 + ˇ println!("hello there");
14837
14838 println!("around the");
14839 println!("world");
14840 }
14841 "#
14842 .unindent(),
14843 );
14844
14845 cx.update_editor(|editor, window, cx| {
14846 for _ in 0..2 {
14847 editor.go_to_next_hunk(&GoToHunk, window, cx);
14848 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14849 }
14850 });
14851 executor.run_until_parked();
14852 cx.assert_state_with_diff(
14853 r#"
14854 - use some::mod;
14855 + ˇuse some::modified;
14856
14857
14858 fn main() {
14859 - println!("hello");
14860 + println!("hello there");
14861
14862 + println!("around the");
14863 println!("world");
14864 }
14865 "#
14866 .unindent(),
14867 );
14868
14869 cx.update_editor(|editor, window, cx| {
14870 editor.go_to_next_hunk(&GoToHunk, window, cx);
14871 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14872 });
14873 executor.run_until_parked();
14874 cx.assert_state_with_diff(
14875 r#"
14876 - use some::mod;
14877 + use some::modified;
14878
14879 - const A: u32 = 42;
14880 ˇ
14881 fn main() {
14882 - println!("hello");
14883 + println!("hello there");
14884
14885 + println!("around the");
14886 println!("world");
14887 }
14888 "#
14889 .unindent(),
14890 );
14891
14892 cx.update_editor(|editor, window, cx| {
14893 editor.cancel(&Cancel, window, cx);
14894 });
14895
14896 cx.assert_state_with_diff(
14897 r#"
14898 use some::modified;
14899
14900 ˇ
14901 fn main() {
14902 println!("hello there");
14903
14904 println!("around the");
14905 println!("world");
14906 }
14907 "#
14908 .unindent(),
14909 );
14910}
14911
14912#[gpui::test]
14913async fn test_diff_base_change_with_expanded_diff_hunks(
14914 executor: BackgroundExecutor,
14915 cx: &mut TestAppContext,
14916) {
14917 init_test(cx, |_| {});
14918
14919 let mut cx = EditorTestContext::new(cx).await;
14920
14921 let diff_base = r#"
14922 use some::mod1;
14923 use some::mod2;
14924
14925 const A: u32 = 42;
14926 const B: u32 = 42;
14927 const C: u32 = 42;
14928
14929 fn main() {
14930 println!("hello");
14931
14932 println!("world");
14933 }
14934 "#
14935 .unindent();
14936
14937 cx.set_state(
14938 &r#"
14939 use some::mod2;
14940
14941 const A: u32 = 42;
14942 const C: u32 = 42;
14943
14944 fn main(ˇ) {
14945 //println!("hello");
14946
14947 println!("world");
14948 //
14949 //
14950 }
14951 "#
14952 .unindent(),
14953 );
14954
14955 cx.set_head_text(&diff_base);
14956 executor.run_until_parked();
14957
14958 cx.update_editor(|editor, window, cx| {
14959 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14960 });
14961 executor.run_until_parked();
14962 cx.assert_state_with_diff(
14963 r#"
14964 - use some::mod1;
14965 use some::mod2;
14966
14967 const A: u32 = 42;
14968 - const B: u32 = 42;
14969 const C: u32 = 42;
14970
14971 fn main(ˇ) {
14972 - println!("hello");
14973 + //println!("hello");
14974
14975 println!("world");
14976 + //
14977 + //
14978 }
14979 "#
14980 .unindent(),
14981 );
14982
14983 cx.set_head_text("new diff base!");
14984 executor.run_until_parked();
14985 cx.assert_state_with_diff(
14986 r#"
14987 - new diff base!
14988 + use some::mod2;
14989 +
14990 + const A: u32 = 42;
14991 + const C: u32 = 42;
14992 +
14993 + fn main(ˇ) {
14994 + //println!("hello");
14995 +
14996 + println!("world");
14997 + //
14998 + //
14999 + }
15000 "#
15001 .unindent(),
15002 );
15003}
15004
15005#[gpui::test]
15006async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15007 init_test(cx, |_| {});
15008
15009 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15010 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15011 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15012 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15013 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15014 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15015
15016 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15017 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15018 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15019
15020 let multi_buffer = cx.new(|cx| {
15021 let mut multibuffer = MultiBuffer::new(ReadWrite);
15022 multibuffer.push_excerpts(
15023 buffer_1.clone(),
15024 [
15025 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15026 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15027 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15028 ],
15029 cx,
15030 );
15031 multibuffer.push_excerpts(
15032 buffer_2.clone(),
15033 [
15034 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15035 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15036 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15037 ],
15038 cx,
15039 );
15040 multibuffer.push_excerpts(
15041 buffer_3.clone(),
15042 [
15043 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15044 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15045 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15046 ],
15047 cx,
15048 );
15049 multibuffer
15050 });
15051
15052 let editor =
15053 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15054 editor
15055 .update(cx, |editor, _window, cx| {
15056 for (buffer, diff_base) in [
15057 (buffer_1.clone(), file_1_old),
15058 (buffer_2.clone(), file_2_old),
15059 (buffer_3.clone(), file_3_old),
15060 ] {
15061 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15062 editor
15063 .buffer
15064 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15065 }
15066 })
15067 .unwrap();
15068
15069 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15070 cx.run_until_parked();
15071
15072 cx.assert_editor_state(
15073 &"
15074 ˇaaa
15075 ccc
15076 ddd
15077
15078 ggg
15079 hhh
15080
15081
15082 lll
15083 mmm
15084 NNN
15085
15086 qqq
15087 rrr
15088
15089 uuu
15090 111
15091 222
15092 333
15093
15094 666
15095 777
15096
15097 000
15098 !!!"
15099 .unindent(),
15100 );
15101
15102 cx.update_editor(|editor, window, cx| {
15103 editor.select_all(&SelectAll, window, cx);
15104 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15105 });
15106 cx.executor().run_until_parked();
15107
15108 cx.assert_state_with_diff(
15109 "
15110 «aaa
15111 - bbb
15112 ccc
15113 ddd
15114
15115 ggg
15116 hhh
15117
15118
15119 lll
15120 mmm
15121 - nnn
15122 + NNN
15123
15124 qqq
15125 rrr
15126
15127 uuu
15128 111
15129 222
15130 333
15131
15132 + 666
15133 777
15134
15135 000
15136 !!!ˇ»"
15137 .unindent(),
15138 );
15139}
15140
15141#[gpui::test]
15142async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15143 init_test(cx, |_| {});
15144
15145 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15146 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15147
15148 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15149 let multi_buffer = cx.new(|cx| {
15150 let mut multibuffer = MultiBuffer::new(ReadWrite);
15151 multibuffer.push_excerpts(
15152 buffer.clone(),
15153 [
15154 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15155 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15156 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15157 ],
15158 cx,
15159 );
15160 multibuffer
15161 });
15162
15163 let editor =
15164 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15165 editor
15166 .update(cx, |editor, _window, cx| {
15167 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15168 editor
15169 .buffer
15170 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15171 })
15172 .unwrap();
15173
15174 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15175 cx.run_until_parked();
15176
15177 cx.update_editor(|editor, window, cx| {
15178 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15179 });
15180 cx.executor().run_until_parked();
15181
15182 // When the start of a hunk coincides with the start of its excerpt,
15183 // the hunk is expanded. When the start of a a hunk is earlier than
15184 // the start of its excerpt, the hunk is not expanded.
15185 cx.assert_state_with_diff(
15186 "
15187 ˇaaa
15188 - bbb
15189 + BBB
15190
15191 - ddd
15192 - eee
15193 + DDD
15194 + EEE
15195 fff
15196
15197 iii
15198 "
15199 .unindent(),
15200 );
15201}
15202
15203#[gpui::test]
15204async fn test_edits_around_expanded_insertion_hunks(
15205 executor: BackgroundExecutor,
15206 cx: &mut TestAppContext,
15207) {
15208 init_test(cx, |_| {});
15209
15210 let mut cx = EditorTestContext::new(cx).await;
15211
15212 let diff_base = r#"
15213 use some::mod1;
15214 use some::mod2;
15215
15216 const A: u32 = 42;
15217
15218 fn main() {
15219 println!("hello");
15220
15221 println!("world");
15222 }
15223 "#
15224 .unindent();
15225 executor.run_until_parked();
15226 cx.set_state(
15227 &r#"
15228 use some::mod1;
15229 use some::mod2;
15230
15231 const A: u32 = 42;
15232 const B: u32 = 42;
15233 const C: u32 = 42;
15234 ˇ
15235
15236 fn main() {
15237 println!("hello");
15238
15239 println!("world");
15240 }
15241 "#
15242 .unindent(),
15243 );
15244
15245 cx.set_head_text(&diff_base);
15246 executor.run_until_parked();
15247
15248 cx.update_editor(|editor, window, cx| {
15249 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15250 });
15251 executor.run_until_parked();
15252
15253 cx.assert_state_with_diff(
15254 r#"
15255 use some::mod1;
15256 use some::mod2;
15257
15258 const A: u32 = 42;
15259 + const B: u32 = 42;
15260 + const C: u32 = 42;
15261 + ˇ
15262
15263 fn main() {
15264 println!("hello");
15265
15266 println!("world");
15267 }
15268 "#
15269 .unindent(),
15270 );
15271
15272 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15273 executor.run_until_parked();
15274
15275 cx.assert_state_with_diff(
15276 r#"
15277 use some::mod1;
15278 use some::mod2;
15279
15280 const A: u32 = 42;
15281 + const B: u32 = 42;
15282 + const C: u32 = 42;
15283 + const D: u32 = 42;
15284 + ˇ
15285
15286 fn main() {
15287 println!("hello");
15288
15289 println!("world");
15290 }
15291 "#
15292 .unindent(),
15293 );
15294
15295 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15296 executor.run_until_parked();
15297
15298 cx.assert_state_with_diff(
15299 r#"
15300 use some::mod1;
15301 use some::mod2;
15302
15303 const A: u32 = 42;
15304 + const B: u32 = 42;
15305 + const C: u32 = 42;
15306 + const D: u32 = 42;
15307 + const E: u32 = 42;
15308 + ˇ
15309
15310 fn main() {
15311 println!("hello");
15312
15313 println!("world");
15314 }
15315 "#
15316 .unindent(),
15317 );
15318
15319 cx.update_editor(|editor, window, cx| {
15320 editor.delete_line(&DeleteLine, window, cx);
15321 });
15322 executor.run_until_parked();
15323
15324 cx.assert_state_with_diff(
15325 r#"
15326 use some::mod1;
15327 use some::mod2;
15328
15329 const A: u32 = 42;
15330 + const B: u32 = 42;
15331 + const C: u32 = 42;
15332 + const D: u32 = 42;
15333 + const E: u32 = 42;
15334 ˇ
15335 fn main() {
15336 println!("hello");
15337
15338 println!("world");
15339 }
15340 "#
15341 .unindent(),
15342 );
15343
15344 cx.update_editor(|editor, window, cx| {
15345 editor.move_up(&MoveUp, window, cx);
15346 editor.delete_line(&DeleteLine, window, cx);
15347 editor.move_up(&MoveUp, window, cx);
15348 editor.delete_line(&DeleteLine, window, cx);
15349 editor.move_up(&MoveUp, window, cx);
15350 editor.delete_line(&DeleteLine, window, cx);
15351 });
15352 executor.run_until_parked();
15353 cx.assert_state_with_diff(
15354 r#"
15355 use some::mod1;
15356 use some::mod2;
15357
15358 const A: u32 = 42;
15359 + const B: u32 = 42;
15360 ˇ
15361 fn main() {
15362 println!("hello");
15363
15364 println!("world");
15365 }
15366 "#
15367 .unindent(),
15368 );
15369
15370 cx.update_editor(|editor, window, cx| {
15371 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15372 editor.delete_line(&DeleteLine, window, cx);
15373 });
15374 executor.run_until_parked();
15375 cx.assert_state_with_diff(
15376 r#"
15377 ˇ
15378 fn main() {
15379 println!("hello");
15380
15381 println!("world");
15382 }
15383 "#
15384 .unindent(),
15385 );
15386}
15387
15388#[gpui::test]
15389async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15390 init_test(cx, |_| {});
15391
15392 let mut cx = EditorTestContext::new(cx).await;
15393 cx.set_head_text(indoc! { "
15394 one
15395 two
15396 three
15397 four
15398 five
15399 "
15400 });
15401 cx.set_state(indoc! { "
15402 one
15403 ˇthree
15404 five
15405 "});
15406 cx.run_until_parked();
15407 cx.update_editor(|editor, window, cx| {
15408 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15409 });
15410 cx.assert_state_with_diff(
15411 indoc! { "
15412 one
15413 - two
15414 ˇthree
15415 - four
15416 five
15417 "}
15418 .to_string(),
15419 );
15420 cx.update_editor(|editor, window, cx| {
15421 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15422 });
15423
15424 cx.assert_state_with_diff(
15425 indoc! { "
15426 one
15427 ˇthree
15428 five
15429 "}
15430 .to_string(),
15431 );
15432
15433 cx.set_state(indoc! { "
15434 one
15435 ˇTWO
15436 three
15437 four
15438 five
15439 "});
15440 cx.run_until_parked();
15441 cx.update_editor(|editor, window, cx| {
15442 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15443 });
15444
15445 cx.assert_state_with_diff(
15446 indoc! { "
15447 one
15448 - two
15449 + ˇTWO
15450 three
15451 four
15452 five
15453 "}
15454 .to_string(),
15455 );
15456 cx.update_editor(|editor, window, cx| {
15457 editor.move_up(&Default::default(), window, cx);
15458 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15459 });
15460 cx.assert_state_with_diff(
15461 indoc! { "
15462 one
15463 ˇTWO
15464 three
15465 four
15466 five
15467 "}
15468 .to_string(),
15469 );
15470}
15471
15472#[gpui::test]
15473async fn test_edits_around_expanded_deletion_hunks(
15474 executor: BackgroundExecutor,
15475 cx: &mut TestAppContext,
15476) {
15477 init_test(cx, |_| {});
15478
15479 let mut cx = EditorTestContext::new(cx).await;
15480
15481 let diff_base = r#"
15482 use some::mod1;
15483 use some::mod2;
15484
15485 const A: u32 = 42;
15486 const B: u32 = 42;
15487 const C: u32 = 42;
15488
15489
15490 fn main() {
15491 println!("hello");
15492
15493 println!("world");
15494 }
15495 "#
15496 .unindent();
15497 executor.run_until_parked();
15498 cx.set_state(
15499 &r#"
15500 use some::mod1;
15501 use some::mod2;
15502
15503 ˇconst B: u32 = 42;
15504 const C: u32 = 42;
15505
15506
15507 fn main() {
15508 println!("hello");
15509
15510 println!("world");
15511 }
15512 "#
15513 .unindent(),
15514 );
15515
15516 cx.set_head_text(&diff_base);
15517 executor.run_until_parked();
15518
15519 cx.update_editor(|editor, window, cx| {
15520 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15521 });
15522 executor.run_until_parked();
15523
15524 cx.assert_state_with_diff(
15525 r#"
15526 use some::mod1;
15527 use some::mod2;
15528
15529 - const A: u32 = 42;
15530 ˇconst B: u32 = 42;
15531 const C: u32 = 42;
15532
15533
15534 fn main() {
15535 println!("hello");
15536
15537 println!("world");
15538 }
15539 "#
15540 .unindent(),
15541 );
15542
15543 cx.update_editor(|editor, window, cx| {
15544 editor.delete_line(&DeleteLine, window, cx);
15545 });
15546 executor.run_until_parked();
15547 cx.assert_state_with_diff(
15548 r#"
15549 use some::mod1;
15550 use some::mod2;
15551
15552 - const A: u32 = 42;
15553 - const B: u32 = 42;
15554 ˇconst C: u32 = 42;
15555
15556
15557 fn main() {
15558 println!("hello");
15559
15560 println!("world");
15561 }
15562 "#
15563 .unindent(),
15564 );
15565
15566 cx.update_editor(|editor, window, cx| {
15567 editor.delete_line(&DeleteLine, window, cx);
15568 });
15569 executor.run_until_parked();
15570 cx.assert_state_with_diff(
15571 r#"
15572 use some::mod1;
15573 use some::mod2;
15574
15575 - const A: u32 = 42;
15576 - const B: u32 = 42;
15577 - const C: u32 = 42;
15578 ˇ
15579
15580 fn main() {
15581 println!("hello");
15582
15583 println!("world");
15584 }
15585 "#
15586 .unindent(),
15587 );
15588
15589 cx.update_editor(|editor, window, cx| {
15590 editor.handle_input("replacement", window, cx);
15591 });
15592 executor.run_until_parked();
15593 cx.assert_state_with_diff(
15594 r#"
15595 use some::mod1;
15596 use some::mod2;
15597
15598 - const A: u32 = 42;
15599 - const B: u32 = 42;
15600 - const C: u32 = 42;
15601 -
15602 + replacementˇ
15603
15604 fn main() {
15605 println!("hello");
15606
15607 println!("world");
15608 }
15609 "#
15610 .unindent(),
15611 );
15612}
15613
15614#[gpui::test]
15615async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15616 init_test(cx, |_| {});
15617
15618 let mut cx = EditorTestContext::new(cx).await;
15619
15620 let base_text = r#"
15621 one
15622 two
15623 three
15624 four
15625 five
15626 "#
15627 .unindent();
15628 executor.run_until_parked();
15629 cx.set_state(
15630 &r#"
15631 one
15632 two
15633 fˇour
15634 five
15635 "#
15636 .unindent(),
15637 );
15638
15639 cx.set_head_text(&base_text);
15640 executor.run_until_parked();
15641
15642 cx.update_editor(|editor, window, cx| {
15643 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15644 });
15645 executor.run_until_parked();
15646
15647 cx.assert_state_with_diff(
15648 r#"
15649 one
15650 two
15651 - three
15652 fˇour
15653 five
15654 "#
15655 .unindent(),
15656 );
15657
15658 cx.update_editor(|editor, window, cx| {
15659 editor.backspace(&Backspace, window, cx);
15660 editor.backspace(&Backspace, window, cx);
15661 });
15662 executor.run_until_parked();
15663 cx.assert_state_with_diff(
15664 r#"
15665 one
15666 two
15667 - threeˇ
15668 - four
15669 + our
15670 five
15671 "#
15672 .unindent(),
15673 );
15674}
15675
15676#[gpui::test]
15677async fn test_edit_after_expanded_modification_hunk(
15678 executor: BackgroundExecutor,
15679 cx: &mut TestAppContext,
15680) {
15681 init_test(cx, |_| {});
15682
15683 let mut cx = EditorTestContext::new(cx).await;
15684
15685 let diff_base = r#"
15686 use some::mod1;
15687 use some::mod2;
15688
15689 const A: u32 = 42;
15690 const B: u32 = 42;
15691 const C: u32 = 42;
15692 const D: u32 = 42;
15693
15694
15695 fn main() {
15696 println!("hello");
15697
15698 println!("world");
15699 }"#
15700 .unindent();
15701
15702 cx.set_state(
15703 &r#"
15704 use some::mod1;
15705 use some::mod2;
15706
15707 const A: u32 = 42;
15708 const B: u32 = 42;
15709 const C: u32 = 43ˇ
15710 const D: u32 = 42;
15711
15712
15713 fn main() {
15714 println!("hello");
15715
15716 println!("world");
15717 }"#
15718 .unindent(),
15719 );
15720
15721 cx.set_head_text(&diff_base);
15722 executor.run_until_parked();
15723 cx.update_editor(|editor, window, cx| {
15724 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15725 });
15726 executor.run_until_parked();
15727
15728 cx.assert_state_with_diff(
15729 r#"
15730 use some::mod1;
15731 use some::mod2;
15732
15733 const A: u32 = 42;
15734 const B: u32 = 42;
15735 - const C: u32 = 42;
15736 + const C: u32 = 43ˇ
15737 const D: u32 = 42;
15738
15739
15740 fn main() {
15741 println!("hello");
15742
15743 println!("world");
15744 }"#
15745 .unindent(),
15746 );
15747
15748 cx.update_editor(|editor, window, cx| {
15749 editor.handle_input("\nnew_line\n", window, cx);
15750 });
15751 executor.run_until_parked();
15752
15753 cx.assert_state_with_diff(
15754 r#"
15755 use some::mod1;
15756 use some::mod2;
15757
15758 const A: u32 = 42;
15759 const B: u32 = 42;
15760 - const C: u32 = 42;
15761 + const C: u32 = 43
15762 + new_line
15763 + ˇ
15764 const D: u32 = 42;
15765
15766
15767 fn main() {
15768 println!("hello");
15769
15770 println!("world");
15771 }"#
15772 .unindent(),
15773 );
15774}
15775
15776#[gpui::test]
15777async fn test_stage_and_unstage_added_file_hunk(
15778 executor: BackgroundExecutor,
15779 cx: &mut TestAppContext,
15780) {
15781 init_test(cx, |_| {});
15782
15783 let mut cx = EditorTestContext::new(cx).await;
15784 cx.update_editor(|editor, _, cx| {
15785 editor.set_expand_all_diff_hunks(cx);
15786 });
15787
15788 let working_copy = r#"
15789 ˇfn main() {
15790 println!("hello, world!");
15791 }
15792 "#
15793 .unindent();
15794
15795 cx.set_state(&working_copy);
15796 executor.run_until_parked();
15797
15798 cx.assert_state_with_diff(
15799 r#"
15800 + ˇfn main() {
15801 + println!("hello, world!");
15802 + }
15803 "#
15804 .unindent(),
15805 );
15806 cx.assert_index_text(None);
15807
15808 cx.update_editor(|editor, window, cx| {
15809 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15810 });
15811 executor.run_until_parked();
15812 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15813 cx.assert_state_with_diff(
15814 r#"
15815 + ˇfn main() {
15816 + println!("hello, world!");
15817 + }
15818 "#
15819 .unindent(),
15820 );
15821
15822 cx.update_editor(|editor, window, cx| {
15823 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15824 });
15825 executor.run_until_parked();
15826 cx.assert_index_text(None);
15827}
15828
15829async fn setup_indent_guides_editor(
15830 text: &str,
15831 cx: &mut TestAppContext,
15832) -> (BufferId, EditorTestContext) {
15833 init_test(cx, |_| {});
15834
15835 let mut cx = EditorTestContext::new(cx).await;
15836
15837 let buffer_id = cx.update_editor(|editor, window, cx| {
15838 editor.set_text(text, window, cx);
15839 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15840
15841 buffer_ids[0]
15842 });
15843
15844 (buffer_id, cx)
15845}
15846
15847fn assert_indent_guides(
15848 range: Range<u32>,
15849 expected: Vec<IndentGuide>,
15850 active_indices: Option<Vec<usize>>,
15851 cx: &mut EditorTestContext,
15852) {
15853 let indent_guides = cx.update_editor(|editor, window, cx| {
15854 let snapshot = editor.snapshot(window, cx).display_snapshot;
15855 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15856 editor,
15857 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15858 true,
15859 &snapshot,
15860 cx,
15861 );
15862
15863 indent_guides.sort_by(|a, b| {
15864 a.depth.cmp(&b.depth).then(
15865 a.start_row
15866 .cmp(&b.start_row)
15867 .then(a.end_row.cmp(&b.end_row)),
15868 )
15869 });
15870 indent_guides
15871 });
15872
15873 if let Some(expected) = active_indices {
15874 let active_indices = cx.update_editor(|editor, window, cx| {
15875 let snapshot = editor.snapshot(window, cx).display_snapshot;
15876 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15877 });
15878
15879 assert_eq!(
15880 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15881 expected,
15882 "Active indent guide indices do not match"
15883 );
15884 }
15885
15886 assert_eq!(indent_guides, expected, "Indent guides do not match");
15887}
15888
15889fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15890 IndentGuide {
15891 buffer_id,
15892 start_row: MultiBufferRow(start_row),
15893 end_row: MultiBufferRow(end_row),
15894 depth,
15895 tab_size: 4,
15896 settings: IndentGuideSettings {
15897 enabled: true,
15898 line_width: 1,
15899 active_line_width: 1,
15900 ..Default::default()
15901 },
15902 }
15903}
15904
15905#[gpui::test]
15906async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15907 let (buffer_id, mut cx) = setup_indent_guides_editor(
15908 &"
15909 fn main() {
15910 let a = 1;
15911 }"
15912 .unindent(),
15913 cx,
15914 )
15915 .await;
15916
15917 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15918}
15919
15920#[gpui::test]
15921async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15922 let (buffer_id, mut cx) = setup_indent_guides_editor(
15923 &"
15924 fn main() {
15925 let a = 1;
15926 let b = 2;
15927 }"
15928 .unindent(),
15929 cx,
15930 )
15931 .await;
15932
15933 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15934}
15935
15936#[gpui::test]
15937async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15938 let (buffer_id, mut cx) = setup_indent_guides_editor(
15939 &"
15940 fn main() {
15941 let a = 1;
15942 if a == 3 {
15943 let b = 2;
15944 } else {
15945 let c = 3;
15946 }
15947 }"
15948 .unindent(),
15949 cx,
15950 )
15951 .await;
15952
15953 assert_indent_guides(
15954 0..8,
15955 vec![
15956 indent_guide(buffer_id, 1, 6, 0),
15957 indent_guide(buffer_id, 3, 3, 1),
15958 indent_guide(buffer_id, 5, 5, 1),
15959 ],
15960 None,
15961 &mut cx,
15962 );
15963}
15964
15965#[gpui::test]
15966async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15967 let (buffer_id, mut cx) = setup_indent_guides_editor(
15968 &"
15969 fn main() {
15970 let a = 1;
15971 let b = 2;
15972 let c = 3;
15973 }"
15974 .unindent(),
15975 cx,
15976 )
15977 .await;
15978
15979 assert_indent_guides(
15980 0..5,
15981 vec![
15982 indent_guide(buffer_id, 1, 3, 0),
15983 indent_guide(buffer_id, 2, 2, 1),
15984 ],
15985 None,
15986 &mut cx,
15987 );
15988}
15989
15990#[gpui::test]
15991async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15992 let (buffer_id, mut cx) = setup_indent_guides_editor(
15993 &"
15994 fn main() {
15995 let a = 1;
15996
15997 let c = 3;
15998 }"
15999 .unindent(),
16000 cx,
16001 )
16002 .await;
16003
16004 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16005}
16006
16007#[gpui::test]
16008async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16009 let (buffer_id, mut cx) = setup_indent_guides_editor(
16010 &"
16011 fn main() {
16012 let a = 1;
16013
16014 let c = 3;
16015
16016 if a == 3 {
16017 let b = 2;
16018 } else {
16019 let c = 3;
16020 }
16021 }"
16022 .unindent(),
16023 cx,
16024 )
16025 .await;
16026
16027 assert_indent_guides(
16028 0..11,
16029 vec![
16030 indent_guide(buffer_id, 1, 9, 0),
16031 indent_guide(buffer_id, 6, 6, 1),
16032 indent_guide(buffer_id, 8, 8, 1),
16033 ],
16034 None,
16035 &mut cx,
16036 );
16037}
16038
16039#[gpui::test]
16040async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16041 let (buffer_id, mut cx) = setup_indent_guides_editor(
16042 &"
16043 fn main() {
16044 let a = 1;
16045
16046 let c = 3;
16047
16048 if a == 3 {
16049 let b = 2;
16050 } else {
16051 let c = 3;
16052 }
16053 }"
16054 .unindent(),
16055 cx,
16056 )
16057 .await;
16058
16059 assert_indent_guides(
16060 1..11,
16061 vec![
16062 indent_guide(buffer_id, 1, 9, 0),
16063 indent_guide(buffer_id, 6, 6, 1),
16064 indent_guide(buffer_id, 8, 8, 1),
16065 ],
16066 None,
16067 &mut cx,
16068 );
16069}
16070
16071#[gpui::test]
16072async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16073 let (buffer_id, mut cx) = setup_indent_guides_editor(
16074 &"
16075 fn main() {
16076 let a = 1;
16077
16078 let c = 3;
16079
16080 if a == 3 {
16081 let b = 2;
16082 } else {
16083 let c = 3;
16084 }
16085 }"
16086 .unindent(),
16087 cx,
16088 )
16089 .await;
16090
16091 assert_indent_guides(
16092 1..10,
16093 vec![
16094 indent_guide(buffer_id, 1, 9, 0),
16095 indent_guide(buffer_id, 6, 6, 1),
16096 indent_guide(buffer_id, 8, 8, 1),
16097 ],
16098 None,
16099 &mut cx,
16100 );
16101}
16102
16103#[gpui::test]
16104async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16105 let (buffer_id, mut cx) = setup_indent_guides_editor(
16106 &"
16107 block1
16108 block2
16109 block3
16110 block4
16111 block2
16112 block1
16113 block1"
16114 .unindent(),
16115 cx,
16116 )
16117 .await;
16118
16119 assert_indent_guides(
16120 1..10,
16121 vec![
16122 indent_guide(buffer_id, 1, 4, 0),
16123 indent_guide(buffer_id, 2, 3, 1),
16124 indent_guide(buffer_id, 3, 3, 2),
16125 ],
16126 None,
16127 &mut cx,
16128 );
16129}
16130
16131#[gpui::test]
16132async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16133 let (buffer_id, mut cx) = setup_indent_guides_editor(
16134 &"
16135 block1
16136 block2
16137 block3
16138
16139 block1
16140 block1"
16141 .unindent(),
16142 cx,
16143 )
16144 .await;
16145
16146 assert_indent_guides(
16147 0..6,
16148 vec![
16149 indent_guide(buffer_id, 1, 2, 0),
16150 indent_guide(buffer_id, 2, 2, 1),
16151 ],
16152 None,
16153 &mut cx,
16154 );
16155}
16156
16157#[gpui::test]
16158async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16159 let (buffer_id, mut cx) = setup_indent_guides_editor(
16160 &"
16161 block1
16162
16163
16164
16165 block2
16166 "
16167 .unindent(),
16168 cx,
16169 )
16170 .await;
16171
16172 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16173}
16174
16175#[gpui::test]
16176async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16177 let (buffer_id, mut cx) = setup_indent_guides_editor(
16178 &"
16179 def a:
16180 \tb = 3
16181 \tif True:
16182 \t\tc = 4
16183 \t\td = 5
16184 \tprint(b)
16185 "
16186 .unindent(),
16187 cx,
16188 )
16189 .await;
16190
16191 assert_indent_guides(
16192 0..6,
16193 vec![
16194 indent_guide(buffer_id, 1, 6, 0),
16195 indent_guide(buffer_id, 3, 4, 1),
16196 ],
16197 None,
16198 &mut cx,
16199 );
16200}
16201
16202#[gpui::test]
16203async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16204 let (buffer_id, mut cx) = setup_indent_guides_editor(
16205 &"
16206 fn main() {
16207 let a = 1;
16208 }"
16209 .unindent(),
16210 cx,
16211 )
16212 .await;
16213
16214 cx.update_editor(|editor, window, cx| {
16215 editor.change_selections(None, window, cx, |s| {
16216 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16217 });
16218 });
16219
16220 assert_indent_guides(
16221 0..3,
16222 vec![indent_guide(buffer_id, 1, 1, 0)],
16223 Some(vec![0]),
16224 &mut cx,
16225 );
16226}
16227
16228#[gpui::test]
16229async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16230 let (buffer_id, mut cx) = setup_indent_guides_editor(
16231 &"
16232 fn main() {
16233 if 1 == 2 {
16234 let a = 1;
16235 }
16236 }"
16237 .unindent(),
16238 cx,
16239 )
16240 .await;
16241
16242 cx.update_editor(|editor, window, cx| {
16243 editor.change_selections(None, window, cx, |s| {
16244 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16245 });
16246 });
16247
16248 assert_indent_guides(
16249 0..4,
16250 vec![
16251 indent_guide(buffer_id, 1, 3, 0),
16252 indent_guide(buffer_id, 2, 2, 1),
16253 ],
16254 Some(vec![1]),
16255 &mut cx,
16256 );
16257
16258 cx.update_editor(|editor, window, cx| {
16259 editor.change_selections(None, window, cx, |s| {
16260 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16261 });
16262 });
16263
16264 assert_indent_guides(
16265 0..4,
16266 vec![
16267 indent_guide(buffer_id, 1, 3, 0),
16268 indent_guide(buffer_id, 2, 2, 1),
16269 ],
16270 Some(vec![1]),
16271 &mut cx,
16272 );
16273
16274 cx.update_editor(|editor, window, cx| {
16275 editor.change_selections(None, window, cx, |s| {
16276 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16277 });
16278 });
16279
16280 assert_indent_guides(
16281 0..4,
16282 vec![
16283 indent_guide(buffer_id, 1, 3, 0),
16284 indent_guide(buffer_id, 2, 2, 1),
16285 ],
16286 Some(vec![0]),
16287 &mut cx,
16288 );
16289}
16290
16291#[gpui::test]
16292async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16293 let (buffer_id, mut cx) = setup_indent_guides_editor(
16294 &"
16295 fn main() {
16296 let a = 1;
16297
16298 let b = 2;
16299 }"
16300 .unindent(),
16301 cx,
16302 )
16303 .await;
16304
16305 cx.update_editor(|editor, window, cx| {
16306 editor.change_selections(None, window, cx, |s| {
16307 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16308 });
16309 });
16310
16311 assert_indent_guides(
16312 0..5,
16313 vec![indent_guide(buffer_id, 1, 3, 0)],
16314 Some(vec![0]),
16315 &mut cx,
16316 );
16317}
16318
16319#[gpui::test]
16320async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16321 let (buffer_id, mut cx) = setup_indent_guides_editor(
16322 &"
16323 def m:
16324 a = 1
16325 pass"
16326 .unindent(),
16327 cx,
16328 )
16329 .await;
16330
16331 cx.update_editor(|editor, window, cx| {
16332 editor.change_selections(None, window, cx, |s| {
16333 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16334 });
16335 });
16336
16337 assert_indent_guides(
16338 0..3,
16339 vec![indent_guide(buffer_id, 1, 2, 0)],
16340 Some(vec![0]),
16341 &mut cx,
16342 );
16343}
16344
16345#[gpui::test]
16346async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16347 init_test(cx, |_| {});
16348 let mut cx = EditorTestContext::new(cx).await;
16349 let text = indoc! {
16350 "
16351 impl A {
16352 fn b() {
16353 0;
16354 3;
16355 5;
16356 6;
16357 7;
16358 }
16359 }
16360 "
16361 };
16362 let base_text = indoc! {
16363 "
16364 impl A {
16365 fn b() {
16366 0;
16367 1;
16368 2;
16369 3;
16370 4;
16371 }
16372 fn c() {
16373 5;
16374 6;
16375 7;
16376 }
16377 }
16378 "
16379 };
16380
16381 cx.update_editor(|editor, window, cx| {
16382 editor.set_text(text, window, cx);
16383
16384 editor.buffer().update(cx, |multibuffer, cx| {
16385 let buffer = multibuffer.as_singleton().unwrap();
16386 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16387
16388 multibuffer.set_all_diff_hunks_expanded(cx);
16389 multibuffer.add_diff(diff, cx);
16390
16391 buffer.read(cx).remote_id()
16392 })
16393 });
16394 cx.run_until_parked();
16395
16396 cx.assert_state_with_diff(
16397 indoc! { "
16398 impl A {
16399 fn b() {
16400 0;
16401 - 1;
16402 - 2;
16403 3;
16404 - 4;
16405 - }
16406 - fn c() {
16407 5;
16408 6;
16409 7;
16410 }
16411 }
16412 ˇ"
16413 }
16414 .to_string(),
16415 );
16416
16417 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16418 editor
16419 .snapshot(window, cx)
16420 .buffer_snapshot
16421 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16422 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16423 .collect::<Vec<_>>()
16424 });
16425 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16426 assert_eq!(
16427 actual_guides,
16428 vec![
16429 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16430 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16431 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16432 ]
16433 );
16434}
16435
16436#[gpui::test]
16437async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16438 init_test(cx, |_| {});
16439 let mut cx = EditorTestContext::new(cx).await;
16440
16441 let diff_base = r#"
16442 a
16443 b
16444 c
16445 "#
16446 .unindent();
16447
16448 cx.set_state(
16449 &r#"
16450 ˇA
16451 b
16452 C
16453 "#
16454 .unindent(),
16455 );
16456 cx.set_head_text(&diff_base);
16457 cx.update_editor(|editor, window, cx| {
16458 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16459 });
16460 executor.run_until_parked();
16461
16462 let both_hunks_expanded = r#"
16463 - a
16464 + ˇA
16465 b
16466 - c
16467 + C
16468 "#
16469 .unindent();
16470
16471 cx.assert_state_with_diff(both_hunks_expanded.clone());
16472
16473 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16474 let snapshot = editor.snapshot(window, cx);
16475 let hunks = editor
16476 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16477 .collect::<Vec<_>>();
16478 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16479 let buffer_id = hunks[0].buffer_id;
16480 hunks
16481 .into_iter()
16482 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16483 .collect::<Vec<_>>()
16484 });
16485 assert_eq!(hunk_ranges.len(), 2);
16486
16487 cx.update_editor(|editor, _, cx| {
16488 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16489 });
16490 executor.run_until_parked();
16491
16492 let second_hunk_expanded = r#"
16493 ˇA
16494 b
16495 - c
16496 + C
16497 "#
16498 .unindent();
16499
16500 cx.assert_state_with_diff(second_hunk_expanded);
16501
16502 cx.update_editor(|editor, _, cx| {
16503 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16504 });
16505 executor.run_until_parked();
16506
16507 cx.assert_state_with_diff(both_hunks_expanded.clone());
16508
16509 cx.update_editor(|editor, _, cx| {
16510 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16511 });
16512 executor.run_until_parked();
16513
16514 let first_hunk_expanded = r#"
16515 - a
16516 + ˇA
16517 b
16518 C
16519 "#
16520 .unindent();
16521
16522 cx.assert_state_with_diff(first_hunk_expanded);
16523
16524 cx.update_editor(|editor, _, cx| {
16525 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16526 });
16527 executor.run_until_parked();
16528
16529 cx.assert_state_with_diff(both_hunks_expanded);
16530
16531 cx.set_state(
16532 &r#"
16533 ˇA
16534 b
16535 "#
16536 .unindent(),
16537 );
16538 cx.run_until_parked();
16539
16540 // TODO this cursor position seems bad
16541 cx.assert_state_with_diff(
16542 r#"
16543 - ˇa
16544 + A
16545 b
16546 "#
16547 .unindent(),
16548 );
16549
16550 cx.update_editor(|editor, window, cx| {
16551 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16552 });
16553
16554 cx.assert_state_with_diff(
16555 r#"
16556 - ˇa
16557 + A
16558 b
16559 - c
16560 "#
16561 .unindent(),
16562 );
16563
16564 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16565 let snapshot = editor.snapshot(window, cx);
16566 let hunks = editor
16567 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16568 .collect::<Vec<_>>();
16569 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16570 let buffer_id = hunks[0].buffer_id;
16571 hunks
16572 .into_iter()
16573 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16574 .collect::<Vec<_>>()
16575 });
16576 assert_eq!(hunk_ranges.len(), 2);
16577
16578 cx.update_editor(|editor, _, cx| {
16579 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16580 });
16581 executor.run_until_parked();
16582
16583 cx.assert_state_with_diff(
16584 r#"
16585 - ˇa
16586 + A
16587 b
16588 "#
16589 .unindent(),
16590 );
16591}
16592
16593#[gpui::test]
16594async fn test_toggle_deletion_hunk_at_start_of_file(
16595 executor: BackgroundExecutor,
16596 cx: &mut TestAppContext,
16597) {
16598 init_test(cx, |_| {});
16599 let mut cx = EditorTestContext::new(cx).await;
16600
16601 let diff_base = r#"
16602 a
16603 b
16604 c
16605 "#
16606 .unindent();
16607
16608 cx.set_state(
16609 &r#"
16610 ˇb
16611 c
16612 "#
16613 .unindent(),
16614 );
16615 cx.set_head_text(&diff_base);
16616 cx.update_editor(|editor, window, cx| {
16617 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16618 });
16619 executor.run_until_parked();
16620
16621 let hunk_expanded = r#"
16622 - a
16623 ˇb
16624 c
16625 "#
16626 .unindent();
16627
16628 cx.assert_state_with_diff(hunk_expanded.clone());
16629
16630 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16631 let snapshot = editor.snapshot(window, cx);
16632 let hunks = editor
16633 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16634 .collect::<Vec<_>>();
16635 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16636 let buffer_id = hunks[0].buffer_id;
16637 hunks
16638 .into_iter()
16639 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16640 .collect::<Vec<_>>()
16641 });
16642 assert_eq!(hunk_ranges.len(), 1);
16643
16644 cx.update_editor(|editor, _, cx| {
16645 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16646 });
16647 executor.run_until_parked();
16648
16649 let hunk_collapsed = r#"
16650 ˇb
16651 c
16652 "#
16653 .unindent();
16654
16655 cx.assert_state_with_diff(hunk_collapsed);
16656
16657 cx.update_editor(|editor, _, cx| {
16658 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16659 });
16660 executor.run_until_parked();
16661
16662 cx.assert_state_with_diff(hunk_expanded.clone());
16663}
16664
16665#[gpui::test]
16666async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16667 init_test(cx, |_| {});
16668
16669 let fs = FakeFs::new(cx.executor());
16670 fs.insert_tree(
16671 path!("/test"),
16672 json!({
16673 ".git": {},
16674 "file-1": "ONE\n",
16675 "file-2": "TWO\n",
16676 "file-3": "THREE\n",
16677 }),
16678 )
16679 .await;
16680
16681 fs.set_head_for_repo(
16682 path!("/test/.git").as_ref(),
16683 &[
16684 ("file-1".into(), "one\n".into()),
16685 ("file-2".into(), "two\n".into()),
16686 ("file-3".into(), "three\n".into()),
16687 ],
16688 );
16689
16690 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16691 let mut buffers = vec![];
16692 for i in 1..=3 {
16693 let buffer = project
16694 .update(cx, |project, cx| {
16695 let path = format!(path!("/test/file-{}"), i);
16696 project.open_local_buffer(path, cx)
16697 })
16698 .await
16699 .unwrap();
16700 buffers.push(buffer);
16701 }
16702
16703 let multibuffer = cx.new(|cx| {
16704 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16705 multibuffer.set_all_diff_hunks_expanded(cx);
16706 for buffer in &buffers {
16707 let snapshot = buffer.read(cx).snapshot();
16708 multibuffer.set_excerpts_for_path(
16709 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16710 buffer.clone(),
16711 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16712 DEFAULT_MULTIBUFFER_CONTEXT,
16713 cx,
16714 );
16715 }
16716 multibuffer
16717 });
16718
16719 let editor = cx.add_window(|window, cx| {
16720 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
16721 });
16722 cx.run_until_parked();
16723
16724 let snapshot = editor
16725 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16726 .unwrap();
16727 let hunks = snapshot
16728 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16729 .map(|hunk| match hunk {
16730 DisplayDiffHunk::Unfolded {
16731 display_row_range, ..
16732 } => display_row_range,
16733 DisplayDiffHunk::Folded { .. } => unreachable!(),
16734 })
16735 .collect::<Vec<_>>();
16736 assert_eq!(
16737 hunks,
16738 [
16739 DisplayRow(2)..DisplayRow(4),
16740 DisplayRow(7)..DisplayRow(9),
16741 DisplayRow(12)..DisplayRow(14),
16742 ]
16743 );
16744}
16745
16746#[gpui::test]
16747async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16748 init_test(cx, |_| {});
16749
16750 let mut cx = EditorTestContext::new(cx).await;
16751 cx.set_head_text(indoc! { "
16752 one
16753 two
16754 three
16755 four
16756 five
16757 "
16758 });
16759 cx.set_index_text(indoc! { "
16760 one
16761 two
16762 three
16763 four
16764 five
16765 "
16766 });
16767 cx.set_state(indoc! {"
16768 one
16769 TWO
16770 ˇTHREE
16771 FOUR
16772 five
16773 "});
16774 cx.run_until_parked();
16775 cx.update_editor(|editor, window, cx| {
16776 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16777 });
16778 cx.run_until_parked();
16779 cx.assert_index_text(Some(indoc! {"
16780 one
16781 TWO
16782 THREE
16783 FOUR
16784 five
16785 "}));
16786 cx.set_state(indoc! { "
16787 one
16788 TWO
16789 ˇTHREE-HUNDRED
16790 FOUR
16791 five
16792 "});
16793 cx.run_until_parked();
16794 cx.update_editor(|editor, window, cx| {
16795 let snapshot = editor.snapshot(window, cx);
16796 let hunks = editor
16797 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16798 .collect::<Vec<_>>();
16799 assert_eq!(hunks.len(), 1);
16800 assert_eq!(
16801 hunks[0].status(),
16802 DiffHunkStatus {
16803 kind: DiffHunkStatusKind::Modified,
16804 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16805 }
16806 );
16807
16808 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16809 });
16810 cx.run_until_parked();
16811 cx.assert_index_text(Some(indoc! {"
16812 one
16813 TWO
16814 THREE-HUNDRED
16815 FOUR
16816 five
16817 "}));
16818}
16819
16820#[gpui::test]
16821fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16822 init_test(cx, |_| {});
16823
16824 let editor = cx.add_window(|window, cx| {
16825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16826 build_editor(buffer, window, cx)
16827 });
16828
16829 let render_args = Arc::new(Mutex::new(None));
16830 let snapshot = editor
16831 .update(cx, |editor, window, cx| {
16832 let snapshot = editor.buffer().read(cx).snapshot(cx);
16833 let range =
16834 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16835
16836 struct RenderArgs {
16837 row: MultiBufferRow,
16838 folded: bool,
16839 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16840 }
16841
16842 let crease = Crease::inline(
16843 range,
16844 FoldPlaceholder::test(),
16845 {
16846 let toggle_callback = render_args.clone();
16847 move |row, folded, callback, _window, _cx| {
16848 *toggle_callback.lock() = Some(RenderArgs {
16849 row,
16850 folded,
16851 callback,
16852 });
16853 div()
16854 }
16855 },
16856 |_row, _folded, _window, _cx| div(),
16857 );
16858
16859 editor.insert_creases(Some(crease), cx);
16860 let snapshot = editor.snapshot(window, cx);
16861 let _div = snapshot.render_crease_toggle(
16862 MultiBufferRow(1),
16863 false,
16864 cx.entity().clone(),
16865 window,
16866 cx,
16867 );
16868 snapshot
16869 })
16870 .unwrap();
16871
16872 let render_args = render_args.lock().take().unwrap();
16873 assert_eq!(render_args.row, MultiBufferRow(1));
16874 assert!(!render_args.folded);
16875 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16876
16877 cx.update_window(*editor, |_, window, cx| {
16878 (render_args.callback)(true, window, cx)
16879 })
16880 .unwrap();
16881 let snapshot = editor
16882 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16883 .unwrap();
16884 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16885
16886 cx.update_window(*editor, |_, window, cx| {
16887 (render_args.callback)(false, window, cx)
16888 })
16889 .unwrap();
16890 let snapshot = editor
16891 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16892 .unwrap();
16893 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16894}
16895
16896#[gpui::test]
16897async fn test_input_text(cx: &mut TestAppContext) {
16898 init_test(cx, |_| {});
16899 let mut cx = EditorTestContext::new(cx).await;
16900
16901 cx.set_state(
16902 &r#"ˇone
16903 two
16904
16905 three
16906 fourˇ
16907 five
16908
16909 siˇx"#
16910 .unindent(),
16911 );
16912
16913 cx.dispatch_action(HandleInput(String::new()));
16914 cx.assert_editor_state(
16915 &r#"ˇone
16916 two
16917
16918 three
16919 fourˇ
16920 five
16921
16922 siˇx"#
16923 .unindent(),
16924 );
16925
16926 cx.dispatch_action(HandleInput("AAAA".to_string()));
16927 cx.assert_editor_state(
16928 &r#"AAAAˇone
16929 two
16930
16931 three
16932 fourAAAAˇ
16933 five
16934
16935 siAAAAˇx"#
16936 .unindent(),
16937 );
16938}
16939
16940#[gpui::test]
16941async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16942 init_test(cx, |_| {});
16943
16944 let mut cx = EditorTestContext::new(cx).await;
16945 cx.set_state(
16946 r#"let foo = 1;
16947let foo = 2;
16948let foo = 3;
16949let fooˇ = 4;
16950let foo = 5;
16951let foo = 6;
16952let foo = 7;
16953let foo = 8;
16954let foo = 9;
16955let foo = 10;
16956let foo = 11;
16957let foo = 12;
16958let foo = 13;
16959let foo = 14;
16960let foo = 15;"#,
16961 );
16962
16963 cx.update_editor(|e, window, cx| {
16964 assert_eq!(
16965 e.next_scroll_position,
16966 NextScrollCursorCenterTopBottom::Center,
16967 "Default next scroll direction is center",
16968 );
16969
16970 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16971 assert_eq!(
16972 e.next_scroll_position,
16973 NextScrollCursorCenterTopBottom::Top,
16974 "After center, next scroll direction should be top",
16975 );
16976
16977 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16978 assert_eq!(
16979 e.next_scroll_position,
16980 NextScrollCursorCenterTopBottom::Bottom,
16981 "After top, next scroll direction should be bottom",
16982 );
16983
16984 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16985 assert_eq!(
16986 e.next_scroll_position,
16987 NextScrollCursorCenterTopBottom::Center,
16988 "After bottom, scrolling should start over",
16989 );
16990
16991 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16992 assert_eq!(
16993 e.next_scroll_position,
16994 NextScrollCursorCenterTopBottom::Top,
16995 "Scrolling continues if retriggered fast enough"
16996 );
16997 });
16998
16999 cx.executor()
17000 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17001 cx.executor().run_until_parked();
17002 cx.update_editor(|e, _, _| {
17003 assert_eq!(
17004 e.next_scroll_position,
17005 NextScrollCursorCenterTopBottom::Center,
17006 "If scrolling is not triggered fast enough, it should reset"
17007 );
17008 });
17009}
17010
17011#[gpui::test]
17012async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17013 init_test(cx, |_| {});
17014 let mut cx = EditorLspTestContext::new_rust(
17015 lsp::ServerCapabilities {
17016 definition_provider: Some(lsp::OneOf::Left(true)),
17017 references_provider: Some(lsp::OneOf::Left(true)),
17018 ..lsp::ServerCapabilities::default()
17019 },
17020 cx,
17021 )
17022 .await;
17023
17024 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17025 let go_to_definition = cx
17026 .lsp
17027 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17028 move |params, _| async move {
17029 if empty_go_to_definition {
17030 Ok(None)
17031 } else {
17032 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17033 uri: params.text_document_position_params.text_document.uri,
17034 range: lsp::Range::new(
17035 lsp::Position::new(4, 3),
17036 lsp::Position::new(4, 6),
17037 ),
17038 })))
17039 }
17040 },
17041 );
17042 let references = cx
17043 .lsp
17044 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17045 Ok(Some(vec![lsp::Location {
17046 uri: params.text_document_position.text_document.uri,
17047 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17048 }]))
17049 });
17050 (go_to_definition, references)
17051 };
17052
17053 cx.set_state(
17054 &r#"fn one() {
17055 let mut a = ˇtwo();
17056 }
17057
17058 fn two() {}"#
17059 .unindent(),
17060 );
17061 set_up_lsp_handlers(false, &mut cx);
17062 let navigated = cx
17063 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17064 .await
17065 .expect("Failed to navigate to definition");
17066 assert_eq!(
17067 navigated,
17068 Navigated::Yes,
17069 "Should have navigated to definition from the GetDefinition response"
17070 );
17071 cx.assert_editor_state(
17072 &r#"fn one() {
17073 let mut a = two();
17074 }
17075
17076 fn «twoˇ»() {}"#
17077 .unindent(),
17078 );
17079
17080 let editors = cx.update_workspace(|workspace, _, cx| {
17081 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17082 });
17083 cx.update_editor(|_, _, test_editor_cx| {
17084 assert_eq!(
17085 editors.len(),
17086 1,
17087 "Initially, only one, test, editor should be open in the workspace"
17088 );
17089 assert_eq!(
17090 test_editor_cx.entity(),
17091 editors.last().expect("Asserted len is 1").clone()
17092 );
17093 });
17094
17095 set_up_lsp_handlers(true, &mut cx);
17096 let navigated = cx
17097 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17098 .await
17099 .expect("Failed to navigate to lookup references");
17100 assert_eq!(
17101 navigated,
17102 Navigated::Yes,
17103 "Should have navigated to references as a fallback after empty GoToDefinition response"
17104 );
17105 // We should not change the selections in the existing file,
17106 // if opening another milti buffer with the references
17107 cx.assert_editor_state(
17108 &r#"fn one() {
17109 let mut a = two();
17110 }
17111
17112 fn «twoˇ»() {}"#
17113 .unindent(),
17114 );
17115 let editors = cx.update_workspace(|workspace, _, cx| {
17116 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17117 });
17118 cx.update_editor(|_, _, test_editor_cx| {
17119 assert_eq!(
17120 editors.len(),
17121 2,
17122 "After falling back to references search, we open a new editor with the results"
17123 );
17124 let references_fallback_text = editors
17125 .into_iter()
17126 .find(|new_editor| *new_editor != test_editor_cx.entity())
17127 .expect("Should have one non-test editor now")
17128 .read(test_editor_cx)
17129 .text(test_editor_cx);
17130 assert_eq!(
17131 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17132 "Should use the range from the references response and not the GoToDefinition one"
17133 );
17134 });
17135}
17136
17137#[gpui::test]
17138async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17139 init_test(cx, |_| {});
17140 cx.update(|cx| {
17141 let mut editor_settings = EditorSettings::get_global(cx).clone();
17142 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17143 EditorSettings::override_global(editor_settings, cx);
17144 });
17145 let mut cx = EditorLspTestContext::new_rust(
17146 lsp::ServerCapabilities {
17147 definition_provider: Some(lsp::OneOf::Left(true)),
17148 references_provider: Some(lsp::OneOf::Left(true)),
17149 ..lsp::ServerCapabilities::default()
17150 },
17151 cx,
17152 )
17153 .await;
17154 let original_state = r#"fn one() {
17155 let mut a = ˇtwo();
17156 }
17157
17158 fn two() {}"#
17159 .unindent();
17160 cx.set_state(&original_state);
17161
17162 let mut go_to_definition = cx
17163 .lsp
17164 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17165 move |_, _| async move { Ok(None) },
17166 );
17167 let _references = cx
17168 .lsp
17169 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17170 panic!("Should not call for references with no go to definition fallback")
17171 });
17172
17173 let navigated = cx
17174 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17175 .await
17176 .expect("Failed to navigate to lookup references");
17177 go_to_definition
17178 .next()
17179 .await
17180 .expect("Should have called the go_to_definition handler");
17181
17182 assert_eq!(
17183 navigated,
17184 Navigated::No,
17185 "Should have navigated to references as a fallback after empty GoToDefinition response"
17186 );
17187 cx.assert_editor_state(&original_state);
17188 let editors = cx.update_workspace(|workspace, _, cx| {
17189 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17190 });
17191 cx.update_editor(|_, _, _| {
17192 assert_eq!(
17193 editors.len(),
17194 1,
17195 "After unsuccessful fallback, no other editor should have been opened"
17196 );
17197 });
17198}
17199
17200#[gpui::test]
17201async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17202 init_test(cx, |_| {});
17203
17204 let language = Arc::new(Language::new(
17205 LanguageConfig::default(),
17206 Some(tree_sitter_rust::LANGUAGE.into()),
17207 ));
17208
17209 let text = r#"
17210 #[cfg(test)]
17211 mod tests() {
17212 #[test]
17213 fn runnable_1() {
17214 let a = 1;
17215 }
17216
17217 #[test]
17218 fn runnable_2() {
17219 let a = 1;
17220 let b = 2;
17221 }
17222 }
17223 "#
17224 .unindent();
17225
17226 let fs = FakeFs::new(cx.executor());
17227 fs.insert_file("/file.rs", Default::default()).await;
17228
17229 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17230 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17231 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17232 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17233 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17234
17235 let editor = cx.new_window_entity(|window, cx| {
17236 Editor::new(
17237 EditorMode::full(),
17238 multi_buffer,
17239 Some(project.clone()),
17240 window,
17241 cx,
17242 )
17243 });
17244
17245 editor.update_in(cx, |editor, window, cx| {
17246 let snapshot = editor.buffer().read(cx).snapshot(cx);
17247 editor.tasks.insert(
17248 (buffer.read(cx).remote_id(), 3),
17249 RunnableTasks {
17250 templates: vec![],
17251 offset: snapshot.anchor_before(43),
17252 column: 0,
17253 extra_variables: HashMap::default(),
17254 context_range: BufferOffset(43)..BufferOffset(85),
17255 },
17256 );
17257 editor.tasks.insert(
17258 (buffer.read(cx).remote_id(), 8),
17259 RunnableTasks {
17260 templates: vec![],
17261 offset: snapshot.anchor_before(86),
17262 column: 0,
17263 extra_variables: HashMap::default(),
17264 context_range: BufferOffset(86)..BufferOffset(191),
17265 },
17266 );
17267
17268 // Test finding task when cursor is inside function body
17269 editor.change_selections(None, window, cx, |s| {
17270 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17271 });
17272 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17273 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17274
17275 // Test finding task when cursor is on function name
17276 editor.change_selections(None, window, cx, |s| {
17277 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17278 });
17279 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17280 assert_eq!(row, 8, "Should find task when cursor is on function name");
17281 });
17282}
17283
17284#[gpui::test]
17285async fn test_folding_buffers(cx: &mut TestAppContext) {
17286 init_test(cx, |_| {});
17287
17288 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17289 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17290 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17291
17292 let fs = FakeFs::new(cx.executor());
17293 fs.insert_tree(
17294 path!("/a"),
17295 json!({
17296 "first.rs": sample_text_1,
17297 "second.rs": sample_text_2,
17298 "third.rs": sample_text_3,
17299 }),
17300 )
17301 .await;
17302 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17303 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17304 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17305 let worktree = project.update(cx, |project, cx| {
17306 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17307 assert_eq!(worktrees.len(), 1);
17308 worktrees.pop().unwrap()
17309 });
17310 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17311
17312 let buffer_1 = project
17313 .update(cx, |project, cx| {
17314 project.open_buffer((worktree_id, "first.rs"), cx)
17315 })
17316 .await
17317 .unwrap();
17318 let buffer_2 = project
17319 .update(cx, |project, cx| {
17320 project.open_buffer((worktree_id, "second.rs"), cx)
17321 })
17322 .await
17323 .unwrap();
17324 let buffer_3 = project
17325 .update(cx, |project, cx| {
17326 project.open_buffer((worktree_id, "third.rs"), cx)
17327 })
17328 .await
17329 .unwrap();
17330
17331 let multi_buffer = cx.new(|cx| {
17332 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17333 multi_buffer.push_excerpts(
17334 buffer_1.clone(),
17335 [
17336 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17337 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17338 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17339 ],
17340 cx,
17341 );
17342 multi_buffer.push_excerpts(
17343 buffer_2.clone(),
17344 [
17345 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17346 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17347 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17348 ],
17349 cx,
17350 );
17351 multi_buffer.push_excerpts(
17352 buffer_3.clone(),
17353 [
17354 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17355 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17356 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17357 ],
17358 cx,
17359 );
17360 multi_buffer
17361 });
17362 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17363 Editor::new(
17364 EditorMode::full(),
17365 multi_buffer.clone(),
17366 Some(project.clone()),
17367 window,
17368 cx,
17369 )
17370 });
17371
17372 assert_eq!(
17373 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17374 "\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",
17375 );
17376
17377 multi_buffer_editor.update(cx, |editor, cx| {
17378 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17379 });
17380 assert_eq!(
17381 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17382 "\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",
17383 "After folding the first buffer, its text should not be displayed"
17384 );
17385
17386 multi_buffer_editor.update(cx, |editor, cx| {
17387 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17388 });
17389 assert_eq!(
17390 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17391 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17392 "After folding the second buffer, its text should not be displayed"
17393 );
17394
17395 multi_buffer_editor.update(cx, |editor, cx| {
17396 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17397 });
17398 assert_eq!(
17399 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17400 "\n\n\n\n\n",
17401 "After folding the third buffer, its text should not be displayed"
17402 );
17403
17404 // Emulate selection inside the fold logic, that should work
17405 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17406 editor
17407 .snapshot(window, cx)
17408 .next_line_boundary(Point::new(0, 4));
17409 });
17410
17411 multi_buffer_editor.update(cx, |editor, cx| {
17412 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17413 });
17414 assert_eq!(
17415 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17416 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17417 "After unfolding the second buffer, its text should be displayed"
17418 );
17419
17420 // Typing inside of buffer 1 causes that buffer to be unfolded.
17421 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17422 assert_eq!(
17423 multi_buffer
17424 .read(cx)
17425 .snapshot(cx)
17426 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17427 .collect::<String>(),
17428 "bbbb"
17429 );
17430 editor.change_selections(None, window, cx, |selections| {
17431 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17432 });
17433 editor.handle_input("B", window, cx);
17434 });
17435
17436 assert_eq!(
17437 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17438 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17439 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17440 );
17441
17442 multi_buffer_editor.update(cx, |editor, cx| {
17443 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17444 });
17445 assert_eq!(
17446 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17447 "\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",
17448 "After unfolding the all buffers, all original text should be displayed"
17449 );
17450}
17451
17452#[gpui::test]
17453async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17454 init_test(cx, |_| {});
17455
17456 let sample_text_1 = "1111\n2222\n3333".to_string();
17457 let sample_text_2 = "4444\n5555\n6666".to_string();
17458 let sample_text_3 = "7777\n8888\n9999".to_string();
17459
17460 let fs = FakeFs::new(cx.executor());
17461 fs.insert_tree(
17462 path!("/a"),
17463 json!({
17464 "first.rs": sample_text_1,
17465 "second.rs": sample_text_2,
17466 "third.rs": sample_text_3,
17467 }),
17468 )
17469 .await;
17470 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17471 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17472 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17473 let worktree = project.update(cx, |project, cx| {
17474 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17475 assert_eq!(worktrees.len(), 1);
17476 worktrees.pop().unwrap()
17477 });
17478 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17479
17480 let buffer_1 = project
17481 .update(cx, |project, cx| {
17482 project.open_buffer((worktree_id, "first.rs"), cx)
17483 })
17484 .await
17485 .unwrap();
17486 let buffer_2 = project
17487 .update(cx, |project, cx| {
17488 project.open_buffer((worktree_id, "second.rs"), cx)
17489 })
17490 .await
17491 .unwrap();
17492 let buffer_3 = project
17493 .update(cx, |project, cx| {
17494 project.open_buffer((worktree_id, "third.rs"), cx)
17495 })
17496 .await
17497 .unwrap();
17498
17499 let multi_buffer = cx.new(|cx| {
17500 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17501 multi_buffer.push_excerpts(
17502 buffer_1.clone(),
17503 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17504 cx,
17505 );
17506 multi_buffer.push_excerpts(
17507 buffer_2.clone(),
17508 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17509 cx,
17510 );
17511 multi_buffer.push_excerpts(
17512 buffer_3.clone(),
17513 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17514 cx,
17515 );
17516 multi_buffer
17517 });
17518
17519 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17520 Editor::new(
17521 EditorMode::full(),
17522 multi_buffer,
17523 Some(project.clone()),
17524 window,
17525 cx,
17526 )
17527 });
17528
17529 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17530 assert_eq!(
17531 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17532 full_text,
17533 );
17534
17535 multi_buffer_editor.update(cx, |editor, cx| {
17536 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17537 });
17538 assert_eq!(
17539 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17540 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17541 "After folding the first buffer, its text should not be displayed"
17542 );
17543
17544 multi_buffer_editor.update(cx, |editor, cx| {
17545 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17546 });
17547
17548 assert_eq!(
17549 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17550 "\n\n\n\n\n\n7777\n8888\n9999",
17551 "After folding the second buffer, its text should not be displayed"
17552 );
17553
17554 multi_buffer_editor.update(cx, |editor, cx| {
17555 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17556 });
17557 assert_eq!(
17558 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17559 "\n\n\n\n\n",
17560 "After folding the third buffer, its text should not be displayed"
17561 );
17562
17563 multi_buffer_editor.update(cx, |editor, cx| {
17564 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17565 });
17566 assert_eq!(
17567 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17568 "\n\n\n\n4444\n5555\n6666\n\n",
17569 "After unfolding the second buffer, its text should be displayed"
17570 );
17571
17572 multi_buffer_editor.update(cx, |editor, cx| {
17573 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17574 });
17575 assert_eq!(
17576 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17577 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17578 "After unfolding the first buffer, its text should be displayed"
17579 );
17580
17581 multi_buffer_editor.update(cx, |editor, cx| {
17582 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17583 });
17584 assert_eq!(
17585 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17586 full_text,
17587 "After unfolding all buffers, all original text should be displayed"
17588 );
17589}
17590
17591#[gpui::test]
17592async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17593 init_test(cx, |_| {});
17594
17595 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17596
17597 let fs = FakeFs::new(cx.executor());
17598 fs.insert_tree(
17599 path!("/a"),
17600 json!({
17601 "main.rs": sample_text,
17602 }),
17603 )
17604 .await;
17605 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17606 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17607 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17608 let worktree = project.update(cx, |project, cx| {
17609 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17610 assert_eq!(worktrees.len(), 1);
17611 worktrees.pop().unwrap()
17612 });
17613 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17614
17615 let buffer_1 = project
17616 .update(cx, |project, cx| {
17617 project.open_buffer((worktree_id, "main.rs"), cx)
17618 })
17619 .await
17620 .unwrap();
17621
17622 let multi_buffer = cx.new(|cx| {
17623 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17624 multi_buffer.push_excerpts(
17625 buffer_1.clone(),
17626 [ExcerptRange::new(
17627 Point::new(0, 0)
17628 ..Point::new(
17629 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17630 0,
17631 ),
17632 )],
17633 cx,
17634 );
17635 multi_buffer
17636 });
17637 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17638 Editor::new(
17639 EditorMode::full(),
17640 multi_buffer,
17641 Some(project.clone()),
17642 window,
17643 cx,
17644 )
17645 });
17646
17647 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17648 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17649 enum TestHighlight {}
17650 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17651 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17652 editor.highlight_text::<TestHighlight>(
17653 vec![highlight_range.clone()],
17654 HighlightStyle::color(Hsla::green()),
17655 cx,
17656 );
17657 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17658 });
17659
17660 let full_text = format!("\n\n{sample_text}");
17661 assert_eq!(
17662 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17663 full_text,
17664 );
17665}
17666
17667#[gpui::test]
17668async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17669 init_test(cx, |_| {});
17670 cx.update(|cx| {
17671 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17672 "keymaps/default-linux.json",
17673 cx,
17674 )
17675 .unwrap();
17676 cx.bind_keys(default_key_bindings);
17677 });
17678
17679 let (editor, cx) = cx.add_window_view(|window, cx| {
17680 let multi_buffer = MultiBuffer::build_multi(
17681 [
17682 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17683 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17684 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17685 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17686 ],
17687 cx,
17688 );
17689 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
17690
17691 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17692 // fold all but the second buffer, so that we test navigating between two
17693 // adjacent folded buffers, as well as folded buffers at the start and
17694 // end the multibuffer
17695 editor.fold_buffer(buffer_ids[0], cx);
17696 editor.fold_buffer(buffer_ids[2], cx);
17697 editor.fold_buffer(buffer_ids[3], cx);
17698
17699 editor
17700 });
17701 cx.simulate_resize(size(px(1000.), px(1000.)));
17702
17703 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17704 cx.assert_excerpts_with_selections(indoc! {"
17705 [EXCERPT]
17706 ˇ[FOLDED]
17707 [EXCERPT]
17708 a1
17709 b1
17710 [EXCERPT]
17711 [FOLDED]
17712 [EXCERPT]
17713 [FOLDED]
17714 "
17715 });
17716 cx.simulate_keystroke("down");
17717 cx.assert_excerpts_with_selections(indoc! {"
17718 [EXCERPT]
17719 [FOLDED]
17720 [EXCERPT]
17721 ˇa1
17722 b1
17723 [EXCERPT]
17724 [FOLDED]
17725 [EXCERPT]
17726 [FOLDED]
17727 "
17728 });
17729 cx.simulate_keystroke("down");
17730 cx.assert_excerpts_with_selections(indoc! {"
17731 [EXCERPT]
17732 [FOLDED]
17733 [EXCERPT]
17734 a1
17735 ˇb1
17736 [EXCERPT]
17737 [FOLDED]
17738 [EXCERPT]
17739 [FOLDED]
17740 "
17741 });
17742 cx.simulate_keystroke("down");
17743 cx.assert_excerpts_with_selections(indoc! {"
17744 [EXCERPT]
17745 [FOLDED]
17746 [EXCERPT]
17747 a1
17748 b1
17749 ˇ[EXCERPT]
17750 [FOLDED]
17751 [EXCERPT]
17752 [FOLDED]
17753 "
17754 });
17755 cx.simulate_keystroke("down");
17756 cx.assert_excerpts_with_selections(indoc! {"
17757 [EXCERPT]
17758 [FOLDED]
17759 [EXCERPT]
17760 a1
17761 b1
17762 [EXCERPT]
17763 ˇ[FOLDED]
17764 [EXCERPT]
17765 [FOLDED]
17766 "
17767 });
17768 for _ in 0..5 {
17769 cx.simulate_keystroke("down");
17770 cx.assert_excerpts_with_selections(indoc! {"
17771 [EXCERPT]
17772 [FOLDED]
17773 [EXCERPT]
17774 a1
17775 b1
17776 [EXCERPT]
17777 [FOLDED]
17778 [EXCERPT]
17779 ˇ[FOLDED]
17780 "
17781 });
17782 }
17783
17784 cx.simulate_keystroke("up");
17785 cx.assert_excerpts_with_selections(indoc! {"
17786 [EXCERPT]
17787 [FOLDED]
17788 [EXCERPT]
17789 a1
17790 b1
17791 [EXCERPT]
17792 ˇ[FOLDED]
17793 [EXCERPT]
17794 [FOLDED]
17795 "
17796 });
17797 cx.simulate_keystroke("up");
17798 cx.assert_excerpts_with_selections(indoc! {"
17799 [EXCERPT]
17800 [FOLDED]
17801 [EXCERPT]
17802 a1
17803 b1
17804 ˇ[EXCERPT]
17805 [FOLDED]
17806 [EXCERPT]
17807 [FOLDED]
17808 "
17809 });
17810 cx.simulate_keystroke("up");
17811 cx.assert_excerpts_with_selections(indoc! {"
17812 [EXCERPT]
17813 [FOLDED]
17814 [EXCERPT]
17815 a1
17816 ˇb1
17817 [EXCERPT]
17818 [FOLDED]
17819 [EXCERPT]
17820 [FOLDED]
17821 "
17822 });
17823 cx.simulate_keystroke("up");
17824 cx.assert_excerpts_with_selections(indoc! {"
17825 [EXCERPT]
17826 [FOLDED]
17827 [EXCERPT]
17828 ˇa1
17829 b1
17830 [EXCERPT]
17831 [FOLDED]
17832 [EXCERPT]
17833 [FOLDED]
17834 "
17835 });
17836 for _ in 0..5 {
17837 cx.simulate_keystroke("up");
17838 cx.assert_excerpts_with_selections(indoc! {"
17839 [EXCERPT]
17840 ˇ[FOLDED]
17841 [EXCERPT]
17842 a1
17843 b1
17844 [EXCERPT]
17845 [FOLDED]
17846 [EXCERPT]
17847 [FOLDED]
17848 "
17849 });
17850 }
17851}
17852
17853#[gpui::test]
17854async fn test_inline_completion_text(cx: &mut TestAppContext) {
17855 init_test(cx, |_| {});
17856
17857 // Simple insertion
17858 assert_highlighted_edits(
17859 "Hello, world!",
17860 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17861 true,
17862 cx,
17863 |highlighted_edits, cx| {
17864 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17865 assert_eq!(highlighted_edits.highlights.len(), 1);
17866 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17867 assert_eq!(
17868 highlighted_edits.highlights[0].1.background_color,
17869 Some(cx.theme().status().created_background)
17870 );
17871 },
17872 )
17873 .await;
17874
17875 // Replacement
17876 assert_highlighted_edits(
17877 "This is a test.",
17878 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17879 false,
17880 cx,
17881 |highlighted_edits, cx| {
17882 assert_eq!(highlighted_edits.text, "That is a test.");
17883 assert_eq!(highlighted_edits.highlights.len(), 1);
17884 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17885 assert_eq!(
17886 highlighted_edits.highlights[0].1.background_color,
17887 Some(cx.theme().status().created_background)
17888 );
17889 },
17890 )
17891 .await;
17892
17893 // Multiple edits
17894 assert_highlighted_edits(
17895 "Hello, world!",
17896 vec![
17897 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17898 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17899 ],
17900 false,
17901 cx,
17902 |highlighted_edits, cx| {
17903 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17904 assert_eq!(highlighted_edits.highlights.len(), 2);
17905 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17906 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17907 assert_eq!(
17908 highlighted_edits.highlights[0].1.background_color,
17909 Some(cx.theme().status().created_background)
17910 );
17911 assert_eq!(
17912 highlighted_edits.highlights[1].1.background_color,
17913 Some(cx.theme().status().created_background)
17914 );
17915 },
17916 )
17917 .await;
17918
17919 // Multiple lines with edits
17920 assert_highlighted_edits(
17921 "First line\nSecond line\nThird line\nFourth line",
17922 vec![
17923 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17924 (
17925 Point::new(2, 0)..Point::new(2, 10),
17926 "New third line".to_string(),
17927 ),
17928 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17929 ],
17930 false,
17931 cx,
17932 |highlighted_edits, cx| {
17933 assert_eq!(
17934 highlighted_edits.text,
17935 "Second modified\nNew third line\nFourth updated line"
17936 );
17937 assert_eq!(highlighted_edits.highlights.len(), 3);
17938 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17939 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17940 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17941 for highlight in &highlighted_edits.highlights {
17942 assert_eq!(
17943 highlight.1.background_color,
17944 Some(cx.theme().status().created_background)
17945 );
17946 }
17947 },
17948 )
17949 .await;
17950}
17951
17952#[gpui::test]
17953async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17954 init_test(cx, |_| {});
17955
17956 // Deletion
17957 assert_highlighted_edits(
17958 "Hello, world!",
17959 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17960 true,
17961 cx,
17962 |highlighted_edits, cx| {
17963 assert_eq!(highlighted_edits.text, "Hello, world!");
17964 assert_eq!(highlighted_edits.highlights.len(), 1);
17965 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17966 assert_eq!(
17967 highlighted_edits.highlights[0].1.background_color,
17968 Some(cx.theme().status().deleted_background)
17969 );
17970 },
17971 )
17972 .await;
17973
17974 // Insertion
17975 assert_highlighted_edits(
17976 "Hello, world!",
17977 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17978 true,
17979 cx,
17980 |highlighted_edits, cx| {
17981 assert_eq!(highlighted_edits.highlights.len(), 1);
17982 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17983 assert_eq!(
17984 highlighted_edits.highlights[0].1.background_color,
17985 Some(cx.theme().status().created_background)
17986 );
17987 },
17988 )
17989 .await;
17990}
17991
17992async fn assert_highlighted_edits(
17993 text: &str,
17994 edits: Vec<(Range<Point>, String)>,
17995 include_deletions: bool,
17996 cx: &mut TestAppContext,
17997 assertion_fn: impl Fn(HighlightedText, &App),
17998) {
17999 let window = cx.add_window(|window, cx| {
18000 let buffer = MultiBuffer::build_simple(text, cx);
18001 Editor::new(EditorMode::full(), buffer, None, window, cx)
18002 });
18003 let cx = &mut VisualTestContext::from_window(*window, cx);
18004
18005 let (buffer, snapshot) = window
18006 .update(cx, |editor, _window, cx| {
18007 (
18008 editor.buffer().clone(),
18009 editor.buffer().read(cx).snapshot(cx),
18010 )
18011 })
18012 .unwrap();
18013
18014 let edits = edits
18015 .into_iter()
18016 .map(|(range, edit)| {
18017 (
18018 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18019 edit,
18020 )
18021 })
18022 .collect::<Vec<_>>();
18023
18024 let text_anchor_edits = edits
18025 .clone()
18026 .into_iter()
18027 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18028 .collect::<Vec<_>>();
18029
18030 let edit_preview = window
18031 .update(cx, |_, _window, cx| {
18032 buffer
18033 .read(cx)
18034 .as_singleton()
18035 .unwrap()
18036 .read(cx)
18037 .preview_edits(text_anchor_edits.into(), cx)
18038 })
18039 .unwrap()
18040 .await;
18041
18042 cx.update(|_window, cx| {
18043 let highlighted_edits = inline_completion_edit_text(
18044 &snapshot.as_singleton().unwrap().2,
18045 &edits,
18046 &edit_preview,
18047 include_deletions,
18048 cx,
18049 );
18050 assertion_fn(highlighted_edits, cx)
18051 });
18052}
18053
18054#[track_caller]
18055fn assert_breakpoint(
18056 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18057 path: &Arc<Path>,
18058 expected: Vec<(u32, Breakpoint)>,
18059) {
18060 if expected.len() == 0usize {
18061 assert!(!breakpoints.contains_key(path), "{}", path.display());
18062 } else {
18063 let mut breakpoint = breakpoints
18064 .get(path)
18065 .unwrap()
18066 .into_iter()
18067 .map(|breakpoint| {
18068 (
18069 breakpoint.row,
18070 Breakpoint {
18071 message: breakpoint.message.clone(),
18072 state: breakpoint.state,
18073 condition: breakpoint.condition.clone(),
18074 hit_condition: breakpoint.hit_condition.clone(),
18075 },
18076 )
18077 })
18078 .collect::<Vec<_>>();
18079
18080 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18081
18082 assert_eq!(expected, breakpoint);
18083 }
18084}
18085
18086fn add_log_breakpoint_at_cursor(
18087 editor: &mut Editor,
18088 log_message: &str,
18089 window: &mut Window,
18090 cx: &mut Context<Editor>,
18091) {
18092 let (anchor, bp) = editor
18093 .breakpoints_at_cursors(window, cx)
18094 .first()
18095 .and_then(|(anchor, bp)| {
18096 if let Some(bp) = bp {
18097 Some((*anchor, bp.clone()))
18098 } else {
18099 None
18100 }
18101 })
18102 .unwrap_or_else(|| {
18103 let cursor_position: Point = editor.selections.newest(cx).head();
18104
18105 let breakpoint_position = editor
18106 .snapshot(window, cx)
18107 .display_snapshot
18108 .buffer_snapshot
18109 .anchor_before(Point::new(cursor_position.row, 0));
18110
18111 (breakpoint_position, Breakpoint::new_log(&log_message))
18112 });
18113
18114 editor.edit_breakpoint_at_anchor(
18115 anchor,
18116 bp,
18117 BreakpointEditAction::EditLogMessage(log_message.into()),
18118 cx,
18119 );
18120}
18121
18122#[gpui::test]
18123async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18124 init_test(cx, |_| {});
18125
18126 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18127 let fs = FakeFs::new(cx.executor());
18128 fs.insert_tree(
18129 path!("/a"),
18130 json!({
18131 "main.rs": sample_text,
18132 }),
18133 )
18134 .await;
18135 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18136 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18137 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18138
18139 let fs = FakeFs::new(cx.executor());
18140 fs.insert_tree(
18141 path!("/a"),
18142 json!({
18143 "main.rs": sample_text,
18144 }),
18145 )
18146 .await;
18147 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18148 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18149 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18150 let worktree_id = workspace
18151 .update(cx, |workspace, _window, cx| {
18152 workspace.project().update(cx, |project, cx| {
18153 project.worktrees(cx).next().unwrap().read(cx).id()
18154 })
18155 })
18156 .unwrap();
18157
18158 let buffer = project
18159 .update(cx, |project, cx| {
18160 project.open_buffer((worktree_id, "main.rs"), cx)
18161 })
18162 .await
18163 .unwrap();
18164
18165 let (editor, cx) = cx.add_window_view(|window, cx| {
18166 Editor::new(
18167 EditorMode::full(),
18168 MultiBuffer::build_from_buffer(buffer, cx),
18169 Some(project.clone()),
18170 window,
18171 cx,
18172 )
18173 });
18174
18175 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18176 let abs_path = project.read_with(cx, |project, cx| {
18177 project
18178 .absolute_path(&project_path, cx)
18179 .map(|path_buf| Arc::from(path_buf.to_owned()))
18180 .unwrap()
18181 });
18182
18183 // assert we can add breakpoint on the first line
18184 editor.update_in(cx, |editor, window, cx| {
18185 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18186 editor.move_to_end(&MoveToEnd, window, cx);
18187 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18188 });
18189
18190 let breakpoints = editor.update(cx, |editor, cx| {
18191 editor
18192 .breakpoint_store()
18193 .as_ref()
18194 .unwrap()
18195 .read(cx)
18196 .all_breakpoints(cx)
18197 .clone()
18198 });
18199
18200 assert_eq!(1, breakpoints.len());
18201 assert_breakpoint(
18202 &breakpoints,
18203 &abs_path,
18204 vec![
18205 (0, Breakpoint::new_standard()),
18206 (3, Breakpoint::new_standard()),
18207 ],
18208 );
18209
18210 editor.update_in(cx, |editor, window, cx| {
18211 editor.move_to_beginning(&MoveToBeginning, window, cx);
18212 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18213 });
18214
18215 let breakpoints = editor.update(cx, |editor, cx| {
18216 editor
18217 .breakpoint_store()
18218 .as_ref()
18219 .unwrap()
18220 .read(cx)
18221 .all_breakpoints(cx)
18222 .clone()
18223 });
18224
18225 assert_eq!(1, breakpoints.len());
18226 assert_breakpoint(
18227 &breakpoints,
18228 &abs_path,
18229 vec![(3, Breakpoint::new_standard())],
18230 );
18231
18232 editor.update_in(cx, |editor, window, cx| {
18233 editor.move_to_end(&MoveToEnd, window, cx);
18234 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18235 });
18236
18237 let breakpoints = editor.update(cx, |editor, cx| {
18238 editor
18239 .breakpoint_store()
18240 .as_ref()
18241 .unwrap()
18242 .read(cx)
18243 .all_breakpoints(cx)
18244 .clone()
18245 });
18246
18247 assert_eq!(0, breakpoints.len());
18248 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18249}
18250
18251#[gpui::test]
18252async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18253 init_test(cx, |_| {});
18254
18255 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18256
18257 let fs = FakeFs::new(cx.executor());
18258 fs.insert_tree(
18259 path!("/a"),
18260 json!({
18261 "main.rs": sample_text,
18262 }),
18263 )
18264 .await;
18265 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18266 let (workspace, cx) =
18267 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18268
18269 let worktree_id = workspace.update(cx, |workspace, cx| {
18270 workspace.project().update(cx, |project, cx| {
18271 project.worktrees(cx).next().unwrap().read(cx).id()
18272 })
18273 });
18274
18275 let buffer = project
18276 .update(cx, |project, cx| {
18277 project.open_buffer((worktree_id, "main.rs"), cx)
18278 })
18279 .await
18280 .unwrap();
18281
18282 let (editor, cx) = cx.add_window_view(|window, cx| {
18283 Editor::new(
18284 EditorMode::full(),
18285 MultiBuffer::build_from_buffer(buffer, cx),
18286 Some(project.clone()),
18287 window,
18288 cx,
18289 )
18290 });
18291
18292 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18293 let abs_path = project.read_with(cx, |project, cx| {
18294 project
18295 .absolute_path(&project_path, cx)
18296 .map(|path_buf| Arc::from(path_buf.to_owned()))
18297 .unwrap()
18298 });
18299
18300 editor.update_in(cx, |editor, window, cx| {
18301 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18302 });
18303
18304 let breakpoints = editor.update(cx, |editor, cx| {
18305 editor
18306 .breakpoint_store()
18307 .as_ref()
18308 .unwrap()
18309 .read(cx)
18310 .all_breakpoints(cx)
18311 .clone()
18312 });
18313
18314 assert_breakpoint(
18315 &breakpoints,
18316 &abs_path,
18317 vec![(0, Breakpoint::new_log("hello world"))],
18318 );
18319
18320 // Removing a log message from a log breakpoint should remove it
18321 editor.update_in(cx, |editor, window, cx| {
18322 add_log_breakpoint_at_cursor(editor, "", window, cx);
18323 });
18324
18325 let breakpoints = editor.update(cx, |editor, cx| {
18326 editor
18327 .breakpoint_store()
18328 .as_ref()
18329 .unwrap()
18330 .read(cx)
18331 .all_breakpoints(cx)
18332 .clone()
18333 });
18334
18335 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18336
18337 editor.update_in(cx, |editor, window, cx| {
18338 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18339 editor.move_to_end(&MoveToEnd, window, cx);
18340 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18341 // Not adding a log message to a standard breakpoint shouldn't remove it
18342 add_log_breakpoint_at_cursor(editor, "", window, cx);
18343 });
18344
18345 let breakpoints = editor.update(cx, |editor, cx| {
18346 editor
18347 .breakpoint_store()
18348 .as_ref()
18349 .unwrap()
18350 .read(cx)
18351 .all_breakpoints(cx)
18352 .clone()
18353 });
18354
18355 assert_breakpoint(
18356 &breakpoints,
18357 &abs_path,
18358 vec![
18359 (0, Breakpoint::new_standard()),
18360 (3, Breakpoint::new_standard()),
18361 ],
18362 );
18363
18364 editor.update_in(cx, |editor, window, cx| {
18365 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18366 });
18367
18368 let breakpoints = editor.update(cx, |editor, cx| {
18369 editor
18370 .breakpoint_store()
18371 .as_ref()
18372 .unwrap()
18373 .read(cx)
18374 .all_breakpoints(cx)
18375 .clone()
18376 });
18377
18378 assert_breakpoint(
18379 &breakpoints,
18380 &abs_path,
18381 vec![
18382 (0, Breakpoint::new_standard()),
18383 (3, Breakpoint::new_log("hello world")),
18384 ],
18385 );
18386
18387 editor.update_in(cx, |editor, window, cx| {
18388 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18389 });
18390
18391 let breakpoints = editor.update(cx, |editor, cx| {
18392 editor
18393 .breakpoint_store()
18394 .as_ref()
18395 .unwrap()
18396 .read(cx)
18397 .all_breakpoints(cx)
18398 .clone()
18399 });
18400
18401 assert_breakpoint(
18402 &breakpoints,
18403 &abs_path,
18404 vec![
18405 (0, Breakpoint::new_standard()),
18406 (3, Breakpoint::new_log("hello Earth!!")),
18407 ],
18408 );
18409}
18410
18411/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18412/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18413/// or when breakpoints were placed out of order. This tests for a regression too
18414#[gpui::test]
18415async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18416 init_test(cx, |_| {});
18417
18418 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18419 let fs = FakeFs::new(cx.executor());
18420 fs.insert_tree(
18421 path!("/a"),
18422 json!({
18423 "main.rs": sample_text,
18424 }),
18425 )
18426 .await;
18427 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18428 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18429 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18430
18431 let fs = FakeFs::new(cx.executor());
18432 fs.insert_tree(
18433 path!("/a"),
18434 json!({
18435 "main.rs": sample_text,
18436 }),
18437 )
18438 .await;
18439 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18440 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18441 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18442 let worktree_id = workspace
18443 .update(cx, |workspace, _window, cx| {
18444 workspace.project().update(cx, |project, cx| {
18445 project.worktrees(cx).next().unwrap().read(cx).id()
18446 })
18447 })
18448 .unwrap();
18449
18450 let buffer = project
18451 .update(cx, |project, cx| {
18452 project.open_buffer((worktree_id, "main.rs"), cx)
18453 })
18454 .await
18455 .unwrap();
18456
18457 let (editor, cx) = cx.add_window_view(|window, cx| {
18458 Editor::new(
18459 EditorMode::full(),
18460 MultiBuffer::build_from_buffer(buffer, cx),
18461 Some(project.clone()),
18462 window,
18463 cx,
18464 )
18465 });
18466
18467 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18468 let abs_path = project.read_with(cx, |project, cx| {
18469 project
18470 .absolute_path(&project_path, cx)
18471 .map(|path_buf| Arc::from(path_buf.to_owned()))
18472 .unwrap()
18473 });
18474
18475 // assert we can add breakpoint on the first line
18476 editor.update_in(cx, |editor, window, cx| {
18477 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18478 editor.move_to_end(&MoveToEnd, window, cx);
18479 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18480 editor.move_up(&MoveUp, window, cx);
18481 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18482 });
18483
18484 let breakpoints = editor.update(cx, |editor, cx| {
18485 editor
18486 .breakpoint_store()
18487 .as_ref()
18488 .unwrap()
18489 .read(cx)
18490 .all_breakpoints(cx)
18491 .clone()
18492 });
18493
18494 assert_eq!(1, breakpoints.len());
18495 assert_breakpoint(
18496 &breakpoints,
18497 &abs_path,
18498 vec![
18499 (0, Breakpoint::new_standard()),
18500 (2, Breakpoint::new_standard()),
18501 (3, Breakpoint::new_standard()),
18502 ],
18503 );
18504
18505 editor.update_in(cx, |editor, window, cx| {
18506 editor.move_to_beginning(&MoveToBeginning, window, cx);
18507 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18508 editor.move_to_end(&MoveToEnd, window, cx);
18509 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18510 // Disabling a breakpoint that doesn't exist should do nothing
18511 editor.move_up(&MoveUp, window, cx);
18512 editor.move_up(&MoveUp, window, cx);
18513 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18514 });
18515
18516 let breakpoints = editor.update(cx, |editor, cx| {
18517 editor
18518 .breakpoint_store()
18519 .as_ref()
18520 .unwrap()
18521 .read(cx)
18522 .all_breakpoints(cx)
18523 .clone()
18524 });
18525
18526 let disable_breakpoint = {
18527 let mut bp = Breakpoint::new_standard();
18528 bp.state = BreakpointState::Disabled;
18529 bp
18530 };
18531
18532 assert_eq!(1, breakpoints.len());
18533 assert_breakpoint(
18534 &breakpoints,
18535 &abs_path,
18536 vec![
18537 (0, disable_breakpoint.clone()),
18538 (2, Breakpoint::new_standard()),
18539 (3, disable_breakpoint.clone()),
18540 ],
18541 );
18542
18543 editor.update_in(cx, |editor, window, cx| {
18544 editor.move_to_beginning(&MoveToBeginning, window, cx);
18545 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18546 editor.move_to_end(&MoveToEnd, window, cx);
18547 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18548 editor.move_up(&MoveUp, window, cx);
18549 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18550 });
18551
18552 let breakpoints = editor.update(cx, |editor, cx| {
18553 editor
18554 .breakpoint_store()
18555 .as_ref()
18556 .unwrap()
18557 .read(cx)
18558 .all_breakpoints(cx)
18559 .clone()
18560 });
18561
18562 assert_eq!(1, breakpoints.len());
18563 assert_breakpoint(
18564 &breakpoints,
18565 &abs_path,
18566 vec![
18567 (0, Breakpoint::new_standard()),
18568 (2, disable_breakpoint),
18569 (3, Breakpoint::new_standard()),
18570 ],
18571 );
18572}
18573
18574#[gpui::test]
18575async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18576 init_test(cx, |_| {});
18577 let capabilities = lsp::ServerCapabilities {
18578 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18579 prepare_provider: Some(true),
18580 work_done_progress_options: Default::default(),
18581 })),
18582 ..Default::default()
18583 };
18584 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18585
18586 cx.set_state(indoc! {"
18587 struct Fˇoo {}
18588 "});
18589
18590 cx.update_editor(|editor, _, cx| {
18591 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18592 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18593 editor.highlight_background::<DocumentHighlightRead>(
18594 &[highlight_range],
18595 |c| c.editor_document_highlight_read_background,
18596 cx,
18597 );
18598 });
18599
18600 let mut prepare_rename_handler = cx
18601 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18602 move |_, _, _| async move {
18603 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18604 start: lsp::Position {
18605 line: 0,
18606 character: 7,
18607 },
18608 end: lsp::Position {
18609 line: 0,
18610 character: 10,
18611 },
18612 })))
18613 },
18614 );
18615 let prepare_rename_task = cx
18616 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18617 .expect("Prepare rename was not started");
18618 prepare_rename_handler.next().await.unwrap();
18619 prepare_rename_task.await.expect("Prepare rename failed");
18620
18621 let mut rename_handler =
18622 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18623 let edit = lsp::TextEdit {
18624 range: lsp::Range {
18625 start: lsp::Position {
18626 line: 0,
18627 character: 7,
18628 },
18629 end: lsp::Position {
18630 line: 0,
18631 character: 10,
18632 },
18633 },
18634 new_text: "FooRenamed".to_string(),
18635 };
18636 Ok(Some(lsp::WorkspaceEdit::new(
18637 // Specify the same edit twice
18638 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18639 )))
18640 });
18641 let rename_task = cx
18642 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18643 .expect("Confirm rename was not started");
18644 rename_handler.next().await.unwrap();
18645 rename_task.await.expect("Confirm rename failed");
18646 cx.run_until_parked();
18647
18648 // Despite two edits, only one is actually applied as those are identical
18649 cx.assert_editor_state(indoc! {"
18650 struct FooRenamedˇ {}
18651 "});
18652}
18653
18654#[gpui::test]
18655async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18656 init_test(cx, |_| {});
18657 // These capabilities indicate that the server does not support prepare rename.
18658 let capabilities = lsp::ServerCapabilities {
18659 rename_provider: Some(lsp::OneOf::Left(true)),
18660 ..Default::default()
18661 };
18662 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18663
18664 cx.set_state(indoc! {"
18665 struct Fˇoo {}
18666 "});
18667
18668 cx.update_editor(|editor, _window, cx| {
18669 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18670 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18671 editor.highlight_background::<DocumentHighlightRead>(
18672 &[highlight_range],
18673 |c| c.editor_document_highlight_read_background,
18674 cx,
18675 );
18676 });
18677
18678 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18679 .expect("Prepare rename was not started")
18680 .await
18681 .expect("Prepare rename failed");
18682
18683 let mut rename_handler =
18684 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18685 let edit = lsp::TextEdit {
18686 range: lsp::Range {
18687 start: lsp::Position {
18688 line: 0,
18689 character: 7,
18690 },
18691 end: lsp::Position {
18692 line: 0,
18693 character: 10,
18694 },
18695 },
18696 new_text: "FooRenamed".to_string(),
18697 };
18698 Ok(Some(lsp::WorkspaceEdit::new(
18699 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18700 )))
18701 });
18702 let rename_task = cx
18703 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18704 .expect("Confirm rename was not started");
18705 rename_handler.next().await.unwrap();
18706 rename_task.await.expect("Confirm rename failed");
18707 cx.run_until_parked();
18708
18709 // Correct range is renamed, as `surrounding_word` is used to find it.
18710 cx.assert_editor_state(indoc! {"
18711 struct FooRenamedˇ {}
18712 "});
18713}
18714
18715#[gpui::test]
18716async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18717 init_test(cx, |_| {});
18718 let mut cx = EditorTestContext::new(cx).await;
18719
18720 let language = Arc::new(
18721 Language::new(
18722 LanguageConfig::default(),
18723 Some(tree_sitter_html::LANGUAGE.into()),
18724 )
18725 .with_brackets_query(
18726 r#"
18727 ("<" @open "/>" @close)
18728 ("</" @open ">" @close)
18729 ("<" @open ">" @close)
18730 ("\"" @open "\"" @close)
18731 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18732 "#,
18733 )
18734 .unwrap(),
18735 );
18736 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18737
18738 cx.set_state(indoc! {"
18739 <span>ˇ</span>
18740 "});
18741 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18742 cx.assert_editor_state(indoc! {"
18743 <span>
18744 ˇ
18745 </span>
18746 "});
18747
18748 cx.set_state(indoc! {"
18749 <span><span></span>ˇ</span>
18750 "});
18751 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18752 cx.assert_editor_state(indoc! {"
18753 <span><span></span>
18754 ˇ</span>
18755 "});
18756
18757 cx.set_state(indoc! {"
18758 <span>ˇ
18759 </span>
18760 "});
18761 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18762 cx.assert_editor_state(indoc! {"
18763 <span>
18764 ˇ
18765 </span>
18766 "});
18767}
18768
18769#[gpui::test(iterations = 10)]
18770async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18771 init_test(cx, |_| {});
18772
18773 let fs = FakeFs::new(cx.executor());
18774 fs.insert_tree(
18775 path!("/dir"),
18776 json!({
18777 "a.ts": "a",
18778 }),
18779 )
18780 .await;
18781
18782 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18783 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18784 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18785
18786 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18787 language_registry.add(Arc::new(Language::new(
18788 LanguageConfig {
18789 name: "TypeScript".into(),
18790 matcher: LanguageMatcher {
18791 path_suffixes: vec!["ts".to_string()],
18792 ..Default::default()
18793 },
18794 ..Default::default()
18795 },
18796 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18797 )));
18798 let mut fake_language_servers = language_registry.register_fake_lsp(
18799 "TypeScript",
18800 FakeLspAdapter {
18801 capabilities: lsp::ServerCapabilities {
18802 code_lens_provider: Some(lsp::CodeLensOptions {
18803 resolve_provider: Some(true),
18804 }),
18805 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18806 commands: vec!["_the/command".to_string()],
18807 ..lsp::ExecuteCommandOptions::default()
18808 }),
18809 ..lsp::ServerCapabilities::default()
18810 },
18811 ..FakeLspAdapter::default()
18812 },
18813 );
18814
18815 let (buffer, _handle) = project
18816 .update(cx, |p, cx| {
18817 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18818 })
18819 .await
18820 .unwrap();
18821 cx.executor().run_until_parked();
18822
18823 let fake_server = fake_language_servers.next().await.unwrap();
18824
18825 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18826 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18827 drop(buffer_snapshot);
18828 let actions = cx
18829 .update_window(*workspace, |_, window, cx| {
18830 project.code_actions(&buffer, anchor..anchor, window, cx)
18831 })
18832 .unwrap();
18833
18834 fake_server
18835 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18836 Ok(Some(vec![
18837 lsp::CodeLens {
18838 range: lsp::Range::default(),
18839 command: Some(lsp::Command {
18840 title: "Code lens command".to_owned(),
18841 command: "_the/command".to_owned(),
18842 arguments: None,
18843 }),
18844 data: None,
18845 },
18846 lsp::CodeLens {
18847 range: lsp::Range::default(),
18848 command: Some(lsp::Command {
18849 title: "Command not in capabilities".to_owned(),
18850 command: "not in capabilities".to_owned(),
18851 arguments: None,
18852 }),
18853 data: None,
18854 },
18855 lsp::CodeLens {
18856 range: lsp::Range {
18857 start: lsp::Position {
18858 line: 1,
18859 character: 1,
18860 },
18861 end: lsp::Position {
18862 line: 1,
18863 character: 1,
18864 },
18865 },
18866 command: Some(lsp::Command {
18867 title: "Command not in range".to_owned(),
18868 command: "_the/command".to_owned(),
18869 arguments: None,
18870 }),
18871 data: None,
18872 },
18873 ]))
18874 })
18875 .next()
18876 .await;
18877
18878 let actions = actions.await.unwrap();
18879 assert_eq!(
18880 actions.len(),
18881 1,
18882 "Should have only one valid action for the 0..0 range"
18883 );
18884 let action = actions[0].clone();
18885 let apply = project.update(cx, |project, cx| {
18886 project.apply_code_action(buffer.clone(), action, true, cx)
18887 });
18888
18889 // Resolving the code action does not populate its edits. In absence of
18890 // edits, we must execute the given command.
18891 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18892 |mut lens, _| async move {
18893 let lens_command = lens.command.as_mut().expect("should have a command");
18894 assert_eq!(lens_command.title, "Code lens command");
18895 lens_command.arguments = Some(vec![json!("the-argument")]);
18896 Ok(lens)
18897 },
18898 );
18899
18900 // While executing the command, the language server sends the editor
18901 // a `workspaceEdit` request.
18902 fake_server
18903 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18904 let fake = fake_server.clone();
18905 move |params, _| {
18906 assert_eq!(params.command, "_the/command");
18907 let fake = fake.clone();
18908 async move {
18909 fake.server
18910 .request::<lsp::request::ApplyWorkspaceEdit>(
18911 lsp::ApplyWorkspaceEditParams {
18912 label: None,
18913 edit: lsp::WorkspaceEdit {
18914 changes: Some(
18915 [(
18916 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18917 vec![lsp::TextEdit {
18918 range: lsp::Range::new(
18919 lsp::Position::new(0, 0),
18920 lsp::Position::new(0, 0),
18921 ),
18922 new_text: "X".into(),
18923 }],
18924 )]
18925 .into_iter()
18926 .collect(),
18927 ),
18928 ..Default::default()
18929 },
18930 },
18931 )
18932 .await
18933 .unwrap();
18934 Ok(Some(json!(null)))
18935 }
18936 }
18937 })
18938 .next()
18939 .await;
18940
18941 // Applying the code lens command returns a project transaction containing the edits
18942 // sent by the language server in its `workspaceEdit` request.
18943 let transaction = apply.await.unwrap();
18944 assert!(transaction.0.contains_key(&buffer));
18945 buffer.update(cx, |buffer, cx| {
18946 assert_eq!(buffer.text(), "Xa");
18947 buffer.undo(cx);
18948 assert_eq!(buffer.text(), "a");
18949 });
18950}
18951
18952#[gpui::test]
18953async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18954 init_test(cx, |_| {});
18955
18956 let fs = FakeFs::new(cx.executor());
18957 let main_text = r#"fn main() {
18958println!("1");
18959println!("2");
18960println!("3");
18961println!("4");
18962println!("5");
18963}"#;
18964 let lib_text = "mod foo {}";
18965 fs.insert_tree(
18966 path!("/a"),
18967 json!({
18968 "lib.rs": lib_text,
18969 "main.rs": main_text,
18970 }),
18971 )
18972 .await;
18973
18974 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18975 let (workspace, cx) =
18976 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18977 let worktree_id = workspace.update(cx, |workspace, cx| {
18978 workspace.project().update(cx, |project, cx| {
18979 project.worktrees(cx).next().unwrap().read(cx).id()
18980 })
18981 });
18982
18983 let expected_ranges = vec![
18984 Point::new(0, 0)..Point::new(0, 0),
18985 Point::new(1, 0)..Point::new(1, 1),
18986 Point::new(2, 0)..Point::new(2, 2),
18987 Point::new(3, 0)..Point::new(3, 3),
18988 ];
18989
18990 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18991 let editor_1 = workspace
18992 .update_in(cx, |workspace, window, cx| {
18993 workspace.open_path(
18994 (worktree_id, "main.rs"),
18995 Some(pane_1.downgrade()),
18996 true,
18997 window,
18998 cx,
18999 )
19000 })
19001 .unwrap()
19002 .await
19003 .downcast::<Editor>()
19004 .unwrap();
19005 pane_1.update(cx, |pane, cx| {
19006 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19007 open_editor.update(cx, |editor, cx| {
19008 assert_eq!(
19009 editor.display_text(cx),
19010 main_text,
19011 "Original main.rs text on initial open",
19012 );
19013 assert_eq!(
19014 editor
19015 .selections
19016 .all::<Point>(cx)
19017 .into_iter()
19018 .map(|s| s.range())
19019 .collect::<Vec<_>>(),
19020 vec![Point::zero()..Point::zero()],
19021 "Default selections on initial open",
19022 );
19023 })
19024 });
19025 editor_1.update_in(cx, |editor, window, cx| {
19026 editor.change_selections(None, window, cx, |s| {
19027 s.select_ranges(expected_ranges.clone());
19028 });
19029 });
19030
19031 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19032 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19033 });
19034 let editor_2 = workspace
19035 .update_in(cx, |workspace, window, cx| {
19036 workspace.open_path(
19037 (worktree_id, "main.rs"),
19038 Some(pane_2.downgrade()),
19039 true,
19040 window,
19041 cx,
19042 )
19043 })
19044 .unwrap()
19045 .await
19046 .downcast::<Editor>()
19047 .unwrap();
19048 pane_2.update(cx, |pane, cx| {
19049 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19050 open_editor.update(cx, |editor, cx| {
19051 assert_eq!(
19052 editor.display_text(cx),
19053 main_text,
19054 "Original main.rs text on initial open in another panel",
19055 );
19056 assert_eq!(
19057 editor
19058 .selections
19059 .all::<Point>(cx)
19060 .into_iter()
19061 .map(|s| s.range())
19062 .collect::<Vec<_>>(),
19063 vec![Point::zero()..Point::zero()],
19064 "Default selections on initial open in another panel",
19065 );
19066 })
19067 });
19068
19069 editor_2.update_in(cx, |editor, window, cx| {
19070 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19071 });
19072
19073 let _other_editor_1 = workspace
19074 .update_in(cx, |workspace, window, cx| {
19075 workspace.open_path(
19076 (worktree_id, "lib.rs"),
19077 Some(pane_1.downgrade()),
19078 true,
19079 window,
19080 cx,
19081 )
19082 })
19083 .unwrap()
19084 .await
19085 .downcast::<Editor>()
19086 .unwrap();
19087 pane_1
19088 .update_in(cx, |pane, window, cx| {
19089 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19090 .unwrap()
19091 })
19092 .await
19093 .unwrap();
19094 drop(editor_1);
19095 pane_1.update(cx, |pane, cx| {
19096 pane.active_item()
19097 .unwrap()
19098 .downcast::<Editor>()
19099 .unwrap()
19100 .update(cx, |editor, cx| {
19101 assert_eq!(
19102 editor.display_text(cx),
19103 lib_text,
19104 "Other file should be open and active",
19105 );
19106 });
19107 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19108 });
19109
19110 let _other_editor_2 = workspace
19111 .update_in(cx, |workspace, window, cx| {
19112 workspace.open_path(
19113 (worktree_id, "lib.rs"),
19114 Some(pane_2.downgrade()),
19115 true,
19116 window,
19117 cx,
19118 )
19119 })
19120 .unwrap()
19121 .await
19122 .downcast::<Editor>()
19123 .unwrap();
19124 pane_2
19125 .update_in(cx, |pane, window, cx| {
19126 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19127 .unwrap()
19128 })
19129 .await
19130 .unwrap();
19131 drop(editor_2);
19132 pane_2.update(cx, |pane, cx| {
19133 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19134 open_editor.update(cx, |editor, cx| {
19135 assert_eq!(
19136 editor.display_text(cx),
19137 lib_text,
19138 "Other file should be open and active in another panel too",
19139 );
19140 });
19141 assert_eq!(
19142 pane.items().count(),
19143 1,
19144 "No other editors should be open in another pane",
19145 );
19146 });
19147
19148 let _editor_1_reopened = workspace
19149 .update_in(cx, |workspace, window, cx| {
19150 workspace.open_path(
19151 (worktree_id, "main.rs"),
19152 Some(pane_1.downgrade()),
19153 true,
19154 window,
19155 cx,
19156 )
19157 })
19158 .unwrap()
19159 .await
19160 .downcast::<Editor>()
19161 .unwrap();
19162 let _editor_2_reopened = workspace
19163 .update_in(cx, |workspace, window, cx| {
19164 workspace.open_path(
19165 (worktree_id, "main.rs"),
19166 Some(pane_2.downgrade()),
19167 true,
19168 window,
19169 cx,
19170 )
19171 })
19172 .unwrap()
19173 .await
19174 .downcast::<Editor>()
19175 .unwrap();
19176 pane_1.update(cx, |pane, cx| {
19177 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19178 open_editor.update(cx, |editor, cx| {
19179 assert_eq!(
19180 editor.display_text(cx),
19181 main_text,
19182 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19183 );
19184 assert_eq!(
19185 editor
19186 .selections
19187 .all::<Point>(cx)
19188 .into_iter()
19189 .map(|s| s.range())
19190 .collect::<Vec<_>>(),
19191 expected_ranges,
19192 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19193 );
19194 })
19195 });
19196 pane_2.update(cx, |pane, cx| {
19197 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19198 open_editor.update(cx, |editor, cx| {
19199 assert_eq!(
19200 editor.display_text(cx),
19201 r#"fn main() {
19202⋯rintln!("1");
19203⋯intln!("2");
19204⋯ntln!("3");
19205println!("4");
19206println!("5");
19207}"#,
19208 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19209 );
19210 assert_eq!(
19211 editor
19212 .selections
19213 .all::<Point>(cx)
19214 .into_iter()
19215 .map(|s| s.range())
19216 .collect::<Vec<_>>(),
19217 vec![Point::zero()..Point::zero()],
19218 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19219 );
19220 })
19221 });
19222}
19223
19224#[gpui::test]
19225async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19226 init_test(cx, |_| {});
19227
19228 let fs = FakeFs::new(cx.executor());
19229 let main_text = r#"fn main() {
19230println!("1");
19231println!("2");
19232println!("3");
19233println!("4");
19234println!("5");
19235}"#;
19236 let lib_text = "mod foo {}";
19237 fs.insert_tree(
19238 path!("/a"),
19239 json!({
19240 "lib.rs": lib_text,
19241 "main.rs": main_text,
19242 }),
19243 )
19244 .await;
19245
19246 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19247 let (workspace, cx) =
19248 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19249 let worktree_id = workspace.update(cx, |workspace, cx| {
19250 workspace.project().update(cx, |project, cx| {
19251 project.worktrees(cx).next().unwrap().read(cx).id()
19252 })
19253 });
19254
19255 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19256 let editor = workspace
19257 .update_in(cx, |workspace, window, cx| {
19258 workspace.open_path(
19259 (worktree_id, "main.rs"),
19260 Some(pane.downgrade()),
19261 true,
19262 window,
19263 cx,
19264 )
19265 })
19266 .unwrap()
19267 .await
19268 .downcast::<Editor>()
19269 .unwrap();
19270 pane.update(cx, |pane, cx| {
19271 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19272 open_editor.update(cx, |editor, cx| {
19273 assert_eq!(
19274 editor.display_text(cx),
19275 main_text,
19276 "Original main.rs text on initial open",
19277 );
19278 })
19279 });
19280 editor.update_in(cx, |editor, window, cx| {
19281 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19282 });
19283
19284 cx.update_global(|store: &mut SettingsStore, cx| {
19285 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19286 s.restore_on_file_reopen = Some(false);
19287 });
19288 });
19289 editor.update_in(cx, |editor, window, cx| {
19290 editor.fold_ranges(
19291 vec![
19292 Point::new(1, 0)..Point::new(1, 1),
19293 Point::new(2, 0)..Point::new(2, 2),
19294 Point::new(3, 0)..Point::new(3, 3),
19295 ],
19296 false,
19297 window,
19298 cx,
19299 );
19300 });
19301 pane.update_in(cx, |pane, window, cx| {
19302 pane.close_all_items(&CloseAllItems::default(), window, cx)
19303 .unwrap()
19304 })
19305 .await
19306 .unwrap();
19307 pane.update(cx, |pane, _| {
19308 assert!(pane.active_item().is_none());
19309 });
19310 cx.update_global(|store: &mut SettingsStore, cx| {
19311 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19312 s.restore_on_file_reopen = Some(true);
19313 });
19314 });
19315
19316 let _editor_reopened = workspace
19317 .update_in(cx, |workspace, window, cx| {
19318 workspace.open_path(
19319 (worktree_id, "main.rs"),
19320 Some(pane.downgrade()),
19321 true,
19322 window,
19323 cx,
19324 )
19325 })
19326 .unwrap()
19327 .await
19328 .downcast::<Editor>()
19329 .unwrap();
19330 pane.update(cx, |pane, cx| {
19331 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19332 open_editor.update(cx, |editor, cx| {
19333 assert_eq!(
19334 editor.display_text(cx),
19335 main_text,
19336 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19337 );
19338 })
19339 });
19340}
19341
19342fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19343 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19344 point..point
19345}
19346
19347fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19348 let (text, ranges) = marked_text_ranges(marked_text, true);
19349 assert_eq!(editor.text(cx), text);
19350 assert_eq!(
19351 editor.selections.ranges(cx),
19352 ranges,
19353 "Assert selections are {}",
19354 marked_text
19355 );
19356}
19357
19358pub fn handle_signature_help_request(
19359 cx: &mut EditorLspTestContext,
19360 mocked_response: lsp::SignatureHelp,
19361) -> impl Future<Output = ()> + use<> {
19362 let mut request =
19363 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19364 let mocked_response = mocked_response.clone();
19365 async move { Ok(Some(mocked_response)) }
19366 });
19367
19368 async move {
19369 request.next().await;
19370 }
19371}
19372
19373/// Handle completion request passing a marked string specifying where the completion
19374/// should be triggered from using '|' character, what range should be replaced, and what completions
19375/// should be returned using '<' and '>' to delimit the range.
19376///
19377/// Also see `handle_completion_request_with_insert_and_replace`.
19378#[track_caller]
19379pub fn handle_completion_request(
19380 cx: &mut EditorLspTestContext,
19381 marked_string: &str,
19382 completions: Vec<&'static str>,
19383 counter: Arc<AtomicUsize>,
19384) -> impl Future<Output = ()> {
19385 let complete_from_marker: TextRangeMarker = '|'.into();
19386 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19387 let (_, mut marked_ranges) = marked_text_ranges_by(
19388 marked_string,
19389 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19390 );
19391
19392 let complete_from_position =
19393 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19394 let replace_range =
19395 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19396
19397 let mut request =
19398 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19399 let completions = completions.clone();
19400 counter.fetch_add(1, atomic::Ordering::Release);
19401 async move {
19402 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19403 assert_eq!(
19404 params.text_document_position.position,
19405 complete_from_position
19406 );
19407 Ok(Some(lsp::CompletionResponse::Array(
19408 completions
19409 .iter()
19410 .map(|completion_text| lsp::CompletionItem {
19411 label: completion_text.to_string(),
19412 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19413 range: replace_range,
19414 new_text: completion_text.to_string(),
19415 })),
19416 ..Default::default()
19417 })
19418 .collect(),
19419 )))
19420 }
19421 });
19422
19423 async move {
19424 request.next().await;
19425 }
19426}
19427
19428/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19429/// given instead, which also contains an `insert` range.
19430///
19431/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19432/// that is, `replace_range.start..cursor_pos`.
19433pub fn handle_completion_request_with_insert_and_replace(
19434 cx: &mut EditorLspTestContext,
19435 marked_string: &str,
19436 completions: Vec<&'static str>,
19437 counter: Arc<AtomicUsize>,
19438) -> impl Future<Output = ()> {
19439 let complete_from_marker: TextRangeMarker = '|'.into();
19440 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19441 let (_, mut marked_ranges) = marked_text_ranges_by(
19442 marked_string,
19443 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19444 );
19445
19446 let complete_from_position =
19447 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19448 let replace_range =
19449 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19450
19451 let mut request =
19452 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19453 let completions = completions.clone();
19454 counter.fetch_add(1, atomic::Ordering::Release);
19455 async move {
19456 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19457 assert_eq!(
19458 params.text_document_position.position, complete_from_position,
19459 "marker `|` position doesn't match",
19460 );
19461 Ok(Some(lsp::CompletionResponse::Array(
19462 completions
19463 .iter()
19464 .map(|completion_text| lsp::CompletionItem {
19465 label: completion_text.to_string(),
19466 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19467 lsp::InsertReplaceEdit {
19468 insert: lsp::Range {
19469 start: replace_range.start,
19470 end: complete_from_position,
19471 },
19472 replace: replace_range,
19473 new_text: completion_text.to_string(),
19474 },
19475 )),
19476 ..Default::default()
19477 })
19478 .collect(),
19479 )))
19480 }
19481 });
19482
19483 async move {
19484 request.next().await;
19485 }
19486}
19487
19488fn handle_resolve_completion_request(
19489 cx: &mut EditorLspTestContext,
19490 edits: Option<Vec<(&'static str, &'static str)>>,
19491) -> impl Future<Output = ()> {
19492 let edits = edits.map(|edits| {
19493 edits
19494 .iter()
19495 .map(|(marked_string, new_text)| {
19496 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19497 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19498 lsp::TextEdit::new(replace_range, new_text.to_string())
19499 })
19500 .collect::<Vec<_>>()
19501 });
19502
19503 let mut request =
19504 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19505 let edits = edits.clone();
19506 async move {
19507 Ok(lsp::CompletionItem {
19508 additional_text_edits: edits,
19509 ..Default::default()
19510 })
19511 }
19512 });
19513
19514 async move {
19515 request.next().await;
19516 }
19517}
19518
19519pub(crate) fn update_test_language_settings(
19520 cx: &mut TestAppContext,
19521 f: impl Fn(&mut AllLanguageSettingsContent),
19522) {
19523 cx.update(|cx| {
19524 SettingsStore::update_global(cx, |store, cx| {
19525 store.update_user_settings::<AllLanguageSettings>(cx, f);
19526 });
19527 });
19528}
19529
19530pub(crate) fn update_test_project_settings(
19531 cx: &mut TestAppContext,
19532 f: impl Fn(&mut ProjectSettings),
19533) {
19534 cx.update(|cx| {
19535 SettingsStore::update_global(cx, |store, cx| {
19536 store.update_user_settings::<ProjectSettings>(cx, f);
19537 });
19538 });
19539}
19540
19541pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19542 cx.update(|cx| {
19543 assets::Assets.load_test_fonts(cx);
19544 let store = SettingsStore::test(cx);
19545 cx.set_global(store);
19546 theme::init(theme::LoadThemes::JustBase, cx);
19547 release_channel::init(SemanticVersion::default(), cx);
19548 client::init_settings(cx);
19549 language::init(cx);
19550 Project::init_settings(cx);
19551 workspace::init_settings(cx);
19552 crate::init(cx);
19553 });
19554
19555 update_test_language_settings(cx, f);
19556}
19557
19558#[track_caller]
19559fn assert_hunk_revert(
19560 not_reverted_text_with_selections: &str,
19561 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19562 expected_reverted_text_with_selections: &str,
19563 base_text: &str,
19564 cx: &mut EditorLspTestContext,
19565) {
19566 cx.set_state(not_reverted_text_with_selections);
19567 cx.set_head_text(base_text);
19568 cx.executor().run_until_parked();
19569
19570 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19571 let snapshot = editor.snapshot(window, cx);
19572 let reverted_hunk_statuses = snapshot
19573 .buffer_snapshot
19574 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19575 .map(|hunk| hunk.status().kind)
19576 .collect::<Vec<_>>();
19577
19578 editor.git_restore(&Default::default(), window, cx);
19579 reverted_hunk_statuses
19580 });
19581 cx.executor().run_until_parked();
19582 cx.assert_editor_state(expected_reverted_text_with_selections);
19583 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19584}