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, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
16 VisualTestContext, 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 // when all cursors are to the left of the suggested indent, then auto-indent all.
2874 cx.set_state(indoc! {"
2875 const a: B = (
2876 c(
2877 ˇ
2878 ˇ )
2879 );
2880 "});
2881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2882 cx.assert_editor_state(indoc! {"
2883 const a: B = (
2884 c(
2885 ˇ
2886 ˇ)
2887 );
2888 "});
2889
2890 // cursors that are already at the suggested indent level do not move
2891 // until other cursors that are to the left of the suggested indent
2892 // auto-indent.
2893 cx.set_state(indoc! {"
2894 ˇ
2895 const a: B = (
2896 c(
2897 d(
2898 ˇ
2899 )
2900 ˇ
2901 ˇ )
2902 );
2903 "});
2904 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2905 cx.assert_editor_state(indoc! {"
2906 ˇ
2907 const a: B = (
2908 c(
2909 d(
2910 ˇ
2911 )
2912 ˇ
2913 ˇ)
2914 );
2915 "});
2916 // once all multi-cursors are at the suggested
2917 // indent level, they all insert a soft tab together.
2918 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2919 cx.assert_editor_state(indoc! {"
2920 ˇ
2921 const a: B = (
2922 c(
2923 d(
2924 ˇ
2925 )
2926 ˇ
2927 ˇ)
2928 );
2929 "});
2930
2931 // handle auto-indent when there are multiple cursors on the same line
2932 cx.set_state(indoc! {"
2933 const a: B = (
2934 c(
2935 ˇ ˇ
2936 ˇ )
2937 );
2938 "});
2939 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2940 cx.assert_editor_state(indoc! {"
2941 const a: B = (
2942 c(
2943 ˇ
2944 ˇ)
2945 );
2946 "});
2947}
2948
2949#[gpui::test]
2950async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
2951 init_test(cx, |settings| {
2952 settings.defaults.tab_size = NonZeroU32::new(3)
2953 });
2954
2955 let mut cx = EditorTestContext::new(cx).await;
2956 cx.set_state(indoc! {"
2957 ˇ
2958 \t ˇ
2959 \t ˇ
2960 \t ˇ
2961 \t \t\t \t \t\t \t\t \t \t ˇ
2962 "});
2963
2964 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2965 cx.assert_editor_state(indoc! {"
2966 ˇ
2967 \t ˇ
2968 \t ˇ
2969 \t ˇ
2970 \t \t\t \t \t\t \t\t \t \t ˇ
2971 "});
2972}
2973
2974#[gpui::test]
2975async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
2976 init_test(cx, |settings| {
2977 settings.defaults.tab_size = NonZeroU32::new(4)
2978 });
2979
2980 let language = Arc::new(
2981 Language::new(
2982 LanguageConfig::default(),
2983 Some(tree_sitter_rust::LANGUAGE.into()),
2984 )
2985 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2986 .unwrap(),
2987 );
2988
2989 let mut cx = EditorTestContext::new(cx).await;
2990 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2991 cx.set_state(indoc! {"
2992 fn a() {
2993 if b {
2994 \t ˇc
2995 }
2996 }
2997 "});
2998
2999 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3000 cx.assert_editor_state(indoc! {"
3001 fn a() {
3002 if b {
3003 ˇc
3004 }
3005 }
3006 "});
3007}
3008
3009#[gpui::test]
3010async fn test_indent_outdent(cx: &mut TestAppContext) {
3011 init_test(cx, |settings| {
3012 settings.defaults.tab_size = NonZeroU32::new(4);
3013 });
3014
3015 let mut cx = EditorTestContext::new(cx).await;
3016
3017 cx.set_state(indoc! {"
3018 «oneˇ» «twoˇ»
3019 three
3020 four
3021 "});
3022 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3023 cx.assert_editor_state(indoc! {"
3024 «oneˇ» «twoˇ»
3025 three
3026 four
3027 "});
3028
3029 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3030 cx.assert_editor_state(indoc! {"
3031 «oneˇ» «twoˇ»
3032 three
3033 four
3034 "});
3035
3036 // select across line ending
3037 cx.set_state(indoc! {"
3038 one two
3039 t«hree
3040 ˇ» four
3041 "});
3042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3043 cx.assert_editor_state(indoc! {"
3044 one two
3045 t«hree
3046 ˇ» four
3047 "});
3048
3049 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 one two
3052 t«hree
3053 ˇ» four
3054 "});
3055
3056 // Ensure that indenting/outdenting works when the cursor is at column 0.
3057 cx.set_state(indoc! {"
3058 one two
3059 ˇthree
3060 four
3061 "});
3062 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 one two
3065 ˇthree
3066 four
3067 "});
3068
3069 cx.set_state(indoc! {"
3070 one two
3071 ˇ three
3072 four
3073 "});
3074 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3075 cx.assert_editor_state(indoc! {"
3076 one two
3077 ˇthree
3078 four
3079 "});
3080}
3081
3082#[gpui::test]
3083async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3084 init_test(cx, |settings| {
3085 settings.defaults.hard_tabs = Some(true);
3086 });
3087
3088 let mut cx = EditorTestContext::new(cx).await;
3089
3090 // select two ranges on one line
3091 cx.set_state(indoc! {"
3092 «oneˇ» «twoˇ»
3093 three
3094 four
3095 "});
3096 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3097 cx.assert_editor_state(indoc! {"
3098 \t«oneˇ» «twoˇ»
3099 three
3100 four
3101 "});
3102 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3103 cx.assert_editor_state(indoc! {"
3104 \t\t«oneˇ» «twoˇ»
3105 three
3106 four
3107 "});
3108 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3109 cx.assert_editor_state(indoc! {"
3110 \t«oneˇ» «twoˇ»
3111 three
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 three
3118 four
3119 "});
3120
3121 // select across a line ending
3122 cx.set_state(indoc! {"
3123 one two
3124 t«hree
3125 ˇ»four
3126 "});
3127 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 one two
3130 \tt«hree
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\tt«hree
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 \tt«hree
3143 ˇ»four
3144 "});
3145 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3146 cx.assert_editor_state(indoc! {"
3147 one two
3148 t«hree
3149 ˇ»four
3150 "});
3151
3152 // Ensure that indenting/outdenting works when the cursor is at column 0.
3153 cx.set_state(indoc! {"
3154 one two
3155 ˇthree
3156 four
3157 "});
3158 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3159 cx.assert_editor_state(indoc! {"
3160 one two
3161 ˇthree
3162 four
3163 "});
3164 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3165 cx.assert_editor_state(indoc! {"
3166 one two
3167 \tˇthree
3168 four
3169 "});
3170 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3171 cx.assert_editor_state(indoc! {"
3172 one two
3173 ˇthree
3174 four
3175 "});
3176}
3177
3178#[gpui::test]
3179fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3180 init_test(cx, |settings| {
3181 settings.languages.extend([
3182 (
3183 "TOML".into(),
3184 LanguageSettingsContent {
3185 tab_size: NonZeroU32::new(2),
3186 ..Default::default()
3187 },
3188 ),
3189 (
3190 "Rust".into(),
3191 LanguageSettingsContent {
3192 tab_size: NonZeroU32::new(4),
3193 ..Default::default()
3194 },
3195 ),
3196 ]);
3197 });
3198
3199 let toml_language = Arc::new(Language::new(
3200 LanguageConfig {
3201 name: "TOML".into(),
3202 ..Default::default()
3203 },
3204 None,
3205 ));
3206 let rust_language = Arc::new(Language::new(
3207 LanguageConfig {
3208 name: "Rust".into(),
3209 ..Default::default()
3210 },
3211 None,
3212 ));
3213
3214 let toml_buffer =
3215 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3216 let rust_buffer =
3217 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3218 let multibuffer = cx.new(|cx| {
3219 let mut multibuffer = MultiBuffer::new(ReadWrite);
3220 multibuffer.push_excerpts(
3221 toml_buffer.clone(),
3222 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3223 cx,
3224 );
3225 multibuffer.push_excerpts(
3226 rust_buffer.clone(),
3227 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3228 cx,
3229 );
3230 multibuffer
3231 });
3232
3233 cx.add_window(|window, cx| {
3234 let mut editor = build_editor(multibuffer, window, cx);
3235
3236 assert_eq!(
3237 editor.text(cx),
3238 indoc! {"
3239 a = 1
3240 b = 2
3241
3242 const c: usize = 3;
3243 "}
3244 );
3245
3246 select_ranges(
3247 &mut editor,
3248 indoc! {"
3249 «aˇ» = 1
3250 b = 2
3251
3252 «const c:ˇ» usize = 3;
3253 "},
3254 window,
3255 cx,
3256 );
3257
3258 editor.tab(&Tab, window, cx);
3259 assert_text_with_selections(
3260 &mut editor,
3261 indoc! {"
3262 «aˇ» = 1
3263 b = 2
3264
3265 «const c:ˇ» usize = 3;
3266 "},
3267 cx,
3268 );
3269 editor.backtab(&Backtab, window, cx);
3270 assert_text_with_selections(
3271 &mut editor,
3272 indoc! {"
3273 «aˇ» = 1
3274 b = 2
3275
3276 «const c:ˇ» usize = 3;
3277 "},
3278 cx,
3279 );
3280
3281 editor
3282 });
3283}
3284
3285#[gpui::test]
3286async fn test_backspace(cx: &mut TestAppContext) {
3287 init_test(cx, |_| {});
3288
3289 let mut cx = EditorTestContext::new(cx).await;
3290
3291 // Basic backspace
3292 cx.set_state(indoc! {"
3293 onˇe two three
3294 fou«rˇ» five six
3295 seven «ˇeight nine
3296 »ten
3297 "});
3298 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3299 cx.assert_editor_state(indoc! {"
3300 oˇe two three
3301 fouˇ five six
3302 seven ˇten
3303 "});
3304
3305 // Test backspace inside and around indents
3306 cx.set_state(indoc! {"
3307 zero
3308 ˇone
3309 ˇtwo
3310 ˇ ˇ ˇ three
3311 ˇ ˇ four
3312 "});
3313 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3314 cx.assert_editor_state(indoc! {"
3315 zero
3316 ˇone
3317 ˇtwo
3318 ˇ threeˇ four
3319 "});
3320}
3321
3322#[gpui::test]
3323async fn test_delete(cx: &mut TestAppContext) {
3324 init_test(cx, |_| {});
3325
3326 let mut cx = EditorTestContext::new(cx).await;
3327 cx.set_state(indoc! {"
3328 onˇe two three
3329 fou«rˇ» five six
3330 seven «ˇeight nine
3331 »ten
3332 "});
3333 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3334 cx.assert_editor_state(indoc! {"
3335 onˇ two three
3336 fouˇ five six
3337 seven ˇten
3338 "});
3339}
3340
3341#[gpui::test]
3342fn test_delete_line(cx: &mut TestAppContext) {
3343 init_test(cx, |_| {});
3344
3345 let editor = cx.add_window(|window, cx| {
3346 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3347 build_editor(buffer, window, cx)
3348 });
3349 _ = editor.update(cx, |editor, window, cx| {
3350 editor.change_selections(None, window, cx, |s| {
3351 s.select_display_ranges([
3352 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3353 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3354 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3355 ])
3356 });
3357 editor.delete_line(&DeleteLine, window, cx);
3358 assert_eq!(editor.display_text(cx), "ghi");
3359 assert_eq!(
3360 editor.selections.display_ranges(cx),
3361 vec![
3362 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3363 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3364 ]
3365 );
3366 });
3367
3368 let editor = cx.add_window(|window, cx| {
3369 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3370 build_editor(buffer, window, cx)
3371 });
3372 _ = editor.update(cx, |editor, window, cx| {
3373 editor.change_selections(None, window, cx, |s| {
3374 s.select_display_ranges([
3375 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3376 ])
3377 });
3378 editor.delete_line(&DeleteLine, window, cx);
3379 assert_eq!(editor.display_text(cx), "ghi\n");
3380 assert_eq!(
3381 editor.selections.display_ranges(cx),
3382 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3383 );
3384 });
3385}
3386
3387#[gpui::test]
3388fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3389 init_test(cx, |_| {});
3390
3391 cx.add_window(|window, cx| {
3392 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3393 let mut editor = build_editor(buffer.clone(), window, cx);
3394 let buffer = buffer.read(cx).as_singleton().unwrap();
3395
3396 assert_eq!(
3397 editor.selections.ranges::<Point>(cx),
3398 &[Point::new(0, 0)..Point::new(0, 0)]
3399 );
3400
3401 // When on single line, replace newline at end by space
3402 editor.join_lines(&JoinLines, window, cx);
3403 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3404 assert_eq!(
3405 editor.selections.ranges::<Point>(cx),
3406 &[Point::new(0, 3)..Point::new(0, 3)]
3407 );
3408
3409 // When multiple lines are selected, remove newlines that are spanned by the selection
3410 editor.change_selections(None, window, cx, |s| {
3411 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3412 });
3413 editor.join_lines(&JoinLines, window, cx);
3414 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3415 assert_eq!(
3416 editor.selections.ranges::<Point>(cx),
3417 &[Point::new(0, 11)..Point::new(0, 11)]
3418 );
3419
3420 // Undo should be transactional
3421 editor.undo(&Undo, window, cx);
3422 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3423 assert_eq!(
3424 editor.selections.ranges::<Point>(cx),
3425 &[Point::new(0, 5)..Point::new(2, 2)]
3426 );
3427
3428 // When joining an empty line don't insert a space
3429 editor.change_selections(None, window, cx, |s| {
3430 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3431 });
3432 editor.join_lines(&JoinLines, window, cx);
3433 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3434 assert_eq!(
3435 editor.selections.ranges::<Point>(cx),
3436 [Point::new(2, 3)..Point::new(2, 3)]
3437 );
3438
3439 // We can remove trailing newlines
3440 editor.join_lines(&JoinLines, window, cx);
3441 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3442 assert_eq!(
3443 editor.selections.ranges::<Point>(cx),
3444 [Point::new(2, 3)..Point::new(2, 3)]
3445 );
3446
3447 // We don't blow up on the last line
3448 editor.join_lines(&JoinLines, window, cx);
3449 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3450 assert_eq!(
3451 editor.selections.ranges::<Point>(cx),
3452 [Point::new(2, 3)..Point::new(2, 3)]
3453 );
3454
3455 // reset to test indentation
3456 editor.buffer.update(cx, |buffer, cx| {
3457 buffer.edit(
3458 [
3459 (Point::new(1, 0)..Point::new(1, 2), " "),
3460 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3461 ],
3462 None,
3463 cx,
3464 )
3465 });
3466
3467 // We remove any leading spaces
3468 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3469 editor.change_selections(None, window, cx, |s| {
3470 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3471 });
3472 editor.join_lines(&JoinLines, window, cx);
3473 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3474
3475 // We don't insert a space for a line containing only spaces
3476 editor.join_lines(&JoinLines, window, cx);
3477 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3478
3479 // We ignore any leading tabs
3480 editor.join_lines(&JoinLines, window, cx);
3481 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3482
3483 editor
3484 });
3485}
3486
3487#[gpui::test]
3488fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3489 init_test(cx, |_| {});
3490
3491 cx.add_window(|window, cx| {
3492 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3493 let mut editor = build_editor(buffer.clone(), window, cx);
3494 let buffer = buffer.read(cx).as_singleton().unwrap();
3495
3496 editor.change_selections(None, window, cx, |s| {
3497 s.select_ranges([
3498 Point::new(0, 2)..Point::new(1, 1),
3499 Point::new(1, 2)..Point::new(1, 2),
3500 Point::new(3, 1)..Point::new(3, 2),
3501 ])
3502 });
3503
3504 editor.join_lines(&JoinLines, window, cx);
3505 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3506
3507 assert_eq!(
3508 editor.selections.ranges::<Point>(cx),
3509 [
3510 Point::new(0, 7)..Point::new(0, 7),
3511 Point::new(1, 3)..Point::new(1, 3)
3512 ]
3513 );
3514 editor
3515 });
3516}
3517
3518#[gpui::test]
3519async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3520 init_test(cx, |_| {});
3521
3522 let mut cx = EditorTestContext::new(cx).await;
3523
3524 let diff_base = r#"
3525 Line 0
3526 Line 1
3527 Line 2
3528 Line 3
3529 "#
3530 .unindent();
3531
3532 cx.set_state(
3533 &r#"
3534 ˇLine 0
3535 Line 1
3536 Line 2
3537 Line 3
3538 "#
3539 .unindent(),
3540 );
3541
3542 cx.set_head_text(&diff_base);
3543 executor.run_until_parked();
3544
3545 // Join lines
3546 cx.update_editor(|editor, window, cx| {
3547 editor.join_lines(&JoinLines, window, cx);
3548 });
3549 executor.run_until_parked();
3550
3551 cx.assert_editor_state(
3552 &r#"
3553 Line 0ˇ Line 1
3554 Line 2
3555 Line 3
3556 "#
3557 .unindent(),
3558 );
3559 // Join again
3560 cx.update_editor(|editor, window, cx| {
3561 editor.join_lines(&JoinLines, window, cx);
3562 });
3563 executor.run_until_parked();
3564
3565 cx.assert_editor_state(
3566 &r#"
3567 Line 0 Line 1ˇ Line 2
3568 Line 3
3569 "#
3570 .unindent(),
3571 );
3572}
3573
3574#[gpui::test]
3575async fn test_custom_newlines_cause_no_false_positive_diffs(
3576 executor: BackgroundExecutor,
3577 cx: &mut TestAppContext,
3578) {
3579 init_test(cx, |_| {});
3580 let mut cx = EditorTestContext::new(cx).await;
3581 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3582 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3583 executor.run_until_parked();
3584
3585 cx.update_editor(|editor, window, cx| {
3586 let snapshot = editor.snapshot(window, cx);
3587 assert_eq!(
3588 snapshot
3589 .buffer_snapshot
3590 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3591 .collect::<Vec<_>>(),
3592 Vec::new(),
3593 "Should not have any diffs for files with custom newlines"
3594 );
3595 });
3596}
3597
3598#[gpui::test]
3599async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3600 init_test(cx, |_| {});
3601
3602 let mut cx = EditorTestContext::new(cx).await;
3603
3604 // Test sort_lines_case_insensitive()
3605 cx.set_state(indoc! {"
3606 «z
3607 y
3608 x
3609 Z
3610 Y
3611 Xˇ»
3612 "});
3613 cx.update_editor(|e, window, cx| {
3614 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3615 });
3616 cx.assert_editor_state(indoc! {"
3617 «x
3618 X
3619 y
3620 Y
3621 z
3622 Zˇ»
3623 "});
3624
3625 // Test reverse_lines()
3626 cx.set_state(indoc! {"
3627 «5
3628 4
3629 3
3630 2
3631 1ˇ»
3632 "});
3633 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3634 cx.assert_editor_state(indoc! {"
3635 «1
3636 2
3637 3
3638 4
3639 5ˇ»
3640 "});
3641
3642 // Skip testing shuffle_line()
3643
3644 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3645 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3646
3647 // Don't manipulate when cursor is on single line, but expand the selection
3648 cx.set_state(indoc! {"
3649 ddˇdd
3650 ccc
3651 bb
3652 a
3653 "});
3654 cx.update_editor(|e, window, cx| {
3655 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3656 });
3657 cx.assert_editor_state(indoc! {"
3658 «ddddˇ»
3659 ccc
3660 bb
3661 a
3662 "});
3663
3664 // Basic manipulate case
3665 // Start selection moves to column 0
3666 // End of selection shrinks to fit shorter line
3667 cx.set_state(indoc! {"
3668 dd«d
3669 ccc
3670 bb
3671 aaaaaˇ»
3672 "});
3673 cx.update_editor(|e, window, cx| {
3674 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3675 });
3676 cx.assert_editor_state(indoc! {"
3677 «aaaaa
3678 bb
3679 ccc
3680 dddˇ»
3681 "});
3682
3683 // Manipulate case with newlines
3684 cx.set_state(indoc! {"
3685 dd«d
3686 ccc
3687
3688 bb
3689 aaaaa
3690
3691 ˇ»
3692 "});
3693 cx.update_editor(|e, window, cx| {
3694 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3695 });
3696 cx.assert_editor_state(indoc! {"
3697 «
3698
3699 aaaaa
3700 bb
3701 ccc
3702 dddˇ»
3703
3704 "});
3705
3706 // Adding new line
3707 cx.set_state(indoc! {"
3708 aa«a
3709 bbˇ»b
3710 "});
3711 cx.update_editor(|e, window, cx| {
3712 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3713 });
3714 cx.assert_editor_state(indoc! {"
3715 «aaa
3716 bbb
3717 added_lineˇ»
3718 "});
3719
3720 // Removing line
3721 cx.set_state(indoc! {"
3722 aa«a
3723 bbbˇ»
3724 "});
3725 cx.update_editor(|e, window, cx| {
3726 e.manipulate_lines(window, cx, |lines| {
3727 lines.pop();
3728 })
3729 });
3730 cx.assert_editor_state(indoc! {"
3731 «aaaˇ»
3732 "});
3733
3734 // Removing all lines
3735 cx.set_state(indoc! {"
3736 aa«a
3737 bbbˇ»
3738 "});
3739 cx.update_editor(|e, window, cx| {
3740 e.manipulate_lines(window, cx, |lines| {
3741 lines.drain(..);
3742 })
3743 });
3744 cx.assert_editor_state(indoc! {"
3745 ˇ
3746 "});
3747}
3748
3749#[gpui::test]
3750async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3751 init_test(cx, |_| {});
3752
3753 let mut cx = EditorTestContext::new(cx).await;
3754
3755 // Consider continuous selection as single selection
3756 cx.set_state(indoc! {"
3757 Aaa«aa
3758 cˇ»c«c
3759 bb
3760 aaaˇ»aa
3761 "});
3762 cx.update_editor(|e, window, cx| {
3763 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3764 });
3765 cx.assert_editor_state(indoc! {"
3766 «Aaaaa
3767 ccc
3768 bb
3769 aaaaaˇ»
3770 "});
3771
3772 cx.set_state(indoc! {"
3773 Aaa«aa
3774 cˇ»c«c
3775 bb
3776 aaaˇ»aa
3777 "});
3778 cx.update_editor(|e, window, cx| {
3779 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3780 });
3781 cx.assert_editor_state(indoc! {"
3782 «Aaaaa
3783 ccc
3784 bbˇ»
3785 "});
3786
3787 // Consider non continuous selection as distinct dedup operations
3788 cx.set_state(indoc! {"
3789 «aaaaa
3790 bb
3791 aaaaa
3792 aaaaaˇ»
3793
3794 aaa«aaˇ»
3795 "});
3796 cx.update_editor(|e, window, cx| {
3797 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3798 });
3799 cx.assert_editor_state(indoc! {"
3800 «aaaaa
3801 bbˇ»
3802
3803 «aaaaaˇ»
3804 "});
3805}
3806
3807#[gpui::test]
3808async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3809 init_test(cx, |_| {});
3810
3811 let mut cx = EditorTestContext::new(cx).await;
3812
3813 cx.set_state(indoc! {"
3814 «Aaa
3815 aAa
3816 Aaaˇ»
3817 "});
3818 cx.update_editor(|e, window, cx| {
3819 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3820 });
3821 cx.assert_editor_state(indoc! {"
3822 «Aaa
3823 aAaˇ»
3824 "});
3825
3826 cx.set_state(indoc! {"
3827 «Aaa
3828 aAa
3829 aaAˇ»
3830 "});
3831 cx.update_editor(|e, window, cx| {
3832 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3833 });
3834 cx.assert_editor_state(indoc! {"
3835 «Aaaˇ»
3836 "});
3837}
3838
3839#[gpui::test]
3840async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3841 init_test(cx, |_| {});
3842
3843 let mut cx = EditorTestContext::new(cx).await;
3844
3845 // Manipulate with multiple selections on a single line
3846 cx.set_state(indoc! {"
3847 dd«dd
3848 cˇ»c«c
3849 bb
3850 aaaˇ»aa
3851 "});
3852 cx.update_editor(|e, window, cx| {
3853 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3854 });
3855 cx.assert_editor_state(indoc! {"
3856 «aaaaa
3857 bb
3858 ccc
3859 ddddˇ»
3860 "});
3861
3862 // Manipulate with multiple disjoin selections
3863 cx.set_state(indoc! {"
3864 5«
3865 4
3866 3
3867 2
3868 1ˇ»
3869
3870 dd«dd
3871 ccc
3872 bb
3873 aaaˇ»aa
3874 "});
3875 cx.update_editor(|e, window, cx| {
3876 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3877 });
3878 cx.assert_editor_state(indoc! {"
3879 «1
3880 2
3881 3
3882 4
3883 5ˇ»
3884
3885 «aaaaa
3886 bb
3887 ccc
3888 ddddˇ»
3889 "});
3890
3891 // Adding lines on each selection
3892 cx.set_state(indoc! {"
3893 2«
3894 1ˇ»
3895
3896 bb«bb
3897 aaaˇ»aa
3898 "});
3899 cx.update_editor(|e, window, cx| {
3900 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3901 });
3902 cx.assert_editor_state(indoc! {"
3903 «2
3904 1
3905 added lineˇ»
3906
3907 «bbbb
3908 aaaaa
3909 added lineˇ»
3910 "});
3911
3912 // Removing lines on each selection
3913 cx.set_state(indoc! {"
3914 2«
3915 1ˇ»
3916
3917 bb«bb
3918 aaaˇ»aa
3919 "});
3920 cx.update_editor(|e, window, cx| {
3921 e.manipulate_lines(window, cx, |lines| {
3922 lines.pop();
3923 })
3924 });
3925 cx.assert_editor_state(indoc! {"
3926 «2ˇ»
3927
3928 «bbbbˇ»
3929 "});
3930}
3931
3932#[gpui::test]
3933async fn test_toggle_case(cx: &mut TestAppContext) {
3934 init_test(cx, |_| {});
3935
3936 let mut cx = EditorTestContext::new(cx).await;
3937
3938 // If all lower case -> upper case
3939 cx.set_state(indoc! {"
3940 «hello worldˇ»
3941 "});
3942 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3943 cx.assert_editor_state(indoc! {"
3944 «HELLO WORLDˇ»
3945 "});
3946
3947 // If all upper case -> lower case
3948 cx.set_state(indoc! {"
3949 «HELLO WORLDˇ»
3950 "});
3951 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3952 cx.assert_editor_state(indoc! {"
3953 «hello worldˇ»
3954 "});
3955
3956 // If any upper case characters are identified -> lower case
3957 // This matches JetBrains IDEs
3958 cx.set_state(indoc! {"
3959 «hEllo worldˇ»
3960 "});
3961 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3962 cx.assert_editor_state(indoc! {"
3963 «hello worldˇ»
3964 "});
3965}
3966
3967#[gpui::test]
3968async fn test_manipulate_text(cx: &mut TestAppContext) {
3969 init_test(cx, |_| {});
3970
3971 let mut cx = EditorTestContext::new(cx).await;
3972
3973 // Test convert_to_upper_case()
3974 cx.set_state(indoc! {"
3975 «hello worldˇ»
3976 "});
3977 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3978 cx.assert_editor_state(indoc! {"
3979 «HELLO WORLDˇ»
3980 "});
3981
3982 // Test convert_to_lower_case()
3983 cx.set_state(indoc! {"
3984 «HELLO WORLDˇ»
3985 "});
3986 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 «hello worldˇ»
3989 "});
3990
3991 // Test multiple line, single selection case
3992 cx.set_state(indoc! {"
3993 «The quick brown
3994 fox jumps over
3995 the lazy dogˇ»
3996 "});
3997 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3998 cx.assert_editor_state(indoc! {"
3999 «The Quick Brown
4000 Fox Jumps Over
4001 The Lazy Dogˇ»
4002 "});
4003
4004 // Test multiple line, single selection case
4005 cx.set_state(indoc! {"
4006 «The quick brown
4007 fox jumps over
4008 the lazy dogˇ»
4009 "});
4010 cx.update_editor(|e, window, cx| {
4011 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4012 });
4013 cx.assert_editor_state(indoc! {"
4014 «TheQuickBrown
4015 FoxJumpsOver
4016 TheLazyDogˇ»
4017 "});
4018
4019 // From here on out, test more complex cases of manipulate_text()
4020
4021 // Test no selection case - should affect words cursors are in
4022 // Cursor at beginning, middle, and end of word
4023 cx.set_state(indoc! {"
4024 ˇhello big beauˇtiful worldˇ
4025 "});
4026 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4027 cx.assert_editor_state(indoc! {"
4028 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4029 "});
4030
4031 // Test multiple selections on a single line and across multiple lines
4032 cx.set_state(indoc! {"
4033 «Theˇ» quick «brown
4034 foxˇ» jumps «overˇ»
4035 the «lazyˇ» dog
4036 "});
4037 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 «THEˇ» quick «BROWN
4040 FOXˇ» jumps «OVERˇ»
4041 the «LAZYˇ» dog
4042 "});
4043
4044 // Test case where text length grows
4045 cx.set_state(indoc! {"
4046 «tschüߡ»
4047 "});
4048 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 «TSCHÜSSˇ»
4051 "});
4052
4053 // Test to make sure we don't crash when text shrinks
4054 cx.set_state(indoc! {"
4055 aaa_bbbˇ
4056 "});
4057 cx.update_editor(|e, window, cx| {
4058 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4059 });
4060 cx.assert_editor_state(indoc! {"
4061 «aaaBbbˇ»
4062 "});
4063
4064 // Test to make sure we all aware of the fact that each word can grow and shrink
4065 // Final selections should be aware of this fact
4066 cx.set_state(indoc! {"
4067 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4068 "});
4069 cx.update_editor(|e, window, cx| {
4070 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4071 });
4072 cx.assert_editor_state(indoc! {"
4073 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4074 "});
4075
4076 cx.set_state(indoc! {"
4077 «hElLo, WoRld!ˇ»
4078 "});
4079 cx.update_editor(|e, window, cx| {
4080 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4081 });
4082 cx.assert_editor_state(indoc! {"
4083 «HeLlO, wOrLD!ˇ»
4084 "});
4085}
4086
4087#[gpui::test]
4088fn test_duplicate_line(cx: &mut TestAppContext) {
4089 init_test(cx, |_| {});
4090
4091 let editor = cx.add_window(|window, cx| {
4092 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4093 build_editor(buffer, window, cx)
4094 });
4095 _ = editor.update(cx, |editor, window, cx| {
4096 editor.change_selections(None, window, cx, |s| {
4097 s.select_display_ranges([
4098 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4099 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4100 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4101 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4102 ])
4103 });
4104 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4105 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4106 assert_eq!(
4107 editor.selections.display_ranges(cx),
4108 vec![
4109 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4110 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4111 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4112 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4113 ]
4114 );
4115 });
4116
4117 let editor = cx.add_window(|window, cx| {
4118 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4119 build_editor(buffer, window, cx)
4120 });
4121 _ = editor.update(cx, |editor, window, cx| {
4122 editor.change_selections(None, window, cx, |s| {
4123 s.select_display_ranges([
4124 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4125 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4126 ])
4127 });
4128 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4129 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4130 assert_eq!(
4131 editor.selections.display_ranges(cx),
4132 vec![
4133 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4134 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4135 ]
4136 );
4137 });
4138
4139 // With `move_upwards` the selections stay in place, except for
4140 // the lines inserted above them
4141 let editor = cx.add_window(|window, cx| {
4142 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4143 build_editor(buffer, window, cx)
4144 });
4145 _ = editor.update(cx, |editor, window, cx| {
4146 editor.change_selections(None, window, cx, |s| {
4147 s.select_display_ranges([
4148 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4149 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4150 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4151 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4152 ])
4153 });
4154 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4155 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4156 assert_eq!(
4157 editor.selections.display_ranges(cx),
4158 vec![
4159 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4160 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4161 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4162 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4163 ]
4164 );
4165 });
4166
4167 let editor = cx.add_window(|window, cx| {
4168 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4169 build_editor(buffer, window, cx)
4170 });
4171 _ = editor.update(cx, |editor, window, cx| {
4172 editor.change_selections(None, window, cx, |s| {
4173 s.select_display_ranges([
4174 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4175 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4176 ])
4177 });
4178 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4179 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4180 assert_eq!(
4181 editor.selections.display_ranges(cx),
4182 vec![
4183 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4184 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4185 ]
4186 );
4187 });
4188
4189 let editor = cx.add_window(|window, cx| {
4190 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4191 build_editor(buffer, window, cx)
4192 });
4193 _ = editor.update(cx, |editor, window, cx| {
4194 editor.change_selections(None, window, cx, |s| {
4195 s.select_display_ranges([
4196 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4197 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4198 ])
4199 });
4200 editor.duplicate_selection(&DuplicateSelection, window, cx);
4201 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4202 assert_eq!(
4203 editor.selections.display_ranges(cx),
4204 vec![
4205 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4206 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4207 ]
4208 );
4209 });
4210}
4211
4212#[gpui::test]
4213fn test_move_line_up_down(cx: &mut TestAppContext) {
4214 init_test(cx, |_| {});
4215
4216 let editor = cx.add_window(|window, cx| {
4217 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4218 build_editor(buffer, window, cx)
4219 });
4220 _ = editor.update(cx, |editor, window, cx| {
4221 editor.fold_creases(
4222 vec![
4223 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4224 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4225 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4226 ],
4227 true,
4228 window,
4229 cx,
4230 );
4231 editor.change_selections(None, window, cx, |s| {
4232 s.select_display_ranges([
4233 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4234 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4235 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4236 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4237 ])
4238 });
4239 assert_eq!(
4240 editor.display_text(cx),
4241 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4242 );
4243
4244 editor.move_line_up(&MoveLineUp, window, cx);
4245 assert_eq!(
4246 editor.display_text(cx),
4247 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4248 );
4249 assert_eq!(
4250 editor.selections.display_ranges(cx),
4251 vec![
4252 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4253 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4254 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4255 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4256 ]
4257 );
4258 });
4259
4260 _ = editor.update(cx, |editor, window, cx| {
4261 editor.move_line_down(&MoveLineDown, window, cx);
4262 assert_eq!(
4263 editor.display_text(cx),
4264 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4265 );
4266 assert_eq!(
4267 editor.selections.display_ranges(cx),
4268 vec![
4269 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4270 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4271 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4272 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4273 ]
4274 );
4275 });
4276
4277 _ = editor.update(cx, |editor, window, cx| {
4278 editor.move_line_down(&MoveLineDown, window, cx);
4279 assert_eq!(
4280 editor.display_text(cx),
4281 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4282 );
4283 assert_eq!(
4284 editor.selections.display_ranges(cx),
4285 vec![
4286 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4287 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4288 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4289 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4290 ]
4291 );
4292 });
4293
4294 _ = editor.update(cx, |editor, window, cx| {
4295 editor.move_line_up(&MoveLineUp, window, cx);
4296 assert_eq!(
4297 editor.display_text(cx),
4298 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4299 );
4300 assert_eq!(
4301 editor.selections.display_ranges(cx),
4302 vec![
4303 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4304 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4305 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4306 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4307 ]
4308 );
4309 });
4310}
4311
4312#[gpui::test]
4313fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let editor = cx.add_window(|window, cx| {
4317 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4318 build_editor(buffer, window, cx)
4319 });
4320 _ = editor.update(cx, |editor, window, cx| {
4321 let snapshot = editor.buffer.read(cx).snapshot(cx);
4322 editor.insert_blocks(
4323 [BlockProperties {
4324 style: BlockStyle::Fixed,
4325 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4326 height: Some(1),
4327 render: Arc::new(|_| div().into_any()),
4328 priority: 0,
4329 }],
4330 Some(Autoscroll::fit()),
4331 cx,
4332 );
4333 editor.change_selections(None, window, cx, |s| {
4334 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4335 });
4336 editor.move_line_down(&MoveLineDown, window, cx);
4337 });
4338}
4339
4340#[gpui::test]
4341async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4342 init_test(cx, |_| {});
4343
4344 let mut cx = EditorTestContext::new(cx).await;
4345 cx.set_state(
4346 &"
4347 ˇzero
4348 one
4349 two
4350 three
4351 four
4352 five
4353 "
4354 .unindent(),
4355 );
4356
4357 // Create a four-line block that replaces three lines of text.
4358 cx.update_editor(|editor, window, cx| {
4359 let snapshot = editor.snapshot(window, cx);
4360 let snapshot = &snapshot.buffer_snapshot;
4361 let placement = BlockPlacement::Replace(
4362 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4363 );
4364 editor.insert_blocks(
4365 [BlockProperties {
4366 placement,
4367 height: Some(4),
4368 style: BlockStyle::Sticky,
4369 render: Arc::new(|_| gpui::div().into_any_element()),
4370 priority: 0,
4371 }],
4372 None,
4373 cx,
4374 );
4375 });
4376
4377 // Move down so that the cursor touches the block.
4378 cx.update_editor(|editor, window, cx| {
4379 editor.move_down(&Default::default(), window, cx);
4380 });
4381 cx.assert_editor_state(
4382 &"
4383 zero
4384 «one
4385 two
4386 threeˇ»
4387 four
4388 five
4389 "
4390 .unindent(),
4391 );
4392
4393 // Move down past the block.
4394 cx.update_editor(|editor, window, cx| {
4395 editor.move_down(&Default::default(), window, cx);
4396 });
4397 cx.assert_editor_state(
4398 &"
4399 zero
4400 one
4401 two
4402 three
4403 ˇfour
4404 five
4405 "
4406 .unindent(),
4407 );
4408}
4409
4410#[gpui::test]
4411fn test_transpose(cx: &mut TestAppContext) {
4412 init_test(cx, |_| {});
4413
4414 _ = cx.add_window(|window, cx| {
4415 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4416 editor.set_style(EditorStyle::default(), window, cx);
4417 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4418 editor.transpose(&Default::default(), window, cx);
4419 assert_eq!(editor.text(cx), "bac");
4420 assert_eq!(editor.selections.ranges(cx), [2..2]);
4421
4422 editor.transpose(&Default::default(), window, cx);
4423 assert_eq!(editor.text(cx), "bca");
4424 assert_eq!(editor.selections.ranges(cx), [3..3]);
4425
4426 editor.transpose(&Default::default(), window, cx);
4427 assert_eq!(editor.text(cx), "bac");
4428 assert_eq!(editor.selections.ranges(cx), [3..3]);
4429
4430 editor
4431 });
4432
4433 _ = cx.add_window(|window, cx| {
4434 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4435 editor.set_style(EditorStyle::default(), window, cx);
4436 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4437 editor.transpose(&Default::default(), window, cx);
4438 assert_eq!(editor.text(cx), "acb\nde");
4439 assert_eq!(editor.selections.ranges(cx), [3..3]);
4440
4441 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4442 editor.transpose(&Default::default(), window, cx);
4443 assert_eq!(editor.text(cx), "acbd\ne");
4444 assert_eq!(editor.selections.ranges(cx), [5..5]);
4445
4446 editor.transpose(&Default::default(), window, cx);
4447 assert_eq!(editor.text(cx), "acbde\n");
4448 assert_eq!(editor.selections.ranges(cx), [6..6]);
4449
4450 editor.transpose(&Default::default(), window, cx);
4451 assert_eq!(editor.text(cx), "acbd\ne");
4452 assert_eq!(editor.selections.ranges(cx), [6..6]);
4453
4454 editor
4455 });
4456
4457 _ = cx.add_window(|window, cx| {
4458 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4459 editor.set_style(EditorStyle::default(), window, cx);
4460 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4461 editor.transpose(&Default::default(), window, cx);
4462 assert_eq!(editor.text(cx), "bacd\ne");
4463 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4464
4465 editor.transpose(&Default::default(), window, cx);
4466 assert_eq!(editor.text(cx), "bcade\n");
4467 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4468
4469 editor.transpose(&Default::default(), window, cx);
4470 assert_eq!(editor.text(cx), "bcda\ne");
4471 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4472
4473 editor.transpose(&Default::default(), window, cx);
4474 assert_eq!(editor.text(cx), "bcade\n");
4475 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4476
4477 editor.transpose(&Default::default(), window, cx);
4478 assert_eq!(editor.text(cx), "bcaed\n");
4479 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4480
4481 editor
4482 });
4483
4484 _ = cx.add_window(|window, cx| {
4485 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4486 editor.set_style(EditorStyle::default(), window, cx);
4487 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4488 editor.transpose(&Default::default(), window, cx);
4489 assert_eq!(editor.text(cx), "🏀🍐✋");
4490 assert_eq!(editor.selections.ranges(cx), [8..8]);
4491
4492 editor.transpose(&Default::default(), window, cx);
4493 assert_eq!(editor.text(cx), "🏀✋🍐");
4494 assert_eq!(editor.selections.ranges(cx), [11..11]);
4495
4496 editor.transpose(&Default::default(), window, cx);
4497 assert_eq!(editor.text(cx), "🏀🍐✋");
4498 assert_eq!(editor.selections.ranges(cx), [11..11]);
4499
4500 editor
4501 });
4502}
4503
4504#[gpui::test]
4505async fn test_rewrap(cx: &mut TestAppContext) {
4506 init_test(cx, |settings| {
4507 settings.languages.extend([
4508 (
4509 "Markdown".into(),
4510 LanguageSettingsContent {
4511 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4512 ..Default::default()
4513 },
4514 ),
4515 (
4516 "Plain Text".into(),
4517 LanguageSettingsContent {
4518 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4519 ..Default::default()
4520 },
4521 ),
4522 ])
4523 });
4524
4525 let mut cx = EditorTestContext::new(cx).await;
4526
4527 let language_with_c_comments = Arc::new(Language::new(
4528 LanguageConfig {
4529 line_comments: vec!["// ".into()],
4530 ..LanguageConfig::default()
4531 },
4532 None,
4533 ));
4534 let language_with_pound_comments = Arc::new(Language::new(
4535 LanguageConfig {
4536 line_comments: vec!["# ".into()],
4537 ..LanguageConfig::default()
4538 },
4539 None,
4540 ));
4541 let markdown_language = Arc::new(Language::new(
4542 LanguageConfig {
4543 name: "Markdown".into(),
4544 ..LanguageConfig::default()
4545 },
4546 None,
4547 ));
4548 let language_with_doc_comments = Arc::new(Language::new(
4549 LanguageConfig {
4550 line_comments: vec!["// ".into(), "/// ".into()],
4551 ..LanguageConfig::default()
4552 },
4553 Some(tree_sitter_rust::LANGUAGE.into()),
4554 ));
4555
4556 let plaintext_language = Arc::new(Language::new(
4557 LanguageConfig {
4558 name: "Plain Text".into(),
4559 ..LanguageConfig::default()
4560 },
4561 None,
4562 ));
4563
4564 assert_rewrap(
4565 indoc! {"
4566 // ˇ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.
4567 "},
4568 indoc! {"
4569 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4570 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4571 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4572 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4573 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4574 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4575 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4576 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4577 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4578 // porttitor id. Aliquam id accumsan eros.
4579 "},
4580 language_with_c_comments.clone(),
4581 &mut cx,
4582 );
4583
4584 // Test that rewrapping works inside of a selection
4585 assert_rewrap(
4586 indoc! {"
4587 «// 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.ˇ»
4588 "},
4589 indoc! {"
4590 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4591 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4592 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4593 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4594 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4595 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4596 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4597 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4598 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4599 // porttitor id. Aliquam id accumsan eros.ˇ»
4600 "},
4601 language_with_c_comments.clone(),
4602 &mut cx,
4603 );
4604
4605 // Test that cursors that expand to the same region are collapsed.
4606 assert_rewrap(
4607 indoc! {"
4608 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4609 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4610 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4611 // ˇ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.
4612 "},
4613 indoc! {"
4614 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4615 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4616 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4617 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4618 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4619 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4620 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4621 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4622 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4623 // porttitor id. Aliquam id accumsan eros.
4624 "},
4625 language_with_c_comments.clone(),
4626 &mut cx,
4627 );
4628
4629 // Test that non-contiguous selections are treated separately.
4630 assert_rewrap(
4631 indoc! {"
4632 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4633 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4634 //
4635 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4636 // ˇ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.
4637 "},
4638 indoc! {"
4639 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4640 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4641 // auctor, eu lacinia sapien scelerisque.
4642 //
4643 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4644 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4645 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4646 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4647 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4648 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4649 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4650 "},
4651 language_with_c_comments.clone(),
4652 &mut cx,
4653 );
4654
4655 // Test that different comment prefixes are supported.
4656 assert_rewrap(
4657 indoc! {"
4658 # ˇ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.
4659 "},
4660 indoc! {"
4661 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4662 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4663 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4664 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4665 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4666 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4667 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4668 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4669 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4670 # accumsan eros.
4671 "},
4672 language_with_pound_comments.clone(),
4673 &mut cx,
4674 );
4675
4676 // Test that rewrapping is ignored outside of comments in most languages.
4677 assert_rewrap(
4678 indoc! {"
4679 /// Adds two numbers.
4680 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4681 fn add(a: u32, b: u32) -> u32 {
4682 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ˇ
4683 }
4684 "},
4685 indoc! {"
4686 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4687 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4688 fn add(a: u32, b: u32) -> u32 {
4689 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ˇ
4690 }
4691 "},
4692 language_with_doc_comments.clone(),
4693 &mut cx,
4694 );
4695
4696 // Test that rewrapping works in Markdown and Plain Text languages.
4697 assert_rewrap(
4698 indoc! {"
4699 # Hello
4700
4701 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.
4702 "},
4703 indoc! {"
4704 # Hello
4705
4706 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4707 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4708 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4709 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4710 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4711 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4712 Integer sit amet scelerisque nisi.
4713 "},
4714 markdown_language,
4715 &mut cx,
4716 );
4717
4718 assert_rewrap(
4719 indoc! {"
4720 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.
4721 "},
4722 indoc! {"
4723 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4724 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4725 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4726 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4727 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4728 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4729 Integer sit amet scelerisque nisi.
4730 "},
4731 plaintext_language,
4732 &mut cx,
4733 );
4734
4735 // Test rewrapping unaligned comments in a selection.
4736 assert_rewrap(
4737 indoc! {"
4738 fn foo() {
4739 if true {
4740 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4741 // Praesent semper egestas tellus id dignissim.ˇ»
4742 do_something();
4743 } else {
4744 //
4745 }
4746 }
4747 "},
4748 indoc! {"
4749 fn foo() {
4750 if true {
4751 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4752 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4753 // egestas tellus id dignissim.ˇ»
4754 do_something();
4755 } else {
4756 //
4757 }
4758 }
4759 "},
4760 language_with_doc_comments.clone(),
4761 &mut cx,
4762 );
4763
4764 assert_rewrap(
4765 indoc! {"
4766 fn foo() {
4767 if true {
4768 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4769 // Praesent semper egestas tellus id dignissim.»
4770 do_something();
4771 } else {
4772 //
4773 }
4774
4775 }
4776 "},
4777 indoc! {"
4778 fn foo() {
4779 if true {
4780 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4781 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4782 // egestas tellus id dignissim.»
4783 do_something();
4784 } else {
4785 //
4786 }
4787
4788 }
4789 "},
4790 language_with_doc_comments.clone(),
4791 &mut cx,
4792 );
4793
4794 #[track_caller]
4795 fn assert_rewrap(
4796 unwrapped_text: &str,
4797 wrapped_text: &str,
4798 language: Arc<Language>,
4799 cx: &mut EditorTestContext,
4800 ) {
4801 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4802 cx.set_state(unwrapped_text);
4803 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4804 cx.assert_editor_state(wrapped_text);
4805 }
4806}
4807
4808#[gpui::test]
4809async fn test_hard_wrap(cx: &mut TestAppContext) {
4810 init_test(cx, |_| {});
4811 let mut cx = EditorTestContext::new(cx).await;
4812
4813 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4814 cx.update_editor(|editor, _, cx| {
4815 editor.set_hard_wrap(Some(14), cx);
4816 });
4817
4818 cx.set_state(indoc!(
4819 "
4820 one two three ˇ
4821 "
4822 ));
4823 cx.simulate_input("four");
4824 cx.run_until_parked();
4825
4826 cx.assert_editor_state(indoc!(
4827 "
4828 one two three
4829 fourˇ
4830 "
4831 ));
4832
4833 cx.update_editor(|editor, window, cx| {
4834 editor.newline(&Default::default(), window, cx);
4835 });
4836 cx.run_until_parked();
4837 cx.assert_editor_state(indoc!(
4838 "
4839 one two three
4840 four
4841 ˇ
4842 "
4843 ));
4844
4845 cx.simulate_input("five");
4846 cx.run_until_parked();
4847 cx.assert_editor_state(indoc!(
4848 "
4849 one two three
4850 four
4851 fiveˇ
4852 "
4853 ));
4854
4855 cx.update_editor(|editor, window, cx| {
4856 editor.newline(&Default::default(), window, cx);
4857 });
4858 cx.run_until_parked();
4859 cx.simulate_input("# ");
4860 cx.run_until_parked();
4861 cx.assert_editor_state(indoc!(
4862 "
4863 one two three
4864 four
4865 five
4866 # ˇ
4867 "
4868 ));
4869
4870 cx.update_editor(|editor, window, cx| {
4871 editor.newline(&Default::default(), window, cx);
4872 });
4873 cx.run_until_parked();
4874 cx.assert_editor_state(indoc!(
4875 "
4876 one two three
4877 four
4878 five
4879 #\x20
4880 #ˇ
4881 "
4882 ));
4883
4884 cx.simulate_input(" 6");
4885 cx.run_until_parked();
4886 cx.assert_editor_state(indoc!(
4887 "
4888 one two three
4889 four
4890 five
4891 #
4892 # 6ˇ
4893 "
4894 ));
4895}
4896
4897#[gpui::test]
4898async fn test_clipboard(cx: &mut TestAppContext) {
4899 init_test(cx, |_| {});
4900
4901 let mut cx = EditorTestContext::new(cx).await;
4902
4903 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4904 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4905 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4906
4907 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4908 cx.set_state("two ˇfour ˇsix ˇ");
4909 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4910 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4911
4912 // Paste again but with only two cursors. Since the number of cursors doesn't
4913 // match the number of slices in the clipboard, the entire clipboard text
4914 // is pasted at each cursor.
4915 cx.set_state("ˇtwo one✅ four three six five ˇ");
4916 cx.update_editor(|e, window, cx| {
4917 e.handle_input("( ", window, cx);
4918 e.paste(&Paste, window, cx);
4919 e.handle_input(") ", window, cx);
4920 });
4921 cx.assert_editor_state(
4922 &([
4923 "( one✅ ",
4924 "three ",
4925 "five ) ˇtwo one✅ four three six five ( one✅ ",
4926 "three ",
4927 "five ) ˇ",
4928 ]
4929 .join("\n")),
4930 );
4931
4932 // Cut with three selections, one of which is full-line.
4933 cx.set_state(indoc! {"
4934 1«2ˇ»3
4935 4ˇ567
4936 «8ˇ»9"});
4937 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4938 cx.assert_editor_state(indoc! {"
4939 1ˇ3
4940 ˇ9"});
4941
4942 // Paste with three selections, noticing how the copied selection that was full-line
4943 // gets inserted before the second cursor.
4944 cx.set_state(indoc! {"
4945 1ˇ3
4946 9ˇ
4947 «oˇ»ne"});
4948 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4949 cx.assert_editor_state(indoc! {"
4950 12ˇ3
4951 4567
4952 9ˇ
4953 8ˇne"});
4954
4955 // Copy with a single cursor only, which writes the whole line into the clipboard.
4956 cx.set_state(indoc! {"
4957 The quick brown
4958 fox juˇmps over
4959 the lazy dog"});
4960 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4961 assert_eq!(
4962 cx.read_from_clipboard()
4963 .and_then(|item| item.text().as_deref().map(str::to_string)),
4964 Some("fox jumps over\n".to_string())
4965 );
4966
4967 // Paste with three selections, noticing how the copied full-line selection is inserted
4968 // before the empty selections but replaces the selection that is non-empty.
4969 cx.set_state(indoc! {"
4970 Tˇhe quick brown
4971 «foˇ»x jumps over
4972 tˇhe lazy dog"});
4973 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4974 cx.assert_editor_state(indoc! {"
4975 fox jumps over
4976 Tˇhe quick brown
4977 fox jumps over
4978 ˇx jumps over
4979 fox jumps over
4980 tˇhe lazy dog"});
4981}
4982
4983#[gpui::test]
4984async fn test_copy_trim(cx: &mut TestAppContext) {
4985 init_test(cx, |_| {});
4986
4987 let mut cx = EditorTestContext::new(cx).await;
4988 cx.set_state(
4989 r#" «for selection in selections.iter() {
4990 let mut start = selection.start;
4991 let mut end = selection.end;
4992 let is_entire_line = selection.is_empty();
4993 if is_entire_line {
4994 start = Point::new(start.row, 0);ˇ»
4995 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4996 }
4997 "#,
4998 );
4999 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5000 assert_eq!(
5001 cx.read_from_clipboard()
5002 .and_then(|item| item.text().as_deref().map(str::to_string)),
5003 Some(
5004 "for selection in selections.iter() {
5005 let mut start = selection.start;
5006 let mut end = selection.end;
5007 let is_entire_line = selection.is_empty();
5008 if is_entire_line {
5009 start = Point::new(start.row, 0);"
5010 .to_string()
5011 ),
5012 "Regular copying preserves all indentation selected",
5013 );
5014 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5015 assert_eq!(
5016 cx.read_from_clipboard()
5017 .and_then(|item| item.text().as_deref().map(str::to_string)),
5018 Some(
5019 "for selection in selections.iter() {
5020let mut start = selection.start;
5021let mut end = selection.end;
5022let is_entire_line = selection.is_empty();
5023if is_entire_line {
5024 start = Point::new(start.row, 0);"
5025 .to_string()
5026 ),
5027 "Copying with stripping should strip all leading whitespaces"
5028 );
5029
5030 cx.set_state(
5031 r#" « for selection in selections.iter() {
5032 let mut start = selection.start;
5033 let mut end = selection.end;
5034 let is_entire_line = selection.is_empty();
5035 if is_entire_line {
5036 start = Point::new(start.row, 0);ˇ»
5037 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5038 }
5039 "#,
5040 );
5041 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5042 assert_eq!(
5043 cx.read_from_clipboard()
5044 .and_then(|item| item.text().as_deref().map(str::to_string)),
5045 Some(
5046 " for selection in selections.iter() {
5047 let mut start = selection.start;
5048 let mut end = selection.end;
5049 let is_entire_line = selection.is_empty();
5050 if is_entire_line {
5051 start = Point::new(start.row, 0);"
5052 .to_string()
5053 ),
5054 "Regular copying preserves all indentation selected",
5055 );
5056 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5057 assert_eq!(
5058 cx.read_from_clipboard()
5059 .and_then(|item| item.text().as_deref().map(str::to_string)),
5060 Some(
5061 "for selection in selections.iter() {
5062let mut start = selection.start;
5063let mut end = selection.end;
5064let is_entire_line = selection.is_empty();
5065if is_entire_line {
5066 start = Point::new(start.row, 0);"
5067 .to_string()
5068 ),
5069 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5070 );
5071
5072 cx.set_state(
5073 r#" «ˇ for selection in selections.iter() {
5074 let mut start = selection.start;
5075 let mut end = selection.end;
5076 let is_entire_line = selection.is_empty();
5077 if is_entire_line {
5078 start = Point::new(start.row, 0);»
5079 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5080 }
5081 "#,
5082 );
5083 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5084 assert_eq!(
5085 cx.read_from_clipboard()
5086 .and_then(|item| item.text().as_deref().map(str::to_string)),
5087 Some(
5088 " for selection in selections.iter() {
5089 let mut start = selection.start;
5090 let mut end = selection.end;
5091 let is_entire_line = selection.is_empty();
5092 if is_entire_line {
5093 start = Point::new(start.row, 0);"
5094 .to_string()
5095 ),
5096 "Regular copying for reverse selection works the same",
5097 );
5098 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5099 assert_eq!(
5100 cx.read_from_clipboard()
5101 .and_then(|item| item.text().as_deref().map(str::to_string)),
5102 Some(
5103 "for selection in selections.iter() {
5104let mut start = selection.start;
5105let mut end = selection.end;
5106let is_entire_line = selection.is_empty();
5107if is_entire_line {
5108 start = Point::new(start.row, 0);"
5109 .to_string()
5110 ),
5111 "Copying with stripping for reverse selection works the same"
5112 );
5113
5114 cx.set_state(
5115 r#" for selection «in selections.iter() {
5116 let mut start = selection.start;
5117 let mut end = selection.end;
5118 let is_entire_line = selection.is_empty();
5119 if is_entire_line {
5120 start = Point::new(start.row, 0);ˇ»
5121 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5122 }
5123 "#,
5124 );
5125 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5126 assert_eq!(
5127 cx.read_from_clipboard()
5128 .and_then(|item| item.text().as_deref().map(str::to_string)),
5129 Some(
5130 "in selections.iter() {
5131 let mut start = selection.start;
5132 let mut end = selection.end;
5133 let is_entire_line = selection.is_empty();
5134 if is_entire_line {
5135 start = Point::new(start.row, 0);"
5136 .to_string()
5137 ),
5138 "When selecting past the indent, the copying works as usual",
5139 );
5140 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5141 assert_eq!(
5142 cx.read_from_clipboard()
5143 .and_then(|item| item.text().as_deref().map(str::to_string)),
5144 Some(
5145 "in selections.iter() {
5146 let mut start = selection.start;
5147 let mut end = selection.end;
5148 let is_entire_line = selection.is_empty();
5149 if is_entire_line {
5150 start = Point::new(start.row, 0);"
5151 .to_string()
5152 ),
5153 "When selecting past the indent, nothing is trimmed"
5154 );
5155
5156 cx.set_state(
5157 r#" «for selection in selections.iter() {
5158 let mut start = selection.start;
5159
5160 let mut end = selection.end;
5161 let is_entire_line = selection.is_empty();
5162 if is_entire_line {
5163 start = Point::new(start.row, 0);
5164ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5165 }
5166 "#,
5167 );
5168 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5169 assert_eq!(
5170 cx.read_from_clipboard()
5171 .and_then(|item| item.text().as_deref().map(str::to_string)),
5172 Some(
5173 "for selection in selections.iter() {
5174let mut start = selection.start;
5175
5176let mut end = selection.end;
5177let is_entire_line = selection.is_empty();
5178if is_entire_line {
5179 start = Point::new(start.row, 0);
5180"
5181 .to_string()
5182 ),
5183 "Copying with stripping should ignore empty lines"
5184 );
5185}
5186
5187#[gpui::test]
5188async fn test_paste_multiline(cx: &mut TestAppContext) {
5189 init_test(cx, |_| {});
5190
5191 let mut cx = EditorTestContext::new(cx).await;
5192 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5193
5194 // Cut an indented block, without the leading whitespace.
5195 cx.set_state(indoc! {"
5196 const a: B = (
5197 c(),
5198 «d(
5199 e,
5200 f
5201 )ˇ»
5202 );
5203 "});
5204 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5205 cx.assert_editor_state(indoc! {"
5206 const a: B = (
5207 c(),
5208 ˇ
5209 );
5210 "});
5211
5212 // Paste it at the same position.
5213 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5214 cx.assert_editor_state(indoc! {"
5215 const a: B = (
5216 c(),
5217 d(
5218 e,
5219 f
5220 )ˇ
5221 );
5222 "});
5223
5224 // Paste it at a line with a lower indent level.
5225 cx.set_state(indoc! {"
5226 ˇ
5227 const a: B = (
5228 c(),
5229 );
5230 "});
5231 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5232 cx.assert_editor_state(indoc! {"
5233 d(
5234 e,
5235 f
5236 )ˇ
5237 const a: B = (
5238 c(),
5239 );
5240 "});
5241
5242 // Cut an indented block, with the leading whitespace.
5243 cx.set_state(indoc! {"
5244 const a: B = (
5245 c(),
5246 « d(
5247 e,
5248 f
5249 )
5250 ˇ»);
5251 "});
5252 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5253 cx.assert_editor_state(indoc! {"
5254 const a: B = (
5255 c(),
5256 ˇ);
5257 "});
5258
5259 // Paste it at the same position.
5260 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5261 cx.assert_editor_state(indoc! {"
5262 const a: B = (
5263 c(),
5264 d(
5265 e,
5266 f
5267 )
5268 ˇ);
5269 "});
5270
5271 // Paste it at a line with a higher indent level.
5272 cx.set_state(indoc! {"
5273 const a: B = (
5274 c(),
5275 d(
5276 e,
5277 fˇ
5278 )
5279 );
5280 "});
5281 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5282 cx.assert_editor_state(indoc! {"
5283 const a: B = (
5284 c(),
5285 d(
5286 e,
5287 f d(
5288 e,
5289 f
5290 )
5291 ˇ
5292 )
5293 );
5294 "});
5295
5296 // Copy an indented block, starting mid-line
5297 cx.set_state(indoc! {"
5298 const a: B = (
5299 c(),
5300 somethin«g(
5301 e,
5302 f
5303 )ˇ»
5304 );
5305 "});
5306 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5307
5308 // Paste it on a line with a lower indent level
5309 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5310 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5311 cx.assert_editor_state(indoc! {"
5312 const a: B = (
5313 c(),
5314 something(
5315 e,
5316 f
5317 )
5318 );
5319 g(
5320 e,
5321 f
5322 )ˇ"});
5323}
5324
5325#[gpui::test]
5326async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5327 init_test(cx, |_| {});
5328
5329 cx.write_to_clipboard(ClipboardItem::new_string(
5330 " d(\n e\n );\n".into(),
5331 ));
5332
5333 let mut cx = EditorTestContext::new(cx).await;
5334 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5335
5336 cx.set_state(indoc! {"
5337 fn a() {
5338 b();
5339 if c() {
5340 ˇ
5341 }
5342 }
5343 "});
5344
5345 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5346 cx.assert_editor_state(indoc! {"
5347 fn a() {
5348 b();
5349 if c() {
5350 d(
5351 e
5352 );
5353 ˇ
5354 }
5355 }
5356 "});
5357
5358 cx.set_state(indoc! {"
5359 fn a() {
5360 b();
5361 ˇ
5362 }
5363 "});
5364
5365 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5366 cx.assert_editor_state(indoc! {"
5367 fn a() {
5368 b();
5369 d(
5370 e
5371 );
5372 ˇ
5373 }
5374 "});
5375}
5376
5377#[gpui::test]
5378fn test_select_all(cx: &mut TestAppContext) {
5379 init_test(cx, |_| {});
5380
5381 let editor = cx.add_window(|window, cx| {
5382 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5383 build_editor(buffer, window, cx)
5384 });
5385 _ = editor.update(cx, |editor, window, cx| {
5386 editor.select_all(&SelectAll, window, cx);
5387 assert_eq!(
5388 editor.selections.display_ranges(cx),
5389 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5390 );
5391 });
5392}
5393
5394#[gpui::test]
5395fn test_select_line(cx: &mut TestAppContext) {
5396 init_test(cx, |_| {});
5397
5398 let editor = cx.add_window(|window, cx| {
5399 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5400 build_editor(buffer, window, cx)
5401 });
5402 _ = editor.update(cx, |editor, window, cx| {
5403 editor.change_selections(None, window, cx, |s| {
5404 s.select_display_ranges([
5405 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5406 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5407 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5408 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5409 ])
5410 });
5411 editor.select_line(&SelectLine, window, cx);
5412 assert_eq!(
5413 editor.selections.display_ranges(cx),
5414 vec![
5415 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5416 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5417 ]
5418 );
5419 });
5420
5421 _ = editor.update(cx, |editor, window, cx| {
5422 editor.select_line(&SelectLine, window, cx);
5423 assert_eq!(
5424 editor.selections.display_ranges(cx),
5425 vec![
5426 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5427 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5428 ]
5429 );
5430 });
5431
5432 _ = editor.update(cx, |editor, window, cx| {
5433 editor.select_line(&SelectLine, window, cx);
5434 assert_eq!(
5435 editor.selections.display_ranges(cx),
5436 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5437 );
5438 });
5439}
5440
5441#[gpui::test]
5442async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5443 init_test(cx, |_| {});
5444 let mut cx = EditorTestContext::new(cx).await;
5445
5446 #[track_caller]
5447 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5448 cx.set_state(initial_state);
5449 cx.update_editor(|e, window, cx| {
5450 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5451 });
5452 cx.assert_editor_state(expected_state);
5453 }
5454
5455 // Selection starts and ends at the middle of lines, left-to-right
5456 test(
5457 &mut cx,
5458 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5459 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5460 );
5461 // Same thing, right-to-left
5462 test(
5463 &mut cx,
5464 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5465 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5466 );
5467
5468 // Whole buffer, left-to-right, last line *doesn't* end with newline
5469 test(
5470 &mut cx,
5471 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5472 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5473 );
5474 // Same thing, right-to-left
5475 test(
5476 &mut cx,
5477 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5478 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5479 );
5480
5481 // Whole buffer, left-to-right, last line ends with newline
5482 test(
5483 &mut cx,
5484 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5485 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5486 );
5487 // Same thing, right-to-left
5488 test(
5489 &mut cx,
5490 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5491 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5492 );
5493
5494 // Starts at the end of a line, ends at the start of another
5495 test(
5496 &mut cx,
5497 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5498 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5499 );
5500}
5501
5502#[gpui::test]
5503async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5504 init_test(cx, |_| {});
5505
5506 let editor = cx.add_window(|window, cx| {
5507 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5508 build_editor(buffer, window, cx)
5509 });
5510
5511 // setup
5512 _ = editor.update(cx, |editor, window, cx| {
5513 editor.fold_creases(
5514 vec![
5515 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5516 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5517 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5518 ],
5519 true,
5520 window,
5521 cx,
5522 );
5523 assert_eq!(
5524 editor.display_text(cx),
5525 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5526 );
5527 });
5528
5529 _ = editor.update(cx, |editor, window, cx| {
5530 editor.change_selections(None, window, cx, |s| {
5531 s.select_display_ranges([
5532 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5533 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5534 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5535 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5536 ])
5537 });
5538 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5539 assert_eq!(
5540 editor.display_text(cx),
5541 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5542 );
5543 });
5544 EditorTestContext::for_editor(editor, cx)
5545 .await
5546 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5547
5548 _ = editor.update(cx, |editor, window, cx| {
5549 editor.change_selections(None, window, cx, |s| {
5550 s.select_display_ranges([
5551 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5552 ])
5553 });
5554 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5555 assert_eq!(
5556 editor.display_text(cx),
5557 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5558 );
5559 assert_eq!(
5560 editor.selections.display_ranges(cx),
5561 [
5562 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5563 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5564 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5565 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5566 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5567 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5568 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5569 ]
5570 );
5571 });
5572 EditorTestContext::for_editor(editor, cx)
5573 .await
5574 .assert_editor_state(
5575 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5576 );
5577}
5578
5579#[gpui::test]
5580async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5581 init_test(cx, |_| {});
5582
5583 let mut cx = EditorTestContext::new(cx).await;
5584
5585 cx.set_state(indoc!(
5586 r#"abc
5587 defˇghi
5588
5589 jk
5590 nlmo
5591 "#
5592 ));
5593
5594 cx.update_editor(|editor, window, cx| {
5595 editor.add_selection_above(&Default::default(), window, cx);
5596 });
5597
5598 cx.assert_editor_state(indoc!(
5599 r#"abcˇ
5600 defˇghi
5601
5602 jk
5603 nlmo
5604 "#
5605 ));
5606
5607 cx.update_editor(|editor, window, cx| {
5608 editor.add_selection_above(&Default::default(), window, cx);
5609 });
5610
5611 cx.assert_editor_state(indoc!(
5612 r#"abcˇ
5613 defˇghi
5614
5615 jk
5616 nlmo
5617 "#
5618 ));
5619
5620 cx.update_editor(|editor, window, cx| {
5621 editor.add_selection_below(&Default::default(), window, cx);
5622 });
5623
5624 cx.assert_editor_state(indoc!(
5625 r#"abc
5626 defˇghi
5627
5628 jk
5629 nlmo
5630 "#
5631 ));
5632
5633 cx.update_editor(|editor, window, cx| {
5634 editor.undo_selection(&Default::default(), window, cx);
5635 });
5636
5637 cx.assert_editor_state(indoc!(
5638 r#"abcˇ
5639 defˇghi
5640
5641 jk
5642 nlmo
5643 "#
5644 ));
5645
5646 cx.update_editor(|editor, window, cx| {
5647 editor.redo_selection(&Default::default(), window, cx);
5648 });
5649
5650 cx.assert_editor_state(indoc!(
5651 r#"abc
5652 defˇghi
5653
5654 jk
5655 nlmo
5656 "#
5657 ));
5658
5659 cx.update_editor(|editor, window, cx| {
5660 editor.add_selection_below(&Default::default(), window, cx);
5661 });
5662
5663 cx.assert_editor_state(indoc!(
5664 r#"abc
5665 defˇghi
5666
5667 jk
5668 nlmˇo
5669 "#
5670 ));
5671
5672 cx.update_editor(|editor, window, cx| {
5673 editor.add_selection_below(&Default::default(), window, cx);
5674 });
5675
5676 cx.assert_editor_state(indoc!(
5677 r#"abc
5678 defˇghi
5679
5680 jk
5681 nlmˇo
5682 "#
5683 ));
5684
5685 // change selections
5686 cx.set_state(indoc!(
5687 r#"abc
5688 def«ˇg»hi
5689
5690 jk
5691 nlmo
5692 "#
5693 ));
5694
5695 cx.update_editor(|editor, window, cx| {
5696 editor.add_selection_below(&Default::default(), window, cx);
5697 });
5698
5699 cx.assert_editor_state(indoc!(
5700 r#"abc
5701 def«ˇg»hi
5702
5703 jk
5704 nlm«ˇo»
5705 "#
5706 ));
5707
5708 cx.update_editor(|editor, window, cx| {
5709 editor.add_selection_below(&Default::default(), window, cx);
5710 });
5711
5712 cx.assert_editor_state(indoc!(
5713 r#"abc
5714 def«ˇg»hi
5715
5716 jk
5717 nlm«ˇo»
5718 "#
5719 ));
5720
5721 cx.update_editor(|editor, window, cx| {
5722 editor.add_selection_above(&Default::default(), window, cx);
5723 });
5724
5725 cx.assert_editor_state(indoc!(
5726 r#"abc
5727 def«ˇg»hi
5728
5729 jk
5730 nlmo
5731 "#
5732 ));
5733
5734 cx.update_editor(|editor, window, cx| {
5735 editor.add_selection_above(&Default::default(), window, cx);
5736 });
5737
5738 cx.assert_editor_state(indoc!(
5739 r#"abc
5740 def«ˇg»hi
5741
5742 jk
5743 nlmo
5744 "#
5745 ));
5746
5747 // Change selections again
5748 cx.set_state(indoc!(
5749 r#"a«bc
5750 defgˇ»hi
5751
5752 jk
5753 nlmo
5754 "#
5755 ));
5756
5757 cx.update_editor(|editor, window, cx| {
5758 editor.add_selection_below(&Default::default(), window, cx);
5759 });
5760
5761 cx.assert_editor_state(indoc!(
5762 r#"a«bcˇ»
5763 d«efgˇ»hi
5764
5765 j«kˇ»
5766 nlmo
5767 "#
5768 ));
5769
5770 cx.update_editor(|editor, window, cx| {
5771 editor.add_selection_below(&Default::default(), window, cx);
5772 });
5773 cx.assert_editor_state(indoc!(
5774 r#"a«bcˇ»
5775 d«efgˇ»hi
5776
5777 j«kˇ»
5778 n«lmoˇ»
5779 "#
5780 ));
5781 cx.update_editor(|editor, window, cx| {
5782 editor.add_selection_above(&Default::default(), window, cx);
5783 });
5784
5785 cx.assert_editor_state(indoc!(
5786 r#"a«bcˇ»
5787 d«efgˇ»hi
5788
5789 j«kˇ»
5790 nlmo
5791 "#
5792 ));
5793
5794 // Change selections again
5795 cx.set_state(indoc!(
5796 r#"abc
5797 d«ˇefghi
5798
5799 jk
5800 nlm»o
5801 "#
5802 ));
5803
5804 cx.update_editor(|editor, window, cx| {
5805 editor.add_selection_above(&Default::default(), window, cx);
5806 });
5807
5808 cx.assert_editor_state(indoc!(
5809 r#"a«ˇbc»
5810 d«ˇef»ghi
5811
5812 j«ˇk»
5813 n«ˇlm»o
5814 "#
5815 ));
5816
5817 cx.update_editor(|editor, window, cx| {
5818 editor.add_selection_below(&Default::default(), window, cx);
5819 });
5820
5821 cx.assert_editor_state(indoc!(
5822 r#"abc
5823 d«ˇef»ghi
5824
5825 j«ˇk»
5826 n«ˇlm»o
5827 "#
5828 ));
5829}
5830
5831#[gpui::test]
5832async fn test_select_next(cx: &mut TestAppContext) {
5833 init_test(cx, |_| {});
5834
5835 let mut cx = EditorTestContext::new(cx).await;
5836 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5837
5838 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5839 .unwrap();
5840 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5841
5842 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5843 .unwrap();
5844 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5845
5846 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5847 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5848
5849 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5850 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5851
5852 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5853 .unwrap();
5854 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5855
5856 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5857 .unwrap();
5858 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5859
5860 // Test selection direction should be preserved
5861 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5862
5863 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5864 .unwrap();
5865 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
5866}
5867
5868#[gpui::test]
5869async fn test_select_all_matches(cx: &mut TestAppContext) {
5870 init_test(cx, |_| {});
5871
5872 let mut cx = EditorTestContext::new(cx).await;
5873
5874 // Test caret-only selections
5875 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5876 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5877 .unwrap();
5878 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5879
5880 // Test left-to-right selections
5881 cx.set_state("abc\n«abcˇ»\nabc");
5882 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5883 .unwrap();
5884 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5885
5886 // Test right-to-left selections
5887 cx.set_state("abc\n«ˇabc»\nabc");
5888 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5889 .unwrap();
5890 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5891
5892 // Test selecting whitespace with caret selection
5893 cx.set_state("abc\nˇ abc\nabc");
5894 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5895 .unwrap();
5896 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5897
5898 // Test selecting whitespace with left-to-right selection
5899 cx.set_state("abc\n«ˇ »abc\nabc");
5900 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5901 .unwrap();
5902 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5903
5904 // Test no matches with right-to-left selection
5905 cx.set_state("abc\n« ˇ»abc\nabc");
5906 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5907 .unwrap();
5908 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5909}
5910
5911#[gpui::test]
5912async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
5913 init_test(cx, |_| {});
5914
5915 let mut cx = EditorTestContext::new(cx).await;
5916
5917 let large_body_1 = "\nd".repeat(200);
5918 let large_body_2 = "\ne".repeat(200);
5919
5920 cx.set_state(&format!(
5921 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
5922 ));
5923 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
5924 let scroll_position = editor.scroll_position(cx);
5925 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
5926 scroll_position
5927 });
5928
5929 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5930 .unwrap();
5931 cx.assert_editor_state(&format!(
5932 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
5933 ));
5934 let scroll_position_after_selection =
5935 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
5936 assert_eq!(
5937 initial_scroll_position, scroll_position_after_selection,
5938 "Scroll position should not change after selecting all matches"
5939 );
5940}
5941
5942#[gpui::test]
5943async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
5944 init_test(cx, |_| {});
5945
5946 let mut cx = EditorLspTestContext::new_rust(
5947 lsp::ServerCapabilities {
5948 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5949 ..Default::default()
5950 },
5951 cx,
5952 )
5953 .await;
5954
5955 cx.set_state(indoc! {"
5956 line 1
5957 line 2
5958 linˇe 3
5959 line 4
5960 line 5
5961 "});
5962
5963 // Make an edit
5964 cx.update_editor(|editor, window, cx| {
5965 editor.handle_input("X", window, cx);
5966 });
5967
5968 // Move cursor to a different position
5969 cx.update_editor(|editor, window, cx| {
5970 editor.change_selections(None, window, cx, |s| {
5971 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
5972 });
5973 });
5974
5975 cx.assert_editor_state(indoc! {"
5976 line 1
5977 line 2
5978 linXe 3
5979 line 4
5980 liˇne 5
5981 "});
5982
5983 cx.lsp
5984 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
5985 Ok(Some(vec![lsp::TextEdit::new(
5986 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
5987 "PREFIX ".to_string(),
5988 )]))
5989 });
5990
5991 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
5992 .unwrap()
5993 .await
5994 .unwrap();
5995
5996 cx.assert_editor_state(indoc! {"
5997 PREFIX line 1
5998 line 2
5999 linXe 3
6000 line 4
6001 liˇne 5
6002 "});
6003
6004 // Undo formatting
6005 cx.update_editor(|editor, window, cx| {
6006 editor.undo(&Default::default(), window, cx);
6007 });
6008
6009 // Verify cursor moved back to position after edit
6010 cx.assert_editor_state(indoc! {"
6011 line 1
6012 line 2
6013 linXˇe 3
6014 line 4
6015 line 5
6016 "});
6017}
6018
6019#[gpui::test]
6020async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6021 init_test(cx, |_| {});
6022
6023 let mut cx = EditorTestContext::new(cx).await;
6024 cx.set_state(
6025 r#"let foo = 2;
6026lˇet foo = 2;
6027let fooˇ = 2;
6028let foo = 2;
6029let foo = ˇ2;"#,
6030 );
6031
6032 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6033 .unwrap();
6034 cx.assert_editor_state(
6035 r#"let foo = 2;
6036«letˇ» foo = 2;
6037let «fooˇ» = 2;
6038let foo = 2;
6039let foo = «2ˇ»;"#,
6040 );
6041
6042 // noop for multiple selections with different contents
6043 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6044 .unwrap();
6045 cx.assert_editor_state(
6046 r#"let foo = 2;
6047«letˇ» foo = 2;
6048let «fooˇ» = 2;
6049let foo = 2;
6050let foo = «2ˇ»;"#,
6051 );
6052
6053 // Test last selection direction should be preserved
6054 cx.set_state(
6055 r#"let foo = 2;
6056let foo = 2;
6057let «fooˇ» = 2;
6058let «ˇfoo» = 2;
6059let foo = 2;"#,
6060 );
6061
6062 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6063 .unwrap();
6064 cx.assert_editor_state(
6065 r#"let foo = 2;
6066let foo = 2;
6067let «fooˇ» = 2;
6068let «ˇfoo» = 2;
6069let «ˇfoo» = 2;"#,
6070 );
6071}
6072
6073#[gpui::test]
6074async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6075 init_test(cx, |_| {});
6076
6077 let mut cx =
6078 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6079
6080 cx.assert_editor_state(indoc! {"
6081 ˇbbb
6082 ccc
6083
6084 bbb
6085 ccc
6086 "});
6087 cx.dispatch_action(SelectPrevious::default());
6088 cx.assert_editor_state(indoc! {"
6089 «bbbˇ»
6090 ccc
6091
6092 bbb
6093 ccc
6094 "});
6095 cx.dispatch_action(SelectPrevious::default());
6096 cx.assert_editor_state(indoc! {"
6097 «bbbˇ»
6098 ccc
6099
6100 «bbbˇ»
6101 ccc
6102 "});
6103}
6104
6105#[gpui::test]
6106async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6107 init_test(cx, |_| {});
6108
6109 let mut cx = EditorTestContext::new(cx).await;
6110 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6111
6112 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6113 .unwrap();
6114 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6115
6116 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6117 .unwrap();
6118 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6119
6120 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6121 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6122
6123 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6124 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6125
6126 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6127 .unwrap();
6128 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6129
6130 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6131 .unwrap();
6132 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
6133
6134 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6135 .unwrap();
6136 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6137}
6138
6139#[gpui::test]
6140async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6141 init_test(cx, |_| {});
6142
6143 let mut cx = EditorTestContext::new(cx).await;
6144 cx.set_state("aˇ");
6145
6146 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6147 .unwrap();
6148 cx.assert_editor_state("«aˇ»");
6149 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6150 .unwrap();
6151 cx.assert_editor_state("«aˇ»");
6152}
6153
6154#[gpui::test]
6155async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6156 init_test(cx, |_| {});
6157
6158 let mut cx = EditorTestContext::new(cx).await;
6159 cx.set_state(
6160 r#"let foo = 2;
6161lˇet foo = 2;
6162let fooˇ = 2;
6163let foo = 2;
6164let foo = ˇ2;"#,
6165 );
6166
6167 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6168 .unwrap();
6169 cx.assert_editor_state(
6170 r#"let foo = 2;
6171«letˇ» foo = 2;
6172let «fooˇ» = 2;
6173let foo = 2;
6174let foo = «2ˇ»;"#,
6175 );
6176
6177 // noop for multiple selections with different contents
6178 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6179 .unwrap();
6180 cx.assert_editor_state(
6181 r#"let foo = 2;
6182«letˇ» foo = 2;
6183let «fooˇ» = 2;
6184let foo = 2;
6185let foo = «2ˇ»;"#,
6186 );
6187}
6188
6189#[gpui::test]
6190async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6191 init_test(cx, |_| {});
6192
6193 let mut cx = EditorTestContext::new(cx).await;
6194 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6195
6196 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6197 .unwrap();
6198 // selection direction is preserved
6199 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6200
6201 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6202 .unwrap();
6203 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6204
6205 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6206 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6207
6208 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6209 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6210
6211 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6212 .unwrap();
6213 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6214
6215 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6216 .unwrap();
6217 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6218}
6219
6220#[gpui::test]
6221async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6222 init_test(cx, |_| {});
6223
6224 let language = Arc::new(Language::new(
6225 LanguageConfig::default(),
6226 Some(tree_sitter_rust::LANGUAGE.into()),
6227 ));
6228
6229 let text = r#"
6230 use mod1::mod2::{mod3, mod4};
6231
6232 fn fn_1(param1: bool, param2: &str) {
6233 let var1 = "text";
6234 }
6235 "#
6236 .unindent();
6237
6238 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6239 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6240 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6241
6242 editor
6243 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6244 .await;
6245
6246 editor.update_in(cx, |editor, window, cx| {
6247 editor.change_selections(None, window, cx, |s| {
6248 s.select_display_ranges([
6249 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6250 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6251 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6252 ]);
6253 });
6254 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6255 });
6256 editor.update(cx, |editor, cx| {
6257 assert_text_with_selections(
6258 editor,
6259 indoc! {r#"
6260 use mod1::mod2::{mod3, «mod4ˇ»};
6261
6262 fn fn_1«ˇ(param1: bool, param2: &str)» {
6263 let var1 = "«ˇtext»";
6264 }
6265 "#},
6266 cx,
6267 );
6268 });
6269
6270 editor.update_in(cx, |editor, window, cx| {
6271 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6272 });
6273 editor.update(cx, |editor, cx| {
6274 assert_text_with_selections(
6275 editor,
6276 indoc! {r#"
6277 use mod1::mod2::«{mod3, mod4}ˇ»;
6278
6279 «ˇfn fn_1(param1: bool, param2: &str) {
6280 let var1 = "text";
6281 }»
6282 "#},
6283 cx,
6284 );
6285 });
6286
6287 editor.update_in(cx, |editor, window, cx| {
6288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6289 });
6290 assert_eq!(
6291 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6292 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6293 );
6294
6295 // Trying to expand the selected syntax node one more time has no effect.
6296 editor.update_in(cx, |editor, window, cx| {
6297 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6298 });
6299 assert_eq!(
6300 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6301 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6302 );
6303
6304 editor.update_in(cx, |editor, window, cx| {
6305 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6306 });
6307 editor.update(cx, |editor, cx| {
6308 assert_text_with_selections(
6309 editor,
6310 indoc! {r#"
6311 use mod1::mod2::«{mod3, mod4}ˇ»;
6312
6313 «ˇfn fn_1(param1: bool, param2: &str) {
6314 let var1 = "text";
6315 }»
6316 "#},
6317 cx,
6318 );
6319 });
6320
6321 editor.update_in(cx, |editor, window, cx| {
6322 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6323 });
6324 editor.update(cx, |editor, cx| {
6325 assert_text_with_selections(
6326 editor,
6327 indoc! {r#"
6328 use mod1::mod2::{mod3, «mod4ˇ»};
6329
6330 fn fn_1«ˇ(param1: bool, param2: &str)» {
6331 let var1 = "«ˇtext»";
6332 }
6333 "#},
6334 cx,
6335 );
6336 });
6337
6338 editor.update_in(cx, |editor, window, cx| {
6339 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6340 });
6341 editor.update(cx, |editor, cx| {
6342 assert_text_with_selections(
6343 editor,
6344 indoc! {r#"
6345 use mod1::mod2::{mod3, mo«ˇ»d4};
6346
6347 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6348 let var1 = "te«ˇ»xt";
6349 }
6350 "#},
6351 cx,
6352 );
6353 });
6354
6355 // Trying to shrink the selected syntax node one more time has no effect.
6356 editor.update_in(cx, |editor, window, cx| {
6357 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6358 });
6359 editor.update_in(cx, |editor, _, cx| {
6360 assert_text_with_selections(
6361 editor,
6362 indoc! {r#"
6363 use mod1::mod2::{mod3, mo«ˇ»d4};
6364
6365 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6366 let var1 = "te«ˇ»xt";
6367 }
6368 "#},
6369 cx,
6370 );
6371 });
6372
6373 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6374 // a fold.
6375 editor.update_in(cx, |editor, window, cx| {
6376 editor.fold_creases(
6377 vec![
6378 Crease::simple(
6379 Point::new(0, 21)..Point::new(0, 24),
6380 FoldPlaceholder::test(),
6381 ),
6382 Crease::simple(
6383 Point::new(3, 20)..Point::new(3, 22),
6384 FoldPlaceholder::test(),
6385 ),
6386 ],
6387 true,
6388 window,
6389 cx,
6390 );
6391 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6392 });
6393 editor.update(cx, |editor, cx| {
6394 assert_text_with_selections(
6395 editor,
6396 indoc! {r#"
6397 use mod1::mod2::«{mod3, mod4}ˇ»;
6398
6399 fn fn_1«ˇ(param1: bool, param2: &str)» {
6400 let var1 = "«ˇtext»";
6401 }
6402 "#},
6403 cx,
6404 );
6405 });
6406}
6407
6408#[gpui::test]
6409async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6410 init_test(cx, |_| {});
6411
6412 let language = Arc::new(Language::new(
6413 LanguageConfig::default(),
6414 Some(tree_sitter_rust::LANGUAGE.into()),
6415 ));
6416
6417 let text = r#"
6418 use mod1::mod2::{mod3, mod4};
6419
6420 fn fn_1(param1: bool, param2: &str) {
6421 let var1 = "hello world";
6422 }
6423 "#
6424 .unindent();
6425
6426 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6428 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6429
6430 editor
6431 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6432 .await;
6433
6434 // Test 1: Cursor on a letter of a string word
6435 editor.update_in(cx, |editor, window, cx| {
6436 editor.change_selections(None, window, cx, |s| {
6437 s.select_display_ranges([
6438 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6439 ]);
6440 });
6441 });
6442 editor.update_in(cx, |editor, window, cx| {
6443 assert_text_with_selections(
6444 editor,
6445 indoc! {r#"
6446 use mod1::mod2::{mod3, mod4};
6447
6448 fn fn_1(param1: bool, param2: &str) {
6449 let var1 = "hˇello world";
6450 }
6451 "#},
6452 cx,
6453 );
6454 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6455 assert_text_with_selections(
6456 editor,
6457 indoc! {r#"
6458 use mod1::mod2::{mod3, mod4};
6459
6460 fn fn_1(param1: bool, param2: &str) {
6461 let var1 = "«ˇhello» world";
6462 }
6463 "#},
6464 cx,
6465 );
6466 });
6467
6468 // Test 2: Partial selection within a word
6469 editor.update_in(cx, |editor, window, cx| {
6470 editor.change_selections(None, window, cx, |s| {
6471 s.select_display_ranges([
6472 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6473 ]);
6474 });
6475 });
6476 editor.update_in(cx, |editor, window, cx| {
6477 assert_text_with_selections(
6478 editor,
6479 indoc! {r#"
6480 use mod1::mod2::{mod3, mod4};
6481
6482 fn fn_1(param1: bool, param2: &str) {
6483 let var1 = "h«elˇ»lo world";
6484 }
6485 "#},
6486 cx,
6487 );
6488 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6489 assert_text_with_selections(
6490 editor,
6491 indoc! {r#"
6492 use mod1::mod2::{mod3, mod4};
6493
6494 fn fn_1(param1: bool, param2: &str) {
6495 let var1 = "«ˇhello» world";
6496 }
6497 "#},
6498 cx,
6499 );
6500 });
6501
6502 // Test 3: Complete word already selected
6503 editor.update_in(cx, |editor, window, cx| {
6504 editor.change_selections(None, window, cx, |s| {
6505 s.select_display_ranges([
6506 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6507 ]);
6508 });
6509 });
6510 editor.update_in(cx, |editor, window, cx| {
6511 assert_text_with_selections(
6512 editor,
6513 indoc! {r#"
6514 use mod1::mod2::{mod3, mod4};
6515
6516 fn fn_1(param1: bool, param2: &str) {
6517 let var1 = "«helloˇ» world";
6518 }
6519 "#},
6520 cx,
6521 );
6522 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6523 assert_text_with_selections(
6524 editor,
6525 indoc! {r#"
6526 use mod1::mod2::{mod3, mod4};
6527
6528 fn fn_1(param1: bool, param2: &str) {
6529 let var1 = "«hello worldˇ»";
6530 }
6531 "#},
6532 cx,
6533 );
6534 });
6535
6536 // Test 4: Selection spanning across words
6537 editor.update_in(cx, |editor, window, cx| {
6538 editor.change_selections(None, window, cx, |s| {
6539 s.select_display_ranges([
6540 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6541 ]);
6542 });
6543 });
6544 editor.update_in(cx, |editor, window, cx| {
6545 assert_text_with_selections(
6546 editor,
6547 indoc! {r#"
6548 use mod1::mod2::{mod3, mod4};
6549
6550 fn fn_1(param1: bool, param2: &str) {
6551 let var1 = "hel«lo woˇ»rld";
6552 }
6553 "#},
6554 cx,
6555 );
6556 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6557 assert_text_with_selections(
6558 editor,
6559 indoc! {r#"
6560 use mod1::mod2::{mod3, mod4};
6561
6562 fn fn_1(param1: bool, param2: &str) {
6563 let var1 = "«ˇhello world»";
6564 }
6565 "#},
6566 cx,
6567 );
6568 });
6569
6570 // Test 5: Expansion beyond string
6571 editor.update_in(cx, |editor, window, cx| {
6572 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6573 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6574 assert_text_with_selections(
6575 editor,
6576 indoc! {r#"
6577 use mod1::mod2::{mod3, mod4};
6578
6579 fn fn_1(param1: bool, param2: &str) {
6580 «ˇlet var1 = "hello world";»
6581 }
6582 "#},
6583 cx,
6584 );
6585 });
6586}
6587
6588#[gpui::test]
6589async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6590 init_test(cx, |_| {});
6591
6592 let base_text = r#"
6593 impl A {
6594 // this is an uncommitted comment
6595
6596 fn b() {
6597 c();
6598 }
6599
6600 // this is another uncommitted comment
6601
6602 fn d() {
6603 // e
6604 // f
6605 }
6606 }
6607
6608 fn g() {
6609 // h
6610 }
6611 "#
6612 .unindent();
6613
6614 let text = r#"
6615 ˇimpl A {
6616
6617 fn b() {
6618 c();
6619 }
6620
6621 fn d() {
6622 // e
6623 // f
6624 }
6625 }
6626
6627 fn g() {
6628 // h
6629 }
6630 "#
6631 .unindent();
6632
6633 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6634 cx.set_state(&text);
6635 cx.set_head_text(&base_text);
6636 cx.update_editor(|editor, window, cx| {
6637 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6638 });
6639
6640 cx.assert_state_with_diff(
6641 "
6642 ˇimpl A {
6643 - // this is an uncommitted comment
6644
6645 fn b() {
6646 c();
6647 }
6648
6649 - // this is another uncommitted comment
6650 -
6651 fn d() {
6652 // e
6653 // f
6654 }
6655 }
6656
6657 fn g() {
6658 // h
6659 }
6660 "
6661 .unindent(),
6662 );
6663
6664 let expected_display_text = "
6665 impl A {
6666 // this is an uncommitted comment
6667
6668 fn b() {
6669 ⋯
6670 }
6671
6672 // this is another uncommitted comment
6673
6674 fn d() {
6675 ⋯
6676 }
6677 }
6678
6679 fn g() {
6680 ⋯
6681 }
6682 "
6683 .unindent();
6684
6685 cx.update_editor(|editor, window, cx| {
6686 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6687 assert_eq!(editor.display_text(cx), expected_display_text);
6688 });
6689}
6690
6691#[gpui::test]
6692async fn test_autoindent(cx: &mut TestAppContext) {
6693 init_test(cx, |_| {});
6694
6695 let language = Arc::new(
6696 Language::new(
6697 LanguageConfig {
6698 brackets: BracketPairConfig {
6699 pairs: vec![
6700 BracketPair {
6701 start: "{".to_string(),
6702 end: "}".to_string(),
6703 close: false,
6704 surround: false,
6705 newline: true,
6706 },
6707 BracketPair {
6708 start: "(".to_string(),
6709 end: ")".to_string(),
6710 close: false,
6711 surround: false,
6712 newline: true,
6713 },
6714 ],
6715 ..Default::default()
6716 },
6717 ..Default::default()
6718 },
6719 Some(tree_sitter_rust::LANGUAGE.into()),
6720 )
6721 .with_indents_query(
6722 r#"
6723 (_ "(" ")" @end) @indent
6724 (_ "{" "}" @end) @indent
6725 "#,
6726 )
6727 .unwrap(),
6728 );
6729
6730 let text = "fn a() {}";
6731
6732 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6733 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6734 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6735 editor
6736 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6737 .await;
6738
6739 editor.update_in(cx, |editor, window, cx| {
6740 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6741 editor.newline(&Newline, window, cx);
6742 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6743 assert_eq!(
6744 editor.selections.ranges(cx),
6745 &[
6746 Point::new(1, 4)..Point::new(1, 4),
6747 Point::new(3, 4)..Point::new(3, 4),
6748 Point::new(5, 0)..Point::new(5, 0)
6749 ]
6750 );
6751 });
6752}
6753
6754#[gpui::test]
6755async fn test_autoindent_selections(cx: &mut TestAppContext) {
6756 init_test(cx, |_| {});
6757
6758 {
6759 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6760 cx.set_state(indoc! {"
6761 impl A {
6762
6763 fn b() {}
6764
6765 «fn c() {
6766
6767 }ˇ»
6768 }
6769 "});
6770
6771 cx.update_editor(|editor, window, cx| {
6772 editor.autoindent(&Default::default(), window, cx);
6773 });
6774
6775 cx.assert_editor_state(indoc! {"
6776 impl A {
6777
6778 fn b() {}
6779
6780 «fn c() {
6781
6782 }ˇ»
6783 }
6784 "});
6785 }
6786
6787 {
6788 let mut cx = EditorTestContext::new_multibuffer(
6789 cx,
6790 [indoc! { "
6791 impl A {
6792 «
6793 // a
6794 fn b(){}
6795 »
6796 «
6797 }
6798 fn c(){}
6799 »
6800 "}],
6801 );
6802
6803 let buffer = cx.update_editor(|editor, _, cx| {
6804 let buffer = editor.buffer().update(cx, |buffer, _| {
6805 buffer.all_buffers().iter().next().unwrap().clone()
6806 });
6807 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6808 buffer
6809 });
6810
6811 cx.run_until_parked();
6812 cx.update_editor(|editor, window, cx| {
6813 editor.select_all(&Default::default(), window, cx);
6814 editor.autoindent(&Default::default(), window, cx)
6815 });
6816 cx.run_until_parked();
6817
6818 cx.update(|_, cx| {
6819 assert_eq!(
6820 buffer.read(cx).text(),
6821 indoc! { "
6822 impl A {
6823
6824 // a
6825 fn b(){}
6826
6827
6828 }
6829 fn c(){}
6830
6831 " }
6832 )
6833 });
6834 }
6835}
6836
6837#[gpui::test]
6838async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6839 init_test(cx, |_| {});
6840
6841 let mut cx = EditorTestContext::new(cx).await;
6842
6843 let language = Arc::new(Language::new(
6844 LanguageConfig {
6845 brackets: BracketPairConfig {
6846 pairs: vec![
6847 BracketPair {
6848 start: "{".to_string(),
6849 end: "}".to_string(),
6850 close: true,
6851 surround: true,
6852 newline: true,
6853 },
6854 BracketPair {
6855 start: "(".to_string(),
6856 end: ")".to_string(),
6857 close: true,
6858 surround: true,
6859 newline: true,
6860 },
6861 BracketPair {
6862 start: "/*".to_string(),
6863 end: " */".to_string(),
6864 close: true,
6865 surround: true,
6866 newline: true,
6867 },
6868 BracketPair {
6869 start: "[".to_string(),
6870 end: "]".to_string(),
6871 close: false,
6872 surround: false,
6873 newline: true,
6874 },
6875 BracketPair {
6876 start: "\"".to_string(),
6877 end: "\"".to_string(),
6878 close: true,
6879 surround: true,
6880 newline: false,
6881 },
6882 BracketPair {
6883 start: "<".to_string(),
6884 end: ">".to_string(),
6885 close: false,
6886 surround: true,
6887 newline: true,
6888 },
6889 ],
6890 ..Default::default()
6891 },
6892 autoclose_before: "})]".to_string(),
6893 ..Default::default()
6894 },
6895 Some(tree_sitter_rust::LANGUAGE.into()),
6896 ));
6897
6898 cx.language_registry().add(language.clone());
6899 cx.update_buffer(|buffer, cx| {
6900 buffer.set_language(Some(language), cx);
6901 });
6902
6903 cx.set_state(
6904 &r#"
6905 🏀ˇ
6906 εˇ
6907 ❤️ˇ
6908 "#
6909 .unindent(),
6910 );
6911
6912 // autoclose multiple nested brackets at multiple cursors
6913 cx.update_editor(|editor, window, cx| {
6914 editor.handle_input("{", window, cx);
6915 editor.handle_input("{", window, cx);
6916 editor.handle_input("{", window, cx);
6917 });
6918 cx.assert_editor_state(
6919 &"
6920 🏀{{{ˇ}}}
6921 ε{{{ˇ}}}
6922 ❤️{{{ˇ}}}
6923 "
6924 .unindent(),
6925 );
6926
6927 // insert a different closing bracket
6928 cx.update_editor(|editor, window, cx| {
6929 editor.handle_input(")", window, cx);
6930 });
6931 cx.assert_editor_state(
6932 &"
6933 🏀{{{)ˇ}}}
6934 ε{{{)ˇ}}}
6935 ❤️{{{)ˇ}}}
6936 "
6937 .unindent(),
6938 );
6939
6940 // skip over the auto-closed brackets when typing a closing bracket
6941 cx.update_editor(|editor, window, cx| {
6942 editor.move_right(&MoveRight, window, cx);
6943 editor.handle_input("}", window, cx);
6944 editor.handle_input("}", window, cx);
6945 editor.handle_input("}", window, cx);
6946 });
6947 cx.assert_editor_state(
6948 &"
6949 🏀{{{)}}}}ˇ
6950 ε{{{)}}}}ˇ
6951 ❤️{{{)}}}}ˇ
6952 "
6953 .unindent(),
6954 );
6955
6956 // autoclose multi-character pairs
6957 cx.set_state(
6958 &"
6959 ˇ
6960 ˇ
6961 "
6962 .unindent(),
6963 );
6964 cx.update_editor(|editor, window, cx| {
6965 editor.handle_input("/", window, cx);
6966 editor.handle_input("*", window, cx);
6967 });
6968 cx.assert_editor_state(
6969 &"
6970 /*ˇ */
6971 /*ˇ */
6972 "
6973 .unindent(),
6974 );
6975
6976 // one cursor autocloses a multi-character pair, one cursor
6977 // does not autoclose.
6978 cx.set_state(
6979 &"
6980 /ˇ
6981 ˇ
6982 "
6983 .unindent(),
6984 );
6985 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6986 cx.assert_editor_state(
6987 &"
6988 /*ˇ */
6989 *ˇ
6990 "
6991 .unindent(),
6992 );
6993
6994 // Don't autoclose if the next character isn't whitespace and isn't
6995 // listed in the language's "autoclose_before" section.
6996 cx.set_state("ˇa b");
6997 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6998 cx.assert_editor_state("{ˇa b");
6999
7000 // Don't autoclose if `close` is false for the bracket pair
7001 cx.set_state("ˇ");
7002 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7003 cx.assert_editor_state("[ˇ");
7004
7005 // Surround with brackets if text is selected
7006 cx.set_state("«aˇ» b");
7007 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7008 cx.assert_editor_state("{«aˇ»} b");
7009
7010 // Autoclose when not immediately after a word character
7011 cx.set_state("a ˇ");
7012 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7013 cx.assert_editor_state("a \"ˇ\"");
7014
7015 // Autoclose pair where the start and end characters are the same
7016 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7017 cx.assert_editor_state("a \"\"ˇ");
7018
7019 // Don't autoclose when immediately after a word character
7020 cx.set_state("aˇ");
7021 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7022 cx.assert_editor_state("a\"ˇ");
7023
7024 // Do autoclose when after a non-word character
7025 cx.set_state("{ˇ");
7026 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7027 cx.assert_editor_state("{\"ˇ\"");
7028
7029 // Non identical pairs autoclose regardless of preceding character
7030 cx.set_state("aˇ");
7031 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7032 cx.assert_editor_state("a{ˇ}");
7033
7034 // Don't autoclose pair if autoclose is disabled
7035 cx.set_state("ˇ");
7036 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7037 cx.assert_editor_state("<ˇ");
7038
7039 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7040 cx.set_state("«aˇ» b");
7041 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7042 cx.assert_editor_state("<«aˇ»> b");
7043}
7044
7045#[gpui::test]
7046async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7047 init_test(cx, |settings| {
7048 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7049 });
7050
7051 let mut cx = EditorTestContext::new(cx).await;
7052
7053 let language = Arc::new(Language::new(
7054 LanguageConfig {
7055 brackets: BracketPairConfig {
7056 pairs: vec![
7057 BracketPair {
7058 start: "{".to_string(),
7059 end: "}".to_string(),
7060 close: true,
7061 surround: true,
7062 newline: true,
7063 },
7064 BracketPair {
7065 start: "(".to_string(),
7066 end: ")".to_string(),
7067 close: true,
7068 surround: true,
7069 newline: true,
7070 },
7071 BracketPair {
7072 start: "[".to_string(),
7073 end: "]".to_string(),
7074 close: false,
7075 surround: false,
7076 newline: true,
7077 },
7078 ],
7079 ..Default::default()
7080 },
7081 autoclose_before: "})]".to_string(),
7082 ..Default::default()
7083 },
7084 Some(tree_sitter_rust::LANGUAGE.into()),
7085 ));
7086
7087 cx.language_registry().add(language.clone());
7088 cx.update_buffer(|buffer, cx| {
7089 buffer.set_language(Some(language), cx);
7090 });
7091
7092 cx.set_state(
7093 &"
7094 ˇ
7095 ˇ
7096 ˇ
7097 "
7098 .unindent(),
7099 );
7100
7101 // ensure only matching closing brackets are skipped over
7102 cx.update_editor(|editor, window, cx| {
7103 editor.handle_input("}", window, cx);
7104 editor.move_left(&MoveLeft, window, cx);
7105 editor.handle_input(")", window, cx);
7106 editor.move_left(&MoveLeft, window, cx);
7107 });
7108 cx.assert_editor_state(
7109 &"
7110 ˇ)}
7111 ˇ)}
7112 ˇ)}
7113 "
7114 .unindent(),
7115 );
7116
7117 // skip-over closing brackets at multiple cursors
7118 cx.update_editor(|editor, window, cx| {
7119 editor.handle_input(")", window, cx);
7120 editor.handle_input("}", window, cx);
7121 });
7122 cx.assert_editor_state(
7123 &"
7124 )}ˇ
7125 )}ˇ
7126 )}ˇ
7127 "
7128 .unindent(),
7129 );
7130
7131 // ignore non-close brackets
7132 cx.update_editor(|editor, window, cx| {
7133 editor.handle_input("]", window, cx);
7134 editor.move_left(&MoveLeft, window, cx);
7135 editor.handle_input("]", window, cx);
7136 });
7137 cx.assert_editor_state(
7138 &"
7139 )}]ˇ]
7140 )}]ˇ]
7141 )}]ˇ]
7142 "
7143 .unindent(),
7144 );
7145}
7146
7147#[gpui::test]
7148async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7149 init_test(cx, |_| {});
7150
7151 let mut cx = EditorTestContext::new(cx).await;
7152
7153 let html_language = Arc::new(
7154 Language::new(
7155 LanguageConfig {
7156 name: "HTML".into(),
7157 brackets: BracketPairConfig {
7158 pairs: vec![
7159 BracketPair {
7160 start: "<".into(),
7161 end: ">".into(),
7162 close: true,
7163 ..Default::default()
7164 },
7165 BracketPair {
7166 start: "{".into(),
7167 end: "}".into(),
7168 close: true,
7169 ..Default::default()
7170 },
7171 BracketPair {
7172 start: "(".into(),
7173 end: ")".into(),
7174 close: true,
7175 ..Default::default()
7176 },
7177 ],
7178 ..Default::default()
7179 },
7180 autoclose_before: "})]>".into(),
7181 ..Default::default()
7182 },
7183 Some(tree_sitter_html::LANGUAGE.into()),
7184 )
7185 .with_injection_query(
7186 r#"
7187 (script_element
7188 (raw_text) @injection.content
7189 (#set! injection.language "javascript"))
7190 "#,
7191 )
7192 .unwrap(),
7193 );
7194
7195 let javascript_language = Arc::new(Language::new(
7196 LanguageConfig {
7197 name: "JavaScript".into(),
7198 brackets: BracketPairConfig {
7199 pairs: vec![
7200 BracketPair {
7201 start: "/*".into(),
7202 end: " */".into(),
7203 close: true,
7204 ..Default::default()
7205 },
7206 BracketPair {
7207 start: "{".into(),
7208 end: "}".into(),
7209 close: true,
7210 ..Default::default()
7211 },
7212 BracketPair {
7213 start: "(".into(),
7214 end: ")".into(),
7215 close: true,
7216 ..Default::default()
7217 },
7218 ],
7219 ..Default::default()
7220 },
7221 autoclose_before: "})]>".into(),
7222 ..Default::default()
7223 },
7224 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7225 ));
7226
7227 cx.language_registry().add(html_language.clone());
7228 cx.language_registry().add(javascript_language.clone());
7229
7230 cx.update_buffer(|buffer, cx| {
7231 buffer.set_language(Some(html_language), cx);
7232 });
7233
7234 cx.set_state(
7235 &r#"
7236 <body>ˇ
7237 <script>
7238 var x = 1;ˇ
7239 </script>
7240 </body>ˇ
7241 "#
7242 .unindent(),
7243 );
7244
7245 // Precondition: different languages are active at different locations.
7246 cx.update_editor(|editor, window, cx| {
7247 let snapshot = editor.snapshot(window, cx);
7248 let cursors = editor.selections.ranges::<usize>(cx);
7249 let languages = cursors
7250 .iter()
7251 .map(|c| snapshot.language_at(c.start).unwrap().name())
7252 .collect::<Vec<_>>();
7253 assert_eq!(
7254 languages,
7255 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7256 );
7257 });
7258
7259 // Angle brackets autoclose in HTML, but not JavaScript.
7260 cx.update_editor(|editor, window, cx| {
7261 editor.handle_input("<", window, cx);
7262 editor.handle_input("a", window, cx);
7263 });
7264 cx.assert_editor_state(
7265 &r#"
7266 <body><aˇ>
7267 <script>
7268 var x = 1;<aˇ
7269 </script>
7270 </body><aˇ>
7271 "#
7272 .unindent(),
7273 );
7274
7275 // Curly braces and parens autoclose in both HTML and JavaScript.
7276 cx.update_editor(|editor, window, cx| {
7277 editor.handle_input(" b=", window, cx);
7278 editor.handle_input("{", window, cx);
7279 editor.handle_input("c", window, cx);
7280 editor.handle_input("(", window, cx);
7281 });
7282 cx.assert_editor_state(
7283 &r#"
7284 <body><a b={c(ˇ)}>
7285 <script>
7286 var x = 1;<a b={c(ˇ)}
7287 </script>
7288 </body><a b={c(ˇ)}>
7289 "#
7290 .unindent(),
7291 );
7292
7293 // Brackets that were already autoclosed are skipped.
7294 cx.update_editor(|editor, window, cx| {
7295 editor.handle_input(")", window, cx);
7296 editor.handle_input("d", window, cx);
7297 editor.handle_input("}", window, cx);
7298 });
7299 cx.assert_editor_state(
7300 &r#"
7301 <body><a b={c()d}ˇ>
7302 <script>
7303 var x = 1;<a b={c()d}ˇ
7304 </script>
7305 </body><a b={c()d}ˇ>
7306 "#
7307 .unindent(),
7308 );
7309 cx.update_editor(|editor, window, cx| {
7310 editor.handle_input(">", window, cx);
7311 });
7312 cx.assert_editor_state(
7313 &r#"
7314 <body><a b={c()d}>ˇ
7315 <script>
7316 var x = 1;<a b={c()d}>ˇ
7317 </script>
7318 </body><a b={c()d}>ˇ
7319 "#
7320 .unindent(),
7321 );
7322
7323 // Reset
7324 cx.set_state(
7325 &r#"
7326 <body>ˇ
7327 <script>
7328 var x = 1;ˇ
7329 </script>
7330 </body>ˇ
7331 "#
7332 .unindent(),
7333 );
7334
7335 cx.update_editor(|editor, window, cx| {
7336 editor.handle_input("<", window, cx);
7337 });
7338 cx.assert_editor_state(
7339 &r#"
7340 <body><ˇ>
7341 <script>
7342 var x = 1;<ˇ
7343 </script>
7344 </body><ˇ>
7345 "#
7346 .unindent(),
7347 );
7348
7349 // When backspacing, the closing angle brackets are removed.
7350 cx.update_editor(|editor, window, cx| {
7351 editor.backspace(&Backspace, window, cx);
7352 });
7353 cx.assert_editor_state(
7354 &r#"
7355 <body>ˇ
7356 <script>
7357 var x = 1;ˇ
7358 </script>
7359 </body>ˇ
7360 "#
7361 .unindent(),
7362 );
7363
7364 // Block comments autoclose in JavaScript, but not HTML.
7365 cx.update_editor(|editor, window, cx| {
7366 editor.handle_input("/", window, cx);
7367 editor.handle_input("*", window, cx);
7368 });
7369 cx.assert_editor_state(
7370 &r#"
7371 <body>/*ˇ
7372 <script>
7373 var x = 1;/*ˇ */
7374 </script>
7375 </body>/*ˇ
7376 "#
7377 .unindent(),
7378 );
7379}
7380
7381#[gpui::test]
7382async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7383 init_test(cx, |_| {});
7384
7385 let mut cx = EditorTestContext::new(cx).await;
7386
7387 let rust_language = Arc::new(
7388 Language::new(
7389 LanguageConfig {
7390 name: "Rust".into(),
7391 brackets: serde_json::from_value(json!([
7392 { "start": "{", "end": "}", "close": true, "newline": true },
7393 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7394 ]))
7395 .unwrap(),
7396 autoclose_before: "})]>".into(),
7397 ..Default::default()
7398 },
7399 Some(tree_sitter_rust::LANGUAGE.into()),
7400 )
7401 .with_override_query("(string_literal) @string")
7402 .unwrap(),
7403 );
7404
7405 cx.language_registry().add(rust_language.clone());
7406 cx.update_buffer(|buffer, cx| {
7407 buffer.set_language(Some(rust_language), cx);
7408 });
7409
7410 cx.set_state(
7411 &r#"
7412 let x = ˇ
7413 "#
7414 .unindent(),
7415 );
7416
7417 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7418 cx.update_editor(|editor, window, cx| {
7419 editor.handle_input("\"", window, cx);
7420 });
7421 cx.assert_editor_state(
7422 &r#"
7423 let x = "ˇ"
7424 "#
7425 .unindent(),
7426 );
7427
7428 // Inserting another quotation mark. The cursor moves across the existing
7429 // automatically-inserted quotation mark.
7430 cx.update_editor(|editor, window, cx| {
7431 editor.handle_input("\"", window, cx);
7432 });
7433 cx.assert_editor_state(
7434 &r#"
7435 let x = ""ˇ
7436 "#
7437 .unindent(),
7438 );
7439
7440 // Reset
7441 cx.set_state(
7442 &r#"
7443 let x = ˇ
7444 "#
7445 .unindent(),
7446 );
7447
7448 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7449 cx.update_editor(|editor, window, cx| {
7450 editor.handle_input("\"", window, cx);
7451 editor.handle_input(" ", window, cx);
7452 editor.move_left(&Default::default(), window, cx);
7453 editor.handle_input("\\", window, cx);
7454 editor.handle_input("\"", window, cx);
7455 });
7456 cx.assert_editor_state(
7457 &r#"
7458 let x = "\"ˇ "
7459 "#
7460 .unindent(),
7461 );
7462
7463 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7464 // mark. Nothing is inserted.
7465 cx.update_editor(|editor, window, cx| {
7466 editor.move_right(&Default::default(), window, cx);
7467 editor.handle_input("\"", window, cx);
7468 });
7469 cx.assert_editor_state(
7470 &r#"
7471 let x = "\" "ˇ
7472 "#
7473 .unindent(),
7474 );
7475}
7476
7477#[gpui::test]
7478async fn test_surround_with_pair(cx: &mut TestAppContext) {
7479 init_test(cx, |_| {});
7480
7481 let language = Arc::new(Language::new(
7482 LanguageConfig {
7483 brackets: BracketPairConfig {
7484 pairs: vec![
7485 BracketPair {
7486 start: "{".to_string(),
7487 end: "}".to_string(),
7488 close: true,
7489 surround: true,
7490 newline: true,
7491 },
7492 BracketPair {
7493 start: "/* ".to_string(),
7494 end: "*/".to_string(),
7495 close: true,
7496 surround: true,
7497 ..Default::default()
7498 },
7499 ],
7500 ..Default::default()
7501 },
7502 ..Default::default()
7503 },
7504 Some(tree_sitter_rust::LANGUAGE.into()),
7505 ));
7506
7507 let text = r#"
7508 a
7509 b
7510 c
7511 "#
7512 .unindent();
7513
7514 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7515 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7516 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7517 editor
7518 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7519 .await;
7520
7521 editor.update_in(cx, |editor, window, cx| {
7522 editor.change_selections(None, window, cx, |s| {
7523 s.select_display_ranges([
7524 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7525 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7526 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7527 ])
7528 });
7529
7530 editor.handle_input("{", window, cx);
7531 editor.handle_input("{", window, cx);
7532 editor.handle_input("{", window, cx);
7533 assert_eq!(
7534 editor.text(cx),
7535 "
7536 {{{a}}}
7537 {{{b}}}
7538 {{{c}}}
7539 "
7540 .unindent()
7541 );
7542 assert_eq!(
7543 editor.selections.display_ranges(cx),
7544 [
7545 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7546 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7547 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7548 ]
7549 );
7550
7551 editor.undo(&Undo, window, cx);
7552 editor.undo(&Undo, window, cx);
7553 editor.undo(&Undo, window, cx);
7554 assert_eq!(
7555 editor.text(cx),
7556 "
7557 a
7558 b
7559 c
7560 "
7561 .unindent()
7562 );
7563 assert_eq!(
7564 editor.selections.display_ranges(cx),
7565 [
7566 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7567 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7568 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7569 ]
7570 );
7571
7572 // Ensure inserting the first character of a multi-byte bracket pair
7573 // doesn't surround the selections with the bracket.
7574 editor.handle_input("/", window, cx);
7575 assert_eq!(
7576 editor.text(cx),
7577 "
7578 /
7579 /
7580 /
7581 "
7582 .unindent()
7583 );
7584 assert_eq!(
7585 editor.selections.display_ranges(cx),
7586 [
7587 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7588 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7589 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7590 ]
7591 );
7592
7593 editor.undo(&Undo, window, cx);
7594 assert_eq!(
7595 editor.text(cx),
7596 "
7597 a
7598 b
7599 c
7600 "
7601 .unindent()
7602 );
7603 assert_eq!(
7604 editor.selections.display_ranges(cx),
7605 [
7606 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7607 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7608 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7609 ]
7610 );
7611
7612 // Ensure inserting the last character of a multi-byte bracket pair
7613 // doesn't surround the selections with the bracket.
7614 editor.handle_input("*", window, cx);
7615 assert_eq!(
7616 editor.text(cx),
7617 "
7618 *
7619 *
7620 *
7621 "
7622 .unindent()
7623 );
7624 assert_eq!(
7625 editor.selections.display_ranges(cx),
7626 [
7627 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7628 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7629 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7630 ]
7631 );
7632 });
7633}
7634
7635#[gpui::test]
7636async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7637 init_test(cx, |_| {});
7638
7639 let language = Arc::new(Language::new(
7640 LanguageConfig {
7641 brackets: BracketPairConfig {
7642 pairs: vec![BracketPair {
7643 start: "{".to_string(),
7644 end: "}".to_string(),
7645 close: true,
7646 surround: true,
7647 newline: true,
7648 }],
7649 ..Default::default()
7650 },
7651 autoclose_before: "}".to_string(),
7652 ..Default::default()
7653 },
7654 Some(tree_sitter_rust::LANGUAGE.into()),
7655 ));
7656
7657 let text = r#"
7658 a
7659 b
7660 c
7661 "#
7662 .unindent();
7663
7664 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7665 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7666 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7667 editor
7668 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7669 .await;
7670
7671 editor.update_in(cx, |editor, window, cx| {
7672 editor.change_selections(None, window, cx, |s| {
7673 s.select_ranges([
7674 Point::new(0, 1)..Point::new(0, 1),
7675 Point::new(1, 1)..Point::new(1, 1),
7676 Point::new(2, 1)..Point::new(2, 1),
7677 ])
7678 });
7679
7680 editor.handle_input("{", window, cx);
7681 editor.handle_input("{", window, cx);
7682 editor.handle_input("_", window, cx);
7683 assert_eq!(
7684 editor.text(cx),
7685 "
7686 a{{_}}
7687 b{{_}}
7688 c{{_}}
7689 "
7690 .unindent()
7691 );
7692 assert_eq!(
7693 editor.selections.ranges::<Point>(cx),
7694 [
7695 Point::new(0, 4)..Point::new(0, 4),
7696 Point::new(1, 4)..Point::new(1, 4),
7697 Point::new(2, 4)..Point::new(2, 4)
7698 ]
7699 );
7700
7701 editor.backspace(&Default::default(), window, cx);
7702 editor.backspace(&Default::default(), window, cx);
7703 assert_eq!(
7704 editor.text(cx),
7705 "
7706 a{}
7707 b{}
7708 c{}
7709 "
7710 .unindent()
7711 );
7712 assert_eq!(
7713 editor.selections.ranges::<Point>(cx),
7714 [
7715 Point::new(0, 2)..Point::new(0, 2),
7716 Point::new(1, 2)..Point::new(1, 2),
7717 Point::new(2, 2)..Point::new(2, 2)
7718 ]
7719 );
7720
7721 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7722 assert_eq!(
7723 editor.text(cx),
7724 "
7725 a
7726 b
7727 c
7728 "
7729 .unindent()
7730 );
7731 assert_eq!(
7732 editor.selections.ranges::<Point>(cx),
7733 [
7734 Point::new(0, 1)..Point::new(0, 1),
7735 Point::new(1, 1)..Point::new(1, 1),
7736 Point::new(2, 1)..Point::new(2, 1)
7737 ]
7738 );
7739 });
7740}
7741
7742#[gpui::test]
7743async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7744 init_test(cx, |settings| {
7745 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7746 });
7747
7748 let mut cx = EditorTestContext::new(cx).await;
7749
7750 let language = Arc::new(Language::new(
7751 LanguageConfig {
7752 brackets: BracketPairConfig {
7753 pairs: vec![
7754 BracketPair {
7755 start: "{".to_string(),
7756 end: "}".to_string(),
7757 close: true,
7758 surround: true,
7759 newline: true,
7760 },
7761 BracketPair {
7762 start: "(".to_string(),
7763 end: ")".to_string(),
7764 close: true,
7765 surround: true,
7766 newline: true,
7767 },
7768 BracketPair {
7769 start: "[".to_string(),
7770 end: "]".to_string(),
7771 close: false,
7772 surround: true,
7773 newline: true,
7774 },
7775 ],
7776 ..Default::default()
7777 },
7778 autoclose_before: "})]".to_string(),
7779 ..Default::default()
7780 },
7781 Some(tree_sitter_rust::LANGUAGE.into()),
7782 ));
7783
7784 cx.language_registry().add(language.clone());
7785 cx.update_buffer(|buffer, cx| {
7786 buffer.set_language(Some(language), cx);
7787 });
7788
7789 cx.set_state(
7790 &"
7791 {(ˇ)}
7792 [[ˇ]]
7793 {(ˇ)}
7794 "
7795 .unindent(),
7796 );
7797
7798 cx.update_editor(|editor, window, cx| {
7799 editor.backspace(&Default::default(), window, cx);
7800 editor.backspace(&Default::default(), window, cx);
7801 });
7802
7803 cx.assert_editor_state(
7804 &"
7805 ˇ
7806 ˇ]]
7807 ˇ
7808 "
7809 .unindent(),
7810 );
7811
7812 cx.update_editor(|editor, window, cx| {
7813 editor.handle_input("{", window, cx);
7814 editor.handle_input("{", window, cx);
7815 editor.move_right(&MoveRight, window, cx);
7816 editor.move_right(&MoveRight, window, cx);
7817 editor.move_left(&MoveLeft, window, cx);
7818 editor.move_left(&MoveLeft, window, cx);
7819 editor.backspace(&Default::default(), window, cx);
7820 });
7821
7822 cx.assert_editor_state(
7823 &"
7824 {ˇ}
7825 {ˇ}]]
7826 {ˇ}
7827 "
7828 .unindent(),
7829 );
7830
7831 cx.update_editor(|editor, window, cx| {
7832 editor.backspace(&Default::default(), window, cx);
7833 });
7834
7835 cx.assert_editor_state(
7836 &"
7837 ˇ
7838 ˇ]]
7839 ˇ
7840 "
7841 .unindent(),
7842 );
7843}
7844
7845#[gpui::test]
7846async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7847 init_test(cx, |_| {});
7848
7849 let language = Arc::new(Language::new(
7850 LanguageConfig::default(),
7851 Some(tree_sitter_rust::LANGUAGE.into()),
7852 ));
7853
7854 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7855 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7856 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7857 editor
7858 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7859 .await;
7860
7861 editor.update_in(cx, |editor, window, cx| {
7862 editor.set_auto_replace_emoji_shortcode(true);
7863
7864 editor.handle_input("Hello ", window, cx);
7865 editor.handle_input(":wave", window, cx);
7866 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7867
7868 editor.handle_input(":", window, cx);
7869 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7870
7871 editor.handle_input(" :smile", window, cx);
7872 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7873
7874 editor.handle_input(":", window, cx);
7875 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7876
7877 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7878 editor.handle_input(":wave", window, cx);
7879 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7880
7881 editor.handle_input(":", window, cx);
7882 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7883
7884 editor.handle_input(":1", window, cx);
7885 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7886
7887 editor.handle_input(":", window, cx);
7888 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7889
7890 // Ensure shortcode does not get replaced when it is part of a word
7891 editor.handle_input(" Test:wave", window, cx);
7892 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7893
7894 editor.handle_input(":", window, cx);
7895 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7896
7897 editor.set_auto_replace_emoji_shortcode(false);
7898
7899 // Ensure shortcode does not get replaced when auto replace is off
7900 editor.handle_input(" :wave", window, cx);
7901 assert_eq!(
7902 editor.text(cx),
7903 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7904 );
7905
7906 editor.handle_input(":", window, cx);
7907 assert_eq!(
7908 editor.text(cx),
7909 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7910 );
7911 });
7912}
7913
7914#[gpui::test]
7915async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7916 init_test(cx, |_| {});
7917
7918 let (text, insertion_ranges) = marked_text_ranges(
7919 indoc! {"
7920 ˇ
7921 "},
7922 false,
7923 );
7924
7925 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7926 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7927
7928 _ = editor.update_in(cx, |editor, window, cx| {
7929 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7930
7931 editor
7932 .insert_snippet(&insertion_ranges, snippet, window, cx)
7933 .unwrap();
7934
7935 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7936 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7937 assert_eq!(editor.text(cx), expected_text);
7938 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7939 }
7940
7941 assert(
7942 editor,
7943 cx,
7944 indoc! {"
7945 type «» =•
7946 "},
7947 );
7948
7949 assert!(editor.context_menu_visible(), "There should be a matches");
7950 });
7951}
7952
7953#[gpui::test]
7954async fn test_snippets(cx: &mut TestAppContext) {
7955 init_test(cx, |_| {});
7956
7957 let (text, insertion_ranges) = marked_text_ranges(
7958 indoc! {"
7959 a.ˇ b
7960 a.ˇ b
7961 a.ˇ b
7962 "},
7963 false,
7964 );
7965
7966 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7968
7969 editor.update_in(cx, |editor, window, cx| {
7970 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7971
7972 editor
7973 .insert_snippet(&insertion_ranges, snippet, window, cx)
7974 .unwrap();
7975
7976 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7977 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7978 assert_eq!(editor.text(cx), expected_text);
7979 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7980 }
7981
7982 assert(
7983 editor,
7984 cx,
7985 indoc! {"
7986 a.f(«one», two, «three») b
7987 a.f(«one», two, «three») b
7988 a.f(«one», two, «three») b
7989 "},
7990 );
7991
7992 // Can't move earlier than the first tab stop
7993 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7994 assert(
7995 editor,
7996 cx,
7997 indoc! {"
7998 a.f(«one», two, «three») b
7999 a.f(«one», two, «three») b
8000 a.f(«one», two, «three») b
8001 "},
8002 );
8003
8004 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8005 assert(
8006 editor,
8007 cx,
8008 indoc! {"
8009 a.f(one, «two», three) b
8010 a.f(one, «two», three) b
8011 a.f(one, «two», three) b
8012 "},
8013 );
8014
8015 editor.move_to_prev_snippet_tabstop(window, cx);
8016 assert(
8017 editor,
8018 cx,
8019 indoc! {"
8020 a.f(«one», two, «three») b
8021 a.f(«one», two, «three») b
8022 a.f(«one», two, «three») b
8023 "},
8024 );
8025
8026 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8027 assert(
8028 editor,
8029 cx,
8030 indoc! {"
8031 a.f(one, «two», three) b
8032 a.f(one, «two», three) b
8033 a.f(one, «two», three) b
8034 "},
8035 );
8036 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8037 assert(
8038 editor,
8039 cx,
8040 indoc! {"
8041 a.f(one, two, three)ˇ b
8042 a.f(one, two, three)ˇ b
8043 a.f(one, two, three)ˇ b
8044 "},
8045 );
8046
8047 // As soon as the last tab stop is reached, snippet state is gone
8048 editor.move_to_prev_snippet_tabstop(window, cx);
8049 assert(
8050 editor,
8051 cx,
8052 indoc! {"
8053 a.f(one, two, three)ˇ b
8054 a.f(one, two, three)ˇ b
8055 a.f(one, two, three)ˇ b
8056 "},
8057 );
8058 });
8059}
8060
8061#[gpui::test]
8062async fn test_document_format_during_save(cx: &mut TestAppContext) {
8063 init_test(cx, |_| {});
8064
8065 let fs = FakeFs::new(cx.executor());
8066 fs.insert_file(path!("/file.rs"), Default::default()).await;
8067
8068 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8069
8070 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8071 language_registry.add(rust_lang());
8072 let mut fake_servers = language_registry.register_fake_lsp(
8073 "Rust",
8074 FakeLspAdapter {
8075 capabilities: lsp::ServerCapabilities {
8076 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8077 ..Default::default()
8078 },
8079 ..Default::default()
8080 },
8081 );
8082
8083 let buffer = project
8084 .update(cx, |project, cx| {
8085 project.open_local_buffer(path!("/file.rs"), cx)
8086 })
8087 .await
8088 .unwrap();
8089
8090 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8091 let (editor, cx) = cx.add_window_view(|window, cx| {
8092 build_editor_with_project(project.clone(), buffer, window, cx)
8093 });
8094 editor.update_in(cx, |editor, window, cx| {
8095 editor.set_text("one\ntwo\nthree\n", window, cx)
8096 });
8097 assert!(cx.read(|cx| editor.is_dirty(cx)));
8098
8099 cx.executor().start_waiting();
8100 let fake_server = fake_servers.next().await.unwrap();
8101
8102 {
8103 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8104 move |params, _| async move {
8105 assert_eq!(
8106 params.text_document.uri,
8107 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8108 );
8109 assert_eq!(params.options.tab_size, 4);
8110 Ok(Some(vec![lsp::TextEdit::new(
8111 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8112 ", ".to_string(),
8113 )]))
8114 },
8115 );
8116 let save = editor
8117 .update_in(cx, |editor, window, cx| {
8118 editor.save(true, project.clone(), window, cx)
8119 })
8120 .unwrap();
8121 cx.executor().start_waiting();
8122 save.await;
8123
8124 assert_eq!(
8125 editor.update(cx, |editor, cx| editor.text(cx)),
8126 "one, two\nthree\n"
8127 );
8128 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8129 }
8130
8131 {
8132 editor.update_in(cx, |editor, window, cx| {
8133 editor.set_text("one\ntwo\nthree\n", window, cx)
8134 });
8135 assert!(cx.read(|cx| editor.is_dirty(cx)));
8136
8137 // Ensure we can still save even if formatting hangs.
8138 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8139 move |params, _| async move {
8140 assert_eq!(
8141 params.text_document.uri,
8142 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8143 );
8144 futures::future::pending::<()>().await;
8145 unreachable!()
8146 },
8147 );
8148 let save = editor
8149 .update_in(cx, |editor, window, cx| {
8150 editor.save(true, project.clone(), window, cx)
8151 })
8152 .unwrap();
8153 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8154 cx.executor().start_waiting();
8155 save.await;
8156 assert_eq!(
8157 editor.update(cx, |editor, cx| editor.text(cx)),
8158 "one\ntwo\nthree\n"
8159 );
8160 }
8161
8162 // For non-dirty buffer, no formatting request should be sent
8163 {
8164 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8165
8166 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8167 panic!("Should not be invoked on non-dirty buffer");
8168 });
8169 let save = editor
8170 .update_in(cx, |editor, window, cx| {
8171 editor.save(true, project.clone(), window, cx)
8172 })
8173 .unwrap();
8174 cx.executor().start_waiting();
8175 save.await;
8176 }
8177
8178 // Set rust language override and assert overridden tabsize is sent to language server
8179 update_test_language_settings(cx, |settings| {
8180 settings.languages.insert(
8181 "Rust".into(),
8182 LanguageSettingsContent {
8183 tab_size: NonZeroU32::new(8),
8184 ..Default::default()
8185 },
8186 );
8187 });
8188
8189 {
8190 editor.update_in(cx, |editor, window, cx| {
8191 editor.set_text("somehting_new\n", window, cx)
8192 });
8193 assert!(cx.read(|cx| editor.is_dirty(cx)));
8194 let _formatting_request_signal = fake_server
8195 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8196 assert_eq!(
8197 params.text_document.uri,
8198 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8199 );
8200 assert_eq!(params.options.tab_size, 8);
8201 Ok(Some(vec![]))
8202 });
8203 let save = editor
8204 .update_in(cx, |editor, window, cx| {
8205 editor.save(true, project.clone(), window, cx)
8206 })
8207 .unwrap();
8208 cx.executor().start_waiting();
8209 save.await;
8210 }
8211}
8212
8213#[gpui::test]
8214async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8215 init_test(cx, |_| {});
8216
8217 let cols = 4;
8218 let rows = 10;
8219 let sample_text_1 = sample_text(rows, cols, 'a');
8220 assert_eq!(
8221 sample_text_1,
8222 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8223 );
8224 let sample_text_2 = sample_text(rows, cols, 'l');
8225 assert_eq!(
8226 sample_text_2,
8227 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8228 );
8229 let sample_text_3 = sample_text(rows, cols, 'v');
8230 assert_eq!(
8231 sample_text_3,
8232 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8233 );
8234
8235 let fs = FakeFs::new(cx.executor());
8236 fs.insert_tree(
8237 path!("/a"),
8238 json!({
8239 "main.rs": sample_text_1,
8240 "other.rs": sample_text_2,
8241 "lib.rs": sample_text_3,
8242 }),
8243 )
8244 .await;
8245
8246 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8247 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8248 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8249
8250 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8251 language_registry.add(rust_lang());
8252 let mut fake_servers = language_registry.register_fake_lsp(
8253 "Rust",
8254 FakeLspAdapter {
8255 capabilities: lsp::ServerCapabilities {
8256 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8257 ..Default::default()
8258 },
8259 ..Default::default()
8260 },
8261 );
8262
8263 let worktree = project.update(cx, |project, cx| {
8264 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8265 assert_eq!(worktrees.len(), 1);
8266 worktrees.pop().unwrap()
8267 });
8268 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8269
8270 let buffer_1 = project
8271 .update(cx, |project, cx| {
8272 project.open_buffer((worktree_id, "main.rs"), cx)
8273 })
8274 .await
8275 .unwrap();
8276 let buffer_2 = project
8277 .update(cx, |project, cx| {
8278 project.open_buffer((worktree_id, "other.rs"), cx)
8279 })
8280 .await
8281 .unwrap();
8282 let buffer_3 = project
8283 .update(cx, |project, cx| {
8284 project.open_buffer((worktree_id, "lib.rs"), cx)
8285 })
8286 .await
8287 .unwrap();
8288
8289 let multi_buffer = cx.new(|cx| {
8290 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8291 multi_buffer.push_excerpts(
8292 buffer_1.clone(),
8293 [
8294 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8295 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8296 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8297 ],
8298 cx,
8299 );
8300 multi_buffer.push_excerpts(
8301 buffer_2.clone(),
8302 [
8303 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8304 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8305 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8306 ],
8307 cx,
8308 );
8309 multi_buffer.push_excerpts(
8310 buffer_3.clone(),
8311 [
8312 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8313 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8314 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8315 ],
8316 cx,
8317 );
8318 multi_buffer
8319 });
8320 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8321 Editor::new(
8322 EditorMode::full(),
8323 multi_buffer,
8324 Some(project.clone()),
8325 window,
8326 cx,
8327 )
8328 });
8329
8330 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8331 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8332 s.select_ranges(Some(1..2))
8333 });
8334 editor.insert("|one|two|three|", window, cx);
8335 });
8336 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8337 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8338 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8339 s.select_ranges(Some(60..70))
8340 });
8341 editor.insert("|four|five|six|", window, cx);
8342 });
8343 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8344
8345 // First two buffers should be edited, but not the third one.
8346 assert_eq!(
8347 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8348 "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}",
8349 );
8350 buffer_1.update(cx, |buffer, _| {
8351 assert!(buffer.is_dirty());
8352 assert_eq!(
8353 buffer.text(),
8354 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8355 )
8356 });
8357 buffer_2.update(cx, |buffer, _| {
8358 assert!(buffer.is_dirty());
8359 assert_eq!(
8360 buffer.text(),
8361 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8362 )
8363 });
8364 buffer_3.update(cx, |buffer, _| {
8365 assert!(!buffer.is_dirty());
8366 assert_eq!(buffer.text(), sample_text_3,)
8367 });
8368 cx.executor().run_until_parked();
8369
8370 cx.executor().start_waiting();
8371 let save = multi_buffer_editor
8372 .update_in(cx, |editor, window, cx| {
8373 editor.save(true, project.clone(), window, cx)
8374 })
8375 .unwrap();
8376
8377 let fake_server = fake_servers.next().await.unwrap();
8378 fake_server
8379 .server
8380 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8381 Ok(Some(vec![lsp::TextEdit::new(
8382 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8383 format!("[{} formatted]", params.text_document.uri),
8384 )]))
8385 })
8386 .detach();
8387 save.await;
8388
8389 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8390 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8391 assert_eq!(
8392 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8393 uri!(
8394 "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}"
8395 ),
8396 );
8397 buffer_1.update(cx, |buffer, _| {
8398 assert!(!buffer.is_dirty());
8399 assert_eq!(
8400 buffer.text(),
8401 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8402 )
8403 });
8404 buffer_2.update(cx, |buffer, _| {
8405 assert!(!buffer.is_dirty());
8406 assert_eq!(
8407 buffer.text(),
8408 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8409 )
8410 });
8411 buffer_3.update(cx, |buffer, _| {
8412 assert!(!buffer.is_dirty());
8413 assert_eq!(buffer.text(), sample_text_3,)
8414 });
8415}
8416
8417#[gpui::test]
8418async fn test_range_format_during_save(cx: &mut TestAppContext) {
8419 init_test(cx, |_| {});
8420
8421 let fs = FakeFs::new(cx.executor());
8422 fs.insert_file(path!("/file.rs"), Default::default()).await;
8423
8424 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8425
8426 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8427 language_registry.add(rust_lang());
8428 let mut fake_servers = language_registry.register_fake_lsp(
8429 "Rust",
8430 FakeLspAdapter {
8431 capabilities: lsp::ServerCapabilities {
8432 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8433 ..Default::default()
8434 },
8435 ..Default::default()
8436 },
8437 );
8438
8439 let buffer = project
8440 .update(cx, |project, cx| {
8441 project.open_local_buffer(path!("/file.rs"), cx)
8442 })
8443 .await
8444 .unwrap();
8445
8446 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8447 let (editor, cx) = cx.add_window_view(|window, cx| {
8448 build_editor_with_project(project.clone(), buffer, window, cx)
8449 });
8450 editor.update_in(cx, |editor, window, cx| {
8451 editor.set_text("one\ntwo\nthree\n", window, cx)
8452 });
8453 assert!(cx.read(|cx| editor.is_dirty(cx)));
8454
8455 cx.executor().start_waiting();
8456 let fake_server = fake_servers.next().await.unwrap();
8457
8458 let save = editor
8459 .update_in(cx, |editor, window, cx| {
8460 editor.save(true, project.clone(), window, cx)
8461 })
8462 .unwrap();
8463 fake_server
8464 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8465 assert_eq!(
8466 params.text_document.uri,
8467 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8468 );
8469 assert_eq!(params.options.tab_size, 4);
8470 Ok(Some(vec![lsp::TextEdit::new(
8471 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8472 ", ".to_string(),
8473 )]))
8474 })
8475 .next()
8476 .await;
8477 cx.executor().start_waiting();
8478 save.await;
8479 assert_eq!(
8480 editor.update(cx, |editor, cx| editor.text(cx)),
8481 "one, two\nthree\n"
8482 );
8483 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8484
8485 editor.update_in(cx, |editor, window, cx| {
8486 editor.set_text("one\ntwo\nthree\n", window, cx)
8487 });
8488 assert!(cx.read(|cx| editor.is_dirty(cx)));
8489
8490 // Ensure we can still save even if formatting hangs.
8491 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8492 move |params, _| async move {
8493 assert_eq!(
8494 params.text_document.uri,
8495 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8496 );
8497 futures::future::pending::<()>().await;
8498 unreachable!()
8499 },
8500 );
8501 let save = editor
8502 .update_in(cx, |editor, window, cx| {
8503 editor.save(true, project.clone(), window, cx)
8504 })
8505 .unwrap();
8506 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8507 cx.executor().start_waiting();
8508 save.await;
8509 assert_eq!(
8510 editor.update(cx, |editor, cx| editor.text(cx)),
8511 "one\ntwo\nthree\n"
8512 );
8513 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8514
8515 // For non-dirty buffer, no formatting request should be sent
8516 let save = editor
8517 .update_in(cx, |editor, window, cx| {
8518 editor.save(true, project.clone(), window, cx)
8519 })
8520 .unwrap();
8521 let _pending_format_request = fake_server
8522 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8523 panic!("Should not be invoked on non-dirty buffer");
8524 })
8525 .next();
8526 cx.executor().start_waiting();
8527 save.await;
8528
8529 // Set Rust language override and assert overridden tabsize is sent to language server
8530 update_test_language_settings(cx, |settings| {
8531 settings.languages.insert(
8532 "Rust".into(),
8533 LanguageSettingsContent {
8534 tab_size: NonZeroU32::new(8),
8535 ..Default::default()
8536 },
8537 );
8538 });
8539
8540 editor.update_in(cx, |editor, window, cx| {
8541 editor.set_text("somehting_new\n", window, cx)
8542 });
8543 assert!(cx.read(|cx| editor.is_dirty(cx)));
8544 let save = editor
8545 .update_in(cx, |editor, window, cx| {
8546 editor.save(true, project.clone(), window, cx)
8547 })
8548 .unwrap();
8549 fake_server
8550 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8551 assert_eq!(
8552 params.text_document.uri,
8553 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8554 );
8555 assert_eq!(params.options.tab_size, 8);
8556 Ok(Some(vec![]))
8557 })
8558 .next()
8559 .await;
8560 cx.executor().start_waiting();
8561 save.await;
8562}
8563
8564#[gpui::test]
8565async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8566 init_test(cx, |settings| {
8567 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8568 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8569 ))
8570 });
8571
8572 let fs = FakeFs::new(cx.executor());
8573 fs.insert_file(path!("/file.rs"), Default::default()).await;
8574
8575 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8576
8577 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8578 language_registry.add(Arc::new(Language::new(
8579 LanguageConfig {
8580 name: "Rust".into(),
8581 matcher: LanguageMatcher {
8582 path_suffixes: vec!["rs".to_string()],
8583 ..Default::default()
8584 },
8585 ..LanguageConfig::default()
8586 },
8587 Some(tree_sitter_rust::LANGUAGE.into()),
8588 )));
8589 update_test_language_settings(cx, |settings| {
8590 // Enable Prettier formatting for the same buffer, and ensure
8591 // LSP is called instead of Prettier.
8592 settings.defaults.prettier = Some(PrettierSettings {
8593 allowed: true,
8594 ..PrettierSettings::default()
8595 });
8596 });
8597 let mut fake_servers = language_registry.register_fake_lsp(
8598 "Rust",
8599 FakeLspAdapter {
8600 capabilities: lsp::ServerCapabilities {
8601 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8602 ..Default::default()
8603 },
8604 ..Default::default()
8605 },
8606 );
8607
8608 let buffer = project
8609 .update(cx, |project, cx| {
8610 project.open_local_buffer(path!("/file.rs"), cx)
8611 })
8612 .await
8613 .unwrap();
8614
8615 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8616 let (editor, cx) = cx.add_window_view(|window, cx| {
8617 build_editor_with_project(project.clone(), buffer, window, cx)
8618 });
8619 editor.update_in(cx, |editor, window, cx| {
8620 editor.set_text("one\ntwo\nthree\n", window, cx)
8621 });
8622
8623 cx.executor().start_waiting();
8624 let fake_server = fake_servers.next().await.unwrap();
8625
8626 let format = editor
8627 .update_in(cx, |editor, window, cx| {
8628 editor.perform_format(
8629 project.clone(),
8630 FormatTrigger::Manual,
8631 FormatTarget::Buffers,
8632 window,
8633 cx,
8634 )
8635 })
8636 .unwrap();
8637 fake_server
8638 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8639 assert_eq!(
8640 params.text_document.uri,
8641 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8642 );
8643 assert_eq!(params.options.tab_size, 4);
8644 Ok(Some(vec![lsp::TextEdit::new(
8645 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8646 ", ".to_string(),
8647 )]))
8648 })
8649 .next()
8650 .await;
8651 cx.executor().start_waiting();
8652 format.await;
8653 assert_eq!(
8654 editor.update(cx, |editor, cx| editor.text(cx)),
8655 "one, two\nthree\n"
8656 );
8657
8658 editor.update_in(cx, |editor, window, cx| {
8659 editor.set_text("one\ntwo\nthree\n", window, cx)
8660 });
8661 // Ensure we don't lock if formatting hangs.
8662 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8663 move |params, _| async move {
8664 assert_eq!(
8665 params.text_document.uri,
8666 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8667 );
8668 futures::future::pending::<()>().await;
8669 unreachable!()
8670 },
8671 );
8672 let format = editor
8673 .update_in(cx, |editor, window, cx| {
8674 editor.perform_format(
8675 project,
8676 FormatTrigger::Manual,
8677 FormatTarget::Buffers,
8678 window,
8679 cx,
8680 )
8681 })
8682 .unwrap();
8683 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8684 cx.executor().start_waiting();
8685 format.await;
8686 assert_eq!(
8687 editor.update(cx, |editor, cx| editor.text(cx)),
8688 "one\ntwo\nthree\n"
8689 );
8690}
8691
8692#[gpui::test]
8693async fn test_multiple_formatters(cx: &mut TestAppContext) {
8694 init_test(cx, |settings| {
8695 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8696 settings.defaults.formatter =
8697 Some(language_settings::SelectedFormatter::List(FormatterList(
8698 vec![
8699 Formatter::LanguageServer { name: None },
8700 Formatter::CodeActions(
8701 [
8702 ("code-action-1".into(), true),
8703 ("code-action-2".into(), true),
8704 ]
8705 .into_iter()
8706 .collect(),
8707 ),
8708 ]
8709 .into(),
8710 )))
8711 });
8712
8713 let fs = FakeFs::new(cx.executor());
8714 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8715 .await;
8716
8717 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8718 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8719 language_registry.add(rust_lang());
8720
8721 let mut fake_servers = language_registry.register_fake_lsp(
8722 "Rust",
8723 FakeLspAdapter {
8724 capabilities: lsp::ServerCapabilities {
8725 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8726 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8727 commands: vec!["the-command-for-code-action-1".into()],
8728 ..Default::default()
8729 }),
8730 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8731 ..Default::default()
8732 },
8733 ..Default::default()
8734 },
8735 );
8736
8737 let buffer = project
8738 .update(cx, |project, cx| {
8739 project.open_local_buffer(path!("/file.rs"), cx)
8740 })
8741 .await
8742 .unwrap();
8743
8744 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8745 let (editor, cx) = cx.add_window_view(|window, cx| {
8746 build_editor_with_project(project.clone(), buffer, window, cx)
8747 });
8748
8749 cx.executor().start_waiting();
8750
8751 let fake_server = fake_servers.next().await.unwrap();
8752 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8753 move |_params, _| async move {
8754 Ok(Some(vec![lsp::TextEdit::new(
8755 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8756 "applied-formatting\n".to_string(),
8757 )]))
8758 },
8759 );
8760 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8761 move |params, _| async move {
8762 assert_eq!(
8763 params.context.only,
8764 Some(vec!["code-action-1".into(), "code-action-2".into()])
8765 );
8766 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8767 Ok(Some(vec![
8768 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8769 kind: Some("code-action-1".into()),
8770 edit: Some(lsp::WorkspaceEdit::new(
8771 [(
8772 uri.clone(),
8773 vec![lsp::TextEdit::new(
8774 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8775 "applied-code-action-1-edit\n".to_string(),
8776 )],
8777 )]
8778 .into_iter()
8779 .collect(),
8780 )),
8781 command: Some(lsp::Command {
8782 command: "the-command-for-code-action-1".into(),
8783 ..Default::default()
8784 }),
8785 ..Default::default()
8786 }),
8787 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8788 kind: Some("code-action-2".into()),
8789 edit: Some(lsp::WorkspaceEdit::new(
8790 [(
8791 uri.clone(),
8792 vec![lsp::TextEdit::new(
8793 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8794 "applied-code-action-2-edit\n".to_string(),
8795 )],
8796 )]
8797 .into_iter()
8798 .collect(),
8799 )),
8800 ..Default::default()
8801 }),
8802 ]))
8803 },
8804 );
8805
8806 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8807 move |params, _| async move { Ok(params) }
8808 });
8809
8810 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8811 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8812 let fake = fake_server.clone();
8813 let lock = command_lock.clone();
8814 move |params, _| {
8815 assert_eq!(params.command, "the-command-for-code-action-1");
8816 let fake = fake.clone();
8817 let lock = lock.clone();
8818 async move {
8819 lock.lock().await;
8820 fake.server
8821 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8822 label: None,
8823 edit: lsp::WorkspaceEdit {
8824 changes: Some(
8825 [(
8826 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8827 vec![lsp::TextEdit {
8828 range: lsp::Range::new(
8829 lsp::Position::new(0, 0),
8830 lsp::Position::new(0, 0),
8831 ),
8832 new_text: "applied-code-action-1-command\n".into(),
8833 }],
8834 )]
8835 .into_iter()
8836 .collect(),
8837 ),
8838 ..Default::default()
8839 },
8840 })
8841 .await
8842 .unwrap();
8843 Ok(Some(json!(null)))
8844 }
8845 }
8846 });
8847
8848 cx.executor().start_waiting();
8849 editor
8850 .update_in(cx, |editor, window, cx| {
8851 editor.perform_format(
8852 project.clone(),
8853 FormatTrigger::Manual,
8854 FormatTarget::Buffers,
8855 window,
8856 cx,
8857 )
8858 })
8859 .unwrap()
8860 .await;
8861 editor.update(cx, |editor, cx| {
8862 assert_eq!(
8863 editor.text(cx),
8864 r#"
8865 applied-code-action-2-edit
8866 applied-code-action-1-command
8867 applied-code-action-1-edit
8868 applied-formatting
8869 one
8870 two
8871 three
8872 "#
8873 .unindent()
8874 );
8875 });
8876
8877 editor.update_in(cx, |editor, window, cx| {
8878 editor.undo(&Default::default(), window, cx);
8879 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8880 });
8881
8882 // Perform a manual edit while waiting for an LSP command
8883 // that's being run as part of a formatting code action.
8884 let lock_guard = command_lock.lock().await;
8885 let format = editor
8886 .update_in(cx, |editor, window, cx| {
8887 editor.perform_format(
8888 project.clone(),
8889 FormatTrigger::Manual,
8890 FormatTarget::Buffers,
8891 window,
8892 cx,
8893 )
8894 })
8895 .unwrap();
8896 cx.run_until_parked();
8897 editor.update(cx, |editor, cx| {
8898 assert_eq!(
8899 editor.text(cx),
8900 r#"
8901 applied-code-action-1-edit
8902 applied-formatting
8903 one
8904 two
8905 three
8906 "#
8907 .unindent()
8908 );
8909
8910 editor.buffer.update(cx, |buffer, cx| {
8911 let ix = buffer.len(cx);
8912 buffer.edit([(ix..ix, "edited\n")], None, cx);
8913 });
8914 });
8915
8916 // Allow the LSP command to proceed. Because the buffer was edited,
8917 // the second code action will not be run.
8918 drop(lock_guard);
8919 format.await;
8920 editor.update_in(cx, |editor, window, cx| {
8921 assert_eq!(
8922 editor.text(cx),
8923 r#"
8924 applied-code-action-1-command
8925 applied-code-action-1-edit
8926 applied-formatting
8927 one
8928 two
8929 three
8930 edited
8931 "#
8932 .unindent()
8933 );
8934
8935 // The manual edit is undone first, because it is the last thing the user did
8936 // (even though the command completed afterwards).
8937 editor.undo(&Default::default(), window, cx);
8938 assert_eq!(
8939 editor.text(cx),
8940 r#"
8941 applied-code-action-1-command
8942 applied-code-action-1-edit
8943 applied-formatting
8944 one
8945 two
8946 three
8947 "#
8948 .unindent()
8949 );
8950
8951 // All the formatting (including the command, which completed after the manual edit)
8952 // is undone together.
8953 editor.undo(&Default::default(), window, cx);
8954 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8955 });
8956}
8957
8958#[gpui::test]
8959async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8960 init_test(cx, |settings| {
8961 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8962 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8963 ))
8964 });
8965
8966 let fs = FakeFs::new(cx.executor());
8967 fs.insert_file(path!("/file.ts"), Default::default()).await;
8968
8969 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8970
8971 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8972 language_registry.add(Arc::new(Language::new(
8973 LanguageConfig {
8974 name: "TypeScript".into(),
8975 matcher: LanguageMatcher {
8976 path_suffixes: vec!["ts".to_string()],
8977 ..Default::default()
8978 },
8979 ..LanguageConfig::default()
8980 },
8981 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8982 )));
8983 update_test_language_settings(cx, |settings| {
8984 settings.defaults.prettier = Some(PrettierSettings {
8985 allowed: true,
8986 ..PrettierSettings::default()
8987 });
8988 });
8989 let mut fake_servers = language_registry.register_fake_lsp(
8990 "TypeScript",
8991 FakeLspAdapter {
8992 capabilities: lsp::ServerCapabilities {
8993 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8994 ..Default::default()
8995 },
8996 ..Default::default()
8997 },
8998 );
8999
9000 let buffer = project
9001 .update(cx, |project, cx| {
9002 project.open_local_buffer(path!("/file.ts"), cx)
9003 })
9004 .await
9005 .unwrap();
9006
9007 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9008 let (editor, cx) = cx.add_window_view(|window, cx| {
9009 build_editor_with_project(project.clone(), buffer, window, cx)
9010 });
9011 editor.update_in(cx, |editor, window, cx| {
9012 editor.set_text(
9013 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9014 window,
9015 cx,
9016 )
9017 });
9018
9019 cx.executor().start_waiting();
9020 let fake_server = fake_servers.next().await.unwrap();
9021
9022 let format = editor
9023 .update_in(cx, |editor, window, cx| {
9024 editor.perform_code_action_kind(
9025 project.clone(),
9026 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9027 window,
9028 cx,
9029 )
9030 })
9031 .unwrap();
9032 fake_server
9033 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9034 assert_eq!(
9035 params.text_document.uri,
9036 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9037 );
9038 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9039 lsp::CodeAction {
9040 title: "Organize Imports".to_string(),
9041 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9042 edit: Some(lsp::WorkspaceEdit {
9043 changes: Some(
9044 [(
9045 params.text_document.uri.clone(),
9046 vec![lsp::TextEdit::new(
9047 lsp::Range::new(
9048 lsp::Position::new(1, 0),
9049 lsp::Position::new(2, 0),
9050 ),
9051 "".to_string(),
9052 )],
9053 )]
9054 .into_iter()
9055 .collect(),
9056 ),
9057 ..Default::default()
9058 }),
9059 ..Default::default()
9060 },
9061 )]))
9062 })
9063 .next()
9064 .await;
9065 cx.executor().start_waiting();
9066 format.await;
9067 assert_eq!(
9068 editor.update(cx, |editor, cx| editor.text(cx)),
9069 "import { a } from 'module';\n\nconst x = a;\n"
9070 );
9071
9072 editor.update_in(cx, |editor, window, cx| {
9073 editor.set_text(
9074 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9075 window,
9076 cx,
9077 )
9078 });
9079 // Ensure we don't lock if code action hangs.
9080 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9081 move |params, _| async move {
9082 assert_eq!(
9083 params.text_document.uri,
9084 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9085 );
9086 futures::future::pending::<()>().await;
9087 unreachable!()
9088 },
9089 );
9090 let format = editor
9091 .update_in(cx, |editor, window, cx| {
9092 editor.perform_code_action_kind(
9093 project,
9094 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9095 window,
9096 cx,
9097 )
9098 })
9099 .unwrap();
9100 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9101 cx.executor().start_waiting();
9102 format.await;
9103 assert_eq!(
9104 editor.update(cx, |editor, cx| editor.text(cx)),
9105 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9106 );
9107}
9108
9109#[gpui::test]
9110async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9111 init_test(cx, |_| {});
9112
9113 let mut cx = EditorLspTestContext::new_rust(
9114 lsp::ServerCapabilities {
9115 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9116 ..Default::default()
9117 },
9118 cx,
9119 )
9120 .await;
9121
9122 cx.set_state(indoc! {"
9123 one.twoˇ
9124 "});
9125
9126 // The format request takes a long time. When it completes, it inserts
9127 // a newline and an indent before the `.`
9128 cx.lsp
9129 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9130 let executor = cx.background_executor().clone();
9131 async move {
9132 executor.timer(Duration::from_millis(100)).await;
9133 Ok(Some(vec![lsp::TextEdit {
9134 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9135 new_text: "\n ".into(),
9136 }]))
9137 }
9138 });
9139
9140 // Submit a format request.
9141 let format_1 = cx
9142 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9143 .unwrap();
9144 cx.executor().run_until_parked();
9145
9146 // Submit a second format request.
9147 let format_2 = cx
9148 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9149 .unwrap();
9150 cx.executor().run_until_parked();
9151
9152 // Wait for both format requests to complete
9153 cx.executor().advance_clock(Duration::from_millis(200));
9154 cx.executor().start_waiting();
9155 format_1.await.unwrap();
9156 cx.executor().start_waiting();
9157 format_2.await.unwrap();
9158
9159 // The formatting edits only happens once.
9160 cx.assert_editor_state(indoc! {"
9161 one
9162 .twoˇ
9163 "});
9164}
9165
9166#[gpui::test]
9167async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9168 init_test(cx, |settings| {
9169 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9170 });
9171
9172 let mut cx = EditorLspTestContext::new_rust(
9173 lsp::ServerCapabilities {
9174 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9175 ..Default::default()
9176 },
9177 cx,
9178 )
9179 .await;
9180
9181 // Set up a buffer white some trailing whitespace and no trailing newline.
9182 cx.set_state(
9183 &[
9184 "one ", //
9185 "twoˇ", //
9186 "three ", //
9187 "four", //
9188 ]
9189 .join("\n"),
9190 );
9191
9192 // Submit a format request.
9193 let format = cx
9194 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9195 .unwrap();
9196
9197 // Record which buffer changes have been sent to the language server
9198 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9199 cx.lsp
9200 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9201 let buffer_changes = buffer_changes.clone();
9202 move |params, _| {
9203 buffer_changes.lock().extend(
9204 params
9205 .content_changes
9206 .into_iter()
9207 .map(|e| (e.range.unwrap(), e.text)),
9208 );
9209 }
9210 });
9211
9212 // Handle formatting requests to the language server.
9213 cx.lsp
9214 .set_request_handler::<lsp::request::Formatting, _, _>({
9215 let buffer_changes = buffer_changes.clone();
9216 move |_, _| {
9217 // When formatting is requested, trailing whitespace has already been stripped,
9218 // and the trailing newline has already been added.
9219 assert_eq!(
9220 &buffer_changes.lock()[1..],
9221 &[
9222 (
9223 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9224 "".into()
9225 ),
9226 (
9227 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9228 "".into()
9229 ),
9230 (
9231 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9232 "\n".into()
9233 ),
9234 ]
9235 );
9236
9237 // Insert blank lines between each line of the buffer.
9238 async move {
9239 Ok(Some(vec![
9240 lsp::TextEdit {
9241 range: lsp::Range::new(
9242 lsp::Position::new(1, 0),
9243 lsp::Position::new(1, 0),
9244 ),
9245 new_text: "\n".into(),
9246 },
9247 lsp::TextEdit {
9248 range: lsp::Range::new(
9249 lsp::Position::new(2, 0),
9250 lsp::Position::new(2, 0),
9251 ),
9252 new_text: "\n".into(),
9253 },
9254 ]))
9255 }
9256 }
9257 });
9258
9259 // After formatting the buffer, the trailing whitespace is stripped,
9260 // a newline is appended, and the edits provided by the language server
9261 // have been applied.
9262 format.await.unwrap();
9263 cx.assert_editor_state(
9264 &[
9265 "one", //
9266 "", //
9267 "twoˇ", //
9268 "", //
9269 "three", //
9270 "four", //
9271 "", //
9272 ]
9273 .join("\n"),
9274 );
9275
9276 // Undoing the formatting undoes the trailing whitespace removal, the
9277 // trailing newline, and the LSP edits.
9278 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9279 cx.assert_editor_state(
9280 &[
9281 "one ", //
9282 "twoˇ", //
9283 "three ", //
9284 "four", //
9285 ]
9286 .join("\n"),
9287 );
9288}
9289
9290#[gpui::test]
9291async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9292 cx: &mut TestAppContext,
9293) {
9294 init_test(cx, |_| {});
9295
9296 cx.update(|cx| {
9297 cx.update_global::<SettingsStore, _>(|settings, cx| {
9298 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9299 settings.auto_signature_help = Some(true);
9300 });
9301 });
9302 });
9303
9304 let mut cx = EditorLspTestContext::new_rust(
9305 lsp::ServerCapabilities {
9306 signature_help_provider: Some(lsp::SignatureHelpOptions {
9307 ..Default::default()
9308 }),
9309 ..Default::default()
9310 },
9311 cx,
9312 )
9313 .await;
9314
9315 let language = Language::new(
9316 LanguageConfig {
9317 name: "Rust".into(),
9318 brackets: BracketPairConfig {
9319 pairs: vec![
9320 BracketPair {
9321 start: "{".to_string(),
9322 end: "}".to_string(),
9323 close: true,
9324 surround: true,
9325 newline: true,
9326 },
9327 BracketPair {
9328 start: "(".to_string(),
9329 end: ")".to_string(),
9330 close: true,
9331 surround: true,
9332 newline: true,
9333 },
9334 BracketPair {
9335 start: "/*".to_string(),
9336 end: " */".to_string(),
9337 close: true,
9338 surround: true,
9339 newline: true,
9340 },
9341 BracketPair {
9342 start: "[".to_string(),
9343 end: "]".to_string(),
9344 close: false,
9345 surround: false,
9346 newline: true,
9347 },
9348 BracketPair {
9349 start: "\"".to_string(),
9350 end: "\"".to_string(),
9351 close: true,
9352 surround: true,
9353 newline: false,
9354 },
9355 BracketPair {
9356 start: "<".to_string(),
9357 end: ">".to_string(),
9358 close: false,
9359 surround: true,
9360 newline: true,
9361 },
9362 ],
9363 ..Default::default()
9364 },
9365 autoclose_before: "})]".to_string(),
9366 ..Default::default()
9367 },
9368 Some(tree_sitter_rust::LANGUAGE.into()),
9369 );
9370 let language = Arc::new(language);
9371
9372 cx.language_registry().add(language.clone());
9373 cx.update_buffer(|buffer, cx| {
9374 buffer.set_language(Some(language), cx);
9375 });
9376
9377 cx.set_state(
9378 &r#"
9379 fn main() {
9380 sampleˇ
9381 }
9382 "#
9383 .unindent(),
9384 );
9385
9386 cx.update_editor(|editor, window, cx| {
9387 editor.handle_input("(", window, cx);
9388 });
9389 cx.assert_editor_state(
9390 &"
9391 fn main() {
9392 sample(ˇ)
9393 }
9394 "
9395 .unindent(),
9396 );
9397
9398 let mocked_response = lsp::SignatureHelp {
9399 signatures: vec![lsp::SignatureInformation {
9400 label: "fn sample(param1: u8, param2: u8)".to_string(),
9401 documentation: None,
9402 parameters: Some(vec![
9403 lsp::ParameterInformation {
9404 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9405 documentation: None,
9406 },
9407 lsp::ParameterInformation {
9408 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9409 documentation: None,
9410 },
9411 ]),
9412 active_parameter: None,
9413 }],
9414 active_signature: Some(0),
9415 active_parameter: Some(0),
9416 };
9417 handle_signature_help_request(&mut cx, mocked_response).await;
9418
9419 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9420 .await;
9421
9422 cx.editor(|editor, _, _| {
9423 let signature_help_state = editor.signature_help_state.popover().cloned();
9424 assert_eq!(
9425 signature_help_state.unwrap().label,
9426 "param1: u8, param2: u8"
9427 );
9428 });
9429}
9430
9431#[gpui::test]
9432async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9433 init_test(cx, |_| {});
9434
9435 cx.update(|cx| {
9436 cx.update_global::<SettingsStore, _>(|settings, cx| {
9437 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9438 settings.auto_signature_help = Some(false);
9439 settings.show_signature_help_after_edits = Some(false);
9440 });
9441 });
9442 });
9443
9444 let mut cx = EditorLspTestContext::new_rust(
9445 lsp::ServerCapabilities {
9446 signature_help_provider: Some(lsp::SignatureHelpOptions {
9447 ..Default::default()
9448 }),
9449 ..Default::default()
9450 },
9451 cx,
9452 )
9453 .await;
9454
9455 let language = Language::new(
9456 LanguageConfig {
9457 name: "Rust".into(),
9458 brackets: BracketPairConfig {
9459 pairs: vec![
9460 BracketPair {
9461 start: "{".to_string(),
9462 end: "}".to_string(),
9463 close: true,
9464 surround: true,
9465 newline: true,
9466 },
9467 BracketPair {
9468 start: "(".to_string(),
9469 end: ")".to_string(),
9470 close: true,
9471 surround: true,
9472 newline: true,
9473 },
9474 BracketPair {
9475 start: "/*".to_string(),
9476 end: " */".to_string(),
9477 close: true,
9478 surround: true,
9479 newline: true,
9480 },
9481 BracketPair {
9482 start: "[".to_string(),
9483 end: "]".to_string(),
9484 close: false,
9485 surround: false,
9486 newline: true,
9487 },
9488 BracketPair {
9489 start: "\"".to_string(),
9490 end: "\"".to_string(),
9491 close: true,
9492 surround: true,
9493 newline: false,
9494 },
9495 BracketPair {
9496 start: "<".to_string(),
9497 end: ">".to_string(),
9498 close: false,
9499 surround: true,
9500 newline: true,
9501 },
9502 ],
9503 ..Default::default()
9504 },
9505 autoclose_before: "})]".to_string(),
9506 ..Default::default()
9507 },
9508 Some(tree_sitter_rust::LANGUAGE.into()),
9509 );
9510 let language = Arc::new(language);
9511
9512 cx.language_registry().add(language.clone());
9513 cx.update_buffer(|buffer, cx| {
9514 buffer.set_language(Some(language), cx);
9515 });
9516
9517 // Ensure that signature_help is not called when no signature help is enabled.
9518 cx.set_state(
9519 &r#"
9520 fn main() {
9521 sampleˇ
9522 }
9523 "#
9524 .unindent(),
9525 );
9526 cx.update_editor(|editor, window, cx| {
9527 editor.handle_input("(", window, cx);
9528 });
9529 cx.assert_editor_state(
9530 &"
9531 fn main() {
9532 sample(ˇ)
9533 }
9534 "
9535 .unindent(),
9536 );
9537 cx.editor(|editor, _, _| {
9538 assert!(editor.signature_help_state.task().is_none());
9539 });
9540
9541 let mocked_response = lsp::SignatureHelp {
9542 signatures: vec![lsp::SignatureInformation {
9543 label: "fn sample(param1: u8, param2: u8)".to_string(),
9544 documentation: None,
9545 parameters: Some(vec![
9546 lsp::ParameterInformation {
9547 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9548 documentation: None,
9549 },
9550 lsp::ParameterInformation {
9551 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9552 documentation: None,
9553 },
9554 ]),
9555 active_parameter: None,
9556 }],
9557 active_signature: Some(0),
9558 active_parameter: Some(0),
9559 };
9560
9561 // Ensure that signature_help is called when enabled afte edits
9562 cx.update(|_, cx| {
9563 cx.update_global::<SettingsStore, _>(|settings, cx| {
9564 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9565 settings.auto_signature_help = Some(false);
9566 settings.show_signature_help_after_edits = Some(true);
9567 });
9568 });
9569 });
9570 cx.set_state(
9571 &r#"
9572 fn main() {
9573 sampleˇ
9574 }
9575 "#
9576 .unindent(),
9577 );
9578 cx.update_editor(|editor, window, cx| {
9579 editor.handle_input("(", window, cx);
9580 });
9581 cx.assert_editor_state(
9582 &"
9583 fn main() {
9584 sample(ˇ)
9585 }
9586 "
9587 .unindent(),
9588 );
9589 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9590 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9591 .await;
9592 cx.update_editor(|editor, _, _| {
9593 let signature_help_state = editor.signature_help_state.popover().cloned();
9594 assert!(signature_help_state.is_some());
9595 assert_eq!(
9596 signature_help_state.unwrap().label,
9597 "param1: u8, param2: u8"
9598 );
9599 editor.signature_help_state = SignatureHelpState::default();
9600 });
9601
9602 // Ensure that signature_help is called when auto signature help override is enabled
9603 cx.update(|_, cx| {
9604 cx.update_global::<SettingsStore, _>(|settings, cx| {
9605 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9606 settings.auto_signature_help = Some(true);
9607 settings.show_signature_help_after_edits = Some(false);
9608 });
9609 });
9610 });
9611 cx.set_state(
9612 &r#"
9613 fn main() {
9614 sampleˇ
9615 }
9616 "#
9617 .unindent(),
9618 );
9619 cx.update_editor(|editor, window, cx| {
9620 editor.handle_input("(", window, cx);
9621 });
9622 cx.assert_editor_state(
9623 &"
9624 fn main() {
9625 sample(ˇ)
9626 }
9627 "
9628 .unindent(),
9629 );
9630 handle_signature_help_request(&mut cx, mocked_response).await;
9631 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9632 .await;
9633 cx.editor(|editor, _, _| {
9634 let signature_help_state = editor.signature_help_state.popover().cloned();
9635 assert!(signature_help_state.is_some());
9636 assert_eq!(
9637 signature_help_state.unwrap().label,
9638 "param1: u8, param2: u8"
9639 );
9640 });
9641}
9642
9643#[gpui::test]
9644async fn test_signature_help(cx: &mut TestAppContext) {
9645 init_test(cx, |_| {});
9646 cx.update(|cx| {
9647 cx.update_global::<SettingsStore, _>(|settings, cx| {
9648 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9649 settings.auto_signature_help = Some(true);
9650 });
9651 });
9652 });
9653
9654 let mut cx = EditorLspTestContext::new_rust(
9655 lsp::ServerCapabilities {
9656 signature_help_provider: Some(lsp::SignatureHelpOptions {
9657 ..Default::default()
9658 }),
9659 ..Default::default()
9660 },
9661 cx,
9662 )
9663 .await;
9664
9665 // A test that directly calls `show_signature_help`
9666 cx.update_editor(|editor, window, cx| {
9667 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9668 });
9669
9670 let mocked_response = lsp::SignatureHelp {
9671 signatures: vec![lsp::SignatureInformation {
9672 label: "fn sample(param1: u8, param2: u8)".to_string(),
9673 documentation: None,
9674 parameters: Some(vec![
9675 lsp::ParameterInformation {
9676 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9677 documentation: None,
9678 },
9679 lsp::ParameterInformation {
9680 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9681 documentation: None,
9682 },
9683 ]),
9684 active_parameter: None,
9685 }],
9686 active_signature: Some(0),
9687 active_parameter: Some(0),
9688 };
9689 handle_signature_help_request(&mut cx, mocked_response).await;
9690
9691 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9692 .await;
9693
9694 cx.editor(|editor, _, _| {
9695 let signature_help_state = editor.signature_help_state.popover().cloned();
9696 assert!(signature_help_state.is_some());
9697 assert_eq!(
9698 signature_help_state.unwrap().label,
9699 "param1: u8, param2: u8"
9700 );
9701 });
9702
9703 // When exiting outside from inside the brackets, `signature_help` is closed.
9704 cx.set_state(indoc! {"
9705 fn main() {
9706 sample(ˇ);
9707 }
9708
9709 fn sample(param1: u8, param2: u8) {}
9710 "});
9711
9712 cx.update_editor(|editor, window, cx| {
9713 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9714 });
9715
9716 let mocked_response = lsp::SignatureHelp {
9717 signatures: Vec::new(),
9718 active_signature: None,
9719 active_parameter: None,
9720 };
9721 handle_signature_help_request(&mut cx, mocked_response).await;
9722
9723 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9724 .await;
9725
9726 cx.editor(|editor, _, _| {
9727 assert!(!editor.signature_help_state.is_shown());
9728 });
9729
9730 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9731 cx.set_state(indoc! {"
9732 fn main() {
9733 sample(ˇ);
9734 }
9735
9736 fn sample(param1: u8, param2: u8) {}
9737 "});
9738
9739 let mocked_response = lsp::SignatureHelp {
9740 signatures: vec![lsp::SignatureInformation {
9741 label: "fn sample(param1: u8, param2: u8)".to_string(),
9742 documentation: None,
9743 parameters: Some(vec![
9744 lsp::ParameterInformation {
9745 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9746 documentation: None,
9747 },
9748 lsp::ParameterInformation {
9749 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9750 documentation: None,
9751 },
9752 ]),
9753 active_parameter: None,
9754 }],
9755 active_signature: Some(0),
9756 active_parameter: Some(0),
9757 };
9758 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9759 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9760 .await;
9761 cx.editor(|editor, _, _| {
9762 assert!(editor.signature_help_state.is_shown());
9763 });
9764
9765 // Restore the popover with more parameter input
9766 cx.set_state(indoc! {"
9767 fn main() {
9768 sample(param1, param2ˇ);
9769 }
9770
9771 fn sample(param1: u8, param2: u8) {}
9772 "});
9773
9774 let mocked_response = lsp::SignatureHelp {
9775 signatures: vec![lsp::SignatureInformation {
9776 label: "fn sample(param1: u8, param2: u8)".to_string(),
9777 documentation: None,
9778 parameters: Some(vec![
9779 lsp::ParameterInformation {
9780 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9781 documentation: None,
9782 },
9783 lsp::ParameterInformation {
9784 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9785 documentation: None,
9786 },
9787 ]),
9788 active_parameter: None,
9789 }],
9790 active_signature: Some(0),
9791 active_parameter: Some(1),
9792 };
9793 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9794 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9795 .await;
9796
9797 // When selecting a range, the popover is gone.
9798 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9799 cx.update_editor(|editor, window, cx| {
9800 editor.change_selections(None, window, cx, |s| {
9801 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9802 })
9803 });
9804 cx.assert_editor_state(indoc! {"
9805 fn main() {
9806 sample(param1, «ˇparam2»);
9807 }
9808
9809 fn sample(param1: u8, param2: u8) {}
9810 "});
9811 cx.editor(|editor, _, _| {
9812 assert!(!editor.signature_help_state.is_shown());
9813 });
9814
9815 // When unselecting again, the popover is back if within the brackets.
9816 cx.update_editor(|editor, window, cx| {
9817 editor.change_selections(None, window, cx, |s| {
9818 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9819 })
9820 });
9821 cx.assert_editor_state(indoc! {"
9822 fn main() {
9823 sample(param1, ˇparam2);
9824 }
9825
9826 fn sample(param1: u8, param2: u8) {}
9827 "});
9828 handle_signature_help_request(&mut cx, mocked_response).await;
9829 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9830 .await;
9831 cx.editor(|editor, _, _| {
9832 assert!(editor.signature_help_state.is_shown());
9833 });
9834
9835 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9836 cx.update_editor(|editor, window, cx| {
9837 editor.change_selections(None, window, cx, |s| {
9838 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9839 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9840 })
9841 });
9842 cx.assert_editor_state(indoc! {"
9843 fn main() {
9844 sample(param1, ˇparam2);
9845 }
9846
9847 fn sample(param1: u8, param2: u8) {}
9848 "});
9849
9850 let mocked_response = lsp::SignatureHelp {
9851 signatures: vec![lsp::SignatureInformation {
9852 label: "fn sample(param1: u8, param2: u8)".to_string(),
9853 documentation: None,
9854 parameters: Some(vec![
9855 lsp::ParameterInformation {
9856 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9857 documentation: None,
9858 },
9859 lsp::ParameterInformation {
9860 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9861 documentation: None,
9862 },
9863 ]),
9864 active_parameter: None,
9865 }],
9866 active_signature: Some(0),
9867 active_parameter: Some(1),
9868 };
9869 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9870 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9871 .await;
9872 cx.update_editor(|editor, _, cx| {
9873 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9874 });
9875 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9876 .await;
9877 cx.update_editor(|editor, window, cx| {
9878 editor.change_selections(None, window, cx, |s| {
9879 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9880 })
9881 });
9882 cx.assert_editor_state(indoc! {"
9883 fn main() {
9884 sample(param1, «ˇparam2»);
9885 }
9886
9887 fn sample(param1: u8, param2: u8) {}
9888 "});
9889 cx.update_editor(|editor, window, cx| {
9890 editor.change_selections(None, window, cx, |s| {
9891 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9892 })
9893 });
9894 cx.assert_editor_state(indoc! {"
9895 fn main() {
9896 sample(param1, ˇparam2);
9897 }
9898
9899 fn sample(param1: u8, param2: u8) {}
9900 "});
9901 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9902 .await;
9903}
9904
9905#[gpui::test]
9906async fn test_completion_mode(cx: &mut TestAppContext) {
9907 init_test(cx, |_| {});
9908 let mut cx = EditorLspTestContext::new_rust(
9909 lsp::ServerCapabilities {
9910 completion_provider: Some(lsp::CompletionOptions {
9911 resolve_provider: Some(true),
9912 ..Default::default()
9913 }),
9914 ..Default::default()
9915 },
9916 cx,
9917 )
9918 .await;
9919
9920 struct Run {
9921 run_description: &'static str,
9922 initial_state: String,
9923 buffer_marked_text: String,
9924 completion_text: &'static str,
9925 expected_with_insert_mode: String,
9926 expected_with_replace_mode: String,
9927 expected_with_replace_subsequence_mode: String,
9928 expected_with_replace_suffix_mode: String,
9929 }
9930
9931 let runs = [
9932 Run {
9933 run_description: "Start of word matches completion text",
9934 initial_state: "before ediˇ after".into(),
9935 buffer_marked_text: "before <edi|> after".into(),
9936 completion_text: "editor",
9937 expected_with_insert_mode: "before editorˇ after".into(),
9938 expected_with_replace_mode: "before editorˇ after".into(),
9939 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9940 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9941 },
9942 Run {
9943 run_description: "Accept same text at the middle of the word",
9944 initial_state: "before ediˇtor after".into(),
9945 buffer_marked_text: "before <edi|tor> after".into(),
9946 completion_text: "editor",
9947 expected_with_insert_mode: "before editorˇtor after".into(),
9948 expected_with_replace_mode: "before editorˇ after".into(),
9949 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9950 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9951 },
9952 Run {
9953 run_description: "End of word matches completion text -- cursor at end",
9954 initial_state: "before torˇ after".into(),
9955 buffer_marked_text: "before <tor|> after".into(),
9956 completion_text: "editor",
9957 expected_with_insert_mode: "before editorˇ after".into(),
9958 expected_with_replace_mode: "before editorˇ after".into(),
9959 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9960 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9961 },
9962 Run {
9963 run_description: "End of word matches completion text -- cursor at start",
9964 initial_state: "before ˇtor after".into(),
9965 buffer_marked_text: "before <|tor> after".into(),
9966 completion_text: "editor",
9967 expected_with_insert_mode: "before editorˇtor after".into(),
9968 expected_with_replace_mode: "before editorˇ after".into(),
9969 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9970 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9971 },
9972 Run {
9973 run_description: "Prepend text containing whitespace",
9974 initial_state: "pˇfield: bool".into(),
9975 buffer_marked_text: "<p|field>: bool".into(),
9976 completion_text: "pub ",
9977 expected_with_insert_mode: "pub ˇfield: bool".into(),
9978 expected_with_replace_mode: "pub ˇ: bool".into(),
9979 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9980 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9981 },
9982 Run {
9983 run_description: "Add element to start of list",
9984 initial_state: "[element_ˇelement_2]".into(),
9985 buffer_marked_text: "[<element_|element_2>]".into(),
9986 completion_text: "element_1",
9987 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9988 expected_with_replace_mode: "[element_1ˇ]".into(),
9989 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9990 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9991 },
9992 Run {
9993 run_description: "Add element to start of list -- first and second elements are equal",
9994 initial_state: "[elˇelement]".into(),
9995 buffer_marked_text: "[<el|element>]".into(),
9996 completion_text: "element",
9997 expected_with_insert_mode: "[elementˇelement]".into(),
9998 expected_with_replace_mode: "[elementˇ]".into(),
9999 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10000 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10001 },
10002 Run {
10003 run_description: "Ends with matching suffix",
10004 initial_state: "SubˇError".into(),
10005 buffer_marked_text: "<Sub|Error>".into(),
10006 completion_text: "SubscriptionError",
10007 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10008 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10009 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10010 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10011 },
10012 Run {
10013 run_description: "Suffix is a subsequence -- contiguous",
10014 initial_state: "SubˇErr".into(),
10015 buffer_marked_text: "<Sub|Err>".into(),
10016 completion_text: "SubscriptionError",
10017 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10018 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10019 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10020 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10021 },
10022 Run {
10023 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10024 initial_state: "Suˇscrirr".into(),
10025 buffer_marked_text: "<Su|scrirr>".into(),
10026 completion_text: "SubscriptionError",
10027 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10028 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10029 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10030 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10031 },
10032 Run {
10033 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10034 initial_state: "foo(indˇix)".into(),
10035 buffer_marked_text: "foo(<ind|ix>)".into(),
10036 completion_text: "node_index",
10037 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10038 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10039 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10040 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10041 },
10042 ];
10043
10044 for run in runs {
10045 let run_variations = [
10046 (LspInsertMode::Insert, run.expected_with_insert_mode),
10047 (LspInsertMode::Replace, run.expected_with_replace_mode),
10048 (
10049 LspInsertMode::ReplaceSubsequence,
10050 run.expected_with_replace_subsequence_mode,
10051 ),
10052 (
10053 LspInsertMode::ReplaceSuffix,
10054 run.expected_with_replace_suffix_mode,
10055 ),
10056 ];
10057
10058 for (lsp_insert_mode, expected_text) in run_variations {
10059 eprintln!(
10060 "run = {:?}, mode = {lsp_insert_mode:.?}",
10061 run.run_description,
10062 );
10063
10064 update_test_language_settings(&mut cx, |settings| {
10065 settings.defaults.completions = Some(CompletionSettings {
10066 lsp_insert_mode,
10067 words: WordsCompletionMode::Disabled,
10068 lsp: true,
10069 lsp_fetch_timeout_ms: 0,
10070 });
10071 });
10072
10073 cx.set_state(&run.initial_state);
10074 cx.update_editor(|editor, window, cx| {
10075 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10076 });
10077
10078 let counter = Arc::new(AtomicUsize::new(0));
10079 handle_completion_request_with_insert_and_replace(
10080 &mut cx,
10081 &run.buffer_marked_text,
10082 vec![run.completion_text],
10083 counter.clone(),
10084 )
10085 .await;
10086 cx.condition(|editor, _| editor.context_menu_visible())
10087 .await;
10088 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10089
10090 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10091 editor
10092 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10093 .unwrap()
10094 });
10095 cx.assert_editor_state(&expected_text);
10096 handle_resolve_completion_request(&mut cx, None).await;
10097 apply_additional_edits.await.unwrap();
10098 }
10099 }
10100}
10101
10102#[gpui::test]
10103async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10104 init_test(cx, |_| {});
10105 let mut cx = EditorLspTestContext::new_rust(
10106 lsp::ServerCapabilities {
10107 completion_provider: Some(lsp::CompletionOptions {
10108 resolve_provider: Some(true),
10109 ..Default::default()
10110 }),
10111 ..Default::default()
10112 },
10113 cx,
10114 )
10115 .await;
10116
10117 let initial_state = "SubˇError";
10118 let buffer_marked_text = "<Sub|Error>";
10119 let completion_text = "SubscriptionError";
10120 let expected_with_insert_mode = "SubscriptionErrorˇError";
10121 let expected_with_replace_mode = "SubscriptionErrorˇ";
10122
10123 update_test_language_settings(&mut cx, |settings| {
10124 settings.defaults.completions = Some(CompletionSettings {
10125 words: WordsCompletionMode::Disabled,
10126 // set the opposite here to ensure that the action is overriding the default behavior
10127 lsp_insert_mode: LspInsertMode::Insert,
10128 lsp: true,
10129 lsp_fetch_timeout_ms: 0,
10130 });
10131 });
10132
10133 cx.set_state(initial_state);
10134 cx.update_editor(|editor, window, cx| {
10135 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10136 });
10137
10138 let counter = Arc::new(AtomicUsize::new(0));
10139 handle_completion_request_with_insert_and_replace(
10140 &mut cx,
10141 &buffer_marked_text,
10142 vec![completion_text],
10143 counter.clone(),
10144 )
10145 .await;
10146 cx.condition(|editor, _| editor.context_menu_visible())
10147 .await;
10148 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10149
10150 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10151 editor
10152 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10153 .unwrap()
10154 });
10155 cx.assert_editor_state(&expected_with_replace_mode);
10156 handle_resolve_completion_request(&mut cx, None).await;
10157 apply_additional_edits.await.unwrap();
10158
10159 update_test_language_settings(&mut cx, |settings| {
10160 settings.defaults.completions = Some(CompletionSettings {
10161 words: WordsCompletionMode::Disabled,
10162 // set the opposite here to ensure that the action is overriding the default behavior
10163 lsp_insert_mode: LspInsertMode::Replace,
10164 lsp: true,
10165 lsp_fetch_timeout_ms: 0,
10166 });
10167 });
10168
10169 cx.set_state(initial_state);
10170 cx.update_editor(|editor, window, cx| {
10171 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10172 });
10173 handle_completion_request_with_insert_and_replace(
10174 &mut cx,
10175 &buffer_marked_text,
10176 vec![completion_text],
10177 counter.clone(),
10178 )
10179 .await;
10180 cx.condition(|editor, _| editor.context_menu_visible())
10181 .await;
10182 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10183
10184 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10185 editor
10186 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10187 .unwrap()
10188 });
10189 cx.assert_editor_state(&expected_with_insert_mode);
10190 handle_resolve_completion_request(&mut cx, None).await;
10191 apply_additional_edits.await.unwrap();
10192}
10193
10194#[gpui::test]
10195async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10196 init_test(cx, |_| {});
10197 let mut cx = EditorLspTestContext::new_rust(
10198 lsp::ServerCapabilities {
10199 completion_provider: Some(lsp::CompletionOptions {
10200 resolve_provider: Some(true),
10201 ..Default::default()
10202 }),
10203 ..Default::default()
10204 },
10205 cx,
10206 )
10207 .await;
10208
10209 // scenario: surrounding text matches completion text
10210 let completion_text = "to_offset";
10211 let initial_state = indoc! {"
10212 1. buf.to_offˇsuffix
10213 2. buf.to_offˇsuf
10214 3. buf.to_offˇfix
10215 4. buf.to_offˇ
10216 5. into_offˇensive
10217 6. ˇsuffix
10218 7. let ˇ //
10219 8. aaˇzz
10220 9. buf.to_off«zzzzzˇ»suffix
10221 10. buf.«ˇzzzzz»suffix
10222 11. to_off«ˇzzzzz»
10223
10224 buf.to_offˇsuffix // newest cursor
10225 "};
10226 let completion_marked_buffer = indoc! {"
10227 1. buf.to_offsuffix
10228 2. buf.to_offsuf
10229 3. buf.to_offfix
10230 4. buf.to_off
10231 5. into_offensive
10232 6. suffix
10233 7. let //
10234 8. aazz
10235 9. buf.to_offzzzzzsuffix
10236 10. buf.zzzzzsuffix
10237 11. to_offzzzzz
10238
10239 buf.<to_off|suffix> // newest cursor
10240 "};
10241 let expected = indoc! {"
10242 1. buf.to_offsetˇ
10243 2. buf.to_offsetˇsuf
10244 3. buf.to_offsetˇfix
10245 4. buf.to_offsetˇ
10246 5. into_offsetˇensive
10247 6. to_offsetˇsuffix
10248 7. let to_offsetˇ //
10249 8. aato_offsetˇzz
10250 9. buf.to_offsetˇ
10251 10. buf.to_offsetˇsuffix
10252 11. to_offsetˇ
10253
10254 buf.to_offsetˇ // newest cursor
10255 "};
10256 cx.set_state(initial_state);
10257 cx.update_editor(|editor, window, cx| {
10258 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10259 });
10260 handle_completion_request_with_insert_and_replace(
10261 &mut cx,
10262 completion_marked_buffer,
10263 vec![completion_text],
10264 Arc::new(AtomicUsize::new(0)),
10265 )
10266 .await;
10267 cx.condition(|editor, _| editor.context_menu_visible())
10268 .await;
10269 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10270 editor
10271 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10272 .unwrap()
10273 });
10274 cx.assert_editor_state(expected);
10275 handle_resolve_completion_request(&mut cx, None).await;
10276 apply_additional_edits.await.unwrap();
10277
10278 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10279 let completion_text = "foo_and_bar";
10280 let initial_state = indoc! {"
10281 1. ooanbˇ
10282 2. zooanbˇ
10283 3. ooanbˇz
10284 4. zooanbˇz
10285 5. ooanˇ
10286 6. oanbˇ
10287
10288 ooanbˇ
10289 "};
10290 let completion_marked_buffer = indoc! {"
10291 1. ooanb
10292 2. zooanb
10293 3. ooanbz
10294 4. zooanbz
10295 5. ooan
10296 6. oanb
10297
10298 <ooanb|>
10299 "};
10300 let expected = indoc! {"
10301 1. foo_and_barˇ
10302 2. zfoo_and_barˇ
10303 3. foo_and_barˇz
10304 4. zfoo_and_barˇz
10305 5. ooanfoo_and_barˇ
10306 6. oanbfoo_and_barˇ
10307
10308 foo_and_barˇ
10309 "};
10310 cx.set_state(initial_state);
10311 cx.update_editor(|editor, window, cx| {
10312 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10313 });
10314 handle_completion_request_with_insert_and_replace(
10315 &mut cx,
10316 completion_marked_buffer,
10317 vec![completion_text],
10318 Arc::new(AtomicUsize::new(0)),
10319 )
10320 .await;
10321 cx.condition(|editor, _| editor.context_menu_visible())
10322 .await;
10323 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10324 editor
10325 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10326 .unwrap()
10327 });
10328 cx.assert_editor_state(expected);
10329 handle_resolve_completion_request(&mut cx, None).await;
10330 apply_additional_edits.await.unwrap();
10331
10332 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10333 // (expects the same as if it was inserted at the end)
10334 let completion_text = "foo_and_bar";
10335 let initial_state = indoc! {"
10336 1. ooˇanb
10337 2. zooˇanb
10338 3. ooˇanbz
10339 4. zooˇanbz
10340
10341 ooˇanb
10342 "};
10343 let completion_marked_buffer = indoc! {"
10344 1. ooanb
10345 2. zooanb
10346 3. ooanbz
10347 4. zooanbz
10348
10349 <oo|anb>
10350 "};
10351 let expected = indoc! {"
10352 1. foo_and_barˇ
10353 2. zfoo_and_barˇ
10354 3. foo_and_barˇz
10355 4. zfoo_and_barˇz
10356
10357 foo_and_barˇ
10358 "};
10359 cx.set_state(initial_state);
10360 cx.update_editor(|editor, window, cx| {
10361 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10362 });
10363 handle_completion_request_with_insert_and_replace(
10364 &mut cx,
10365 completion_marked_buffer,
10366 vec![completion_text],
10367 Arc::new(AtomicUsize::new(0)),
10368 )
10369 .await;
10370 cx.condition(|editor, _| editor.context_menu_visible())
10371 .await;
10372 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10373 editor
10374 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10375 .unwrap()
10376 });
10377 cx.assert_editor_state(expected);
10378 handle_resolve_completion_request(&mut cx, None).await;
10379 apply_additional_edits.await.unwrap();
10380}
10381
10382// This used to crash
10383#[gpui::test]
10384async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10385 init_test(cx, |_| {});
10386
10387 let buffer_text = indoc! {"
10388 fn main() {
10389 10.satu;
10390
10391 //
10392 // separate cursors so they open in different excerpts (manually reproducible)
10393 //
10394
10395 10.satu20;
10396 }
10397 "};
10398 let multibuffer_text_with_selections = indoc! {"
10399 fn main() {
10400 10.satuˇ;
10401
10402 //
10403
10404 //
10405
10406 10.satuˇ20;
10407 }
10408 "};
10409 let expected_multibuffer = indoc! {"
10410 fn main() {
10411 10.saturating_sub()ˇ;
10412
10413 //
10414
10415 //
10416
10417 10.saturating_sub()ˇ;
10418 }
10419 "};
10420
10421 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10422 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10423
10424 let fs = FakeFs::new(cx.executor());
10425 fs.insert_tree(
10426 path!("/a"),
10427 json!({
10428 "main.rs": buffer_text,
10429 }),
10430 )
10431 .await;
10432
10433 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10434 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10435 language_registry.add(rust_lang());
10436 let mut fake_servers = language_registry.register_fake_lsp(
10437 "Rust",
10438 FakeLspAdapter {
10439 capabilities: lsp::ServerCapabilities {
10440 completion_provider: Some(lsp::CompletionOptions {
10441 resolve_provider: None,
10442 ..lsp::CompletionOptions::default()
10443 }),
10444 ..lsp::ServerCapabilities::default()
10445 },
10446 ..FakeLspAdapter::default()
10447 },
10448 );
10449 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10450 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10451 let buffer = project
10452 .update(cx, |project, cx| {
10453 project.open_local_buffer(path!("/a/main.rs"), cx)
10454 })
10455 .await
10456 .unwrap();
10457
10458 let multi_buffer = cx.new(|cx| {
10459 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10460 multi_buffer.push_excerpts(
10461 buffer.clone(),
10462 [ExcerptRange::new(0..first_excerpt_end)],
10463 cx,
10464 );
10465 multi_buffer.push_excerpts(
10466 buffer.clone(),
10467 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10468 cx,
10469 );
10470 multi_buffer
10471 });
10472
10473 let editor = workspace
10474 .update(cx, |_, window, cx| {
10475 cx.new(|cx| {
10476 Editor::new(
10477 EditorMode::Full {
10478 scale_ui_elements_with_buffer_font_size: false,
10479 show_active_line_background: false,
10480 sized_by_content: false,
10481 },
10482 multi_buffer.clone(),
10483 Some(project.clone()),
10484 window,
10485 cx,
10486 )
10487 })
10488 })
10489 .unwrap();
10490
10491 let pane = workspace
10492 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10493 .unwrap();
10494 pane.update_in(cx, |pane, window, cx| {
10495 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10496 });
10497
10498 let fake_server = fake_servers.next().await.unwrap();
10499
10500 editor.update_in(cx, |editor, window, cx| {
10501 editor.change_selections(None, window, cx, |s| {
10502 s.select_ranges([
10503 Point::new(1, 11)..Point::new(1, 11),
10504 Point::new(7, 11)..Point::new(7, 11),
10505 ])
10506 });
10507
10508 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10509 });
10510
10511 editor.update_in(cx, |editor, window, cx| {
10512 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10513 });
10514
10515 fake_server
10516 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10517 let completion_item = lsp::CompletionItem {
10518 label: "saturating_sub()".into(),
10519 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10520 lsp::InsertReplaceEdit {
10521 new_text: "saturating_sub()".to_owned(),
10522 insert: lsp::Range::new(
10523 lsp::Position::new(7, 7),
10524 lsp::Position::new(7, 11),
10525 ),
10526 replace: lsp::Range::new(
10527 lsp::Position::new(7, 7),
10528 lsp::Position::new(7, 13),
10529 ),
10530 },
10531 )),
10532 ..lsp::CompletionItem::default()
10533 };
10534
10535 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10536 })
10537 .next()
10538 .await
10539 .unwrap();
10540
10541 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10542 .await;
10543
10544 editor
10545 .update_in(cx, |editor, window, cx| {
10546 editor
10547 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10548 .unwrap()
10549 })
10550 .await
10551 .unwrap();
10552
10553 editor.update(cx, |editor, cx| {
10554 assert_text_with_selections(editor, expected_multibuffer, cx);
10555 })
10556}
10557
10558#[gpui::test]
10559async fn test_completion(cx: &mut TestAppContext) {
10560 init_test(cx, |_| {});
10561
10562 let mut cx = EditorLspTestContext::new_rust(
10563 lsp::ServerCapabilities {
10564 completion_provider: Some(lsp::CompletionOptions {
10565 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10566 resolve_provider: Some(true),
10567 ..Default::default()
10568 }),
10569 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10570 ..Default::default()
10571 },
10572 cx,
10573 )
10574 .await;
10575 let counter = Arc::new(AtomicUsize::new(0));
10576
10577 cx.set_state(indoc! {"
10578 oneˇ
10579 two
10580 three
10581 "});
10582 cx.simulate_keystroke(".");
10583 handle_completion_request(
10584 &mut cx,
10585 indoc! {"
10586 one.|<>
10587 two
10588 three
10589 "},
10590 vec!["first_completion", "second_completion"],
10591 counter.clone(),
10592 )
10593 .await;
10594 cx.condition(|editor, _| editor.context_menu_visible())
10595 .await;
10596 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10597
10598 let _handler = handle_signature_help_request(
10599 &mut cx,
10600 lsp::SignatureHelp {
10601 signatures: vec![lsp::SignatureInformation {
10602 label: "test signature".to_string(),
10603 documentation: None,
10604 parameters: Some(vec![lsp::ParameterInformation {
10605 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10606 documentation: None,
10607 }]),
10608 active_parameter: None,
10609 }],
10610 active_signature: None,
10611 active_parameter: None,
10612 },
10613 );
10614 cx.update_editor(|editor, window, cx| {
10615 assert!(
10616 !editor.signature_help_state.is_shown(),
10617 "No signature help was called for"
10618 );
10619 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10620 });
10621 cx.run_until_parked();
10622 cx.update_editor(|editor, _, _| {
10623 assert!(
10624 !editor.signature_help_state.is_shown(),
10625 "No signature help should be shown when completions menu is open"
10626 );
10627 });
10628
10629 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10630 editor.context_menu_next(&Default::default(), window, cx);
10631 editor
10632 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10633 .unwrap()
10634 });
10635 cx.assert_editor_state(indoc! {"
10636 one.second_completionˇ
10637 two
10638 three
10639 "});
10640
10641 handle_resolve_completion_request(
10642 &mut cx,
10643 Some(vec![
10644 (
10645 //This overlaps with the primary completion edit which is
10646 //misbehavior from the LSP spec, test that we filter it out
10647 indoc! {"
10648 one.second_ˇcompletion
10649 two
10650 threeˇ
10651 "},
10652 "overlapping additional edit",
10653 ),
10654 (
10655 indoc! {"
10656 one.second_completion
10657 two
10658 threeˇ
10659 "},
10660 "\nadditional edit",
10661 ),
10662 ]),
10663 )
10664 .await;
10665 apply_additional_edits.await.unwrap();
10666 cx.assert_editor_state(indoc! {"
10667 one.second_completionˇ
10668 two
10669 three
10670 additional edit
10671 "});
10672
10673 cx.set_state(indoc! {"
10674 one.second_completion
10675 twoˇ
10676 threeˇ
10677 additional edit
10678 "});
10679 cx.simulate_keystroke(" ");
10680 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10681 cx.simulate_keystroke("s");
10682 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10683
10684 cx.assert_editor_state(indoc! {"
10685 one.second_completion
10686 two sˇ
10687 three sˇ
10688 additional edit
10689 "});
10690 handle_completion_request(
10691 &mut cx,
10692 indoc! {"
10693 one.second_completion
10694 two s
10695 three <s|>
10696 additional edit
10697 "},
10698 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10699 counter.clone(),
10700 )
10701 .await;
10702 cx.condition(|editor, _| editor.context_menu_visible())
10703 .await;
10704 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10705
10706 cx.simulate_keystroke("i");
10707
10708 handle_completion_request(
10709 &mut cx,
10710 indoc! {"
10711 one.second_completion
10712 two si
10713 three <si|>
10714 additional edit
10715 "},
10716 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10717 counter.clone(),
10718 )
10719 .await;
10720 cx.condition(|editor, _| editor.context_menu_visible())
10721 .await;
10722 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10723
10724 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10725 editor
10726 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10727 .unwrap()
10728 });
10729 cx.assert_editor_state(indoc! {"
10730 one.second_completion
10731 two sixth_completionˇ
10732 three sixth_completionˇ
10733 additional edit
10734 "});
10735
10736 apply_additional_edits.await.unwrap();
10737
10738 update_test_language_settings(&mut cx, |settings| {
10739 settings.defaults.show_completions_on_input = Some(false);
10740 });
10741 cx.set_state("editorˇ");
10742 cx.simulate_keystroke(".");
10743 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10744 cx.simulate_keystrokes("c l o");
10745 cx.assert_editor_state("editor.cloˇ");
10746 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10747 cx.update_editor(|editor, window, cx| {
10748 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10749 });
10750 handle_completion_request(
10751 &mut cx,
10752 "editor.<clo|>",
10753 vec!["close", "clobber"],
10754 counter.clone(),
10755 )
10756 .await;
10757 cx.condition(|editor, _| editor.context_menu_visible())
10758 .await;
10759 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10760
10761 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10762 editor
10763 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10764 .unwrap()
10765 });
10766 cx.assert_editor_state("editor.closeˇ");
10767 handle_resolve_completion_request(&mut cx, None).await;
10768 apply_additional_edits.await.unwrap();
10769}
10770
10771#[gpui::test]
10772async fn test_word_completion(cx: &mut TestAppContext) {
10773 let lsp_fetch_timeout_ms = 10;
10774 init_test(cx, |language_settings| {
10775 language_settings.defaults.completions = Some(CompletionSettings {
10776 words: WordsCompletionMode::Fallback,
10777 lsp: true,
10778 lsp_fetch_timeout_ms: 10,
10779 lsp_insert_mode: LspInsertMode::Insert,
10780 });
10781 });
10782
10783 let mut cx = EditorLspTestContext::new_rust(
10784 lsp::ServerCapabilities {
10785 completion_provider: Some(lsp::CompletionOptions {
10786 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10787 ..lsp::CompletionOptions::default()
10788 }),
10789 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10790 ..lsp::ServerCapabilities::default()
10791 },
10792 cx,
10793 )
10794 .await;
10795
10796 let throttle_completions = Arc::new(AtomicBool::new(false));
10797
10798 let lsp_throttle_completions = throttle_completions.clone();
10799 let _completion_requests_handler =
10800 cx.lsp
10801 .server
10802 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10803 let lsp_throttle_completions = lsp_throttle_completions.clone();
10804 let cx = cx.clone();
10805 async move {
10806 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10807 cx.background_executor()
10808 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10809 .await;
10810 }
10811 Ok(Some(lsp::CompletionResponse::Array(vec![
10812 lsp::CompletionItem {
10813 label: "first".into(),
10814 ..lsp::CompletionItem::default()
10815 },
10816 lsp::CompletionItem {
10817 label: "last".into(),
10818 ..lsp::CompletionItem::default()
10819 },
10820 ])))
10821 }
10822 });
10823
10824 cx.set_state(indoc! {"
10825 oneˇ
10826 two
10827 three
10828 "});
10829 cx.simulate_keystroke(".");
10830 cx.executor().run_until_parked();
10831 cx.condition(|editor, _| editor.context_menu_visible())
10832 .await;
10833 cx.update_editor(|editor, window, cx| {
10834 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10835 {
10836 assert_eq!(
10837 completion_menu_entries(&menu),
10838 &["first", "last"],
10839 "When LSP server is fast to reply, no fallback word completions are used"
10840 );
10841 } else {
10842 panic!("expected completion menu to be open");
10843 }
10844 editor.cancel(&Cancel, window, cx);
10845 });
10846 cx.executor().run_until_parked();
10847 cx.condition(|editor, _| !editor.context_menu_visible())
10848 .await;
10849
10850 throttle_completions.store(true, atomic::Ordering::Release);
10851 cx.simulate_keystroke(".");
10852 cx.executor()
10853 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
10854 cx.executor().run_until_parked();
10855 cx.condition(|editor, _| editor.context_menu_visible())
10856 .await;
10857 cx.update_editor(|editor, _, _| {
10858 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10859 {
10860 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
10861 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10862 } else {
10863 panic!("expected completion menu to be open");
10864 }
10865 });
10866}
10867
10868#[gpui::test]
10869async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10870 init_test(cx, |language_settings| {
10871 language_settings.defaults.completions = Some(CompletionSettings {
10872 words: WordsCompletionMode::Enabled,
10873 lsp: true,
10874 lsp_fetch_timeout_ms: 0,
10875 lsp_insert_mode: LspInsertMode::Insert,
10876 });
10877 });
10878
10879 let mut cx = EditorLspTestContext::new_rust(
10880 lsp::ServerCapabilities {
10881 completion_provider: Some(lsp::CompletionOptions {
10882 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10883 ..lsp::CompletionOptions::default()
10884 }),
10885 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10886 ..lsp::ServerCapabilities::default()
10887 },
10888 cx,
10889 )
10890 .await;
10891
10892 let _completion_requests_handler =
10893 cx.lsp
10894 .server
10895 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10896 Ok(Some(lsp::CompletionResponse::Array(vec![
10897 lsp::CompletionItem {
10898 label: "first".into(),
10899 ..lsp::CompletionItem::default()
10900 },
10901 lsp::CompletionItem {
10902 label: "last".into(),
10903 ..lsp::CompletionItem::default()
10904 },
10905 ])))
10906 });
10907
10908 cx.set_state(indoc! {"ˇ
10909 first
10910 last
10911 second
10912 "});
10913 cx.simulate_keystroke(".");
10914 cx.executor().run_until_parked();
10915 cx.condition(|editor, _| editor.context_menu_visible())
10916 .await;
10917 cx.update_editor(|editor, _, _| {
10918 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10919 {
10920 assert_eq!(
10921 completion_menu_entries(&menu),
10922 &["first", "last", "second"],
10923 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10924 );
10925 } else {
10926 panic!("expected completion menu to be open");
10927 }
10928 });
10929}
10930
10931#[gpui::test]
10932async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10933 init_test(cx, |language_settings| {
10934 language_settings.defaults.completions = Some(CompletionSettings {
10935 words: WordsCompletionMode::Disabled,
10936 lsp: true,
10937 lsp_fetch_timeout_ms: 0,
10938 lsp_insert_mode: LspInsertMode::Insert,
10939 });
10940 });
10941
10942 let mut cx = EditorLspTestContext::new_rust(
10943 lsp::ServerCapabilities {
10944 completion_provider: Some(lsp::CompletionOptions {
10945 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10946 ..lsp::CompletionOptions::default()
10947 }),
10948 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10949 ..lsp::ServerCapabilities::default()
10950 },
10951 cx,
10952 )
10953 .await;
10954
10955 let _completion_requests_handler =
10956 cx.lsp
10957 .server
10958 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10959 panic!("LSP completions should not be queried when dealing with word completions")
10960 });
10961
10962 cx.set_state(indoc! {"ˇ
10963 first
10964 last
10965 second
10966 "});
10967 cx.update_editor(|editor, window, cx| {
10968 editor.show_word_completions(&ShowWordCompletions, window, cx);
10969 });
10970 cx.executor().run_until_parked();
10971 cx.condition(|editor, _| editor.context_menu_visible())
10972 .await;
10973 cx.update_editor(|editor, _, _| {
10974 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10975 {
10976 assert_eq!(
10977 completion_menu_entries(&menu),
10978 &["first", "last", "second"],
10979 "`ShowWordCompletions` action should show word completions"
10980 );
10981 } else {
10982 panic!("expected completion menu to be open");
10983 }
10984 });
10985
10986 cx.simulate_keystroke("l");
10987 cx.executor().run_until_parked();
10988 cx.condition(|editor, _| editor.context_menu_visible())
10989 .await;
10990 cx.update_editor(|editor, _, _| {
10991 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10992 {
10993 assert_eq!(
10994 completion_menu_entries(&menu),
10995 &["last"],
10996 "After showing word completions, further editing should filter them and not query the LSP"
10997 );
10998 } else {
10999 panic!("expected completion menu to be open");
11000 }
11001 });
11002}
11003
11004#[gpui::test]
11005async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11006 init_test(cx, |language_settings| {
11007 language_settings.defaults.completions = Some(CompletionSettings {
11008 words: WordsCompletionMode::Fallback,
11009 lsp: false,
11010 lsp_fetch_timeout_ms: 0,
11011 lsp_insert_mode: LspInsertMode::Insert,
11012 });
11013 });
11014
11015 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11016
11017 cx.set_state(indoc! {"ˇ
11018 0_usize
11019 let
11020 33
11021 4.5f32
11022 "});
11023 cx.update_editor(|editor, window, cx| {
11024 editor.show_completions(&ShowCompletions::default(), window, cx);
11025 });
11026 cx.executor().run_until_parked();
11027 cx.condition(|editor, _| editor.context_menu_visible())
11028 .await;
11029 cx.update_editor(|editor, window, cx| {
11030 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11031 {
11032 assert_eq!(
11033 completion_menu_entries(&menu),
11034 &["let"],
11035 "With no digits in the completion query, no digits should be in the word completions"
11036 );
11037 } else {
11038 panic!("expected completion menu to be open");
11039 }
11040 editor.cancel(&Cancel, window, cx);
11041 });
11042
11043 cx.set_state(indoc! {"3ˇ
11044 0_usize
11045 let
11046 3
11047 33.35f32
11048 "});
11049 cx.update_editor(|editor, window, cx| {
11050 editor.show_completions(&ShowCompletions::default(), window, cx);
11051 });
11052 cx.executor().run_until_parked();
11053 cx.condition(|editor, _| editor.context_menu_visible())
11054 .await;
11055 cx.update_editor(|editor, _, _| {
11056 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11057 {
11058 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11059 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11060 } else {
11061 panic!("expected completion menu to be open");
11062 }
11063 });
11064}
11065
11066fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11067 let position = || lsp::Position {
11068 line: params.text_document_position.position.line,
11069 character: params.text_document_position.position.character,
11070 };
11071 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11072 range: lsp::Range {
11073 start: position(),
11074 end: position(),
11075 },
11076 new_text: text.to_string(),
11077 }))
11078}
11079
11080#[gpui::test]
11081async fn test_multiline_completion(cx: &mut TestAppContext) {
11082 init_test(cx, |_| {});
11083
11084 let fs = FakeFs::new(cx.executor());
11085 fs.insert_tree(
11086 path!("/a"),
11087 json!({
11088 "main.ts": "a",
11089 }),
11090 )
11091 .await;
11092
11093 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11094 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11095 let typescript_language = Arc::new(Language::new(
11096 LanguageConfig {
11097 name: "TypeScript".into(),
11098 matcher: LanguageMatcher {
11099 path_suffixes: vec!["ts".to_string()],
11100 ..LanguageMatcher::default()
11101 },
11102 line_comments: vec!["// ".into()],
11103 ..LanguageConfig::default()
11104 },
11105 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11106 ));
11107 language_registry.add(typescript_language.clone());
11108 let mut fake_servers = language_registry.register_fake_lsp(
11109 "TypeScript",
11110 FakeLspAdapter {
11111 capabilities: lsp::ServerCapabilities {
11112 completion_provider: Some(lsp::CompletionOptions {
11113 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11114 ..lsp::CompletionOptions::default()
11115 }),
11116 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11117 ..lsp::ServerCapabilities::default()
11118 },
11119 // Emulate vtsls label generation
11120 label_for_completion: Some(Box::new(|item, _| {
11121 let text = if let Some(description) = item
11122 .label_details
11123 .as_ref()
11124 .and_then(|label_details| label_details.description.as_ref())
11125 {
11126 format!("{} {}", item.label, description)
11127 } else if let Some(detail) = &item.detail {
11128 format!("{} {}", item.label, detail)
11129 } else {
11130 item.label.clone()
11131 };
11132 let len = text.len();
11133 Some(language::CodeLabel {
11134 text,
11135 runs: Vec::new(),
11136 filter_range: 0..len,
11137 })
11138 })),
11139 ..FakeLspAdapter::default()
11140 },
11141 );
11142 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11143 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11144 let worktree_id = workspace
11145 .update(cx, |workspace, _window, cx| {
11146 workspace.project().update(cx, |project, cx| {
11147 project.worktrees(cx).next().unwrap().read(cx).id()
11148 })
11149 })
11150 .unwrap();
11151 let _buffer = project
11152 .update(cx, |project, cx| {
11153 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11154 })
11155 .await
11156 .unwrap();
11157 let editor = workspace
11158 .update(cx, |workspace, window, cx| {
11159 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11160 })
11161 .unwrap()
11162 .await
11163 .unwrap()
11164 .downcast::<Editor>()
11165 .unwrap();
11166 let fake_server = fake_servers.next().await.unwrap();
11167
11168 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11169 let multiline_label_2 = "a\nb\nc\n";
11170 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11171 let multiline_description = "d\ne\nf\n";
11172 let multiline_detail_2 = "g\nh\ni\n";
11173
11174 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11175 move |params, _| async move {
11176 Ok(Some(lsp::CompletionResponse::Array(vec![
11177 lsp::CompletionItem {
11178 label: multiline_label.to_string(),
11179 text_edit: gen_text_edit(¶ms, "new_text_1"),
11180 ..lsp::CompletionItem::default()
11181 },
11182 lsp::CompletionItem {
11183 label: "single line label 1".to_string(),
11184 detail: Some(multiline_detail.to_string()),
11185 text_edit: gen_text_edit(¶ms, "new_text_2"),
11186 ..lsp::CompletionItem::default()
11187 },
11188 lsp::CompletionItem {
11189 label: "single line label 2".to_string(),
11190 label_details: Some(lsp::CompletionItemLabelDetails {
11191 description: Some(multiline_description.to_string()),
11192 detail: None,
11193 }),
11194 text_edit: gen_text_edit(¶ms, "new_text_2"),
11195 ..lsp::CompletionItem::default()
11196 },
11197 lsp::CompletionItem {
11198 label: multiline_label_2.to_string(),
11199 detail: Some(multiline_detail_2.to_string()),
11200 text_edit: gen_text_edit(¶ms, "new_text_3"),
11201 ..lsp::CompletionItem::default()
11202 },
11203 lsp::CompletionItem {
11204 label: "Label with many spaces and \t but without newlines".to_string(),
11205 detail: Some(
11206 "Details with many spaces and \t but without newlines".to_string(),
11207 ),
11208 text_edit: gen_text_edit(¶ms, "new_text_4"),
11209 ..lsp::CompletionItem::default()
11210 },
11211 ])))
11212 },
11213 );
11214
11215 editor.update_in(cx, |editor, window, cx| {
11216 cx.focus_self(window);
11217 editor.move_to_end(&MoveToEnd, window, cx);
11218 editor.handle_input(".", window, cx);
11219 });
11220 cx.run_until_parked();
11221 completion_handle.next().await.unwrap();
11222
11223 editor.update(cx, |editor, _| {
11224 assert!(editor.context_menu_visible());
11225 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11226 {
11227 let completion_labels = menu
11228 .completions
11229 .borrow()
11230 .iter()
11231 .map(|c| c.label.text.clone())
11232 .collect::<Vec<_>>();
11233 assert_eq!(
11234 completion_labels,
11235 &[
11236 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11237 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11238 "single line label 2 d e f ",
11239 "a b c g h i ",
11240 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11241 ],
11242 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11243 );
11244
11245 for completion in menu
11246 .completions
11247 .borrow()
11248 .iter() {
11249 assert_eq!(
11250 completion.label.filter_range,
11251 0..completion.label.text.len(),
11252 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11253 );
11254 }
11255 } else {
11256 panic!("expected completion menu to be open");
11257 }
11258 });
11259}
11260
11261#[gpui::test]
11262async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11263 init_test(cx, |_| {});
11264 let mut cx = EditorLspTestContext::new_rust(
11265 lsp::ServerCapabilities {
11266 completion_provider: Some(lsp::CompletionOptions {
11267 trigger_characters: Some(vec![".".to_string()]),
11268 ..Default::default()
11269 }),
11270 ..Default::default()
11271 },
11272 cx,
11273 )
11274 .await;
11275 cx.lsp
11276 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11277 Ok(Some(lsp::CompletionResponse::Array(vec![
11278 lsp::CompletionItem {
11279 label: "first".into(),
11280 ..Default::default()
11281 },
11282 lsp::CompletionItem {
11283 label: "last".into(),
11284 ..Default::default()
11285 },
11286 ])))
11287 });
11288 cx.set_state("variableˇ");
11289 cx.simulate_keystroke(".");
11290 cx.executor().run_until_parked();
11291
11292 cx.update_editor(|editor, _, _| {
11293 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11294 {
11295 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11296 } else {
11297 panic!("expected completion menu to be open");
11298 }
11299 });
11300
11301 cx.update_editor(|editor, window, cx| {
11302 editor.move_page_down(&MovePageDown::default(), window, cx);
11303 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11304 {
11305 assert!(
11306 menu.selected_item == 1,
11307 "expected PageDown to select the last item from the context menu"
11308 );
11309 } else {
11310 panic!("expected completion menu to stay open after PageDown");
11311 }
11312 });
11313
11314 cx.update_editor(|editor, window, cx| {
11315 editor.move_page_up(&MovePageUp::default(), window, cx);
11316 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11317 {
11318 assert!(
11319 menu.selected_item == 0,
11320 "expected PageUp to select the first item from the context menu"
11321 );
11322 } else {
11323 panic!("expected completion menu to stay open after PageUp");
11324 }
11325 });
11326}
11327
11328#[gpui::test]
11329async fn test_as_is_completions(cx: &mut TestAppContext) {
11330 init_test(cx, |_| {});
11331 let mut cx = EditorLspTestContext::new_rust(
11332 lsp::ServerCapabilities {
11333 completion_provider: Some(lsp::CompletionOptions {
11334 ..Default::default()
11335 }),
11336 ..Default::default()
11337 },
11338 cx,
11339 )
11340 .await;
11341 cx.lsp
11342 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11343 Ok(Some(lsp::CompletionResponse::Array(vec![
11344 lsp::CompletionItem {
11345 label: "unsafe".into(),
11346 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11347 range: lsp::Range {
11348 start: lsp::Position {
11349 line: 1,
11350 character: 2,
11351 },
11352 end: lsp::Position {
11353 line: 1,
11354 character: 3,
11355 },
11356 },
11357 new_text: "unsafe".to_string(),
11358 })),
11359 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11360 ..Default::default()
11361 },
11362 ])))
11363 });
11364 cx.set_state("fn a() {}\n nˇ");
11365 cx.executor().run_until_parked();
11366 cx.update_editor(|editor, window, cx| {
11367 editor.show_completions(
11368 &ShowCompletions {
11369 trigger: Some("\n".into()),
11370 },
11371 window,
11372 cx,
11373 );
11374 });
11375 cx.executor().run_until_parked();
11376
11377 cx.update_editor(|editor, window, cx| {
11378 editor.confirm_completion(&Default::default(), window, cx)
11379 });
11380 cx.executor().run_until_parked();
11381 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11382}
11383
11384#[gpui::test]
11385async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11386 init_test(cx, |_| {});
11387
11388 let mut cx = EditorLspTestContext::new_rust(
11389 lsp::ServerCapabilities {
11390 completion_provider: Some(lsp::CompletionOptions {
11391 trigger_characters: Some(vec![".".to_string()]),
11392 resolve_provider: Some(true),
11393 ..Default::default()
11394 }),
11395 ..Default::default()
11396 },
11397 cx,
11398 )
11399 .await;
11400
11401 cx.set_state("fn main() { let a = 2ˇ; }");
11402 cx.simulate_keystroke(".");
11403 let completion_item = lsp::CompletionItem {
11404 label: "Some".into(),
11405 kind: Some(lsp::CompletionItemKind::SNIPPET),
11406 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11407 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11408 kind: lsp::MarkupKind::Markdown,
11409 value: "```rust\nSome(2)\n```".to_string(),
11410 })),
11411 deprecated: Some(false),
11412 sort_text: Some("Some".to_string()),
11413 filter_text: Some("Some".to_string()),
11414 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11415 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11416 range: lsp::Range {
11417 start: lsp::Position {
11418 line: 0,
11419 character: 22,
11420 },
11421 end: lsp::Position {
11422 line: 0,
11423 character: 22,
11424 },
11425 },
11426 new_text: "Some(2)".to_string(),
11427 })),
11428 additional_text_edits: Some(vec![lsp::TextEdit {
11429 range: lsp::Range {
11430 start: lsp::Position {
11431 line: 0,
11432 character: 20,
11433 },
11434 end: lsp::Position {
11435 line: 0,
11436 character: 22,
11437 },
11438 },
11439 new_text: "".to_string(),
11440 }]),
11441 ..Default::default()
11442 };
11443
11444 let closure_completion_item = completion_item.clone();
11445 let counter = Arc::new(AtomicUsize::new(0));
11446 let counter_clone = counter.clone();
11447 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11448 let task_completion_item = closure_completion_item.clone();
11449 counter_clone.fetch_add(1, atomic::Ordering::Release);
11450 async move {
11451 Ok(Some(lsp::CompletionResponse::Array(vec![
11452 task_completion_item,
11453 ])))
11454 }
11455 });
11456
11457 cx.condition(|editor, _| editor.context_menu_visible())
11458 .await;
11459 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11460 assert!(request.next().await.is_some());
11461 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11462
11463 cx.simulate_keystrokes("S o m");
11464 cx.condition(|editor, _| editor.context_menu_visible())
11465 .await;
11466 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11467 assert!(request.next().await.is_some());
11468 assert!(request.next().await.is_some());
11469 assert!(request.next().await.is_some());
11470 request.close();
11471 assert!(request.next().await.is_none());
11472 assert_eq!(
11473 counter.load(atomic::Ordering::Acquire),
11474 4,
11475 "With the completions menu open, only one LSP request should happen per input"
11476 );
11477}
11478
11479#[gpui::test]
11480async fn test_toggle_comment(cx: &mut TestAppContext) {
11481 init_test(cx, |_| {});
11482 let mut cx = EditorTestContext::new(cx).await;
11483 let language = Arc::new(Language::new(
11484 LanguageConfig {
11485 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11486 ..Default::default()
11487 },
11488 Some(tree_sitter_rust::LANGUAGE.into()),
11489 ));
11490 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11491
11492 // If multiple selections intersect a line, the line is only toggled once.
11493 cx.set_state(indoc! {"
11494 fn a() {
11495 «//b();
11496 ˇ»// «c();
11497 //ˇ» d();
11498 }
11499 "});
11500
11501 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11502
11503 cx.assert_editor_state(indoc! {"
11504 fn a() {
11505 «b();
11506 c();
11507 ˇ» d();
11508 }
11509 "});
11510
11511 // The comment prefix is inserted at the same column for every line in a
11512 // selection.
11513 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11514
11515 cx.assert_editor_state(indoc! {"
11516 fn a() {
11517 // «b();
11518 // c();
11519 ˇ»// d();
11520 }
11521 "});
11522
11523 // If a selection ends at the beginning of a line, that line is not toggled.
11524 cx.set_selections_state(indoc! {"
11525 fn a() {
11526 // b();
11527 «// c();
11528 ˇ» // d();
11529 }
11530 "});
11531
11532 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11533
11534 cx.assert_editor_state(indoc! {"
11535 fn a() {
11536 // b();
11537 «c();
11538 ˇ» // d();
11539 }
11540 "});
11541
11542 // If a selection span a single line and is empty, the line is toggled.
11543 cx.set_state(indoc! {"
11544 fn a() {
11545 a();
11546 b();
11547 ˇ
11548 }
11549 "});
11550
11551 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11552
11553 cx.assert_editor_state(indoc! {"
11554 fn a() {
11555 a();
11556 b();
11557 //•ˇ
11558 }
11559 "});
11560
11561 // If a selection span multiple lines, empty lines are not toggled.
11562 cx.set_state(indoc! {"
11563 fn a() {
11564 «a();
11565
11566 c();ˇ»
11567 }
11568 "});
11569
11570 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11571
11572 cx.assert_editor_state(indoc! {"
11573 fn a() {
11574 // «a();
11575
11576 // c();ˇ»
11577 }
11578 "});
11579
11580 // If a selection includes multiple comment prefixes, all lines are uncommented.
11581 cx.set_state(indoc! {"
11582 fn a() {
11583 «// a();
11584 /// b();
11585 //! c();ˇ»
11586 }
11587 "});
11588
11589 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11590
11591 cx.assert_editor_state(indoc! {"
11592 fn a() {
11593 «a();
11594 b();
11595 c();ˇ»
11596 }
11597 "});
11598}
11599
11600#[gpui::test]
11601async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11602 init_test(cx, |_| {});
11603 let mut cx = EditorTestContext::new(cx).await;
11604 let language = Arc::new(Language::new(
11605 LanguageConfig {
11606 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11607 ..Default::default()
11608 },
11609 Some(tree_sitter_rust::LANGUAGE.into()),
11610 ));
11611 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11612
11613 let toggle_comments = &ToggleComments {
11614 advance_downwards: false,
11615 ignore_indent: true,
11616 };
11617
11618 // If multiple selections intersect a line, the line is only toggled once.
11619 cx.set_state(indoc! {"
11620 fn a() {
11621 // «b();
11622 // c();
11623 // ˇ» d();
11624 }
11625 "});
11626
11627 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11628
11629 cx.assert_editor_state(indoc! {"
11630 fn a() {
11631 «b();
11632 c();
11633 ˇ» d();
11634 }
11635 "});
11636
11637 // The comment prefix is inserted at the beginning of each line
11638 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11639
11640 cx.assert_editor_state(indoc! {"
11641 fn a() {
11642 // «b();
11643 // c();
11644 // ˇ» d();
11645 }
11646 "});
11647
11648 // If a selection ends at the beginning of a line, that line is not toggled.
11649 cx.set_selections_state(indoc! {"
11650 fn a() {
11651 // b();
11652 // «c();
11653 ˇ»// d();
11654 }
11655 "});
11656
11657 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11658
11659 cx.assert_editor_state(indoc! {"
11660 fn a() {
11661 // b();
11662 «c();
11663 ˇ»// d();
11664 }
11665 "});
11666
11667 // If a selection span a single line and is empty, the line is toggled.
11668 cx.set_state(indoc! {"
11669 fn a() {
11670 a();
11671 b();
11672 ˇ
11673 }
11674 "});
11675
11676 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11677
11678 cx.assert_editor_state(indoc! {"
11679 fn a() {
11680 a();
11681 b();
11682 //ˇ
11683 }
11684 "});
11685
11686 // If a selection span multiple lines, empty lines are not toggled.
11687 cx.set_state(indoc! {"
11688 fn a() {
11689 «a();
11690
11691 c();ˇ»
11692 }
11693 "});
11694
11695 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11696
11697 cx.assert_editor_state(indoc! {"
11698 fn a() {
11699 // «a();
11700
11701 // c();ˇ»
11702 }
11703 "});
11704
11705 // If a selection includes multiple comment prefixes, all lines are uncommented.
11706 cx.set_state(indoc! {"
11707 fn a() {
11708 // «a();
11709 /// b();
11710 //! c();ˇ»
11711 }
11712 "});
11713
11714 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11715
11716 cx.assert_editor_state(indoc! {"
11717 fn a() {
11718 «a();
11719 b();
11720 c();ˇ»
11721 }
11722 "});
11723}
11724
11725#[gpui::test]
11726async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11727 init_test(cx, |_| {});
11728
11729 let language = Arc::new(Language::new(
11730 LanguageConfig {
11731 line_comments: vec!["// ".into()],
11732 ..Default::default()
11733 },
11734 Some(tree_sitter_rust::LANGUAGE.into()),
11735 ));
11736
11737 let mut cx = EditorTestContext::new(cx).await;
11738
11739 cx.language_registry().add(language.clone());
11740 cx.update_buffer(|buffer, cx| {
11741 buffer.set_language(Some(language), cx);
11742 });
11743
11744 let toggle_comments = &ToggleComments {
11745 advance_downwards: true,
11746 ignore_indent: false,
11747 };
11748
11749 // Single cursor on one line -> advance
11750 // Cursor moves horizontally 3 characters as well on non-blank line
11751 cx.set_state(indoc!(
11752 "fn a() {
11753 ˇdog();
11754 cat();
11755 }"
11756 ));
11757 cx.update_editor(|editor, window, cx| {
11758 editor.toggle_comments(toggle_comments, window, cx);
11759 });
11760 cx.assert_editor_state(indoc!(
11761 "fn a() {
11762 // dog();
11763 catˇ();
11764 }"
11765 ));
11766
11767 // Single selection on one line -> don't advance
11768 cx.set_state(indoc!(
11769 "fn a() {
11770 «dog()ˇ»;
11771 cat();
11772 }"
11773 ));
11774 cx.update_editor(|editor, window, cx| {
11775 editor.toggle_comments(toggle_comments, window, cx);
11776 });
11777 cx.assert_editor_state(indoc!(
11778 "fn a() {
11779 // «dog()ˇ»;
11780 cat();
11781 }"
11782 ));
11783
11784 // Multiple cursors on one line -> advance
11785 cx.set_state(indoc!(
11786 "fn a() {
11787 ˇdˇog();
11788 cat();
11789 }"
11790 ));
11791 cx.update_editor(|editor, window, cx| {
11792 editor.toggle_comments(toggle_comments, window, cx);
11793 });
11794 cx.assert_editor_state(indoc!(
11795 "fn a() {
11796 // dog();
11797 catˇ(ˇ);
11798 }"
11799 ));
11800
11801 // Multiple cursors on one line, with selection -> don't advance
11802 cx.set_state(indoc!(
11803 "fn a() {
11804 ˇdˇog«()ˇ»;
11805 cat();
11806 }"
11807 ));
11808 cx.update_editor(|editor, window, cx| {
11809 editor.toggle_comments(toggle_comments, window, cx);
11810 });
11811 cx.assert_editor_state(indoc!(
11812 "fn a() {
11813 // ˇdˇog«()ˇ»;
11814 cat();
11815 }"
11816 ));
11817
11818 // Single cursor on one line -> advance
11819 // Cursor moves to column 0 on blank line
11820 cx.set_state(indoc!(
11821 "fn a() {
11822 ˇdog();
11823
11824 cat();
11825 }"
11826 ));
11827 cx.update_editor(|editor, window, cx| {
11828 editor.toggle_comments(toggle_comments, window, cx);
11829 });
11830 cx.assert_editor_state(indoc!(
11831 "fn a() {
11832 // dog();
11833 ˇ
11834 cat();
11835 }"
11836 ));
11837
11838 // Single cursor on one line -> advance
11839 // Cursor starts and ends at column 0
11840 cx.set_state(indoc!(
11841 "fn a() {
11842 ˇ dog();
11843 cat();
11844 }"
11845 ));
11846 cx.update_editor(|editor, window, cx| {
11847 editor.toggle_comments(toggle_comments, window, cx);
11848 });
11849 cx.assert_editor_state(indoc!(
11850 "fn a() {
11851 // dog();
11852 ˇ cat();
11853 }"
11854 ));
11855}
11856
11857#[gpui::test]
11858async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11859 init_test(cx, |_| {});
11860
11861 let mut cx = EditorTestContext::new(cx).await;
11862
11863 let html_language = Arc::new(
11864 Language::new(
11865 LanguageConfig {
11866 name: "HTML".into(),
11867 block_comment: Some(("<!-- ".into(), " -->".into())),
11868 ..Default::default()
11869 },
11870 Some(tree_sitter_html::LANGUAGE.into()),
11871 )
11872 .with_injection_query(
11873 r#"
11874 (script_element
11875 (raw_text) @injection.content
11876 (#set! injection.language "javascript"))
11877 "#,
11878 )
11879 .unwrap(),
11880 );
11881
11882 let javascript_language = Arc::new(Language::new(
11883 LanguageConfig {
11884 name: "JavaScript".into(),
11885 line_comments: vec!["// ".into()],
11886 ..Default::default()
11887 },
11888 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11889 ));
11890
11891 cx.language_registry().add(html_language.clone());
11892 cx.language_registry().add(javascript_language.clone());
11893 cx.update_buffer(|buffer, cx| {
11894 buffer.set_language(Some(html_language), cx);
11895 });
11896
11897 // Toggle comments for empty selections
11898 cx.set_state(
11899 &r#"
11900 <p>A</p>ˇ
11901 <p>B</p>ˇ
11902 <p>C</p>ˇ
11903 "#
11904 .unindent(),
11905 );
11906 cx.update_editor(|editor, window, cx| {
11907 editor.toggle_comments(&ToggleComments::default(), window, cx)
11908 });
11909 cx.assert_editor_state(
11910 &r#"
11911 <!-- <p>A</p>ˇ -->
11912 <!-- <p>B</p>ˇ -->
11913 <!-- <p>C</p>ˇ -->
11914 "#
11915 .unindent(),
11916 );
11917 cx.update_editor(|editor, window, cx| {
11918 editor.toggle_comments(&ToggleComments::default(), window, cx)
11919 });
11920 cx.assert_editor_state(
11921 &r#"
11922 <p>A</p>ˇ
11923 <p>B</p>ˇ
11924 <p>C</p>ˇ
11925 "#
11926 .unindent(),
11927 );
11928
11929 // Toggle comments for mixture of empty and non-empty selections, where
11930 // multiple selections occupy a given line.
11931 cx.set_state(
11932 &r#"
11933 <p>A«</p>
11934 <p>ˇ»B</p>ˇ
11935 <p>C«</p>
11936 <p>ˇ»D</p>ˇ
11937 "#
11938 .unindent(),
11939 );
11940
11941 cx.update_editor(|editor, window, cx| {
11942 editor.toggle_comments(&ToggleComments::default(), window, cx)
11943 });
11944 cx.assert_editor_state(
11945 &r#"
11946 <!-- <p>A«</p>
11947 <p>ˇ»B</p>ˇ -->
11948 <!-- <p>C«</p>
11949 <p>ˇ»D</p>ˇ -->
11950 "#
11951 .unindent(),
11952 );
11953 cx.update_editor(|editor, window, cx| {
11954 editor.toggle_comments(&ToggleComments::default(), window, cx)
11955 });
11956 cx.assert_editor_state(
11957 &r#"
11958 <p>A«</p>
11959 <p>ˇ»B</p>ˇ
11960 <p>C«</p>
11961 <p>ˇ»D</p>ˇ
11962 "#
11963 .unindent(),
11964 );
11965
11966 // Toggle comments when different languages are active for different
11967 // selections.
11968 cx.set_state(
11969 &r#"
11970 ˇ<script>
11971 ˇvar x = new Y();
11972 ˇ</script>
11973 "#
11974 .unindent(),
11975 );
11976 cx.executor().run_until_parked();
11977 cx.update_editor(|editor, window, cx| {
11978 editor.toggle_comments(&ToggleComments::default(), window, cx)
11979 });
11980 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11981 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11982 cx.assert_editor_state(
11983 &r#"
11984 <!-- ˇ<script> -->
11985 // ˇvar x = new Y();
11986 <!-- ˇ</script> -->
11987 "#
11988 .unindent(),
11989 );
11990}
11991
11992#[gpui::test]
11993fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11994 init_test(cx, |_| {});
11995
11996 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11997 let multibuffer = cx.new(|cx| {
11998 let mut multibuffer = MultiBuffer::new(ReadWrite);
11999 multibuffer.push_excerpts(
12000 buffer.clone(),
12001 [
12002 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12003 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12004 ],
12005 cx,
12006 );
12007 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12008 multibuffer
12009 });
12010
12011 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12012 editor.update_in(cx, |editor, window, cx| {
12013 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12014 editor.change_selections(None, window, cx, |s| {
12015 s.select_ranges([
12016 Point::new(0, 0)..Point::new(0, 0),
12017 Point::new(1, 0)..Point::new(1, 0),
12018 ])
12019 });
12020
12021 editor.handle_input("X", window, cx);
12022 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12023 assert_eq!(
12024 editor.selections.ranges(cx),
12025 [
12026 Point::new(0, 1)..Point::new(0, 1),
12027 Point::new(1, 1)..Point::new(1, 1),
12028 ]
12029 );
12030
12031 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12032 editor.change_selections(None, window, cx, |s| {
12033 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12034 });
12035 editor.backspace(&Default::default(), window, cx);
12036 assert_eq!(editor.text(cx), "Xa\nbbb");
12037 assert_eq!(
12038 editor.selections.ranges(cx),
12039 [Point::new(1, 0)..Point::new(1, 0)]
12040 );
12041
12042 editor.change_selections(None, window, cx, |s| {
12043 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12044 });
12045 editor.backspace(&Default::default(), window, cx);
12046 assert_eq!(editor.text(cx), "X\nbb");
12047 assert_eq!(
12048 editor.selections.ranges(cx),
12049 [Point::new(0, 1)..Point::new(0, 1)]
12050 );
12051 });
12052}
12053
12054#[gpui::test]
12055fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12056 init_test(cx, |_| {});
12057
12058 let markers = vec![('[', ']').into(), ('(', ')').into()];
12059 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12060 indoc! {"
12061 [aaaa
12062 (bbbb]
12063 cccc)",
12064 },
12065 markers.clone(),
12066 );
12067 let excerpt_ranges = markers.into_iter().map(|marker| {
12068 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12069 ExcerptRange::new(context.clone())
12070 });
12071 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12072 let multibuffer = cx.new(|cx| {
12073 let mut multibuffer = MultiBuffer::new(ReadWrite);
12074 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12075 multibuffer
12076 });
12077
12078 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12079 editor.update_in(cx, |editor, window, cx| {
12080 let (expected_text, selection_ranges) = marked_text_ranges(
12081 indoc! {"
12082 aaaa
12083 bˇbbb
12084 bˇbbˇb
12085 cccc"
12086 },
12087 true,
12088 );
12089 assert_eq!(editor.text(cx), expected_text);
12090 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12091
12092 editor.handle_input("X", window, cx);
12093
12094 let (expected_text, expected_selections) = marked_text_ranges(
12095 indoc! {"
12096 aaaa
12097 bXˇbbXb
12098 bXˇbbXˇb
12099 cccc"
12100 },
12101 false,
12102 );
12103 assert_eq!(editor.text(cx), expected_text);
12104 assert_eq!(editor.selections.ranges(cx), expected_selections);
12105
12106 editor.newline(&Newline, window, cx);
12107 let (expected_text, expected_selections) = marked_text_ranges(
12108 indoc! {"
12109 aaaa
12110 bX
12111 ˇbbX
12112 b
12113 bX
12114 ˇbbX
12115 ˇb
12116 cccc"
12117 },
12118 false,
12119 );
12120 assert_eq!(editor.text(cx), expected_text);
12121 assert_eq!(editor.selections.ranges(cx), expected_selections);
12122 });
12123}
12124
12125#[gpui::test]
12126fn test_refresh_selections(cx: &mut TestAppContext) {
12127 init_test(cx, |_| {});
12128
12129 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12130 let mut excerpt1_id = None;
12131 let multibuffer = cx.new(|cx| {
12132 let mut multibuffer = MultiBuffer::new(ReadWrite);
12133 excerpt1_id = multibuffer
12134 .push_excerpts(
12135 buffer.clone(),
12136 [
12137 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12138 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12139 ],
12140 cx,
12141 )
12142 .into_iter()
12143 .next();
12144 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12145 multibuffer
12146 });
12147
12148 let editor = cx.add_window(|window, cx| {
12149 let mut editor = build_editor(multibuffer.clone(), window, cx);
12150 let snapshot = editor.snapshot(window, cx);
12151 editor.change_selections(None, window, cx, |s| {
12152 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12153 });
12154 editor.begin_selection(
12155 Point::new(2, 1).to_display_point(&snapshot),
12156 true,
12157 1,
12158 window,
12159 cx,
12160 );
12161 assert_eq!(
12162 editor.selections.ranges(cx),
12163 [
12164 Point::new(1, 3)..Point::new(1, 3),
12165 Point::new(2, 1)..Point::new(2, 1),
12166 ]
12167 );
12168 editor
12169 });
12170
12171 // Refreshing selections is a no-op when excerpts haven't changed.
12172 _ = editor.update(cx, |editor, window, cx| {
12173 editor.change_selections(None, window, cx, |s| s.refresh());
12174 assert_eq!(
12175 editor.selections.ranges(cx),
12176 [
12177 Point::new(1, 3)..Point::new(1, 3),
12178 Point::new(2, 1)..Point::new(2, 1),
12179 ]
12180 );
12181 });
12182
12183 multibuffer.update(cx, |multibuffer, cx| {
12184 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12185 });
12186 _ = editor.update(cx, |editor, window, cx| {
12187 // Removing an excerpt causes the first selection to become degenerate.
12188 assert_eq!(
12189 editor.selections.ranges(cx),
12190 [
12191 Point::new(0, 0)..Point::new(0, 0),
12192 Point::new(0, 1)..Point::new(0, 1)
12193 ]
12194 );
12195
12196 // Refreshing selections will relocate the first selection to the original buffer
12197 // location.
12198 editor.change_selections(None, window, cx, |s| s.refresh());
12199 assert_eq!(
12200 editor.selections.ranges(cx),
12201 [
12202 Point::new(0, 1)..Point::new(0, 1),
12203 Point::new(0, 3)..Point::new(0, 3)
12204 ]
12205 );
12206 assert!(editor.selections.pending_anchor().is_some());
12207 });
12208}
12209
12210#[gpui::test]
12211fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12212 init_test(cx, |_| {});
12213
12214 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12215 let mut excerpt1_id = None;
12216 let multibuffer = cx.new(|cx| {
12217 let mut multibuffer = MultiBuffer::new(ReadWrite);
12218 excerpt1_id = multibuffer
12219 .push_excerpts(
12220 buffer.clone(),
12221 [
12222 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12223 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12224 ],
12225 cx,
12226 )
12227 .into_iter()
12228 .next();
12229 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12230 multibuffer
12231 });
12232
12233 let editor = cx.add_window(|window, cx| {
12234 let mut editor = build_editor(multibuffer.clone(), window, cx);
12235 let snapshot = editor.snapshot(window, cx);
12236 editor.begin_selection(
12237 Point::new(1, 3).to_display_point(&snapshot),
12238 false,
12239 1,
12240 window,
12241 cx,
12242 );
12243 assert_eq!(
12244 editor.selections.ranges(cx),
12245 [Point::new(1, 3)..Point::new(1, 3)]
12246 );
12247 editor
12248 });
12249
12250 multibuffer.update(cx, |multibuffer, cx| {
12251 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12252 });
12253 _ = editor.update(cx, |editor, window, cx| {
12254 assert_eq!(
12255 editor.selections.ranges(cx),
12256 [Point::new(0, 0)..Point::new(0, 0)]
12257 );
12258
12259 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12260 editor.change_selections(None, window, cx, |s| s.refresh());
12261 assert_eq!(
12262 editor.selections.ranges(cx),
12263 [Point::new(0, 3)..Point::new(0, 3)]
12264 );
12265 assert!(editor.selections.pending_anchor().is_some());
12266 });
12267}
12268
12269#[gpui::test]
12270async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12271 init_test(cx, |_| {});
12272
12273 let language = Arc::new(
12274 Language::new(
12275 LanguageConfig {
12276 brackets: BracketPairConfig {
12277 pairs: vec![
12278 BracketPair {
12279 start: "{".to_string(),
12280 end: "}".to_string(),
12281 close: true,
12282 surround: true,
12283 newline: true,
12284 },
12285 BracketPair {
12286 start: "/* ".to_string(),
12287 end: " */".to_string(),
12288 close: true,
12289 surround: true,
12290 newline: true,
12291 },
12292 ],
12293 ..Default::default()
12294 },
12295 ..Default::default()
12296 },
12297 Some(tree_sitter_rust::LANGUAGE.into()),
12298 )
12299 .with_indents_query("")
12300 .unwrap(),
12301 );
12302
12303 let text = concat!(
12304 "{ }\n", //
12305 " x\n", //
12306 " /* */\n", //
12307 "x\n", //
12308 "{{} }\n", //
12309 );
12310
12311 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12312 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12313 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12314 editor
12315 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12316 .await;
12317
12318 editor.update_in(cx, |editor, window, cx| {
12319 editor.change_selections(None, window, cx, |s| {
12320 s.select_display_ranges([
12321 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12322 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12323 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12324 ])
12325 });
12326 editor.newline(&Newline, window, cx);
12327
12328 assert_eq!(
12329 editor.buffer().read(cx).read(cx).text(),
12330 concat!(
12331 "{ \n", // Suppress rustfmt
12332 "\n", //
12333 "}\n", //
12334 " x\n", //
12335 " /* \n", //
12336 " \n", //
12337 " */\n", //
12338 "x\n", //
12339 "{{} \n", //
12340 "}\n", //
12341 )
12342 );
12343 });
12344}
12345
12346#[gpui::test]
12347fn test_highlighted_ranges(cx: &mut TestAppContext) {
12348 init_test(cx, |_| {});
12349
12350 let editor = cx.add_window(|window, cx| {
12351 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12352 build_editor(buffer.clone(), window, cx)
12353 });
12354
12355 _ = editor.update(cx, |editor, window, cx| {
12356 struct Type1;
12357 struct Type2;
12358
12359 let buffer = editor.buffer.read(cx).snapshot(cx);
12360
12361 let anchor_range =
12362 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12363
12364 editor.highlight_background::<Type1>(
12365 &[
12366 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12367 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12368 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12369 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12370 ],
12371 |_| Hsla::red(),
12372 cx,
12373 );
12374 editor.highlight_background::<Type2>(
12375 &[
12376 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12377 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12378 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12379 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12380 ],
12381 |_| Hsla::green(),
12382 cx,
12383 );
12384
12385 let snapshot = editor.snapshot(window, cx);
12386 let mut highlighted_ranges = editor.background_highlights_in_range(
12387 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12388 &snapshot,
12389 cx.theme().colors(),
12390 );
12391 // Enforce a consistent ordering based on color without relying on the ordering of the
12392 // highlight's `TypeId` which is non-executor.
12393 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12394 assert_eq!(
12395 highlighted_ranges,
12396 &[
12397 (
12398 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12399 Hsla::red(),
12400 ),
12401 (
12402 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12403 Hsla::red(),
12404 ),
12405 (
12406 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12407 Hsla::green(),
12408 ),
12409 (
12410 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12411 Hsla::green(),
12412 ),
12413 ]
12414 );
12415 assert_eq!(
12416 editor.background_highlights_in_range(
12417 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12418 &snapshot,
12419 cx.theme().colors(),
12420 ),
12421 &[(
12422 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12423 Hsla::red(),
12424 )]
12425 );
12426 });
12427}
12428
12429#[gpui::test]
12430async fn test_following(cx: &mut TestAppContext) {
12431 init_test(cx, |_| {});
12432
12433 let fs = FakeFs::new(cx.executor());
12434 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12435
12436 let buffer = project.update(cx, |project, cx| {
12437 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12438 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12439 });
12440 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12441 let follower = cx.update(|cx| {
12442 cx.open_window(
12443 WindowOptions {
12444 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12445 gpui::Point::new(px(0.), px(0.)),
12446 gpui::Point::new(px(10.), px(80.)),
12447 ))),
12448 ..Default::default()
12449 },
12450 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12451 )
12452 .unwrap()
12453 });
12454
12455 let is_still_following = Rc::new(RefCell::new(true));
12456 let follower_edit_event_count = Rc::new(RefCell::new(0));
12457 let pending_update = Rc::new(RefCell::new(None));
12458 let leader_entity = leader.root(cx).unwrap();
12459 let follower_entity = follower.root(cx).unwrap();
12460 _ = follower.update(cx, {
12461 let update = pending_update.clone();
12462 let is_still_following = is_still_following.clone();
12463 let follower_edit_event_count = follower_edit_event_count.clone();
12464 |_, window, cx| {
12465 cx.subscribe_in(
12466 &leader_entity,
12467 window,
12468 move |_, leader, event, window, cx| {
12469 leader.read(cx).add_event_to_update_proto(
12470 event,
12471 &mut update.borrow_mut(),
12472 window,
12473 cx,
12474 );
12475 },
12476 )
12477 .detach();
12478
12479 cx.subscribe_in(
12480 &follower_entity,
12481 window,
12482 move |_, _, event: &EditorEvent, _window, _cx| {
12483 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12484 *is_still_following.borrow_mut() = false;
12485 }
12486
12487 if let EditorEvent::BufferEdited = event {
12488 *follower_edit_event_count.borrow_mut() += 1;
12489 }
12490 },
12491 )
12492 .detach();
12493 }
12494 });
12495
12496 // Update the selections only
12497 _ = leader.update(cx, |leader, window, cx| {
12498 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12499 });
12500 follower
12501 .update(cx, |follower, window, cx| {
12502 follower.apply_update_proto(
12503 &project,
12504 pending_update.borrow_mut().take().unwrap(),
12505 window,
12506 cx,
12507 )
12508 })
12509 .unwrap()
12510 .await
12511 .unwrap();
12512 _ = follower.update(cx, |follower, _, cx| {
12513 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12514 });
12515 assert!(*is_still_following.borrow());
12516 assert_eq!(*follower_edit_event_count.borrow(), 0);
12517
12518 // Update the scroll position only
12519 _ = leader.update(cx, |leader, window, cx| {
12520 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12521 });
12522 follower
12523 .update(cx, |follower, window, cx| {
12524 follower.apply_update_proto(
12525 &project,
12526 pending_update.borrow_mut().take().unwrap(),
12527 window,
12528 cx,
12529 )
12530 })
12531 .unwrap()
12532 .await
12533 .unwrap();
12534 assert_eq!(
12535 follower
12536 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12537 .unwrap(),
12538 gpui::Point::new(1.5, 3.5)
12539 );
12540 assert!(*is_still_following.borrow());
12541 assert_eq!(*follower_edit_event_count.borrow(), 0);
12542
12543 // Update the selections and scroll position. The follower's scroll position is updated
12544 // via autoscroll, not via the leader's exact scroll position.
12545 _ = leader.update(cx, |leader, window, cx| {
12546 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12547 leader.request_autoscroll(Autoscroll::newest(), cx);
12548 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12549 });
12550 follower
12551 .update(cx, |follower, window, cx| {
12552 follower.apply_update_proto(
12553 &project,
12554 pending_update.borrow_mut().take().unwrap(),
12555 window,
12556 cx,
12557 )
12558 })
12559 .unwrap()
12560 .await
12561 .unwrap();
12562 _ = follower.update(cx, |follower, _, cx| {
12563 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12564 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12565 });
12566 assert!(*is_still_following.borrow());
12567
12568 // Creating a pending selection that precedes another selection
12569 _ = leader.update(cx, |leader, window, cx| {
12570 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12571 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12572 });
12573 follower
12574 .update(cx, |follower, window, cx| {
12575 follower.apply_update_proto(
12576 &project,
12577 pending_update.borrow_mut().take().unwrap(),
12578 window,
12579 cx,
12580 )
12581 })
12582 .unwrap()
12583 .await
12584 .unwrap();
12585 _ = follower.update(cx, |follower, _, cx| {
12586 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12587 });
12588 assert!(*is_still_following.borrow());
12589
12590 // Extend the pending selection so that it surrounds another selection
12591 _ = leader.update(cx, |leader, window, cx| {
12592 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12593 });
12594 follower
12595 .update(cx, |follower, window, cx| {
12596 follower.apply_update_proto(
12597 &project,
12598 pending_update.borrow_mut().take().unwrap(),
12599 window,
12600 cx,
12601 )
12602 })
12603 .unwrap()
12604 .await
12605 .unwrap();
12606 _ = follower.update(cx, |follower, _, cx| {
12607 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12608 });
12609
12610 // Scrolling locally breaks the follow
12611 _ = follower.update(cx, |follower, window, cx| {
12612 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12613 follower.set_scroll_anchor(
12614 ScrollAnchor {
12615 anchor: top_anchor,
12616 offset: gpui::Point::new(0.0, 0.5),
12617 },
12618 window,
12619 cx,
12620 );
12621 });
12622 assert!(!(*is_still_following.borrow()));
12623}
12624
12625#[gpui::test]
12626async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12627 init_test(cx, |_| {});
12628
12629 let fs = FakeFs::new(cx.executor());
12630 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12631 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12632 let pane = workspace
12633 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12634 .unwrap();
12635
12636 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12637
12638 let leader = pane.update_in(cx, |_, window, cx| {
12639 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12640 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12641 });
12642
12643 // Start following the editor when it has no excerpts.
12644 let mut state_message =
12645 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12646 let workspace_entity = workspace.root(cx).unwrap();
12647 let follower_1 = cx
12648 .update_window(*workspace.deref(), |_, window, cx| {
12649 Editor::from_state_proto(
12650 workspace_entity,
12651 ViewId {
12652 creator: Default::default(),
12653 id: 0,
12654 },
12655 &mut state_message,
12656 window,
12657 cx,
12658 )
12659 })
12660 .unwrap()
12661 .unwrap()
12662 .await
12663 .unwrap();
12664
12665 let update_message = Rc::new(RefCell::new(None));
12666 follower_1.update_in(cx, {
12667 let update = update_message.clone();
12668 |_, window, cx| {
12669 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12670 leader.read(cx).add_event_to_update_proto(
12671 event,
12672 &mut update.borrow_mut(),
12673 window,
12674 cx,
12675 );
12676 })
12677 .detach();
12678 }
12679 });
12680
12681 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12682 (
12683 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12684 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12685 )
12686 });
12687
12688 // Insert some excerpts.
12689 leader.update(cx, |leader, cx| {
12690 leader.buffer.update(cx, |multibuffer, cx| {
12691 multibuffer.set_excerpts_for_path(
12692 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
12693 buffer_1.clone(),
12694 vec![
12695 Point::row_range(0..3),
12696 Point::row_range(1..6),
12697 Point::row_range(12..15),
12698 ],
12699 0,
12700 cx,
12701 );
12702 multibuffer.set_excerpts_for_path(
12703 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
12704 buffer_2.clone(),
12705 vec![Point::row_range(0..6), Point::row_range(8..12)],
12706 0,
12707 cx,
12708 );
12709 });
12710 });
12711
12712 // Apply the update of adding the excerpts.
12713 follower_1
12714 .update_in(cx, |follower, window, cx| {
12715 follower.apply_update_proto(
12716 &project,
12717 update_message.borrow().clone().unwrap(),
12718 window,
12719 cx,
12720 )
12721 })
12722 .await
12723 .unwrap();
12724 assert_eq!(
12725 follower_1.update(cx, |editor, cx| editor.text(cx)),
12726 leader.update(cx, |editor, cx| editor.text(cx))
12727 );
12728 update_message.borrow_mut().take();
12729
12730 // Start following separately after it already has excerpts.
12731 let mut state_message =
12732 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12733 let workspace_entity = workspace.root(cx).unwrap();
12734 let follower_2 = cx
12735 .update_window(*workspace.deref(), |_, window, cx| {
12736 Editor::from_state_proto(
12737 workspace_entity,
12738 ViewId {
12739 creator: Default::default(),
12740 id: 0,
12741 },
12742 &mut state_message,
12743 window,
12744 cx,
12745 )
12746 })
12747 .unwrap()
12748 .unwrap()
12749 .await
12750 .unwrap();
12751 assert_eq!(
12752 follower_2.update(cx, |editor, cx| editor.text(cx)),
12753 leader.update(cx, |editor, cx| editor.text(cx))
12754 );
12755
12756 // Remove some excerpts.
12757 leader.update(cx, |leader, cx| {
12758 leader.buffer.update(cx, |multibuffer, cx| {
12759 let excerpt_ids = multibuffer.excerpt_ids();
12760 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12761 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12762 });
12763 });
12764
12765 // Apply the update of removing the excerpts.
12766 follower_1
12767 .update_in(cx, |follower, window, cx| {
12768 follower.apply_update_proto(
12769 &project,
12770 update_message.borrow().clone().unwrap(),
12771 window,
12772 cx,
12773 )
12774 })
12775 .await
12776 .unwrap();
12777 follower_2
12778 .update_in(cx, |follower, window, cx| {
12779 follower.apply_update_proto(
12780 &project,
12781 update_message.borrow().clone().unwrap(),
12782 window,
12783 cx,
12784 )
12785 })
12786 .await
12787 .unwrap();
12788 update_message.borrow_mut().take();
12789 assert_eq!(
12790 follower_1.update(cx, |editor, cx| editor.text(cx)),
12791 leader.update(cx, |editor, cx| editor.text(cx))
12792 );
12793}
12794
12795#[gpui::test]
12796async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12797 init_test(cx, |_| {});
12798
12799 let mut cx = EditorTestContext::new(cx).await;
12800 let lsp_store =
12801 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12802
12803 cx.set_state(indoc! {"
12804 ˇfn func(abc def: i32) -> u32 {
12805 }
12806 "});
12807
12808 cx.update(|_, cx| {
12809 lsp_store.update(cx, |lsp_store, cx| {
12810 lsp_store
12811 .update_diagnostics(
12812 LanguageServerId(0),
12813 lsp::PublishDiagnosticsParams {
12814 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12815 version: None,
12816 diagnostics: vec![
12817 lsp::Diagnostic {
12818 range: lsp::Range::new(
12819 lsp::Position::new(0, 11),
12820 lsp::Position::new(0, 12),
12821 ),
12822 severity: Some(lsp::DiagnosticSeverity::ERROR),
12823 ..Default::default()
12824 },
12825 lsp::Diagnostic {
12826 range: lsp::Range::new(
12827 lsp::Position::new(0, 12),
12828 lsp::Position::new(0, 15),
12829 ),
12830 severity: Some(lsp::DiagnosticSeverity::ERROR),
12831 ..Default::default()
12832 },
12833 lsp::Diagnostic {
12834 range: lsp::Range::new(
12835 lsp::Position::new(0, 25),
12836 lsp::Position::new(0, 28),
12837 ),
12838 severity: Some(lsp::DiagnosticSeverity::ERROR),
12839 ..Default::default()
12840 },
12841 ],
12842 },
12843 &[],
12844 cx,
12845 )
12846 .unwrap()
12847 });
12848 });
12849
12850 executor.run_until_parked();
12851
12852 cx.update_editor(|editor, window, cx| {
12853 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12854 });
12855
12856 cx.assert_editor_state(indoc! {"
12857 fn func(abc def: i32) -> ˇu32 {
12858 }
12859 "});
12860
12861 cx.update_editor(|editor, window, cx| {
12862 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12863 });
12864
12865 cx.assert_editor_state(indoc! {"
12866 fn func(abc ˇdef: i32) -> u32 {
12867 }
12868 "});
12869
12870 cx.update_editor(|editor, window, cx| {
12871 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12872 });
12873
12874 cx.assert_editor_state(indoc! {"
12875 fn func(abcˇ def: i32) -> u32 {
12876 }
12877 "});
12878
12879 cx.update_editor(|editor, window, cx| {
12880 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12881 });
12882
12883 cx.assert_editor_state(indoc! {"
12884 fn func(abc def: i32) -> ˇu32 {
12885 }
12886 "});
12887}
12888
12889#[gpui::test]
12890async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12891 init_test(cx, |_| {});
12892
12893 let mut cx = EditorTestContext::new(cx).await;
12894
12895 let diff_base = r#"
12896 use some::mod;
12897
12898 const A: u32 = 42;
12899
12900 fn main() {
12901 println!("hello");
12902
12903 println!("world");
12904 }
12905 "#
12906 .unindent();
12907
12908 // Edits are modified, removed, modified, added
12909 cx.set_state(
12910 &r#"
12911 use some::modified;
12912
12913 ˇ
12914 fn main() {
12915 println!("hello there");
12916
12917 println!("around the");
12918 println!("world");
12919 }
12920 "#
12921 .unindent(),
12922 );
12923
12924 cx.set_head_text(&diff_base);
12925 executor.run_until_parked();
12926
12927 cx.update_editor(|editor, window, cx| {
12928 //Wrap around the bottom of the buffer
12929 for _ in 0..3 {
12930 editor.go_to_next_hunk(&GoToHunk, window, cx);
12931 }
12932 });
12933
12934 cx.assert_editor_state(
12935 &r#"
12936 ˇuse some::modified;
12937
12938
12939 fn main() {
12940 println!("hello there");
12941
12942 println!("around the");
12943 println!("world");
12944 }
12945 "#
12946 .unindent(),
12947 );
12948
12949 cx.update_editor(|editor, window, cx| {
12950 //Wrap around the top of the buffer
12951 for _ in 0..2 {
12952 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12953 }
12954 });
12955
12956 cx.assert_editor_state(
12957 &r#"
12958 use some::modified;
12959
12960
12961 fn main() {
12962 ˇ println!("hello there");
12963
12964 println!("around the");
12965 println!("world");
12966 }
12967 "#
12968 .unindent(),
12969 );
12970
12971 cx.update_editor(|editor, window, cx| {
12972 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12973 });
12974
12975 cx.assert_editor_state(
12976 &r#"
12977 use some::modified;
12978
12979 ˇ
12980 fn main() {
12981 println!("hello there");
12982
12983 println!("around the");
12984 println!("world");
12985 }
12986 "#
12987 .unindent(),
12988 );
12989
12990 cx.update_editor(|editor, window, cx| {
12991 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12992 });
12993
12994 cx.assert_editor_state(
12995 &r#"
12996 ˇuse some::modified;
12997
12998
12999 fn main() {
13000 println!("hello there");
13001
13002 println!("around the");
13003 println!("world");
13004 }
13005 "#
13006 .unindent(),
13007 );
13008
13009 cx.update_editor(|editor, window, cx| {
13010 for _ in 0..2 {
13011 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13012 }
13013 });
13014
13015 cx.assert_editor_state(
13016 &r#"
13017 use some::modified;
13018
13019
13020 fn main() {
13021 ˇ println!("hello there");
13022
13023 println!("around the");
13024 println!("world");
13025 }
13026 "#
13027 .unindent(),
13028 );
13029
13030 cx.update_editor(|editor, window, cx| {
13031 editor.fold(&Fold, window, cx);
13032 });
13033
13034 cx.update_editor(|editor, window, cx| {
13035 editor.go_to_next_hunk(&GoToHunk, window, cx);
13036 });
13037
13038 cx.assert_editor_state(
13039 &r#"
13040 ˇuse some::modified;
13041
13042
13043 fn main() {
13044 println!("hello there");
13045
13046 println!("around the");
13047 println!("world");
13048 }
13049 "#
13050 .unindent(),
13051 );
13052}
13053
13054#[test]
13055fn test_split_words() {
13056 fn split(text: &str) -> Vec<&str> {
13057 split_words(text).collect()
13058 }
13059
13060 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13061 assert_eq!(split("hello_world"), &["hello_", "world"]);
13062 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13063 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13064 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13065 assert_eq!(split("helloworld"), &["helloworld"]);
13066
13067 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13068}
13069
13070#[gpui::test]
13071async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13072 init_test(cx, |_| {});
13073
13074 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13075 let mut assert = |before, after| {
13076 let _state_context = cx.set_state(before);
13077 cx.run_until_parked();
13078 cx.update_editor(|editor, window, cx| {
13079 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13080 });
13081 cx.run_until_parked();
13082 cx.assert_editor_state(after);
13083 };
13084
13085 // Outside bracket jumps to outside of matching bracket
13086 assert("console.logˇ(var);", "console.log(var)ˇ;");
13087 assert("console.log(var)ˇ;", "console.logˇ(var);");
13088
13089 // Inside bracket jumps to inside of matching bracket
13090 assert("console.log(ˇvar);", "console.log(varˇ);");
13091 assert("console.log(varˇ);", "console.log(ˇvar);");
13092
13093 // When outside a bracket and inside, favor jumping to the inside bracket
13094 assert(
13095 "console.log('foo', [1, 2, 3]ˇ);",
13096 "console.log(ˇ'foo', [1, 2, 3]);",
13097 );
13098 assert(
13099 "console.log(ˇ'foo', [1, 2, 3]);",
13100 "console.log('foo', [1, 2, 3]ˇ);",
13101 );
13102
13103 // Bias forward if two options are equally likely
13104 assert(
13105 "let result = curried_fun()ˇ();",
13106 "let result = curried_fun()()ˇ;",
13107 );
13108
13109 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13110 assert(
13111 indoc! {"
13112 function test() {
13113 console.log('test')ˇ
13114 }"},
13115 indoc! {"
13116 function test() {
13117 console.logˇ('test')
13118 }"},
13119 );
13120}
13121
13122#[gpui::test]
13123async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13124 init_test(cx, |_| {});
13125
13126 let fs = FakeFs::new(cx.executor());
13127 fs.insert_tree(
13128 path!("/a"),
13129 json!({
13130 "main.rs": "fn main() { let a = 5; }",
13131 "other.rs": "// Test file",
13132 }),
13133 )
13134 .await;
13135 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13136
13137 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13138 language_registry.add(Arc::new(Language::new(
13139 LanguageConfig {
13140 name: "Rust".into(),
13141 matcher: LanguageMatcher {
13142 path_suffixes: vec!["rs".to_string()],
13143 ..Default::default()
13144 },
13145 brackets: BracketPairConfig {
13146 pairs: vec![BracketPair {
13147 start: "{".to_string(),
13148 end: "}".to_string(),
13149 close: true,
13150 surround: true,
13151 newline: true,
13152 }],
13153 disabled_scopes_by_bracket_ix: Vec::new(),
13154 },
13155 ..Default::default()
13156 },
13157 Some(tree_sitter_rust::LANGUAGE.into()),
13158 )));
13159 let mut fake_servers = language_registry.register_fake_lsp(
13160 "Rust",
13161 FakeLspAdapter {
13162 capabilities: lsp::ServerCapabilities {
13163 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13164 first_trigger_character: "{".to_string(),
13165 more_trigger_character: None,
13166 }),
13167 ..Default::default()
13168 },
13169 ..Default::default()
13170 },
13171 );
13172
13173 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13174
13175 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13176
13177 let worktree_id = workspace
13178 .update(cx, |workspace, _, cx| {
13179 workspace.project().update(cx, |project, cx| {
13180 project.worktrees(cx).next().unwrap().read(cx).id()
13181 })
13182 })
13183 .unwrap();
13184
13185 let buffer = project
13186 .update(cx, |project, cx| {
13187 project.open_local_buffer(path!("/a/main.rs"), cx)
13188 })
13189 .await
13190 .unwrap();
13191 let editor_handle = workspace
13192 .update(cx, |workspace, window, cx| {
13193 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13194 })
13195 .unwrap()
13196 .await
13197 .unwrap()
13198 .downcast::<Editor>()
13199 .unwrap();
13200
13201 cx.executor().start_waiting();
13202 let fake_server = fake_servers.next().await.unwrap();
13203
13204 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13205 |params, _| async move {
13206 assert_eq!(
13207 params.text_document_position.text_document.uri,
13208 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13209 );
13210 assert_eq!(
13211 params.text_document_position.position,
13212 lsp::Position::new(0, 21),
13213 );
13214
13215 Ok(Some(vec![lsp::TextEdit {
13216 new_text: "]".to_string(),
13217 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13218 }]))
13219 },
13220 );
13221
13222 editor_handle.update_in(cx, |editor, window, cx| {
13223 window.focus(&editor.focus_handle(cx));
13224 editor.change_selections(None, window, cx, |s| {
13225 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13226 });
13227 editor.handle_input("{", window, cx);
13228 });
13229
13230 cx.executor().run_until_parked();
13231
13232 buffer.update(cx, |buffer, _| {
13233 assert_eq!(
13234 buffer.text(),
13235 "fn main() { let a = {5}; }",
13236 "No extra braces from on type formatting should appear in the buffer"
13237 )
13238 });
13239}
13240
13241#[gpui::test]
13242async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13243 init_test(cx, |_| {});
13244
13245 let fs = FakeFs::new(cx.executor());
13246 fs.insert_tree(
13247 path!("/a"),
13248 json!({
13249 "main.rs": "fn main() { let a = 5; }",
13250 "other.rs": "// Test file",
13251 }),
13252 )
13253 .await;
13254
13255 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13256
13257 let server_restarts = Arc::new(AtomicUsize::new(0));
13258 let closure_restarts = Arc::clone(&server_restarts);
13259 let language_server_name = "test language server";
13260 let language_name: LanguageName = "Rust".into();
13261
13262 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13263 language_registry.add(Arc::new(Language::new(
13264 LanguageConfig {
13265 name: language_name.clone(),
13266 matcher: LanguageMatcher {
13267 path_suffixes: vec!["rs".to_string()],
13268 ..Default::default()
13269 },
13270 ..Default::default()
13271 },
13272 Some(tree_sitter_rust::LANGUAGE.into()),
13273 )));
13274 let mut fake_servers = language_registry.register_fake_lsp(
13275 "Rust",
13276 FakeLspAdapter {
13277 name: language_server_name,
13278 initialization_options: Some(json!({
13279 "testOptionValue": true
13280 })),
13281 initializer: Some(Box::new(move |fake_server| {
13282 let task_restarts = Arc::clone(&closure_restarts);
13283 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13284 task_restarts.fetch_add(1, atomic::Ordering::Release);
13285 futures::future::ready(Ok(()))
13286 });
13287 })),
13288 ..Default::default()
13289 },
13290 );
13291
13292 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13293 let _buffer = project
13294 .update(cx, |project, cx| {
13295 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13296 })
13297 .await
13298 .unwrap();
13299 let _fake_server = fake_servers.next().await.unwrap();
13300 update_test_language_settings(cx, |language_settings| {
13301 language_settings.languages.insert(
13302 language_name.clone(),
13303 LanguageSettingsContent {
13304 tab_size: NonZeroU32::new(8),
13305 ..Default::default()
13306 },
13307 );
13308 });
13309 cx.executor().run_until_parked();
13310 assert_eq!(
13311 server_restarts.load(atomic::Ordering::Acquire),
13312 0,
13313 "Should not restart LSP server on an unrelated change"
13314 );
13315
13316 update_test_project_settings(cx, |project_settings| {
13317 project_settings.lsp.insert(
13318 "Some other server name".into(),
13319 LspSettings {
13320 binary: None,
13321 settings: None,
13322 initialization_options: Some(json!({
13323 "some other init value": false
13324 })),
13325 enable_lsp_tasks: false,
13326 },
13327 );
13328 });
13329 cx.executor().run_until_parked();
13330 assert_eq!(
13331 server_restarts.load(atomic::Ordering::Acquire),
13332 0,
13333 "Should not restart LSP server on an unrelated LSP settings change"
13334 );
13335
13336 update_test_project_settings(cx, |project_settings| {
13337 project_settings.lsp.insert(
13338 language_server_name.into(),
13339 LspSettings {
13340 binary: None,
13341 settings: None,
13342 initialization_options: Some(json!({
13343 "anotherInitValue": false
13344 })),
13345 enable_lsp_tasks: false,
13346 },
13347 );
13348 });
13349 cx.executor().run_until_parked();
13350 assert_eq!(
13351 server_restarts.load(atomic::Ordering::Acquire),
13352 1,
13353 "Should restart LSP server on a related LSP settings change"
13354 );
13355
13356 update_test_project_settings(cx, |project_settings| {
13357 project_settings.lsp.insert(
13358 language_server_name.into(),
13359 LspSettings {
13360 binary: None,
13361 settings: None,
13362 initialization_options: Some(json!({
13363 "anotherInitValue": false
13364 })),
13365 enable_lsp_tasks: false,
13366 },
13367 );
13368 });
13369 cx.executor().run_until_parked();
13370 assert_eq!(
13371 server_restarts.load(atomic::Ordering::Acquire),
13372 1,
13373 "Should not restart LSP server on a related LSP settings change that is the same"
13374 );
13375
13376 update_test_project_settings(cx, |project_settings| {
13377 project_settings.lsp.insert(
13378 language_server_name.into(),
13379 LspSettings {
13380 binary: None,
13381 settings: None,
13382 initialization_options: None,
13383 enable_lsp_tasks: false,
13384 },
13385 );
13386 });
13387 cx.executor().run_until_parked();
13388 assert_eq!(
13389 server_restarts.load(atomic::Ordering::Acquire),
13390 2,
13391 "Should restart LSP server on another related LSP settings change"
13392 );
13393}
13394
13395#[gpui::test]
13396async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13397 init_test(cx, |_| {});
13398
13399 let mut cx = EditorLspTestContext::new_rust(
13400 lsp::ServerCapabilities {
13401 completion_provider: Some(lsp::CompletionOptions {
13402 trigger_characters: Some(vec![".".to_string()]),
13403 resolve_provider: Some(true),
13404 ..Default::default()
13405 }),
13406 ..Default::default()
13407 },
13408 cx,
13409 )
13410 .await;
13411
13412 cx.set_state("fn main() { let a = 2ˇ; }");
13413 cx.simulate_keystroke(".");
13414 let completion_item = lsp::CompletionItem {
13415 label: "some".into(),
13416 kind: Some(lsp::CompletionItemKind::SNIPPET),
13417 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13418 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13419 kind: lsp::MarkupKind::Markdown,
13420 value: "```rust\nSome(2)\n```".to_string(),
13421 })),
13422 deprecated: Some(false),
13423 sort_text: Some("fffffff2".to_string()),
13424 filter_text: Some("some".to_string()),
13425 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13426 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13427 range: lsp::Range {
13428 start: lsp::Position {
13429 line: 0,
13430 character: 22,
13431 },
13432 end: lsp::Position {
13433 line: 0,
13434 character: 22,
13435 },
13436 },
13437 new_text: "Some(2)".to_string(),
13438 })),
13439 additional_text_edits: Some(vec![lsp::TextEdit {
13440 range: lsp::Range {
13441 start: lsp::Position {
13442 line: 0,
13443 character: 20,
13444 },
13445 end: lsp::Position {
13446 line: 0,
13447 character: 22,
13448 },
13449 },
13450 new_text: "".to_string(),
13451 }]),
13452 ..Default::default()
13453 };
13454
13455 let closure_completion_item = completion_item.clone();
13456 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13457 let task_completion_item = closure_completion_item.clone();
13458 async move {
13459 Ok(Some(lsp::CompletionResponse::Array(vec![
13460 task_completion_item,
13461 ])))
13462 }
13463 });
13464
13465 request.next().await;
13466
13467 cx.condition(|editor, _| editor.context_menu_visible())
13468 .await;
13469 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13470 editor
13471 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13472 .unwrap()
13473 });
13474 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13475
13476 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13477 let task_completion_item = completion_item.clone();
13478 async move { Ok(task_completion_item) }
13479 })
13480 .next()
13481 .await
13482 .unwrap();
13483 apply_additional_edits.await.unwrap();
13484 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13485}
13486
13487#[gpui::test]
13488async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13489 init_test(cx, |_| {});
13490
13491 let mut cx = EditorLspTestContext::new_rust(
13492 lsp::ServerCapabilities {
13493 completion_provider: Some(lsp::CompletionOptions {
13494 trigger_characters: Some(vec![".".to_string()]),
13495 resolve_provider: Some(true),
13496 ..Default::default()
13497 }),
13498 ..Default::default()
13499 },
13500 cx,
13501 )
13502 .await;
13503
13504 cx.set_state("fn main() { let a = 2ˇ; }");
13505 cx.simulate_keystroke(".");
13506
13507 let item1 = lsp::CompletionItem {
13508 label: "method id()".to_string(),
13509 filter_text: Some("id".to_string()),
13510 detail: None,
13511 documentation: None,
13512 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13513 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13514 new_text: ".id".to_string(),
13515 })),
13516 ..lsp::CompletionItem::default()
13517 };
13518
13519 let item2 = lsp::CompletionItem {
13520 label: "other".to_string(),
13521 filter_text: Some("other".to_string()),
13522 detail: None,
13523 documentation: None,
13524 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13525 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13526 new_text: ".other".to_string(),
13527 })),
13528 ..lsp::CompletionItem::default()
13529 };
13530
13531 let item1 = item1.clone();
13532 cx.set_request_handler::<lsp::request::Completion, _, _>({
13533 let item1 = item1.clone();
13534 move |_, _, _| {
13535 let item1 = item1.clone();
13536 let item2 = item2.clone();
13537 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13538 }
13539 })
13540 .next()
13541 .await;
13542
13543 cx.condition(|editor, _| editor.context_menu_visible())
13544 .await;
13545 cx.update_editor(|editor, _, _| {
13546 let context_menu = editor.context_menu.borrow_mut();
13547 let context_menu = context_menu
13548 .as_ref()
13549 .expect("Should have the context menu deployed");
13550 match context_menu {
13551 CodeContextMenu::Completions(completions_menu) => {
13552 let completions = completions_menu.completions.borrow_mut();
13553 assert_eq!(
13554 completions
13555 .iter()
13556 .map(|completion| &completion.label.text)
13557 .collect::<Vec<_>>(),
13558 vec!["method id()", "other"]
13559 )
13560 }
13561 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13562 }
13563 });
13564
13565 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13566 let item1 = item1.clone();
13567 move |_, item_to_resolve, _| {
13568 let item1 = item1.clone();
13569 async move {
13570 if item1 == item_to_resolve {
13571 Ok(lsp::CompletionItem {
13572 label: "method id()".to_string(),
13573 filter_text: Some("id".to_string()),
13574 detail: Some("Now resolved!".to_string()),
13575 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13576 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13577 range: lsp::Range::new(
13578 lsp::Position::new(0, 22),
13579 lsp::Position::new(0, 22),
13580 ),
13581 new_text: ".id".to_string(),
13582 })),
13583 ..lsp::CompletionItem::default()
13584 })
13585 } else {
13586 Ok(item_to_resolve)
13587 }
13588 }
13589 }
13590 })
13591 .next()
13592 .await
13593 .unwrap();
13594 cx.run_until_parked();
13595
13596 cx.update_editor(|editor, window, cx| {
13597 editor.context_menu_next(&Default::default(), window, cx);
13598 });
13599
13600 cx.update_editor(|editor, _, _| {
13601 let context_menu = editor.context_menu.borrow_mut();
13602 let context_menu = context_menu
13603 .as_ref()
13604 .expect("Should have the context menu deployed");
13605 match context_menu {
13606 CodeContextMenu::Completions(completions_menu) => {
13607 let completions = completions_menu.completions.borrow_mut();
13608 assert_eq!(
13609 completions
13610 .iter()
13611 .map(|completion| &completion.label.text)
13612 .collect::<Vec<_>>(),
13613 vec!["method id() Now resolved!", "other"],
13614 "Should update first completion label, but not second as the filter text did not match."
13615 );
13616 }
13617 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13618 }
13619 });
13620}
13621
13622#[gpui::test]
13623async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13624 init_test(cx, |_| {});
13625
13626 let mut cx = EditorLspTestContext::new_rust(
13627 lsp::ServerCapabilities {
13628 completion_provider: Some(lsp::CompletionOptions {
13629 trigger_characters: Some(vec![".".to_string()]),
13630 resolve_provider: Some(true),
13631 ..Default::default()
13632 }),
13633 ..Default::default()
13634 },
13635 cx,
13636 )
13637 .await;
13638
13639 cx.set_state("fn main() { let a = 2ˇ; }");
13640 cx.simulate_keystroke(".");
13641
13642 let unresolved_item_1 = lsp::CompletionItem {
13643 label: "id".to_string(),
13644 filter_text: Some("id".to_string()),
13645 detail: None,
13646 documentation: None,
13647 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13648 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13649 new_text: ".id".to_string(),
13650 })),
13651 ..lsp::CompletionItem::default()
13652 };
13653 let resolved_item_1 = lsp::CompletionItem {
13654 additional_text_edits: Some(vec![lsp::TextEdit {
13655 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13656 new_text: "!!".to_string(),
13657 }]),
13658 ..unresolved_item_1.clone()
13659 };
13660 let unresolved_item_2 = lsp::CompletionItem {
13661 label: "other".to_string(),
13662 filter_text: Some("other".to_string()),
13663 detail: None,
13664 documentation: None,
13665 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13666 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13667 new_text: ".other".to_string(),
13668 })),
13669 ..lsp::CompletionItem::default()
13670 };
13671 let resolved_item_2 = lsp::CompletionItem {
13672 additional_text_edits: Some(vec![lsp::TextEdit {
13673 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13674 new_text: "??".to_string(),
13675 }]),
13676 ..unresolved_item_2.clone()
13677 };
13678
13679 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13680 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13681 cx.lsp
13682 .server
13683 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13684 let unresolved_item_1 = unresolved_item_1.clone();
13685 let resolved_item_1 = resolved_item_1.clone();
13686 let unresolved_item_2 = unresolved_item_2.clone();
13687 let resolved_item_2 = resolved_item_2.clone();
13688 let resolve_requests_1 = resolve_requests_1.clone();
13689 let resolve_requests_2 = resolve_requests_2.clone();
13690 move |unresolved_request, _| {
13691 let unresolved_item_1 = unresolved_item_1.clone();
13692 let resolved_item_1 = resolved_item_1.clone();
13693 let unresolved_item_2 = unresolved_item_2.clone();
13694 let resolved_item_2 = resolved_item_2.clone();
13695 let resolve_requests_1 = resolve_requests_1.clone();
13696 let resolve_requests_2 = resolve_requests_2.clone();
13697 async move {
13698 if unresolved_request == unresolved_item_1 {
13699 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13700 Ok(resolved_item_1.clone())
13701 } else if unresolved_request == unresolved_item_2 {
13702 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13703 Ok(resolved_item_2.clone())
13704 } else {
13705 panic!("Unexpected completion item {unresolved_request:?}")
13706 }
13707 }
13708 }
13709 })
13710 .detach();
13711
13712 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13713 let unresolved_item_1 = unresolved_item_1.clone();
13714 let unresolved_item_2 = unresolved_item_2.clone();
13715 async move {
13716 Ok(Some(lsp::CompletionResponse::Array(vec![
13717 unresolved_item_1,
13718 unresolved_item_2,
13719 ])))
13720 }
13721 })
13722 .next()
13723 .await;
13724
13725 cx.condition(|editor, _| editor.context_menu_visible())
13726 .await;
13727 cx.update_editor(|editor, _, _| {
13728 let context_menu = editor.context_menu.borrow_mut();
13729 let context_menu = context_menu
13730 .as_ref()
13731 .expect("Should have the context menu deployed");
13732 match context_menu {
13733 CodeContextMenu::Completions(completions_menu) => {
13734 let completions = completions_menu.completions.borrow_mut();
13735 assert_eq!(
13736 completions
13737 .iter()
13738 .map(|completion| &completion.label.text)
13739 .collect::<Vec<_>>(),
13740 vec!["id", "other"]
13741 )
13742 }
13743 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13744 }
13745 });
13746 cx.run_until_parked();
13747
13748 cx.update_editor(|editor, window, cx| {
13749 editor.context_menu_next(&ContextMenuNext, window, cx);
13750 });
13751 cx.run_until_parked();
13752 cx.update_editor(|editor, window, cx| {
13753 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13754 });
13755 cx.run_until_parked();
13756 cx.update_editor(|editor, window, cx| {
13757 editor.context_menu_next(&ContextMenuNext, window, cx);
13758 });
13759 cx.run_until_parked();
13760 cx.update_editor(|editor, window, cx| {
13761 editor
13762 .compose_completion(&ComposeCompletion::default(), window, cx)
13763 .expect("No task returned")
13764 })
13765 .await
13766 .expect("Completion failed");
13767 cx.run_until_parked();
13768
13769 cx.update_editor(|editor, _, cx| {
13770 assert_eq!(
13771 resolve_requests_1.load(atomic::Ordering::Acquire),
13772 1,
13773 "Should always resolve once despite multiple selections"
13774 );
13775 assert_eq!(
13776 resolve_requests_2.load(atomic::Ordering::Acquire),
13777 1,
13778 "Should always resolve once after multiple selections and applying the completion"
13779 );
13780 assert_eq!(
13781 editor.text(cx),
13782 "fn main() { let a = ??.other; }",
13783 "Should use resolved data when applying the completion"
13784 );
13785 });
13786}
13787
13788#[gpui::test]
13789async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13790 init_test(cx, |_| {});
13791
13792 let item_0 = lsp::CompletionItem {
13793 label: "abs".into(),
13794 insert_text: Some("abs".into()),
13795 data: Some(json!({ "very": "special"})),
13796 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13797 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13798 lsp::InsertReplaceEdit {
13799 new_text: "abs".to_string(),
13800 insert: lsp::Range::default(),
13801 replace: lsp::Range::default(),
13802 },
13803 )),
13804 ..lsp::CompletionItem::default()
13805 };
13806 let items = iter::once(item_0.clone())
13807 .chain((11..51).map(|i| lsp::CompletionItem {
13808 label: format!("item_{}", i),
13809 insert_text: Some(format!("item_{}", i)),
13810 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13811 ..lsp::CompletionItem::default()
13812 }))
13813 .collect::<Vec<_>>();
13814
13815 let default_commit_characters = vec!["?".to_string()];
13816 let default_data = json!({ "default": "data"});
13817 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13818 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13819 let default_edit_range = lsp::Range {
13820 start: lsp::Position {
13821 line: 0,
13822 character: 5,
13823 },
13824 end: lsp::Position {
13825 line: 0,
13826 character: 5,
13827 },
13828 };
13829
13830 let mut cx = EditorLspTestContext::new_rust(
13831 lsp::ServerCapabilities {
13832 completion_provider: Some(lsp::CompletionOptions {
13833 trigger_characters: Some(vec![".".to_string()]),
13834 resolve_provider: Some(true),
13835 ..Default::default()
13836 }),
13837 ..Default::default()
13838 },
13839 cx,
13840 )
13841 .await;
13842
13843 cx.set_state("fn main() { let a = 2ˇ; }");
13844 cx.simulate_keystroke(".");
13845
13846 let completion_data = default_data.clone();
13847 let completion_characters = default_commit_characters.clone();
13848 let completion_items = items.clone();
13849 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13850 let default_data = completion_data.clone();
13851 let default_commit_characters = completion_characters.clone();
13852 let items = completion_items.clone();
13853 async move {
13854 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13855 items,
13856 item_defaults: Some(lsp::CompletionListItemDefaults {
13857 data: Some(default_data.clone()),
13858 commit_characters: Some(default_commit_characters.clone()),
13859 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13860 default_edit_range,
13861 )),
13862 insert_text_format: Some(default_insert_text_format),
13863 insert_text_mode: Some(default_insert_text_mode),
13864 }),
13865 ..lsp::CompletionList::default()
13866 })))
13867 }
13868 })
13869 .next()
13870 .await;
13871
13872 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13873 cx.lsp
13874 .server
13875 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13876 let closure_resolved_items = resolved_items.clone();
13877 move |item_to_resolve, _| {
13878 let closure_resolved_items = closure_resolved_items.clone();
13879 async move {
13880 closure_resolved_items.lock().push(item_to_resolve.clone());
13881 Ok(item_to_resolve)
13882 }
13883 }
13884 })
13885 .detach();
13886
13887 cx.condition(|editor, _| editor.context_menu_visible())
13888 .await;
13889 cx.run_until_parked();
13890 cx.update_editor(|editor, _, _| {
13891 let menu = editor.context_menu.borrow_mut();
13892 match menu.as_ref().expect("should have the completions menu") {
13893 CodeContextMenu::Completions(completions_menu) => {
13894 assert_eq!(
13895 completions_menu
13896 .entries
13897 .borrow()
13898 .iter()
13899 .map(|mat| mat.string.clone())
13900 .collect::<Vec<String>>(),
13901 items
13902 .iter()
13903 .map(|completion| completion.label.clone())
13904 .collect::<Vec<String>>()
13905 );
13906 }
13907 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13908 }
13909 });
13910 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13911 // with 4 from the end.
13912 assert_eq!(
13913 *resolved_items.lock(),
13914 [&items[0..16], &items[items.len() - 4..items.len()]]
13915 .concat()
13916 .iter()
13917 .cloned()
13918 .map(|mut item| {
13919 if item.data.is_none() {
13920 item.data = Some(default_data.clone());
13921 }
13922 item
13923 })
13924 .collect::<Vec<lsp::CompletionItem>>(),
13925 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13926 );
13927 resolved_items.lock().clear();
13928
13929 cx.update_editor(|editor, window, cx| {
13930 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13931 });
13932 cx.run_until_parked();
13933 // Completions that have already been resolved are skipped.
13934 assert_eq!(
13935 *resolved_items.lock(),
13936 items[items.len() - 16..items.len() - 4]
13937 .iter()
13938 .cloned()
13939 .map(|mut item| {
13940 if item.data.is_none() {
13941 item.data = Some(default_data.clone());
13942 }
13943 item
13944 })
13945 .collect::<Vec<lsp::CompletionItem>>()
13946 );
13947 resolved_items.lock().clear();
13948}
13949
13950#[gpui::test]
13951async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13952 init_test(cx, |_| {});
13953
13954 let mut cx = EditorLspTestContext::new(
13955 Language::new(
13956 LanguageConfig {
13957 matcher: LanguageMatcher {
13958 path_suffixes: vec!["jsx".into()],
13959 ..Default::default()
13960 },
13961 overrides: [(
13962 "element".into(),
13963 LanguageConfigOverride {
13964 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13965 ..Default::default()
13966 },
13967 )]
13968 .into_iter()
13969 .collect(),
13970 ..Default::default()
13971 },
13972 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13973 )
13974 .with_override_query("(jsx_self_closing_element) @element")
13975 .unwrap(),
13976 lsp::ServerCapabilities {
13977 completion_provider: Some(lsp::CompletionOptions {
13978 trigger_characters: Some(vec![":".to_string()]),
13979 ..Default::default()
13980 }),
13981 ..Default::default()
13982 },
13983 cx,
13984 )
13985 .await;
13986
13987 cx.lsp
13988 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13989 Ok(Some(lsp::CompletionResponse::Array(vec![
13990 lsp::CompletionItem {
13991 label: "bg-blue".into(),
13992 ..Default::default()
13993 },
13994 lsp::CompletionItem {
13995 label: "bg-red".into(),
13996 ..Default::default()
13997 },
13998 lsp::CompletionItem {
13999 label: "bg-yellow".into(),
14000 ..Default::default()
14001 },
14002 ])))
14003 });
14004
14005 cx.set_state(r#"<p class="bgˇ" />"#);
14006
14007 // Trigger completion when typing a dash, because the dash is an extra
14008 // word character in the 'element' scope, which contains the cursor.
14009 cx.simulate_keystroke("-");
14010 cx.executor().run_until_parked();
14011 cx.update_editor(|editor, _, _| {
14012 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14013 {
14014 assert_eq!(
14015 completion_menu_entries(&menu),
14016 &["bg-red", "bg-blue", "bg-yellow"]
14017 );
14018 } else {
14019 panic!("expected completion menu to be open");
14020 }
14021 });
14022
14023 cx.simulate_keystroke("l");
14024 cx.executor().run_until_parked();
14025 cx.update_editor(|editor, _, _| {
14026 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14027 {
14028 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14029 } else {
14030 panic!("expected completion menu to be open");
14031 }
14032 });
14033
14034 // When filtering completions, consider the character after the '-' to
14035 // be the start of a subword.
14036 cx.set_state(r#"<p class="yelˇ" />"#);
14037 cx.simulate_keystroke("l");
14038 cx.executor().run_until_parked();
14039 cx.update_editor(|editor, _, _| {
14040 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14041 {
14042 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14043 } else {
14044 panic!("expected completion menu to be open");
14045 }
14046 });
14047}
14048
14049fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14050 let entries = menu.entries.borrow();
14051 entries.iter().map(|mat| mat.string.clone()).collect()
14052}
14053
14054#[gpui::test]
14055async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14056 init_test(cx, |settings| {
14057 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14058 FormatterList(vec![Formatter::Prettier].into()),
14059 ))
14060 });
14061
14062 let fs = FakeFs::new(cx.executor());
14063 fs.insert_file(path!("/file.ts"), Default::default()).await;
14064
14065 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14066 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14067
14068 language_registry.add(Arc::new(Language::new(
14069 LanguageConfig {
14070 name: "TypeScript".into(),
14071 matcher: LanguageMatcher {
14072 path_suffixes: vec!["ts".to_string()],
14073 ..Default::default()
14074 },
14075 ..Default::default()
14076 },
14077 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14078 )));
14079 update_test_language_settings(cx, |settings| {
14080 settings.defaults.prettier = Some(PrettierSettings {
14081 allowed: true,
14082 ..PrettierSettings::default()
14083 });
14084 });
14085
14086 let test_plugin = "test_plugin";
14087 let _ = language_registry.register_fake_lsp(
14088 "TypeScript",
14089 FakeLspAdapter {
14090 prettier_plugins: vec![test_plugin],
14091 ..Default::default()
14092 },
14093 );
14094
14095 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14096 let buffer = project
14097 .update(cx, |project, cx| {
14098 project.open_local_buffer(path!("/file.ts"), cx)
14099 })
14100 .await
14101 .unwrap();
14102
14103 let buffer_text = "one\ntwo\nthree\n";
14104 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14105 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14106 editor.update_in(cx, |editor, window, cx| {
14107 editor.set_text(buffer_text, window, cx)
14108 });
14109
14110 editor
14111 .update_in(cx, |editor, window, cx| {
14112 editor.perform_format(
14113 project.clone(),
14114 FormatTrigger::Manual,
14115 FormatTarget::Buffers,
14116 window,
14117 cx,
14118 )
14119 })
14120 .unwrap()
14121 .await;
14122 assert_eq!(
14123 editor.update(cx, |editor, cx| editor.text(cx)),
14124 buffer_text.to_string() + prettier_format_suffix,
14125 "Test prettier formatting was not applied to the original buffer text",
14126 );
14127
14128 update_test_language_settings(cx, |settings| {
14129 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14130 });
14131 let format = editor.update_in(cx, |editor, window, cx| {
14132 editor.perform_format(
14133 project.clone(),
14134 FormatTrigger::Manual,
14135 FormatTarget::Buffers,
14136 window,
14137 cx,
14138 )
14139 });
14140 format.await.unwrap();
14141 assert_eq!(
14142 editor.update(cx, |editor, cx| editor.text(cx)),
14143 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14144 "Autoformatting (via test prettier) was not applied to the original buffer text",
14145 );
14146}
14147
14148#[gpui::test]
14149async fn test_addition_reverts(cx: &mut TestAppContext) {
14150 init_test(cx, |_| {});
14151 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14152 let base_text = indoc! {r#"
14153 struct Row;
14154 struct Row1;
14155 struct Row2;
14156
14157 struct Row4;
14158 struct Row5;
14159 struct Row6;
14160
14161 struct Row8;
14162 struct Row9;
14163 struct Row10;"#};
14164
14165 // When addition hunks are not adjacent to carets, no hunk revert is performed
14166 assert_hunk_revert(
14167 indoc! {r#"struct Row;
14168 struct Row1;
14169 struct Row1.1;
14170 struct Row1.2;
14171 struct Row2;ˇ
14172
14173 struct Row4;
14174 struct Row5;
14175 struct Row6;
14176
14177 struct Row8;
14178 ˇstruct Row9;
14179 struct Row9.1;
14180 struct Row9.2;
14181 struct Row9.3;
14182 struct Row10;"#},
14183 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14184 indoc! {r#"struct Row;
14185 struct Row1;
14186 struct Row1.1;
14187 struct Row1.2;
14188 struct Row2;ˇ
14189
14190 struct Row4;
14191 struct Row5;
14192 struct Row6;
14193
14194 struct Row8;
14195 ˇstruct Row9;
14196 struct Row9.1;
14197 struct Row9.2;
14198 struct Row9.3;
14199 struct Row10;"#},
14200 base_text,
14201 &mut cx,
14202 );
14203 // Same for selections
14204 assert_hunk_revert(
14205 indoc! {r#"struct Row;
14206 struct Row1;
14207 struct Row2;
14208 struct Row2.1;
14209 struct Row2.2;
14210 «ˇ
14211 struct Row4;
14212 struct» Row5;
14213 «struct Row6;
14214 ˇ»
14215 struct Row9.1;
14216 struct Row9.2;
14217 struct Row9.3;
14218 struct Row8;
14219 struct Row9;
14220 struct Row10;"#},
14221 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14222 indoc! {r#"struct Row;
14223 struct Row1;
14224 struct Row2;
14225 struct Row2.1;
14226 struct Row2.2;
14227 «ˇ
14228 struct Row4;
14229 struct» Row5;
14230 «struct Row6;
14231 ˇ»
14232 struct Row9.1;
14233 struct Row9.2;
14234 struct Row9.3;
14235 struct Row8;
14236 struct Row9;
14237 struct Row10;"#},
14238 base_text,
14239 &mut cx,
14240 );
14241
14242 // When carets and selections intersect the addition hunks, those are reverted.
14243 // Adjacent carets got merged.
14244 assert_hunk_revert(
14245 indoc! {r#"struct Row;
14246 ˇ// something on the top
14247 struct Row1;
14248 struct Row2;
14249 struct Roˇw3.1;
14250 struct Row2.2;
14251 struct Row2.3;ˇ
14252
14253 struct Row4;
14254 struct ˇRow5.1;
14255 struct Row5.2;
14256 struct «Rowˇ»5.3;
14257 struct Row5;
14258 struct Row6;
14259 ˇ
14260 struct Row9.1;
14261 struct «Rowˇ»9.2;
14262 struct «ˇRow»9.3;
14263 struct Row8;
14264 struct Row9;
14265 «ˇ// something on bottom»
14266 struct Row10;"#},
14267 vec![
14268 DiffHunkStatusKind::Added,
14269 DiffHunkStatusKind::Added,
14270 DiffHunkStatusKind::Added,
14271 DiffHunkStatusKind::Added,
14272 DiffHunkStatusKind::Added,
14273 ],
14274 indoc! {r#"struct Row;
14275 ˇstruct Row1;
14276 struct Row2;
14277 ˇ
14278 struct Row4;
14279 ˇstruct Row5;
14280 struct Row6;
14281 ˇ
14282 ˇstruct Row8;
14283 struct Row9;
14284 ˇstruct Row10;"#},
14285 base_text,
14286 &mut cx,
14287 );
14288}
14289
14290#[gpui::test]
14291async fn test_modification_reverts(cx: &mut TestAppContext) {
14292 init_test(cx, |_| {});
14293 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14294 let base_text = indoc! {r#"
14295 struct Row;
14296 struct Row1;
14297 struct Row2;
14298
14299 struct Row4;
14300 struct Row5;
14301 struct Row6;
14302
14303 struct Row8;
14304 struct Row9;
14305 struct Row10;"#};
14306
14307 // Modification hunks behave the same as the addition ones.
14308 assert_hunk_revert(
14309 indoc! {r#"struct Row;
14310 struct Row1;
14311 struct Row33;
14312 ˇ
14313 struct Row4;
14314 struct Row5;
14315 struct Row6;
14316 ˇ
14317 struct Row99;
14318 struct Row9;
14319 struct Row10;"#},
14320 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14321 indoc! {r#"struct Row;
14322 struct Row1;
14323 struct Row33;
14324 ˇ
14325 struct Row4;
14326 struct Row5;
14327 struct Row6;
14328 ˇ
14329 struct Row99;
14330 struct Row9;
14331 struct Row10;"#},
14332 base_text,
14333 &mut cx,
14334 );
14335 assert_hunk_revert(
14336 indoc! {r#"struct Row;
14337 struct Row1;
14338 struct Row33;
14339 «ˇ
14340 struct Row4;
14341 struct» Row5;
14342 «struct Row6;
14343 ˇ»
14344 struct Row99;
14345 struct Row9;
14346 struct Row10;"#},
14347 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14348 indoc! {r#"struct Row;
14349 struct Row1;
14350 struct Row33;
14351 «ˇ
14352 struct Row4;
14353 struct» Row5;
14354 «struct Row6;
14355 ˇ»
14356 struct Row99;
14357 struct Row9;
14358 struct Row10;"#},
14359 base_text,
14360 &mut cx,
14361 );
14362
14363 assert_hunk_revert(
14364 indoc! {r#"ˇstruct Row1.1;
14365 struct Row1;
14366 «ˇstr»uct Row22;
14367
14368 struct ˇRow44;
14369 struct Row5;
14370 struct «Rˇ»ow66;ˇ
14371
14372 «struˇ»ct Row88;
14373 struct Row9;
14374 struct Row1011;ˇ"#},
14375 vec![
14376 DiffHunkStatusKind::Modified,
14377 DiffHunkStatusKind::Modified,
14378 DiffHunkStatusKind::Modified,
14379 DiffHunkStatusKind::Modified,
14380 DiffHunkStatusKind::Modified,
14381 DiffHunkStatusKind::Modified,
14382 ],
14383 indoc! {r#"struct Row;
14384 ˇstruct Row1;
14385 struct Row2;
14386 ˇ
14387 struct Row4;
14388 ˇstruct Row5;
14389 struct Row6;
14390 ˇ
14391 struct Row8;
14392 ˇstruct Row9;
14393 struct Row10;ˇ"#},
14394 base_text,
14395 &mut cx,
14396 );
14397}
14398
14399#[gpui::test]
14400async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14401 init_test(cx, |_| {});
14402 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14403 let base_text = indoc! {r#"
14404 one
14405
14406 two
14407 three
14408 "#};
14409
14410 cx.set_head_text(base_text);
14411 cx.set_state("\nˇ\n");
14412 cx.executor().run_until_parked();
14413 cx.update_editor(|editor, _window, cx| {
14414 editor.expand_selected_diff_hunks(cx);
14415 });
14416 cx.executor().run_until_parked();
14417 cx.update_editor(|editor, window, cx| {
14418 editor.backspace(&Default::default(), window, cx);
14419 });
14420 cx.run_until_parked();
14421 cx.assert_state_with_diff(
14422 indoc! {r#"
14423
14424 - two
14425 - threeˇ
14426 +
14427 "#}
14428 .to_string(),
14429 );
14430}
14431
14432#[gpui::test]
14433async fn test_deletion_reverts(cx: &mut TestAppContext) {
14434 init_test(cx, |_| {});
14435 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14436 let base_text = indoc! {r#"struct Row;
14437struct Row1;
14438struct Row2;
14439
14440struct Row4;
14441struct Row5;
14442struct Row6;
14443
14444struct Row8;
14445struct Row9;
14446struct Row10;"#};
14447
14448 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14449 assert_hunk_revert(
14450 indoc! {r#"struct Row;
14451 struct Row2;
14452
14453 ˇstruct Row4;
14454 struct Row5;
14455 struct Row6;
14456 ˇ
14457 struct Row8;
14458 struct Row10;"#},
14459 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14460 indoc! {r#"struct Row;
14461 struct Row2;
14462
14463 ˇstruct Row4;
14464 struct Row5;
14465 struct Row6;
14466 ˇ
14467 struct Row8;
14468 struct Row10;"#},
14469 base_text,
14470 &mut cx,
14471 );
14472 assert_hunk_revert(
14473 indoc! {r#"struct Row;
14474 struct Row2;
14475
14476 «ˇstruct Row4;
14477 struct» Row5;
14478 «struct Row6;
14479 ˇ»
14480 struct Row8;
14481 struct Row10;"#},
14482 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14483 indoc! {r#"struct Row;
14484 struct Row2;
14485
14486 «ˇstruct Row4;
14487 struct» Row5;
14488 «struct Row6;
14489 ˇ»
14490 struct Row8;
14491 struct Row10;"#},
14492 base_text,
14493 &mut cx,
14494 );
14495
14496 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14497 assert_hunk_revert(
14498 indoc! {r#"struct Row;
14499 ˇstruct Row2;
14500
14501 struct Row4;
14502 struct Row5;
14503 struct Row6;
14504
14505 struct Row8;ˇ
14506 struct Row10;"#},
14507 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14508 indoc! {r#"struct Row;
14509 struct Row1;
14510 ˇstruct Row2;
14511
14512 struct Row4;
14513 struct Row5;
14514 struct Row6;
14515
14516 struct Row8;ˇ
14517 struct Row9;
14518 struct Row10;"#},
14519 base_text,
14520 &mut cx,
14521 );
14522 assert_hunk_revert(
14523 indoc! {r#"struct Row;
14524 struct Row2«ˇ;
14525 struct Row4;
14526 struct» Row5;
14527 «struct Row6;
14528
14529 struct Row8;ˇ»
14530 struct Row10;"#},
14531 vec![
14532 DiffHunkStatusKind::Deleted,
14533 DiffHunkStatusKind::Deleted,
14534 DiffHunkStatusKind::Deleted,
14535 ],
14536 indoc! {r#"struct Row;
14537 struct Row1;
14538 struct Row2«ˇ;
14539
14540 struct Row4;
14541 struct» Row5;
14542 «struct Row6;
14543
14544 struct Row8;ˇ»
14545 struct Row9;
14546 struct Row10;"#},
14547 base_text,
14548 &mut cx,
14549 );
14550}
14551
14552#[gpui::test]
14553async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14554 init_test(cx, |_| {});
14555
14556 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14557 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14558 let base_text_3 =
14559 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14560
14561 let text_1 = edit_first_char_of_every_line(base_text_1);
14562 let text_2 = edit_first_char_of_every_line(base_text_2);
14563 let text_3 = edit_first_char_of_every_line(base_text_3);
14564
14565 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14566 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14567 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14568
14569 let multibuffer = cx.new(|cx| {
14570 let mut multibuffer = MultiBuffer::new(ReadWrite);
14571 multibuffer.push_excerpts(
14572 buffer_1.clone(),
14573 [
14574 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14575 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14576 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14577 ],
14578 cx,
14579 );
14580 multibuffer.push_excerpts(
14581 buffer_2.clone(),
14582 [
14583 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14584 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14585 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14586 ],
14587 cx,
14588 );
14589 multibuffer.push_excerpts(
14590 buffer_3.clone(),
14591 [
14592 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14593 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14594 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14595 ],
14596 cx,
14597 );
14598 multibuffer
14599 });
14600
14601 let fs = FakeFs::new(cx.executor());
14602 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14603 let (editor, cx) = cx
14604 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14605 editor.update_in(cx, |editor, _window, cx| {
14606 for (buffer, diff_base) in [
14607 (buffer_1.clone(), base_text_1),
14608 (buffer_2.clone(), base_text_2),
14609 (buffer_3.clone(), base_text_3),
14610 ] {
14611 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14612 editor
14613 .buffer
14614 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14615 }
14616 });
14617 cx.executor().run_until_parked();
14618
14619 editor.update_in(cx, |editor, window, cx| {
14620 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}");
14621 editor.select_all(&SelectAll, window, cx);
14622 editor.git_restore(&Default::default(), window, cx);
14623 });
14624 cx.executor().run_until_parked();
14625
14626 // When all ranges are selected, all buffer hunks are reverted.
14627 editor.update(cx, |editor, cx| {
14628 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");
14629 });
14630 buffer_1.update(cx, |buffer, _| {
14631 assert_eq!(buffer.text(), base_text_1);
14632 });
14633 buffer_2.update(cx, |buffer, _| {
14634 assert_eq!(buffer.text(), base_text_2);
14635 });
14636 buffer_3.update(cx, |buffer, _| {
14637 assert_eq!(buffer.text(), base_text_3);
14638 });
14639
14640 editor.update_in(cx, |editor, window, cx| {
14641 editor.undo(&Default::default(), window, cx);
14642 });
14643
14644 editor.update_in(cx, |editor, window, cx| {
14645 editor.change_selections(None, window, cx, |s| {
14646 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14647 });
14648 editor.git_restore(&Default::default(), window, cx);
14649 });
14650
14651 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14652 // but not affect buffer_2 and its related excerpts.
14653 editor.update(cx, |editor, cx| {
14654 assert_eq!(
14655 editor.text(cx),
14656 "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}"
14657 );
14658 });
14659 buffer_1.update(cx, |buffer, _| {
14660 assert_eq!(buffer.text(), base_text_1);
14661 });
14662 buffer_2.update(cx, |buffer, _| {
14663 assert_eq!(
14664 buffer.text(),
14665 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14666 );
14667 });
14668 buffer_3.update(cx, |buffer, _| {
14669 assert_eq!(
14670 buffer.text(),
14671 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14672 );
14673 });
14674
14675 fn edit_first_char_of_every_line(text: &str) -> String {
14676 text.split('\n')
14677 .map(|line| format!("X{}", &line[1..]))
14678 .collect::<Vec<_>>()
14679 .join("\n")
14680 }
14681}
14682
14683#[gpui::test]
14684async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14685 init_test(cx, |_| {});
14686
14687 let cols = 4;
14688 let rows = 10;
14689 let sample_text_1 = sample_text(rows, cols, 'a');
14690 assert_eq!(
14691 sample_text_1,
14692 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14693 );
14694 let sample_text_2 = sample_text(rows, cols, 'l');
14695 assert_eq!(
14696 sample_text_2,
14697 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14698 );
14699 let sample_text_3 = sample_text(rows, cols, 'v');
14700 assert_eq!(
14701 sample_text_3,
14702 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14703 );
14704
14705 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14706 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14707 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14708
14709 let multi_buffer = cx.new(|cx| {
14710 let mut multibuffer = MultiBuffer::new(ReadWrite);
14711 multibuffer.push_excerpts(
14712 buffer_1.clone(),
14713 [
14714 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14715 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14716 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14717 ],
14718 cx,
14719 );
14720 multibuffer.push_excerpts(
14721 buffer_2.clone(),
14722 [
14723 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14724 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14725 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14726 ],
14727 cx,
14728 );
14729 multibuffer.push_excerpts(
14730 buffer_3.clone(),
14731 [
14732 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14733 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14734 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14735 ],
14736 cx,
14737 );
14738 multibuffer
14739 });
14740
14741 let fs = FakeFs::new(cx.executor());
14742 fs.insert_tree(
14743 "/a",
14744 json!({
14745 "main.rs": sample_text_1,
14746 "other.rs": sample_text_2,
14747 "lib.rs": sample_text_3,
14748 }),
14749 )
14750 .await;
14751 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14752 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14753 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14754 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14755 Editor::new(
14756 EditorMode::full(),
14757 multi_buffer,
14758 Some(project.clone()),
14759 window,
14760 cx,
14761 )
14762 });
14763 let multibuffer_item_id = workspace
14764 .update(cx, |workspace, window, cx| {
14765 assert!(
14766 workspace.active_item(cx).is_none(),
14767 "active item should be None before the first item is added"
14768 );
14769 workspace.add_item_to_active_pane(
14770 Box::new(multi_buffer_editor.clone()),
14771 None,
14772 true,
14773 window,
14774 cx,
14775 );
14776 let active_item = workspace
14777 .active_item(cx)
14778 .expect("should have an active item after adding the multi buffer");
14779 assert!(
14780 !active_item.is_singleton(cx),
14781 "A multi buffer was expected to active after adding"
14782 );
14783 active_item.item_id()
14784 })
14785 .unwrap();
14786 cx.executor().run_until_parked();
14787
14788 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14789 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14790 s.select_ranges(Some(1..2))
14791 });
14792 editor.open_excerpts(&OpenExcerpts, window, cx);
14793 });
14794 cx.executor().run_until_parked();
14795 let first_item_id = workspace
14796 .update(cx, |workspace, window, cx| {
14797 let active_item = workspace
14798 .active_item(cx)
14799 .expect("should have an active item after navigating into the 1st buffer");
14800 let first_item_id = active_item.item_id();
14801 assert_ne!(
14802 first_item_id, multibuffer_item_id,
14803 "Should navigate into the 1st buffer and activate it"
14804 );
14805 assert!(
14806 active_item.is_singleton(cx),
14807 "New active item should be a singleton buffer"
14808 );
14809 assert_eq!(
14810 active_item
14811 .act_as::<Editor>(cx)
14812 .expect("should have navigated into an editor for the 1st buffer")
14813 .read(cx)
14814 .text(cx),
14815 sample_text_1
14816 );
14817
14818 workspace
14819 .go_back(workspace.active_pane().downgrade(), window, cx)
14820 .detach_and_log_err(cx);
14821
14822 first_item_id
14823 })
14824 .unwrap();
14825 cx.executor().run_until_parked();
14826 workspace
14827 .update(cx, |workspace, _, cx| {
14828 let active_item = workspace
14829 .active_item(cx)
14830 .expect("should have an active item after navigating back");
14831 assert_eq!(
14832 active_item.item_id(),
14833 multibuffer_item_id,
14834 "Should navigate back to the multi buffer"
14835 );
14836 assert!(!active_item.is_singleton(cx));
14837 })
14838 .unwrap();
14839
14840 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14841 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14842 s.select_ranges(Some(39..40))
14843 });
14844 editor.open_excerpts(&OpenExcerpts, window, cx);
14845 });
14846 cx.executor().run_until_parked();
14847 let second_item_id = workspace
14848 .update(cx, |workspace, window, cx| {
14849 let active_item = workspace
14850 .active_item(cx)
14851 .expect("should have an active item after navigating into the 2nd buffer");
14852 let second_item_id = active_item.item_id();
14853 assert_ne!(
14854 second_item_id, multibuffer_item_id,
14855 "Should navigate away from the multibuffer"
14856 );
14857 assert_ne!(
14858 second_item_id, first_item_id,
14859 "Should navigate into the 2nd buffer and activate it"
14860 );
14861 assert!(
14862 active_item.is_singleton(cx),
14863 "New active item should be a singleton buffer"
14864 );
14865 assert_eq!(
14866 active_item
14867 .act_as::<Editor>(cx)
14868 .expect("should have navigated into an editor")
14869 .read(cx)
14870 .text(cx),
14871 sample_text_2
14872 );
14873
14874 workspace
14875 .go_back(workspace.active_pane().downgrade(), window, cx)
14876 .detach_and_log_err(cx);
14877
14878 second_item_id
14879 })
14880 .unwrap();
14881 cx.executor().run_until_parked();
14882 workspace
14883 .update(cx, |workspace, _, cx| {
14884 let active_item = workspace
14885 .active_item(cx)
14886 .expect("should have an active item after navigating back from the 2nd buffer");
14887 assert_eq!(
14888 active_item.item_id(),
14889 multibuffer_item_id,
14890 "Should navigate back from the 2nd buffer to the multi buffer"
14891 );
14892 assert!(!active_item.is_singleton(cx));
14893 })
14894 .unwrap();
14895
14896 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14897 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14898 s.select_ranges(Some(70..70))
14899 });
14900 editor.open_excerpts(&OpenExcerpts, window, cx);
14901 });
14902 cx.executor().run_until_parked();
14903 workspace
14904 .update(cx, |workspace, window, cx| {
14905 let active_item = workspace
14906 .active_item(cx)
14907 .expect("should have an active item after navigating into the 3rd buffer");
14908 let third_item_id = active_item.item_id();
14909 assert_ne!(
14910 third_item_id, multibuffer_item_id,
14911 "Should navigate into the 3rd buffer and activate it"
14912 );
14913 assert_ne!(third_item_id, first_item_id);
14914 assert_ne!(third_item_id, second_item_id);
14915 assert!(
14916 active_item.is_singleton(cx),
14917 "New active item should be a singleton buffer"
14918 );
14919 assert_eq!(
14920 active_item
14921 .act_as::<Editor>(cx)
14922 .expect("should have navigated into an editor")
14923 .read(cx)
14924 .text(cx),
14925 sample_text_3
14926 );
14927
14928 workspace
14929 .go_back(workspace.active_pane().downgrade(), window, cx)
14930 .detach_and_log_err(cx);
14931 })
14932 .unwrap();
14933 cx.executor().run_until_parked();
14934 workspace
14935 .update(cx, |workspace, _, cx| {
14936 let active_item = workspace
14937 .active_item(cx)
14938 .expect("should have an active item after navigating back from the 3rd buffer");
14939 assert_eq!(
14940 active_item.item_id(),
14941 multibuffer_item_id,
14942 "Should navigate back from the 3rd buffer to the multi buffer"
14943 );
14944 assert!(!active_item.is_singleton(cx));
14945 })
14946 .unwrap();
14947}
14948
14949#[gpui::test]
14950async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14951 init_test(cx, |_| {});
14952
14953 let mut cx = EditorTestContext::new(cx).await;
14954
14955 let diff_base = r#"
14956 use some::mod;
14957
14958 const A: u32 = 42;
14959
14960 fn main() {
14961 println!("hello");
14962
14963 println!("world");
14964 }
14965 "#
14966 .unindent();
14967
14968 cx.set_state(
14969 &r#"
14970 use some::modified;
14971
14972 ˇ
14973 fn main() {
14974 println!("hello there");
14975
14976 println!("around the");
14977 println!("world");
14978 }
14979 "#
14980 .unindent(),
14981 );
14982
14983 cx.set_head_text(&diff_base);
14984 executor.run_until_parked();
14985
14986 cx.update_editor(|editor, window, cx| {
14987 editor.go_to_next_hunk(&GoToHunk, window, cx);
14988 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14989 });
14990 executor.run_until_parked();
14991 cx.assert_state_with_diff(
14992 r#"
14993 use some::modified;
14994
14995
14996 fn main() {
14997 - println!("hello");
14998 + ˇ println!("hello there");
14999
15000 println!("around the");
15001 println!("world");
15002 }
15003 "#
15004 .unindent(),
15005 );
15006
15007 cx.update_editor(|editor, window, cx| {
15008 for _ in 0..2 {
15009 editor.go_to_next_hunk(&GoToHunk, window, cx);
15010 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15011 }
15012 });
15013 executor.run_until_parked();
15014 cx.assert_state_with_diff(
15015 r#"
15016 - use some::mod;
15017 + ˇuse some::modified;
15018
15019
15020 fn main() {
15021 - println!("hello");
15022 + println!("hello there");
15023
15024 + println!("around the");
15025 println!("world");
15026 }
15027 "#
15028 .unindent(),
15029 );
15030
15031 cx.update_editor(|editor, window, cx| {
15032 editor.go_to_next_hunk(&GoToHunk, window, cx);
15033 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15034 });
15035 executor.run_until_parked();
15036 cx.assert_state_with_diff(
15037 r#"
15038 - use some::mod;
15039 + use some::modified;
15040
15041 - const A: u32 = 42;
15042 ˇ
15043 fn main() {
15044 - println!("hello");
15045 + println!("hello there");
15046
15047 + println!("around the");
15048 println!("world");
15049 }
15050 "#
15051 .unindent(),
15052 );
15053
15054 cx.update_editor(|editor, window, cx| {
15055 editor.cancel(&Cancel, window, cx);
15056 });
15057
15058 cx.assert_state_with_diff(
15059 r#"
15060 use some::modified;
15061
15062 ˇ
15063 fn main() {
15064 println!("hello there");
15065
15066 println!("around the");
15067 println!("world");
15068 }
15069 "#
15070 .unindent(),
15071 );
15072}
15073
15074#[gpui::test]
15075async fn test_diff_base_change_with_expanded_diff_hunks(
15076 executor: BackgroundExecutor,
15077 cx: &mut TestAppContext,
15078) {
15079 init_test(cx, |_| {});
15080
15081 let mut cx = EditorTestContext::new(cx).await;
15082
15083 let diff_base = r#"
15084 use some::mod1;
15085 use some::mod2;
15086
15087 const A: u32 = 42;
15088 const B: u32 = 42;
15089 const C: u32 = 42;
15090
15091 fn main() {
15092 println!("hello");
15093
15094 println!("world");
15095 }
15096 "#
15097 .unindent();
15098
15099 cx.set_state(
15100 &r#"
15101 use some::mod2;
15102
15103 const A: u32 = 42;
15104 const C: u32 = 42;
15105
15106 fn main(ˇ) {
15107 //println!("hello");
15108
15109 println!("world");
15110 //
15111 //
15112 }
15113 "#
15114 .unindent(),
15115 );
15116
15117 cx.set_head_text(&diff_base);
15118 executor.run_until_parked();
15119
15120 cx.update_editor(|editor, window, cx| {
15121 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15122 });
15123 executor.run_until_parked();
15124 cx.assert_state_with_diff(
15125 r#"
15126 - use some::mod1;
15127 use some::mod2;
15128
15129 const A: u32 = 42;
15130 - const B: u32 = 42;
15131 const C: u32 = 42;
15132
15133 fn main(ˇ) {
15134 - println!("hello");
15135 + //println!("hello");
15136
15137 println!("world");
15138 + //
15139 + //
15140 }
15141 "#
15142 .unindent(),
15143 );
15144
15145 cx.set_head_text("new diff base!");
15146 executor.run_until_parked();
15147 cx.assert_state_with_diff(
15148 r#"
15149 - new diff base!
15150 + use some::mod2;
15151 +
15152 + const A: u32 = 42;
15153 + const C: u32 = 42;
15154 +
15155 + fn main(ˇ) {
15156 + //println!("hello");
15157 +
15158 + println!("world");
15159 + //
15160 + //
15161 + }
15162 "#
15163 .unindent(),
15164 );
15165}
15166
15167#[gpui::test]
15168async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15169 init_test(cx, |_| {});
15170
15171 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15172 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15173 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15174 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15175 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15176 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15177
15178 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15179 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15180 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15181
15182 let multi_buffer = cx.new(|cx| {
15183 let mut multibuffer = MultiBuffer::new(ReadWrite);
15184 multibuffer.push_excerpts(
15185 buffer_1.clone(),
15186 [
15187 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15188 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15189 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15190 ],
15191 cx,
15192 );
15193 multibuffer.push_excerpts(
15194 buffer_2.clone(),
15195 [
15196 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15197 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15198 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15199 ],
15200 cx,
15201 );
15202 multibuffer.push_excerpts(
15203 buffer_3.clone(),
15204 [
15205 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15206 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15207 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15208 ],
15209 cx,
15210 );
15211 multibuffer
15212 });
15213
15214 let editor =
15215 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15216 editor
15217 .update(cx, |editor, _window, cx| {
15218 for (buffer, diff_base) in [
15219 (buffer_1.clone(), file_1_old),
15220 (buffer_2.clone(), file_2_old),
15221 (buffer_3.clone(), file_3_old),
15222 ] {
15223 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15224 editor
15225 .buffer
15226 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15227 }
15228 })
15229 .unwrap();
15230
15231 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15232 cx.run_until_parked();
15233
15234 cx.assert_editor_state(
15235 &"
15236 ˇaaa
15237 ccc
15238 ddd
15239
15240 ggg
15241 hhh
15242
15243
15244 lll
15245 mmm
15246 NNN
15247
15248 qqq
15249 rrr
15250
15251 uuu
15252 111
15253 222
15254 333
15255
15256 666
15257 777
15258
15259 000
15260 !!!"
15261 .unindent(),
15262 );
15263
15264 cx.update_editor(|editor, window, cx| {
15265 editor.select_all(&SelectAll, window, cx);
15266 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15267 });
15268 cx.executor().run_until_parked();
15269
15270 cx.assert_state_with_diff(
15271 "
15272 «aaa
15273 - bbb
15274 ccc
15275 ddd
15276
15277 ggg
15278 hhh
15279
15280
15281 lll
15282 mmm
15283 - nnn
15284 + NNN
15285
15286 qqq
15287 rrr
15288
15289 uuu
15290 111
15291 222
15292 333
15293
15294 + 666
15295 777
15296
15297 000
15298 !!!ˇ»"
15299 .unindent(),
15300 );
15301}
15302
15303#[gpui::test]
15304async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15305 init_test(cx, |_| {});
15306
15307 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15308 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15309
15310 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15311 let multi_buffer = cx.new(|cx| {
15312 let mut multibuffer = MultiBuffer::new(ReadWrite);
15313 multibuffer.push_excerpts(
15314 buffer.clone(),
15315 [
15316 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15317 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15318 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15319 ],
15320 cx,
15321 );
15322 multibuffer
15323 });
15324
15325 let editor =
15326 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15327 editor
15328 .update(cx, |editor, _window, cx| {
15329 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15330 editor
15331 .buffer
15332 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15333 })
15334 .unwrap();
15335
15336 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15337 cx.run_until_parked();
15338
15339 cx.update_editor(|editor, window, cx| {
15340 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15341 });
15342 cx.executor().run_until_parked();
15343
15344 // When the start of a hunk coincides with the start of its excerpt,
15345 // the hunk is expanded. When the start of a a hunk is earlier than
15346 // the start of its excerpt, the hunk is not expanded.
15347 cx.assert_state_with_diff(
15348 "
15349 ˇaaa
15350 - bbb
15351 + BBB
15352
15353 - ddd
15354 - eee
15355 + DDD
15356 + EEE
15357 fff
15358
15359 iii
15360 "
15361 .unindent(),
15362 );
15363}
15364
15365#[gpui::test]
15366async fn test_edits_around_expanded_insertion_hunks(
15367 executor: BackgroundExecutor,
15368 cx: &mut TestAppContext,
15369) {
15370 init_test(cx, |_| {});
15371
15372 let mut cx = EditorTestContext::new(cx).await;
15373
15374 let diff_base = r#"
15375 use some::mod1;
15376 use some::mod2;
15377
15378 const A: u32 = 42;
15379
15380 fn main() {
15381 println!("hello");
15382
15383 println!("world");
15384 }
15385 "#
15386 .unindent();
15387 executor.run_until_parked();
15388 cx.set_state(
15389 &r#"
15390 use some::mod1;
15391 use some::mod2;
15392
15393 const A: u32 = 42;
15394 const B: u32 = 42;
15395 const C: u32 = 42;
15396 ˇ
15397
15398 fn main() {
15399 println!("hello");
15400
15401 println!("world");
15402 }
15403 "#
15404 .unindent(),
15405 );
15406
15407 cx.set_head_text(&diff_base);
15408 executor.run_until_parked();
15409
15410 cx.update_editor(|editor, window, cx| {
15411 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15412 });
15413 executor.run_until_parked();
15414
15415 cx.assert_state_with_diff(
15416 r#"
15417 use some::mod1;
15418 use some::mod2;
15419
15420 const A: u32 = 42;
15421 + const B: u32 = 42;
15422 + const C: u32 = 42;
15423 + ˇ
15424
15425 fn main() {
15426 println!("hello");
15427
15428 println!("world");
15429 }
15430 "#
15431 .unindent(),
15432 );
15433
15434 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15435 executor.run_until_parked();
15436
15437 cx.assert_state_with_diff(
15438 r#"
15439 use some::mod1;
15440 use some::mod2;
15441
15442 const A: u32 = 42;
15443 + const B: u32 = 42;
15444 + const C: u32 = 42;
15445 + const D: u32 = 42;
15446 + ˇ
15447
15448 fn main() {
15449 println!("hello");
15450
15451 println!("world");
15452 }
15453 "#
15454 .unindent(),
15455 );
15456
15457 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15458 executor.run_until_parked();
15459
15460 cx.assert_state_with_diff(
15461 r#"
15462 use some::mod1;
15463 use some::mod2;
15464
15465 const A: u32 = 42;
15466 + const B: u32 = 42;
15467 + const C: u32 = 42;
15468 + const D: u32 = 42;
15469 + const E: u32 = 42;
15470 + ˇ
15471
15472 fn main() {
15473 println!("hello");
15474
15475 println!("world");
15476 }
15477 "#
15478 .unindent(),
15479 );
15480
15481 cx.update_editor(|editor, window, cx| {
15482 editor.delete_line(&DeleteLine, window, cx);
15483 });
15484 executor.run_until_parked();
15485
15486 cx.assert_state_with_diff(
15487 r#"
15488 use some::mod1;
15489 use some::mod2;
15490
15491 const A: u32 = 42;
15492 + const B: u32 = 42;
15493 + const C: u32 = 42;
15494 + const D: u32 = 42;
15495 + const E: u32 = 42;
15496 ˇ
15497 fn main() {
15498 println!("hello");
15499
15500 println!("world");
15501 }
15502 "#
15503 .unindent(),
15504 );
15505
15506 cx.update_editor(|editor, window, cx| {
15507 editor.move_up(&MoveUp, window, cx);
15508 editor.delete_line(&DeleteLine, window, cx);
15509 editor.move_up(&MoveUp, window, cx);
15510 editor.delete_line(&DeleteLine, window, cx);
15511 editor.move_up(&MoveUp, window, cx);
15512 editor.delete_line(&DeleteLine, window, cx);
15513 });
15514 executor.run_until_parked();
15515 cx.assert_state_with_diff(
15516 r#"
15517 use some::mod1;
15518 use some::mod2;
15519
15520 const A: u32 = 42;
15521 + const B: u32 = 42;
15522 ˇ
15523 fn main() {
15524 println!("hello");
15525
15526 println!("world");
15527 }
15528 "#
15529 .unindent(),
15530 );
15531
15532 cx.update_editor(|editor, window, cx| {
15533 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15534 editor.delete_line(&DeleteLine, window, cx);
15535 });
15536 executor.run_until_parked();
15537 cx.assert_state_with_diff(
15538 r#"
15539 ˇ
15540 fn main() {
15541 println!("hello");
15542
15543 println!("world");
15544 }
15545 "#
15546 .unindent(),
15547 );
15548}
15549
15550#[gpui::test]
15551async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15552 init_test(cx, |_| {});
15553
15554 let mut cx = EditorTestContext::new(cx).await;
15555 cx.set_head_text(indoc! { "
15556 one
15557 two
15558 three
15559 four
15560 five
15561 "
15562 });
15563 cx.set_state(indoc! { "
15564 one
15565 ˇthree
15566 five
15567 "});
15568 cx.run_until_parked();
15569 cx.update_editor(|editor, window, cx| {
15570 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15571 });
15572 cx.assert_state_with_diff(
15573 indoc! { "
15574 one
15575 - two
15576 ˇthree
15577 - four
15578 five
15579 "}
15580 .to_string(),
15581 );
15582 cx.update_editor(|editor, window, cx| {
15583 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15584 });
15585
15586 cx.assert_state_with_diff(
15587 indoc! { "
15588 one
15589 ˇthree
15590 five
15591 "}
15592 .to_string(),
15593 );
15594
15595 cx.set_state(indoc! { "
15596 one
15597 ˇTWO
15598 three
15599 four
15600 five
15601 "});
15602 cx.run_until_parked();
15603 cx.update_editor(|editor, window, cx| {
15604 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15605 });
15606
15607 cx.assert_state_with_diff(
15608 indoc! { "
15609 one
15610 - two
15611 + ˇTWO
15612 three
15613 four
15614 five
15615 "}
15616 .to_string(),
15617 );
15618 cx.update_editor(|editor, window, cx| {
15619 editor.move_up(&Default::default(), window, cx);
15620 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15621 });
15622 cx.assert_state_with_diff(
15623 indoc! { "
15624 one
15625 ˇTWO
15626 three
15627 four
15628 five
15629 "}
15630 .to_string(),
15631 );
15632}
15633
15634#[gpui::test]
15635async fn test_edits_around_expanded_deletion_hunks(
15636 executor: BackgroundExecutor,
15637 cx: &mut TestAppContext,
15638) {
15639 init_test(cx, |_| {});
15640
15641 let mut cx = EditorTestContext::new(cx).await;
15642
15643 let diff_base = r#"
15644 use some::mod1;
15645 use some::mod2;
15646
15647 const A: u32 = 42;
15648 const B: u32 = 42;
15649 const C: u32 = 42;
15650
15651
15652 fn main() {
15653 println!("hello");
15654
15655 println!("world");
15656 }
15657 "#
15658 .unindent();
15659 executor.run_until_parked();
15660 cx.set_state(
15661 &r#"
15662 use some::mod1;
15663 use some::mod2;
15664
15665 ˇconst B: u32 = 42;
15666 const C: u32 = 42;
15667
15668
15669 fn main() {
15670 println!("hello");
15671
15672 println!("world");
15673 }
15674 "#
15675 .unindent(),
15676 );
15677
15678 cx.set_head_text(&diff_base);
15679 executor.run_until_parked();
15680
15681 cx.update_editor(|editor, window, cx| {
15682 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15683 });
15684 executor.run_until_parked();
15685
15686 cx.assert_state_with_diff(
15687 r#"
15688 use some::mod1;
15689 use some::mod2;
15690
15691 - const A: u32 = 42;
15692 ˇconst B: u32 = 42;
15693 const C: u32 = 42;
15694
15695
15696 fn main() {
15697 println!("hello");
15698
15699 println!("world");
15700 }
15701 "#
15702 .unindent(),
15703 );
15704
15705 cx.update_editor(|editor, window, cx| {
15706 editor.delete_line(&DeleteLine, window, cx);
15707 });
15708 executor.run_until_parked();
15709 cx.assert_state_with_diff(
15710 r#"
15711 use some::mod1;
15712 use some::mod2;
15713
15714 - const A: u32 = 42;
15715 - const B: u32 = 42;
15716 ˇconst C: u32 = 42;
15717
15718
15719 fn main() {
15720 println!("hello");
15721
15722 println!("world");
15723 }
15724 "#
15725 .unindent(),
15726 );
15727
15728 cx.update_editor(|editor, window, cx| {
15729 editor.delete_line(&DeleteLine, window, cx);
15730 });
15731 executor.run_until_parked();
15732 cx.assert_state_with_diff(
15733 r#"
15734 use some::mod1;
15735 use some::mod2;
15736
15737 - const A: u32 = 42;
15738 - const B: u32 = 42;
15739 - const C: u32 = 42;
15740 ˇ
15741
15742 fn main() {
15743 println!("hello");
15744
15745 println!("world");
15746 }
15747 "#
15748 .unindent(),
15749 );
15750
15751 cx.update_editor(|editor, window, cx| {
15752 editor.handle_input("replacement", window, cx);
15753 });
15754 executor.run_until_parked();
15755 cx.assert_state_with_diff(
15756 r#"
15757 use some::mod1;
15758 use some::mod2;
15759
15760 - const A: u32 = 42;
15761 - const B: u32 = 42;
15762 - const C: u32 = 42;
15763 -
15764 + replacementˇ
15765
15766 fn main() {
15767 println!("hello");
15768
15769 println!("world");
15770 }
15771 "#
15772 .unindent(),
15773 );
15774}
15775
15776#[gpui::test]
15777async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15778 init_test(cx, |_| {});
15779
15780 let mut cx = EditorTestContext::new(cx).await;
15781
15782 let base_text = r#"
15783 one
15784 two
15785 three
15786 four
15787 five
15788 "#
15789 .unindent();
15790 executor.run_until_parked();
15791 cx.set_state(
15792 &r#"
15793 one
15794 two
15795 fˇour
15796 five
15797 "#
15798 .unindent(),
15799 );
15800
15801 cx.set_head_text(&base_text);
15802 executor.run_until_parked();
15803
15804 cx.update_editor(|editor, window, cx| {
15805 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15806 });
15807 executor.run_until_parked();
15808
15809 cx.assert_state_with_diff(
15810 r#"
15811 one
15812 two
15813 - three
15814 fˇour
15815 five
15816 "#
15817 .unindent(),
15818 );
15819
15820 cx.update_editor(|editor, window, cx| {
15821 editor.backspace(&Backspace, window, cx);
15822 editor.backspace(&Backspace, window, cx);
15823 });
15824 executor.run_until_parked();
15825 cx.assert_state_with_diff(
15826 r#"
15827 one
15828 two
15829 - threeˇ
15830 - four
15831 + our
15832 five
15833 "#
15834 .unindent(),
15835 );
15836}
15837
15838#[gpui::test]
15839async fn test_edit_after_expanded_modification_hunk(
15840 executor: BackgroundExecutor,
15841 cx: &mut TestAppContext,
15842) {
15843 init_test(cx, |_| {});
15844
15845 let mut cx = EditorTestContext::new(cx).await;
15846
15847 let diff_base = r#"
15848 use some::mod1;
15849 use some::mod2;
15850
15851 const A: u32 = 42;
15852 const B: u32 = 42;
15853 const C: u32 = 42;
15854 const D: u32 = 42;
15855
15856
15857 fn main() {
15858 println!("hello");
15859
15860 println!("world");
15861 }"#
15862 .unindent();
15863
15864 cx.set_state(
15865 &r#"
15866 use some::mod1;
15867 use some::mod2;
15868
15869 const A: u32 = 42;
15870 const B: u32 = 42;
15871 const C: u32 = 43ˇ
15872 const D: u32 = 42;
15873
15874
15875 fn main() {
15876 println!("hello");
15877
15878 println!("world");
15879 }"#
15880 .unindent(),
15881 );
15882
15883 cx.set_head_text(&diff_base);
15884 executor.run_until_parked();
15885 cx.update_editor(|editor, window, cx| {
15886 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15887 });
15888 executor.run_until_parked();
15889
15890 cx.assert_state_with_diff(
15891 r#"
15892 use some::mod1;
15893 use some::mod2;
15894
15895 const A: u32 = 42;
15896 const B: u32 = 42;
15897 - const C: u32 = 42;
15898 + const C: u32 = 43ˇ
15899 const D: u32 = 42;
15900
15901
15902 fn main() {
15903 println!("hello");
15904
15905 println!("world");
15906 }"#
15907 .unindent(),
15908 );
15909
15910 cx.update_editor(|editor, window, cx| {
15911 editor.handle_input("\nnew_line\n", window, cx);
15912 });
15913 executor.run_until_parked();
15914
15915 cx.assert_state_with_diff(
15916 r#"
15917 use some::mod1;
15918 use some::mod2;
15919
15920 const A: u32 = 42;
15921 const B: u32 = 42;
15922 - const C: u32 = 42;
15923 + const C: u32 = 43
15924 + new_line
15925 + ˇ
15926 const D: u32 = 42;
15927
15928
15929 fn main() {
15930 println!("hello");
15931
15932 println!("world");
15933 }"#
15934 .unindent(),
15935 );
15936}
15937
15938#[gpui::test]
15939async fn test_stage_and_unstage_added_file_hunk(
15940 executor: BackgroundExecutor,
15941 cx: &mut TestAppContext,
15942) {
15943 init_test(cx, |_| {});
15944
15945 let mut cx = EditorTestContext::new(cx).await;
15946 cx.update_editor(|editor, _, cx| {
15947 editor.set_expand_all_diff_hunks(cx);
15948 });
15949
15950 let working_copy = r#"
15951 ˇfn main() {
15952 println!("hello, world!");
15953 }
15954 "#
15955 .unindent();
15956
15957 cx.set_state(&working_copy);
15958 executor.run_until_parked();
15959
15960 cx.assert_state_with_diff(
15961 r#"
15962 + ˇfn main() {
15963 + println!("hello, world!");
15964 + }
15965 "#
15966 .unindent(),
15967 );
15968 cx.assert_index_text(None);
15969
15970 cx.update_editor(|editor, window, cx| {
15971 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15972 });
15973 executor.run_until_parked();
15974 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15975 cx.assert_state_with_diff(
15976 r#"
15977 + ˇfn main() {
15978 + println!("hello, world!");
15979 + }
15980 "#
15981 .unindent(),
15982 );
15983
15984 cx.update_editor(|editor, window, cx| {
15985 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15986 });
15987 executor.run_until_parked();
15988 cx.assert_index_text(None);
15989}
15990
15991async fn setup_indent_guides_editor(
15992 text: &str,
15993 cx: &mut TestAppContext,
15994) -> (BufferId, EditorTestContext) {
15995 init_test(cx, |_| {});
15996
15997 let mut cx = EditorTestContext::new(cx).await;
15998
15999 let buffer_id = cx.update_editor(|editor, window, cx| {
16000 editor.set_text(text, window, cx);
16001 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16002
16003 buffer_ids[0]
16004 });
16005
16006 (buffer_id, cx)
16007}
16008
16009fn assert_indent_guides(
16010 range: Range<u32>,
16011 expected: Vec<IndentGuide>,
16012 active_indices: Option<Vec<usize>>,
16013 cx: &mut EditorTestContext,
16014) {
16015 let indent_guides = cx.update_editor(|editor, window, cx| {
16016 let snapshot = editor.snapshot(window, cx).display_snapshot;
16017 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16018 editor,
16019 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16020 true,
16021 &snapshot,
16022 cx,
16023 );
16024
16025 indent_guides.sort_by(|a, b| {
16026 a.depth.cmp(&b.depth).then(
16027 a.start_row
16028 .cmp(&b.start_row)
16029 .then(a.end_row.cmp(&b.end_row)),
16030 )
16031 });
16032 indent_guides
16033 });
16034
16035 if let Some(expected) = active_indices {
16036 let active_indices = cx.update_editor(|editor, window, cx| {
16037 let snapshot = editor.snapshot(window, cx).display_snapshot;
16038 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16039 });
16040
16041 assert_eq!(
16042 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16043 expected,
16044 "Active indent guide indices do not match"
16045 );
16046 }
16047
16048 assert_eq!(indent_guides, expected, "Indent guides do not match");
16049}
16050
16051fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16052 IndentGuide {
16053 buffer_id,
16054 start_row: MultiBufferRow(start_row),
16055 end_row: MultiBufferRow(end_row),
16056 depth,
16057 tab_size: 4,
16058 settings: IndentGuideSettings {
16059 enabled: true,
16060 line_width: 1,
16061 active_line_width: 1,
16062 ..Default::default()
16063 },
16064 }
16065}
16066
16067#[gpui::test]
16068async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16069 let (buffer_id, mut cx) = setup_indent_guides_editor(
16070 &"
16071 fn main() {
16072 let a = 1;
16073 }"
16074 .unindent(),
16075 cx,
16076 )
16077 .await;
16078
16079 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16080}
16081
16082#[gpui::test]
16083async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16084 let (buffer_id, mut cx) = setup_indent_guides_editor(
16085 &"
16086 fn main() {
16087 let a = 1;
16088 let b = 2;
16089 }"
16090 .unindent(),
16091 cx,
16092 )
16093 .await;
16094
16095 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16096}
16097
16098#[gpui::test]
16099async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16100 let (buffer_id, mut cx) = setup_indent_guides_editor(
16101 &"
16102 fn main() {
16103 let a = 1;
16104 if a == 3 {
16105 let b = 2;
16106 } else {
16107 let c = 3;
16108 }
16109 }"
16110 .unindent(),
16111 cx,
16112 )
16113 .await;
16114
16115 assert_indent_guides(
16116 0..8,
16117 vec![
16118 indent_guide(buffer_id, 1, 6, 0),
16119 indent_guide(buffer_id, 3, 3, 1),
16120 indent_guide(buffer_id, 5, 5, 1),
16121 ],
16122 None,
16123 &mut cx,
16124 );
16125}
16126
16127#[gpui::test]
16128async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16129 let (buffer_id, mut cx) = setup_indent_guides_editor(
16130 &"
16131 fn main() {
16132 let a = 1;
16133 let b = 2;
16134 let c = 3;
16135 }"
16136 .unindent(),
16137 cx,
16138 )
16139 .await;
16140
16141 assert_indent_guides(
16142 0..5,
16143 vec![
16144 indent_guide(buffer_id, 1, 3, 0),
16145 indent_guide(buffer_id, 2, 2, 1),
16146 ],
16147 None,
16148 &mut cx,
16149 );
16150}
16151
16152#[gpui::test]
16153async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16154 let (buffer_id, mut cx) = setup_indent_guides_editor(
16155 &"
16156 fn main() {
16157 let a = 1;
16158
16159 let c = 3;
16160 }"
16161 .unindent(),
16162 cx,
16163 )
16164 .await;
16165
16166 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16167}
16168
16169#[gpui::test]
16170async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16171 let (buffer_id, mut cx) = setup_indent_guides_editor(
16172 &"
16173 fn main() {
16174 let a = 1;
16175
16176 let c = 3;
16177
16178 if a == 3 {
16179 let b = 2;
16180 } else {
16181 let c = 3;
16182 }
16183 }"
16184 .unindent(),
16185 cx,
16186 )
16187 .await;
16188
16189 assert_indent_guides(
16190 0..11,
16191 vec![
16192 indent_guide(buffer_id, 1, 9, 0),
16193 indent_guide(buffer_id, 6, 6, 1),
16194 indent_guide(buffer_id, 8, 8, 1),
16195 ],
16196 None,
16197 &mut cx,
16198 );
16199}
16200
16201#[gpui::test]
16202async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16203 let (buffer_id, mut cx) = setup_indent_guides_editor(
16204 &"
16205 fn main() {
16206 let a = 1;
16207
16208 let c = 3;
16209
16210 if a == 3 {
16211 let b = 2;
16212 } else {
16213 let c = 3;
16214 }
16215 }"
16216 .unindent(),
16217 cx,
16218 )
16219 .await;
16220
16221 assert_indent_guides(
16222 1..11,
16223 vec![
16224 indent_guide(buffer_id, 1, 9, 0),
16225 indent_guide(buffer_id, 6, 6, 1),
16226 indent_guide(buffer_id, 8, 8, 1),
16227 ],
16228 None,
16229 &mut cx,
16230 );
16231}
16232
16233#[gpui::test]
16234async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16235 let (buffer_id, mut cx) = setup_indent_guides_editor(
16236 &"
16237 fn main() {
16238 let a = 1;
16239
16240 let c = 3;
16241
16242 if a == 3 {
16243 let b = 2;
16244 } else {
16245 let c = 3;
16246 }
16247 }"
16248 .unindent(),
16249 cx,
16250 )
16251 .await;
16252
16253 assert_indent_guides(
16254 1..10,
16255 vec![
16256 indent_guide(buffer_id, 1, 9, 0),
16257 indent_guide(buffer_id, 6, 6, 1),
16258 indent_guide(buffer_id, 8, 8, 1),
16259 ],
16260 None,
16261 &mut cx,
16262 );
16263}
16264
16265#[gpui::test]
16266async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16267 let (buffer_id, mut cx) = setup_indent_guides_editor(
16268 &"
16269 block1
16270 block2
16271 block3
16272 block4
16273 block2
16274 block1
16275 block1"
16276 .unindent(),
16277 cx,
16278 )
16279 .await;
16280
16281 assert_indent_guides(
16282 1..10,
16283 vec![
16284 indent_guide(buffer_id, 1, 4, 0),
16285 indent_guide(buffer_id, 2, 3, 1),
16286 indent_guide(buffer_id, 3, 3, 2),
16287 ],
16288 None,
16289 &mut cx,
16290 );
16291}
16292
16293#[gpui::test]
16294async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16295 let (buffer_id, mut cx) = setup_indent_guides_editor(
16296 &"
16297 block1
16298 block2
16299 block3
16300
16301 block1
16302 block1"
16303 .unindent(),
16304 cx,
16305 )
16306 .await;
16307
16308 assert_indent_guides(
16309 0..6,
16310 vec![
16311 indent_guide(buffer_id, 1, 2, 0),
16312 indent_guide(buffer_id, 2, 2, 1),
16313 ],
16314 None,
16315 &mut cx,
16316 );
16317}
16318
16319#[gpui::test]
16320async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16321 let (buffer_id, mut cx) = setup_indent_guides_editor(
16322 &"
16323 block1
16324
16325
16326
16327 block2
16328 "
16329 .unindent(),
16330 cx,
16331 )
16332 .await;
16333
16334 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16335}
16336
16337#[gpui::test]
16338async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16339 let (buffer_id, mut cx) = setup_indent_guides_editor(
16340 &"
16341 def a:
16342 \tb = 3
16343 \tif True:
16344 \t\tc = 4
16345 \t\td = 5
16346 \tprint(b)
16347 "
16348 .unindent(),
16349 cx,
16350 )
16351 .await;
16352
16353 assert_indent_guides(
16354 0..6,
16355 vec![
16356 indent_guide(buffer_id, 1, 6, 0),
16357 indent_guide(buffer_id, 3, 4, 1),
16358 ],
16359 None,
16360 &mut cx,
16361 );
16362}
16363
16364#[gpui::test]
16365async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16366 let (buffer_id, mut cx) = setup_indent_guides_editor(
16367 &"
16368 fn main() {
16369 let a = 1;
16370 }"
16371 .unindent(),
16372 cx,
16373 )
16374 .await;
16375
16376 cx.update_editor(|editor, window, cx| {
16377 editor.change_selections(None, window, cx, |s| {
16378 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16379 });
16380 });
16381
16382 assert_indent_guides(
16383 0..3,
16384 vec![indent_guide(buffer_id, 1, 1, 0)],
16385 Some(vec![0]),
16386 &mut cx,
16387 );
16388}
16389
16390#[gpui::test]
16391async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16392 let (buffer_id, mut cx) = setup_indent_guides_editor(
16393 &"
16394 fn main() {
16395 if 1 == 2 {
16396 let a = 1;
16397 }
16398 }"
16399 .unindent(),
16400 cx,
16401 )
16402 .await;
16403
16404 cx.update_editor(|editor, window, cx| {
16405 editor.change_selections(None, window, cx, |s| {
16406 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16407 });
16408 });
16409
16410 assert_indent_guides(
16411 0..4,
16412 vec![
16413 indent_guide(buffer_id, 1, 3, 0),
16414 indent_guide(buffer_id, 2, 2, 1),
16415 ],
16416 Some(vec![1]),
16417 &mut cx,
16418 );
16419
16420 cx.update_editor(|editor, window, cx| {
16421 editor.change_selections(None, window, cx, |s| {
16422 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16423 });
16424 });
16425
16426 assert_indent_guides(
16427 0..4,
16428 vec![
16429 indent_guide(buffer_id, 1, 3, 0),
16430 indent_guide(buffer_id, 2, 2, 1),
16431 ],
16432 Some(vec![1]),
16433 &mut cx,
16434 );
16435
16436 cx.update_editor(|editor, window, cx| {
16437 editor.change_selections(None, window, cx, |s| {
16438 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16439 });
16440 });
16441
16442 assert_indent_guides(
16443 0..4,
16444 vec![
16445 indent_guide(buffer_id, 1, 3, 0),
16446 indent_guide(buffer_id, 2, 2, 1),
16447 ],
16448 Some(vec![0]),
16449 &mut cx,
16450 );
16451}
16452
16453#[gpui::test]
16454async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16455 let (buffer_id, mut cx) = setup_indent_guides_editor(
16456 &"
16457 fn main() {
16458 let a = 1;
16459
16460 let b = 2;
16461 }"
16462 .unindent(),
16463 cx,
16464 )
16465 .await;
16466
16467 cx.update_editor(|editor, window, cx| {
16468 editor.change_selections(None, window, cx, |s| {
16469 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16470 });
16471 });
16472
16473 assert_indent_guides(
16474 0..5,
16475 vec![indent_guide(buffer_id, 1, 3, 0)],
16476 Some(vec![0]),
16477 &mut cx,
16478 );
16479}
16480
16481#[gpui::test]
16482async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16483 let (buffer_id, mut cx) = setup_indent_guides_editor(
16484 &"
16485 def m:
16486 a = 1
16487 pass"
16488 .unindent(),
16489 cx,
16490 )
16491 .await;
16492
16493 cx.update_editor(|editor, window, cx| {
16494 editor.change_selections(None, window, cx, |s| {
16495 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16496 });
16497 });
16498
16499 assert_indent_guides(
16500 0..3,
16501 vec![indent_guide(buffer_id, 1, 2, 0)],
16502 Some(vec![0]),
16503 &mut cx,
16504 );
16505}
16506
16507#[gpui::test]
16508async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16509 init_test(cx, |_| {});
16510 let mut cx = EditorTestContext::new(cx).await;
16511 let text = indoc! {
16512 "
16513 impl A {
16514 fn b() {
16515 0;
16516 3;
16517 5;
16518 6;
16519 7;
16520 }
16521 }
16522 "
16523 };
16524 let base_text = indoc! {
16525 "
16526 impl A {
16527 fn b() {
16528 0;
16529 1;
16530 2;
16531 3;
16532 4;
16533 }
16534 fn c() {
16535 5;
16536 6;
16537 7;
16538 }
16539 }
16540 "
16541 };
16542
16543 cx.update_editor(|editor, window, cx| {
16544 editor.set_text(text, window, cx);
16545
16546 editor.buffer().update(cx, |multibuffer, cx| {
16547 let buffer = multibuffer.as_singleton().unwrap();
16548 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16549
16550 multibuffer.set_all_diff_hunks_expanded(cx);
16551 multibuffer.add_diff(diff, cx);
16552
16553 buffer.read(cx).remote_id()
16554 })
16555 });
16556 cx.run_until_parked();
16557
16558 cx.assert_state_with_diff(
16559 indoc! { "
16560 impl A {
16561 fn b() {
16562 0;
16563 - 1;
16564 - 2;
16565 3;
16566 - 4;
16567 - }
16568 - fn c() {
16569 5;
16570 6;
16571 7;
16572 }
16573 }
16574 ˇ"
16575 }
16576 .to_string(),
16577 );
16578
16579 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16580 editor
16581 .snapshot(window, cx)
16582 .buffer_snapshot
16583 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16584 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16585 .collect::<Vec<_>>()
16586 });
16587 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16588 assert_eq!(
16589 actual_guides,
16590 vec![
16591 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16592 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16593 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16594 ]
16595 );
16596}
16597
16598#[gpui::test]
16599async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16600 init_test(cx, |_| {});
16601 let mut cx = EditorTestContext::new(cx).await;
16602
16603 let diff_base = r#"
16604 a
16605 b
16606 c
16607 "#
16608 .unindent();
16609
16610 cx.set_state(
16611 &r#"
16612 ˇA
16613 b
16614 C
16615 "#
16616 .unindent(),
16617 );
16618 cx.set_head_text(&diff_base);
16619 cx.update_editor(|editor, window, cx| {
16620 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16621 });
16622 executor.run_until_parked();
16623
16624 let both_hunks_expanded = r#"
16625 - a
16626 + ˇA
16627 b
16628 - c
16629 + C
16630 "#
16631 .unindent();
16632
16633 cx.assert_state_with_diff(both_hunks_expanded.clone());
16634
16635 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16636 let snapshot = editor.snapshot(window, cx);
16637 let hunks = editor
16638 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16639 .collect::<Vec<_>>();
16640 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16641 let buffer_id = hunks[0].buffer_id;
16642 hunks
16643 .into_iter()
16644 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16645 .collect::<Vec<_>>()
16646 });
16647 assert_eq!(hunk_ranges.len(), 2);
16648
16649 cx.update_editor(|editor, _, cx| {
16650 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16651 });
16652 executor.run_until_parked();
16653
16654 let second_hunk_expanded = r#"
16655 ˇA
16656 b
16657 - c
16658 + C
16659 "#
16660 .unindent();
16661
16662 cx.assert_state_with_diff(second_hunk_expanded);
16663
16664 cx.update_editor(|editor, _, cx| {
16665 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16666 });
16667 executor.run_until_parked();
16668
16669 cx.assert_state_with_diff(both_hunks_expanded.clone());
16670
16671 cx.update_editor(|editor, _, cx| {
16672 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16673 });
16674 executor.run_until_parked();
16675
16676 let first_hunk_expanded = r#"
16677 - a
16678 + ˇA
16679 b
16680 C
16681 "#
16682 .unindent();
16683
16684 cx.assert_state_with_diff(first_hunk_expanded);
16685
16686 cx.update_editor(|editor, _, cx| {
16687 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16688 });
16689 executor.run_until_parked();
16690
16691 cx.assert_state_with_diff(both_hunks_expanded);
16692
16693 cx.set_state(
16694 &r#"
16695 ˇA
16696 b
16697 "#
16698 .unindent(),
16699 );
16700 cx.run_until_parked();
16701
16702 // TODO this cursor position seems bad
16703 cx.assert_state_with_diff(
16704 r#"
16705 - ˇa
16706 + A
16707 b
16708 "#
16709 .unindent(),
16710 );
16711
16712 cx.update_editor(|editor, window, cx| {
16713 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16714 });
16715
16716 cx.assert_state_with_diff(
16717 r#"
16718 - ˇa
16719 + A
16720 b
16721 - c
16722 "#
16723 .unindent(),
16724 );
16725
16726 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16727 let snapshot = editor.snapshot(window, cx);
16728 let hunks = editor
16729 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16730 .collect::<Vec<_>>();
16731 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16732 let buffer_id = hunks[0].buffer_id;
16733 hunks
16734 .into_iter()
16735 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16736 .collect::<Vec<_>>()
16737 });
16738 assert_eq!(hunk_ranges.len(), 2);
16739
16740 cx.update_editor(|editor, _, cx| {
16741 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16742 });
16743 executor.run_until_parked();
16744
16745 cx.assert_state_with_diff(
16746 r#"
16747 - ˇa
16748 + A
16749 b
16750 "#
16751 .unindent(),
16752 );
16753}
16754
16755#[gpui::test]
16756async fn test_toggle_deletion_hunk_at_start_of_file(
16757 executor: BackgroundExecutor,
16758 cx: &mut TestAppContext,
16759) {
16760 init_test(cx, |_| {});
16761 let mut cx = EditorTestContext::new(cx).await;
16762
16763 let diff_base = r#"
16764 a
16765 b
16766 c
16767 "#
16768 .unindent();
16769
16770 cx.set_state(
16771 &r#"
16772 ˇb
16773 c
16774 "#
16775 .unindent(),
16776 );
16777 cx.set_head_text(&diff_base);
16778 cx.update_editor(|editor, window, cx| {
16779 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16780 });
16781 executor.run_until_parked();
16782
16783 let hunk_expanded = r#"
16784 - a
16785 ˇb
16786 c
16787 "#
16788 .unindent();
16789
16790 cx.assert_state_with_diff(hunk_expanded.clone());
16791
16792 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16793 let snapshot = editor.snapshot(window, cx);
16794 let hunks = editor
16795 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16796 .collect::<Vec<_>>();
16797 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16798 let buffer_id = hunks[0].buffer_id;
16799 hunks
16800 .into_iter()
16801 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16802 .collect::<Vec<_>>()
16803 });
16804 assert_eq!(hunk_ranges.len(), 1);
16805
16806 cx.update_editor(|editor, _, cx| {
16807 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16808 });
16809 executor.run_until_parked();
16810
16811 let hunk_collapsed = r#"
16812 ˇb
16813 c
16814 "#
16815 .unindent();
16816
16817 cx.assert_state_with_diff(hunk_collapsed);
16818
16819 cx.update_editor(|editor, _, cx| {
16820 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16821 });
16822 executor.run_until_parked();
16823
16824 cx.assert_state_with_diff(hunk_expanded.clone());
16825}
16826
16827#[gpui::test]
16828async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16829 init_test(cx, |_| {});
16830
16831 let fs = FakeFs::new(cx.executor());
16832 fs.insert_tree(
16833 path!("/test"),
16834 json!({
16835 ".git": {},
16836 "file-1": "ONE\n",
16837 "file-2": "TWO\n",
16838 "file-3": "THREE\n",
16839 }),
16840 )
16841 .await;
16842
16843 fs.set_head_for_repo(
16844 path!("/test/.git").as_ref(),
16845 &[
16846 ("file-1".into(), "one\n".into()),
16847 ("file-2".into(), "two\n".into()),
16848 ("file-3".into(), "three\n".into()),
16849 ],
16850 );
16851
16852 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16853 let mut buffers = vec![];
16854 for i in 1..=3 {
16855 let buffer = project
16856 .update(cx, |project, cx| {
16857 let path = format!(path!("/test/file-{}"), i);
16858 project.open_local_buffer(path, cx)
16859 })
16860 .await
16861 .unwrap();
16862 buffers.push(buffer);
16863 }
16864
16865 let multibuffer = cx.new(|cx| {
16866 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16867 multibuffer.set_all_diff_hunks_expanded(cx);
16868 for buffer in &buffers {
16869 let snapshot = buffer.read(cx).snapshot();
16870 multibuffer.set_excerpts_for_path(
16871 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16872 buffer.clone(),
16873 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16874 DEFAULT_MULTIBUFFER_CONTEXT,
16875 cx,
16876 );
16877 }
16878 multibuffer
16879 });
16880
16881 let editor = cx.add_window(|window, cx| {
16882 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
16883 });
16884 cx.run_until_parked();
16885
16886 let snapshot = editor
16887 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16888 .unwrap();
16889 let hunks = snapshot
16890 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16891 .map(|hunk| match hunk {
16892 DisplayDiffHunk::Unfolded {
16893 display_row_range, ..
16894 } => display_row_range,
16895 DisplayDiffHunk::Folded { .. } => unreachable!(),
16896 })
16897 .collect::<Vec<_>>();
16898 assert_eq!(
16899 hunks,
16900 [
16901 DisplayRow(2)..DisplayRow(4),
16902 DisplayRow(7)..DisplayRow(9),
16903 DisplayRow(12)..DisplayRow(14),
16904 ]
16905 );
16906}
16907
16908#[gpui::test]
16909async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16910 init_test(cx, |_| {});
16911
16912 let mut cx = EditorTestContext::new(cx).await;
16913 cx.set_head_text(indoc! { "
16914 one
16915 two
16916 three
16917 four
16918 five
16919 "
16920 });
16921 cx.set_index_text(indoc! { "
16922 one
16923 two
16924 three
16925 four
16926 five
16927 "
16928 });
16929 cx.set_state(indoc! {"
16930 one
16931 TWO
16932 ˇTHREE
16933 FOUR
16934 five
16935 "});
16936 cx.run_until_parked();
16937 cx.update_editor(|editor, window, cx| {
16938 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16939 });
16940 cx.run_until_parked();
16941 cx.assert_index_text(Some(indoc! {"
16942 one
16943 TWO
16944 THREE
16945 FOUR
16946 five
16947 "}));
16948 cx.set_state(indoc! { "
16949 one
16950 TWO
16951 ˇTHREE-HUNDRED
16952 FOUR
16953 five
16954 "});
16955 cx.run_until_parked();
16956 cx.update_editor(|editor, window, cx| {
16957 let snapshot = editor.snapshot(window, cx);
16958 let hunks = editor
16959 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16960 .collect::<Vec<_>>();
16961 assert_eq!(hunks.len(), 1);
16962 assert_eq!(
16963 hunks[0].status(),
16964 DiffHunkStatus {
16965 kind: DiffHunkStatusKind::Modified,
16966 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16967 }
16968 );
16969
16970 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16971 });
16972 cx.run_until_parked();
16973 cx.assert_index_text(Some(indoc! {"
16974 one
16975 TWO
16976 THREE-HUNDRED
16977 FOUR
16978 five
16979 "}));
16980}
16981
16982#[gpui::test]
16983fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16984 init_test(cx, |_| {});
16985
16986 let editor = cx.add_window(|window, cx| {
16987 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16988 build_editor(buffer, window, cx)
16989 });
16990
16991 let render_args = Arc::new(Mutex::new(None));
16992 let snapshot = editor
16993 .update(cx, |editor, window, cx| {
16994 let snapshot = editor.buffer().read(cx).snapshot(cx);
16995 let range =
16996 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16997
16998 struct RenderArgs {
16999 row: MultiBufferRow,
17000 folded: bool,
17001 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17002 }
17003
17004 let crease = Crease::inline(
17005 range,
17006 FoldPlaceholder::test(),
17007 {
17008 let toggle_callback = render_args.clone();
17009 move |row, folded, callback, _window, _cx| {
17010 *toggle_callback.lock() = Some(RenderArgs {
17011 row,
17012 folded,
17013 callback,
17014 });
17015 div()
17016 }
17017 },
17018 |_row, _folded, _window, _cx| div(),
17019 );
17020
17021 editor.insert_creases(Some(crease), cx);
17022 let snapshot = editor.snapshot(window, cx);
17023 let _div = snapshot.render_crease_toggle(
17024 MultiBufferRow(1),
17025 false,
17026 cx.entity().clone(),
17027 window,
17028 cx,
17029 );
17030 snapshot
17031 })
17032 .unwrap();
17033
17034 let render_args = render_args.lock().take().unwrap();
17035 assert_eq!(render_args.row, MultiBufferRow(1));
17036 assert!(!render_args.folded);
17037 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17038
17039 cx.update_window(*editor, |_, window, cx| {
17040 (render_args.callback)(true, window, cx)
17041 })
17042 .unwrap();
17043 let snapshot = editor
17044 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17045 .unwrap();
17046 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17047
17048 cx.update_window(*editor, |_, window, cx| {
17049 (render_args.callback)(false, window, cx)
17050 })
17051 .unwrap();
17052 let snapshot = editor
17053 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17054 .unwrap();
17055 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17056}
17057
17058#[gpui::test]
17059async fn test_input_text(cx: &mut TestAppContext) {
17060 init_test(cx, |_| {});
17061 let mut cx = EditorTestContext::new(cx).await;
17062
17063 cx.set_state(
17064 &r#"ˇone
17065 two
17066
17067 three
17068 fourˇ
17069 five
17070
17071 siˇx"#
17072 .unindent(),
17073 );
17074
17075 cx.dispatch_action(HandleInput(String::new()));
17076 cx.assert_editor_state(
17077 &r#"ˇone
17078 two
17079
17080 three
17081 fourˇ
17082 five
17083
17084 siˇx"#
17085 .unindent(),
17086 );
17087
17088 cx.dispatch_action(HandleInput("AAAA".to_string()));
17089 cx.assert_editor_state(
17090 &r#"AAAAˇone
17091 two
17092
17093 three
17094 fourAAAAˇ
17095 five
17096
17097 siAAAAˇx"#
17098 .unindent(),
17099 );
17100}
17101
17102#[gpui::test]
17103async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17104 init_test(cx, |_| {});
17105
17106 let mut cx = EditorTestContext::new(cx).await;
17107 cx.set_state(
17108 r#"let foo = 1;
17109let foo = 2;
17110let foo = 3;
17111let fooˇ = 4;
17112let foo = 5;
17113let foo = 6;
17114let foo = 7;
17115let foo = 8;
17116let foo = 9;
17117let foo = 10;
17118let foo = 11;
17119let foo = 12;
17120let foo = 13;
17121let foo = 14;
17122let foo = 15;"#,
17123 );
17124
17125 cx.update_editor(|e, window, cx| {
17126 assert_eq!(
17127 e.next_scroll_position,
17128 NextScrollCursorCenterTopBottom::Center,
17129 "Default next scroll direction is center",
17130 );
17131
17132 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17133 assert_eq!(
17134 e.next_scroll_position,
17135 NextScrollCursorCenterTopBottom::Top,
17136 "After center, next scroll direction should be top",
17137 );
17138
17139 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17140 assert_eq!(
17141 e.next_scroll_position,
17142 NextScrollCursorCenterTopBottom::Bottom,
17143 "After top, next scroll direction should be bottom",
17144 );
17145
17146 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17147 assert_eq!(
17148 e.next_scroll_position,
17149 NextScrollCursorCenterTopBottom::Center,
17150 "After bottom, scrolling should start over",
17151 );
17152
17153 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17154 assert_eq!(
17155 e.next_scroll_position,
17156 NextScrollCursorCenterTopBottom::Top,
17157 "Scrolling continues if retriggered fast enough"
17158 );
17159 });
17160
17161 cx.executor()
17162 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17163 cx.executor().run_until_parked();
17164 cx.update_editor(|e, _, _| {
17165 assert_eq!(
17166 e.next_scroll_position,
17167 NextScrollCursorCenterTopBottom::Center,
17168 "If scrolling is not triggered fast enough, it should reset"
17169 );
17170 });
17171}
17172
17173#[gpui::test]
17174async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17175 init_test(cx, |_| {});
17176 let mut cx = EditorLspTestContext::new_rust(
17177 lsp::ServerCapabilities {
17178 definition_provider: Some(lsp::OneOf::Left(true)),
17179 references_provider: Some(lsp::OneOf::Left(true)),
17180 ..lsp::ServerCapabilities::default()
17181 },
17182 cx,
17183 )
17184 .await;
17185
17186 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17187 let go_to_definition = cx
17188 .lsp
17189 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17190 move |params, _| async move {
17191 if empty_go_to_definition {
17192 Ok(None)
17193 } else {
17194 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17195 uri: params.text_document_position_params.text_document.uri,
17196 range: lsp::Range::new(
17197 lsp::Position::new(4, 3),
17198 lsp::Position::new(4, 6),
17199 ),
17200 })))
17201 }
17202 },
17203 );
17204 let references = cx
17205 .lsp
17206 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17207 Ok(Some(vec![lsp::Location {
17208 uri: params.text_document_position.text_document.uri,
17209 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17210 }]))
17211 });
17212 (go_to_definition, references)
17213 };
17214
17215 cx.set_state(
17216 &r#"fn one() {
17217 let mut a = ˇtwo();
17218 }
17219
17220 fn two() {}"#
17221 .unindent(),
17222 );
17223 set_up_lsp_handlers(false, &mut cx);
17224 let navigated = cx
17225 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17226 .await
17227 .expect("Failed to navigate to definition");
17228 assert_eq!(
17229 navigated,
17230 Navigated::Yes,
17231 "Should have navigated to definition from the GetDefinition response"
17232 );
17233 cx.assert_editor_state(
17234 &r#"fn one() {
17235 let mut a = two();
17236 }
17237
17238 fn «twoˇ»() {}"#
17239 .unindent(),
17240 );
17241
17242 let editors = cx.update_workspace(|workspace, _, cx| {
17243 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17244 });
17245 cx.update_editor(|_, _, test_editor_cx| {
17246 assert_eq!(
17247 editors.len(),
17248 1,
17249 "Initially, only one, test, editor should be open in the workspace"
17250 );
17251 assert_eq!(
17252 test_editor_cx.entity(),
17253 editors.last().expect("Asserted len is 1").clone()
17254 );
17255 });
17256
17257 set_up_lsp_handlers(true, &mut cx);
17258 let navigated = cx
17259 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17260 .await
17261 .expect("Failed to navigate to lookup references");
17262 assert_eq!(
17263 navigated,
17264 Navigated::Yes,
17265 "Should have navigated to references as a fallback after empty GoToDefinition response"
17266 );
17267 // We should not change the selections in the existing file,
17268 // if opening another milti buffer with the references
17269 cx.assert_editor_state(
17270 &r#"fn one() {
17271 let mut a = two();
17272 }
17273
17274 fn «twoˇ»() {}"#
17275 .unindent(),
17276 );
17277 let editors = cx.update_workspace(|workspace, _, cx| {
17278 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17279 });
17280 cx.update_editor(|_, _, test_editor_cx| {
17281 assert_eq!(
17282 editors.len(),
17283 2,
17284 "After falling back to references search, we open a new editor with the results"
17285 );
17286 let references_fallback_text = editors
17287 .into_iter()
17288 .find(|new_editor| *new_editor != test_editor_cx.entity())
17289 .expect("Should have one non-test editor now")
17290 .read(test_editor_cx)
17291 .text(test_editor_cx);
17292 assert_eq!(
17293 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17294 "Should use the range from the references response and not the GoToDefinition one"
17295 );
17296 });
17297}
17298
17299#[gpui::test]
17300async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17301 init_test(cx, |_| {});
17302 cx.update(|cx| {
17303 let mut editor_settings = EditorSettings::get_global(cx).clone();
17304 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17305 EditorSettings::override_global(editor_settings, cx);
17306 });
17307 let mut cx = EditorLspTestContext::new_rust(
17308 lsp::ServerCapabilities {
17309 definition_provider: Some(lsp::OneOf::Left(true)),
17310 references_provider: Some(lsp::OneOf::Left(true)),
17311 ..lsp::ServerCapabilities::default()
17312 },
17313 cx,
17314 )
17315 .await;
17316 let original_state = r#"fn one() {
17317 let mut a = ˇtwo();
17318 }
17319
17320 fn two() {}"#
17321 .unindent();
17322 cx.set_state(&original_state);
17323
17324 let mut go_to_definition = cx
17325 .lsp
17326 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17327 move |_, _| async move { Ok(None) },
17328 );
17329 let _references = cx
17330 .lsp
17331 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17332 panic!("Should not call for references with no go to definition fallback")
17333 });
17334
17335 let navigated = cx
17336 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17337 .await
17338 .expect("Failed to navigate to lookup references");
17339 go_to_definition
17340 .next()
17341 .await
17342 .expect("Should have called the go_to_definition handler");
17343
17344 assert_eq!(
17345 navigated,
17346 Navigated::No,
17347 "Should have navigated to references as a fallback after empty GoToDefinition response"
17348 );
17349 cx.assert_editor_state(&original_state);
17350 let editors = cx.update_workspace(|workspace, _, cx| {
17351 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17352 });
17353 cx.update_editor(|_, _, _| {
17354 assert_eq!(
17355 editors.len(),
17356 1,
17357 "After unsuccessful fallback, no other editor should have been opened"
17358 );
17359 });
17360}
17361
17362#[gpui::test]
17363async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17364 init_test(cx, |_| {});
17365
17366 let language = Arc::new(Language::new(
17367 LanguageConfig::default(),
17368 Some(tree_sitter_rust::LANGUAGE.into()),
17369 ));
17370
17371 let text = r#"
17372 #[cfg(test)]
17373 mod tests() {
17374 #[test]
17375 fn runnable_1() {
17376 let a = 1;
17377 }
17378
17379 #[test]
17380 fn runnable_2() {
17381 let a = 1;
17382 let b = 2;
17383 }
17384 }
17385 "#
17386 .unindent();
17387
17388 let fs = FakeFs::new(cx.executor());
17389 fs.insert_file("/file.rs", Default::default()).await;
17390
17391 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17392 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17393 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17394 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17395 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17396
17397 let editor = cx.new_window_entity(|window, cx| {
17398 Editor::new(
17399 EditorMode::full(),
17400 multi_buffer,
17401 Some(project.clone()),
17402 window,
17403 cx,
17404 )
17405 });
17406
17407 editor.update_in(cx, |editor, window, cx| {
17408 let snapshot = editor.buffer().read(cx).snapshot(cx);
17409 editor.tasks.insert(
17410 (buffer.read(cx).remote_id(), 3),
17411 RunnableTasks {
17412 templates: vec![],
17413 offset: snapshot.anchor_before(43),
17414 column: 0,
17415 extra_variables: HashMap::default(),
17416 context_range: BufferOffset(43)..BufferOffset(85),
17417 },
17418 );
17419 editor.tasks.insert(
17420 (buffer.read(cx).remote_id(), 8),
17421 RunnableTasks {
17422 templates: vec![],
17423 offset: snapshot.anchor_before(86),
17424 column: 0,
17425 extra_variables: HashMap::default(),
17426 context_range: BufferOffset(86)..BufferOffset(191),
17427 },
17428 );
17429
17430 // Test finding task when cursor is inside function body
17431 editor.change_selections(None, window, cx, |s| {
17432 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17433 });
17434 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17435 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17436
17437 // Test finding task when cursor is on function name
17438 editor.change_selections(None, window, cx, |s| {
17439 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17440 });
17441 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17442 assert_eq!(row, 8, "Should find task when cursor is on function name");
17443 });
17444}
17445
17446#[gpui::test]
17447async fn test_folding_buffers(cx: &mut TestAppContext) {
17448 init_test(cx, |_| {});
17449
17450 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17451 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17452 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17453
17454 let fs = FakeFs::new(cx.executor());
17455 fs.insert_tree(
17456 path!("/a"),
17457 json!({
17458 "first.rs": sample_text_1,
17459 "second.rs": sample_text_2,
17460 "third.rs": sample_text_3,
17461 }),
17462 )
17463 .await;
17464 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17465 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17466 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17467 let worktree = project.update(cx, |project, cx| {
17468 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17469 assert_eq!(worktrees.len(), 1);
17470 worktrees.pop().unwrap()
17471 });
17472 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17473
17474 let buffer_1 = project
17475 .update(cx, |project, cx| {
17476 project.open_buffer((worktree_id, "first.rs"), cx)
17477 })
17478 .await
17479 .unwrap();
17480 let buffer_2 = project
17481 .update(cx, |project, cx| {
17482 project.open_buffer((worktree_id, "second.rs"), cx)
17483 })
17484 .await
17485 .unwrap();
17486 let buffer_3 = project
17487 .update(cx, |project, cx| {
17488 project.open_buffer((worktree_id, "third.rs"), cx)
17489 })
17490 .await
17491 .unwrap();
17492
17493 let multi_buffer = cx.new(|cx| {
17494 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17495 multi_buffer.push_excerpts(
17496 buffer_1.clone(),
17497 [
17498 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17499 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17500 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17501 ],
17502 cx,
17503 );
17504 multi_buffer.push_excerpts(
17505 buffer_2.clone(),
17506 [
17507 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17508 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17509 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17510 ],
17511 cx,
17512 );
17513 multi_buffer.push_excerpts(
17514 buffer_3.clone(),
17515 [
17516 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17517 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17518 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17519 ],
17520 cx,
17521 );
17522 multi_buffer
17523 });
17524 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17525 Editor::new(
17526 EditorMode::full(),
17527 multi_buffer.clone(),
17528 Some(project.clone()),
17529 window,
17530 cx,
17531 )
17532 });
17533
17534 assert_eq!(
17535 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17536 "\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",
17537 );
17538
17539 multi_buffer_editor.update(cx, |editor, cx| {
17540 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17541 });
17542 assert_eq!(
17543 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17544 "\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",
17545 "After folding the first buffer, its text should not be displayed"
17546 );
17547
17548 multi_buffer_editor.update(cx, |editor, cx| {
17549 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17550 });
17551 assert_eq!(
17552 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17553 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17554 "After folding the second buffer, its text should not be displayed"
17555 );
17556
17557 multi_buffer_editor.update(cx, |editor, cx| {
17558 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17559 });
17560 assert_eq!(
17561 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17562 "\n\n\n\n\n",
17563 "After folding the third buffer, its text should not be displayed"
17564 );
17565
17566 // Emulate selection inside the fold logic, that should work
17567 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17568 editor
17569 .snapshot(window, cx)
17570 .next_line_boundary(Point::new(0, 4));
17571 });
17572
17573 multi_buffer_editor.update(cx, |editor, cx| {
17574 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17575 });
17576 assert_eq!(
17577 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17578 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17579 "After unfolding the second buffer, its text should be displayed"
17580 );
17581
17582 // Typing inside of buffer 1 causes that buffer to be unfolded.
17583 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17584 assert_eq!(
17585 multi_buffer
17586 .read(cx)
17587 .snapshot(cx)
17588 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17589 .collect::<String>(),
17590 "bbbb"
17591 );
17592 editor.change_selections(None, window, cx, |selections| {
17593 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17594 });
17595 editor.handle_input("B", window, cx);
17596 });
17597
17598 assert_eq!(
17599 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17600 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17601 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17602 );
17603
17604 multi_buffer_editor.update(cx, |editor, cx| {
17605 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17606 });
17607 assert_eq!(
17608 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17609 "\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",
17610 "After unfolding the all buffers, all original text should be displayed"
17611 );
17612}
17613
17614#[gpui::test]
17615async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17616 init_test(cx, |_| {});
17617
17618 let sample_text_1 = "1111\n2222\n3333".to_string();
17619 let sample_text_2 = "4444\n5555\n6666".to_string();
17620 let sample_text_3 = "7777\n8888\n9999".to_string();
17621
17622 let fs = FakeFs::new(cx.executor());
17623 fs.insert_tree(
17624 path!("/a"),
17625 json!({
17626 "first.rs": sample_text_1,
17627 "second.rs": sample_text_2,
17628 "third.rs": sample_text_3,
17629 }),
17630 )
17631 .await;
17632 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17633 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17634 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17635 let worktree = project.update(cx, |project, cx| {
17636 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17637 assert_eq!(worktrees.len(), 1);
17638 worktrees.pop().unwrap()
17639 });
17640 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17641
17642 let buffer_1 = project
17643 .update(cx, |project, cx| {
17644 project.open_buffer((worktree_id, "first.rs"), cx)
17645 })
17646 .await
17647 .unwrap();
17648 let buffer_2 = project
17649 .update(cx, |project, cx| {
17650 project.open_buffer((worktree_id, "second.rs"), cx)
17651 })
17652 .await
17653 .unwrap();
17654 let buffer_3 = project
17655 .update(cx, |project, cx| {
17656 project.open_buffer((worktree_id, "third.rs"), cx)
17657 })
17658 .await
17659 .unwrap();
17660
17661 let multi_buffer = cx.new(|cx| {
17662 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17663 multi_buffer.push_excerpts(
17664 buffer_1.clone(),
17665 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17666 cx,
17667 );
17668 multi_buffer.push_excerpts(
17669 buffer_2.clone(),
17670 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17671 cx,
17672 );
17673 multi_buffer.push_excerpts(
17674 buffer_3.clone(),
17675 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17676 cx,
17677 );
17678 multi_buffer
17679 });
17680
17681 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17682 Editor::new(
17683 EditorMode::full(),
17684 multi_buffer,
17685 Some(project.clone()),
17686 window,
17687 cx,
17688 )
17689 });
17690
17691 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17692 assert_eq!(
17693 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17694 full_text,
17695 );
17696
17697 multi_buffer_editor.update(cx, |editor, cx| {
17698 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17699 });
17700 assert_eq!(
17701 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17702 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17703 "After folding the first buffer, its text should not be displayed"
17704 );
17705
17706 multi_buffer_editor.update(cx, |editor, cx| {
17707 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17708 });
17709
17710 assert_eq!(
17711 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17712 "\n\n\n\n\n\n7777\n8888\n9999",
17713 "After folding the second buffer, its text should not be displayed"
17714 );
17715
17716 multi_buffer_editor.update(cx, |editor, cx| {
17717 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17718 });
17719 assert_eq!(
17720 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17721 "\n\n\n\n\n",
17722 "After folding the third buffer, its text should not be displayed"
17723 );
17724
17725 multi_buffer_editor.update(cx, |editor, cx| {
17726 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17727 });
17728 assert_eq!(
17729 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17730 "\n\n\n\n4444\n5555\n6666\n\n",
17731 "After unfolding the second buffer, its text should be displayed"
17732 );
17733
17734 multi_buffer_editor.update(cx, |editor, cx| {
17735 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17736 });
17737 assert_eq!(
17738 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17739 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17740 "After unfolding the first buffer, its text should be displayed"
17741 );
17742
17743 multi_buffer_editor.update(cx, |editor, cx| {
17744 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17745 });
17746 assert_eq!(
17747 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17748 full_text,
17749 "After unfolding all buffers, all original text should be displayed"
17750 );
17751}
17752
17753#[gpui::test]
17754async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17755 init_test(cx, |_| {});
17756
17757 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17758
17759 let fs = FakeFs::new(cx.executor());
17760 fs.insert_tree(
17761 path!("/a"),
17762 json!({
17763 "main.rs": sample_text,
17764 }),
17765 )
17766 .await;
17767 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17768 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17769 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17770 let worktree = project.update(cx, |project, cx| {
17771 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17772 assert_eq!(worktrees.len(), 1);
17773 worktrees.pop().unwrap()
17774 });
17775 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17776
17777 let buffer_1 = project
17778 .update(cx, |project, cx| {
17779 project.open_buffer((worktree_id, "main.rs"), cx)
17780 })
17781 .await
17782 .unwrap();
17783
17784 let multi_buffer = cx.new(|cx| {
17785 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17786 multi_buffer.push_excerpts(
17787 buffer_1.clone(),
17788 [ExcerptRange::new(
17789 Point::new(0, 0)
17790 ..Point::new(
17791 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17792 0,
17793 ),
17794 )],
17795 cx,
17796 );
17797 multi_buffer
17798 });
17799 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17800 Editor::new(
17801 EditorMode::full(),
17802 multi_buffer,
17803 Some(project.clone()),
17804 window,
17805 cx,
17806 )
17807 });
17808
17809 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17810 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17811 enum TestHighlight {}
17812 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17813 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17814 editor.highlight_text::<TestHighlight>(
17815 vec![highlight_range.clone()],
17816 HighlightStyle::color(Hsla::green()),
17817 cx,
17818 );
17819 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17820 });
17821
17822 let full_text = format!("\n\n{sample_text}");
17823 assert_eq!(
17824 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17825 full_text,
17826 );
17827}
17828
17829#[gpui::test]
17830async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17831 init_test(cx, |_| {});
17832 cx.update(|cx| {
17833 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17834 "keymaps/default-linux.json",
17835 cx,
17836 )
17837 .unwrap();
17838 cx.bind_keys(default_key_bindings);
17839 });
17840
17841 let (editor, cx) = cx.add_window_view(|window, cx| {
17842 let multi_buffer = MultiBuffer::build_multi(
17843 [
17844 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17845 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17846 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17847 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17848 ],
17849 cx,
17850 );
17851 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
17852
17853 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17854 // fold all but the second buffer, so that we test navigating between two
17855 // adjacent folded buffers, as well as folded buffers at the start and
17856 // end the multibuffer
17857 editor.fold_buffer(buffer_ids[0], cx);
17858 editor.fold_buffer(buffer_ids[2], cx);
17859 editor.fold_buffer(buffer_ids[3], cx);
17860
17861 editor
17862 });
17863 cx.simulate_resize(size(px(1000.), px(1000.)));
17864
17865 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17866 cx.assert_excerpts_with_selections(indoc! {"
17867 [EXCERPT]
17868 ˇ[FOLDED]
17869 [EXCERPT]
17870 a1
17871 b1
17872 [EXCERPT]
17873 [FOLDED]
17874 [EXCERPT]
17875 [FOLDED]
17876 "
17877 });
17878 cx.simulate_keystroke("down");
17879 cx.assert_excerpts_with_selections(indoc! {"
17880 [EXCERPT]
17881 [FOLDED]
17882 [EXCERPT]
17883 ˇa1
17884 b1
17885 [EXCERPT]
17886 [FOLDED]
17887 [EXCERPT]
17888 [FOLDED]
17889 "
17890 });
17891 cx.simulate_keystroke("down");
17892 cx.assert_excerpts_with_selections(indoc! {"
17893 [EXCERPT]
17894 [FOLDED]
17895 [EXCERPT]
17896 a1
17897 ˇb1
17898 [EXCERPT]
17899 [FOLDED]
17900 [EXCERPT]
17901 [FOLDED]
17902 "
17903 });
17904 cx.simulate_keystroke("down");
17905 cx.assert_excerpts_with_selections(indoc! {"
17906 [EXCERPT]
17907 [FOLDED]
17908 [EXCERPT]
17909 a1
17910 b1
17911 ˇ[EXCERPT]
17912 [FOLDED]
17913 [EXCERPT]
17914 [FOLDED]
17915 "
17916 });
17917 cx.simulate_keystroke("down");
17918 cx.assert_excerpts_with_selections(indoc! {"
17919 [EXCERPT]
17920 [FOLDED]
17921 [EXCERPT]
17922 a1
17923 b1
17924 [EXCERPT]
17925 ˇ[FOLDED]
17926 [EXCERPT]
17927 [FOLDED]
17928 "
17929 });
17930 for _ in 0..5 {
17931 cx.simulate_keystroke("down");
17932 cx.assert_excerpts_with_selections(indoc! {"
17933 [EXCERPT]
17934 [FOLDED]
17935 [EXCERPT]
17936 a1
17937 b1
17938 [EXCERPT]
17939 [FOLDED]
17940 [EXCERPT]
17941 ˇ[FOLDED]
17942 "
17943 });
17944 }
17945
17946 cx.simulate_keystroke("up");
17947 cx.assert_excerpts_with_selections(indoc! {"
17948 [EXCERPT]
17949 [FOLDED]
17950 [EXCERPT]
17951 a1
17952 b1
17953 [EXCERPT]
17954 ˇ[FOLDED]
17955 [EXCERPT]
17956 [FOLDED]
17957 "
17958 });
17959 cx.simulate_keystroke("up");
17960 cx.assert_excerpts_with_selections(indoc! {"
17961 [EXCERPT]
17962 [FOLDED]
17963 [EXCERPT]
17964 a1
17965 b1
17966 ˇ[EXCERPT]
17967 [FOLDED]
17968 [EXCERPT]
17969 [FOLDED]
17970 "
17971 });
17972 cx.simulate_keystroke("up");
17973 cx.assert_excerpts_with_selections(indoc! {"
17974 [EXCERPT]
17975 [FOLDED]
17976 [EXCERPT]
17977 a1
17978 ˇb1
17979 [EXCERPT]
17980 [FOLDED]
17981 [EXCERPT]
17982 [FOLDED]
17983 "
17984 });
17985 cx.simulate_keystroke("up");
17986 cx.assert_excerpts_with_selections(indoc! {"
17987 [EXCERPT]
17988 [FOLDED]
17989 [EXCERPT]
17990 ˇa1
17991 b1
17992 [EXCERPT]
17993 [FOLDED]
17994 [EXCERPT]
17995 [FOLDED]
17996 "
17997 });
17998 for _ in 0..5 {
17999 cx.simulate_keystroke("up");
18000 cx.assert_excerpts_with_selections(indoc! {"
18001 [EXCERPT]
18002 ˇ[FOLDED]
18003 [EXCERPT]
18004 a1
18005 b1
18006 [EXCERPT]
18007 [FOLDED]
18008 [EXCERPT]
18009 [FOLDED]
18010 "
18011 });
18012 }
18013}
18014
18015#[gpui::test]
18016async fn test_inline_completion_text(cx: &mut TestAppContext) {
18017 init_test(cx, |_| {});
18018
18019 // Simple insertion
18020 assert_highlighted_edits(
18021 "Hello, world!",
18022 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18023 true,
18024 cx,
18025 |highlighted_edits, cx| {
18026 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18027 assert_eq!(highlighted_edits.highlights.len(), 1);
18028 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18029 assert_eq!(
18030 highlighted_edits.highlights[0].1.background_color,
18031 Some(cx.theme().status().created_background)
18032 );
18033 },
18034 )
18035 .await;
18036
18037 // Replacement
18038 assert_highlighted_edits(
18039 "This is a test.",
18040 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18041 false,
18042 cx,
18043 |highlighted_edits, cx| {
18044 assert_eq!(highlighted_edits.text, "That is a test.");
18045 assert_eq!(highlighted_edits.highlights.len(), 1);
18046 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18047 assert_eq!(
18048 highlighted_edits.highlights[0].1.background_color,
18049 Some(cx.theme().status().created_background)
18050 );
18051 },
18052 )
18053 .await;
18054
18055 // Multiple edits
18056 assert_highlighted_edits(
18057 "Hello, world!",
18058 vec![
18059 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18060 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18061 ],
18062 false,
18063 cx,
18064 |highlighted_edits, cx| {
18065 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18066 assert_eq!(highlighted_edits.highlights.len(), 2);
18067 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18068 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18069 assert_eq!(
18070 highlighted_edits.highlights[0].1.background_color,
18071 Some(cx.theme().status().created_background)
18072 );
18073 assert_eq!(
18074 highlighted_edits.highlights[1].1.background_color,
18075 Some(cx.theme().status().created_background)
18076 );
18077 },
18078 )
18079 .await;
18080
18081 // Multiple lines with edits
18082 assert_highlighted_edits(
18083 "First line\nSecond line\nThird line\nFourth line",
18084 vec![
18085 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18086 (
18087 Point::new(2, 0)..Point::new(2, 10),
18088 "New third line".to_string(),
18089 ),
18090 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18091 ],
18092 false,
18093 cx,
18094 |highlighted_edits, cx| {
18095 assert_eq!(
18096 highlighted_edits.text,
18097 "Second modified\nNew third line\nFourth updated line"
18098 );
18099 assert_eq!(highlighted_edits.highlights.len(), 3);
18100 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18101 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18102 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18103 for highlight in &highlighted_edits.highlights {
18104 assert_eq!(
18105 highlight.1.background_color,
18106 Some(cx.theme().status().created_background)
18107 );
18108 }
18109 },
18110 )
18111 .await;
18112}
18113
18114#[gpui::test]
18115async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18116 init_test(cx, |_| {});
18117
18118 // Deletion
18119 assert_highlighted_edits(
18120 "Hello, world!",
18121 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18122 true,
18123 cx,
18124 |highlighted_edits, cx| {
18125 assert_eq!(highlighted_edits.text, "Hello, world!");
18126 assert_eq!(highlighted_edits.highlights.len(), 1);
18127 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18128 assert_eq!(
18129 highlighted_edits.highlights[0].1.background_color,
18130 Some(cx.theme().status().deleted_background)
18131 );
18132 },
18133 )
18134 .await;
18135
18136 // Insertion
18137 assert_highlighted_edits(
18138 "Hello, world!",
18139 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18140 true,
18141 cx,
18142 |highlighted_edits, cx| {
18143 assert_eq!(highlighted_edits.highlights.len(), 1);
18144 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18145 assert_eq!(
18146 highlighted_edits.highlights[0].1.background_color,
18147 Some(cx.theme().status().created_background)
18148 );
18149 },
18150 )
18151 .await;
18152}
18153
18154async fn assert_highlighted_edits(
18155 text: &str,
18156 edits: Vec<(Range<Point>, String)>,
18157 include_deletions: bool,
18158 cx: &mut TestAppContext,
18159 assertion_fn: impl Fn(HighlightedText, &App),
18160) {
18161 let window = cx.add_window(|window, cx| {
18162 let buffer = MultiBuffer::build_simple(text, cx);
18163 Editor::new(EditorMode::full(), buffer, None, window, cx)
18164 });
18165 let cx = &mut VisualTestContext::from_window(*window, cx);
18166
18167 let (buffer, snapshot) = window
18168 .update(cx, |editor, _window, cx| {
18169 (
18170 editor.buffer().clone(),
18171 editor.buffer().read(cx).snapshot(cx),
18172 )
18173 })
18174 .unwrap();
18175
18176 let edits = edits
18177 .into_iter()
18178 .map(|(range, edit)| {
18179 (
18180 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18181 edit,
18182 )
18183 })
18184 .collect::<Vec<_>>();
18185
18186 let text_anchor_edits = edits
18187 .clone()
18188 .into_iter()
18189 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18190 .collect::<Vec<_>>();
18191
18192 let edit_preview = window
18193 .update(cx, |_, _window, cx| {
18194 buffer
18195 .read(cx)
18196 .as_singleton()
18197 .unwrap()
18198 .read(cx)
18199 .preview_edits(text_anchor_edits.into(), cx)
18200 })
18201 .unwrap()
18202 .await;
18203
18204 cx.update(|_window, cx| {
18205 let highlighted_edits = inline_completion_edit_text(
18206 &snapshot.as_singleton().unwrap().2,
18207 &edits,
18208 &edit_preview,
18209 include_deletions,
18210 cx,
18211 );
18212 assertion_fn(highlighted_edits, cx)
18213 });
18214}
18215
18216#[track_caller]
18217fn assert_breakpoint(
18218 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18219 path: &Arc<Path>,
18220 expected: Vec<(u32, Breakpoint)>,
18221) {
18222 if expected.len() == 0usize {
18223 assert!(!breakpoints.contains_key(path), "{}", path.display());
18224 } else {
18225 let mut breakpoint = breakpoints
18226 .get(path)
18227 .unwrap()
18228 .into_iter()
18229 .map(|breakpoint| {
18230 (
18231 breakpoint.row,
18232 Breakpoint {
18233 message: breakpoint.message.clone(),
18234 state: breakpoint.state,
18235 condition: breakpoint.condition.clone(),
18236 hit_condition: breakpoint.hit_condition.clone(),
18237 },
18238 )
18239 })
18240 .collect::<Vec<_>>();
18241
18242 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18243
18244 assert_eq!(expected, breakpoint);
18245 }
18246}
18247
18248fn add_log_breakpoint_at_cursor(
18249 editor: &mut Editor,
18250 log_message: &str,
18251 window: &mut Window,
18252 cx: &mut Context<Editor>,
18253) {
18254 let (anchor, bp) = editor
18255 .breakpoints_at_cursors(window, cx)
18256 .first()
18257 .and_then(|(anchor, bp)| {
18258 if let Some(bp) = bp {
18259 Some((*anchor, bp.clone()))
18260 } else {
18261 None
18262 }
18263 })
18264 .unwrap_or_else(|| {
18265 let cursor_position: Point = editor.selections.newest(cx).head();
18266
18267 let breakpoint_position = editor
18268 .snapshot(window, cx)
18269 .display_snapshot
18270 .buffer_snapshot
18271 .anchor_before(Point::new(cursor_position.row, 0));
18272
18273 (breakpoint_position, Breakpoint::new_log(&log_message))
18274 });
18275
18276 editor.edit_breakpoint_at_anchor(
18277 anchor,
18278 bp,
18279 BreakpointEditAction::EditLogMessage(log_message.into()),
18280 cx,
18281 );
18282}
18283
18284#[gpui::test]
18285async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18286 init_test(cx, |_| {});
18287
18288 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18289 let fs = FakeFs::new(cx.executor());
18290 fs.insert_tree(
18291 path!("/a"),
18292 json!({
18293 "main.rs": sample_text,
18294 }),
18295 )
18296 .await;
18297 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18298 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18299 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18300
18301 let fs = FakeFs::new(cx.executor());
18302 fs.insert_tree(
18303 path!("/a"),
18304 json!({
18305 "main.rs": sample_text,
18306 }),
18307 )
18308 .await;
18309 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18310 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18311 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18312 let worktree_id = workspace
18313 .update(cx, |workspace, _window, cx| {
18314 workspace.project().update(cx, |project, cx| {
18315 project.worktrees(cx).next().unwrap().read(cx).id()
18316 })
18317 })
18318 .unwrap();
18319
18320 let buffer = project
18321 .update(cx, |project, cx| {
18322 project.open_buffer((worktree_id, "main.rs"), cx)
18323 })
18324 .await
18325 .unwrap();
18326
18327 let (editor, cx) = cx.add_window_view(|window, cx| {
18328 Editor::new(
18329 EditorMode::full(),
18330 MultiBuffer::build_from_buffer(buffer, cx),
18331 Some(project.clone()),
18332 window,
18333 cx,
18334 )
18335 });
18336
18337 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18338 let abs_path = project.read_with(cx, |project, cx| {
18339 project
18340 .absolute_path(&project_path, cx)
18341 .map(|path_buf| Arc::from(path_buf.to_owned()))
18342 .unwrap()
18343 });
18344
18345 // assert we can add breakpoint on the first line
18346 editor.update_in(cx, |editor, window, cx| {
18347 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18348 editor.move_to_end(&MoveToEnd, window, cx);
18349 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18350 });
18351
18352 let breakpoints = editor.update(cx, |editor, cx| {
18353 editor
18354 .breakpoint_store()
18355 .as_ref()
18356 .unwrap()
18357 .read(cx)
18358 .all_breakpoints(cx)
18359 .clone()
18360 });
18361
18362 assert_eq!(1, breakpoints.len());
18363 assert_breakpoint(
18364 &breakpoints,
18365 &abs_path,
18366 vec![
18367 (0, Breakpoint::new_standard()),
18368 (3, Breakpoint::new_standard()),
18369 ],
18370 );
18371
18372 editor.update_in(cx, |editor, window, cx| {
18373 editor.move_to_beginning(&MoveToBeginning, window, cx);
18374 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18375 });
18376
18377 let breakpoints = editor.update(cx, |editor, cx| {
18378 editor
18379 .breakpoint_store()
18380 .as_ref()
18381 .unwrap()
18382 .read(cx)
18383 .all_breakpoints(cx)
18384 .clone()
18385 });
18386
18387 assert_eq!(1, breakpoints.len());
18388 assert_breakpoint(
18389 &breakpoints,
18390 &abs_path,
18391 vec![(3, Breakpoint::new_standard())],
18392 );
18393
18394 editor.update_in(cx, |editor, window, cx| {
18395 editor.move_to_end(&MoveToEnd, window, cx);
18396 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18397 });
18398
18399 let breakpoints = editor.update(cx, |editor, cx| {
18400 editor
18401 .breakpoint_store()
18402 .as_ref()
18403 .unwrap()
18404 .read(cx)
18405 .all_breakpoints(cx)
18406 .clone()
18407 });
18408
18409 assert_eq!(0, breakpoints.len());
18410 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18411}
18412
18413#[gpui::test]
18414async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18415 init_test(cx, |_| {});
18416
18417 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18418
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) =
18429 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18430
18431 let worktree_id = workspace.update(cx, |workspace, cx| {
18432 workspace.project().update(cx, |project, cx| {
18433 project.worktrees(cx).next().unwrap().read(cx).id()
18434 })
18435 });
18436
18437 let buffer = project
18438 .update(cx, |project, cx| {
18439 project.open_buffer((worktree_id, "main.rs"), cx)
18440 })
18441 .await
18442 .unwrap();
18443
18444 let (editor, cx) = cx.add_window_view(|window, cx| {
18445 Editor::new(
18446 EditorMode::full(),
18447 MultiBuffer::build_from_buffer(buffer, cx),
18448 Some(project.clone()),
18449 window,
18450 cx,
18451 )
18452 });
18453
18454 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18455 let abs_path = project.read_with(cx, |project, cx| {
18456 project
18457 .absolute_path(&project_path, cx)
18458 .map(|path_buf| Arc::from(path_buf.to_owned()))
18459 .unwrap()
18460 });
18461
18462 editor.update_in(cx, |editor, window, cx| {
18463 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18464 });
18465
18466 let breakpoints = editor.update(cx, |editor, cx| {
18467 editor
18468 .breakpoint_store()
18469 .as_ref()
18470 .unwrap()
18471 .read(cx)
18472 .all_breakpoints(cx)
18473 .clone()
18474 });
18475
18476 assert_breakpoint(
18477 &breakpoints,
18478 &abs_path,
18479 vec![(0, Breakpoint::new_log("hello world"))],
18480 );
18481
18482 // Removing a log message from a log breakpoint should remove it
18483 editor.update_in(cx, |editor, window, cx| {
18484 add_log_breakpoint_at_cursor(editor, "", window, cx);
18485 });
18486
18487 let breakpoints = editor.update(cx, |editor, cx| {
18488 editor
18489 .breakpoint_store()
18490 .as_ref()
18491 .unwrap()
18492 .read(cx)
18493 .all_breakpoints(cx)
18494 .clone()
18495 });
18496
18497 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18498
18499 editor.update_in(cx, |editor, window, cx| {
18500 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18501 editor.move_to_end(&MoveToEnd, window, cx);
18502 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18503 // Not adding a log message to a standard breakpoint shouldn't remove it
18504 add_log_breakpoint_at_cursor(editor, "", window, cx);
18505 });
18506
18507 let breakpoints = editor.update(cx, |editor, cx| {
18508 editor
18509 .breakpoint_store()
18510 .as_ref()
18511 .unwrap()
18512 .read(cx)
18513 .all_breakpoints(cx)
18514 .clone()
18515 });
18516
18517 assert_breakpoint(
18518 &breakpoints,
18519 &abs_path,
18520 vec![
18521 (0, Breakpoint::new_standard()),
18522 (3, Breakpoint::new_standard()),
18523 ],
18524 );
18525
18526 editor.update_in(cx, |editor, window, cx| {
18527 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18528 });
18529
18530 let breakpoints = editor.update(cx, |editor, cx| {
18531 editor
18532 .breakpoint_store()
18533 .as_ref()
18534 .unwrap()
18535 .read(cx)
18536 .all_breakpoints(cx)
18537 .clone()
18538 });
18539
18540 assert_breakpoint(
18541 &breakpoints,
18542 &abs_path,
18543 vec![
18544 (0, Breakpoint::new_standard()),
18545 (3, Breakpoint::new_log("hello world")),
18546 ],
18547 );
18548
18549 editor.update_in(cx, |editor, window, cx| {
18550 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18551 });
18552
18553 let breakpoints = editor.update(cx, |editor, cx| {
18554 editor
18555 .breakpoint_store()
18556 .as_ref()
18557 .unwrap()
18558 .read(cx)
18559 .all_breakpoints(cx)
18560 .clone()
18561 });
18562
18563 assert_breakpoint(
18564 &breakpoints,
18565 &abs_path,
18566 vec![
18567 (0, Breakpoint::new_standard()),
18568 (3, Breakpoint::new_log("hello Earth!!")),
18569 ],
18570 );
18571}
18572
18573/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18574/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18575/// or when breakpoints were placed out of order. This tests for a regression too
18576#[gpui::test]
18577async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18578 init_test(cx, |_| {});
18579
18580 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18581 let fs = FakeFs::new(cx.executor());
18582 fs.insert_tree(
18583 path!("/a"),
18584 json!({
18585 "main.rs": sample_text,
18586 }),
18587 )
18588 .await;
18589 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18590 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18591 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18592
18593 let fs = FakeFs::new(cx.executor());
18594 fs.insert_tree(
18595 path!("/a"),
18596 json!({
18597 "main.rs": sample_text,
18598 }),
18599 )
18600 .await;
18601 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18602 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18603 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18604 let worktree_id = workspace
18605 .update(cx, |workspace, _window, cx| {
18606 workspace.project().update(cx, |project, cx| {
18607 project.worktrees(cx).next().unwrap().read(cx).id()
18608 })
18609 })
18610 .unwrap();
18611
18612 let buffer = project
18613 .update(cx, |project, cx| {
18614 project.open_buffer((worktree_id, "main.rs"), cx)
18615 })
18616 .await
18617 .unwrap();
18618
18619 let (editor, cx) = cx.add_window_view(|window, cx| {
18620 Editor::new(
18621 EditorMode::full(),
18622 MultiBuffer::build_from_buffer(buffer, cx),
18623 Some(project.clone()),
18624 window,
18625 cx,
18626 )
18627 });
18628
18629 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18630 let abs_path = project.read_with(cx, |project, cx| {
18631 project
18632 .absolute_path(&project_path, cx)
18633 .map(|path_buf| Arc::from(path_buf.to_owned()))
18634 .unwrap()
18635 });
18636
18637 // assert we can add breakpoint on the first line
18638 editor.update_in(cx, |editor, window, cx| {
18639 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18640 editor.move_to_end(&MoveToEnd, window, cx);
18641 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18642 editor.move_up(&MoveUp, window, cx);
18643 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18644 });
18645
18646 let breakpoints = editor.update(cx, |editor, cx| {
18647 editor
18648 .breakpoint_store()
18649 .as_ref()
18650 .unwrap()
18651 .read(cx)
18652 .all_breakpoints(cx)
18653 .clone()
18654 });
18655
18656 assert_eq!(1, breakpoints.len());
18657 assert_breakpoint(
18658 &breakpoints,
18659 &abs_path,
18660 vec![
18661 (0, Breakpoint::new_standard()),
18662 (2, Breakpoint::new_standard()),
18663 (3, Breakpoint::new_standard()),
18664 ],
18665 );
18666
18667 editor.update_in(cx, |editor, window, cx| {
18668 editor.move_to_beginning(&MoveToBeginning, window, cx);
18669 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18670 editor.move_to_end(&MoveToEnd, window, cx);
18671 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18672 // Disabling a breakpoint that doesn't exist should do nothing
18673 editor.move_up(&MoveUp, window, cx);
18674 editor.move_up(&MoveUp, window, cx);
18675 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18676 });
18677
18678 let breakpoints = editor.update(cx, |editor, cx| {
18679 editor
18680 .breakpoint_store()
18681 .as_ref()
18682 .unwrap()
18683 .read(cx)
18684 .all_breakpoints(cx)
18685 .clone()
18686 });
18687
18688 let disable_breakpoint = {
18689 let mut bp = Breakpoint::new_standard();
18690 bp.state = BreakpointState::Disabled;
18691 bp
18692 };
18693
18694 assert_eq!(1, breakpoints.len());
18695 assert_breakpoint(
18696 &breakpoints,
18697 &abs_path,
18698 vec![
18699 (0, disable_breakpoint.clone()),
18700 (2, Breakpoint::new_standard()),
18701 (3, disable_breakpoint.clone()),
18702 ],
18703 );
18704
18705 editor.update_in(cx, |editor, window, cx| {
18706 editor.move_to_beginning(&MoveToBeginning, window, cx);
18707 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18708 editor.move_to_end(&MoveToEnd, window, cx);
18709 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18710 editor.move_up(&MoveUp, window, cx);
18711 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18712 });
18713
18714 let breakpoints = editor.update(cx, |editor, cx| {
18715 editor
18716 .breakpoint_store()
18717 .as_ref()
18718 .unwrap()
18719 .read(cx)
18720 .all_breakpoints(cx)
18721 .clone()
18722 });
18723
18724 assert_eq!(1, breakpoints.len());
18725 assert_breakpoint(
18726 &breakpoints,
18727 &abs_path,
18728 vec![
18729 (0, Breakpoint::new_standard()),
18730 (2, disable_breakpoint),
18731 (3, Breakpoint::new_standard()),
18732 ],
18733 );
18734}
18735
18736#[gpui::test]
18737async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18738 init_test(cx, |_| {});
18739 let capabilities = lsp::ServerCapabilities {
18740 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18741 prepare_provider: Some(true),
18742 work_done_progress_options: Default::default(),
18743 })),
18744 ..Default::default()
18745 };
18746 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18747
18748 cx.set_state(indoc! {"
18749 struct Fˇoo {}
18750 "});
18751
18752 cx.update_editor(|editor, _, cx| {
18753 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18754 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18755 editor.highlight_background::<DocumentHighlightRead>(
18756 &[highlight_range],
18757 |c| c.editor_document_highlight_read_background,
18758 cx,
18759 );
18760 });
18761
18762 let mut prepare_rename_handler = cx
18763 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18764 move |_, _, _| async move {
18765 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18766 start: lsp::Position {
18767 line: 0,
18768 character: 7,
18769 },
18770 end: lsp::Position {
18771 line: 0,
18772 character: 10,
18773 },
18774 })))
18775 },
18776 );
18777 let prepare_rename_task = cx
18778 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18779 .expect("Prepare rename was not started");
18780 prepare_rename_handler.next().await.unwrap();
18781 prepare_rename_task.await.expect("Prepare rename failed");
18782
18783 let mut rename_handler =
18784 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18785 let edit = lsp::TextEdit {
18786 range: lsp::Range {
18787 start: lsp::Position {
18788 line: 0,
18789 character: 7,
18790 },
18791 end: lsp::Position {
18792 line: 0,
18793 character: 10,
18794 },
18795 },
18796 new_text: "FooRenamed".to_string(),
18797 };
18798 Ok(Some(lsp::WorkspaceEdit::new(
18799 // Specify the same edit twice
18800 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18801 )))
18802 });
18803 let rename_task = cx
18804 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18805 .expect("Confirm rename was not started");
18806 rename_handler.next().await.unwrap();
18807 rename_task.await.expect("Confirm rename failed");
18808 cx.run_until_parked();
18809
18810 // Despite two edits, only one is actually applied as those are identical
18811 cx.assert_editor_state(indoc! {"
18812 struct FooRenamedˇ {}
18813 "});
18814}
18815
18816#[gpui::test]
18817async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18818 init_test(cx, |_| {});
18819 // These capabilities indicate that the server does not support prepare rename.
18820 let capabilities = lsp::ServerCapabilities {
18821 rename_provider: Some(lsp::OneOf::Left(true)),
18822 ..Default::default()
18823 };
18824 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18825
18826 cx.set_state(indoc! {"
18827 struct Fˇoo {}
18828 "});
18829
18830 cx.update_editor(|editor, _window, cx| {
18831 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18832 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18833 editor.highlight_background::<DocumentHighlightRead>(
18834 &[highlight_range],
18835 |c| c.editor_document_highlight_read_background,
18836 cx,
18837 );
18838 });
18839
18840 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18841 .expect("Prepare rename was not started")
18842 .await
18843 .expect("Prepare rename failed");
18844
18845 let mut rename_handler =
18846 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18847 let edit = lsp::TextEdit {
18848 range: lsp::Range {
18849 start: lsp::Position {
18850 line: 0,
18851 character: 7,
18852 },
18853 end: lsp::Position {
18854 line: 0,
18855 character: 10,
18856 },
18857 },
18858 new_text: "FooRenamed".to_string(),
18859 };
18860 Ok(Some(lsp::WorkspaceEdit::new(
18861 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18862 )))
18863 });
18864 let rename_task = cx
18865 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18866 .expect("Confirm rename was not started");
18867 rename_handler.next().await.unwrap();
18868 rename_task.await.expect("Confirm rename failed");
18869 cx.run_until_parked();
18870
18871 // Correct range is renamed, as `surrounding_word` is used to find it.
18872 cx.assert_editor_state(indoc! {"
18873 struct FooRenamedˇ {}
18874 "});
18875}
18876
18877#[gpui::test]
18878async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18879 init_test(cx, |_| {});
18880 let mut cx = EditorTestContext::new(cx).await;
18881
18882 let language = Arc::new(
18883 Language::new(
18884 LanguageConfig::default(),
18885 Some(tree_sitter_html::LANGUAGE.into()),
18886 )
18887 .with_brackets_query(
18888 r#"
18889 ("<" @open "/>" @close)
18890 ("</" @open ">" @close)
18891 ("<" @open ">" @close)
18892 ("\"" @open "\"" @close)
18893 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18894 "#,
18895 )
18896 .unwrap(),
18897 );
18898 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18899
18900 cx.set_state(indoc! {"
18901 <span>ˇ</span>
18902 "});
18903 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18904 cx.assert_editor_state(indoc! {"
18905 <span>
18906 ˇ
18907 </span>
18908 "});
18909
18910 cx.set_state(indoc! {"
18911 <span><span></span>ˇ</span>
18912 "});
18913 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18914 cx.assert_editor_state(indoc! {"
18915 <span><span></span>
18916 ˇ</span>
18917 "});
18918
18919 cx.set_state(indoc! {"
18920 <span>ˇ
18921 </span>
18922 "});
18923 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18924 cx.assert_editor_state(indoc! {"
18925 <span>
18926 ˇ
18927 </span>
18928 "});
18929}
18930
18931#[gpui::test(iterations = 10)]
18932async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18933 init_test(cx, |_| {});
18934
18935 let fs = FakeFs::new(cx.executor());
18936 fs.insert_tree(
18937 path!("/dir"),
18938 json!({
18939 "a.ts": "a",
18940 }),
18941 )
18942 .await;
18943
18944 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18945 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18946 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18947
18948 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18949 language_registry.add(Arc::new(Language::new(
18950 LanguageConfig {
18951 name: "TypeScript".into(),
18952 matcher: LanguageMatcher {
18953 path_suffixes: vec!["ts".to_string()],
18954 ..Default::default()
18955 },
18956 ..Default::default()
18957 },
18958 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18959 )));
18960 let mut fake_language_servers = language_registry.register_fake_lsp(
18961 "TypeScript",
18962 FakeLspAdapter {
18963 capabilities: lsp::ServerCapabilities {
18964 code_lens_provider: Some(lsp::CodeLensOptions {
18965 resolve_provider: Some(true),
18966 }),
18967 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18968 commands: vec!["_the/command".to_string()],
18969 ..lsp::ExecuteCommandOptions::default()
18970 }),
18971 ..lsp::ServerCapabilities::default()
18972 },
18973 ..FakeLspAdapter::default()
18974 },
18975 );
18976
18977 let (buffer, _handle) = project
18978 .update(cx, |p, cx| {
18979 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18980 })
18981 .await
18982 .unwrap();
18983 cx.executor().run_until_parked();
18984
18985 let fake_server = fake_language_servers.next().await.unwrap();
18986
18987 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18988 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18989 drop(buffer_snapshot);
18990 let actions = cx
18991 .update_window(*workspace, |_, window, cx| {
18992 project.code_actions(&buffer, anchor..anchor, window, cx)
18993 })
18994 .unwrap();
18995
18996 fake_server
18997 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18998 Ok(Some(vec![
18999 lsp::CodeLens {
19000 range: lsp::Range::default(),
19001 command: Some(lsp::Command {
19002 title: "Code lens command".to_owned(),
19003 command: "_the/command".to_owned(),
19004 arguments: None,
19005 }),
19006 data: None,
19007 },
19008 lsp::CodeLens {
19009 range: lsp::Range::default(),
19010 command: Some(lsp::Command {
19011 title: "Command not in capabilities".to_owned(),
19012 command: "not in capabilities".to_owned(),
19013 arguments: None,
19014 }),
19015 data: None,
19016 },
19017 lsp::CodeLens {
19018 range: lsp::Range {
19019 start: lsp::Position {
19020 line: 1,
19021 character: 1,
19022 },
19023 end: lsp::Position {
19024 line: 1,
19025 character: 1,
19026 },
19027 },
19028 command: Some(lsp::Command {
19029 title: "Command not in range".to_owned(),
19030 command: "_the/command".to_owned(),
19031 arguments: None,
19032 }),
19033 data: None,
19034 },
19035 ]))
19036 })
19037 .next()
19038 .await;
19039
19040 let actions = actions.await.unwrap();
19041 assert_eq!(
19042 actions.len(),
19043 1,
19044 "Should have only one valid action for the 0..0 range"
19045 );
19046 let action = actions[0].clone();
19047 let apply = project.update(cx, |project, cx| {
19048 project.apply_code_action(buffer.clone(), action, true, cx)
19049 });
19050
19051 // Resolving the code action does not populate its edits. In absence of
19052 // edits, we must execute the given command.
19053 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19054 |mut lens, _| async move {
19055 let lens_command = lens.command.as_mut().expect("should have a command");
19056 assert_eq!(lens_command.title, "Code lens command");
19057 lens_command.arguments = Some(vec![json!("the-argument")]);
19058 Ok(lens)
19059 },
19060 );
19061
19062 // While executing the command, the language server sends the editor
19063 // a `workspaceEdit` request.
19064 fake_server
19065 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19066 let fake = fake_server.clone();
19067 move |params, _| {
19068 assert_eq!(params.command, "_the/command");
19069 let fake = fake.clone();
19070 async move {
19071 fake.server
19072 .request::<lsp::request::ApplyWorkspaceEdit>(
19073 lsp::ApplyWorkspaceEditParams {
19074 label: None,
19075 edit: lsp::WorkspaceEdit {
19076 changes: Some(
19077 [(
19078 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19079 vec![lsp::TextEdit {
19080 range: lsp::Range::new(
19081 lsp::Position::new(0, 0),
19082 lsp::Position::new(0, 0),
19083 ),
19084 new_text: "X".into(),
19085 }],
19086 )]
19087 .into_iter()
19088 .collect(),
19089 ),
19090 ..Default::default()
19091 },
19092 },
19093 )
19094 .await
19095 .unwrap();
19096 Ok(Some(json!(null)))
19097 }
19098 }
19099 })
19100 .next()
19101 .await;
19102
19103 // Applying the code lens command returns a project transaction containing the edits
19104 // sent by the language server in its `workspaceEdit` request.
19105 let transaction = apply.await.unwrap();
19106 assert!(transaction.0.contains_key(&buffer));
19107 buffer.update(cx, |buffer, cx| {
19108 assert_eq!(buffer.text(), "Xa");
19109 buffer.undo(cx);
19110 assert_eq!(buffer.text(), "a");
19111 });
19112}
19113
19114#[gpui::test]
19115async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19116 init_test(cx, |_| {});
19117
19118 let fs = FakeFs::new(cx.executor());
19119 let main_text = r#"fn main() {
19120println!("1");
19121println!("2");
19122println!("3");
19123println!("4");
19124println!("5");
19125}"#;
19126 let lib_text = "mod foo {}";
19127 fs.insert_tree(
19128 path!("/a"),
19129 json!({
19130 "lib.rs": lib_text,
19131 "main.rs": main_text,
19132 }),
19133 )
19134 .await;
19135
19136 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19137 let (workspace, cx) =
19138 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19139 let worktree_id = workspace.update(cx, |workspace, cx| {
19140 workspace.project().update(cx, |project, cx| {
19141 project.worktrees(cx).next().unwrap().read(cx).id()
19142 })
19143 });
19144
19145 let expected_ranges = vec![
19146 Point::new(0, 0)..Point::new(0, 0),
19147 Point::new(1, 0)..Point::new(1, 1),
19148 Point::new(2, 0)..Point::new(2, 2),
19149 Point::new(3, 0)..Point::new(3, 3),
19150 ];
19151
19152 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19153 let editor_1 = workspace
19154 .update_in(cx, |workspace, window, cx| {
19155 workspace.open_path(
19156 (worktree_id, "main.rs"),
19157 Some(pane_1.downgrade()),
19158 true,
19159 window,
19160 cx,
19161 )
19162 })
19163 .unwrap()
19164 .await
19165 .downcast::<Editor>()
19166 .unwrap();
19167 pane_1.update(cx, |pane, cx| {
19168 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19169 open_editor.update(cx, |editor, cx| {
19170 assert_eq!(
19171 editor.display_text(cx),
19172 main_text,
19173 "Original main.rs text on initial open",
19174 );
19175 assert_eq!(
19176 editor
19177 .selections
19178 .all::<Point>(cx)
19179 .into_iter()
19180 .map(|s| s.range())
19181 .collect::<Vec<_>>(),
19182 vec![Point::zero()..Point::zero()],
19183 "Default selections on initial open",
19184 );
19185 })
19186 });
19187 editor_1.update_in(cx, |editor, window, cx| {
19188 editor.change_selections(None, window, cx, |s| {
19189 s.select_ranges(expected_ranges.clone());
19190 });
19191 });
19192
19193 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19194 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19195 });
19196 let editor_2 = workspace
19197 .update_in(cx, |workspace, window, cx| {
19198 workspace.open_path(
19199 (worktree_id, "main.rs"),
19200 Some(pane_2.downgrade()),
19201 true,
19202 window,
19203 cx,
19204 )
19205 })
19206 .unwrap()
19207 .await
19208 .downcast::<Editor>()
19209 .unwrap();
19210 pane_2.update(cx, |pane, cx| {
19211 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19212 open_editor.update(cx, |editor, cx| {
19213 assert_eq!(
19214 editor.display_text(cx),
19215 main_text,
19216 "Original main.rs text on initial open in another panel",
19217 );
19218 assert_eq!(
19219 editor
19220 .selections
19221 .all::<Point>(cx)
19222 .into_iter()
19223 .map(|s| s.range())
19224 .collect::<Vec<_>>(),
19225 vec![Point::zero()..Point::zero()],
19226 "Default selections on initial open in another panel",
19227 );
19228 })
19229 });
19230
19231 editor_2.update_in(cx, |editor, window, cx| {
19232 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19233 });
19234
19235 let _other_editor_1 = workspace
19236 .update_in(cx, |workspace, window, cx| {
19237 workspace.open_path(
19238 (worktree_id, "lib.rs"),
19239 Some(pane_1.downgrade()),
19240 true,
19241 window,
19242 cx,
19243 )
19244 })
19245 .unwrap()
19246 .await
19247 .downcast::<Editor>()
19248 .unwrap();
19249 pane_1
19250 .update_in(cx, |pane, window, cx| {
19251 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19252 .unwrap()
19253 })
19254 .await
19255 .unwrap();
19256 drop(editor_1);
19257 pane_1.update(cx, |pane, cx| {
19258 pane.active_item()
19259 .unwrap()
19260 .downcast::<Editor>()
19261 .unwrap()
19262 .update(cx, |editor, cx| {
19263 assert_eq!(
19264 editor.display_text(cx),
19265 lib_text,
19266 "Other file should be open and active",
19267 );
19268 });
19269 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19270 });
19271
19272 let _other_editor_2 = workspace
19273 .update_in(cx, |workspace, window, cx| {
19274 workspace.open_path(
19275 (worktree_id, "lib.rs"),
19276 Some(pane_2.downgrade()),
19277 true,
19278 window,
19279 cx,
19280 )
19281 })
19282 .unwrap()
19283 .await
19284 .downcast::<Editor>()
19285 .unwrap();
19286 pane_2
19287 .update_in(cx, |pane, window, cx| {
19288 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19289 .unwrap()
19290 })
19291 .await
19292 .unwrap();
19293 drop(editor_2);
19294 pane_2.update(cx, |pane, cx| {
19295 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19296 open_editor.update(cx, |editor, cx| {
19297 assert_eq!(
19298 editor.display_text(cx),
19299 lib_text,
19300 "Other file should be open and active in another panel too",
19301 );
19302 });
19303 assert_eq!(
19304 pane.items().count(),
19305 1,
19306 "No other editors should be open in another pane",
19307 );
19308 });
19309
19310 let _editor_1_reopened = workspace
19311 .update_in(cx, |workspace, window, cx| {
19312 workspace.open_path(
19313 (worktree_id, "main.rs"),
19314 Some(pane_1.downgrade()),
19315 true,
19316 window,
19317 cx,
19318 )
19319 })
19320 .unwrap()
19321 .await
19322 .downcast::<Editor>()
19323 .unwrap();
19324 let _editor_2_reopened = workspace
19325 .update_in(cx, |workspace, window, cx| {
19326 workspace.open_path(
19327 (worktree_id, "main.rs"),
19328 Some(pane_2.downgrade()),
19329 true,
19330 window,
19331 cx,
19332 )
19333 })
19334 .unwrap()
19335 .await
19336 .downcast::<Editor>()
19337 .unwrap();
19338 pane_1.update(cx, |pane, cx| {
19339 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19340 open_editor.update(cx, |editor, cx| {
19341 assert_eq!(
19342 editor.display_text(cx),
19343 main_text,
19344 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19345 );
19346 assert_eq!(
19347 editor
19348 .selections
19349 .all::<Point>(cx)
19350 .into_iter()
19351 .map(|s| s.range())
19352 .collect::<Vec<_>>(),
19353 expected_ranges,
19354 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19355 );
19356 })
19357 });
19358 pane_2.update(cx, |pane, cx| {
19359 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19360 open_editor.update(cx, |editor, cx| {
19361 assert_eq!(
19362 editor.display_text(cx),
19363 r#"fn main() {
19364⋯rintln!("1");
19365⋯intln!("2");
19366⋯ntln!("3");
19367println!("4");
19368println!("5");
19369}"#,
19370 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19371 );
19372 assert_eq!(
19373 editor
19374 .selections
19375 .all::<Point>(cx)
19376 .into_iter()
19377 .map(|s| s.range())
19378 .collect::<Vec<_>>(),
19379 vec![Point::zero()..Point::zero()],
19380 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19381 );
19382 })
19383 });
19384}
19385
19386#[gpui::test]
19387async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19388 init_test(cx, |_| {});
19389
19390 let fs = FakeFs::new(cx.executor());
19391 let main_text = r#"fn main() {
19392println!("1");
19393println!("2");
19394println!("3");
19395println!("4");
19396println!("5");
19397}"#;
19398 let lib_text = "mod foo {}";
19399 fs.insert_tree(
19400 path!("/a"),
19401 json!({
19402 "lib.rs": lib_text,
19403 "main.rs": main_text,
19404 }),
19405 )
19406 .await;
19407
19408 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19409 let (workspace, cx) =
19410 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19411 let worktree_id = workspace.update(cx, |workspace, cx| {
19412 workspace.project().update(cx, |project, cx| {
19413 project.worktrees(cx).next().unwrap().read(cx).id()
19414 })
19415 });
19416
19417 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19418 let editor = workspace
19419 .update_in(cx, |workspace, window, cx| {
19420 workspace.open_path(
19421 (worktree_id, "main.rs"),
19422 Some(pane.downgrade()),
19423 true,
19424 window,
19425 cx,
19426 )
19427 })
19428 .unwrap()
19429 .await
19430 .downcast::<Editor>()
19431 .unwrap();
19432 pane.update(cx, |pane, cx| {
19433 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19434 open_editor.update(cx, |editor, cx| {
19435 assert_eq!(
19436 editor.display_text(cx),
19437 main_text,
19438 "Original main.rs text on initial open",
19439 );
19440 })
19441 });
19442 editor.update_in(cx, |editor, window, cx| {
19443 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19444 });
19445
19446 cx.update_global(|store: &mut SettingsStore, cx| {
19447 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19448 s.restore_on_file_reopen = Some(false);
19449 });
19450 });
19451 editor.update_in(cx, |editor, window, cx| {
19452 editor.fold_ranges(
19453 vec![
19454 Point::new(1, 0)..Point::new(1, 1),
19455 Point::new(2, 0)..Point::new(2, 2),
19456 Point::new(3, 0)..Point::new(3, 3),
19457 ],
19458 false,
19459 window,
19460 cx,
19461 );
19462 });
19463 pane.update_in(cx, |pane, window, cx| {
19464 pane.close_all_items(&CloseAllItems::default(), window, cx)
19465 .unwrap()
19466 })
19467 .await
19468 .unwrap();
19469 pane.update(cx, |pane, _| {
19470 assert!(pane.active_item().is_none());
19471 });
19472 cx.update_global(|store: &mut SettingsStore, cx| {
19473 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19474 s.restore_on_file_reopen = Some(true);
19475 });
19476 });
19477
19478 let _editor_reopened = workspace
19479 .update_in(cx, |workspace, window, cx| {
19480 workspace.open_path(
19481 (worktree_id, "main.rs"),
19482 Some(pane.downgrade()),
19483 true,
19484 window,
19485 cx,
19486 )
19487 })
19488 .unwrap()
19489 .await
19490 .downcast::<Editor>()
19491 .unwrap();
19492 pane.update(cx, |pane, cx| {
19493 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19494 open_editor.update(cx, |editor, cx| {
19495 assert_eq!(
19496 editor.display_text(cx),
19497 main_text,
19498 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19499 );
19500 })
19501 });
19502}
19503
19504#[gpui::test]
19505async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19506 struct EmptyModalView {
19507 focus_handle: gpui::FocusHandle,
19508 }
19509 impl EventEmitter<DismissEvent> for EmptyModalView {}
19510 impl Render for EmptyModalView {
19511 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19512 div()
19513 }
19514 }
19515 impl Focusable for EmptyModalView {
19516 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19517 self.focus_handle.clone()
19518 }
19519 }
19520 impl workspace::ModalView for EmptyModalView {}
19521 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19522 EmptyModalView {
19523 focus_handle: cx.focus_handle(),
19524 }
19525 }
19526
19527 init_test(cx, |_| {});
19528
19529 let fs = FakeFs::new(cx.executor());
19530 let project = Project::test(fs, [], cx).await;
19531 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19532 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19533 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19534 let editor = cx.new_window_entity(|window, cx| {
19535 Editor::new(
19536 EditorMode::full(),
19537 buffer,
19538 Some(project.clone()),
19539 window,
19540 cx,
19541 )
19542 });
19543 workspace
19544 .update(cx, |workspace, window, cx| {
19545 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19546 })
19547 .unwrap();
19548 editor.update_in(cx, |editor, window, cx| {
19549 editor.open_context_menu(&OpenContextMenu, window, cx);
19550 assert!(editor.mouse_context_menu.is_some());
19551 });
19552 workspace
19553 .update(cx, |workspace, window, cx| {
19554 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19555 })
19556 .unwrap();
19557 cx.read(|cx| {
19558 assert!(editor.read(cx).mouse_context_menu.is_none());
19559 });
19560}
19561
19562fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19563 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19564 point..point
19565}
19566
19567fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19568 let (text, ranges) = marked_text_ranges(marked_text, true);
19569 assert_eq!(editor.text(cx), text);
19570 assert_eq!(
19571 editor.selections.ranges(cx),
19572 ranges,
19573 "Assert selections are {}",
19574 marked_text
19575 );
19576}
19577
19578pub fn handle_signature_help_request(
19579 cx: &mut EditorLspTestContext,
19580 mocked_response: lsp::SignatureHelp,
19581) -> impl Future<Output = ()> + use<> {
19582 let mut request =
19583 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19584 let mocked_response = mocked_response.clone();
19585 async move { Ok(Some(mocked_response)) }
19586 });
19587
19588 async move {
19589 request.next().await;
19590 }
19591}
19592
19593/// Handle completion request passing a marked string specifying where the completion
19594/// should be triggered from using '|' character, what range should be replaced, and what completions
19595/// should be returned using '<' and '>' to delimit the range.
19596///
19597/// Also see `handle_completion_request_with_insert_and_replace`.
19598#[track_caller]
19599pub fn handle_completion_request(
19600 cx: &mut EditorLspTestContext,
19601 marked_string: &str,
19602 completions: Vec<&'static str>,
19603 counter: Arc<AtomicUsize>,
19604) -> impl Future<Output = ()> {
19605 let complete_from_marker: TextRangeMarker = '|'.into();
19606 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19607 let (_, mut marked_ranges) = marked_text_ranges_by(
19608 marked_string,
19609 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19610 );
19611
19612 let complete_from_position =
19613 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19614 let replace_range =
19615 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19616
19617 let mut request =
19618 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19619 let completions = completions.clone();
19620 counter.fetch_add(1, atomic::Ordering::Release);
19621 async move {
19622 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19623 assert_eq!(
19624 params.text_document_position.position,
19625 complete_from_position
19626 );
19627 Ok(Some(lsp::CompletionResponse::Array(
19628 completions
19629 .iter()
19630 .map(|completion_text| lsp::CompletionItem {
19631 label: completion_text.to_string(),
19632 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19633 range: replace_range,
19634 new_text: completion_text.to_string(),
19635 })),
19636 ..Default::default()
19637 })
19638 .collect(),
19639 )))
19640 }
19641 });
19642
19643 async move {
19644 request.next().await;
19645 }
19646}
19647
19648/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19649/// given instead, which also contains an `insert` range.
19650///
19651/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19652/// that is, `replace_range.start..cursor_pos`.
19653pub fn handle_completion_request_with_insert_and_replace(
19654 cx: &mut EditorLspTestContext,
19655 marked_string: &str,
19656 completions: Vec<&'static str>,
19657 counter: Arc<AtomicUsize>,
19658) -> impl Future<Output = ()> {
19659 let complete_from_marker: TextRangeMarker = '|'.into();
19660 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19661 let (_, mut marked_ranges) = marked_text_ranges_by(
19662 marked_string,
19663 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19664 );
19665
19666 let complete_from_position =
19667 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19668 let replace_range =
19669 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19670
19671 let mut request =
19672 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19673 let completions = completions.clone();
19674 counter.fetch_add(1, atomic::Ordering::Release);
19675 async move {
19676 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19677 assert_eq!(
19678 params.text_document_position.position, complete_from_position,
19679 "marker `|` position doesn't match",
19680 );
19681 Ok(Some(lsp::CompletionResponse::Array(
19682 completions
19683 .iter()
19684 .map(|completion_text| lsp::CompletionItem {
19685 label: completion_text.to_string(),
19686 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19687 lsp::InsertReplaceEdit {
19688 insert: lsp::Range {
19689 start: replace_range.start,
19690 end: complete_from_position,
19691 },
19692 replace: replace_range,
19693 new_text: completion_text.to_string(),
19694 },
19695 )),
19696 ..Default::default()
19697 })
19698 .collect(),
19699 )))
19700 }
19701 });
19702
19703 async move {
19704 request.next().await;
19705 }
19706}
19707
19708fn handle_resolve_completion_request(
19709 cx: &mut EditorLspTestContext,
19710 edits: Option<Vec<(&'static str, &'static str)>>,
19711) -> impl Future<Output = ()> {
19712 let edits = edits.map(|edits| {
19713 edits
19714 .iter()
19715 .map(|(marked_string, new_text)| {
19716 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19717 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19718 lsp::TextEdit::new(replace_range, new_text.to_string())
19719 })
19720 .collect::<Vec<_>>()
19721 });
19722
19723 let mut request =
19724 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19725 let edits = edits.clone();
19726 async move {
19727 Ok(lsp::CompletionItem {
19728 additional_text_edits: edits,
19729 ..Default::default()
19730 })
19731 }
19732 });
19733
19734 async move {
19735 request.next().await;
19736 }
19737}
19738
19739pub(crate) fn update_test_language_settings(
19740 cx: &mut TestAppContext,
19741 f: impl Fn(&mut AllLanguageSettingsContent),
19742) {
19743 cx.update(|cx| {
19744 SettingsStore::update_global(cx, |store, cx| {
19745 store.update_user_settings::<AllLanguageSettings>(cx, f);
19746 });
19747 });
19748}
19749
19750pub(crate) fn update_test_project_settings(
19751 cx: &mut TestAppContext,
19752 f: impl Fn(&mut ProjectSettings),
19753) {
19754 cx.update(|cx| {
19755 SettingsStore::update_global(cx, |store, cx| {
19756 store.update_user_settings::<ProjectSettings>(cx, f);
19757 });
19758 });
19759}
19760
19761pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19762 cx.update(|cx| {
19763 assets::Assets.load_test_fonts(cx);
19764 let store = SettingsStore::test(cx);
19765 cx.set_global(store);
19766 theme::init(theme::LoadThemes::JustBase, cx);
19767 release_channel::init(SemanticVersion::default(), cx);
19768 client::init_settings(cx);
19769 language::init(cx);
19770 Project::init_settings(cx);
19771 workspace::init_settings(cx);
19772 crate::init(cx);
19773 });
19774
19775 update_test_language_settings(cx, f);
19776}
19777
19778#[track_caller]
19779fn assert_hunk_revert(
19780 not_reverted_text_with_selections: &str,
19781 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19782 expected_reverted_text_with_selections: &str,
19783 base_text: &str,
19784 cx: &mut EditorLspTestContext,
19785) {
19786 cx.set_state(not_reverted_text_with_selections);
19787 cx.set_head_text(base_text);
19788 cx.executor().run_until_parked();
19789
19790 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19791 let snapshot = editor.snapshot(window, cx);
19792 let reverted_hunk_statuses = snapshot
19793 .buffer_snapshot
19794 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19795 .map(|hunk| hunk.status().kind)
19796 .collect::<Vec<_>>();
19797
19798 editor.git_restore(&Default::default(), window, cx);
19799 reverted_hunk_statuses
19800 });
19801 cx.executor().run_until_parked();
19802 cx.assert_editor_state(expected_reverted_text_with_selections);
19803 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19804}