1use super::*;
2use crate::{
3 JoinLines,
4 scroll::scroll_amount::ScrollAmount,
5 test::{
6 assert_text_with_selections, build_editor,
7 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
8 editor_test_context::EditorTestContext,
9 select_ranges,
10 },
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions, div,
17};
18use indoc::indoc;
19use language::{
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, Point,
24 language_settings::{
25 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
26 LanguageSettingsContent, LspInsertMode, PrettierSettings,
27 },
28};
29use language_settings::{Formatter, FormatterList, IndentGuideSettings};
30use lsp::CompletionParams;
31use multi_buffer::{IndentGuide, PathKey};
32use parking_lot::Mutex;
33use pretty_assertions::{assert_eq, assert_ne};
34use project::{
35 FakeFs,
36 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
37 project_settings::{LspSettings, ProjectSettings},
38};
39use serde_json::{self, json};
40use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
41use std::{
42 iter,
43 sync::atomic::{self, AtomicUsize},
44};
45use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
46use text::ToPoint as _;
47use unindent::Unindent;
48use util::{
49 assert_set_eq, path,
50 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
51 uri,
52};
53use workspace::{
54 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
55 item::{FollowEvent, FollowableItem, Item, ItemHandle},
56};
57
58#[gpui::test]
59fn test_edit_events(cx: &mut TestAppContext) {
60 init_test(cx, |_| {});
61
62 let buffer = cx.new(|cx| {
63 let mut buffer = language::Buffer::local("123456", cx);
64 buffer.set_group_interval(Duration::from_secs(1));
65 buffer
66 });
67
68 let events = Rc::new(RefCell::new(Vec::new()));
69 let editor1 = cx.add_window({
70 let events = events.clone();
71 |window, cx| {
72 let entity = cx.entity().clone();
73 cx.subscribe_in(
74 &entity,
75 window,
76 move |_, _, event: &EditorEvent, _, _| match event {
77 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
78 EditorEvent::BufferEdited => {
79 events.borrow_mut().push(("editor1", "buffer edited"))
80 }
81 _ => {}
82 },
83 )
84 .detach();
85 Editor::for_buffer(buffer.clone(), None, window, cx)
86 }
87 });
88
89 let editor2 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 cx.subscribe_in(
93 &cx.entity().clone(),
94 window,
95 move |_, _, event: &EditorEvent, _, _| match event {
96 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
97 EditorEvent::BufferEdited => {
98 events.borrow_mut().push(("editor2", "buffer edited"))
99 }
100 _ => {}
101 },
102 )
103 .detach();
104 Editor::for_buffer(buffer.clone(), None, window, cx)
105 }
106 });
107
108 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
109
110 // Mutating editor 1 will emit an `Edited` event only for that editor.
111 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
112 assert_eq!(
113 mem::take(&mut *events.borrow_mut()),
114 [
115 ("editor1", "edited"),
116 ("editor1", "buffer edited"),
117 ("editor2", "buffer edited"),
118 ]
119 );
120
121 // Mutating editor 2 will emit an `Edited` event only for that editor.
122 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor2", "edited"),
127 ("editor1", "buffer edited"),
128 ("editor2", "buffer edited"),
129 ]
130 );
131
132 // Undoing on editor 1 will emit an `Edited` event only for that editor.
133 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor1", "edited"),
138 ("editor1", "buffer edited"),
139 ("editor2", "buffer edited"),
140 ]
141 );
142
143 // Redoing on editor 1 will emit an `Edited` event only for that editor.
144 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
145 assert_eq!(
146 mem::take(&mut *events.borrow_mut()),
147 [
148 ("editor1", "edited"),
149 ("editor1", "buffer edited"),
150 ("editor2", "buffer edited"),
151 ]
152 );
153
154 // Undoing on editor 2 will emit an `Edited` event only for that editor.
155 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
156 assert_eq!(
157 mem::take(&mut *events.borrow_mut()),
158 [
159 ("editor2", "edited"),
160 ("editor1", "buffer edited"),
161 ("editor2", "buffer edited"),
162 ]
163 );
164
165 // Redoing on editor 2 will emit an `Edited` event only for that editor.
166 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
167 assert_eq!(
168 mem::take(&mut *events.borrow_mut()),
169 [
170 ("editor2", "edited"),
171 ("editor1", "buffer edited"),
172 ("editor2", "buffer edited"),
173 ]
174 );
175
176 // No event is emitted when the mutation is a no-op.
177 _ = editor2.update(cx, |editor, window, cx| {
178 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
179
180 editor.backspace(&Backspace, window, cx);
181 });
182 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
183}
184
185#[gpui::test]
186fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
187 init_test(cx, |_| {});
188
189 let mut now = Instant::now();
190 let group_interval = Duration::from_millis(1);
191 let buffer = cx.new(|cx| {
192 let mut buf = language::Buffer::local("123456", cx);
193 buf.set_group_interval(group_interval);
194 buf
195 });
196 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
197 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
198
199 _ = editor.update(cx, |editor, window, cx| {
200 editor.start_transaction_at(now, window, cx);
201 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
202
203 editor.insert("cd", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cd56");
206 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
207
208 editor.start_transaction_at(now, window, cx);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
210 editor.insert("e", window, cx);
211 editor.end_transaction_at(now, cx);
212 assert_eq!(editor.text(cx), "12cde6");
213 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
214
215 now += group_interval + Duration::from_millis(1);
216 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
217
218 // Simulate an edit in another editor
219 buffer.update(cx, |buffer, cx| {
220 buffer.start_transaction_at(now, cx);
221 buffer.edit([(0..1, "a")], None, cx);
222 buffer.edit([(1..1, "b")], None, cx);
223 buffer.end_transaction_at(now, cx);
224 });
225
226 assert_eq!(editor.text(cx), "ab2cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
228
229 // Last transaction happened past the group interval in a different editor.
230 // Undo it individually and don't restore selections.
231 editor.undo(&Undo, window, cx);
232 assert_eq!(editor.text(cx), "12cde6");
233 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
234
235 // First two transactions happened within the group interval in this editor.
236 // Undo them together and restore selections.
237 editor.undo(&Undo, window, cx);
238 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
239 assert_eq!(editor.text(cx), "123456");
240 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
241
242 // Redo the first two transactions together.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
246
247 // Redo the last transaction on its own.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "ab2cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
251
252 // Test empty transactions.
253 editor.start_transaction_at(now, window, cx);
254 editor.end_transaction_at(now, cx);
255 editor.undo(&Undo, window, cx);
256 assert_eq!(editor.text(cx), "12cde6");
257 });
258}
259
260#[gpui::test]
261fn test_ime_composition(cx: &mut TestAppContext) {
262 init_test(cx, |_| {});
263
264 let buffer = cx.new(|cx| {
265 let mut buffer = language::Buffer::local("abcde", cx);
266 // Ensure automatic grouping doesn't occur.
267 buffer.set_group_interval(Duration::ZERO);
268 buffer
269 });
270
271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
272 cx.add_window(|window, cx| {
273 let mut editor = build_editor(buffer.clone(), window, cx);
274
275 // Start a new IME composition.
276 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
277 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
278 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
279 assert_eq!(editor.text(cx), "äbcde");
280 assert_eq!(
281 editor.marked_text_ranges(cx),
282 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
283 );
284
285 // Finalize IME composition.
286 editor.replace_text_in_range(None, "ā", window, cx);
287 assert_eq!(editor.text(cx), "ābcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // IME composition edits are grouped and are undone/redone at once.
291 editor.undo(&Default::default(), window, cx);
292 assert_eq!(editor.text(cx), "abcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294 editor.redo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "ābcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297
298 // Start a new IME composition.
299 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Undoing during an IME composition cancels it.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
311 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
312 assert_eq!(editor.text(cx), "ābcdè");
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
316 );
317
318 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
319 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
320 assert_eq!(editor.text(cx), "ābcdę");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with multiple cursors.
324 editor.change_selections(None, window, cx, |s| {
325 s.select_ranges([
326 OffsetUtf16(1)..OffsetUtf16(1),
327 OffsetUtf16(3)..OffsetUtf16(3),
328 OffsetUtf16(5)..OffsetUtf16(5),
329 ])
330 });
331 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
332 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![
336 OffsetUtf16(0)..OffsetUtf16(3),
337 OffsetUtf16(4)..OffsetUtf16(7),
338 OffsetUtf16(8)..OffsetUtf16(11)
339 ])
340 );
341
342 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
343 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
344 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
345 assert_eq!(
346 editor.marked_text_ranges(cx),
347 Some(vec![
348 OffsetUtf16(1)..OffsetUtf16(2),
349 OffsetUtf16(5)..OffsetUtf16(6),
350 OffsetUtf16(9)..OffsetUtf16(10)
351 ])
352 );
353
354 // Finalize IME composition with multiple cursors.
355 editor.replace_text_in_range(Some(9..10), "2", window, cx);
356 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 editor
360 });
361}
362
363#[gpui::test]
364fn test_selection_with_mouse(cx: &mut TestAppContext) {
365 init_test(cx, |_| {});
366
367 let editor = cx.add_window(|window, cx| {
368 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
369 build_editor(buffer, window, cx)
370 });
371
372 _ = editor.update(cx, |editor, window, cx| {
373 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
374 });
375 assert_eq!(
376 editor
377 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
378 .unwrap(),
379 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
380 );
381
382 _ = editor.update(cx, |editor, window, cx| {
383 editor.update_selection(
384 DisplayPoint::new(DisplayRow(3), 3),
385 0,
386 gpui::Point::<f32>::default(),
387 window,
388 cx,
389 );
390 });
391
392 assert_eq!(
393 editor
394 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
395 .unwrap(),
396 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
397 );
398
399 _ = editor.update(cx, |editor, window, cx| {
400 editor.update_selection(
401 DisplayPoint::new(DisplayRow(1), 1),
402 0,
403 gpui::Point::<f32>::default(),
404 window,
405 cx,
406 );
407 });
408
409 assert_eq!(
410 editor
411 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
412 .unwrap(),
413 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
414 );
415
416 _ = editor.update(cx, |editor, window, cx| {
417 editor.end_selection(window, cx);
418 editor.update_selection(
419 DisplayPoint::new(DisplayRow(3), 3),
420 0,
421 gpui::Point::<f32>::default(),
422 window,
423 cx,
424 );
425 });
426
427 assert_eq!(
428 editor
429 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
430 .unwrap(),
431 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
432 );
433
434 _ = editor.update(cx, |editor, window, cx| {
435 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
436 editor.update_selection(
437 DisplayPoint::new(DisplayRow(0), 0),
438 0,
439 gpui::Point::<f32>::default(),
440 window,
441 cx,
442 );
443 });
444
445 assert_eq!(
446 editor
447 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
448 .unwrap(),
449 [
450 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
451 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
452 ]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.end_selection(window, cx);
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
464 );
465}
466
467#[gpui::test]
468fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
469 init_test(cx, |_| {});
470
471 let editor = cx.add_window(|window, cx| {
472 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
473 build_editor(buffer, window, cx)
474 });
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.end_selection(window, cx);
490 });
491
492 assert_eq!(
493 editor
494 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
495 .unwrap(),
496 [
497 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
498 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
499 ]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
504 });
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.end_selection(window, cx);
508 });
509
510 assert_eq!(
511 editor
512 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
513 .unwrap(),
514 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
515 );
516}
517
518#[gpui::test]
519fn test_canceling_pending_selection(cx: &mut TestAppContext) {
520 init_test(cx, |_| {});
521
522 let editor = cx.add_window(|window, cx| {
523 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
524 build_editor(buffer, window, cx)
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
529 assert_eq!(
530 editor.selections.display_ranges(cx),
531 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
532 );
533 });
534
535 _ = editor.update(cx, |editor, window, cx| {
536 editor.update_selection(
537 DisplayPoint::new(DisplayRow(3), 3),
538 0,
539 gpui::Point::<f32>::default(),
540 window,
541 cx,
542 );
543 assert_eq!(
544 editor.selections.display_ranges(cx),
545 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
546 );
547 });
548
549 _ = editor.update(cx, |editor, window, cx| {
550 editor.cancel(&Cancel, window, cx);
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(1), 1),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563}
564
565#[gpui::test]
566fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
567 init_test(cx, |_| {});
568
569 let editor = cx.add_window(|window, cx| {
570 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
571 build_editor(buffer, window, cx)
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
576 assert_eq!(
577 editor.selections.display_ranges(cx),
578 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
579 );
580
581 editor.move_down(&Default::default(), window, cx);
582 assert_eq!(
583 editor.selections.display_ranges(cx),
584 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
585 );
586
587 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
591 );
592
593 editor.move_up(&Default::default(), window, cx);
594 assert_eq!(
595 editor.selections.display_ranges(cx),
596 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
597 );
598 });
599}
600
601#[gpui::test]
602fn test_clone(cx: &mut TestAppContext) {
603 init_test(cx, |_| {});
604
605 let (text, selection_ranges) = marked_text_ranges(
606 indoc! {"
607 one
608 two
609 threeˇ
610 four
611 fiveˇ
612 "},
613 true,
614 );
615
616 let editor = cx.add_window(|window, cx| {
617 let buffer = MultiBuffer::build_simple(&text, cx);
618 build_editor(buffer, window, cx)
619 });
620
621 _ = editor.update(cx, |editor, window, cx| {
622 editor.change_selections(None, window, cx, |s| {
623 s.select_ranges(selection_ranges.clone())
624 });
625 editor.fold_creases(
626 vec![
627 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
628 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
629 ],
630 true,
631 window,
632 cx,
633 );
634 });
635
636 let cloned_editor = editor
637 .update(cx, |editor, _, cx| {
638 cx.open_window(Default::default(), |window, cx| {
639 cx.new(|cx| editor.clone(window, cx))
640 })
641 })
642 .unwrap()
643 .unwrap();
644
645 let snapshot = editor
646 .update(cx, |e, window, cx| e.snapshot(window, cx))
647 .unwrap();
648 let cloned_snapshot = cloned_editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651
652 assert_eq!(
653 cloned_editor
654 .update(cx, |e, _, cx| e.display_text(cx))
655 .unwrap(),
656 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
657 );
658 assert_eq!(
659 cloned_snapshot
660 .folds_in_range(0..text.len())
661 .collect::<Vec<_>>(),
662 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
663 );
664 assert_set_eq!(
665 cloned_editor
666 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
667 .unwrap(),
668 editor
669 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
670 .unwrap()
671 );
672 assert_set_eq!(
673 cloned_editor
674 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
675 .unwrap(),
676 editor
677 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
678 .unwrap()
679 );
680}
681
682#[gpui::test]
683async fn test_navigation_history(cx: &mut TestAppContext) {
684 init_test(cx, |_| {});
685
686 use workspace::item::Item;
687
688 let fs = FakeFs::new(cx.executor());
689 let project = Project::test(fs, [], cx).await;
690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
691 let pane = workspace
692 .update(cx, |workspace, _, _| workspace.active_pane().clone())
693 .unwrap();
694
695 _ = workspace.update(cx, |_v, window, cx| {
696 cx.new(|cx| {
697 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
698 let mut editor = build_editor(buffer.clone(), window, cx);
699 let handle = cx.entity();
700 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
701
702 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
703 editor.nav_history.as_mut().unwrap().pop_backward(cx)
704 }
705
706 // Move the cursor a small distance.
707 // Nothing is added to the navigation history.
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
711 ])
712 });
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
716 ])
717 });
718 assert!(pop_history(&mut editor, cx).is_none());
719
720 // Move the cursor a large distance.
721 // The history can jump back to the previous position.
722 editor.change_selections(None, window, cx, |s| {
723 s.select_display_ranges([
724 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
725 ])
726 });
727 let nav_entry = pop_history(&mut editor, cx).unwrap();
728 editor.navigate(nav_entry.data.unwrap(), window, cx);
729 assert_eq!(nav_entry.item.id(), cx.entity_id());
730 assert_eq!(
731 editor.selections.display_ranges(cx),
732 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
733 );
734 assert!(pop_history(&mut editor, cx).is_none());
735
736 // Move the cursor a small distance via the mouse.
737 // Nothing is added to the navigation history.
738 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
739 editor.end_selection(window, cx);
740 assert_eq!(
741 editor.selections.display_ranges(cx),
742 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
743 );
744 assert!(pop_history(&mut editor, cx).is_none());
745
746 // Move the cursor a large distance via the mouse.
747 // The history can jump back to the previous position.
748 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
749 editor.end_selection(window, cx);
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
753 );
754 let nav_entry = pop_history(&mut editor, cx).unwrap();
755 editor.navigate(nav_entry.data.unwrap(), window, cx);
756 assert_eq!(nav_entry.item.id(), cx.entity_id());
757 assert_eq!(
758 editor.selections.display_ranges(cx),
759 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
760 );
761 assert!(pop_history(&mut editor, cx).is_none());
762
763 // Set scroll position to check later
764 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
765 let original_scroll_position = editor.scroll_manager.anchor();
766
767 // Jump to the end of the document and adjust scroll
768 editor.move_to_end(&MoveToEnd, window, cx);
769 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
770 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
771
772 let nav_entry = pop_history(&mut editor, cx).unwrap();
773 editor.navigate(nav_entry.data.unwrap(), window, cx);
774 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
777 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
778 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
779 let invalid_point = Point::new(9999, 0);
780 editor.navigate(
781 Box::new(NavigationData {
782 cursor_anchor: invalid_anchor,
783 cursor_position: invalid_point,
784 scroll_anchor: ScrollAnchor {
785 anchor: invalid_anchor,
786 offset: Default::default(),
787 },
788 scroll_top_row: invalid_point.row,
789 }),
790 window,
791 cx,
792 );
793 assert_eq!(
794 editor.selections.display_ranges(cx),
795 &[editor.max_point(cx)..editor.max_point(cx)]
796 );
797 assert_eq!(
798 editor.scroll_position(cx),
799 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
800 );
801
802 editor
803 })
804 });
805}
806
807#[gpui::test]
808fn test_cancel(cx: &mut TestAppContext) {
809 init_test(cx, |_| {});
810
811 let editor = cx.add_window(|window, cx| {
812 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
813 build_editor(buffer, window, cx)
814 });
815
816 _ = editor.update(cx, |editor, window, cx| {
817 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
818 editor.update_selection(
819 DisplayPoint::new(DisplayRow(1), 1),
820 0,
821 gpui::Point::<f32>::default(),
822 window,
823 cx,
824 );
825 editor.end_selection(window, cx);
826
827 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
828 editor.update_selection(
829 DisplayPoint::new(DisplayRow(0), 3),
830 0,
831 gpui::Point::<f32>::default(),
832 window,
833 cx,
834 );
835 editor.end_selection(window, cx);
836 assert_eq!(
837 editor.selections.display_ranges(cx),
838 [
839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
840 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
841 ]
842 );
843 });
844
845 _ = editor.update(cx, |editor, window, cx| {
846 editor.cancel(&Cancel, window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
850 );
851 });
852
853 _ = editor.update(cx, |editor, window, cx| {
854 editor.cancel(&Cancel, window, cx);
855 assert_eq!(
856 editor.selections.display_ranges(cx),
857 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
858 );
859 });
860}
861
862#[gpui::test]
863fn test_fold_action(cx: &mut TestAppContext) {
864 init_test(cx, |_| {});
865
866 let editor = cx.add_window(|window, cx| {
867 let buffer = MultiBuffer::build_simple(
868 &"
869 impl Foo {
870 // Hello!
871
872 fn a() {
873 1
874 }
875
876 fn b() {
877 2
878 }
879
880 fn c() {
881 3
882 }
883 }
884 "
885 .unindent(),
886 cx,
887 );
888 build_editor(buffer.clone(), window, cx)
889 });
890
891 _ = editor.update(cx, |editor, window, cx| {
892 editor.change_selections(None, window, cx, |s| {
893 s.select_display_ranges([
894 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
895 ]);
896 });
897 editor.fold(&Fold, window, cx);
898 assert_eq!(
899 editor.display_text(cx),
900 "
901 impl Foo {
902 // Hello!
903
904 fn a() {
905 1
906 }
907
908 fn b() {⋯
909 }
910
911 fn c() {⋯
912 }
913 }
914 "
915 .unindent(),
916 );
917
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {⋯
923 }
924 "
925 .unindent(),
926 );
927
928 editor.unfold_lines(&UnfoldLines, window, cx);
929 assert_eq!(
930 editor.display_text(cx),
931 "
932 impl Foo {
933 // Hello!
934
935 fn a() {
936 1
937 }
938
939 fn b() {⋯
940 }
941
942 fn c() {⋯
943 }
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 editor.buffer.read(cx).read(cx).text()
953 );
954 });
955}
956
957#[gpui::test]
958fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
959 init_test(cx, |_| {});
960
961 let editor = cx.add_window(|window, cx| {
962 let buffer = MultiBuffer::build_simple(
963 &"
964 class Foo:
965 # Hello!
966
967 def a():
968 print(1)
969
970 def b():
971 print(2)
972
973 def c():
974 print(3)
975 "
976 .unindent(),
977 cx,
978 );
979 build_editor(buffer.clone(), window, cx)
980 });
981
982 _ = editor.update(cx, |editor, window, cx| {
983 editor.change_selections(None, window, cx, |s| {
984 s.select_display_ranges([
985 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
986 ]);
987 });
988 editor.fold(&Fold, window, cx);
989 assert_eq!(
990 editor.display_text(cx),
991 "
992 class Foo:
993 # Hello!
994
995 def a():
996 print(1)
997
998 def b():⋯
999
1000 def c():⋯
1001 "
1002 .unindent(),
1003 );
1004
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 class Foo:⋯
1010 "
1011 .unindent(),
1012 );
1013
1014 editor.unfold_lines(&UnfoldLines, window, cx);
1015 assert_eq!(
1016 editor.display_text(cx),
1017 "
1018 class Foo:
1019 # Hello!
1020
1021 def a():
1022 print(1)
1023
1024 def b():⋯
1025
1026 def c():⋯
1027 "
1028 .unindent(),
1029 );
1030
1031 editor.unfold_lines(&UnfoldLines, window, cx);
1032 assert_eq!(
1033 editor.display_text(cx),
1034 editor.buffer.read(cx).read(cx).text()
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 class Foo:
1047 # Hello!
1048
1049 def a():
1050 print(1)
1051
1052 def b():
1053 print(2)
1054
1055
1056 def c():
1057 print(3)
1058
1059
1060 "
1061 .unindent(),
1062 cx,
1063 );
1064 build_editor(buffer.clone(), window, cx)
1065 });
1066
1067 _ = editor.update(cx, |editor, window, cx| {
1068 editor.change_selections(None, window, cx, |s| {
1069 s.select_display_ranges([
1070 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1071 ]);
1072 });
1073 editor.fold(&Fold, window, cx);
1074 assert_eq!(
1075 editor.display_text(cx),
1076 "
1077 class Foo:
1078 # Hello!
1079
1080 def a():
1081 print(1)
1082
1083 def b():⋯
1084
1085
1086 def c():⋯
1087
1088
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:⋯
1098
1099
1100 "
1101 .unindent(),
1102 );
1103
1104 editor.unfold_lines(&UnfoldLines, window, cx);
1105 assert_eq!(
1106 editor.display_text(cx),
1107 "
1108 class Foo:
1109 # Hello!
1110
1111 def a():
1112 print(1)
1113
1114 def b():⋯
1115
1116
1117 def c():⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_at_level(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148
1149 class Bar:
1150 # World!
1151
1152 def a():
1153 print(1)
1154
1155 def b():
1156 print(2)
1157
1158
1159 "
1160 .unindent(),
1161 cx,
1162 );
1163 build_editor(buffer.clone(), window, cx)
1164 });
1165
1166 _ = editor.update(cx, |editor, window, cx| {
1167 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():⋯
1175
1176 def b():⋯
1177
1178
1179 class Bar:
1180 # World!
1181
1182 def a():⋯
1183
1184 def b():⋯
1185
1186
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:⋯
1196
1197
1198 class Bar:⋯
1199
1200
1201 "
1202 .unindent(),
1203 );
1204
1205 editor.unfold_all(&UnfoldAll, window, cx);
1206 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():
1217 print(2)
1218
1219
1220 class Bar:
1221 # World!
1222
1223 def a():
1224 print(1)
1225
1226 def b():
1227 print(2)
1228
1229
1230 "
1231 .unindent(),
1232 );
1233
1234 assert_eq!(
1235 editor.display_text(cx),
1236 editor.buffer.read(cx).read(cx).text()
1237 );
1238 });
1239}
1240
1241#[gpui::test]
1242fn test_move_cursor(cx: &mut TestAppContext) {
1243 init_test(cx, |_| {});
1244
1245 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1246 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1247
1248 buffer.update(cx, |buffer, cx| {
1249 buffer.edit(
1250 vec![
1251 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1252 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1253 ],
1254 None,
1255 cx,
1256 );
1257 });
1258 _ = editor.update(cx, |editor, window, cx| {
1259 assert_eq!(
1260 editor.selections.display_ranges(cx),
1261 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1262 );
1263
1264 editor.move_down(&MoveDown, window, cx);
1265 assert_eq!(
1266 editor.selections.display_ranges(cx),
1267 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1268 );
1269
1270 editor.move_right(&MoveRight, window, cx);
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1274 );
1275
1276 editor.move_left(&MoveLeft, window, cx);
1277 assert_eq!(
1278 editor.selections.display_ranges(cx),
1279 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1280 );
1281
1282 editor.move_up(&MoveUp, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1286 );
1287
1288 editor.move_to_end(&MoveToEnd, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1292 );
1293
1294 editor.move_to_beginning(&MoveToBeginning, window, cx);
1295 assert_eq!(
1296 editor.selections.display_ranges(cx),
1297 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1298 );
1299
1300 editor.change_selections(None, window, cx, |s| {
1301 s.select_display_ranges([
1302 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1303 ]);
1304 });
1305 editor.select_to_beginning(&SelectToBeginning, window, cx);
1306 assert_eq!(
1307 editor.selections.display_ranges(cx),
1308 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1309 );
1310
1311 editor.select_to_end(&SelectToEnd, window, cx);
1312 assert_eq!(
1313 editor.selections.display_ranges(cx),
1314 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1315 );
1316 });
1317}
1318
1319// TODO: Re-enable this test
1320#[cfg(target_os = "macos")]
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["//".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 //ˇ
2775 "});
2776 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2777 cx.set_state(indoc! {"
2778 ˇ// Foo
2779 "});
2780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2781 cx.assert_editor_state(indoc! {"
2782
2783 ˇ// Foo
2784 "});
2785 }
2786 // Ensure that comment continuations can be disabled.
2787 update_test_language_settings(cx, |settings| {
2788 settings.defaults.extend_comment_on_newline = Some(false);
2789 });
2790 let mut cx = EditorTestContext::new(cx).await;
2791 cx.set_state(indoc! {"
2792 // Fooˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 ˇ
2798 "});
2799}
2800
2801#[gpui::test]
2802fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2803 init_test(cx, |_| {});
2804
2805 let editor = cx.add_window(|window, cx| {
2806 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2807 let mut editor = build_editor(buffer.clone(), window, cx);
2808 editor.change_selections(None, window, cx, |s| {
2809 s.select_ranges([3..4, 11..12, 19..20])
2810 });
2811 editor
2812 });
2813
2814 _ = editor.update(cx, |editor, window, cx| {
2815 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2816 editor.buffer.update(cx, |buffer, cx| {
2817 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2818 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2819 });
2820 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2821
2822 editor.insert("Z", window, cx);
2823 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2824
2825 // The selections are moved after the inserted characters
2826 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2827 });
2828}
2829
2830#[gpui::test]
2831async fn test_tab(cx: &mut TestAppContext) {
2832 init_test(cx, |settings| {
2833 settings.defaults.tab_size = NonZeroU32::new(3)
2834 });
2835
2836 let mut cx = EditorTestContext::new(cx).await;
2837 cx.set_state(indoc! {"
2838 ˇabˇc
2839 ˇ🏀ˇ🏀ˇefg
2840 dˇ
2841 "});
2842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2843 cx.assert_editor_state(indoc! {"
2844 ˇab ˇc
2845 ˇ🏀 ˇ🏀 ˇefg
2846 d ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 a
2851 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2852 "});
2853 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 a
2856 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2857 "});
2858}
2859
2860#[gpui::test]
2861async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let mut cx = EditorTestContext::new(cx).await;
2865 let language = Arc::new(
2866 Language::new(
2867 LanguageConfig::default(),
2868 Some(tree_sitter_rust::LANGUAGE.into()),
2869 )
2870 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2871 .unwrap(),
2872 );
2873 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2874
2875 // cursors that are already at the suggested indent level insert
2876 // a soft tab. cursors that are to the left of the suggested indent
2877 // auto-indent their line.
2878 cx.set_state(indoc! {"
2879 ˇ
2880 const a: B = (
2881 c(
2882 d(
2883 ˇ
2884 )
2885 ˇ
2886 ˇ )
2887 );
2888 "});
2889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2890 cx.assert_editor_state(indoc! {"
2891 ˇ
2892 const a: B = (
2893 c(
2894 d(
2895 ˇ
2896 )
2897 ˇ
2898 ˇ)
2899 );
2900 "});
2901
2902 // handle auto-indent when there are multiple cursors on the same line
2903 cx.set_state(indoc! {"
2904 const a: B = (
2905 c(
2906 ˇ ˇ
2907 ˇ )
2908 );
2909 "});
2910 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 const a: B = (
2913 c(
2914 ˇ
2915 ˇ)
2916 );
2917 "});
2918}
2919
2920#[gpui::test]
2921async fn test_tab_with_mixed_whitespace(cx: &mut TestAppContext) {
2922 init_test(cx, |settings| {
2923 settings.defaults.tab_size = NonZeroU32::new(4)
2924 });
2925
2926 let language = Arc::new(
2927 Language::new(
2928 LanguageConfig::default(),
2929 Some(tree_sitter_rust::LANGUAGE.into()),
2930 )
2931 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2932 .unwrap(),
2933 );
2934
2935 let mut cx = EditorTestContext::new(cx).await;
2936 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2937 cx.set_state(indoc! {"
2938 fn a() {
2939 if b {
2940 \t ˇc
2941 }
2942 }
2943 "});
2944
2945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2946 cx.assert_editor_state(indoc! {"
2947 fn a() {
2948 if b {
2949 ˇc
2950 }
2951 }
2952 "});
2953}
2954
2955#[gpui::test]
2956async fn test_indent_outdent(cx: &mut TestAppContext) {
2957 init_test(cx, |settings| {
2958 settings.defaults.tab_size = NonZeroU32::new(4);
2959 });
2960
2961 let mut cx = EditorTestContext::new(cx).await;
2962
2963 cx.set_state(indoc! {"
2964 «oneˇ» «twoˇ»
2965 three
2966 four
2967 "});
2968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 «oneˇ» «twoˇ»
2971 three
2972 four
2973 "});
2974
2975 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2976 cx.assert_editor_state(indoc! {"
2977 «oneˇ» «twoˇ»
2978 three
2979 four
2980 "});
2981
2982 // select across line ending
2983 cx.set_state(indoc! {"
2984 one two
2985 t«hree
2986 ˇ» four
2987 "});
2988 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2989 cx.assert_editor_state(indoc! {"
2990 one two
2991 t«hree
2992 ˇ» four
2993 "});
2994
2995 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 t«hree
2999 ˇ» four
3000 "});
3001
3002 // Ensure that indenting/outdenting works when the cursor is at column 0.
3003 cx.set_state(indoc! {"
3004 one two
3005 ˇthree
3006 four
3007 "});
3008 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3009 cx.assert_editor_state(indoc! {"
3010 one two
3011 ˇthree
3012 four
3013 "});
3014
3015 cx.set_state(indoc! {"
3016 one two
3017 ˇ three
3018 four
3019 "});
3020 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 one two
3023 ˇthree
3024 four
3025 "});
3026}
3027
3028#[gpui::test]
3029async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3030 init_test(cx, |settings| {
3031 settings.defaults.hard_tabs = Some(true);
3032 });
3033
3034 let mut cx = EditorTestContext::new(cx).await;
3035
3036 // select two ranges on one line
3037 cx.set_state(indoc! {"
3038 «oneˇ» «twoˇ»
3039 three
3040 four
3041 "});
3042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3043 cx.assert_editor_state(indoc! {"
3044 \t«oneˇ» «twoˇ»
3045 three
3046 four
3047 "});
3048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 \t\t«oneˇ» «twoˇ»
3051 three
3052 four
3053 "});
3054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 \t«oneˇ» «twoˇ»
3057 three
3058 four
3059 "});
3060 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3061 cx.assert_editor_state(indoc! {"
3062 «oneˇ» «twoˇ»
3063 three
3064 four
3065 "});
3066
3067 // select across a line ending
3068 cx.set_state(indoc! {"
3069 one two
3070 t«hree
3071 ˇ»four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 one two
3076 \tt«hree
3077 ˇ»four
3078 "});
3079 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 one two
3082 \t\tt«hree
3083 ˇ»four
3084 "});
3085 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3086 cx.assert_editor_state(indoc! {"
3087 one two
3088 \tt«hree
3089 ˇ»four
3090 "});
3091 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 one two
3094 t«hree
3095 ˇ»four
3096 "});
3097
3098 // Ensure that indenting/outdenting works when the cursor is at column 0.
3099 cx.set_state(indoc! {"
3100 one two
3101 ˇthree
3102 four
3103 "});
3104 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 ˇthree
3108 four
3109 "});
3110 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 \tˇthree
3114 four
3115 "});
3116 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 one two
3119 ˇthree
3120 four
3121 "});
3122}
3123
3124#[gpui::test]
3125fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3126 init_test(cx, |settings| {
3127 settings.languages.extend([
3128 (
3129 "TOML".into(),
3130 LanguageSettingsContent {
3131 tab_size: NonZeroU32::new(2),
3132 ..Default::default()
3133 },
3134 ),
3135 (
3136 "Rust".into(),
3137 LanguageSettingsContent {
3138 tab_size: NonZeroU32::new(4),
3139 ..Default::default()
3140 },
3141 ),
3142 ]);
3143 });
3144
3145 let toml_language = Arc::new(Language::new(
3146 LanguageConfig {
3147 name: "TOML".into(),
3148 ..Default::default()
3149 },
3150 None,
3151 ));
3152 let rust_language = Arc::new(Language::new(
3153 LanguageConfig {
3154 name: "Rust".into(),
3155 ..Default::default()
3156 },
3157 None,
3158 ));
3159
3160 let toml_buffer =
3161 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3162 let rust_buffer =
3163 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3164 let multibuffer = cx.new(|cx| {
3165 let mut multibuffer = MultiBuffer::new(ReadWrite);
3166 multibuffer.push_excerpts(
3167 toml_buffer.clone(),
3168 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3169 cx,
3170 );
3171 multibuffer.push_excerpts(
3172 rust_buffer.clone(),
3173 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3174 cx,
3175 );
3176 multibuffer
3177 });
3178
3179 cx.add_window(|window, cx| {
3180 let mut editor = build_editor(multibuffer, window, cx);
3181
3182 assert_eq!(
3183 editor.text(cx),
3184 indoc! {"
3185 a = 1
3186 b = 2
3187
3188 const c: usize = 3;
3189 "}
3190 );
3191
3192 select_ranges(
3193 &mut editor,
3194 indoc! {"
3195 «aˇ» = 1
3196 b = 2
3197
3198 «const c:ˇ» usize = 3;
3199 "},
3200 window,
3201 cx,
3202 );
3203
3204 editor.tab(&Tab, window, cx);
3205 assert_text_with_selections(
3206 &mut editor,
3207 indoc! {"
3208 «aˇ» = 1
3209 b = 2
3210
3211 «const c:ˇ» usize = 3;
3212 "},
3213 cx,
3214 );
3215 editor.backtab(&Backtab, window, cx);
3216 assert_text_with_selections(
3217 &mut editor,
3218 indoc! {"
3219 «aˇ» = 1
3220 b = 2
3221
3222 «const c:ˇ» usize = 3;
3223 "},
3224 cx,
3225 );
3226
3227 editor
3228 });
3229}
3230
3231#[gpui::test]
3232async fn test_backspace(cx: &mut TestAppContext) {
3233 init_test(cx, |_| {});
3234
3235 let mut cx = EditorTestContext::new(cx).await;
3236
3237 // Basic backspace
3238 cx.set_state(indoc! {"
3239 onˇe two three
3240 fou«rˇ» five six
3241 seven «ˇeight nine
3242 »ten
3243 "});
3244 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3245 cx.assert_editor_state(indoc! {"
3246 oˇe two three
3247 fouˇ five six
3248 seven ˇten
3249 "});
3250
3251 // Test backspace inside and around indents
3252 cx.set_state(indoc! {"
3253 zero
3254 ˇone
3255 ˇtwo
3256 ˇ ˇ ˇ three
3257 ˇ ˇ four
3258 "});
3259 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3260 cx.assert_editor_state(indoc! {"
3261 zero
3262 ˇone
3263 ˇtwo
3264 ˇ threeˇ four
3265 "});
3266}
3267
3268#[gpui::test]
3269async fn test_delete(cx: &mut TestAppContext) {
3270 init_test(cx, |_| {});
3271
3272 let mut cx = EditorTestContext::new(cx).await;
3273 cx.set_state(indoc! {"
3274 onˇe two three
3275 fou«rˇ» five six
3276 seven «ˇeight nine
3277 »ten
3278 "});
3279 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3280 cx.assert_editor_state(indoc! {"
3281 onˇ two three
3282 fouˇ five six
3283 seven ˇten
3284 "});
3285}
3286
3287#[gpui::test]
3288fn test_delete_line(cx: &mut TestAppContext) {
3289 init_test(cx, |_| {});
3290
3291 let editor = cx.add_window(|window, cx| {
3292 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3293 build_editor(buffer, window, cx)
3294 });
3295 _ = editor.update(cx, |editor, window, cx| {
3296 editor.change_selections(None, window, cx, |s| {
3297 s.select_display_ranges([
3298 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3299 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3300 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3301 ])
3302 });
3303 editor.delete_line(&DeleteLine, window, cx);
3304 assert_eq!(editor.display_text(cx), "ghi");
3305 assert_eq!(
3306 editor.selections.display_ranges(cx),
3307 vec![
3308 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3309 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3310 ]
3311 );
3312 });
3313
3314 let editor = cx.add_window(|window, cx| {
3315 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3316 build_editor(buffer, window, cx)
3317 });
3318 _ = editor.update(cx, |editor, window, cx| {
3319 editor.change_selections(None, window, cx, |s| {
3320 s.select_display_ranges([
3321 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3322 ])
3323 });
3324 editor.delete_line(&DeleteLine, window, cx);
3325 assert_eq!(editor.display_text(cx), "ghi\n");
3326 assert_eq!(
3327 editor.selections.display_ranges(cx),
3328 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3329 );
3330 });
3331}
3332
3333#[gpui::test]
3334fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3335 init_test(cx, |_| {});
3336
3337 cx.add_window(|window, cx| {
3338 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3339 let mut editor = build_editor(buffer.clone(), window, cx);
3340 let buffer = buffer.read(cx).as_singleton().unwrap();
3341
3342 assert_eq!(
3343 editor.selections.ranges::<Point>(cx),
3344 &[Point::new(0, 0)..Point::new(0, 0)]
3345 );
3346
3347 // When on single line, replace newline at end by space
3348 editor.join_lines(&JoinLines, window, cx);
3349 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3350 assert_eq!(
3351 editor.selections.ranges::<Point>(cx),
3352 &[Point::new(0, 3)..Point::new(0, 3)]
3353 );
3354
3355 // When multiple lines are selected, remove newlines that are spanned by the selection
3356 editor.change_selections(None, window, cx, |s| {
3357 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3358 });
3359 editor.join_lines(&JoinLines, window, cx);
3360 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3361 assert_eq!(
3362 editor.selections.ranges::<Point>(cx),
3363 &[Point::new(0, 11)..Point::new(0, 11)]
3364 );
3365
3366 // Undo should be transactional
3367 editor.undo(&Undo, window, cx);
3368 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3369 assert_eq!(
3370 editor.selections.ranges::<Point>(cx),
3371 &[Point::new(0, 5)..Point::new(2, 2)]
3372 );
3373
3374 // When joining an empty line don't insert a space
3375 editor.change_selections(None, window, cx, |s| {
3376 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3377 });
3378 editor.join_lines(&JoinLines, window, cx);
3379 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3380 assert_eq!(
3381 editor.selections.ranges::<Point>(cx),
3382 [Point::new(2, 3)..Point::new(2, 3)]
3383 );
3384
3385 // We can remove trailing newlines
3386 editor.join_lines(&JoinLines, window, cx);
3387 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3388 assert_eq!(
3389 editor.selections.ranges::<Point>(cx),
3390 [Point::new(2, 3)..Point::new(2, 3)]
3391 );
3392
3393 // We don't blow up on the last line
3394 editor.join_lines(&JoinLines, window, cx);
3395 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3396 assert_eq!(
3397 editor.selections.ranges::<Point>(cx),
3398 [Point::new(2, 3)..Point::new(2, 3)]
3399 );
3400
3401 // reset to test indentation
3402 editor.buffer.update(cx, |buffer, cx| {
3403 buffer.edit(
3404 [
3405 (Point::new(1, 0)..Point::new(1, 2), " "),
3406 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3407 ],
3408 None,
3409 cx,
3410 )
3411 });
3412
3413 // We remove any leading spaces
3414 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3415 editor.change_selections(None, window, cx, |s| {
3416 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3417 });
3418 editor.join_lines(&JoinLines, window, cx);
3419 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3420
3421 // We don't insert a space for a line containing only spaces
3422 editor.join_lines(&JoinLines, window, cx);
3423 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3424
3425 // We ignore any leading tabs
3426 editor.join_lines(&JoinLines, window, cx);
3427 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3428
3429 editor
3430 });
3431}
3432
3433#[gpui::test]
3434fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3435 init_test(cx, |_| {});
3436
3437 cx.add_window(|window, cx| {
3438 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3439 let mut editor = build_editor(buffer.clone(), window, cx);
3440 let buffer = buffer.read(cx).as_singleton().unwrap();
3441
3442 editor.change_selections(None, window, cx, |s| {
3443 s.select_ranges([
3444 Point::new(0, 2)..Point::new(1, 1),
3445 Point::new(1, 2)..Point::new(1, 2),
3446 Point::new(3, 1)..Point::new(3, 2),
3447 ])
3448 });
3449
3450 editor.join_lines(&JoinLines, window, cx);
3451 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3452
3453 assert_eq!(
3454 editor.selections.ranges::<Point>(cx),
3455 [
3456 Point::new(0, 7)..Point::new(0, 7),
3457 Point::new(1, 3)..Point::new(1, 3)
3458 ]
3459 );
3460 editor
3461 });
3462}
3463
3464#[gpui::test]
3465async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3466 init_test(cx, |_| {});
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 let diff_base = r#"
3471 Line 0
3472 Line 1
3473 Line 2
3474 Line 3
3475 "#
3476 .unindent();
3477
3478 cx.set_state(
3479 &r#"
3480 ˇLine 0
3481 Line 1
3482 Line 2
3483 Line 3
3484 "#
3485 .unindent(),
3486 );
3487
3488 cx.set_head_text(&diff_base);
3489 executor.run_until_parked();
3490
3491 // Join lines
3492 cx.update_editor(|editor, window, cx| {
3493 editor.join_lines(&JoinLines, window, cx);
3494 });
3495 executor.run_until_parked();
3496
3497 cx.assert_editor_state(
3498 &r#"
3499 Line 0ˇ Line 1
3500 Line 2
3501 Line 3
3502 "#
3503 .unindent(),
3504 );
3505 // Join again
3506 cx.update_editor(|editor, window, cx| {
3507 editor.join_lines(&JoinLines, window, cx);
3508 });
3509 executor.run_until_parked();
3510
3511 cx.assert_editor_state(
3512 &r#"
3513 Line 0 Line 1ˇ Line 2
3514 Line 3
3515 "#
3516 .unindent(),
3517 );
3518}
3519
3520#[gpui::test]
3521async fn test_custom_newlines_cause_no_false_positive_diffs(
3522 executor: BackgroundExecutor,
3523 cx: &mut TestAppContext,
3524) {
3525 init_test(cx, |_| {});
3526 let mut cx = EditorTestContext::new(cx).await;
3527 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3528 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3529 executor.run_until_parked();
3530
3531 cx.update_editor(|editor, window, cx| {
3532 let snapshot = editor.snapshot(window, cx);
3533 assert_eq!(
3534 snapshot
3535 .buffer_snapshot
3536 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3537 .collect::<Vec<_>>(),
3538 Vec::new(),
3539 "Should not have any diffs for files with custom newlines"
3540 );
3541 });
3542}
3543
3544#[gpui::test]
3545async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3546 init_test(cx, |_| {});
3547
3548 let mut cx = EditorTestContext::new(cx).await;
3549
3550 // Test sort_lines_case_insensitive()
3551 cx.set_state(indoc! {"
3552 «z
3553 y
3554 x
3555 Z
3556 Y
3557 Xˇ»
3558 "});
3559 cx.update_editor(|e, window, cx| {
3560 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3561 });
3562 cx.assert_editor_state(indoc! {"
3563 «x
3564 X
3565 y
3566 Y
3567 z
3568 Zˇ»
3569 "});
3570
3571 // Test reverse_lines()
3572 cx.set_state(indoc! {"
3573 «5
3574 4
3575 3
3576 2
3577 1ˇ»
3578 "});
3579 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 «1
3582 2
3583 3
3584 4
3585 5ˇ»
3586 "});
3587
3588 // Skip testing shuffle_line()
3589
3590 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3591 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3592
3593 // Don't manipulate when cursor is on single line, but expand the selection
3594 cx.set_state(indoc! {"
3595 ddˇdd
3596 ccc
3597 bb
3598 a
3599 "});
3600 cx.update_editor(|e, window, cx| {
3601 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3602 });
3603 cx.assert_editor_state(indoc! {"
3604 «ddddˇ»
3605 ccc
3606 bb
3607 a
3608 "});
3609
3610 // Basic manipulate case
3611 // Start selection moves to column 0
3612 // End of selection shrinks to fit shorter line
3613 cx.set_state(indoc! {"
3614 dd«d
3615 ccc
3616 bb
3617 aaaaaˇ»
3618 "});
3619 cx.update_editor(|e, window, cx| {
3620 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3621 });
3622 cx.assert_editor_state(indoc! {"
3623 «aaaaa
3624 bb
3625 ccc
3626 dddˇ»
3627 "});
3628
3629 // Manipulate case with newlines
3630 cx.set_state(indoc! {"
3631 dd«d
3632 ccc
3633
3634 bb
3635 aaaaa
3636
3637 ˇ»
3638 "});
3639 cx.update_editor(|e, window, cx| {
3640 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3641 });
3642 cx.assert_editor_state(indoc! {"
3643 «
3644
3645 aaaaa
3646 bb
3647 ccc
3648 dddˇ»
3649
3650 "});
3651
3652 // Adding new line
3653 cx.set_state(indoc! {"
3654 aa«a
3655 bbˇ»b
3656 "});
3657 cx.update_editor(|e, window, cx| {
3658 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3659 });
3660 cx.assert_editor_state(indoc! {"
3661 «aaa
3662 bbb
3663 added_lineˇ»
3664 "});
3665
3666 // Removing line
3667 cx.set_state(indoc! {"
3668 aa«a
3669 bbbˇ»
3670 "});
3671 cx.update_editor(|e, window, cx| {
3672 e.manipulate_lines(window, cx, |lines| {
3673 lines.pop();
3674 })
3675 });
3676 cx.assert_editor_state(indoc! {"
3677 «aaaˇ»
3678 "});
3679
3680 // Removing all lines
3681 cx.set_state(indoc! {"
3682 aa«a
3683 bbbˇ»
3684 "});
3685 cx.update_editor(|e, window, cx| {
3686 e.manipulate_lines(window, cx, |lines| {
3687 lines.drain(..);
3688 })
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 ˇ
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Consider continuous selection as single selection
3702 cx.set_state(indoc! {"
3703 Aaa«aa
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «Aaaaa
3713 ccc
3714 bb
3715 aaaaaˇ»
3716 "});
3717
3718 cx.set_state(indoc! {"
3719 Aaa«aa
3720 cˇ»c«c
3721 bb
3722 aaaˇ»aa
3723 "});
3724 cx.update_editor(|e, window, cx| {
3725 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3726 });
3727 cx.assert_editor_state(indoc! {"
3728 «Aaaaa
3729 ccc
3730 bbˇ»
3731 "});
3732
3733 // Consider non continuous selection as distinct dedup operations
3734 cx.set_state(indoc! {"
3735 «aaaaa
3736 bb
3737 aaaaa
3738 aaaaaˇ»
3739
3740 aaa«aaˇ»
3741 "});
3742 cx.update_editor(|e, window, cx| {
3743 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3744 });
3745 cx.assert_editor_state(indoc! {"
3746 «aaaaa
3747 bbˇ»
3748
3749 «aaaaaˇ»
3750 "});
3751}
3752
3753#[gpui::test]
3754async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3755 init_test(cx, |_| {});
3756
3757 let mut cx = EditorTestContext::new(cx).await;
3758
3759 cx.set_state(indoc! {"
3760 «Aaa
3761 aAa
3762 Aaaˇ»
3763 "});
3764 cx.update_editor(|e, window, cx| {
3765 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3766 });
3767 cx.assert_editor_state(indoc! {"
3768 «Aaa
3769 aAaˇ»
3770 "});
3771
3772 cx.set_state(indoc! {"
3773 «Aaa
3774 aAa
3775 aaAˇ»
3776 "});
3777 cx.update_editor(|e, window, cx| {
3778 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3779 });
3780 cx.assert_editor_state(indoc! {"
3781 «Aaaˇ»
3782 "});
3783}
3784
3785#[gpui::test]
3786async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3787 init_test(cx, |_| {});
3788
3789 let mut cx = EditorTestContext::new(cx).await;
3790
3791 // Manipulate with multiple selections on a single line
3792 cx.set_state(indoc! {"
3793 dd«dd
3794 cˇ»c«c
3795 bb
3796 aaaˇ»aa
3797 "});
3798 cx.update_editor(|e, window, cx| {
3799 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3800 });
3801 cx.assert_editor_state(indoc! {"
3802 «aaaaa
3803 bb
3804 ccc
3805 ddddˇ»
3806 "});
3807
3808 // Manipulate with multiple disjoin selections
3809 cx.set_state(indoc! {"
3810 5«
3811 4
3812 3
3813 2
3814 1ˇ»
3815
3816 dd«dd
3817 ccc
3818 bb
3819 aaaˇ»aa
3820 "});
3821 cx.update_editor(|e, window, cx| {
3822 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3823 });
3824 cx.assert_editor_state(indoc! {"
3825 «1
3826 2
3827 3
3828 4
3829 5ˇ»
3830
3831 «aaaaa
3832 bb
3833 ccc
3834 ddddˇ»
3835 "});
3836
3837 // Adding lines on each selection
3838 cx.set_state(indoc! {"
3839 2«
3840 1ˇ»
3841
3842 bb«bb
3843 aaaˇ»aa
3844 "});
3845 cx.update_editor(|e, window, cx| {
3846 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3847 });
3848 cx.assert_editor_state(indoc! {"
3849 «2
3850 1
3851 added lineˇ»
3852
3853 «bbbb
3854 aaaaa
3855 added lineˇ»
3856 "});
3857
3858 // Removing lines on each selection
3859 cx.set_state(indoc! {"
3860 2«
3861 1ˇ»
3862
3863 bb«bb
3864 aaaˇ»aa
3865 "});
3866 cx.update_editor(|e, window, cx| {
3867 e.manipulate_lines(window, cx, |lines| {
3868 lines.pop();
3869 })
3870 });
3871 cx.assert_editor_state(indoc! {"
3872 «2ˇ»
3873
3874 «bbbbˇ»
3875 "});
3876}
3877
3878#[gpui::test]
3879async fn test_manipulate_text(cx: &mut TestAppContext) {
3880 init_test(cx, |_| {});
3881
3882 let mut cx = EditorTestContext::new(cx).await;
3883
3884 // Test convert_to_upper_case()
3885 cx.set_state(indoc! {"
3886 «hello worldˇ»
3887 "});
3888 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 «HELLO WORLDˇ»
3891 "});
3892
3893 // Test convert_to_lower_case()
3894 cx.set_state(indoc! {"
3895 «HELLO WORLDˇ»
3896 "});
3897 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3898 cx.assert_editor_state(indoc! {"
3899 «hello worldˇ»
3900 "});
3901
3902 // Test multiple line, single selection case
3903 cx.set_state(indoc! {"
3904 «The quick brown
3905 fox jumps over
3906 the lazy dogˇ»
3907 "});
3908 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3909 cx.assert_editor_state(indoc! {"
3910 «The Quick Brown
3911 Fox Jumps Over
3912 The Lazy Dogˇ»
3913 "});
3914
3915 // Test multiple line, single selection case
3916 cx.set_state(indoc! {"
3917 «The quick brown
3918 fox jumps over
3919 the lazy dogˇ»
3920 "});
3921 cx.update_editor(|e, window, cx| {
3922 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3923 });
3924 cx.assert_editor_state(indoc! {"
3925 «TheQuickBrown
3926 FoxJumpsOver
3927 TheLazyDogˇ»
3928 "});
3929
3930 // From here on out, test more complex cases of manipulate_text()
3931
3932 // Test no selection case - should affect words cursors are in
3933 // Cursor at beginning, middle, and end of word
3934 cx.set_state(indoc! {"
3935 ˇhello big beauˇtiful worldˇ
3936 "});
3937 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3938 cx.assert_editor_state(indoc! {"
3939 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3940 "});
3941
3942 // Test multiple selections on a single line and across multiple lines
3943 cx.set_state(indoc! {"
3944 «Theˇ» quick «brown
3945 foxˇ» jumps «overˇ»
3946 the «lazyˇ» dog
3947 "});
3948 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 «THEˇ» quick «BROWN
3951 FOXˇ» jumps «OVERˇ»
3952 the «LAZYˇ» dog
3953 "});
3954
3955 // Test case where text length grows
3956 cx.set_state(indoc! {"
3957 «tschüߡ»
3958 "});
3959 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3960 cx.assert_editor_state(indoc! {"
3961 «TSCHÜSSˇ»
3962 "});
3963
3964 // Test to make sure we don't crash when text shrinks
3965 cx.set_state(indoc! {"
3966 aaa_bbbˇ
3967 "});
3968 cx.update_editor(|e, window, cx| {
3969 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3970 });
3971 cx.assert_editor_state(indoc! {"
3972 «aaaBbbˇ»
3973 "});
3974
3975 // Test to make sure we all aware of the fact that each word can grow and shrink
3976 // Final selections should be aware of this fact
3977 cx.set_state(indoc! {"
3978 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3979 "});
3980 cx.update_editor(|e, window, cx| {
3981 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3982 });
3983 cx.assert_editor_state(indoc! {"
3984 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3985 "});
3986
3987 cx.set_state(indoc! {"
3988 «hElLo, WoRld!ˇ»
3989 "});
3990 cx.update_editor(|e, window, cx| {
3991 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3992 });
3993 cx.assert_editor_state(indoc! {"
3994 «HeLlO, wOrLD!ˇ»
3995 "});
3996}
3997
3998#[gpui::test]
3999fn test_duplicate_line(cx: &mut TestAppContext) {
4000 init_test(cx, |_| {});
4001
4002 let editor = cx.add_window(|window, cx| {
4003 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4004 build_editor(buffer, window, cx)
4005 });
4006 _ = editor.update(cx, |editor, window, cx| {
4007 editor.change_selections(None, window, cx, |s| {
4008 s.select_display_ranges([
4009 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4010 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4011 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4012 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4013 ])
4014 });
4015 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4016 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4017 assert_eq!(
4018 editor.selections.display_ranges(cx),
4019 vec![
4020 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4021 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4022 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4023 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4024 ]
4025 );
4026 });
4027
4028 let editor = cx.add_window(|window, cx| {
4029 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4030 build_editor(buffer, window, cx)
4031 });
4032 _ = editor.update(cx, |editor, window, cx| {
4033 editor.change_selections(None, window, cx, |s| {
4034 s.select_display_ranges([
4035 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4036 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4037 ])
4038 });
4039 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4040 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4041 assert_eq!(
4042 editor.selections.display_ranges(cx),
4043 vec![
4044 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4045 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4046 ]
4047 );
4048 });
4049
4050 // With `move_upwards` the selections stay in place, except for
4051 // the lines inserted above them
4052 let editor = cx.add_window(|window, cx| {
4053 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4054 build_editor(buffer, window, cx)
4055 });
4056 _ = editor.update(cx, |editor, window, cx| {
4057 editor.change_selections(None, window, cx, |s| {
4058 s.select_display_ranges([
4059 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4060 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4061 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4062 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4063 ])
4064 });
4065 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4066 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4067 assert_eq!(
4068 editor.selections.display_ranges(cx),
4069 vec![
4070 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4071 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4072 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4073 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4074 ]
4075 );
4076 });
4077
4078 let editor = cx.add_window(|window, cx| {
4079 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4080 build_editor(buffer, window, cx)
4081 });
4082 _ = editor.update(cx, |editor, window, cx| {
4083 editor.change_selections(None, window, cx, |s| {
4084 s.select_display_ranges([
4085 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4086 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4087 ])
4088 });
4089 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4090 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4091 assert_eq!(
4092 editor.selections.display_ranges(cx),
4093 vec![
4094 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4095 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4096 ]
4097 );
4098 });
4099
4100 let editor = cx.add_window(|window, cx| {
4101 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4102 build_editor(buffer, window, cx)
4103 });
4104 _ = editor.update(cx, |editor, window, cx| {
4105 editor.change_selections(None, window, cx, |s| {
4106 s.select_display_ranges([
4107 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4108 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4109 ])
4110 });
4111 editor.duplicate_selection(&DuplicateSelection, window, cx);
4112 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4113 assert_eq!(
4114 editor.selections.display_ranges(cx),
4115 vec![
4116 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4117 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4118 ]
4119 );
4120 });
4121}
4122
4123#[gpui::test]
4124fn test_move_line_up_down(cx: &mut TestAppContext) {
4125 init_test(cx, |_| {});
4126
4127 let editor = cx.add_window(|window, cx| {
4128 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4129 build_editor(buffer, window, cx)
4130 });
4131 _ = editor.update(cx, |editor, window, cx| {
4132 editor.fold_creases(
4133 vec![
4134 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4135 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4136 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4137 ],
4138 true,
4139 window,
4140 cx,
4141 );
4142 editor.change_selections(None, window, cx, |s| {
4143 s.select_display_ranges([
4144 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4145 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4146 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4147 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4148 ])
4149 });
4150 assert_eq!(
4151 editor.display_text(cx),
4152 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4153 );
4154
4155 editor.move_line_up(&MoveLineUp, window, cx);
4156 assert_eq!(
4157 editor.display_text(cx),
4158 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4159 );
4160 assert_eq!(
4161 editor.selections.display_ranges(cx),
4162 vec![
4163 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4164 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4165 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4166 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4167 ]
4168 );
4169 });
4170
4171 _ = editor.update(cx, |editor, window, cx| {
4172 editor.move_line_down(&MoveLineDown, window, cx);
4173 assert_eq!(
4174 editor.display_text(cx),
4175 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4176 );
4177 assert_eq!(
4178 editor.selections.display_ranges(cx),
4179 vec![
4180 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4181 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4182 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4183 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4184 ]
4185 );
4186 });
4187
4188 _ = editor.update(cx, |editor, window, cx| {
4189 editor.move_line_down(&MoveLineDown, window, cx);
4190 assert_eq!(
4191 editor.display_text(cx),
4192 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4193 );
4194 assert_eq!(
4195 editor.selections.display_ranges(cx),
4196 vec![
4197 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4198 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4199 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4200 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4201 ]
4202 );
4203 });
4204
4205 _ = editor.update(cx, |editor, window, cx| {
4206 editor.move_line_up(&MoveLineUp, window, cx);
4207 assert_eq!(
4208 editor.display_text(cx),
4209 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4210 );
4211 assert_eq!(
4212 editor.selections.display_ranges(cx),
4213 vec![
4214 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4215 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4216 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4217 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4218 ]
4219 );
4220 });
4221}
4222
4223#[gpui::test]
4224fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4225 init_test(cx, |_| {});
4226
4227 let editor = cx.add_window(|window, cx| {
4228 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4229 build_editor(buffer, window, cx)
4230 });
4231 _ = editor.update(cx, |editor, window, cx| {
4232 let snapshot = editor.buffer.read(cx).snapshot(cx);
4233 editor.insert_blocks(
4234 [BlockProperties {
4235 style: BlockStyle::Fixed,
4236 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4237 height: Some(1),
4238 render: Arc::new(|_| div().into_any()),
4239 priority: 0,
4240 }],
4241 Some(Autoscroll::fit()),
4242 cx,
4243 );
4244 editor.change_selections(None, window, cx, |s| {
4245 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4246 });
4247 editor.move_line_down(&MoveLineDown, window, cx);
4248 });
4249}
4250
4251#[gpui::test]
4252async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4253 init_test(cx, |_| {});
4254
4255 let mut cx = EditorTestContext::new(cx).await;
4256 cx.set_state(
4257 &"
4258 ˇzero
4259 one
4260 two
4261 three
4262 four
4263 five
4264 "
4265 .unindent(),
4266 );
4267
4268 // Create a four-line block that replaces three lines of text.
4269 cx.update_editor(|editor, window, cx| {
4270 let snapshot = editor.snapshot(window, cx);
4271 let snapshot = &snapshot.buffer_snapshot;
4272 let placement = BlockPlacement::Replace(
4273 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4274 );
4275 editor.insert_blocks(
4276 [BlockProperties {
4277 placement,
4278 height: Some(4),
4279 style: BlockStyle::Sticky,
4280 render: Arc::new(|_| gpui::div().into_any_element()),
4281 priority: 0,
4282 }],
4283 None,
4284 cx,
4285 );
4286 });
4287
4288 // Move down so that the cursor touches the block.
4289 cx.update_editor(|editor, window, cx| {
4290 editor.move_down(&Default::default(), window, cx);
4291 });
4292 cx.assert_editor_state(
4293 &"
4294 zero
4295 «one
4296 two
4297 threeˇ»
4298 four
4299 five
4300 "
4301 .unindent(),
4302 );
4303
4304 // Move down past the block.
4305 cx.update_editor(|editor, window, cx| {
4306 editor.move_down(&Default::default(), window, cx);
4307 });
4308 cx.assert_editor_state(
4309 &"
4310 zero
4311 one
4312 two
4313 three
4314 ˇfour
4315 five
4316 "
4317 .unindent(),
4318 );
4319}
4320
4321#[gpui::test]
4322fn test_transpose(cx: &mut TestAppContext) {
4323 init_test(cx, |_| {});
4324
4325 _ = cx.add_window(|window, cx| {
4326 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4327 editor.set_style(EditorStyle::default(), window, cx);
4328 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4329 editor.transpose(&Default::default(), window, cx);
4330 assert_eq!(editor.text(cx), "bac");
4331 assert_eq!(editor.selections.ranges(cx), [2..2]);
4332
4333 editor.transpose(&Default::default(), window, cx);
4334 assert_eq!(editor.text(cx), "bca");
4335 assert_eq!(editor.selections.ranges(cx), [3..3]);
4336
4337 editor.transpose(&Default::default(), window, cx);
4338 assert_eq!(editor.text(cx), "bac");
4339 assert_eq!(editor.selections.ranges(cx), [3..3]);
4340
4341 editor
4342 });
4343
4344 _ = cx.add_window(|window, cx| {
4345 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4346 editor.set_style(EditorStyle::default(), window, cx);
4347 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4348 editor.transpose(&Default::default(), window, cx);
4349 assert_eq!(editor.text(cx), "acb\nde");
4350 assert_eq!(editor.selections.ranges(cx), [3..3]);
4351
4352 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4353 editor.transpose(&Default::default(), window, cx);
4354 assert_eq!(editor.text(cx), "acbd\ne");
4355 assert_eq!(editor.selections.ranges(cx), [5..5]);
4356
4357 editor.transpose(&Default::default(), window, cx);
4358 assert_eq!(editor.text(cx), "acbde\n");
4359 assert_eq!(editor.selections.ranges(cx), [6..6]);
4360
4361 editor.transpose(&Default::default(), window, cx);
4362 assert_eq!(editor.text(cx), "acbd\ne");
4363 assert_eq!(editor.selections.ranges(cx), [6..6]);
4364
4365 editor
4366 });
4367
4368 _ = cx.add_window(|window, cx| {
4369 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4370 editor.set_style(EditorStyle::default(), window, cx);
4371 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4372 editor.transpose(&Default::default(), window, cx);
4373 assert_eq!(editor.text(cx), "bacd\ne");
4374 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4375
4376 editor.transpose(&Default::default(), window, cx);
4377 assert_eq!(editor.text(cx), "bcade\n");
4378 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4379
4380 editor.transpose(&Default::default(), window, cx);
4381 assert_eq!(editor.text(cx), "bcda\ne");
4382 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4383
4384 editor.transpose(&Default::default(), window, cx);
4385 assert_eq!(editor.text(cx), "bcade\n");
4386 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4387
4388 editor.transpose(&Default::default(), window, cx);
4389 assert_eq!(editor.text(cx), "bcaed\n");
4390 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4391
4392 editor
4393 });
4394
4395 _ = cx.add_window(|window, cx| {
4396 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4397 editor.set_style(EditorStyle::default(), window, cx);
4398 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4399 editor.transpose(&Default::default(), window, cx);
4400 assert_eq!(editor.text(cx), "🏀🍐✋");
4401 assert_eq!(editor.selections.ranges(cx), [8..8]);
4402
4403 editor.transpose(&Default::default(), window, cx);
4404 assert_eq!(editor.text(cx), "🏀✋🍐");
4405 assert_eq!(editor.selections.ranges(cx), [11..11]);
4406
4407 editor.transpose(&Default::default(), window, cx);
4408 assert_eq!(editor.text(cx), "🏀🍐✋");
4409 assert_eq!(editor.selections.ranges(cx), [11..11]);
4410
4411 editor
4412 });
4413}
4414
4415#[gpui::test]
4416async fn test_rewrap(cx: &mut TestAppContext) {
4417 init_test(cx, |settings| {
4418 settings.languages.extend([
4419 (
4420 "Markdown".into(),
4421 LanguageSettingsContent {
4422 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4423 ..Default::default()
4424 },
4425 ),
4426 (
4427 "Plain Text".into(),
4428 LanguageSettingsContent {
4429 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4430 ..Default::default()
4431 },
4432 ),
4433 ])
4434 });
4435
4436 let mut cx = EditorTestContext::new(cx).await;
4437
4438 let language_with_c_comments = Arc::new(Language::new(
4439 LanguageConfig {
4440 line_comments: vec!["// ".into()],
4441 ..LanguageConfig::default()
4442 },
4443 None,
4444 ));
4445 let language_with_pound_comments = Arc::new(Language::new(
4446 LanguageConfig {
4447 line_comments: vec!["# ".into()],
4448 ..LanguageConfig::default()
4449 },
4450 None,
4451 ));
4452 let markdown_language = Arc::new(Language::new(
4453 LanguageConfig {
4454 name: "Markdown".into(),
4455 ..LanguageConfig::default()
4456 },
4457 None,
4458 ));
4459 let language_with_doc_comments = Arc::new(Language::new(
4460 LanguageConfig {
4461 line_comments: vec!["// ".into(), "/// ".into()],
4462 ..LanguageConfig::default()
4463 },
4464 Some(tree_sitter_rust::LANGUAGE.into()),
4465 ));
4466
4467 let plaintext_language = Arc::new(Language::new(
4468 LanguageConfig {
4469 name: "Plain Text".into(),
4470 ..LanguageConfig::default()
4471 },
4472 None,
4473 ));
4474
4475 assert_rewrap(
4476 indoc! {"
4477 // ˇ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.
4478 "},
4479 indoc! {"
4480 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4481 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4482 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4483 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4484 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4485 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4486 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4487 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4488 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4489 // porttitor id. Aliquam id accumsan eros.
4490 "},
4491 language_with_c_comments.clone(),
4492 &mut cx,
4493 );
4494
4495 // Test that rewrapping works inside of a selection
4496 assert_rewrap(
4497 indoc! {"
4498 «// 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.ˇ»
4499 "},
4500 indoc! {"
4501 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4502 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4503 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4504 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4505 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4506 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4507 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4508 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4509 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4510 // porttitor id. Aliquam id accumsan eros.ˇ»
4511 "},
4512 language_with_c_comments.clone(),
4513 &mut cx,
4514 );
4515
4516 // Test that cursors that expand to the same region are collapsed.
4517 assert_rewrap(
4518 indoc! {"
4519 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4520 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4521 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4522 // ˇ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.
4523 "},
4524 indoc! {"
4525 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4526 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4527 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4528 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4529 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4530 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4531 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4532 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4533 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4534 // porttitor id. Aliquam id accumsan eros.
4535 "},
4536 language_with_c_comments.clone(),
4537 &mut cx,
4538 );
4539
4540 // Test that non-contiguous selections are treated separately.
4541 assert_rewrap(
4542 indoc! {"
4543 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4544 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4545 //
4546 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4547 // ˇ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.
4548 "},
4549 indoc! {"
4550 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4551 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4552 // auctor, eu lacinia sapien scelerisque.
4553 //
4554 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4555 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4556 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4557 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4558 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4559 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4560 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4561 "},
4562 language_with_c_comments.clone(),
4563 &mut cx,
4564 );
4565
4566 // Test that different comment prefixes are supported.
4567 assert_rewrap(
4568 indoc! {"
4569 # ˇ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.
4570 "},
4571 indoc! {"
4572 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4573 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4574 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4575 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4576 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4577 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4578 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4579 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4580 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4581 # accumsan eros.
4582 "},
4583 language_with_pound_comments.clone(),
4584 &mut cx,
4585 );
4586
4587 // Test that rewrapping is ignored outside of comments in most languages.
4588 assert_rewrap(
4589 indoc! {"
4590 /// Adds two numbers.
4591 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4592 fn add(a: u32, b: u32) -> u32 {
4593 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ˇ
4594 }
4595 "},
4596 indoc! {"
4597 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4598 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4599 fn add(a: u32, b: u32) -> u32 {
4600 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ˇ
4601 }
4602 "},
4603 language_with_doc_comments.clone(),
4604 &mut cx,
4605 );
4606
4607 // Test that rewrapping works in Markdown and Plain Text languages.
4608 assert_rewrap(
4609 indoc! {"
4610 # Hello
4611
4612 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.
4613 "},
4614 indoc! {"
4615 # Hello
4616
4617 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4618 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4619 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4620 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4621 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4622 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4623 Integer sit amet scelerisque nisi.
4624 "},
4625 markdown_language,
4626 &mut cx,
4627 );
4628
4629 assert_rewrap(
4630 indoc! {"
4631 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.
4632 "},
4633 indoc! {"
4634 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4635 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4636 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4637 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4638 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4639 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4640 Integer sit amet scelerisque nisi.
4641 "},
4642 plaintext_language,
4643 &mut cx,
4644 );
4645
4646 // Test rewrapping unaligned comments in a selection.
4647 assert_rewrap(
4648 indoc! {"
4649 fn foo() {
4650 if true {
4651 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4652 // Praesent semper egestas tellus id dignissim.ˇ»
4653 do_something();
4654 } else {
4655 //
4656 }
4657 }
4658 "},
4659 indoc! {"
4660 fn foo() {
4661 if true {
4662 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4663 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4664 // egestas tellus id dignissim.ˇ»
4665 do_something();
4666 } else {
4667 //
4668 }
4669 }
4670 "},
4671 language_with_doc_comments.clone(),
4672 &mut cx,
4673 );
4674
4675 assert_rewrap(
4676 indoc! {"
4677 fn foo() {
4678 if true {
4679 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4680 // Praesent semper egestas tellus id dignissim.»
4681 do_something();
4682 } else {
4683 //
4684 }
4685
4686 }
4687 "},
4688 indoc! {"
4689 fn foo() {
4690 if true {
4691 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4692 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4693 // egestas tellus id dignissim.»
4694 do_something();
4695 } else {
4696 //
4697 }
4698
4699 }
4700 "},
4701 language_with_doc_comments.clone(),
4702 &mut cx,
4703 );
4704
4705 #[track_caller]
4706 fn assert_rewrap(
4707 unwrapped_text: &str,
4708 wrapped_text: &str,
4709 language: Arc<Language>,
4710 cx: &mut EditorTestContext,
4711 ) {
4712 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4713 cx.set_state(unwrapped_text);
4714 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4715 cx.assert_editor_state(wrapped_text);
4716 }
4717}
4718
4719#[gpui::test]
4720async fn test_hard_wrap(cx: &mut TestAppContext) {
4721 init_test(cx, |_| {});
4722 let mut cx = EditorTestContext::new(cx).await;
4723
4724 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4725 cx.update_editor(|editor, _, cx| {
4726 editor.set_hard_wrap(Some(14), cx);
4727 });
4728
4729 cx.set_state(indoc!(
4730 "
4731 one two three ˇ
4732 "
4733 ));
4734 cx.simulate_input("four");
4735 cx.run_until_parked();
4736
4737 cx.assert_editor_state(indoc!(
4738 "
4739 one two three
4740 fourˇ
4741 "
4742 ));
4743
4744 cx.update_editor(|editor, window, cx| {
4745 editor.newline(&Default::default(), window, cx);
4746 });
4747 cx.run_until_parked();
4748 cx.assert_editor_state(indoc!(
4749 "
4750 one two three
4751 four
4752 ˇ
4753 "
4754 ));
4755
4756 cx.simulate_input("five");
4757 cx.run_until_parked();
4758 cx.assert_editor_state(indoc!(
4759 "
4760 one two three
4761 four
4762 fiveˇ
4763 "
4764 ));
4765
4766 cx.update_editor(|editor, window, cx| {
4767 editor.newline(&Default::default(), window, cx);
4768 });
4769 cx.run_until_parked();
4770 cx.simulate_input("# ");
4771 cx.run_until_parked();
4772 cx.assert_editor_state(indoc!(
4773 "
4774 one two three
4775 four
4776 five
4777 # ˇ
4778 "
4779 ));
4780
4781 cx.update_editor(|editor, window, cx| {
4782 editor.newline(&Default::default(), window, cx);
4783 });
4784 cx.run_until_parked();
4785 cx.assert_editor_state(indoc!(
4786 "
4787 one two three
4788 four
4789 five
4790 #\x20
4791 #ˇ
4792 "
4793 ));
4794
4795 cx.simulate_input(" 6");
4796 cx.run_until_parked();
4797 cx.assert_editor_state(indoc!(
4798 "
4799 one two three
4800 four
4801 five
4802 #
4803 # 6ˇ
4804 "
4805 ));
4806}
4807
4808#[gpui::test]
4809async fn test_clipboard(cx: &mut TestAppContext) {
4810 init_test(cx, |_| {});
4811
4812 let mut cx = EditorTestContext::new(cx).await;
4813
4814 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4815 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4816 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4817
4818 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4819 cx.set_state("two ˇfour ˇsix ˇ");
4820 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4821 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4822
4823 // Paste again but with only two cursors. Since the number of cursors doesn't
4824 // match the number of slices in the clipboard, the entire clipboard text
4825 // is pasted at each cursor.
4826 cx.set_state("ˇtwo one✅ four three six five ˇ");
4827 cx.update_editor(|e, window, cx| {
4828 e.handle_input("( ", window, cx);
4829 e.paste(&Paste, window, cx);
4830 e.handle_input(") ", window, cx);
4831 });
4832 cx.assert_editor_state(
4833 &([
4834 "( one✅ ",
4835 "three ",
4836 "five ) ˇtwo one✅ four three six five ( one✅ ",
4837 "three ",
4838 "five ) ˇ",
4839 ]
4840 .join("\n")),
4841 );
4842
4843 // Cut with three selections, one of which is full-line.
4844 cx.set_state(indoc! {"
4845 1«2ˇ»3
4846 4ˇ567
4847 «8ˇ»9"});
4848 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4849 cx.assert_editor_state(indoc! {"
4850 1ˇ3
4851 ˇ9"});
4852
4853 // Paste with three selections, noticing how the copied selection that was full-line
4854 // gets inserted before the second cursor.
4855 cx.set_state(indoc! {"
4856 1ˇ3
4857 9ˇ
4858 «oˇ»ne"});
4859 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4860 cx.assert_editor_state(indoc! {"
4861 12ˇ3
4862 4567
4863 9ˇ
4864 8ˇne"});
4865
4866 // Copy with a single cursor only, which writes the whole line into the clipboard.
4867 cx.set_state(indoc! {"
4868 The quick brown
4869 fox juˇmps over
4870 the lazy dog"});
4871 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4872 assert_eq!(
4873 cx.read_from_clipboard()
4874 .and_then(|item| item.text().as_deref().map(str::to_string)),
4875 Some("fox jumps over\n".to_string())
4876 );
4877
4878 // Paste with three selections, noticing how the copied full-line selection is inserted
4879 // before the empty selections but replaces the selection that is non-empty.
4880 cx.set_state(indoc! {"
4881 Tˇhe quick brown
4882 «foˇ»x jumps over
4883 tˇhe lazy dog"});
4884 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4885 cx.assert_editor_state(indoc! {"
4886 fox jumps over
4887 Tˇhe quick brown
4888 fox jumps over
4889 ˇx jumps over
4890 fox jumps over
4891 tˇhe lazy dog"});
4892}
4893
4894#[gpui::test]
4895async fn test_copy_trim(cx: &mut TestAppContext) {
4896 init_test(cx, |_| {});
4897
4898 let mut cx = EditorTestContext::new(cx).await;
4899 cx.set_state(
4900 r#" «for selection in selections.iter() {
4901 let mut start = selection.start;
4902 let mut end = selection.end;
4903 let is_entire_line = selection.is_empty();
4904 if is_entire_line {
4905 start = Point::new(start.row, 0);ˇ»
4906 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4907 }
4908 "#,
4909 );
4910 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4911 assert_eq!(
4912 cx.read_from_clipboard()
4913 .and_then(|item| item.text().as_deref().map(str::to_string)),
4914 Some(
4915 "for selection in selections.iter() {
4916 let mut start = selection.start;
4917 let mut end = selection.end;
4918 let is_entire_line = selection.is_empty();
4919 if is_entire_line {
4920 start = Point::new(start.row, 0);"
4921 .to_string()
4922 ),
4923 "Regular copying preserves all indentation selected",
4924 );
4925 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4926 assert_eq!(
4927 cx.read_from_clipboard()
4928 .and_then(|item| item.text().as_deref().map(str::to_string)),
4929 Some(
4930 "for selection in selections.iter() {
4931let mut start = selection.start;
4932let mut end = selection.end;
4933let is_entire_line = selection.is_empty();
4934if is_entire_line {
4935 start = Point::new(start.row, 0);"
4936 .to_string()
4937 ),
4938 "Copying with stripping should strip all leading whitespaces"
4939 );
4940
4941 cx.set_state(
4942 r#" « for selection in selections.iter() {
4943 let mut start = selection.start;
4944 let mut end = selection.end;
4945 let is_entire_line = selection.is_empty();
4946 if is_entire_line {
4947 start = Point::new(start.row, 0);ˇ»
4948 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4949 }
4950 "#,
4951 );
4952 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4953 assert_eq!(
4954 cx.read_from_clipboard()
4955 .and_then(|item| item.text().as_deref().map(str::to_string)),
4956 Some(
4957 " for selection in selections.iter() {
4958 let mut start = selection.start;
4959 let mut end = selection.end;
4960 let is_entire_line = selection.is_empty();
4961 if is_entire_line {
4962 start = Point::new(start.row, 0);"
4963 .to_string()
4964 ),
4965 "Regular copying preserves all indentation selected",
4966 );
4967 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4968 assert_eq!(
4969 cx.read_from_clipboard()
4970 .and_then(|item| item.text().as_deref().map(str::to_string)),
4971 Some(
4972 "for selection in selections.iter() {
4973let mut start = selection.start;
4974let mut end = selection.end;
4975let is_entire_line = selection.is_empty();
4976if is_entire_line {
4977 start = Point::new(start.row, 0);"
4978 .to_string()
4979 ),
4980 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
4981 );
4982
4983 cx.set_state(
4984 r#" «ˇ for selection in selections.iter() {
4985 let mut start = selection.start;
4986 let mut end = selection.end;
4987 let is_entire_line = selection.is_empty();
4988 if is_entire_line {
4989 start = Point::new(start.row, 0);»
4990 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4991 }
4992 "#,
4993 );
4994 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4995 assert_eq!(
4996 cx.read_from_clipboard()
4997 .and_then(|item| item.text().as_deref().map(str::to_string)),
4998 Some(
4999 " for selection in selections.iter() {
5000 let mut start = selection.start;
5001 let mut end = selection.end;
5002 let is_entire_line = selection.is_empty();
5003 if is_entire_line {
5004 start = Point::new(start.row, 0);"
5005 .to_string()
5006 ),
5007 "Regular copying for reverse selection works the same",
5008 );
5009 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5010 assert_eq!(
5011 cx.read_from_clipboard()
5012 .and_then(|item| item.text().as_deref().map(str::to_string)),
5013 Some(
5014 "for selection in selections.iter() {
5015let mut start = selection.start;
5016let mut end = selection.end;
5017let is_entire_line = selection.is_empty();
5018if is_entire_line {
5019 start = Point::new(start.row, 0);"
5020 .to_string()
5021 ),
5022 "Copying with stripping for reverse selection works the same"
5023 );
5024
5025 cx.set_state(
5026 r#" for selection «in selections.iter() {
5027 let mut start = selection.start;
5028 let mut end = selection.end;
5029 let is_entire_line = selection.is_empty();
5030 if is_entire_line {
5031 start = Point::new(start.row, 0);ˇ»
5032 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5033 }
5034 "#,
5035 );
5036 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5037 assert_eq!(
5038 cx.read_from_clipboard()
5039 .and_then(|item| item.text().as_deref().map(str::to_string)),
5040 Some(
5041 "in selections.iter() {
5042 let mut start = selection.start;
5043 let mut end = selection.end;
5044 let is_entire_line = selection.is_empty();
5045 if is_entire_line {
5046 start = Point::new(start.row, 0);"
5047 .to_string()
5048 ),
5049 "When selecting past the indent, the copying works as usual",
5050 );
5051 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5052 assert_eq!(
5053 cx.read_from_clipboard()
5054 .and_then(|item| item.text().as_deref().map(str::to_string)),
5055 Some(
5056 "in selections.iter() {
5057 let mut start = selection.start;
5058 let mut end = selection.end;
5059 let is_entire_line = selection.is_empty();
5060 if is_entire_line {
5061 start = Point::new(start.row, 0);"
5062 .to_string()
5063 ),
5064 "When selecting past the indent, nothing is trimmed"
5065 );
5066}
5067
5068#[gpui::test]
5069async fn test_paste_multiline(cx: &mut TestAppContext) {
5070 init_test(cx, |_| {});
5071
5072 let mut cx = EditorTestContext::new(cx).await;
5073 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5074
5075 // Cut an indented block, without the leading whitespace.
5076 cx.set_state(indoc! {"
5077 const a: B = (
5078 c(),
5079 «d(
5080 e,
5081 f
5082 )ˇ»
5083 );
5084 "});
5085 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5086 cx.assert_editor_state(indoc! {"
5087 const a: B = (
5088 c(),
5089 ˇ
5090 );
5091 "});
5092
5093 // Paste it at the same position.
5094 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5095 cx.assert_editor_state(indoc! {"
5096 const a: B = (
5097 c(),
5098 d(
5099 e,
5100 f
5101 )ˇ
5102 );
5103 "});
5104
5105 // Paste it at a line with a lower indent level.
5106 cx.set_state(indoc! {"
5107 ˇ
5108 const a: B = (
5109 c(),
5110 );
5111 "});
5112 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5113 cx.assert_editor_state(indoc! {"
5114 d(
5115 e,
5116 f
5117 )ˇ
5118 const a: B = (
5119 c(),
5120 );
5121 "});
5122
5123 // Cut an indented block, with the leading whitespace.
5124 cx.set_state(indoc! {"
5125 const a: B = (
5126 c(),
5127 « d(
5128 e,
5129 f
5130 )
5131 ˇ»);
5132 "});
5133 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5134 cx.assert_editor_state(indoc! {"
5135 const a: B = (
5136 c(),
5137 ˇ);
5138 "});
5139
5140 // Paste it at the same position.
5141 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5142 cx.assert_editor_state(indoc! {"
5143 const a: B = (
5144 c(),
5145 d(
5146 e,
5147 f
5148 )
5149 ˇ);
5150 "});
5151
5152 // Paste it at a line with a higher indent level.
5153 cx.set_state(indoc! {"
5154 const a: B = (
5155 c(),
5156 d(
5157 e,
5158 fˇ
5159 )
5160 );
5161 "});
5162 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5163 cx.assert_editor_state(indoc! {"
5164 const a: B = (
5165 c(),
5166 d(
5167 e,
5168 f d(
5169 e,
5170 f
5171 )
5172 ˇ
5173 )
5174 );
5175 "});
5176
5177 // Copy an indented block, starting mid-line
5178 cx.set_state(indoc! {"
5179 const a: B = (
5180 c(),
5181 somethin«g(
5182 e,
5183 f
5184 )ˇ»
5185 );
5186 "});
5187 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5188
5189 // Paste it on a line with a lower indent level
5190 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5191 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5192 cx.assert_editor_state(indoc! {"
5193 const a: B = (
5194 c(),
5195 something(
5196 e,
5197 f
5198 )
5199 );
5200 g(
5201 e,
5202 f
5203 )ˇ"});
5204}
5205
5206#[gpui::test]
5207async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5208 init_test(cx, |_| {});
5209
5210 cx.write_to_clipboard(ClipboardItem::new_string(
5211 " d(\n e\n );\n".into(),
5212 ));
5213
5214 let mut cx = EditorTestContext::new(cx).await;
5215 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5216
5217 cx.set_state(indoc! {"
5218 fn a() {
5219 b();
5220 if c() {
5221 ˇ
5222 }
5223 }
5224 "});
5225
5226 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5227 cx.assert_editor_state(indoc! {"
5228 fn a() {
5229 b();
5230 if c() {
5231 d(
5232 e
5233 );
5234 ˇ
5235 }
5236 }
5237 "});
5238
5239 cx.set_state(indoc! {"
5240 fn a() {
5241 b();
5242 ˇ
5243 }
5244 "});
5245
5246 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5247 cx.assert_editor_state(indoc! {"
5248 fn a() {
5249 b();
5250 d(
5251 e
5252 );
5253 ˇ
5254 }
5255 "});
5256}
5257
5258#[gpui::test]
5259fn test_select_all(cx: &mut TestAppContext) {
5260 init_test(cx, |_| {});
5261
5262 let editor = cx.add_window(|window, cx| {
5263 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5264 build_editor(buffer, window, cx)
5265 });
5266 _ = editor.update(cx, |editor, window, cx| {
5267 editor.select_all(&SelectAll, window, cx);
5268 assert_eq!(
5269 editor.selections.display_ranges(cx),
5270 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5271 );
5272 });
5273}
5274
5275#[gpui::test]
5276fn test_select_line(cx: &mut TestAppContext) {
5277 init_test(cx, |_| {});
5278
5279 let editor = cx.add_window(|window, cx| {
5280 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5281 build_editor(buffer, window, cx)
5282 });
5283 _ = editor.update(cx, |editor, window, cx| {
5284 editor.change_selections(None, window, cx, |s| {
5285 s.select_display_ranges([
5286 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5287 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5288 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5289 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5290 ])
5291 });
5292 editor.select_line(&SelectLine, window, cx);
5293 assert_eq!(
5294 editor.selections.display_ranges(cx),
5295 vec![
5296 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5297 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5298 ]
5299 );
5300 });
5301
5302 _ = editor.update(cx, |editor, window, cx| {
5303 editor.select_line(&SelectLine, window, cx);
5304 assert_eq!(
5305 editor.selections.display_ranges(cx),
5306 vec![
5307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5308 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5309 ]
5310 );
5311 });
5312
5313 _ = editor.update(cx, |editor, window, cx| {
5314 editor.select_line(&SelectLine, window, cx);
5315 assert_eq!(
5316 editor.selections.display_ranges(cx),
5317 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5318 );
5319 });
5320}
5321
5322#[gpui::test]
5323async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5324 init_test(cx, |_| {});
5325 let mut cx = EditorTestContext::new(cx).await;
5326
5327 #[track_caller]
5328 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5329 cx.set_state(initial_state);
5330 cx.update_editor(|e, window, cx| {
5331 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5332 });
5333 cx.assert_editor_state(expected_state);
5334 }
5335
5336 // Selection starts and ends at the middle of lines, left-to-right
5337 test(
5338 &mut cx,
5339 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5340 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5341 );
5342 // Same thing, right-to-left
5343 test(
5344 &mut cx,
5345 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5346 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5347 );
5348
5349 // Whole buffer, left-to-right, last line *doesn't* end with newline
5350 test(
5351 &mut cx,
5352 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5353 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5354 );
5355 // Same thing, right-to-left
5356 test(
5357 &mut cx,
5358 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5359 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5360 );
5361
5362 // Whole buffer, left-to-right, last line ends with newline
5363 test(
5364 &mut cx,
5365 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5366 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5367 );
5368 // Same thing, right-to-left
5369 test(
5370 &mut cx,
5371 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5372 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5373 );
5374
5375 // Starts at the end of a line, ends at the start of another
5376 test(
5377 &mut cx,
5378 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5379 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5380 );
5381}
5382
5383#[gpui::test]
5384async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5385 init_test(cx, |_| {});
5386
5387 let editor = cx.add_window(|window, cx| {
5388 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5389 build_editor(buffer, window, cx)
5390 });
5391
5392 // setup
5393 _ = editor.update(cx, |editor, window, cx| {
5394 editor.fold_creases(
5395 vec![
5396 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5397 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5398 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5399 ],
5400 true,
5401 window,
5402 cx,
5403 );
5404 assert_eq!(
5405 editor.display_text(cx),
5406 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5407 );
5408 });
5409
5410 _ = editor.update(cx, |editor, window, cx| {
5411 editor.change_selections(None, window, cx, |s| {
5412 s.select_display_ranges([
5413 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5414 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5415 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5416 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5417 ])
5418 });
5419 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5420 assert_eq!(
5421 editor.display_text(cx),
5422 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5423 );
5424 });
5425 EditorTestContext::for_editor(editor, cx)
5426 .await
5427 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5428
5429 _ = editor.update(cx, |editor, window, cx| {
5430 editor.change_selections(None, window, cx, |s| {
5431 s.select_display_ranges([
5432 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5433 ])
5434 });
5435 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5436 assert_eq!(
5437 editor.display_text(cx),
5438 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5439 );
5440 assert_eq!(
5441 editor.selections.display_ranges(cx),
5442 [
5443 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5444 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5445 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5446 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5447 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5448 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5449 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5450 ]
5451 );
5452 });
5453 EditorTestContext::for_editor(editor, cx)
5454 .await
5455 .assert_editor_state(
5456 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5457 );
5458}
5459
5460#[gpui::test]
5461async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5462 init_test(cx, |_| {});
5463
5464 let mut cx = EditorTestContext::new(cx).await;
5465
5466 cx.set_state(indoc!(
5467 r#"abc
5468 defˇghi
5469
5470 jk
5471 nlmo
5472 "#
5473 ));
5474
5475 cx.update_editor(|editor, window, cx| {
5476 editor.add_selection_above(&Default::default(), window, cx);
5477 });
5478
5479 cx.assert_editor_state(indoc!(
5480 r#"abcˇ
5481 defˇghi
5482
5483 jk
5484 nlmo
5485 "#
5486 ));
5487
5488 cx.update_editor(|editor, window, cx| {
5489 editor.add_selection_above(&Default::default(), window, cx);
5490 });
5491
5492 cx.assert_editor_state(indoc!(
5493 r#"abcˇ
5494 defˇghi
5495
5496 jk
5497 nlmo
5498 "#
5499 ));
5500
5501 cx.update_editor(|editor, window, cx| {
5502 editor.add_selection_below(&Default::default(), window, cx);
5503 });
5504
5505 cx.assert_editor_state(indoc!(
5506 r#"abc
5507 defˇghi
5508
5509 jk
5510 nlmo
5511 "#
5512 ));
5513
5514 cx.update_editor(|editor, window, cx| {
5515 editor.undo_selection(&Default::default(), window, cx);
5516 });
5517
5518 cx.assert_editor_state(indoc!(
5519 r#"abcˇ
5520 defˇghi
5521
5522 jk
5523 nlmo
5524 "#
5525 ));
5526
5527 cx.update_editor(|editor, window, cx| {
5528 editor.redo_selection(&Default::default(), window, cx);
5529 });
5530
5531 cx.assert_editor_state(indoc!(
5532 r#"abc
5533 defˇghi
5534
5535 jk
5536 nlmo
5537 "#
5538 ));
5539
5540 cx.update_editor(|editor, window, cx| {
5541 editor.add_selection_below(&Default::default(), window, cx);
5542 });
5543
5544 cx.assert_editor_state(indoc!(
5545 r#"abc
5546 defˇghi
5547
5548 jk
5549 nlmˇo
5550 "#
5551 ));
5552
5553 cx.update_editor(|editor, window, cx| {
5554 editor.add_selection_below(&Default::default(), window, cx);
5555 });
5556
5557 cx.assert_editor_state(indoc!(
5558 r#"abc
5559 defˇghi
5560
5561 jk
5562 nlmˇo
5563 "#
5564 ));
5565
5566 // change selections
5567 cx.set_state(indoc!(
5568 r#"abc
5569 def«ˇg»hi
5570
5571 jk
5572 nlmo
5573 "#
5574 ));
5575
5576 cx.update_editor(|editor, window, cx| {
5577 editor.add_selection_below(&Default::default(), window, cx);
5578 });
5579
5580 cx.assert_editor_state(indoc!(
5581 r#"abc
5582 def«ˇg»hi
5583
5584 jk
5585 nlm«ˇo»
5586 "#
5587 ));
5588
5589 cx.update_editor(|editor, window, cx| {
5590 editor.add_selection_below(&Default::default(), window, cx);
5591 });
5592
5593 cx.assert_editor_state(indoc!(
5594 r#"abc
5595 def«ˇg»hi
5596
5597 jk
5598 nlm«ˇo»
5599 "#
5600 ));
5601
5602 cx.update_editor(|editor, window, cx| {
5603 editor.add_selection_above(&Default::default(), window, cx);
5604 });
5605
5606 cx.assert_editor_state(indoc!(
5607 r#"abc
5608 def«ˇg»hi
5609
5610 jk
5611 nlmo
5612 "#
5613 ));
5614
5615 cx.update_editor(|editor, window, cx| {
5616 editor.add_selection_above(&Default::default(), window, cx);
5617 });
5618
5619 cx.assert_editor_state(indoc!(
5620 r#"abc
5621 def«ˇg»hi
5622
5623 jk
5624 nlmo
5625 "#
5626 ));
5627
5628 // Change selections again
5629 cx.set_state(indoc!(
5630 r#"a«bc
5631 defgˇ»hi
5632
5633 jk
5634 nlmo
5635 "#
5636 ));
5637
5638 cx.update_editor(|editor, window, cx| {
5639 editor.add_selection_below(&Default::default(), window, cx);
5640 });
5641
5642 cx.assert_editor_state(indoc!(
5643 r#"a«bcˇ»
5644 d«efgˇ»hi
5645
5646 j«kˇ»
5647 nlmo
5648 "#
5649 ));
5650
5651 cx.update_editor(|editor, window, cx| {
5652 editor.add_selection_below(&Default::default(), window, cx);
5653 });
5654 cx.assert_editor_state(indoc!(
5655 r#"a«bcˇ»
5656 d«efgˇ»hi
5657
5658 j«kˇ»
5659 n«lmoˇ»
5660 "#
5661 ));
5662 cx.update_editor(|editor, window, cx| {
5663 editor.add_selection_above(&Default::default(), window, cx);
5664 });
5665
5666 cx.assert_editor_state(indoc!(
5667 r#"a«bcˇ»
5668 d«efgˇ»hi
5669
5670 j«kˇ»
5671 nlmo
5672 "#
5673 ));
5674
5675 // Change selections again
5676 cx.set_state(indoc!(
5677 r#"abc
5678 d«ˇefghi
5679
5680 jk
5681 nlm»o
5682 "#
5683 ));
5684
5685 cx.update_editor(|editor, window, cx| {
5686 editor.add_selection_above(&Default::default(), window, cx);
5687 });
5688
5689 cx.assert_editor_state(indoc!(
5690 r#"a«ˇbc»
5691 d«ˇef»ghi
5692
5693 j«ˇk»
5694 n«ˇlm»o
5695 "#
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.add_selection_below(&Default::default(), window, cx);
5700 });
5701
5702 cx.assert_editor_state(indoc!(
5703 r#"abc
5704 d«ˇef»ghi
5705
5706 j«ˇk»
5707 n«ˇlm»o
5708 "#
5709 ));
5710}
5711
5712#[gpui::test]
5713async fn test_select_next(cx: &mut TestAppContext) {
5714 init_test(cx, |_| {});
5715
5716 let mut cx = EditorTestContext::new(cx).await;
5717 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5718
5719 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5720 .unwrap();
5721 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5722
5723 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5724 .unwrap();
5725 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5726
5727 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5728 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5729
5730 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5731 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5732
5733 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5734 .unwrap();
5735 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5736
5737 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5738 .unwrap();
5739 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5740}
5741
5742#[gpui::test]
5743async fn test_select_all_matches(cx: &mut TestAppContext) {
5744 init_test(cx, |_| {});
5745
5746 let mut cx = EditorTestContext::new(cx).await;
5747
5748 // Test caret-only selections
5749 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5750 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5751 .unwrap();
5752 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5753
5754 // Test left-to-right selections
5755 cx.set_state("abc\n«abcˇ»\nabc");
5756 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5757 .unwrap();
5758 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5759
5760 // Test right-to-left selections
5761 cx.set_state("abc\n«ˇabc»\nabc");
5762 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5763 .unwrap();
5764 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5765
5766 // Test selecting whitespace with caret selection
5767 cx.set_state("abc\nˇ abc\nabc");
5768 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5769 .unwrap();
5770 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5771
5772 // Test selecting whitespace with left-to-right selection
5773 cx.set_state("abc\n«ˇ »abc\nabc");
5774 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5775 .unwrap();
5776 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5777
5778 // Test no matches with right-to-left selection
5779 cx.set_state("abc\n« ˇ»abc\nabc");
5780 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5781 .unwrap();
5782 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5783}
5784
5785#[gpui::test]
5786async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5787 init_test(cx, |_| {});
5788
5789 let mut cx = EditorTestContext::new(cx).await;
5790 cx.set_state(
5791 r#"let foo = 2;
5792lˇet foo = 2;
5793let fooˇ = 2;
5794let foo = 2;
5795let foo = ˇ2;"#,
5796 );
5797
5798 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5799 .unwrap();
5800 cx.assert_editor_state(
5801 r#"let foo = 2;
5802«letˇ» foo = 2;
5803let «fooˇ» = 2;
5804let foo = 2;
5805let foo = «2ˇ»;"#,
5806 );
5807
5808 // noop for multiple selections with different contents
5809 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5810 .unwrap();
5811 cx.assert_editor_state(
5812 r#"let foo = 2;
5813«letˇ» foo = 2;
5814let «fooˇ» = 2;
5815let foo = 2;
5816let foo = «2ˇ»;"#,
5817 );
5818}
5819
5820#[gpui::test]
5821async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5822 init_test(cx, |_| {});
5823
5824 let mut cx =
5825 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5826
5827 cx.assert_editor_state(indoc! {"
5828 ˇbbb
5829 ccc
5830
5831 bbb
5832 ccc
5833 "});
5834 cx.dispatch_action(SelectPrevious::default());
5835 cx.assert_editor_state(indoc! {"
5836 «bbbˇ»
5837 ccc
5838
5839 bbb
5840 ccc
5841 "});
5842 cx.dispatch_action(SelectPrevious::default());
5843 cx.assert_editor_state(indoc! {"
5844 «bbbˇ»
5845 ccc
5846
5847 «bbbˇ»
5848 ccc
5849 "});
5850}
5851
5852#[gpui::test]
5853async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5854 init_test(cx, |_| {});
5855
5856 let mut cx = EditorTestContext::new(cx).await;
5857 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5858
5859 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5860 .unwrap();
5861 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5862
5863 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5864 .unwrap();
5865 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5866
5867 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5868 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5869
5870 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5871 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5872
5873 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5874 .unwrap();
5875 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5876
5877 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5878 .unwrap();
5879 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5880
5881 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5882 .unwrap();
5883 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5884}
5885
5886#[gpui::test]
5887async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5888 init_test(cx, |_| {});
5889
5890 let mut cx = EditorTestContext::new(cx).await;
5891 cx.set_state("aˇ");
5892
5893 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5894 .unwrap();
5895 cx.assert_editor_state("«aˇ»");
5896 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5897 .unwrap();
5898 cx.assert_editor_state("«aˇ»");
5899}
5900
5901#[gpui::test]
5902async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5903 init_test(cx, |_| {});
5904
5905 let mut cx = EditorTestContext::new(cx).await;
5906 cx.set_state(
5907 r#"let foo = 2;
5908lˇet foo = 2;
5909let fooˇ = 2;
5910let foo = 2;
5911let foo = ˇ2;"#,
5912 );
5913
5914 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5915 .unwrap();
5916 cx.assert_editor_state(
5917 r#"let foo = 2;
5918«letˇ» foo = 2;
5919let «fooˇ» = 2;
5920let foo = 2;
5921let foo = «2ˇ»;"#,
5922 );
5923
5924 // noop for multiple selections with different contents
5925 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5926 .unwrap();
5927 cx.assert_editor_state(
5928 r#"let foo = 2;
5929«letˇ» foo = 2;
5930let «fooˇ» = 2;
5931let foo = 2;
5932let foo = «2ˇ»;"#,
5933 );
5934}
5935
5936#[gpui::test]
5937async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5938 init_test(cx, |_| {});
5939
5940 let mut cx = EditorTestContext::new(cx).await;
5941 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5942
5943 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5944 .unwrap();
5945 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5946
5947 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5948 .unwrap();
5949 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5950
5951 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5952 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5953
5954 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5955 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5956
5957 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5958 .unwrap();
5959 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5960
5961 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5962 .unwrap();
5963 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5964}
5965
5966#[gpui::test]
5967async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5968 init_test(cx, |_| {});
5969
5970 let language = Arc::new(Language::new(
5971 LanguageConfig::default(),
5972 Some(tree_sitter_rust::LANGUAGE.into()),
5973 ));
5974
5975 let text = r#"
5976 use mod1::mod2::{mod3, mod4};
5977
5978 fn fn_1(param1: bool, param2: &str) {
5979 let var1 = "text";
5980 }
5981 "#
5982 .unindent();
5983
5984 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5985 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5986 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5987
5988 editor
5989 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5990 .await;
5991
5992 editor.update_in(cx, |editor, window, cx| {
5993 editor.change_selections(None, window, cx, |s| {
5994 s.select_display_ranges([
5995 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5996 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5997 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5998 ]);
5999 });
6000 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6001 });
6002 editor.update(cx, |editor, cx| {
6003 assert_text_with_selections(
6004 editor,
6005 indoc! {r#"
6006 use mod1::mod2::{mod3, «mod4ˇ»};
6007
6008 fn fn_1«ˇ(param1: bool, param2: &str)» {
6009 let var1 = "«ˇtext»";
6010 }
6011 "#},
6012 cx,
6013 );
6014 });
6015
6016 editor.update_in(cx, |editor, window, cx| {
6017 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6018 });
6019 editor.update(cx, |editor, cx| {
6020 assert_text_with_selections(
6021 editor,
6022 indoc! {r#"
6023 use mod1::mod2::«{mod3, mod4}ˇ»;
6024
6025 «ˇfn fn_1(param1: bool, param2: &str) {
6026 let var1 = "text";
6027 }»
6028 "#},
6029 cx,
6030 );
6031 });
6032
6033 editor.update_in(cx, |editor, window, cx| {
6034 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6035 });
6036 assert_eq!(
6037 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6038 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6039 );
6040
6041 // Trying to expand the selected syntax node one more time has no effect.
6042 editor.update_in(cx, |editor, window, cx| {
6043 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6044 });
6045 assert_eq!(
6046 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6047 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6048 );
6049
6050 editor.update_in(cx, |editor, window, cx| {
6051 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6052 });
6053 editor.update(cx, |editor, cx| {
6054 assert_text_with_selections(
6055 editor,
6056 indoc! {r#"
6057 use mod1::mod2::«{mod3, mod4}ˇ»;
6058
6059 «ˇfn fn_1(param1: bool, param2: &str) {
6060 let var1 = "text";
6061 }»
6062 "#},
6063 cx,
6064 );
6065 });
6066
6067 editor.update_in(cx, |editor, window, cx| {
6068 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6069 });
6070 editor.update(cx, |editor, cx| {
6071 assert_text_with_selections(
6072 editor,
6073 indoc! {r#"
6074 use mod1::mod2::{mod3, «mod4ˇ»};
6075
6076 fn fn_1«ˇ(param1: bool, param2: &str)» {
6077 let var1 = "«ˇtext»";
6078 }
6079 "#},
6080 cx,
6081 );
6082 });
6083
6084 editor.update_in(cx, |editor, window, cx| {
6085 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6086 });
6087 editor.update(cx, |editor, cx| {
6088 assert_text_with_selections(
6089 editor,
6090 indoc! {r#"
6091 use mod1::mod2::{mod3, mo«ˇ»d4};
6092
6093 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6094 let var1 = "te«ˇ»xt";
6095 }
6096 "#},
6097 cx,
6098 );
6099 });
6100
6101 // Trying to shrink the selected syntax node one more time has no effect.
6102 editor.update_in(cx, |editor, window, cx| {
6103 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6104 });
6105 editor.update_in(cx, |editor, _, cx| {
6106 assert_text_with_selections(
6107 editor,
6108 indoc! {r#"
6109 use mod1::mod2::{mod3, mo«ˇ»d4};
6110
6111 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6112 let var1 = "te«ˇ»xt";
6113 }
6114 "#},
6115 cx,
6116 );
6117 });
6118
6119 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6120 // a fold.
6121 editor.update_in(cx, |editor, window, cx| {
6122 editor.fold_creases(
6123 vec![
6124 Crease::simple(
6125 Point::new(0, 21)..Point::new(0, 24),
6126 FoldPlaceholder::test(),
6127 ),
6128 Crease::simple(
6129 Point::new(3, 20)..Point::new(3, 22),
6130 FoldPlaceholder::test(),
6131 ),
6132 ],
6133 true,
6134 window,
6135 cx,
6136 );
6137 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6138 });
6139 editor.update(cx, |editor, cx| {
6140 assert_text_with_selections(
6141 editor,
6142 indoc! {r#"
6143 use mod1::mod2::«{mod3, mod4}ˇ»;
6144
6145 fn fn_1«ˇ(param1: bool, param2: &str)» {
6146 «ˇlet var1 = "text";»
6147 }
6148 "#},
6149 cx,
6150 );
6151 });
6152}
6153
6154#[gpui::test]
6155async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6156 init_test(cx, |_| {});
6157
6158 let base_text = r#"
6159 impl A {
6160 // this is an uncommitted comment
6161
6162 fn b() {
6163 c();
6164 }
6165
6166 // this is another uncommitted comment
6167
6168 fn d() {
6169 // e
6170 // f
6171 }
6172 }
6173
6174 fn g() {
6175 // h
6176 }
6177 "#
6178 .unindent();
6179
6180 let text = r#"
6181 ˇimpl A {
6182
6183 fn b() {
6184 c();
6185 }
6186
6187 fn d() {
6188 // e
6189 // f
6190 }
6191 }
6192
6193 fn g() {
6194 // h
6195 }
6196 "#
6197 .unindent();
6198
6199 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6200 cx.set_state(&text);
6201 cx.set_head_text(&base_text);
6202 cx.update_editor(|editor, window, cx| {
6203 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6204 });
6205
6206 cx.assert_state_with_diff(
6207 "
6208 ˇimpl A {
6209 - // this is an uncommitted comment
6210
6211 fn b() {
6212 c();
6213 }
6214
6215 - // this is another uncommitted comment
6216 -
6217 fn d() {
6218 // e
6219 // f
6220 }
6221 }
6222
6223 fn g() {
6224 // h
6225 }
6226 "
6227 .unindent(),
6228 );
6229
6230 let expected_display_text = "
6231 impl A {
6232 // this is an uncommitted comment
6233
6234 fn b() {
6235 ⋯
6236 }
6237
6238 // this is another uncommitted comment
6239
6240 fn d() {
6241 ⋯
6242 }
6243 }
6244
6245 fn g() {
6246 ⋯
6247 }
6248 "
6249 .unindent();
6250
6251 cx.update_editor(|editor, window, cx| {
6252 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6253 assert_eq!(editor.display_text(cx), expected_display_text);
6254 });
6255}
6256
6257#[gpui::test]
6258async fn test_autoindent(cx: &mut TestAppContext) {
6259 init_test(cx, |_| {});
6260
6261 let language = Arc::new(
6262 Language::new(
6263 LanguageConfig {
6264 brackets: BracketPairConfig {
6265 pairs: vec![
6266 BracketPair {
6267 start: "{".to_string(),
6268 end: "}".to_string(),
6269 close: false,
6270 surround: false,
6271 newline: true,
6272 },
6273 BracketPair {
6274 start: "(".to_string(),
6275 end: ")".to_string(),
6276 close: false,
6277 surround: false,
6278 newline: true,
6279 },
6280 ],
6281 ..Default::default()
6282 },
6283 ..Default::default()
6284 },
6285 Some(tree_sitter_rust::LANGUAGE.into()),
6286 )
6287 .with_indents_query(
6288 r#"
6289 (_ "(" ")" @end) @indent
6290 (_ "{" "}" @end) @indent
6291 "#,
6292 )
6293 .unwrap(),
6294 );
6295
6296 let text = "fn a() {}";
6297
6298 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6299 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6300 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6301 editor
6302 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6303 .await;
6304
6305 editor.update_in(cx, |editor, window, cx| {
6306 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6307 editor.newline(&Newline, window, cx);
6308 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6309 assert_eq!(
6310 editor.selections.ranges(cx),
6311 &[
6312 Point::new(1, 4)..Point::new(1, 4),
6313 Point::new(3, 4)..Point::new(3, 4),
6314 Point::new(5, 0)..Point::new(5, 0)
6315 ]
6316 );
6317 });
6318}
6319
6320#[gpui::test]
6321async fn test_autoindent_selections(cx: &mut TestAppContext) {
6322 init_test(cx, |_| {});
6323
6324 {
6325 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6326 cx.set_state(indoc! {"
6327 impl A {
6328
6329 fn b() {}
6330
6331 «fn c() {
6332
6333 }ˇ»
6334 }
6335 "});
6336
6337 cx.update_editor(|editor, window, cx| {
6338 editor.autoindent(&Default::default(), window, cx);
6339 });
6340
6341 cx.assert_editor_state(indoc! {"
6342 impl A {
6343
6344 fn b() {}
6345
6346 «fn c() {
6347
6348 }ˇ»
6349 }
6350 "});
6351 }
6352
6353 {
6354 let mut cx = EditorTestContext::new_multibuffer(
6355 cx,
6356 [indoc! { "
6357 impl A {
6358 «
6359 // a
6360 fn b(){}
6361 »
6362 «
6363 }
6364 fn c(){}
6365 »
6366 "}],
6367 );
6368
6369 let buffer = cx.update_editor(|editor, _, cx| {
6370 let buffer = editor.buffer().update(cx, |buffer, _| {
6371 buffer.all_buffers().iter().next().unwrap().clone()
6372 });
6373 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6374 buffer
6375 });
6376
6377 cx.run_until_parked();
6378 cx.update_editor(|editor, window, cx| {
6379 editor.select_all(&Default::default(), window, cx);
6380 editor.autoindent(&Default::default(), window, cx)
6381 });
6382 cx.run_until_parked();
6383
6384 cx.update(|_, cx| {
6385 assert_eq!(
6386 buffer.read(cx).text(),
6387 indoc! { "
6388 impl A {
6389
6390 // a
6391 fn b(){}
6392
6393
6394 }
6395 fn c(){}
6396
6397 " }
6398 )
6399 });
6400 }
6401}
6402
6403#[gpui::test]
6404async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6405 init_test(cx, |_| {});
6406
6407 let mut cx = EditorTestContext::new(cx).await;
6408
6409 let language = Arc::new(Language::new(
6410 LanguageConfig {
6411 brackets: BracketPairConfig {
6412 pairs: vec![
6413 BracketPair {
6414 start: "{".to_string(),
6415 end: "}".to_string(),
6416 close: true,
6417 surround: true,
6418 newline: true,
6419 },
6420 BracketPair {
6421 start: "(".to_string(),
6422 end: ")".to_string(),
6423 close: true,
6424 surround: true,
6425 newline: true,
6426 },
6427 BracketPair {
6428 start: "/*".to_string(),
6429 end: " */".to_string(),
6430 close: true,
6431 surround: true,
6432 newline: true,
6433 },
6434 BracketPair {
6435 start: "[".to_string(),
6436 end: "]".to_string(),
6437 close: false,
6438 surround: false,
6439 newline: true,
6440 },
6441 BracketPair {
6442 start: "\"".to_string(),
6443 end: "\"".to_string(),
6444 close: true,
6445 surround: true,
6446 newline: false,
6447 },
6448 BracketPair {
6449 start: "<".to_string(),
6450 end: ">".to_string(),
6451 close: false,
6452 surround: true,
6453 newline: true,
6454 },
6455 ],
6456 ..Default::default()
6457 },
6458 autoclose_before: "})]".to_string(),
6459 ..Default::default()
6460 },
6461 Some(tree_sitter_rust::LANGUAGE.into()),
6462 ));
6463
6464 cx.language_registry().add(language.clone());
6465 cx.update_buffer(|buffer, cx| {
6466 buffer.set_language(Some(language), cx);
6467 });
6468
6469 cx.set_state(
6470 &r#"
6471 🏀ˇ
6472 εˇ
6473 ❤️ˇ
6474 "#
6475 .unindent(),
6476 );
6477
6478 // autoclose multiple nested brackets at multiple cursors
6479 cx.update_editor(|editor, window, cx| {
6480 editor.handle_input("{", window, cx);
6481 editor.handle_input("{", window, cx);
6482 editor.handle_input("{", window, cx);
6483 });
6484 cx.assert_editor_state(
6485 &"
6486 🏀{{{ˇ}}}
6487 ε{{{ˇ}}}
6488 ❤️{{{ˇ}}}
6489 "
6490 .unindent(),
6491 );
6492
6493 // insert a different closing bracket
6494 cx.update_editor(|editor, window, cx| {
6495 editor.handle_input(")", window, cx);
6496 });
6497 cx.assert_editor_state(
6498 &"
6499 🏀{{{)ˇ}}}
6500 ε{{{)ˇ}}}
6501 ❤️{{{)ˇ}}}
6502 "
6503 .unindent(),
6504 );
6505
6506 // skip over the auto-closed brackets when typing a closing bracket
6507 cx.update_editor(|editor, window, cx| {
6508 editor.move_right(&MoveRight, window, cx);
6509 editor.handle_input("}", window, cx);
6510 editor.handle_input("}", window, cx);
6511 editor.handle_input("}", window, cx);
6512 });
6513 cx.assert_editor_state(
6514 &"
6515 🏀{{{)}}}}ˇ
6516 ε{{{)}}}}ˇ
6517 ❤️{{{)}}}}ˇ
6518 "
6519 .unindent(),
6520 );
6521
6522 // autoclose multi-character pairs
6523 cx.set_state(
6524 &"
6525 ˇ
6526 ˇ
6527 "
6528 .unindent(),
6529 );
6530 cx.update_editor(|editor, window, cx| {
6531 editor.handle_input("/", window, cx);
6532 editor.handle_input("*", window, cx);
6533 });
6534 cx.assert_editor_state(
6535 &"
6536 /*ˇ */
6537 /*ˇ */
6538 "
6539 .unindent(),
6540 );
6541
6542 // one cursor autocloses a multi-character pair, one cursor
6543 // does not autoclose.
6544 cx.set_state(
6545 &"
6546 /ˇ
6547 ˇ
6548 "
6549 .unindent(),
6550 );
6551 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6552 cx.assert_editor_state(
6553 &"
6554 /*ˇ */
6555 *ˇ
6556 "
6557 .unindent(),
6558 );
6559
6560 // Don't autoclose if the next character isn't whitespace and isn't
6561 // listed in the language's "autoclose_before" section.
6562 cx.set_state("ˇa b");
6563 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6564 cx.assert_editor_state("{ˇa b");
6565
6566 // Don't autoclose if `close` is false for the bracket pair
6567 cx.set_state("ˇ");
6568 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6569 cx.assert_editor_state("[ˇ");
6570
6571 // Surround with brackets if text is selected
6572 cx.set_state("«aˇ» b");
6573 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6574 cx.assert_editor_state("{«aˇ»} b");
6575
6576 // Autoclose when not immediately after a word character
6577 cx.set_state("a ˇ");
6578 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6579 cx.assert_editor_state("a \"ˇ\"");
6580
6581 // Autoclose pair where the start and end characters are the same
6582 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6583 cx.assert_editor_state("a \"\"ˇ");
6584
6585 // Don't autoclose when immediately after a word character
6586 cx.set_state("aˇ");
6587 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6588 cx.assert_editor_state("a\"ˇ");
6589
6590 // Do autoclose when after a non-word character
6591 cx.set_state("{ˇ");
6592 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6593 cx.assert_editor_state("{\"ˇ\"");
6594
6595 // Non identical pairs autoclose regardless of preceding character
6596 cx.set_state("aˇ");
6597 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6598 cx.assert_editor_state("a{ˇ}");
6599
6600 // Don't autoclose pair if autoclose is disabled
6601 cx.set_state("ˇ");
6602 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6603 cx.assert_editor_state("<ˇ");
6604
6605 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6606 cx.set_state("«aˇ» b");
6607 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6608 cx.assert_editor_state("<«aˇ»> b");
6609}
6610
6611#[gpui::test]
6612async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6613 init_test(cx, |settings| {
6614 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6615 });
6616
6617 let mut cx = EditorTestContext::new(cx).await;
6618
6619 let language = Arc::new(Language::new(
6620 LanguageConfig {
6621 brackets: BracketPairConfig {
6622 pairs: vec![
6623 BracketPair {
6624 start: "{".to_string(),
6625 end: "}".to_string(),
6626 close: true,
6627 surround: true,
6628 newline: true,
6629 },
6630 BracketPair {
6631 start: "(".to_string(),
6632 end: ")".to_string(),
6633 close: true,
6634 surround: true,
6635 newline: true,
6636 },
6637 BracketPair {
6638 start: "[".to_string(),
6639 end: "]".to_string(),
6640 close: false,
6641 surround: false,
6642 newline: true,
6643 },
6644 ],
6645 ..Default::default()
6646 },
6647 autoclose_before: "})]".to_string(),
6648 ..Default::default()
6649 },
6650 Some(tree_sitter_rust::LANGUAGE.into()),
6651 ));
6652
6653 cx.language_registry().add(language.clone());
6654 cx.update_buffer(|buffer, cx| {
6655 buffer.set_language(Some(language), cx);
6656 });
6657
6658 cx.set_state(
6659 &"
6660 ˇ
6661 ˇ
6662 ˇ
6663 "
6664 .unindent(),
6665 );
6666
6667 // ensure only matching closing brackets are skipped over
6668 cx.update_editor(|editor, window, cx| {
6669 editor.handle_input("}", window, cx);
6670 editor.move_left(&MoveLeft, window, cx);
6671 editor.handle_input(")", window, cx);
6672 editor.move_left(&MoveLeft, window, cx);
6673 });
6674 cx.assert_editor_state(
6675 &"
6676 ˇ)}
6677 ˇ)}
6678 ˇ)}
6679 "
6680 .unindent(),
6681 );
6682
6683 // skip-over closing brackets at multiple cursors
6684 cx.update_editor(|editor, window, cx| {
6685 editor.handle_input(")", window, cx);
6686 editor.handle_input("}", window, cx);
6687 });
6688 cx.assert_editor_state(
6689 &"
6690 )}ˇ
6691 )}ˇ
6692 )}ˇ
6693 "
6694 .unindent(),
6695 );
6696
6697 // ignore non-close brackets
6698 cx.update_editor(|editor, window, cx| {
6699 editor.handle_input("]", window, cx);
6700 editor.move_left(&MoveLeft, window, cx);
6701 editor.handle_input("]", window, cx);
6702 });
6703 cx.assert_editor_state(
6704 &"
6705 )}]ˇ]
6706 )}]ˇ]
6707 )}]ˇ]
6708 "
6709 .unindent(),
6710 );
6711}
6712
6713#[gpui::test]
6714async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6715 init_test(cx, |_| {});
6716
6717 let mut cx = EditorTestContext::new(cx).await;
6718
6719 let html_language = Arc::new(
6720 Language::new(
6721 LanguageConfig {
6722 name: "HTML".into(),
6723 brackets: BracketPairConfig {
6724 pairs: vec![
6725 BracketPair {
6726 start: "<".into(),
6727 end: ">".into(),
6728 close: true,
6729 ..Default::default()
6730 },
6731 BracketPair {
6732 start: "{".into(),
6733 end: "}".into(),
6734 close: true,
6735 ..Default::default()
6736 },
6737 BracketPair {
6738 start: "(".into(),
6739 end: ")".into(),
6740 close: true,
6741 ..Default::default()
6742 },
6743 ],
6744 ..Default::default()
6745 },
6746 autoclose_before: "})]>".into(),
6747 ..Default::default()
6748 },
6749 Some(tree_sitter_html::LANGUAGE.into()),
6750 )
6751 .with_injection_query(
6752 r#"
6753 (script_element
6754 (raw_text) @injection.content
6755 (#set! injection.language "javascript"))
6756 "#,
6757 )
6758 .unwrap(),
6759 );
6760
6761 let javascript_language = Arc::new(Language::new(
6762 LanguageConfig {
6763 name: "JavaScript".into(),
6764 brackets: BracketPairConfig {
6765 pairs: vec![
6766 BracketPair {
6767 start: "/*".into(),
6768 end: " */".into(),
6769 close: true,
6770 ..Default::default()
6771 },
6772 BracketPair {
6773 start: "{".into(),
6774 end: "}".into(),
6775 close: true,
6776 ..Default::default()
6777 },
6778 BracketPair {
6779 start: "(".into(),
6780 end: ")".into(),
6781 close: true,
6782 ..Default::default()
6783 },
6784 ],
6785 ..Default::default()
6786 },
6787 autoclose_before: "})]>".into(),
6788 ..Default::default()
6789 },
6790 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6791 ));
6792
6793 cx.language_registry().add(html_language.clone());
6794 cx.language_registry().add(javascript_language.clone());
6795
6796 cx.update_buffer(|buffer, cx| {
6797 buffer.set_language(Some(html_language), cx);
6798 });
6799
6800 cx.set_state(
6801 &r#"
6802 <body>ˇ
6803 <script>
6804 var x = 1;ˇ
6805 </script>
6806 </body>ˇ
6807 "#
6808 .unindent(),
6809 );
6810
6811 // Precondition: different languages are active at different locations.
6812 cx.update_editor(|editor, window, cx| {
6813 let snapshot = editor.snapshot(window, cx);
6814 let cursors = editor.selections.ranges::<usize>(cx);
6815 let languages = cursors
6816 .iter()
6817 .map(|c| snapshot.language_at(c.start).unwrap().name())
6818 .collect::<Vec<_>>();
6819 assert_eq!(
6820 languages,
6821 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6822 );
6823 });
6824
6825 // Angle brackets autoclose in HTML, but not JavaScript.
6826 cx.update_editor(|editor, window, cx| {
6827 editor.handle_input("<", window, cx);
6828 editor.handle_input("a", window, cx);
6829 });
6830 cx.assert_editor_state(
6831 &r#"
6832 <body><aˇ>
6833 <script>
6834 var x = 1;<aˇ
6835 </script>
6836 </body><aˇ>
6837 "#
6838 .unindent(),
6839 );
6840
6841 // Curly braces and parens autoclose in both HTML and JavaScript.
6842 cx.update_editor(|editor, window, cx| {
6843 editor.handle_input(" b=", window, cx);
6844 editor.handle_input("{", window, cx);
6845 editor.handle_input("c", window, cx);
6846 editor.handle_input("(", window, cx);
6847 });
6848 cx.assert_editor_state(
6849 &r#"
6850 <body><a b={c(ˇ)}>
6851 <script>
6852 var x = 1;<a b={c(ˇ)}
6853 </script>
6854 </body><a b={c(ˇ)}>
6855 "#
6856 .unindent(),
6857 );
6858
6859 // Brackets that were already autoclosed are skipped.
6860 cx.update_editor(|editor, window, cx| {
6861 editor.handle_input(")", window, cx);
6862 editor.handle_input("d", window, cx);
6863 editor.handle_input("}", window, cx);
6864 });
6865 cx.assert_editor_state(
6866 &r#"
6867 <body><a b={c()d}ˇ>
6868 <script>
6869 var x = 1;<a b={c()d}ˇ
6870 </script>
6871 </body><a b={c()d}ˇ>
6872 "#
6873 .unindent(),
6874 );
6875 cx.update_editor(|editor, window, cx| {
6876 editor.handle_input(">", window, cx);
6877 });
6878 cx.assert_editor_state(
6879 &r#"
6880 <body><a b={c()d}>ˇ
6881 <script>
6882 var x = 1;<a b={c()d}>ˇ
6883 </script>
6884 </body><a b={c()d}>ˇ
6885 "#
6886 .unindent(),
6887 );
6888
6889 // Reset
6890 cx.set_state(
6891 &r#"
6892 <body>ˇ
6893 <script>
6894 var x = 1;ˇ
6895 </script>
6896 </body>ˇ
6897 "#
6898 .unindent(),
6899 );
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.handle_input("<", window, cx);
6903 });
6904 cx.assert_editor_state(
6905 &r#"
6906 <body><ˇ>
6907 <script>
6908 var x = 1;<ˇ
6909 </script>
6910 </body><ˇ>
6911 "#
6912 .unindent(),
6913 );
6914
6915 // When backspacing, the closing angle brackets are removed.
6916 cx.update_editor(|editor, window, cx| {
6917 editor.backspace(&Backspace, window, cx);
6918 });
6919 cx.assert_editor_state(
6920 &r#"
6921 <body>ˇ
6922 <script>
6923 var x = 1;ˇ
6924 </script>
6925 </body>ˇ
6926 "#
6927 .unindent(),
6928 );
6929
6930 // Block comments autoclose in JavaScript, but not HTML.
6931 cx.update_editor(|editor, window, cx| {
6932 editor.handle_input("/", window, cx);
6933 editor.handle_input("*", window, cx);
6934 });
6935 cx.assert_editor_state(
6936 &r#"
6937 <body>/*ˇ
6938 <script>
6939 var x = 1;/*ˇ */
6940 </script>
6941 </body>/*ˇ
6942 "#
6943 .unindent(),
6944 );
6945}
6946
6947#[gpui::test]
6948async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6949 init_test(cx, |_| {});
6950
6951 let mut cx = EditorTestContext::new(cx).await;
6952
6953 let rust_language = Arc::new(
6954 Language::new(
6955 LanguageConfig {
6956 name: "Rust".into(),
6957 brackets: serde_json::from_value(json!([
6958 { "start": "{", "end": "}", "close": true, "newline": true },
6959 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6960 ]))
6961 .unwrap(),
6962 autoclose_before: "})]>".into(),
6963 ..Default::default()
6964 },
6965 Some(tree_sitter_rust::LANGUAGE.into()),
6966 )
6967 .with_override_query("(string_literal) @string")
6968 .unwrap(),
6969 );
6970
6971 cx.language_registry().add(rust_language.clone());
6972 cx.update_buffer(|buffer, cx| {
6973 buffer.set_language(Some(rust_language), cx);
6974 });
6975
6976 cx.set_state(
6977 &r#"
6978 let x = ˇ
6979 "#
6980 .unindent(),
6981 );
6982
6983 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6984 cx.update_editor(|editor, window, cx| {
6985 editor.handle_input("\"", window, cx);
6986 });
6987 cx.assert_editor_state(
6988 &r#"
6989 let x = "ˇ"
6990 "#
6991 .unindent(),
6992 );
6993
6994 // Inserting another quotation mark. The cursor moves across the existing
6995 // automatically-inserted quotation mark.
6996 cx.update_editor(|editor, window, cx| {
6997 editor.handle_input("\"", window, cx);
6998 });
6999 cx.assert_editor_state(
7000 &r#"
7001 let x = ""ˇ
7002 "#
7003 .unindent(),
7004 );
7005
7006 // Reset
7007 cx.set_state(
7008 &r#"
7009 let x = ˇ
7010 "#
7011 .unindent(),
7012 );
7013
7014 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7015 cx.update_editor(|editor, window, cx| {
7016 editor.handle_input("\"", window, cx);
7017 editor.handle_input(" ", window, cx);
7018 editor.move_left(&Default::default(), window, cx);
7019 editor.handle_input("\\", window, cx);
7020 editor.handle_input("\"", window, cx);
7021 });
7022 cx.assert_editor_state(
7023 &r#"
7024 let x = "\"ˇ "
7025 "#
7026 .unindent(),
7027 );
7028
7029 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7030 // mark. Nothing is inserted.
7031 cx.update_editor(|editor, window, cx| {
7032 editor.move_right(&Default::default(), window, cx);
7033 editor.handle_input("\"", window, cx);
7034 });
7035 cx.assert_editor_state(
7036 &r#"
7037 let x = "\" "ˇ
7038 "#
7039 .unindent(),
7040 );
7041}
7042
7043#[gpui::test]
7044async fn test_surround_with_pair(cx: &mut TestAppContext) {
7045 init_test(cx, |_| {});
7046
7047 let language = Arc::new(Language::new(
7048 LanguageConfig {
7049 brackets: BracketPairConfig {
7050 pairs: vec![
7051 BracketPair {
7052 start: "{".to_string(),
7053 end: "}".to_string(),
7054 close: true,
7055 surround: true,
7056 newline: true,
7057 },
7058 BracketPair {
7059 start: "/* ".to_string(),
7060 end: "*/".to_string(),
7061 close: true,
7062 surround: true,
7063 ..Default::default()
7064 },
7065 ],
7066 ..Default::default()
7067 },
7068 ..Default::default()
7069 },
7070 Some(tree_sitter_rust::LANGUAGE.into()),
7071 ));
7072
7073 let text = r#"
7074 a
7075 b
7076 c
7077 "#
7078 .unindent();
7079
7080 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7081 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7082 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7083 editor
7084 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7085 .await;
7086
7087 editor.update_in(cx, |editor, window, cx| {
7088 editor.change_selections(None, window, cx, |s| {
7089 s.select_display_ranges([
7090 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7091 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7092 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7093 ])
7094 });
7095
7096 editor.handle_input("{", window, cx);
7097 editor.handle_input("{", window, cx);
7098 editor.handle_input("{", window, cx);
7099 assert_eq!(
7100 editor.text(cx),
7101 "
7102 {{{a}}}
7103 {{{b}}}
7104 {{{c}}}
7105 "
7106 .unindent()
7107 );
7108 assert_eq!(
7109 editor.selections.display_ranges(cx),
7110 [
7111 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7112 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7113 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7114 ]
7115 );
7116
7117 editor.undo(&Undo, window, cx);
7118 editor.undo(&Undo, window, cx);
7119 editor.undo(&Undo, window, cx);
7120 assert_eq!(
7121 editor.text(cx),
7122 "
7123 a
7124 b
7125 c
7126 "
7127 .unindent()
7128 );
7129 assert_eq!(
7130 editor.selections.display_ranges(cx),
7131 [
7132 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7133 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7134 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7135 ]
7136 );
7137
7138 // Ensure inserting the first character of a multi-byte bracket pair
7139 // doesn't surround the selections with the bracket.
7140 editor.handle_input("/", window, cx);
7141 assert_eq!(
7142 editor.text(cx),
7143 "
7144 /
7145 /
7146 /
7147 "
7148 .unindent()
7149 );
7150 assert_eq!(
7151 editor.selections.display_ranges(cx),
7152 [
7153 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7154 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7155 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7156 ]
7157 );
7158
7159 editor.undo(&Undo, window, cx);
7160 assert_eq!(
7161 editor.text(cx),
7162 "
7163 a
7164 b
7165 c
7166 "
7167 .unindent()
7168 );
7169 assert_eq!(
7170 editor.selections.display_ranges(cx),
7171 [
7172 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7173 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7174 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7175 ]
7176 );
7177
7178 // Ensure inserting the last character of a multi-byte bracket pair
7179 // doesn't surround the selections with the bracket.
7180 editor.handle_input("*", window, cx);
7181 assert_eq!(
7182 editor.text(cx),
7183 "
7184 *
7185 *
7186 *
7187 "
7188 .unindent()
7189 );
7190 assert_eq!(
7191 editor.selections.display_ranges(cx),
7192 [
7193 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7194 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7195 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7196 ]
7197 );
7198 });
7199}
7200
7201#[gpui::test]
7202async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7203 init_test(cx, |_| {});
7204
7205 let language = Arc::new(Language::new(
7206 LanguageConfig {
7207 brackets: BracketPairConfig {
7208 pairs: vec![BracketPair {
7209 start: "{".to_string(),
7210 end: "}".to_string(),
7211 close: true,
7212 surround: true,
7213 newline: true,
7214 }],
7215 ..Default::default()
7216 },
7217 autoclose_before: "}".to_string(),
7218 ..Default::default()
7219 },
7220 Some(tree_sitter_rust::LANGUAGE.into()),
7221 ));
7222
7223 let text = r#"
7224 a
7225 b
7226 c
7227 "#
7228 .unindent();
7229
7230 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7231 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7232 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7233 editor
7234 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7235 .await;
7236
7237 editor.update_in(cx, |editor, window, cx| {
7238 editor.change_selections(None, window, cx, |s| {
7239 s.select_ranges([
7240 Point::new(0, 1)..Point::new(0, 1),
7241 Point::new(1, 1)..Point::new(1, 1),
7242 Point::new(2, 1)..Point::new(2, 1),
7243 ])
7244 });
7245
7246 editor.handle_input("{", window, cx);
7247 editor.handle_input("{", window, cx);
7248 editor.handle_input("_", window, cx);
7249 assert_eq!(
7250 editor.text(cx),
7251 "
7252 a{{_}}
7253 b{{_}}
7254 c{{_}}
7255 "
7256 .unindent()
7257 );
7258 assert_eq!(
7259 editor.selections.ranges::<Point>(cx),
7260 [
7261 Point::new(0, 4)..Point::new(0, 4),
7262 Point::new(1, 4)..Point::new(1, 4),
7263 Point::new(2, 4)..Point::new(2, 4)
7264 ]
7265 );
7266
7267 editor.backspace(&Default::default(), window, cx);
7268 editor.backspace(&Default::default(), window, cx);
7269 assert_eq!(
7270 editor.text(cx),
7271 "
7272 a{}
7273 b{}
7274 c{}
7275 "
7276 .unindent()
7277 );
7278 assert_eq!(
7279 editor.selections.ranges::<Point>(cx),
7280 [
7281 Point::new(0, 2)..Point::new(0, 2),
7282 Point::new(1, 2)..Point::new(1, 2),
7283 Point::new(2, 2)..Point::new(2, 2)
7284 ]
7285 );
7286
7287 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7288 assert_eq!(
7289 editor.text(cx),
7290 "
7291 a
7292 b
7293 c
7294 "
7295 .unindent()
7296 );
7297 assert_eq!(
7298 editor.selections.ranges::<Point>(cx),
7299 [
7300 Point::new(0, 1)..Point::new(0, 1),
7301 Point::new(1, 1)..Point::new(1, 1),
7302 Point::new(2, 1)..Point::new(2, 1)
7303 ]
7304 );
7305 });
7306}
7307
7308#[gpui::test]
7309async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7310 init_test(cx, |settings| {
7311 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7312 });
7313
7314 let mut cx = EditorTestContext::new(cx).await;
7315
7316 let language = Arc::new(Language::new(
7317 LanguageConfig {
7318 brackets: BracketPairConfig {
7319 pairs: vec![
7320 BracketPair {
7321 start: "{".to_string(),
7322 end: "}".to_string(),
7323 close: true,
7324 surround: true,
7325 newline: true,
7326 },
7327 BracketPair {
7328 start: "(".to_string(),
7329 end: ")".to_string(),
7330 close: true,
7331 surround: true,
7332 newline: true,
7333 },
7334 BracketPair {
7335 start: "[".to_string(),
7336 end: "]".to_string(),
7337 close: false,
7338 surround: true,
7339 newline: true,
7340 },
7341 ],
7342 ..Default::default()
7343 },
7344 autoclose_before: "})]".to_string(),
7345 ..Default::default()
7346 },
7347 Some(tree_sitter_rust::LANGUAGE.into()),
7348 ));
7349
7350 cx.language_registry().add(language.clone());
7351 cx.update_buffer(|buffer, cx| {
7352 buffer.set_language(Some(language), cx);
7353 });
7354
7355 cx.set_state(
7356 &"
7357 {(ˇ)}
7358 [[ˇ]]
7359 {(ˇ)}
7360 "
7361 .unindent(),
7362 );
7363
7364 cx.update_editor(|editor, window, cx| {
7365 editor.backspace(&Default::default(), window, cx);
7366 editor.backspace(&Default::default(), window, cx);
7367 });
7368
7369 cx.assert_editor_state(
7370 &"
7371 ˇ
7372 ˇ]]
7373 ˇ
7374 "
7375 .unindent(),
7376 );
7377
7378 cx.update_editor(|editor, window, cx| {
7379 editor.handle_input("{", window, cx);
7380 editor.handle_input("{", window, cx);
7381 editor.move_right(&MoveRight, window, cx);
7382 editor.move_right(&MoveRight, window, cx);
7383 editor.move_left(&MoveLeft, window, cx);
7384 editor.move_left(&MoveLeft, window, cx);
7385 editor.backspace(&Default::default(), window, cx);
7386 });
7387
7388 cx.assert_editor_state(
7389 &"
7390 {ˇ}
7391 {ˇ}]]
7392 {ˇ}
7393 "
7394 .unindent(),
7395 );
7396
7397 cx.update_editor(|editor, window, cx| {
7398 editor.backspace(&Default::default(), window, cx);
7399 });
7400
7401 cx.assert_editor_state(
7402 &"
7403 ˇ
7404 ˇ]]
7405 ˇ
7406 "
7407 .unindent(),
7408 );
7409}
7410
7411#[gpui::test]
7412async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7413 init_test(cx, |_| {});
7414
7415 let language = Arc::new(Language::new(
7416 LanguageConfig::default(),
7417 Some(tree_sitter_rust::LANGUAGE.into()),
7418 ));
7419
7420 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7421 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7422 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7423 editor
7424 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7425 .await;
7426
7427 editor.update_in(cx, |editor, window, cx| {
7428 editor.set_auto_replace_emoji_shortcode(true);
7429
7430 editor.handle_input("Hello ", window, cx);
7431 editor.handle_input(":wave", window, cx);
7432 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7433
7434 editor.handle_input(":", window, cx);
7435 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7436
7437 editor.handle_input(" :smile", window, cx);
7438 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7439
7440 editor.handle_input(":", window, cx);
7441 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7442
7443 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7444 editor.handle_input(":wave", window, cx);
7445 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7446
7447 editor.handle_input(":", window, cx);
7448 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7449
7450 editor.handle_input(":1", window, cx);
7451 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7452
7453 editor.handle_input(":", window, cx);
7454 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7455
7456 // Ensure shortcode does not get replaced when it is part of a word
7457 editor.handle_input(" Test:wave", window, cx);
7458 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7459
7460 editor.handle_input(":", window, cx);
7461 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7462
7463 editor.set_auto_replace_emoji_shortcode(false);
7464
7465 // Ensure shortcode does not get replaced when auto replace is off
7466 editor.handle_input(" :wave", window, cx);
7467 assert_eq!(
7468 editor.text(cx),
7469 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7470 );
7471
7472 editor.handle_input(":", window, cx);
7473 assert_eq!(
7474 editor.text(cx),
7475 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7476 );
7477 });
7478}
7479
7480#[gpui::test]
7481async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7482 init_test(cx, |_| {});
7483
7484 let (text, insertion_ranges) = marked_text_ranges(
7485 indoc! {"
7486 ˇ
7487 "},
7488 false,
7489 );
7490
7491 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7492 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7493
7494 _ = editor.update_in(cx, |editor, window, cx| {
7495 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7496
7497 editor
7498 .insert_snippet(&insertion_ranges, snippet, window, cx)
7499 .unwrap();
7500
7501 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7502 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7503 assert_eq!(editor.text(cx), expected_text);
7504 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7505 }
7506
7507 assert(
7508 editor,
7509 cx,
7510 indoc! {"
7511 type «» =•
7512 "},
7513 );
7514
7515 assert!(editor.context_menu_visible(), "There should be a matches");
7516 });
7517}
7518
7519#[gpui::test]
7520async fn test_snippets(cx: &mut TestAppContext) {
7521 init_test(cx, |_| {});
7522
7523 let (text, insertion_ranges) = marked_text_ranges(
7524 indoc! {"
7525 a.ˇ b
7526 a.ˇ b
7527 a.ˇ b
7528 "},
7529 false,
7530 );
7531
7532 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7533 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7534
7535 editor.update_in(cx, |editor, window, cx| {
7536 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7537
7538 editor
7539 .insert_snippet(&insertion_ranges, snippet, window, cx)
7540 .unwrap();
7541
7542 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7543 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7544 assert_eq!(editor.text(cx), expected_text);
7545 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7546 }
7547
7548 assert(
7549 editor,
7550 cx,
7551 indoc! {"
7552 a.f(«one», two, «three») b
7553 a.f(«one», two, «three») b
7554 a.f(«one», two, «three») b
7555 "},
7556 );
7557
7558 // Can't move earlier than the first tab stop
7559 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7560 assert(
7561 editor,
7562 cx,
7563 indoc! {"
7564 a.f(«one», two, «three») b
7565 a.f(«one», two, «three») b
7566 a.f(«one», two, «three») b
7567 "},
7568 );
7569
7570 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7571 assert(
7572 editor,
7573 cx,
7574 indoc! {"
7575 a.f(one, «two», three) b
7576 a.f(one, «two», three) b
7577 a.f(one, «two», three) b
7578 "},
7579 );
7580
7581 editor.move_to_prev_snippet_tabstop(window, cx);
7582 assert(
7583 editor,
7584 cx,
7585 indoc! {"
7586 a.f(«one», two, «three») b
7587 a.f(«one», two, «three») b
7588 a.f(«one», two, «three») b
7589 "},
7590 );
7591
7592 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7593 assert(
7594 editor,
7595 cx,
7596 indoc! {"
7597 a.f(one, «two», three) b
7598 a.f(one, «two», three) b
7599 a.f(one, «two», three) b
7600 "},
7601 );
7602 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7603 assert(
7604 editor,
7605 cx,
7606 indoc! {"
7607 a.f(one, two, three)ˇ b
7608 a.f(one, two, three)ˇ b
7609 a.f(one, two, three)ˇ b
7610 "},
7611 );
7612
7613 // As soon as the last tab stop is reached, snippet state is gone
7614 editor.move_to_prev_snippet_tabstop(window, cx);
7615 assert(
7616 editor,
7617 cx,
7618 indoc! {"
7619 a.f(one, two, three)ˇ b
7620 a.f(one, two, three)ˇ b
7621 a.f(one, two, three)ˇ b
7622 "},
7623 );
7624 });
7625}
7626
7627#[gpui::test]
7628async fn test_document_format_during_save(cx: &mut TestAppContext) {
7629 init_test(cx, |_| {});
7630
7631 let fs = FakeFs::new(cx.executor());
7632 fs.insert_file(path!("/file.rs"), Default::default()).await;
7633
7634 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7635
7636 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7637 language_registry.add(rust_lang());
7638 let mut fake_servers = language_registry.register_fake_lsp(
7639 "Rust",
7640 FakeLspAdapter {
7641 capabilities: lsp::ServerCapabilities {
7642 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7643 ..Default::default()
7644 },
7645 ..Default::default()
7646 },
7647 );
7648
7649 let buffer = project
7650 .update(cx, |project, cx| {
7651 project.open_local_buffer(path!("/file.rs"), cx)
7652 })
7653 .await
7654 .unwrap();
7655
7656 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7657 let (editor, cx) = cx.add_window_view(|window, cx| {
7658 build_editor_with_project(project.clone(), buffer, window, cx)
7659 });
7660 editor.update_in(cx, |editor, window, cx| {
7661 editor.set_text("one\ntwo\nthree\n", window, cx)
7662 });
7663 assert!(cx.read(|cx| editor.is_dirty(cx)));
7664
7665 cx.executor().start_waiting();
7666 let fake_server = fake_servers.next().await.unwrap();
7667
7668 let save = editor
7669 .update_in(cx, |editor, window, cx| {
7670 editor.save(true, project.clone(), window, cx)
7671 })
7672 .unwrap();
7673 fake_server
7674 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7675 assert_eq!(
7676 params.text_document.uri,
7677 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7678 );
7679 assert_eq!(params.options.tab_size, 4);
7680 Ok(Some(vec![lsp::TextEdit::new(
7681 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7682 ", ".to_string(),
7683 )]))
7684 })
7685 .next()
7686 .await;
7687 cx.executor().start_waiting();
7688 save.await;
7689
7690 assert_eq!(
7691 editor.update(cx, |editor, cx| editor.text(cx)),
7692 "one, two\nthree\n"
7693 );
7694 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7695
7696 editor.update_in(cx, |editor, window, cx| {
7697 editor.set_text("one\ntwo\nthree\n", window, cx)
7698 });
7699 assert!(cx.read(|cx| editor.is_dirty(cx)));
7700
7701 // Ensure we can still save even if formatting hangs.
7702 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7703 move |params, _| async move {
7704 assert_eq!(
7705 params.text_document.uri,
7706 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7707 );
7708 futures::future::pending::<()>().await;
7709 unreachable!()
7710 },
7711 );
7712 let save = editor
7713 .update_in(cx, |editor, window, cx| {
7714 editor.save(true, project.clone(), window, cx)
7715 })
7716 .unwrap();
7717 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7718 cx.executor().start_waiting();
7719 save.await;
7720 assert_eq!(
7721 editor.update(cx, |editor, cx| editor.text(cx)),
7722 "one\ntwo\nthree\n"
7723 );
7724 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7725
7726 // For non-dirty buffer, no formatting request should be sent
7727 let save = editor
7728 .update_in(cx, |editor, window, cx| {
7729 editor.save(true, project.clone(), window, cx)
7730 })
7731 .unwrap();
7732 let _pending_format_request = fake_server
7733 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7734 panic!("Should not be invoked on non-dirty buffer");
7735 })
7736 .next();
7737 cx.executor().start_waiting();
7738 save.await;
7739
7740 // Set rust language override and assert overridden tabsize is sent to language server
7741 update_test_language_settings(cx, |settings| {
7742 settings.languages.insert(
7743 "Rust".into(),
7744 LanguageSettingsContent {
7745 tab_size: NonZeroU32::new(8),
7746 ..Default::default()
7747 },
7748 );
7749 });
7750
7751 editor.update_in(cx, |editor, window, cx| {
7752 editor.set_text("somehting_new\n", window, cx)
7753 });
7754 assert!(cx.read(|cx| editor.is_dirty(cx)));
7755 let save = editor
7756 .update_in(cx, |editor, window, cx| {
7757 editor.save(true, project.clone(), window, cx)
7758 })
7759 .unwrap();
7760 fake_server
7761 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7762 assert_eq!(
7763 params.text_document.uri,
7764 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7765 );
7766 assert_eq!(params.options.tab_size, 8);
7767 Ok(Some(vec![]))
7768 })
7769 .next()
7770 .await;
7771 cx.executor().start_waiting();
7772 save.await;
7773}
7774
7775#[gpui::test]
7776async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7777 init_test(cx, |_| {});
7778
7779 let cols = 4;
7780 let rows = 10;
7781 let sample_text_1 = sample_text(rows, cols, 'a');
7782 assert_eq!(
7783 sample_text_1,
7784 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7785 );
7786 let sample_text_2 = sample_text(rows, cols, 'l');
7787 assert_eq!(
7788 sample_text_2,
7789 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7790 );
7791 let sample_text_3 = sample_text(rows, cols, 'v');
7792 assert_eq!(
7793 sample_text_3,
7794 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7795 );
7796
7797 let fs = FakeFs::new(cx.executor());
7798 fs.insert_tree(
7799 path!("/a"),
7800 json!({
7801 "main.rs": sample_text_1,
7802 "other.rs": sample_text_2,
7803 "lib.rs": sample_text_3,
7804 }),
7805 )
7806 .await;
7807
7808 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7810 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7811
7812 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7813 language_registry.add(rust_lang());
7814 let mut fake_servers = language_registry.register_fake_lsp(
7815 "Rust",
7816 FakeLspAdapter {
7817 capabilities: lsp::ServerCapabilities {
7818 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7819 ..Default::default()
7820 },
7821 ..Default::default()
7822 },
7823 );
7824
7825 let worktree = project.update(cx, |project, cx| {
7826 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7827 assert_eq!(worktrees.len(), 1);
7828 worktrees.pop().unwrap()
7829 });
7830 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7831
7832 let buffer_1 = project
7833 .update(cx, |project, cx| {
7834 project.open_buffer((worktree_id, "main.rs"), cx)
7835 })
7836 .await
7837 .unwrap();
7838 let buffer_2 = project
7839 .update(cx, |project, cx| {
7840 project.open_buffer((worktree_id, "other.rs"), cx)
7841 })
7842 .await
7843 .unwrap();
7844 let buffer_3 = project
7845 .update(cx, |project, cx| {
7846 project.open_buffer((worktree_id, "lib.rs"), cx)
7847 })
7848 .await
7849 .unwrap();
7850
7851 let multi_buffer = cx.new(|cx| {
7852 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7853 multi_buffer.push_excerpts(
7854 buffer_1.clone(),
7855 [
7856 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7857 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7858 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7859 ],
7860 cx,
7861 );
7862 multi_buffer.push_excerpts(
7863 buffer_2.clone(),
7864 [
7865 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7866 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7867 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7868 ],
7869 cx,
7870 );
7871 multi_buffer.push_excerpts(
7872 buffer_3.clone(),
7873 [
7874 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7875 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7876 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7877 ],
7878 cx,
7879 );
7880 multi_buffer
7881 });
7882 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7883 Editor::new(
7884 EditorMode::Full,
7885 multi_buffer,
7886 Some(project.clone()),
7887 window,
7888 cx,
7889 )
7890 });
7891
7892 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7893 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7894 s.select_ranges(Some(1..2))
7895 });
7896 editor.insert("|one|two|three|", window, cx);
7897 });
7898 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7899 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7900 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7901 s.select_ranges(Some(60..70))
7902 });
7903 editor.insert("|four|five|six|", window, cx);
7904 });
7905 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7906
7907 // First two buffers should be edited, but not the third one.
7908 assert_eq!(
7909 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7910 "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}",
7911 );
7912 buffer_1.update(cx, |buffer, _| {
7913 assert!(buffer.is_dirty());
7914 assert_eq!(
7915 buffer.text(),
7916 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7917 )
7918 });
7919 buffer_2.update(cx, |buffer, _| {
7920 assert!(buffer.is_dirty());
7921 assert_eq!(
7922 buffer.text(),
7923 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7924 )
7925 });
7926 buffer_3.update(cx, |buffer, _| {
7927 assert!(!buffer.is_dirty());
7928 assert_eq!(buffer.text(), sample_text_3,)
7929 });
7930 cx.executor().run_until_parked();
7931
7932 cx.executor().start_waiting();
7933 let save = multi_buffer_editor
7934 .update_in(cx, |editor, window, cx| {
7935 editor.save(true, project.clone(), window, cx)
7936 })
7937 .unwrap();
7938
7939 let fake_server = fake_servers.next().await.unwrap();
7940 fake_server
7941 .server
7942 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7943 Ok(Some(vec![lsp::TextEdit::new(
7944 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7945 format!("[{} formatted]", params.text_document.uri),
7946 )]))
7947 })
7948 .detach();
7949 save.await;
7950
7951 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7952 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7953 assert_eq!(
7954 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7955 uri!(
7956 "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}"
7957 ),
7958 );
7959 buffer_1.update(cx, |buffer, _| {
7960 assert!(!buffer.is_dirty());
7961 assert_eq!(
7962 buffer.text(),
7963 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7964 )
7965 });
7966 buffer_2.update(cx, |buffer, _| {
7967 assert!(!buffer.is_dirty());
7968 assert_eq!(
7969 buffer.text(),
7970 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7971 )
7972 });
7973 buffer_3.update(cx, |buffer, _| {
7974 assert!(!buffer.is_dirty());
7975 assert_eq!(buffer.text(), sample_text_3,)
7976 });
7977}
7978
7979#[gpui::test]
7980async fn test_range_format_during_save(cx: &mut TestAppContext) {
7981 init_test(cx, |_| {});
7982
7983 let fs = FakeFs::new(cx.executor());
7984 fs.insert_file(path!("/file.rs"), Default::default()).await;
7985
7986 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7987
7988 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7989 language_registry.add(rust_lang());
7990 let mut fake_servers = language_registry.register_fake_lsp(
7991 "Rust",
7992 FakeLspAdapter {
7993 capabilities: lsp::ServerCapabilities {
7994 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7995 ..Default::default()
7996 },
7997 ..Default::default()
7998 },
7999 );
8000
8001 let buffer = project
8002 .update(cx, |project, cx| {
8003 project.open_local_buffer(path!("/file.rs"), cx)
8004 })
8005 .await
8006 .unwrap();
8007
8008 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8009 let (editor, cx) = cx.add_window_view(|window, cx| {
8010 build_editor_with_project(project.clone(), buffer, window, cx)
8011 });
8012 editor.update_in(cx, |editor, window, cx| {
8013 editor.set_text("one\ntwo\nthree\n", window, cx)
8014 });
8015 assert!(cx.read(|cx| editor.is_dirty(cx)));
8016
8017 cx.executor().start_waiting();
8018 let fake_server = fake_servers.next().await.unwrap();
8019
8020 let save = editor
8021 .update_in(cx, |editor, window, cx| {
8022 editor.save(true, project.clone(), window, cx)
8023 })
8024 .unwrap();
8025 fake_server
8026 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8027 assert_eq!(
8028 params.text_document.uri,
8029 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8030 );
8031 assert_eq!(params.options.tab_size, 4);
8032 Ok(Some(vec![lsp::TextEdit::new(
8033 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8034 ", ".to_string(),
8035 )]))
8036 })
8037 .next()
8038 .await;
8039 cx.executor().start_waiting();
8040 save.await;
8041 assert_eq!(
8042 editor.update(cx, |editor, cx| editor.text(cx)),
8043 "one, two\nthree\n"
8044 );
8045 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8046
8047 editor.update_in(cx, |editor, window, cx| {
8048 editor.set_text("one\ntwo\nthree\n", window, cx)
8049 });
8050 assert!(cx.read(|cx| editor.is_dirty(cx)));
8051
8052 // Ensure we can still save even if formatting hangs.
8053 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8054 move |params, _| async move {
8055 assert_eq!(
8056 params.text_document.uri,
8057 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8058 );
8059 futures::future::pending::<()>().await;
8060 unreachable!()
8061 },
8062 );
8063 let save = editor
8064 .update_in(cx, |editor, window, cx| {
8065 editor.save(true, project.clone(), window, cx)
8066 })
8067 .unwrap();
8068 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8069 cx.executor().start_waiting();
8070 save.await;
8071 assert_eq!(
8072 editor.update(cx, |editor, cx| editor.text(cx)),
8073 "one\ntwo\nthree\n"
8074 );
8075 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8076
8077 // For non-dirty buffer, no formatting request should be sent
8078 let save = editor
8079 .update_in(cx, |editor, window, cx| {
8080 editor.save(true, project.clone(), window, cx)
8081 })
8082 .unwrap();
8083 let _pending_format_request = fake_server
8084 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8085 panic!("Should not be invoked on non-dirty buffer");
8086 })
8087 .next();
8088 cx.executor().start_waiting();
8089 save.await;
8090
8091 // Set Rust language override and assert overridden tabsize is sent to language server
8092 update_test_language_settings(cx, |settings| {
8093 settings.languages.insert(
8094 "Rust".into(),
8095 LanguageSettingsContent {
8096 tab_size: NonZeroU32::new(8),
8097 ..Default::default()
8098 },
8099 );
8100 });
8101
8102 editor.update_in(cx, |editor, window, cx| {
8103 editor.set_text("somehting_new\n", window, cx)
8104 });
8105 assert!(cx.read(|cx| editor.is_dirty(cx)));
8106 let save = editor
8107 .update_in(cx, |editor, window, cx| {
8108 editor.save(true, project.clone(), window, cx)
8109 })
8110 .unwrap();
8111 fake_server
8112 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8113 assert_eq!(
8114 params.text_document.uri,
8115 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8116 );
8117 assert_eq!(params.options.tab_size, 8);
8118 Ok(Some(vec![]))
8119 })
8120 .next()
8121 .await;
8122 cx.executor().start_waiting();
8123 save.await;
8124}
8125
8126#[gpui::test]
8127async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8128 init_test(cx, |settings| {
8129 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8130 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8131 ))
8132 });
8133
8134 let fs = FakeFs::new(cx.executor());
8135 fs.insert_file(path!("/file.rs"), Default::default()).await;
8136
8137 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8138
8139 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8140 language_registry.add(Arc::new(Language::new(
8141 LanguageConfig {
8142 name: "Rust".into(),
8143 matcher: LanguageMatcher {
8144 path_suffixes: vec!["rs".to_string()],
8145 ..Default::default()
8146 },
8147 ..LanguageConfig::default()
8148 },
8149 Some(tree_sitter_rust::LANGUAGE.into()),
8150 )));
8151 update_test_language_settings(cx, |settings| {
8152 // Enable Prettier formatting for the same buffer, and ensure
8153 // LSP is called instead of Prettier.
8154 settings.defaults.prettier = Some(PrettierSettings {
8155 allowed: true,
8156 ..PrettierSettings::default()
8157 });
8158 });
8159 let mut fake_servers = language_registry.register_fake_lsp(
8160 "Rust",
8161 FakeLspAdapter {
8162 capabilities: lsp::ServerCapabilities {
8163 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8164 ..Default::default()
8165 },
8166 ..Default::default()
8167 },
8168 );
8169
8170 let buffer = project
8171 .update(cx, |project, cx| {
8172 project.open_local_buffer(path!("/file.rs"), cx)
8173 })
8174 .await
8175 .unwrap();
8176
8177 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8178 let (editor, cx) = cx.add_window_view(|window, cx| {
8179 build_editor_with_project(project.clone(), buffer, window, cx)
8180 });
8181 editor.update_in(cx, |editor, window, cx| {
8182 editor.set_text("one\ntwo\nthree\n", window, cx)
8183 });
8184
8185 cx.executor().start_waiting();
8186 let fake_server = fake_servers.next().await.unwrap();
8187
8188 let format = editor
8189 .update_in(cx, |editor, window, cx| {
8190 editor.perform_format(
8191 project.clone(),
8192 FormatTrigger::Manual,
8193 FormatTarget::Buffers,
8194 window,
8195 cx,
8196 )
8197 })
8198 .unwrap();
8199 fake_server
8200 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8201 assert_eq!(
8202 params.text_document.uri,
8203 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8204 );
8205 assert_eq!(params.options.tab_size, 4);
8206 Ok(Some(vec![lsp::TextEdit::new(
8207 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8208 ", ".to_string(),
8209 )]))
8210 })
8211 .next()
8212 .await;
8213 cx.executor().start_waiting();
8214 format.await;
8215 assert_eq!(
8216 editor.update(cx, |editor, cx| editor.text(cx)),
8217 "one, two\nthree\n"
8218 );
8219
8220 editor.update_in(cx, |editor, window, cx| {
8221 editor.set_text("one\ntwo\nthree\n", window, cx)
8222 });
8223 // Ensure we don't lock if formatting hangs.
8224 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8225 move |params, _| async move {
8226 assert_eq!(
8227 params.text_document.uri,
8228 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8229 );
8230 futures::future::pending::<()>().await;
8231 unreachable!()
8232 },
8233 );
8234 let format = editor
8235 .update_in(cx, |editor, window, cx| {
8236 editor.perform_format(
8237 project,
8238 FormatTrigger::Manual,
8239 FormatTarget::Buffers,
8240 window,
8241 cx,
8242 )
8243 })
8244 .unwrap();
8245 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8246 cx.executor().start_waiting();
8247 format.await;
8248 assert_eq!(
8249 editor.update(cx, |editor, cx| editor.text(cx)),
8250 "one\ntwo\nthree\n"
8251 );
8252}
8253
8254#[gpui::test]
8255async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8256 init_test(cx, |settings| {
8257 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8258 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8259 ))
8260 });
8261
8262 let fs = FakeFs::new(cx.executor());
8263 fs.insert_file(path!("/file.ts"), Default::default()).await;
8264
8265 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8266
8267 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8268 language_registry.add(Arc::new(Language::new(
8269 LanguageConfig {
8270 name: "TypeScript".into(),
8271 matcher: LanguageMatcher {
8272 path_suffixes: vec!["ts".to_string()],
8273 ..Default::default()
8274 },
8275 ..LanguageConfig::default()
8276 },
8277 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8278 )));
8279 update_test_language_settings(cx, |settings| {
8280 settings.defaults.prettier = Some(PrettierSettings {
8281 allowed: true,
8282 ..PrettierSettings::default()
8283 });
8284 });
8285 let mut fake_servers = language_registry.register_fake_lsp(
8286 "TypeScript",
8287 FakeLspAdapter {
8288 capabilities: lsp::ServerCapabilities {
8289 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8290 ..Default::default()
8291 },
8292 ..Default::default()
8293 },
8294 );
8295
8296 let buffer = project
8297 .update(cx, |project, cx| {
8298 project.open_local_buffer(path!("/file.ts"), cx)
8299 })
8300 .await
8301 .unwrap();
8302
8303 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8304 let (editor, cx) = cx.add_window_view(|window, cx| {
8305 build_editor_with_project(project.clone(), buffer, window, cx)
8306 });
8307 editor.update_in(cx, |editor, window, cx| {
8308 editor.set_text(
8309 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8310 window,
8311 cx,
8312 )
8313 });
8314
8315 cx.executor().start_waiting();
8316 let fake_server = fake_servers.next().await.unwrap();
8317
8318 let format = editor
8319 .update_in(cx, |editor, window, cx| {
8320 editor.perform_code_action_kind(
8321 project.clone(),
8322 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8323 window,
8324 cx,
8325 )
8326 })
8327 .unwrap();
8328 fake_server
8329 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8330 assert_eq!(
8331 params.text_document.uri,
8332 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8333 );
8334 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8335 lsp::CodeAction {
8336 title: "Organize Imports".to_string(),
8337 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8338 edit: Some(lsp::WorkspaceEdit {
8339 changes: Some(
8340 [(
8341 params.text_document.uri.clone(),
8342 vec![lsp::TextEdit::new(
8343 lsp::Range::new(
8344 lsp::Position::new(1, 0),
8345 lsp::Position::new(2, 0),
8346 ),
8347 "".to_string(),
8348 )],
8349 )]
8350 .into_iter()
8351 .collect(),
8352 ),
8353 ..Default::default()
8354 }),
8355 ..Default::default()
8356 },
8357 )]))
8358 })
8359 .next()
8360 .await;
8361 cx.executor().start_waiting();
8362 format.await;
8363 assert_eq!(
8364 editor.update(cx, |editor, cx| editor.text(cx)),
8365 "import { a } from 'module';\n\nconst x = a;\n"
8366 );
8367
8368 editor.update_in(cx, |editor, window, cx| {
8369 editor.set_text(
8370 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8371 window,
8372 cx,
8373 )
8374 });
8375 // Ensure we don't lock if code action hangs.
8376 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8377 move |params, _| async move {
8378 assert_eq!(
8379 params.text_document.uri,
8380 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8381 );
8382 futures::future::pending::<()>().await;
8383 unreachable!()
8384 },
8385 );
8386 let format = editor
8387 .update_in(cx, |editor, window, cx| {
8388 editor.perform_code_action_kind(
8389 project,
8390 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8391 window,
8392 cx,
8393 )
8394 })
8395 .unwrap();
8396 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8397 cx.executor().start_waiting();
8398 format.await;
8399 assert_eq!(
8400 editor.update(cx, |editor, cx| editor.text(cx)),
8401 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8402 );
8403}
8404
8405#[gpui::test]
8406async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8407 init_test(cx, |_| {});
8408
8409 let mut cx = EditorLspTestContext::new_rust(
8410 lsp::ServerCapabilities {
8411 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8412 ..Default::default()
8413 },
8414 cx,
8415 )
8416 .await;
8417
8418 cx.set_state(indoc! {"
8419 one.twoˇ
8420 "});
8421
8422 // The format request takes a long time. When it completes, it inserts
8423 // a newline and an indent before the `.`
8424 cx.lsp
8425 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8426 let executor = cx.background_executor().clone();
8427 async move {
8428 executor.timer(Duration::from_millis(100)).await;
8429 Ok(Some(vec![lsp::TextEdit {
8430 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8431 new_text: "\n ".into(),
8432 }]))
8433 }
8434 });
8435
8436 // Submit a format request.
8437 let format_1 = cx
8438 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8439 .unwrap();
8440 cx.executor().run_until_parked();
8441
8442 // Submit a second format request.
8443 let format_2 = cx
8444 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8445 .unwrap();
8446 cx.executor().run_until_parked();
8447
8448 // Wait for both format requests to complete
8449 cx.executor().advance_clock(Duration::from_millis(200));
8450 cx.executor().start_waiting();
8451 format_1.await.unwrap();
8452 cx.executor().start_waiting();
8453 format_2.await.unwrap();
8454
8455 // The formatting edits only happens once.
8456 cx.assert_editor_state(indoc! {"
8457 one
8458 .twoˇ
8459 "});
8460}
8461
8462#[gpui::test]
8463async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8464 init_test(cx, |settings| {
8465 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8466 });
8467
8468 let mut cx = EditorLspTestContext::new_rust(
8469 lsp::ServerCapabilities {
8470 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8471 ..Default::default()
8472 },
8473 cx,
8474 )
8475 .await;
8476
8477 // Set up a buffer white some trailing whitespace and no trailing newline.
8478 cx.set_state(
8479 &[
8480 "one ", //
8481 "twoˇ", //
8482 "three ", //
8483 "four", //
8484 ]
8485 .join("\n"),
8486 );
8487
8488 // Submit a format request.
8489 let format = cx
8490 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8491 .unwrap();
8492
8493 // Record which buffer changes have been sent to the language server
8494 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8495 cx.lsp
8496 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8497 let buffer_changes = buffer_changes.clone();
8498 move |params, _| {
8499 buffer_changes.lock().extend(
8500 params
8501 .content_changes
8502 .into_iter()
8503 .map(|e| (e.range.unwrap(), e.text)),
8504 );
8505 }
8506 });
8507
8508 // Handle formatting requests to the language server.
8509 cx.lsp
8510 .set_request_handler::<lsp::request::Formatting, _, _>({
8511 let buffer_changes = buffer_changes.clone();
8512 move |_, _| {
8513 // When formatting is requested, trailing whitespace has already been stripped,
8514 // and the trailing newline has already been added.
8515 assert_eq!(
8516 &buffer_changes.lock()[1..],
8517 &[
8518 (
8519 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8520 "".into()
8521 ),
8522 (
8523 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8524 "".into()
8525 ),
8526 (
8527 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8528 "\n".into()
8529 ),
8530 ]
8531 );
8532
8533 // Insert blank lines between each line of the buffer.
8534 async move {
8535 Ok(Some(vec![
8536 lsp::TextEdit {
8537 range: lsp::Range::new(
8538 lsp::Position::new(1, 0),
8539 lsp::Position::new(1, 0),
8540 ),
8541 new_text: "\n".into(),
8542 },
8543 lsp::TextEdit {
8544 range: lsp::Range::new(
8545 lsp::Position::new(2, 0),
8546 lsp::Position::new(2, 0),
8547 ),
8548 new_text: "\n".into(),
8549 },
8550 ]))
8551 }
8552 }
8553 });
8554
8555 // After formatting the buffer, the trailing whitespace is stripped,
8556 // a newline is appended, and the edits provided by the language server
8557 // have been applied.
8558 format.await.unwrap();
8559 cx.assert_editor_state(
8560 &[
8561 "one", //
8562 "", //
8563 "twoˇ", //
8564 "", //
8565 "three", //
8566 "four", //
8567 "", //
8568 ]
8569 .join("\n"),
8570 );
8571
8572 // Undoing the formatting undoes the trailing whitespace removal, the
8573 // trailing newline, and the LSP edits.
8574 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8575 cx.assert_editor_state(
8576 &[
8577 "one ", //
8578 "twoˇ", //
8579 "three ", //
8580 "four", //
8581 ]
8582 .join("\n"),
8583 );
8584}
8585
8586#[gpui::test]
8587async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8588 cx: &mut TestAppContext,
8589) {
8590 init_test(cx, |_| {});
8591
8592 cx.update(|cx| {
8593 cx.update_global::<SettingsStore, _>(|settings, cx| {
8594 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8595 settings.auto_signature_help = Some(true);
8596 });
8597 });
8598 });
8599
8600 let mut cx = EditorLspTestContext::new_rust(
8601 lsp::ServerCapabilities {
8602 signature_help_provider: Some(lsp::SignatureHelpOptions {
8603 ..Default::default()
8604 }),
8605 ..Default::default()
8606 },
8607 cx,
8608 )
8609 .await;
8610
8611 let language = Language::new(
8612 LanguageConfig {
8613 name: "Rust".into(),
8614 brackets: BracketPairConfig {
8615 pairs: vec![
8616 BracketPair {
8617 start: "{".to_string(),
8618 end: "}".to_string(),
8619 close: true,
8620 surround: true,
8621 newline: true,
8622 },
8623 BracketPair {
8624 start: "(".to_string(),
8625 end: ")".to_string(),
8626 close: true,
8627 surround: true,
8628 newline: true,
8629 },
8630 BracketPair {
8631 start: "/*".to_string(),
8632 end: " */".to_string(),
8633 close: true,
8634 surround: true,
8635 newline: true,
8636 },
8637 BracketPair {
8638 start: "[".to_string(),
8639 end: "]".to_string(),
8640 close: false,
8641 surround: false,
8642 newline: true,
8643 },
8644 BracketPair {
8645 start: "\"".to_string(),
8646 end: "\"".to_string(),
8647 close: true,
8648 surround: true,
8649 newline: false,
8650 },
8651 BracketPair {
8652 start: "<".to_string(),
8653 end: ">".to_string(),
8654 close: false,
8655 surround: true,
8656 newline: true,
8657 },
8658 ],
8659 ..Default::default()
8660 },
8661 autoclose_before: "})]".to_string(),
8662 ..Default::default()
8663 },
8664 Some(tree_sitter_rust::LANGUAGE.into()),
8665 );
8666 let language = Arc::new(language);
8667
8668 cx.language_registry().add(language.clone());
8669 cx.update_buffer(|buffer, cx| {
8670 buffer.set_language(Some(language), cx);
8671 });
8672
8673 cx.set_state(
8674 &r#"
8675 fn main() {
8676 sampleˇ
8677 }
8678 "#
8679 .unindent(),
8680 );
8681
8682 cx.update_editor(|editor, window, cx| {
8683 editor.handle_input("(", window, cx);
8684 });
8685 cx.assert_editor_state(
8686 &"
8687 fn main() {
8688 sample(ˇ)
8689 }
8690 "
8691 .unindent(),
8692 );
8693
8694 let mocked_response = lsp::SignatureHelp {
8695 signatures: vec![lsp::SignatureInformation {
8696 label: "fn sample(param1: u8, param2: u8)".to_string(),
8697 documentation: None,
8698 parameters: Some(vec![
8699 lsp::ParameterInformation {
8700 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8701 documentation: None,
8702 },
8703 lsp::ParameterInformation {
8704 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8705 documentation: None,
8706 },
8707 ]),
8708 active_parameter: None,
8709 }],
8710 active_signature: Some(0),
8711 active_parameter: Some(0),
8712 };
8713 handle_signature_help_request(&mut cx, mocked_response).await;
8714
8715 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8716 .await;
8717
8718 cx.editor(|editor, _, _| {
8719 let signature_help_state = editor.signature_help_state.popover().cloned();
8720 assert_eq!(
8721 signature_help_state.unwrap().label,
8722 "param1: u8, param2: u8"
8723 );
8724 });
8725}
8726
8727#[gpui::test]
8728async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8729 init_test(cx, |_| {});
8730
8731 cx.update(|cx| {
8732 cx.update_global::<SettingsStore, _>(|settings, cx| {
8733 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8734 settings.auto_signature_help = Some(false);
8735 settings.show_signature_help_after_edits = Some(false);
8736 });
8737 });
8738 });
8739
8740 let mut cx = EditorLspTestContext::new_rust(
8741 lsp::ServerCapabilities {
8742 signature_help_provider: Some(lsp::SignatureHelpOptions {
8743 ..Default::default()
8744 }),
8745 ..Default::default()
8746 },
8747 cx,
8748 )
8749 .await;
8750
8751 let language = Language::new(
8752 LanguageConfig {
8753 name: "Rust".into(),
8754 brackets: BracketPairConfig {
8755 pairs: vec![
8756 BracketPair {
8757 start: "{".to_string(),
8758 end: "}".to_string(),
8759 close: true,
8760 surround: true,
8761 newline: true,
8762 },
8763 BracketPair {
8764 start: "(".to_string(),
8765 end: ")".to_string(),
8766 close: true,
8767 surround: true,
8768 newline: true,
8769 },
8770 BracketPair {
8771 start: "/*".to_string(),
8772 end: " */".to_string(),
8773 close: true,
8774 surround: true,
8775 newline: true,
8776 },
8777 BracketPair {
8778 start: "[".to_string(),
8779 end: "]".to_string(),
8780 close: false,
8781 surround: false,
8782 newline: true,
8783 },
8784 BracketPair {
8785 start: "\"".to_string(),
8786 end: "\"".to_string(),
8787 close: true,
8788 surround: true,
8789 newline: false,
8790 },
8791 BracketPair {
8792 start: "<".to_string(),
8793 end: ">".to_string(),
8794 close: false,
8795 surround: true,
8796 newline: true,
8797 },
8798 ],
8799 ..Default::default()
8800 },
8801 autoclose_before: "})]".to_string(),
8802 ..Default::default()
8803 },
8804 Some(tree_sitter_rust::LANGUAGE.into()),
8805 );
8806 let language = Arc::new(language);
8807
8808 cx.language_registry().add(language.clone());
8809 cx.update_buffer(|buffer, cx| {
8810 buffer.set_language(Some(language), cx);
8811 });
8812
8813 // Ensure that signature_help is not called when no signature help is enabled.
8814 cx.set_state(
8815 &r#"
8816 fn main() {
8817 sampleˇ
8818 }
8819 "#
8820 .unindent(),
8821 );
8822 cx.update_editor(|editor, window, cx| {
8823 editor.handle_input("(", window, cx);
8824 });
8825 cx.assert_editor_state(
8826 &"
8827 fn main() {
8828 sample(ˇ)
8829 }
8830 "
8831 .unindent(),
8832 );
8833 cx.editor(|editor, _, _| {
8834 assert!(editor.signature_help_state.task().is_none());
8835 });
8836
8837 let mocked_response = lsp::SignatureHelp {
8838 signatures: vec![lsp::SignatureInformation {
8839 label: "fn sample(param1: u8, param2: u8)".to_string(),
8840 documentation: None,
8841 parameters: Some(vec![
8842 lsp::ParameterInformation {
8843 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8844 documentation: None,
8845 },
8846 lsp::ParameterInformation {
8847 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8848 documentation: None,
8849 },
8850 ]),
8851 active_parameter: None,
8852 }],
8853 active_signature: Some(0),
8854 active_parameter: Some(0),
8855 };
8856
8857 // Ensure that signature_help is called when enabled afte edits
8858 cx.update(|_, cx| {
8859 cx.update_global::<SettingsStore, _>(|settings, cx| {
8860 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8861 settings.auto_signature_help = Some(false);
8862 settings.show_signature_help_after_edits = Some(true);
8863 });
8864 });
8865 });
8866 cx.set_state(
8867 &r#"
8868 fn main() {
8869 sampleˇ
8870 }
8871 "#
8872 .unindent(),
8873 );
8874 cx.update_editor(|editor, window, cx| {
8875 editor.handle_input("(", window, cx);
8876 });
8877 cx.assert_editor_state(
8878 &"
8879 fn main() {
8880 sample(ˇ)
8881 }
8882 "
8883 .unindent(),
8884 );
8885 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8886 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8887 .await;
8888 cx.update_editor(|editor, _, _| {
8889 let signature_help_state = editor.signature_help_state.popover().cloned();
8890 assert!(signature_help_state.is_some());
8891 assert_eq!(
8892 signature_help_state.unwrap().label,
8893 "param1: u8, param2: u8"
8894 );
8895 editor.signature_help_state = SignatureHelpState::default();
8896 });
8897
8898 // Ensure that signature_help is called when auto signature help override is enabled
8899 cx.update(|_, cx| {
8900 cx.update_global::<SettingsStore, _>(|settings, cx| {
8901 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8902 settings.auto_signature_help = Some(true);
8903 settings.show_signature_help_after_edits = Some(false);
8904 });
8905 });
8906 });
8907 cx.set_state(
8908 &r#"
8909 fn main() {
8910 sampleˇ
8911 }
8912 "#
8913 .unindent(),
8914 );
8915 cx.update_editor(|editor, window, cx| {
8916 editor.handle_input("(", window, cx);
8917 });
8918 cx.assert_editor_state(
8919 &"
8920 fn main() {
8921 sample(ˇ)
8922 }
8923 "
8924 .unindent(),
8925 );
8926 handle_signature_help_request(&mut cx, mocked_response).await;
8927 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8928 .await;
8929 cx.editor(|editor, _, _| {
8930 let signature_help_state = editor.signature_help_state.popover().cloned();
8931 assert!(signature_help_state.is_some());
8932 assert_eq!(
8933 signature_help_state.unwrap().label,
8934 "param1: u8, param2: u8"
8935 );
8936 });
8937}
8938
8939#[gpui::test]
8940async fn test_signature_help(cx: &mut TestAppContext) {
8941 init_test(cx, |_| {});
8942 cx.update(|cx| {
8943 cx.update_global::<SettingsStore, _>(|settings, cx| {
8944 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8945 settings.auto_signature_help = Some(true);
8946 });
8947 });
8948 });
8949
8950 let mut cx = EditorLspTestContext::new_rust(
8951 lsp::ServerCapabilities {
8952 signature_help_provider: Some(lsp::SignatureHelpOptions {
8953 ..Default::default()
8954 }),
8955 ..Default::default()
8956 },
8957 cx,
8958 )
8959 .await;
8960
8961 // A test that directly calls `show_signature_help`
8962 cx.update_editor(|editor, window, cx| {
8963 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8964 });
8965
8966 let mocked_response = lsp::SignatureHelp {
8967 signatures: vec![lsp::SignatureInformation {
8968 label: "fn sample(param1: u8, param2: u8)".to_string(),
8969 documentation: None,
8970 parameters: Some(vec![
8971 lsp::ParameterInformation {
8972 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8973 documentation: None,
8974 },
8975 lsp::ParameterInformation {
8976 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8977 documentation: None,
8978 },
8979 ]),
8980 active_parameter: None,
8981 }],
8982 active_signature: Some(0),
8983 active_parameter: Some(0),
8984 };
8985 handle_signature_help_request(&mut cx, mocked_response).await;
8986
8987 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8988 .await;
8989
8990 cx.editor(|editor, _, _| {
8991 let signature_help_state = editor.signature_help_state.popover().cloned();
8992 assert!(signature_help_state.is_some());
8993 assert_eq!(
8994 signature_help_state.unwrap().label,
8995 "param1: u8, param2: u8"
8996 );
8997 });
8998
8999 // When exiting outside from inside the brackets, `signature_help` is closed.
9000 cx.set_state(indoc! {"
9001 fn main() {
9002 sample(ˇ);
9003 }
9004
9005 fn sample(param1: u8, param2: u8) {}
9006 "});
9007
9008 cx.update_editor(|editor, window, cx| {
9009 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9010 });
9011
9012 let mocked_response = lsp::SignatureHelp {
9013 signatures: Vec::new(),
9014 active_signature: None,
9015 active_parameter: None,
9016 };
9017 handle_signature_help_request(&mut cx, mocked_response).await;
9018
9019 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9020 .await;
9021
9022 cx.editor(|editor, _, _| {
9023 assert!(!editor.signature_help_state.is_shown());
9024 });
9025
9026 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9027 cx.set_state(indoc! {"
9028 fn main() {
9029 sample(ˇ);
9030 }
9031
9032 fn sample(param1: u8, param2: u8) {}
9033 "});
9034
9035 let mocked_response = lsp::SignatureHelp {
9036 signatures: vec![lsp::SignatureInformation {
9037 label: "fn sample(param1: u8, param2: u8)".to_string(),
9038 documentation: None,
9039 parameters: Some(vec![
9040 lsp::ParameterInformation {
9041 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9042 documentation: None,
9043 },
9044 lsp::ParameterInformation {
9045 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9046 documentation: None,
9047 },
9048 ]),
9049 active_parameter: None,
9050 }],
9051 active_signature: Some(0),
9052 active_parameter: Some(0),
9053 };
9054 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9055 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9056 .await;
9057 cx.editor(|editor, _, _| {
9058 assert!(editor.signature_help_state.is_shown());
9059 });
9060
9061 // Restore the popover with more parameter input
9062 cx.set_state(indoc! {"
9063 fn main() {
9064 sample(param1, param2ˇ);
9065 }
9066
9067 fn sample(param1: u8, param2: u8) {}
9068 "});
9069
9070 let mocked_response = lsp::SignatureHelp {
9071 signatures: vec![lsp::SignatureInformation {
9072 label: "fn sample(param1: u8, param2: u8)".to_string(),
9073 documentation: None,
9074 parameters: Some(vec![
9075 lsp::ParameterInformation {
9076 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9077 documentation: None,
9078 },
9079 lsp::ParameterInformation {
9080 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9081 documentation: None,
9082 },
9083 ]),
9084 active_parameter: None,
9085 }],
9086 active_signature: Some(0),
9087 active_parameter: Some(1),
9088 };
9089 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9090 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9091 .await;
9092
9093 // When selecting a range, the popover is gone.
9094 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9095 cx.update_editor(|editor, window, cx| {
9096 editor.change_selections(None, window, cx, |s| {
9097 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9098 })
9099 });
9100 cx.assert_editor_state(indoc! {"
9101 fn main() {
9102 sample(param1, «ˇparam2»);
9103 }
9104
9105 fn sample(param1: u8, param2: u8) {}
9106 "});
9107 cx.editor(|editor, _, _| {
9108 assert!(!editor.signature_help_state.is_shown());
9109 });
9110
9111 // When unselecting again, the popover is back if within the brackets.
9112 cx.update_editor(|editor, window, cx| {
9113 editor.change_selections(None, window, cx, |s| {
9114 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9115 })
9116 });
9117 cx.assert_editor_state(indoc! {"
9118 fn main() {
9119 sample(param1, ˇparam2);
9120 }
9121
9122 fn sample(param1: u8, param2: u8) {}
9123 "});
9124 handle_signature_help_request(&mut cx, mocked_response).await;
9125 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9126 .await;
9127 cx.editor(|editor, _, _| {
9128 assert!(editor.signature_help_state.is_shown());
9129 });
9130
9131 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9132 cx.update_editor(|editor, window, cx| {
9133 editor.change_selections(None, window, cx, |s| {
9134 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9135 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9136 })
9137 });
9138 cx.assert_editor_state(indoc! {"
9139 fn main() {
9140 sample(param1, ˇparam2);
9141 }
9142
9143 fn sample(param1: u8, param2: u8) {}
9144 "});
9145
9146 let mocked_response = lsp::SignatureHelp {
9147 signatures: vec![lsp::SignatureInformation {
9148 label: "fn sample(param1: u8, param2: u8)".to_string(),
9149 documentation: None,
9150 parameters: Some(vec![
9151 lsp::ParameterInformation {
9152 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9153 documentation: None,
9154 },
9155 lsp::ParameterInformation {
9156 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9157 documentation: None,
9158 },
9159 ]),
9160 active_parameter: None,
9161 }],
9162 active_signature: Some(0),
9163 active_parameter: Some(1),
9164 };
9165 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9166 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9167 .await;
9168 cx.update_editor(|editor, _, cx| {
9169 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9170 });
9171 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9172 .await;
9173 cx.update_editor(|editor, window, cx| {
9174 editor.change_selections(None, window, cx, |s| {
9175 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9176 })
9177 });
9178 cx.assert_editor_state(indoc! {"
9179 fn main() {
9180 sample(param1, «ˇparam2»);
9181 }
9182
9183 fn sample(param1: u8, param2: u8) {}
9184 "});
9185 cx.update_editor(|editor, window, cx| {
9186 editor.change_selections(None, window, cx, |s| {
9187 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9188 })
9189 });
9190 cx.assert_editor_state(indoc! {"
9191 fn main() {
9192 sample(param1, ˇparam2);
9193 }
9194
9195 fn sample(param1: u8, param2: u8) {}
9196 "});
9197 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9198 .await;
9199}
9200
9201#[gpui::test]
9202async fn test_completion_mode(cx: &mut TestAppContext) {
9203 init_test(cx, |_| {});
9204 let mut cx = EditorLspTestContext::new_rust(
9205 lsp::ServerCapabilities {
9206 completion_provider: Some(lsp::CompletionOptions {
9207 resolve_provider: Some(true),
9208 ..Default::default()
9209 }),
9210 ..Default::default()
9211 },
9212 cx,
9213 )
9214 .await;
9215
9216 struct Run {
9217 run_description: &'static str,
9218 initial_state: String,
9219 buffer_marked_text: String,
9220 completion_text: &'static str,
9221 expected_with_insert_mode: String,
9222 expected_with_replace_mode: String,
9223 expected_with_replace_subsequence_mode: String,
9224 expected_with_replace_suffix_mode: String,
9225 }
9226
9227 let runs = [
9228 Run {
9229 run_description: "Start of word matches completion text",
9230 initial_state: "before ediˇ after".into(),
9231 buffer_marked_text: "before <edi|> after".into(),
9232 completion_text: "editor",
9233 expected_with_insert_mode: "before editorˇ after".into(),
9234 expected_with_replace_mode: "before editorˇ after".into(),
9235 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9236 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9237 },
9238 Run {
9239 run_description: "Accept same text at the middle of the word",
9240 initial_state: "before ediˇtor after".into(),
9241 buffer_marked_text: "before <edi|tor> after".into(),
9242 completion_text: "editor",
9243 expected_with_insert_mode: "before editorˇtor after".into(),
9244 expected_with_replace_mode: "before ediˇtor after".into(),
9245 expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
9246 expected_with_replace_suffix_mode: "before ediˇtor after".into(),
9247 },
9248 Run {
9249 run_description: "End of word matches completion text -- cursor at end",
9250 initial_state: "before torˇ after".into(),
9251 buffer_marked_text: "before <tor|> after".into(),
9252 completion_text: "editor",
9253 expected_with_insert_mode: "before editorˇ after".into(),
9254 expected_with_replace_mode: "before editorˇ after".into(),
9255 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9256 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9257 },
9258 Run {
9259 run_description: "End of word matches completion text -- cursor at start",
9260 initial_state: "before ˇtor after".into(),
9261 buffer_marked_text: "before <|tor> after".into(),
9262 completion_text: "editor",
9263 expected_with_insert_mode: "before editorˇtor after".into(),
9264 expected_with_replace_mode: "before editorˇ after".into(),
9265 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9266 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9267 },
9268 Run {
9269 run_description: "Prepend text containing whitespace",
9270 initial_state: "pˇfield: bool".into(),
9271 buffer_marked_text: "<p|field>: bool".into(),
9272 completion_text: "pub ",
9273 expected_with_insert_mode: "pub ˇfield: bool".into(),
9274 expected_with_replace_mode: "pub ˇ: bool".into(),
9275 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9276 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9277 },
9278 Run {
9279 run_description: "Add element to start of list",
9280 initial_state: "[element_ˇelement_2]".into(),
9281 buffer_marked_text: "[<element_|element_2>]".into(),
9282 completion_text: "element_1",
9283 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9284 expected_with_replace_mode: "[element_1ˇ]".into(),
9285 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9286 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9287 },
9288 Run {
9289 run_description: "Add element to start of list -- first and second elements are equal",
9290 initial_state: "[elˇelement]".into(),
9291 buffer_marked_text: "[<el|element>]".into(),
9292 completion_text: "element",
9293 expected_with_insert_mode: "[elementˇelement]".into(),
9294 expected_with_replace_mode: "[elˇement]".into(),
9295 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9296 expected_with_replace_suffix_mode: "[elˇement]".into(),
9297 },
9298 Run {
9299 run_description: "Ends with matching suffix",
9300 initial_state: "SubˇError".into(),
9301 buffer_marked_text: "<Sub|Error>".into(),
9302 completion_text: "SubscriptionError",
9303 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
9304 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9305 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9306 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9307 },
9308 Run {
9309 run_description: "Suffix is a subsequence -- contiguous",
9310 initial_state: "SubˇErr".into(),
9311 buffer_marked_text: "<Sub|Err>".into(),
9312 completion_text: "SubscriptionError",
9313 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
9314 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9315 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9316 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9317 },
9318 Run {
9319 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9320 initial_state: "Suˇscrirr".into(),
9321 buffer_marked_text: "<Su|scrirr>".into(),
9322 completion_text: "SubscriptionError",
9323 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
9324 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9325 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9326 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9327 },
9328 Run {
9329 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9330 initial_state: "foo(indˇix)".into(),
9331 buffer_marked_text: "foo(<ind|ix>)".into(),
9332 completion_text: "node_index",
9333 expected_with_insert_mode: "foo(node_indexˇix)".into(),
9334 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9335 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9336 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9337 },
9338 ];
9339
9340 for run in runs {
9341 let run_variations = [
9342 (LspInsertMode::Insert, run.expected_with_insert_mode),
9343 (LspInsertMode::Replace, run.expected_with_replace_mode),
9344 (
9345 LspInsertMode::ReplaceSubsequence,
9346 run.expected_with_replace_subsequence_mode,
9347 ),
9348 (
9349 LspInsertMode::ReplaceSuffix,
9350 run.expected_with_replace_suffix_mode,
9351 ),
9352 ];
9353
9354 for (lsp_insert_mode, expected_text) in run_variations {
9355 eprintln!(
9356 "run = {:?}, mode = {lsp_insert_mode:.?}",
9357 run.run_description,
9358 );
9359
9360 update_test_language_settings(&mut cx, |settings| {
9361 settings.defaults.completions = Some(CompletionSettings {
9362 lsp_insert_mode,
9363 words: WordsCompletionMode::Disabled,
9364 lsp: true,
9365 lsp_fetch_timeout_ms: 0,
9366 });
9367 });
9368
9369 cx.set_state(&run.initial_state);
9370 cx.update_editor(|editor, window, cx| {
9371 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9372 });
9373
9374 let counter = Arc::new(AtomicUsize::new(0));
9375 handle_completion_request_with_insert_and_replace(
9376 &mut cx,
9377 &run.buffer_marked_text,
9378 vec![run.completion_text],
9379 counter.clone(),
9380 )
9381 .await;
9382 cx.condition(|editor, _| editor.context_menu_visible())
9383 .await;
9384 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9385
9386 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9387 editor
9388 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9389 .unwrap()
9390 });
9391 cx.assert_editor_state(&expected_text);
9392 handle_resolve_completion_request(&mut cx, None).await;
9393 apply_additional_edits.await.unwrap();
9394 }
9395 }
9396}
9397
9398#[gpui::test]
9399async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
9400 init_test(cx, |_| {});
9401 let mut cx = EditorLspTestContext::new_rust(
9402 lsp::ServerCapabilities {
9403 completion_provider: Some(lsp::CompletionOptions {
9404 resolve_provider: Some(true),
9405 ..Default::default()
9406 }),
9407 ..Default::default()
9408 },
9409 cx,
9410 )
9411 .await;
9412
9413 let initial_state = "SubˇError";
9414 let buffer_marked_text = "<Sub|Error>";
9415 let completion_text = "SubscriptionError";
9416 let expected_with_insert_mode = "SubscriptionErrorˇError";
9417 let expected_with_replace_mode = "SubscriptionErrorˇ";
9418
9419 update_test_language_settings(&mut cx, |settings| {
9420 settings.defaults.completions = Some(CompletionSettings {
9421 words: WordsCompletionMode::Disabled,
9422 // set the opposite here to ensure that the action is overriding the default behavior
9423 lsp_insert_mode: LspInsertMode::Insert,
9424 lsp: true,
9425 lsp_fetch_timeout_ms: 0,
9426 });
9427 });
9428
9429 cx.set_state(initial_state);
9430 cx.update_editor(|editor, window, cx| {
9431 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9432 });
9433
9434 let counter = Arc::new(AtomicUsize::new(0));
9435 handle_completion_request_with_insert_and_replace(
9436 &mut cx,
9437 &buffer_marked_text,
9438 vec![completion_text],
9439 counter.clone(),
9440 )
9441 .await;
9442 cx.condition(|editor, _| editor.context_menu_visible())
9443 .await;
9444 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9445
9446 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9447 editor
9448 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
9449 .unwrap()
9450 });
9451 cx.assert_editor_state(&expected_with_replace_mode);
9452 handle_resolve_completion_request(&mut cx, None).await;
9453 apply_additional_edits.await.unwrap();
9454
9455 update_test_language_settings(&mut cx, |settings| {
9456 settings.defaults.completions = Some(CompletionSettings {
9457 words: WordsCompletionMode::Disabled,
9458 // set the opposite here to ensure that the action is overriding the default behavior
9459 lsp_insert_mode: LspInsertMode::Replace,
9460 lsp: true,
9461 lsp_fetch_timeout_ms: 0,
9462 });
9463 });
9464
9465 cx.set_state(initial_state);
9466 cx.update_editor(|editor, window, cx| {
9467 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9468 });
9469 handle_completion_request_with_insert_and_replace(
9470 &mut cx,
9471 &buffer_marked_text,
9472 vec![completion_text],
9473 counter.clone(),
9474 )
9475 .await;
9476 cx.condition(|editor, _| editor.context_menu_visible())
9477 .await;
9478 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9479
9480 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9481 editor
9482 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
9483 .unwrap()
9484 });
9485 cx.assert_editor_state(&expected_with_insert_mode);
9486 handle_resolve_completion_request(&mut cx, None).await;
9487 apply_additional_edits.await.unwrap();
9488}
9489
9490#[gpui::test]
9491async fn test_completion(cx: &mut TestAppContext) {
9492 init_test(cx, |_| {});
9493
9494 let mut cx = EditorLspTestContext::new_rust(
9495 lsp::ServerCapabilities {
9496 completion_provider: Some(lsp::CompletionOptions {
9497 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9498 resolve_provider: Some(true),
9499 ..Default::default()
9500 }),
9501 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9502 ..Default::default()
9503 },
9504 cx,
9505 )
9506 .await;
9507 let counter = Arc::new(AtomicUsize::new(0));
9508
9509 cx.set_state(indoc! {"
9510 oneˇ
9511 two
9512 three
9513 "});
9514 cx.simulate_keystroke(".");
9515 handle_completion_request(
9516 &mut cx,
9517 indoc! {"
9518 one.|<>
9519 two
9520 three
9521 "},
9522 vec!["first_completion", "second_completion"],
9523 counter.clone(),
9524 )
9525 .await;
9526 cx.condition(|editor, _| editor.context_menu_visible())
9527 .await;
9528 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9529
9530 let _handler = handle_signature_help_request(
9531 &mut cx,
9532 lsp::SignatureHelp {
9533 signatures: vec![lsp::SignatureInformation {
9534 label: "test signature".to_string(),
9535 documentation: None,
9536 parameters: Some(vec![lsp::ParameterInformation {
9537 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9538 documentation: None,
9539 }]),
9540 active_parameter: None,
9541 }],
9542 active_signature: None,
9543 active_parameter: None,
9544 },
9545 );
9546 cx.update_editor(|editor, window, cx| {
9547 assert!(
9548 !editor.signature_help_state.is_shown(),
9549 "No signature help was called for"
9550 );
9551 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9552 });
9553 cx.run_until_parked();
9554 cx.update_editor(|editor, _, _| {
9555 assert!(
9556 !editor.signature_help_state.is_shown(),
9557 "No signature help should be shown when completions menu is open"
9558 );
9559 });
9560
9561 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9562 editor.context_menu_next(&Default::default(), window, cx);
9563 editor
9564 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9565 .unwrap()
9566 });
9567 cx.assert_editor_state(indoc! {"
9568 one.second_completionˇ
9569 two
9570 three
9571 "});
9572
9573 handle_resolve_completion_request(
9574 &mut cx,
9575 Some(vec![
9576 (
9577 //This overlaps with the primary completion edit which is
9578 //misbehavior from the LSP spec, test that we filter it out
9579 indoc! {"
9580 one.second_ˇcompletion
9581 two
9582 threeˇ
9583 "},
9584 "overlapping additional edit",
9585 ),
9586 (
9587 indoc! {"
9588 one.second_completion
9589 two
9590 threeˇ
9591 "},
9592 "\nadditional edit",
9593 ),
9594 ]),
9595 )
9596 .await;
9597 apply_additional_edits.await.unwrap();
9598 cx.assert_editor_state(indoc! {"
9599 one.second_completionˇ
9600 two
9601 three
9602 additional edit
9603 "});
9604
9605 cx.set_state(indoc! {"
9606 one.second_completion
9607 twoˇ
9608 threeˇ
9609 additional edit
9610 "});
9611 cx.simulate_keystroke(" ");
9612 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9613 cx.simulate_keystroke("s");
9614 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9615
9616 cx.assert_editor_state(indoc! {"
9617 one.second_completion
9618 two sˇ
9619 three sˇ
9620 additional edit
9621 "});
9622 handle_completion_request(
9623 &mut cx,
9624 indoc! {"
9625 one.second_completion
9626 two s
9627 three <s|>
9628 additional edit
9629 "},
9630 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9631 counter.clone(),
9632 )
9633 .await;
9634 cx.condition(|editor, _| editor.context_menu_visible())
9635 .await;
9636 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9637
9638 cx.simulate_keystroke("i");
9639
9640 handle_completion_request(
9641 &mut cx,
9642 indoc! {"
9643 one.second_completion
9644 two si
9645 three <si|>
9646 additional edit
9647 "},
9648 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9649 counter.clone(),
9650 )
9651 .await;
9652 cx.condition(|editor, _| editor.context_menu_visible())
9653 .await;
9654 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9655
9656 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9657 editor
9658 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9659 .unwrap()
9660 });
9661 cx.assert_editor_state(indoc! {"
9662 one.second_completion
9663 two sixth_completionˇ
9664 three sixth_completionˇ
9665 additional edit
9666 "});
9667
9668 apply_additional_edits.await.unwrap();
9669
9670 update_test_language_settings(&mut cx, |settings| {
9671 settings.defaults.show_completions_on_input = Some(false);
9672 });
9673 cx.set_state("editorˇ");
9674 cx.simulate_keystroke(".");
9675 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9676 cx.simulate_keystrokes("c l o");
9677 cx.assert_editor_state("editor.cloˇ");
9678 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9679 cx.update_editor(|editor, window, cx| {
9680 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9681 });
9682 handle_completion_request(
9683 &mut cx,
9684 "editor.<clo|>",
9685 vec!["close", "clobber"],
9686 counter.clone(),
9687 )
9688 .await;
9689 cx.condition(|editor, _| editor.context_menu_visible())
9690 .await;
9691 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9692
9693 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9694 editor
9695 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9696 .unwrap()
9697 });
9698 cx.assert_editor_state("editor.closeˇ");
9699 handle_resolve_completion_request(&mut cx, None).await;
9700 apply_additional_edits.await.unwrap();
9701}
9702
9703#[gpui::test]
9704async fn test_word_completion(cx: &mut TestAppContext) {
9705 let lsp_fetch_timeout_ms = 10;
9706 init_test(cx, |language_settings| {
9707 language_settings.defaults.completions = Some(CompletionSettings {
9708 words: WordsCompletionMode::Fallback,
9709 lsp: true,
9710 lsp_fetch_timeout_ms: 10,
9711 lsp_insert_mode: LspInsertMode::Insert,
9712 });
9713 });
9714
9715 let mut cx = EditorLspTestContext::new_rust(
9716 lsp::ServerCapabilities {
9717 completion_provider: Some(lsp::CompletionOptions {
9718 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9719 ..lsp::CompletionOptions::default()
9720 }),
9721 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9722 ..lsp::ServerCapabilities::default()
9723 },
9724 cx,
9725 )
9726 .await;
9727
9728 let throttle_completions = Arc::new(AtomicBool::new(false));
9729
9730 let lsp_throttle_completions = throttle_completions.clone();
9731 let _completion_requests_handler =
9732 cx.lsp
9733 .server
9734 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9735 let lsp_throttle_completions = lsp_throttle_completions.clone();
9736 let cx = cx.clone();
9737 async move {
9738 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9739 cx.background_executor()
9740 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9741 .await;
9742 }
9743 Ok(Some(lsp::CompletionResponse::Array(vec![
9744 lsp::CompletionItem {
9745 label: "first".into(),
9746 ..lsp::CompletionItem::default()
9747 },
9748 lsp::CompletionItem {
9749 label: "last".into(),
9750 ..lsp::CompletionItem::default()
9751 },
9752 ])))
9753 }
9754 });
9755
9756 cx.set_state(indoc! {"
9757 oneˇ
9758 two
9759 three
9760 "});
9761 cx.simulate_keystroke(".");
9762 cx.executor().run_until_parked();
9763 cx.condition(|editor, _| editor.context_menu_visible())
9764 .await;
9765 cx.update_editor(|editor, window, cx| {
9766 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9767 {
9768 assert_eq!(
9769 completion_menu_entries(&menu),
9770 &["first", "last"],
9771 "When LSP server is fast to reply, no fallback word completions are used"
9772 );
9773 } else {
9774 panic!("expected completion menu to be open");
9775 }
9776 editor.cancel(&Cancel, window, cx);
9777 });
9778 cx.executor().run_until_parked();
9779 cx.condition(|editor, _| !editor.context_menu_visible())
9780 .await;
9781
9782 throttle_completions.store(true, atomic::Ordering::Release);
9783 cx.simulate_keystroke(".");
9784 cx.executor()
9785 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9786 cx.executor().run_until_parked();
9787 cx.condition(|editor, _| editor.context_menu_visible())
9788 .await;
9789 cx.update_editor(|editor, _, _| {
9790 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9791 {
9792 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9793 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9794 } else {
9795 panic!("expected completion menu to be open");
9796 }
9797 });
9798}
9799
9800#[gpui::test]
9801async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9802 init_test(cx, |language_settings| {
9803 language_settings.defaults.completions = Some(CompletionSettings {
9804 words: WordsCompletionMode::Enabled,
9805 lsp: true,
9806 lsp_fetch_timeout_ms: 0,
9807 lsp_insert_mode: LspInsertMode::Insert,
9808 });
9809 });
9810
9811 let mut cx = EditorLspTestContext::new_rust(
9812 lsp::ServerCapabilities {
9813 completion_provider: Some(lsp::CompletionOptions {
9814 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9815 ..lsp::CompletionOptions::default()
9816 }),
9817 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9818 ..lsp::ServerCapabilities::default()
9819 },
9820 cx,
9821 )
9822 .await;
9823
9824 let _completion_requests_handler =
9825 cx.lsp
9826 .server
9827 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9828 Ok(Some(lsp::CompletionResponse::Array(vec![
9829 lsp::CompletionItem {
9830 label: "first".into(),
9831 ..lsp::CompletionItem::default()
9832 },
9833 lsp::CompletionItem {
9834 label: "last".into(),
9835 ..lsp::CompletionItem::default()
9836 },
9837 ])))
9838 });
9839
9840 cx.set_state(indoc! {"ˇ
9841 first
9842 last
9843 second
9844 "});
9845 cx.simulate_keystroke(".");
9846 cx.executor().run_until_parked();
9847 cx.condition(|editor, _| editor.context_menu_visible())
9848 .await;
9849 cx.update_editor(|editor, _, _| {
9850 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9851 {
9852 assert_eq!(
9853 completion_menu_entries(&menu),
9854 &["first", "last", "second"],
9855 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9856 );
9857 } else {
9858 panic!("expected completion menu to be open");
9859 }
9860 });
9861}
9862
9863#[gpui::test]
9864async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9865 init_test(cx, |language_settings| {
9866 language_settings.defaults.completions = Some(CompletionSettings {
9867 words: WordsCompletionMode::Disabled,
9868 lsp: true,
9869 lsp_fetch_timeout_ms: 0,
9870 lsp_insert_mode: LspInsertMode::Insert,
9871 });
9872 });
9873
9874 let mut cx = EditorLspTestContext::new_rust(
9875 lsp::ServerCapabilities {
9876 completion_provider: Some(lsp::CompletionOptions {
9877 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9878 ..lsp::CompletionOptions::default()
9879 }),
9880 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9881 ..lsp::ServerCapabilities::default()
9882 },
9883 cx,
9884 )
9885 .await;
9886
9887 let _completion_requests_handler =
9888 cx.lsp
9889 .server
9890 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9891 panic!("LSP completions should not be queried when dealing with word completions")
9892 });
9893
9894 cx.set_state(indoc! {"ˇ
9895 first
9896 last
9897 second
9898 "});
9899 cx.update_editor(|editor, window, cx| {
9900 editor.show_word_completions(&ShowWordCompletions, window, cx);
9901 });
9902 cx.executor().run_until_parked();
9903 cx.condition(|editor, _| editor.context_menu_visible())
9904 .await;
9905 cx.update_editor(|editor, _, _| {
9906 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9907 {
9908 assert_eq!(
9909 completion_menu_entries(&menu),
9910 &["first", "last", "second"],
9911 "`ShowWordCompletions` action should show word completions"
9912 );
9913 } else {
9914 panic!("expected completion menu to be open");
9915 }
9916 });
9917
9918 cx.simulate_keystroke("l");
9919 cx.executor().run_until_parked();
9920 cx.condition(|editor, _| editor.context_menu_visible())
9921 .await;
9922 cx.update_editor(|editor, _, _| {
9923 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9924 {
9925 assert_eq!(
9926 completion_menu_entries(&menu),
9927 &["last"],
9928 "After showing word completions, further editing should filter them and not query the LSP"
9929 );
9930 } else {
9931 panic!("expected completion menu to be open");
9932 }
9933 });
9934}
9935
9936#[gpui::test]
9937async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9938 init_test(cx, |language_settings| {
9939 language_settings.defaults.completions = Some(CompletionSettings {
9940 words: WordsCompletionMode::Fallback,
9941 lsp: false,
9942 lsp_fetch_timeout_ms: 0,
9943 lsp_insert_mode: LspInsertMode::Insert,
9944 });
9945 });
9946
9947 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9948
9949 cx.set_state(indoc! {"ˇ
9950 0_usize
9951 let
9952 33
9953 4.5f32
9954 "});
9955 cx.update_editor(|editor, window, cx| {
9956 editor.show_completions(&ShowCompletions::default(), window, cx);
9957 });
9958 cx.executor().run_until_parked();
9959 cx.condition(|editor, _| editor.context_menu_visible())
9960 .await;
9961 cx.update_editor(|editor, window, cx| {
9962 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9963 {
9964 assert_eq!(
9965 completion_menu_entries(&menu),
9966 &["let"],
9967 "With no digits in the completion query, no digits should be in the word completions"
9968 );
9969 } else {
9970 panic!("expected completion menu to be open");
9971 }
9972 editor.cancel(&Cancel, window, cx);
9973 });
9974
9975 cx.set_state(indoc! {"3ˇ
9976 0_usize
9977 let
9978 3
9979 33.35f32
9980 "});
9981 cx.update_editor(|editor, window, cx| {
9982 editor.show_completions(&ShowCompletions::default(), window, cx);
9983 });
9984 cx.executor().run_until_parked();
9985 cx.condition(|editor, _| editor.context_menu_visible())
9986 .await;
9987 cx.update_editor(|editor, _, _| {
9988 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9989 {
9990 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
9991 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
9992 } else {
9993 panic!("expected completion menu to be open");
9994 }
9995 });
9996}
9997
9998fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
9999 let position = || lsp::Position {
10000 line: params.text_document_position.position.line,
10001 character: params.text_document_position.position.character,
10002 };
10003 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10004 range: lsp::Range {
10005 start: position(),
10006 end: position(),
10007 },
10008 new_text: text.to_string(),
10009 }))
10010}
10011
10012#[gpui::test]
10013async fn test_multiline_completion(cx: &mut TestAppContext) {
10014 init_test(cx, |_| {});
10015
10016 let fs = FakeFs::new(cx.executor());
10017 fs.insert_tree(
10018 path!("/a"),
10019 json!({
10020 "main.ts": "a",
10021 }),
10022 )
10023 .await;
10024
10025 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10026 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10027 let typescript_language = Arc::new(Language::new(
10028 LanguageConfig {
10029 name: "TypeScript".into(),
10030 matcher: LanguageMatcher {
10031 path_suffixes: vec!["ts".to_string()],
10032 ..LanguageMatcher::default()
10033 },
10034 line_comments: vec!["// ".into()],
10035 ..LanguageConfig::default()
10036 },
10037 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10038 ));
10039 language_registry.add(typescript_language.clone());
10040 let mut fake_servers = language_registry.register_fake_lsp(
10041 "TypeScript",
10042 FakeLspAdapter {
10043 capabilities: lsp::ServerCapabilities {
10044 completion_provider: Some(lsp::CompletionOptions {
10045 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10046 ..lsp::CompletionOptions::default()
10047 }),
10048 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10049 ..lsp::ServerCapabilities::default()
10050 },
10051 // Emulate vtsls label generation
10052 label_for_completion: Some(Box::new(|item, _| {
10053 let text = if let Some(description) = item
10054 .label_details
10055 .as_ref()
10056 .and_then(|label_details| label_details.description.as_ref())
10057 {
10058 format!("{} {}", item.label, description)
10059 } else if let Some(detail) = &item.detail {
10060 format!("{} {}", item.label, detail)
10061 } else {
10062 item.label.clone()
10063 };
10064 let len = text.len();
10065 Some(language::CodeLabel {
10066 text,
10067 runs: Vec::new(),
10068 filter_range: 0..len,
10069 })
10070 })),
10071 ..FakeLspAdapter::default()
10072 },
10073 );
10074 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10075 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10076 let worktree_id = workspace
10077 .update(cx, |workspace, _window, cx| {
10078 workspace.project().update(cx, |project, cx| {
10079 project.worktrees(cx).next().unwrap().read(cx).id()
10080 })
10081 })
10082 .unwrap();
10083 let _buffer = project
10084 .update(cx, |project, cx| {
10085 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
10086 })
10087 .await
10088 .unwrap();
10089 let editor = workspace
10090 .update(cx, |workspace, window, cx| {
10091 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10092 })
10093 .unwrap()
10094 .await
10095 .unwrap()
10096 .downcast::<Editor>()
10097 .unwrap();
10098 let fake_server = fake_servers.next().await.unwrap();
10099
10100 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10101 let multiline_label_2 = "a\nb\nc\n";
10102 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10103 let multiline_description = "d\ne\nf\n";
10104 let multiline_detail_2 = "g\nh\ni\n";
10105
10106 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10107 move |params, _| async move {
10108 Ok(Some(lsp::CompletionResponse::Array(vec![
10109 lsp::CompletionItem {
10110 label: multiline_label.to_string(),
10111 text_edit: gen_text_edit(¶ms, "new_text_1"),
10112 ..lsp::CompletionItem::default()
10113 },
10114 lsp::CompletionItem {
10115 label: "single line label 1".to_string(),
10116 detail: Some(multiline_detail.to_string()),
10117 text_edit: gen_text_edit(¶ms, "new_text_2"),
10118 ..lsp::CompletionItem::default()
10119 },
10120 lsp::CompletionItem {
10121 label: "single line label 2".to_string(),
10122 label_details: Some(lsp::CompletionItemLabelDetails {
10123 description: Some(multiline_description.to_string()),
10124 detail: None,
10125 }),
10126 text_edit: gen_text_edit(¶ms, "new_text_2"),
10127 ..lsp::CompletionItem::default()
10128 },
10129 lsp::CompletionItem {
10130 label: multiline_label_2.to_string(),
10131 detail: Some(multiline_detail_2.to_string()),
10132 text_edit: gen_text_edit(¶ms, "new_text_3"),
10133 ..lsp::CompletionItem::default()
10134 },
10135 lsp::CompletionItem {
10136 label: "Label with many spaces and \t but without newlines".to_string(),
10137 detail: Some(
10138 "Details with many spaces and \t but without newlines".to_string(),
10139 ),
10140 text_edit: gen_text_edit(¶ms, "new_text_4"),
10141 ..lsp::CompletionItem::default()
10142 },
10143 ])))
10144 },
10145 );
10146
10147 editor.update_in(cx, |editor, window, cx| {
10148 cx.focus_self(window);
10149 editor.move_to_end(&MoveToEnd, window, cx);
10150 editor.handle_input(".", window, cx);
10151 });
10152 cx.run_until_parked();
10153 completion_handle.next().await.unwrap();
10154
10155 editor.update(cx, |editor, _| {
10156 assert!(editor.context_menu_visible());
10157 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10158 {
10159 let completion_labels = menu
10160 .completions
10161 .borrow()
10162 .iter()
10163 .map(|c| c.label.text.clone())
10164 .collect::<Vec<_>>();
10165 assert_eq!(
10166 completion_labels,
10167 &[
10168 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10169 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10170 "single line label 2 d e f ",
10171 "a b c g h i ",
10172 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
10173 ],
10174 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
10175 );
10176
10177 for completion in menu
10178 .completions
10179 .borrow()
10180 .iter() {
10181 assert_eq!(
10182 completion.label.filter_range,
10183 0..completion.label.text.len(),
10184 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
10185 );
10186 }
10187 } else {
10188 panic!("expected completion menu to be open");
10189 }
10190 });
10191}
10192
10193#[gpui::test]
10194async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
10195 init_test(cx, |_| {});
10196 let mut cx = EditorLspTestContext::new_rust(
10197 lsp::ServerCapabilities {
10198 completion_provider: Some(lsp::CompletionOptions {
10199 trigger_characters: Some(vec![".".to_string()]),
10200 ..Default::default()
10201 }),
10202 ..Default::default()
10203 },
10204 cx,
10205 )
10206 .await;
10207 cx.lsp
10208 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10209 Ok(Some(lsp::CompletionResponse::Array(vec![
10210 lsp::CompletionItem {
10211 label: "first".into(),
10212 ..Default::default()
10213 },
10214 lsp::CompletionItem {
10215 label: "last".into(),
10216 ..Default::default()
10217 },
10218 ])))
10219 });
10220 cx.set_state("variableˇ");
10221 cx.simulate_keystroke(".");
10222 cx.executor().run_until_parked();
10223
10224 cx.update_editor(|editor, _, _| {
10225 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10226 {
10227 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
10228 } else {
10229 panic!("expected completion menu to be open");
10230 }
10231 });
10232
10233 cx.update_editor(|editor, window, cx| {
10234 editor.move_page_down(&MovePageDown::default(), window, cx);
10235 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10236 {
10237 assert!(
10238 menu.selected_item == 1,
10239 "expected PageDown to select the last item from the context menu"
10240 );
10241 } else {
10242 panic!("expected completion menu to stay open after PageDown");
10243 }
10244 });
10245
10246 cx.update_editor(|editor, window, cx| {
10247 editor.move_page_up(&MovePageUp::default(), window, cx);
10248 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10249 {
10250 assert!(
10251 menu.selected_item == 0,
10252 "expected PageUp to select the first item from the context menu"
10253 );
10254 } else {
10255 panic!("expected completion menu to stay open after PageUp");
10256 }
10257 });
10258}
10259
10260#[gpui::test]
10261async fn test_completion_sort(cx: &mut TestAppContext) {
10262 init_test(cx, |_| {});
10263 let mut cx = EditorLspTestContext::new_rust(
10264 lsp::ServerCapabilities {
10265 completion_provider: Some(lsp::CompletionOptions {
10266 trigger_characters: Some(vec![".".to_string()]),
10267 ..Default::default()
10268 }),
10269 ..Default::default()
10270 },
10271 cx,
10272 )
10273 .await;
10274 cx.lsp
10275 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10276 Ok(Some(lsp::CompletionResponse::Array(vec![
10277 lsp::CompletionItem {
10278 label: "Range".into(),
10279 sort_text: Some("a".into()),
10280 ..Default::default()
10281 },
10282 lsp::CompletionItem {
10283 label: "r".into(),
10284 sort_text: Some("b".into()),
10285 ..Default::default()
10286 },
10287 lsp::CompletionItem {
10288 label: "ret".into(),
10289 sort_text: Some("c".into()),
10290 ..Default::default()
10291 },
10292 lsp::CompletionItem {
10293 label: "return".into(),
10294 sort_text: Some("d".into()),
10295 ..Default::default()
10296 },
10297 lsp::CompletionItem {
10298 label: "slice".into(),
10299 sort_text: Some("d".into()),
10300 ..Default::default()
10301 },
10302 ])))
10303 });
10304 cx.set_state("rˇ");
10305 cx.executor().run_until_parked();
10306 cx.update_editor(|editor, window, cx| {
10307 editor.show_completions(
10308 &ShowCompletions {
10309 trigger: Some("r".into()),
10310 },
10311 window,
10312 cx,
10313 );
10314 });
10315 cx.executor().run_until_parked();
10316
10317 cx.update_editor(|editor, _, _| {
10318 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10319 {
10320 assert_eq!(
10321 completion_menu_entries(&menu),
10322 &["r", "ret", "Range", "return"]
10323 );
10324 } else {
10325 panic!("expected completion menu to be open");
10326 }
10327 });
10328}
10329
10330#[gpui::test]
10331async fn test_as_is_completions(cx: &mut TestAppContext) {
10332 init_test(cx, |_| {});
10333 let mut cx = EditorLspTestContext::new_rust(
10334 lsp::ServerCapabilities {
10335 completion_provider: Some(lsp::CompletionOptions {
10336 ..Default::default()
10337 }),
10338 ..Default::default()
10339 },
10340 cx,
10341 )
10342 .await;
10343 cx.lsp
10344 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10345 Ok(Some(lsp::CompletionResponse::Array(vec![
10346 lsp::CompletionItem {
10347 label: "unsafe".into(),
10348 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10349 range: lsp::Range {
10350 start: lsp::Position {
10351 line: 1,
10352 character: 2,
10353 },
10354 end: lsp::Position {
10355 line: 1,
10356 character: 3,
10357 },
10358 },
10359 new_text: "unsafe".to_string(),
10360 })),
10361 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
10362 ..Default::default()
10363 },
10364 ])))
10365 });
10366 cx.set_state("fn a() {}\n nˇ");
10367 cx.executor().run_until_parked();
10368 cx.update_editor(|editor, window, cx| {
10369 editor.show_completions(
10370 &ShowCompletions {
10371 trigger: Some("\n".into()),
10372 },
10373 window,
10374 cx,
10375 );
10376 });
10377 cx.executor().run_until_parked();
10378
10379 cx.update_editor(|editor, window, cx| {
10380 editor.confirm_completion(&Default::default(), window, cx)
10381 });
10382 cx.executor().run_until_parked();
10383 cx.assert_editor_state("fn a() {}\n unsafeˇ");
10384}
10385
10386#[gpui::test]
10387async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10388 init_test(cx, |_| {});
10389
10390 let mut cx = EditorLspTestContext::new_rust(
10391 lsp::ServerCapabilities {
10392 completion_provider: Some(lsp::CompletionOptions {
10393 trigger_characters: Some(vec![".".to_string()]),
10394 resolve_provider: Some(true),
10395 ..Default::default()
10396 }),
10397 ..Default::default()
10398 },
10399 cx,
10400 )
10401 .await;
10402
10403 cx.set_state("fn main() { let a = 2ˇ; }");
10404 cx.simulate_keystroke(".");
10405 let completion_item = lsp::CompletionItem {
10406 label: "Some".into(),
10407 kind: Some(lsp::CompletionItemKind::SNIPPET),
10408 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10409 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10410 kind: lsp::MarkupKind::Markdown,
10411 value: "```rust\nSome(2)\n```".to_string(),
10412 })),
10413 deprecated: Some(false),
10414 sort_text: Some("Some".to_string()),
10415 filter_text: Some("Some".to_string()),
10416 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10417 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10418 range: lsp::Range {
10419 start: lsp::Position {
10420 line: 0,
10421 character: 22,
10422 },
10423 end: lsp::Position {
10424 line: 0,
10425 character: 22,
10426 },
10427 },
10428 new_text: "Some(2)".to_string(),
10429 })),
10430 additional_text_edits: Some(vec![lsp::TextEdit {
10431 range: lsp::Range {
10432 start: lsp::Position {
10433 line: 0,
10434 character: 20,
10435 },
10436 end: lsp::Position {
10437 line: 0,
10438 character: 22,
10439 },
10440 },
10441 new_text: "".to_string(),
10442 }]),
10443 ..Default::default()
10444 };
10445
10446 let closure_completion_item = completion_item.clone();
10447 let counter = Arc::new(AtomicUsize::new(0));
10448 let counter_clone = counter.clone();
10449 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10450 let task_completion_item = closure_completion_item.clone();
10451 counter_clone.fetch_add(1, atomic::Ordering::Release);
10452 async move {
10453 Ok(Some(lsp::CompletionResponse::Array(vec![
10454 task_completion_item,
10455 ])))
10456 }
10457 });
10458
10459 cx.condition(|editor, _| editor.context_menu_visible())
10460 .await;
10461 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10462 assert!(request.next().await.is_some());
10463 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10464
10465 cx.simulate_keystrokes("S o m");
10466 cx.condition(|editor, _| editor.context_menu_visible())
10467 .await;
10468 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10469 assert!(request.next().await.is_some());
10470 assert!(request.next().await.is_some());
10471 assert!(request.next().await.is_some());
10472 request.close();
10473 assert!(request.next().await.is_none());
10474 assert_eq!(
10475 counter.load(atomic::Ordering::Acquire),
10476 4,
10477 "With the completions menu open, only one LSP request should happen per input"
10478 );
10479}
10480
10481#[gpui::test]
10482async fn test_toggle_comment(cx: &mut TestAppContext) {
10483 init_test(cx, |_| {});
10484 let mut cx = EditorTestContext::new(cx).await;
10485 let language = Arc::new(Language::new(
10486 LanguageConfig {
10487 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10488 ..Default::default()
10489 },
10490 Some(tree_sitter_rust::LANGUAGE.into()),
10491 ));
10492 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10493
10494 // If multiple selections intersect a line, the line is only toggled once.
10495 cx.set_state(indoc! {"
10496 fn a() {
10497 «//b();
10498 ˇ»// «c();
10499 //ˇ» d();
10500 }
10501 "});
10502
10503 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10504
10505 cx.assert_editor_state(indoc! {"
10506 fn a() {
10507 «b();
10508 c();
10509 ˇ» d();
10510 }
10511 "});
10512
10513 // The comment prefix is inserted at the same column for every line in a
10514 // selection.
10515 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10516
10517 cx.assert_editor_state(indoc! {"
10518 fn a() {
10519 // «b();
10520 // c();
10521 ˇ»// d();
10522 }
10523 "});
10524
10525 // If a selection ends at the beginning of a line, that line is not toggled.
10526 cx.set_selections_state(indoc! {"
10527 fn a() {
10528 // b();
10529 «// c();
10530 ˇ» // d();
10531 }
10532 "});
10533
10534 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10535
10536 cx.assert_editor_state(indoc! {"
10537 fn a() {
10538 // b();
10539 «c();
10540 ˇ» // d();
10541 }
10542 "});
10543
10544 // If a selection span a single line and is empty, the line is toggled.
10545 cx.set_state(indoc! {"
10546 fn a() {
10547 a();
10548 b();
10549 ˇ
10550 }
10551 "});
10552
10553 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10554
10555 cx.assert_editor_state(indoc! {"
10556 fn a() {
10557 a();
10558 b();
10559 //•ˇ
10560 }
10561 "});
10562
10563 // If a selection span multiple lines, empty lines are not toggled.
10564 cx.set_state(indoc! {"
10565 fn a() {
10566 «a();
10567
10568 c();ˇ»
10569 }
10570 "});
10571
10572 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10573
10574 cx.assert_editor_state(indoc! {"
10575 fn a() {
10576 // «a();
10577
10578 // c();ˇ»
10579 }
10580 "});
10581
10582 // If a selection includes multiple comment prefixes, all lines are uncommented.
10583 cx.set_state(indoc! {"
10584 fn a() {
10585 «// a();
10586 /// b();
10587 //! c();ˇ»
10588 }
10589 "});
10590
10591 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10592
10593 cx.assert_editor_state(indoc! {"
10594 fn a() {
10595 «a();
10596 b();
10597 c();ˇ»
10598 }
10599 "});
10600}
10601
10602#[gpui::test]
10603async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10604 init_test(cx, |_| {});
10605 let mut cx = EditorTestContext::new(cx).await;
10606 let language = Arc::new(Language::new(
10607 LanguageConfig {
10608 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10609 ..Default::default()
10610 },
10611 Some(tree_sitter_rust::LANGUAGE.into()),
10612 ));
10613 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10614
10615 let toggle_comments = &ToggleComments {
10616 advance_downwards: false,
10617 ignore_indent: true,
10618 };
10619
10620 // If multiple selections intersect a line, the line is only toggled once.
10621 cx.set_state(indoc! {"
10622 fn a() {
10623 // «b();
10624 // c();
10625 // ˇ» d();
10626 }
10627 "});
10628
10629 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10630
10631 cx.assert_editor_state(indoc! {"
10632 fn a() {
10633 «b();
10634 c();
10635 ˇ» d();
10636 }
10637 "});
10638
10639 // The comment prefix is inserted at the beginning of each line
10640 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10641
10642 cx.assert_editor_state(indoc! {"
10643 fn a() {
10644 // «b();
10645 // c();
10646 // ˇ» d();
10647 }
10648 "});
10649
10650 // If a selection ends at the beginning of a line, that line is not toggled.
10651 cx.set_selections_state(indoc! {"
10652 fn a() {
10653 // b();
10654 // «c();
10655 ˇ»// d();
10656 }
10657 "});
10658
10659 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10660
10661 cx.assert_editor_state(indoc! {"
10662 fn a() {
10663 // b();
10664 «c();
10665 ˇ»// d();
10666 }
10667 "});
10668
10669 // If a selection span a single line and is empty, the line is toggled.
10670 cx.set_state(indoc! {"
10671 fn a() {
10672 a();
10673 b();
10674 ˇ
10675 }
10676 "});
10677
10678 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10679
10680 cx.assert_editor_state(indoc! {"
10681 fn a() {
10682 a();
10683 b();
10684 //ˇ
10685 }
10686 "});
10687
10688 // If a selection span multiple lines, empty lines are not toggled.
10689 cx.set_state(indoc! {"
10690 fn a() {
10691 «a();
10692
10693 c();ˇ»
10694 }
10695 "});
10696
10697 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10698
10699 cx.assert_editor_state(indoc! {"
10700 fn a() {
10701 // «a();
10702
10703 // c();ˇ»
10704 }
10705 "});
10706
10707 // If a selection includes multiple comment prefixes, all lines are uncommented.
10708 cx.set_state(indoc! {"
10709 fn a() {
10710 // «a();
10711 /// b();
10712 //! c();ˇ»
10713 }
10714 "});
10715
10716 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10717
10718 cx.assert_editor_state(indoc! {"
10719 fn a() {
10720 «a();
10721 b();
10722 c();ˇ»
10723 }
10724 "});
10725}
10726
10727#[gpui::test]
10728async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10729 init_test(cx, |_| {});
10730
10731 let language = Arc::new(Language::new(
10732 LanguageConfig {
10733 line_comments: vec!["// ".into()],
10734 ..Default::default()
10735 },
10736 Some(tree_sitter_rust::LANGUAGE.into()),
10737 ));
10738
10739 let mut cx = EditorTestContext::new(cx).await;
10740
10741 cx.language_registry().add(language.clone());
10742 cx.update_buffer(|buffer, cx| {
10743 buffer.set_language(Some(language), cx);
10744 });
10745
10746 let toggle_comments = &ToggleComments {
10747 advance_downwards: true,
10748 ignore_indent: false,
10749 };
10750
10751 // Single cursor on one line -> advance
10752 // Cursor moves horizontally 3 characters as well on non-blank line
10753 cx.set_state(indoc!(
10754 "fn a() {
10755 ˇdog();
10756 cat();
10757 }"
10758 ));
10759 cx.update_editor(|editor, window, cx| {
10760 editor.toggle_comments(toggle_comments, window, cx);
10761 });
10762 cx.assert_editor_state(indoc!(
10763 "fn a() {
10764 // dog();
10765 catˇ();
10766 }"
10767 ));
10768
10769 // Single selection on one line -> don't advance
10770 cx.set_state(indoc!(
10771 "fn a() {
10772 «dog()ˇ»;
10773 cat();
10774 }"
10775 ));
10776 cx.update_editor(|editor, window, cx| {
10777 editor.toggle_comments(toggle_comments, window, cx);
10778 });
10779 cx.assert_editor_state(indoc!(
10780 "fn a() {
10781 // «dog()ˇ»;
10782 cat();
10783 }"
10784 ));
10785
10786 // Multiple cursors on one line -> advance
10787 cx.set_state(indoc!(
10788 "fn a() {
10789 ˇdˇog();
10790 cat();
10791 }"
10792 ));
10793 cx.update_editor(|editor, window, cx| {
10794 editor.toggle_comments(toggle_comments, window, cx);
10795 });
10796 cx.assert_editor_state(indoc!(
10797 "fn a() {
10798 // dog();
10799 catˇ(ˇ);
10800 }"
10801 ));
10802
10803 // Multiple cursors on one line, with selection -> don't advance
10804 cx.set_state(indoc!(
10805 "fn a() {
10806 ˇdˇog«()ˇ»;
10807 cat();
10808 }"
10809 ));
10810 cx.update_editor(|editor, window, cx| {
10811 editor.toggle_comments(toggle_comments, window, cx);
10812 });
10813 cx.assert_editor_state(indoc!(
10814 "fn a() {
10815 // ˇdˇog«()ˇ»;
10816 cat();
10817 }"
10818 ));
10819
10820 // Single cursor on one line -> advance
10821 // Cursor moves to column 0 on blank line
10822 cx.set_state(indoc!(
10823 "fn a() {
10824 ˇdog();
10825
10826 cat();
10827 }"
10828 ));
10829 cx.update_editor(|editor, window, cx| {
10830 editor.toggle_comments(toggle_comments, window, cx);
10831 });
10832 cx.assert_editor_state(indoc!(
10833 "fn a() {
10834 // dog();
10835 ˇ
10836 cat();
10837 }"
10838 ));
10839
10840 // Single cursor on one line -> advance
10841 // Cursor starts and ends at column 0
10842 cx.set_state(indoc!(
10843 "fn a() {
10844 ˇ dog();
10845 cat();
10846 }"
10847 ));
10848 cx.update_editor(|editor, window, cx| {
10849 editor.toggle_comments(toggle_comments, window, cx);
10850 });
10851 cx.assert_editor_state(indoc!(
10852 "fn a() {
10853 // dog();
10854 ˇ cat();
10855 }"
10856 ));
10857}
10858
10859#[gpui::test]
10860async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10861 init_test(cx, |_| {});
10862
10863 let mut cx = EditorTestContext::new(cx).await;
10864
10865 let html_language = Arc::new(
10866 Language::new(
10867 LanguageConfig {
10868 name: "HTML".into(),
10869 block_comment: Some(("<!-- ".into(), " -->".into())),
10870 ..Default::default()
10871 },
10872 Some(tree_sitter_html::LANGUAGE.into()),
10873 )
10874 .with_injection_query(
10875 r#"
10876 (script_element
10877 (raw_text) @injection.content
10878 (#set! injection.language "javascript"))
10879 "#,
10880 )
10881 .unwrap(),
10882 );
10883
10884 let javascript_language = Arc::new(Language::new(
10885 LanguageConfig {
10886 name: "JavaScript".into(),
10887 line_comments: vec!["// ".into()],
10888 ..Default::default()
10889 },
10890 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10891 ));
10892
10893 cx.language_registry().add(html_language.clone());
10894 cx.language_registry().add(javascript_language.clone());
10895 cx.update_buffer(|buffer, cx| {
10896 buffer.set_language(Some(html_language), cx);
10897 });
10898
10899 // Toggle comments for empty selections
10900 cx.set_state(
10901 &r#"
10902 <p>A</p>ˇ
10903 <p>B</p>ˇ
10904 <p>C</p>ˇ
10905 "#
10906 .unindent(),
10907 );
10908 cx.update_editor(|editor, window, cx| {
10909 editor.toggle_comments(&ToggleComments::default(), window, cx)
10910 });
10911 cx.assert_editor_state(
10912 &r#"
10913 <!-- <p>A</p>ˇ -->
10914 <!-- <p>B</p>ˇ -->
10915 <!-- <p>C</p>ˇ -->
10916 "#
10917 .unindent(),
10918 );
10919 cx.update_editor(|editor, window, cx| {
10920 editor.toggle_comments(&ToggleComments::default(), window, cx)
10921 });
10922 cx.assert_editor_state(
10923 &r#"
10924 <p>A</p>ˇ
10925 <p>B</p>ˇ
10926 <p>C</p>ˇ
10927 "#
10928 .unindent(),
10929 );
10930
10931 // Toggle comments for mixture of empty and non-empty selections, where
10932 // multiple selections occupy a given line.
10933 cx.set_state(
10934 &r#"
10935 <p>A«</p>
10936 <p>ˇ»B</p>ˇ
10937 <p>C«</p>
10938 <p>ˇ»D</p>ˇ
10939 "#
10940 .unindent(),
10941 );
10942
10943 cx.update_editor(|editor, window, cx| {
10944 editor.toggle_comments(&ToggleComments::default(), window, cx)
10945 });
10946 cx.assert_editor_state(
10947 &r#"
10948 <!-- <p>A«</p>
10949 <p>ˇ»B</p>ˇ -->
10950 <!-- <p>C«</p>
10951 <p>ˇ»D</p>ˇ -->
10952 "#
10953 .unindent(),
10954 );
10955 cx.update_editor(|editor, window, cx| {
10956 editor.toggle_comments(&ToggleComments::default(), window, cx)
10957 });
10958 cx.assert_editor_state(
10959 &r#"
10960 <p>A«</p>
10961 <p>ˇ»B</p>ˇ
10962 <p>C«</p>
10963 <p>ˇ»D</p>ˇ
10964 "#
10965 .unindent(),
10966 );
10967
10968 // Toggle comments when different languages are active for different
10969 // selections.
10970 cx.set_state(
10971 &r#"
10972 ˇ<script>
10973 ˇvar x = new Y();
10974 ˇ</script>
10975 "#
10976 .unindent(),
10977 );
10978 cx.executor().run_until_parked();
10979 cx.update_editor(|editor, window, cx| {
10980 editor.toggle_comments(&ToggleComments::default(), window, cx)
10981 });
10982 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10983 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10984 cx.assert_editor_state(
10985 &r#"
10986 <!-- ˇ<script> -->
10987 // ˇvar x = new Y();
10988 <!-- ˇ</script> -->
10989 "#
10990 .unindent(),
10991 );
10992}
10993
10994#[gpui::test]
10995fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10996 init_test(cx, |_| {});
10997
10998 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10999 let multibuffer = cx.new(|cx| {
11000 let mut multibuffer = MultiBuffer::new(ReadWrite);
11001 multibuffer.push_excerpts(
11002 buffer.clone(),
11003 [
11004 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
11005 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
11006 ],
11007 cx,
11008 );
11009 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
11010 multibuffer
11011 });
11012
11013 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11014 editor.update_in(cx, |editor, window, cx| {
11015 assert_eq!(editor.text(cx), "aaaa\nbbbb");
11016 editor.change_selections(None, window, cx, |s| {
11017 s.select_ranges([
11018 Point::new(0, 0)..Point::new(0, 0),
11019 Point::new(1, 0)..Point::new(1, 0),
11020 ])
11021 });
11022
11023 editor.handle_input("X", window, cx);
11024 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
11025 assert_eq!(
11026 editor.selections.ranges(cx),
11027 [
11028 Point::new(0, 1)..Point::new(0, 1),
11029 Point::new(1, 1)..Point::new(1, 1),
11030 ]
11031 );
11032
11033 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
11034 editor.change_selections(None, window, cx, |s| {
11035 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
11036 });
11037 editor.backspace(&Default::default(), window, cx);
11038 assert_eq!(editor.text(cx), "Xa\nbbb");
11039 assert_eq!(
11040 editor.selections.ranges(cx),
11041 [Point::new(1, 0)..Point::new(1, 0)]
11042 );
11043
11044 editor.change_selections(None, window, cx, |s| {
11045 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
11046 });
11047 editor.backspace(&Default::default(), window, cx);
11048 assert_eq!(editor.text(cx), "X\nbb");
11049 assert_eq!(
11050 editor.selections.ranges(cx),
11051 [Point::new(0, 1)..Point::new(0, 1)]
11052 );
11053 });
11054}
11055
11056#[gpui::test]
11057fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
11058 init_test(cx, |_| {});
11059
11060 let markers = vec![('[', ']').into(), ('(', ')').into()];
11061 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
11062 indoc! {"
11063 [aaaa
11064 (bbbb]
11065 cccc)",
11066 },
11067 markers.clone(),
11068 );
11069 let excerpt_ranges = markers.into_iter().map(|marker| {
11070 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
11071 ExcerptRange::new(context.clone())
11072 });
11073 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
11074 let multibuffer = cx.new(|cx| {
11075 let mut multibuffer = MultiBuffer::new(ReadWrite);
11076 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
11077 multibuffer
11078 });
11079
11080 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11081 editor.update_in(cx, |editor, window, cx| {
11082 let (expected_text, selection_ranges) = marked_text_ranges(
11083 indoc! {"
11084 aaaa
11085 bˇbbb
11086 bˇbbˇb
11087 cccc"
11088 },
11089 true,
11090 );
11091 assert_eq!(editor.text(cx), expected_text);
11092 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11093
11094 editor.handle_input("X", window, cx);
11095
11096 let (expected_text, expected_selections) = marked_text_ranges(
11097 indoc! {"
11098 aaaa
11099 bXˇbbXb
11100 bXˇbbXˇb
11101 cccc"
11102 },
11103 false,
11104 );
11105 assert_eq!(editor.text(cx), expected_text);
11106 assert_eq!(editor.selections.ranges(cx), expected_selections);
11107
11108 editor.newline(&Newline, window, cx);
11109 let (expected_text, expected_selections) = marked_text_ranges(
11110 indoc! {"
11111 aaaa
11112 bX
11113 ˇbbX
11114 b
11115 bX
11116 ˇbbX
11117 ˇb
11118 cccc"
11119 },
11120 false,
11121 );
11122 assert_eq!(editor.text(cx), expected_text);
11123 assert_eq!(editor.selections.ranges(cx), expected_selections);
11124 });
11125}
11126
11127#[gpui::test]
11128fn test_refresh_selections(cx: &mut TestAppContext) {
11129 init_test(cx, |_| {});
11130
11131 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11132 let mut excerpt1_id = None;
11133 let multibuffer = cx.new(|cx| {
11134 let mut multibuffer = MultiBuffer::new(ReadWrite);
11135 excerpt1_id = multibuffer
11136 .push_excerpts(
11137 buffer.clone(),
11138 [
11139 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11140 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11141 ],
11142 cx,
11143 )
11144 .into_iter()
11145 .next();
11146 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11147 multibuffer
11148 });
11149
11150 let editor = cx.add_window(|window, cx| {
11151 let mut editor = build_editor(multibuffer.clone(), window, cx);
11152 let snapshot = editor.snapshot(window, cx);
11153 editor.change_selections(None, window, cx, |s| {
11154 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11155 });
11156 editor.begin_selection(
11157 Point::new(2, 1).to_display_point(&snapshot),
11158 true,
11159 1,
11160 window,
11161 cx,
11162 );
11163 assert_eq!(
11164 editor.selections.ranges(cx),
11165 [
11166 Point::new(1, 3)..Point::new(1, 3),
11167 Point::new(2, 1)..Point::new(2, 1),
11168 ]
11169 );
11170 editor
11171 });
11172
11173 // Refreshing selections is a no-op when excerpts haven't changed.
11174 _ = editor.update(cx, |editor, window, cx| {
11175 editor.change_selections(None, window, cx, |s| s.refresh());
11176 assert_eq!(
11177 editor.selections.ranges(cx),
11178 [
11179 Point::new(1, 3)..Point::new(1, 3),
11180 Point::new(2, 1)..Point::new(2, 1),
11181 ]
11182 );
11183 });
11184
11185 multibuffer.update(cx, |multibuffer, cx| {
11186 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11187 });
11188 _ = editor.update(cx, |editor, window, cx| {
11189 // Removing an excerpt causes the first selection to become degenerate.
11190 assert_eq!(
11191 editor.selections.ranges(cx),
11192 [
11193 Point::new(0, 0)..Point::new(0, 0),
11194 Point::new(0, 1)..Point::new(0, 1)
11195 ]
11196 );
11197
11198 // Refreshing selections will relocate the first selection to the original buffer
11199 // location.
11200 editor.change_selections(None, window, cx, |s| s.refresh());
11201 assert_eq!(
11202 editor.selections.ranges(cx),
11203 [
11204 Point::new(0, 1)..Point::new(0, 1),
11205 Point::new(0, 3)..Point::new(0, 3)
11206 ]
11207 );
11208 assert!(editor.selections.pending_anchor().is_some());
11209 });
11210}
11211
11212#[gpui::test]
11213fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
11214 init_test(cx, |_| {});
11215
11216 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11217 let mut excerpt1_id = None;
11218 let multibuffer = cx.new(|cx| {
11219 let mut multibuffer = MultiBuffer::new(ReadWrite);
11220 excerpt1_id = multibuffer
11221 .push_excerpts(
11222 buffer.clone(),
11223 [
11224 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11225 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11226 ],
11227 cx,
11228 )
11229 .into_iter()
11230 .next();
11231 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11232 multibuffer
11233 });
11234
11235 let editor = cx.add_window(|window, cx| {
11236 let mut editor = build_editor(multibuffer.clone(), window, cx);
11237 let snapshot = editor.snapshot(window, cx);
11238 editor.begin_selection(
11239 Point::new(1, 3).to_display_point(&snapshot),
11240 false,
11241 1,
11242 window,
11243 cx,
11244 );
11245 assert_eq!(
11246 editor.selections.ranges(cx),
11247 [Point::new(1, 3)..Point::new(1, 3)]
11248 );
11249 editor
11250 });
11251
11252 multibuffer.update(cx, |multibuffer, cx| {
11253 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11254 });
11255 _ = editor.update(cx, |editor, window, cx| {
11256 assert_eq!(
11257 editor.selections.ranges(cx),
11258 [Point::new(0, 0)..Point::new(0, 0)]
11259 );
11260
11261 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
11262 editor.change_selections(None, window, cx, |s| s.refresh());
11263 assert_eq!(
11264 editor.selections.ranges(cx),
11265 [Point::new(0, 3)..Point::new(0, 3)]
11266 );
11267 assert!(editor.selections.pending_anchor().is_some());
11268 });
11269}
11270
11271#[gpui::test]
11272async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
11273 init_test(cx, |_| {});
11274
11275 let language = Arc::new(
11276 Language::new(
11277 LanguageConfig {
11278 brackets: BracketPairConfig {
11279 pairs: vec![
11280 BracketPair {
11281 start: "{".to_string(),
11282 end: "}".to_string(),
11283 close: true,
11284 surround: true,
11285 newline: true,
11286 },
11287 BracketPair {
11288 start: "/* ".to_string(),
11289 end: " */".to_string(),
11290 close: true,
11291 surround: true,
11292 newline: true,
11293 },
11294 ],
11295 ..Default::default()
11296 },
11297 ..Default::default()
11298 },
11299 Some(tree_sitter_rust::LANGUAGE.into()),
11300 )
11301 .with_indents_query("")
11302 .unwrap(),
11303 );
11304
11305 let text = concat!(
11306 "{ }\n", //
11307 " x\n", //
11308 " /* */\n", //
11309 "x\n", //
11310 "{{} }\n", //
11311 );
11312
11313 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11314 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11315 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11316 editor
11317 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11318 .await;
11319
11320 editor.update_in(cx, |editor, window, cx| {
11321 editor.change_selections(None, window, cx, |s| {
11322 s.select_display_ranges([
11323 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11324 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11325 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11326 ])
11327 });
11328 editor.newline(&Newline, window, cx);
11329
11330 assert_eq!(
11331 editor.buffer().read(cx).read(cx).text(),
11332 concat!(
11333 "{ \n", // Suppress rustfmt
11334 "\n", //
11335 "}\n", //
11336 " x\n", //
11337 " /* \n", //
11338 " \n", //
11339 " */\n", //
11340 "x\n", //
11341 "{{} \n", //
11342 "}\n", //
11343 )
11344 );
11345 });
11346}
11347
11348#[gpui::test]
11349fn test_highlighted_ranges(cx: &mut TestAppContext) {
11350 init_test(cx, |_| {});
11351
11352 let editor = cx.add_window(|window, cx| {
11353 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11354 build_editor(buffer.clone(), window, cx)
11355 });
11356
11357 _ = editor.update(cx, |editor, window, cx| {
11358 struct Type1;
11359 struct Type2;
11360
11361 let buffer = editor.buffer.read(cx).snapshot(cx);
11362
11363 let anchor_range =
11364 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11365
11366 editor.highlight_background::<Type1>(
11367 &[
11368 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11369 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11370 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11371 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11372 ],
11373 |_| Hsla::red(),
11374 cx,
11375 );
11376 editor.highlight_background::<Type2>(
11377 &[
11378 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11379 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11380 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11381 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11382 ],
11383 |_| Hsla::green(),
11384 cx,
11385 );
11386
11387 let snapshot = editor.snapshot(window, cx);
11388 let mut highlighted_ranges = editor.background_highlights_in_range(
11389 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11390 &snapshot,
11391 cx.theme().colors(),
11392 );
11393 // Enforce a consistent ordering based on color without relying on the ordering of the
11394 // highlight's `TypeId` which is non-executor.
11395 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11396 assert_eq!(
11397 highlighted_ranges,
11398 &[
11399 (
11400 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11401 Hsla::red(),
11402 ),
11403 (
11404 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11405 Hsla::red(),
11406 ),
11407 (
11408 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11409 Hsla::green(),
11410 ),
11411 (
11412 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11413 Hsla::green(),
11414 ),
11415 ]
11416 );
11417 assert_eq!(
11418 editor.background_highlights_in_range(
11419 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11420 &snapshot,
11421 cx.theme().colors(),
11422 ),
11423 &[(
11424 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11425 Hsla::red(),
11426 )]
11427 );
11428 });
11429}
11430
11431#[gpui::test]
11432async fn test_following(cx: &mut TestAppContext) {
11433 init_test(cx, |_| {});
11434
11435 let fs = FakeFs::new(cx.executor());
11436 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11437
11438 let buffer = project.update(cx, |project, cx| {
11439 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11440 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11441 });
11442 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11443 let follower = cx.update(|cx| {
11444 cx.open_window(
11445 WindowOptions {
11446 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11447 gpui::Point::new(px(0.), px(0.)),
11448 gpui::Point::new(px(10.), px(80.)),
11449 ))),
11450 ..Default::default()
11451 },
11452 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11453 )
11454 .unwrap()
11455 });
11456
11457 let is_still_following = Rc::new(RefCell::new(true));
11458 let follower_edit_event_count = Rc::new(RefCell::new(0));
11459 let pending_update = Rc::new(RefCell::new(None));
11460 let leader_entity = leader.root(cx).unwrap();
11461 let follower_entity = follower.root(cx).unwrap();
11462 _ = follower.update(cx, {
11463 let update = pending_update.clone();
11464 let is_still_following = is_still_following.clone();
11465 let follower_edit_event_count = follower_edit_event_count.clone();
11466 |_, window, cx| {
11467 cx.subscribe_in(
11468 &leader_entity,
11469 window,
11470 move |_, leader, event, window, cx| {
11471 leader.read(cx).add_event_to_update_proto(
11472 event,
11473 &mut update.borrow_mut(),
11474 window,
11475 cx,
11476 );
11477 },
11478 )
11479 .detach();
11480
11481 cx.subscribe_in(
11482 &follower_entity,
11483 window,
11484 move |_, _, event: &EditorEvent, _window, _cx| {
11485 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11486 *is_still_following.borrow_mut() = false;
11487 }
11488
11489 if let EditorEvent::BufferEdited = event {
11490 *follower_edit_event_count.borrow_mut() += 1;
11491 }
11492 },
11493 )
11494 .detach();
11495 }
11496 });
11497
11498 // Update the selections only
11499 _ = leader.update(cx, |leader, window, cx| {
11500 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11501 });
11502 follower
11503 .update(cx, |follower, window, cx| {
11504 follower.apply_update_proto(
11505 &project,
11506 pending_update.borrow_mut().take().unwrap(),
11507 window,
11508 cx,
11509 )
11510 })
11511 .unwrap()
11512 .await
11513 .unwrap();
11514 _ = follower.update(cx, |follower, _, cx| {
11515 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11516 });
11517 assert!(*is_still_following.borrow());
11518 assert_eq!(*follower_edit_event_count.borrow(), 0);
11519
11520 // Update the scroll position only
11521 _ = leader.update(cx, |leader, window, cx| {
11522 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11523 });
11524 follower
11525 .update(cx, |follower, window, cx| {
11526 follower.apply_update_proto(
11527 &project,
11528 pending_update.borrow_mut().take().unwrap(),
11529 window,
11530 cx,
11531 )
11532 })
11533 .unwrap()
11534 .await
11535 .unwrap();
11536 assert_eq!(
11537 follower
11538 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11539 .unwrap(),
11540 gpui::Point::new(1.5, 3.5)
11541 );
11542 assert!(*is_still_following.borrow());
11543 assert_eq!(*follower_edit_event_count.borrow(), 0);
11544
11545 // Update the selections and scroll position. The follower's scroll position is updated
11546 // via autoscroll, not via the leader's exact scroll position.
11547 _ = leader.update(cx, |leader, window, cx| {
11548 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11549 leader.request_autoscroll(Autoscroll::newest(), cx);
11550 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11551 });
11552 follower
11553 .update(cx, |follower, window, cx| {
11554 follower.apply_update_proto(
11555 &project,
11556 pending_update.borrow_mut().take().unwrap(),
11557 window,
11558 cx,
11559 )
11560 })
11561 .unwrap()
11562 .await
11563 .unwrap();
11564 _ = follower.update(cx, |follower, _, cx| {
11565 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11566 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11567 });
11568 assert!(*is_still_following.borrow());
11569
11570 // Creating a pending selection that precedes another selection
11571 _ = leader.update(cx, |leader, window, cx| {
11572 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11573 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11574 });
11575 follower
11576 .update(cx, |follower, window, cx| {
11577 follower.apply_update_proto(
11578 &project,
11579 pending_update.borrow_mut().take().unwrap(),
11580 window,
11581 cx,
11582 )
11583 })
11584 .unwrap()
11585 .await
11586 .unwrap();
11587 _ = follower.update(cx, |follower, _, cx| {
11588 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11589 });
11590 assert!(*is_still_following.borrow());
11591
11592 // Extend the pending selection so that it surrounds another selection
11593 _ = leader.update(cx, |leader, window, cx| {
11594 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11595 });
11596 follower
11597 .update(cx, |follower, window, cx| {
11598 follower.apply_update_proto(
11599 &project,
11600 pending_update.borrow_mut().take().unwrap(),
11601 window,
11602 cx,
11603 )
11604 })
11605 .unwrap()
11606 .await
11607 .unwrap();
11608 _ = follower.update(cx, |follower, _, cx| {
11609 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11610 });
11611
11612 // Scrolling locally breaks the follow
11613 _ = follower.update(cx, |follower, window, cx| {
11614 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11615 follower.set_scroll_anchor(
11616 ScrollAnchor {
11617 anchor: top_anchor,
11618 offset: gpui::Point::new(0.0, 0.5),
11619 },
11620 window,
11621 cx,
11622 );
11623 });
11624 assert!(!(*is_still_following.borrow()));
11625}
11626
11627#[gpui::test]
11628async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11629 init_test(cx, |_| {});
11630
11631 let fs = FakeFs::new(cx.executor());
11632 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11633 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11634 let pane = workspace
11635 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11636 .unwrap();
11637
11638 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11639
11640 let leader = pane.update_in(cx, |_, window, cx| {
11641 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11642 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11643 });
11644
11645 // Start following the editor when it has no excerpts.
11646 let mut state_message =
11647 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11648 let workspace_entity = workspace.root(cx).unwrap();
11649 let follower_1 = cx
11650 .update_window(*workspace.deref(), |_, window, cx| {
11651 Editor::from_state_proto(
11652 workspace_entity,
11653 ViewId {
11654 creator: Default::default(),
11655 id: 0,
11656 },
11657 &mut state_message,
11658 window,
11659 cx,
11660 )
11661 })
11662 .unwrap()
11663 .unwrap()
11664 .await
11665 .unwrap();
11666
11667 let update_message = Rc::new(RefCell::new(None));
11668 follower_1.update_in(cx, {
11669 let update = update_message.clone();
11670 |_, window, cx| {
11671 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11672 leader.read(cx).add_event_to_update_proto(
11673 event,
11674 &mut update.borrow_mut(),
11675 window,
11676 cx,
11677 );
11678 })
11679 .detach();
11680 }
11681 });
11682
11683 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11684 (
11685 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11686 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11687 )
11688 });
11689
11690 // Insert some excerpts.
11691 leader.update(cx, |leader, cx| {
11692 leader.buffer.update(cx, |multibuffer, cx| {
11693 let excerpt_ids = multibuffer.push_excerpts(
11694 buffer_1.clone(),
11695 [
11696 ExcerptRange::new(1..6),
11697 ExcerptRange::new(12..15),
11698 ExcerptRange::new(0..3),
11699 ],
11700 cx,
11701 );
11702 multibuffer.insert_excerpts_after(
11703 excerpt_ids[0],
11704 buffer_2.clone(),
11705 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
11706 cx,
11707 );
11708 });
11709 });
11710
11711 // Apply the update of adding the excerpts.
11712 follower_1
11713 .update_in(cx, |follower, window, cx| {
11714 follower.apply_update_proto(
11715 &project,
11716 update_message.borrow().clone().unwrap(),
11717 window,
11718 cx,
11719 )
11720 })
11721 .await
11722 .unwrap();
11723 assert_eq!(
11724 follower_1.update(cx, |editor, cx| editor.text(cx)),
11725 leader.update(cx, |editor, cx| editor.text(cx))
11726 );
11727 update_message.borrow_mut().take();
11728
11729 // Start following separately after it already has excerpts.
11730 let mut state_message =
11731 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11732 let workspace_entity = workspace.root(cx).unwrap();
11733 let follower_2 = cx
11734 .update_window(*workspace.deref(), |_, window, cx| {
11735 Editor::from_state_proto(
11736 workspace_entity,
11737 ViewId {
11738 creator: Default::default(),
11739 id: 0,
11740 },
11741 &mut state_message,
11742 window,
11743 cx,
11744 )
11745 })
11746 .unwrap()
11747 .unwrap()
11748 .await
11749 .unwrap();
11750 assert_eq!(
11751 follower_2.update(cx, |editor, cx| editor.text(cx)),
11752 leader.update(cx, |editor, cx| editor.text(cx))
11753 );
11754
11755 // Remove some excerpts.
11756 leader.update(cx, |leader, cx| {
11757 leader.buffer.update(cx, |multibuffer, cx| {
11758 let excerpt_ids = multibuffer.excerpt_ids();
11759 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11760 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11761 });
11762 });
11763
11764 // Apply the update of removing the excerpts.
11765 follower_1
11766 .update_in(cx, |follower, window, cx| {
11767 follower.apply_update_proto(
11768 &project,
11769 update_message.borrow().clone().unwrap(),
11770 window,
11771 cx,
11772 )
11773 })
11774 .await
11775 .unwrap();
11776 follower_2
11777 .update_in(cx, |follower, window, cx| {
11778 follower.apply_update_proto(
11779 &project,
11780 update_message.borrow().clone().unwrap(),
11781 window,
11782 cx,
11783 )
11784 })
11785 .await
11786 .unwrap();
11787 update_message.borrow_mut().take();
11788 assert_eq!(
11789 follower_1.update(cx, |editor, cx| editor.text(cx)),
11790 leader.update(cx, |editor, cx| editor.text(cx))
11791 );
11792}
11793
11794#[gpui::test]
11795async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11796 init_test(cx, |_| {});
11797
11798 let mut cx = EditorTestContext::new(cx).await;
11799 let lsp_store =
11800 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11801
11802 cx.set_state(indoc! {"
11803 ˇfn func(abc def: i32) -> u32 {
11804 }
11805 "});
11806
11807 cx.update(|_, cx| {
11808 lsp_store.update(cx, |lsp_store, cx| {
11809 lsp_store
11810 .update_diagnostics(
11811 LanguageServerId(0),
11812 lsp::PublishDiagnosticsParams {
11813 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11814 version: None,
11815 diagnostics: vec![
11816 lsp::Diagnostic {
11817 range: lsp::Range::new(
11818 lsp::Position::new(0, 11),
11819 lsp::Position::new(0, 12),
11820 ),
11821 severity: Some(lsp::DiagnosticSeverity::ERROR),
11822 ..Default::default()
11823 },
11824 lsp::Diagnostic {
11825 range: lsp::Range::new(
11826 lsp::Position::new(0, 12),
11827 lsp::Position::new(0, 15),
11828 ),
11829 severity: Some(lsp::DiagnosticSeverity::ERROR),
11830 ..Default::default()
11831 },
11832 lsp::Diagnostic {
11833 range: lsp::Range::new(
11834 lsp::Position::new(0, 25),
11835 lsp::Position::new(0, 28),
11836 ),
11837 severity: Some(lsp::DiagnosticSeverity::ERROR),
11838 ..Default::default()
11839 },
11840 ],
11841 },
11842 &[],
11843 cx,
11844 )
11845 .unwrap()
11846 });
11847 });
11848
11849 executor.run_until_parked();
11850
11851 cx.update_editor(|editor, window, cx| {
11852 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11853 });
11854
11855 cx.assert_editor_state(indoc! {"
11856 fn func(abc def: i32) -> ˇu32 {
11857 }
11858 "});
11859
11860 cx.update_editor(|editor, window, cx| {
11861 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11862 });
11863
11864 cx.assert_editor_state(indoc! {"
11865 fn func(abc ˇdef: i32) -> u32 {
11866 }
11867 "});
11868
11869 cx.update_editor(|editor, window, cx| {
11870 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11871 });
11872
11873 cx.assert_editor_state(indoc! {"
11874 fn func(abcˇ def: i32) -> u32 {
11875 }
11876 "});
11877
11878 cx.update_editor(|editor, window, cx| {
11879 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11880 });
11881
11882 cx.assert_editor_state(indoc! {"
11883 fn func(abc def: i32) -> ˇu32 {
11884 }
11885 "});
11886}
11887
11888#[gpui::test]
11889async fn cycle_through_same_place_diagnostics(
11890 executor: BackgroundExecutor,
11891 cx: &mut TestAppContext,
11892) {
11893 init_test(cx, |_| {});
11894
11895 let mut cx = EditorTestContext::new(cx).await;
11896 let lsp_store =
11897 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11898
11899 cx.set_state(indoc! {"
11900 ˇfn func(abc def: i32) -> u32 {
11901 }
11902 "});
11903
11904 cx.update(|_, cx| {
11905 lsp_store.update(cx, |lsp_store, cx| {
11906 lsp_store
11907 .update_diagnostics(
11908 LanguageServerId(0),
11909 lsp::PublishDiagnosticsParams {
11910 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11911 version: None,
11912 diagnostics: vec![
11913 lsp::Diagnostic {
11914 range: lsp::Range::new(
11915 lsp::Position::new(0, 11),
11916 lsp::Position::new(0, 12),
11917 ),
11918 severity: Some(lsp::DiagnosticSeverity::ERROR),
11919 ..Default::default()
11920 },
11921 lsp::Diagnostic {
11922 range: lsp::Range::new(
11923 lsp::Position::new(0, 12),
11924 lsp::Position::new(0, 15),
11925 ),
11926 severity: Some(lsp::DiagnosticSeverity::ERROR),
11927 ..Default::default()
11928 },
11929 lsp::Diagnostic {
11930 range: lsp::Range::new(
11931 lsp::Position::new(0, 12),
11932 lsp::Position::new(0, 15),
11933 ),
11934 severity: Some(lsp::DiagnosticSeverity::ERROR),
11935 ..Default::default()
11936 },
11937 lsp::Diagnostic {
11938 range: lsp::Range::new(
11939 lsp::Position::new(0, 25),
11940 lsp::Position::new(0, 28),
11941 ),
11942 severity: Some(lsp::DiagnosticSeverity::ERROR),
11943 ..Default::default()
11944 },
11945 ],
11946 },
11947 &[],
11948 cx,
11949 )
11950 .unwrap()
11951 });
11952 });
11953 executor.run_until_parked();
11954
11955 //// Backward
11956
11957 // Fourth diagnostic
11958 cx.update_editor(|editor, window, cx| {
11959 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11960 });
11961 cx.assert_editor_state(indoc! {"
11962 fn func(abc def: i32) -> ˇu32 {
11963 }
11964 "});
11965
11966 // Third diagnostic
11967 cx.update_editor(|editor, window, cx| {
11968 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11969 });
11970 cx.assert_editor_state(indoc! {"
11971 fn func(abc ˇdef: i32) -> u32 {
11972 }
11973 "});
11974
11975 // Second diagnostic, same place
11976 cx.update_editor(|editor, window, cx| {
11977 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11978 });
11979 cx.assert_editor_state(indoc! {"
11980 fn func(abc ˇdef: i32) -> u32 {
11981 }
11982 "});
11983
11984 // First diagnostic
11985 cx.update_editor(|editor, window, cx| {
11986 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11987 });
11988 cx.assert_editor_state(indoc! {"
11989 fn func(abcˇ def: i32) -> u32 {
11990 }
11991 "});
11992
11993 // Wrapped over, fourth diagnostic
11994 cx.update_editor(|editor, window, cx| {
11995 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11996 });
11997 cx.assert_editor_state(indoc! {"
11998 fn func(abc def: i32) -> ˇu32 {
11999 }
12000 "});
12001
12002 cx.update_editor(|editor, window, cx| {
12003 editor.move_to_beginning(&MoveToBeginning, window, cx);
12004 });
12005 cx.assert_editor_state(indoc! {"
12006 ˇfn func(abc def: i32) -> u32 {
12007 }
12008 "});
12009
12010 //// Forward
12011
12012 // First diagnostic
12013 cx.update_editor(|editor, window, cx| {
12014 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12015 });
12016 cx.assert_editor_state(indoc! {"
12017 fn func(abcˇ def: i32) -> u32 {
12018 }
12019 "});
12020
12021 // Second diagnostic
12022 cx.update_editor(|editor, window, cx| {
12023 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12024 });
12025 cx.assert_editor_state(indoc! {"
12026 fn func(abc ˇdef: i32) -> u32 {
12027 }
12028 "});
12029
12030 // Third diagnostic, same place
12031 cx.update_editor(|editor, window, cx| {
12032 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12033 });
12034 cx.assert_editor_state(indoc! {"
12035 fn func(abc ˇdef: i32) -> u32 {
12036 }
12037 "});
12038
12039 // Fourth diagnostic
12040 cx.update_editor(|editor, window, cx| {
12041 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12042 });
12043 cx.assert_editor_state(indoc! {"
12044 fn func(abc def: i32) -> ˇu32 {
12045 }
12046 "});
12047
12048 // Wrapped around, first diagnostic
12049 cx.update_editor(|editor, window, cx| {
12050 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12051 });
12052 cx.assert_editor_state(indoc! {"
12053 fn func(abcˇ def: i32) -> u32 {
12054 }
12055 "});
12056}
12057
12058#[gpui::test]
12059async fn active_diagnostics_dismiss_after_invalidation(
12060 executor: BackgroundExecutor,
12061 cx: &mut TestAppContext,
12062) {
12063 init_test(cx, |_| {});
12064
12065 let mut cx = EditorTestContext::new(cx).await;
12066 let lsp_store =
12067 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12068
12069 cx.set_state(indoc! {"
12070 ˇfn func(abc def: i32) -> u32 {
12071 }
12072 "});
12073
12074 let message = "Something's wrong!";
12075 cx.update(|_, cx| {
12076 lsp_store.update(cx, |lsp_store, cx| {
12077 lsp_store
12078 .update_diagnostics(
12079 LanguageServerId(0),
12080 lsp::PublishDiagnosticsParams {
12081 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12082 version: None,
12083 diagnostics: vec![lsp::Diagnostic {
12084 range: lsp::Range::new(
12085 lsp::Position::new(0, 11),
12086 lsp::Position::new(0, 12),
12087 ),
12088 severity: Some(lsp::DiagnosticSeverity::ERROR),
12089 message: message.to_string(),
12090 ..Default::default()
12091 }],
12092 },
12093 &[],
12094 cx,
12095 )
12096 .unwrap()
12097 });
12098 });
12099 executor.run_until_parked();
12100
12101 cx.update_editor(|editor, window, cx| {
12102 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12103 assert_eq!(
12104 editor
12105 .active_diagnostics
12106 .as_ref()
12107 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
12108 Some(message),
12109 "Should have a diagnostics group activated"
12110 );
12111 });
12112 cx.assert_editor_state(indoc! {"
12113 fn func(abcˇ def: i32) -> u32 {
12114 }
12115 "});
12116
12117 cx.update(|_, cx| {
12118 lsp_store.update(cx, |lsp_store, cx| {
12119 lsp_store
12120 .update_diagnostics(
12121 LanguageServerId(0),
12122 lsp::PublishDiagnosticsParams {
12123 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12124 version: None,
12125 diagnostics: Vec::new(),
12126 },
12127 &[],
12128 cx,
12129 )
12130 .unwrap()
12131 });
12132 });
12133 executor.run_until_parked();
12134 cx.update_editor(|editor, _, _| {
12135 assert_eq!(
12136 editor.active_diagnostics, None,
12137 "After no diagnostics set to the editor, no diagnostics should be active"
12138 );
12139 });
12140 cx.assert_editor_state(indoc! {"
12141 fn func(abcˇ def: i32) -> u32 {
12142 }
12143 "});
12144
12145 cx.update_editor(|editor, window, cx| {
12146 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12147 assert_eq!(
12148 editor.active_diagnostics, None,
12149 "Should be no diagnostics to go to and activate"
12150 );
12151 });
12152 cx.assert_editor_state(indoc! {"
12153 fn func(abcˇ def: i32) -> u32 {
12154 }
12155 "});
12156}
12157
12158#[gpui::test]
12159async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12160 init_test(cx, |_| {});
12161
12162 let mut cx = EditorTestContext::new(cx).await;
12163
12164 cx.set_state(indoc! {"
12165 fn func(abˇc def: i32) -> u32 {
12166 }
12167 "});
12168 let lsp_store =
12169 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12170
12171 cx.update(|_, cx| {
12172 lsp_store.update(cx, |lsp_store, cx| {
12173 lsp_store.update_diagnostics(
12174 LanguageServerId(0),
12175 lsp::PublishDiagnosticsParams {
12176 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12177 version: None,
12178 diagnostics: vec![lsp::Diagnostic {
12179 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12180 severity: Some(lsp::DiagnosticSeverity::ERROR),
12181 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12182 ..Default::default()
12183 }],
12184 },
12185 &[],
12186 cx,
12187 )
12188 })
12189 }).unwrap();
12190 cx.run_until_parked();
12191 cx.update_editor(|editor, window, cx| {
12192 hover_popover::hover(editor, &Default::default(), window, cx)
12193 });
12194 cx.run_until_parked();
12195 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12196}
12197
12198#[gpui::test]
12199async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12200 init_test(cx, |_| {});
12201
12202 let mut cx = EditorTestContext::new(cx).await;
12203
12204 let diff_base = r#"
12205 use some::mod;
12206
12207 const A: u32 = 42;
12208
12209 fn main() {
12210 println!("hello");
12211
12212 println!("world");
12213 }
12214 "#
12215 .unindent();
12216
12217 // Edits are modified, removed, modified, added
12218 cx.set_state(
12219 &r#"
12220 use some::modified;
12221
12222 ˇ
12223 fn main() {
12224 println!("hello there");
12225
12226 println!("around the");
12227 println!("world");
12228 }
12229 "#
12230 .unindent(),
12231 );
12232
12233 cx.set_head_text(&diff_base);
12234 executor.run_until_parked();
12235
12236 cx.update_editor(|editor, window, cx| {
12237 //Wrap around the bottom of the buffer
12238 for _ in 0..3 {
12239 editor.go_to_next_hunk(&GoToHunk, window, cx);
12240 }
12241 });
12242
12243 cx.assert_editor_state(
12244 &r#"
12245 ˇuse some::modified;
12246
12247
12248 fn main() {
12249 println!("hello there");
12250
12251 println!("around the");
12252 println!("world");
12253 }
12254 "#
12255 .unindent(),
12256 );
12257
12258 cx.update_editor(|editor, window, cx| {
12259 //Wrap around the top of the buffer
12260 for _ in 0..2 {
12261 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12262 }
12263 });
12264
12265 cx.assert_editor_state(
12266 &r#"
12267 use some::modified;
12268
12269
12270 fn main() {
12271 ˇ println!("hello there");
12272
12273 println!("around the");
12274 println!("world");
12275 }
12276 "#
12277 .unindent(),
12278 );
12279
12280 cx.update_editor(|editor, window, cx| {
12281 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12282 });
12283
12284 cx.assert_editor_state(
12285 &r#"
12286 use some::modified;
12287
12288 ˇ
12289 fn main() {
12290 println!("hello there");
12291
12292 println!("around the");
12293 println!("world");
12294 }
12295 "#
12296 .unindent(),
12297 );
12298
12299 cx.update_editor(|editor, window, cx| {
12300 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12301 });
12302
12303 cx.assert_editor_state(
12304 &r#"
12305 ˇuse some::modified;
12306
12307
12308 fn main() {
12309 println!("hello there");
12310
12311 println!("around the");
12312 println!("world");
12313 }
12314 "#
12315 .unindent(),
12316 );
12317
12318 cx.update_editor(|editor, window, cx| {
12319 for _ in 0..2 {
12320 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12321 }
12322 });
12323
12324 cx.assert_editor_state(
12325 &r#"
12326 use some::modified;
12327
12328
12329 fn main() {
12330 ˇ println!("hello there");
12331
12332 println!("around the");
12333 println!("world");
12334 }
12335 "#
12336 .unindent(),
12337 );
12338
12339 cx.update_editor(|editor, window, cx| {
12340 editor.fold(&Fold, window, cx);
12341 });
12342
12343 cx.update_editor(|editor, window, cx| {
12344 editor.go_to_next_hunk(&GoToHunk, window, cx);
12345 });
12346
12347 cx.assert_editor_state(
12348 &r#"
12349 ˇuse some::modified;
12350
12351
12352 fn main() {
12353 println!("hello there");
12354
12355 println!("around the");
12356 println!("world");
12357 }
12358 "#
12359 .unindent(),
12360 );
12361}
12362
12363#[test]
12364fn test_split_words() {
12365 fn split(text: &str) -> Vec<&str> {
12366 split_words(text).collect()
12367 }
12368
12369 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12370 assert_eq!(split("hello_world"), &["hello_", "world"]);
12371 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12372 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12373 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12374 assert_eq!(split("helloworld"), &["helloworld"]);
12375
12376 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12377}
12378
12379#[gpui::test]
12380async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12381 init_test(cx, |_| {});
12382
12383 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12384 let mut assert = |before, after| {
12385 let _state_context = cx.set_state(before);
12386 cx.run_until_parked();
12387 cx.update_editor(|editor, window, cx| {
12388 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12389 });
12390 cx.run_until_parked();
12391 cx.assert_editor_state(after);
12392 };
12393
12394 // Outside bracket jumps to outside of matching bracket
12395 assert("console.logˇ(var);", "console.log(var)ˇ;");
12396 assert("console.log(var)ˇ;", "console.logˇ(var);");
12397
12398 // Inside bracket jumps to inside of matching bracket
12399 assert("console.log(ˇvar);", "console.log(varˇ);");
12400 assert("console.log(varˇ);", "console.log(ˇvar);");
12401
12402 // When outside a bracket and inside, favor jumping to the inside bracket
12403 assert(
12404 "console.log('foo', [1, 2, 3]ˇ);",
12405 "console.log(ˇ'foo', [1, 2, 3]);",
12406 );
12407 assert(
12408 "console.log(ˇ'foo', [1, 2, 3]);",
12409 "console.log('foo', [1, 2, 3]ˇ);",
12410 );
12411
12412 // Bias forward if two options are equally likely
12413 assert(
12414 "let result = curried_fun()ˇ();",
12415 "let result = curried_fun()()ˇ;",
12416 );
12417
12418 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12419 assert(
12420 indoc! {"
12421 function test() {
12422 console.log('test')ˇ
12423 }"},
12424 indoc! {"
12425 function test() {
12426 console.logˇ('test')
12427 }"},
12428 );
12429}
12430
12431#[gpui::test]
12432async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12433 init_test(cx, |_| {});
12434
12435 let fs = FakeFs::new(cx.executor());
12436 fs.insert_tree(
12437 path!("/a"),
12438 json!({
12439 "main.rs": "fn main() { let a = 5; }",
12440 "other.rs": "// Test file",
12441 }),
12442 )
12443 .await;
12444 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12445
12446 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12447 language_registry.add(Arc::new(Language::new(
12448 LanguageConfig {
12449 name: "Rust".into(),
12450 matcher: LanguageMatcher {
12451 path_suffixes: vec!["rs".to_string()],
12452 ..Default::default()
12453 },
12454 brackets: BracketPairConfig {
12455 pairs: vec![BracketPair {
12456 start: "{".to_string(),
12457 end: "}".to_string(),
12458 close: true,
12459 surround: true,
12460 newline: true,
12461 }],
12462 disabled_scopes_by_bracket_ix: Vec::new(),
12463 },
12464 ..Default::default()
12465 },
12466 Some(tree_sitter_rust::LANGUAGE.into()),
12467 )));
12468 let mut fake_servers = language_registry.register_fake_lsp(
12469 "Rust",
12470 FakeLspAdapter {
12471 capabilities: lsp::ServerCapabilities {
12472 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12473 first_trigger_character: "{".to_string(),
12474 more_trigger_character: None,
12475 }),
12476 ..Default::default()
12477 },
12478 ..Default::default()
12479 },
12480 );
12481
12482 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12483
12484 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12485
12486 let worktree_id = workspace
12487 .update(cx, |workspace, _, cx| {
12488 workspace.project().update(cx, |project, cx| {
12489 project.worktrees(cx).next().unwrap().read(cx).id()
12490 })
12491 })
12492 .unwrap();
12493
12494 let buffer = project
12495 .update(cx, |project, cx| {
12496 project.open_local_buffer(path!("/a/main.rs"), cx)
12497 })
12498 .await
12499 .unwrap();
12500 let editor_handle = workspace
12501 .update(cx, |workspace, window, cx| {
12502 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12503 })
12504 .unwrap()
12505 .await
12506 .unwrap()
12507 .downcast::<Editor>()
12508 .unwrap();
12509
12510 cx.executor().start_waiting();
12511 let fake_server = fake_servers.next().await.unwrap();
12512
12513 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12514 |params, _| async move {
12515 assert_eq!(
12516 params.text_document_position.text_document.uri,
12517 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12518 );
12519 assert_eq!(
12520 params.text_document_position.position,
12521 lsp::Position::new(0, 21),
12522 );
12523
12524 Ok(Some(vec![lsp::TextEdit {
12525 new_text: "]".to_string(),
12526 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12527 }]))
12528 },
12529 );
12530
12531 editor_handle.update_in(cx, |editor, window, cx| {
12532 window.focus(&editor.focus_handle(cx));
12533 editor.change_selections(None, window, cx, |s| {
12534 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12535 });
12536 editor.handle_input("{", window, cx);
12537 });
12538
12539 cx.executor().run_until_parked();
12540
12541 buffer.update(cx, |buffer, _| {
12542 assert_eq!(
12543 buffer.text(),
12544 "fn main() { let a = {5}; }",
12545 "No extra braces from on type formatting should appear in the buffer"
12546 )
12547 });
12548}
12549
12550#[gpui::test]
12551async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12552 init_test(cx, |_| {});
12553
12554 let fs = FakeFs::new(cx.executor());
12555 fs.insert_tree(
12556 path!("/a"),
12557 json!({
12558 "main.rs": "fn main() { let a = 5; }",
12559 "other.rs": "// Test file",
12560 }),
12561 )
12562 .await;
12563
12564 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12565
12566 let server_restarts = Arc::new(AtomicUsize::new(0));
12567 let closure_restarts = Arc::clone(&server_restarts);
12568 let language_server_name = "test language server";
12569 let language_name: LanguageName = "Rust".into();
12570
12571 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12572 language_registry.add(Arc::new(Language::new(
12573 LanguageConfig {
12574 name: language_name.clone(),
12575 matcher: LanguageMatcher {
12576 path_suffixes: vec!["rs".to_string()],
12577 ..Default::default()
12578 },
12579 ..Default::default()
12580 },
12581 Some(tree_sitter_rust::LANGUAGE.into()),
12582 )));
12583 let mut fake_servers = language_registry.register_fake_lsp(
12584 "Rust",
12585 FakeLspAdapter {
12586 name: language_server_name,
12587 initialization_options: Some(json!({
12588 "testOptionValue": true
12589 })),
12590 initializer: Some(Box::new(move |fake_server| {
12591 let task_restarts = Arc::clone(&closure_restarts);
12592 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12593 task_restarts.fetch_add(1, atomic::Ordering::Release);
12594 futures::future::ready(Ok(()))
12595 });
12596 })),
12597 ..Default::default()
12598 },
12599 );
12600
12601 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12602 let _buffer = project
12603 .update(cx, |project, cx| {
12604 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12605 })
12606 .await
12607 .unwrap();
12608 let _fake_server = fake_servers.next().await.unwrap();
12609 update_test_language_settings(cx, |language_settings| {
12610 language_settings.languages.insert(
12611 language_name.clone(),
12612 LanguageSettingsContent {
12613 tab_size: NonZeroU32::new(8),
12614 ..Default::default()
12615 },
12616 );
12617 });
12618 cx.executor().run_until_parked();
12619 assert_eq!(
12620 server_restarts.load(atomic::Ordering::Acquire),
12621 0,
12622 "Should not restart LSP server on an unrelated change"
12623 );
12624
12625 update_test_project_settings(cx, |project_settings| {
12626 project_settings.lsp.insert(
12627 "Some other server name".into(),
12628 LspSettings {
12629 binary: None,
12630 settings: None,
12631 initialization_options: Some(json!({
12632 "some other init value": false
12633 })),
12634 enable_lsp_tasks: false,
12635 },
12636 );
12637 });
12638 cx.executor().run_until_parked();
12639 assert_eq!(
12640 server_restarts.load(atomic::Ordering::Acquire),
12641 0,
12642 "Should not restart LSP server on an unrelated LSP settings change"
12643 );
12644
12645 update_test_project_settings(cx, |project_settings| {
12646 project_settings.lsp.insert(
12647 language_server_name.into(),
12648 LspSettings {
12649 binary: None,
12650 settings: None,
12651 initialization_options: Some(json!({
12652 "anotherInitValue": false
12653 })),
12654 enable_lsp_tasks: false,
12655 },
12656 );
12657 });
12658 cx.executor().run_until_parked();
12659 assert_eq!(
12660 server_restarts.load(atomic::Ordering::Acquire),
12661 1,
12662 "Should restart LSP server on a related LSP settings change"
12663 );
12664
12665 update_test_project_settings(cx, |project_settings| {
12666 project_settings.lsp.insert(
12667 language_server_name.into(),
12668 LspSettings {
12669 binary: None,
12670 settings: None,
12671 initialization_options: Some(json!({
12672 "anotherInitValue": false
12673 })),
12674 enable_lsp_tasks: false,
12675 },
12676 );
12677 });
12678 cx.executor().run_until_parked();
12679 assert_eq!(
12680 server_restarts.load(atomic::Ordering::Acquire),
12681 1,
12682 "Should not restart LSP server on a related LSP settings change that is the same"
12683 );
12684
12685 update_test_project_settings(cx, |project_settings| {
12686 project_settings.lsp.insert(
12687 language_server_name.into(),
12688 LspSettings {
12689 binary: None,
12690 settings: None,
12691 initialization_options: None,
12692 enable_lsp_tasks: false,
12693 },
12694 );
12695 });
12696 cx.executor().run_until_parked();
12697 assert_eq!(
12698 server_restarts.load(atomic::Ordering::Acquire),
12699 2,
12700 "Should restart LSP server on another related LSP settings change"
12701 );
12702}
12703
12704#[gpui::test]
12705async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12706 init_test(cx, |_| {});
12707
12708 let mut cx = EditorLspTestContext::new_rust(
12709 lsp::ServerCapabilities {
12710 completion_provider: Some(lsp::CompletionOptions {
12711 trigger_characters: Some(vec![".".to_string()]),
12712 resolve_provider: Some(true),
12713 ..Default::default()
12714 }),
12715 ..Default::default()
12716 },
12717 cx,
12718 )
12719 .await;
12720
12721 cx.set_state("fn main() { let a = 2ˇ; }");
12722 cx.simulate_keystroke(".");
12723 let completion_item = lsp::CompletionItem {
12724 label: "some".into(),
12725 kind: Some(lsp::CompletionItemKind::SNIPPET),
12726 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12727 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12728 kind: lsp::MarkupKind::Markdown,
12729 value: "```rust\nSome(2)\n```".to_string(),
12730 })),
12731 deprecated: Some(false),
12732 sort_text: Some("fffffff2".to_string()),
12733 filter_text: Some("some".to_string()),
12734 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12735 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12736 range: lsp::Range {
12737 start: lsp::Position {
12738 line: 0,
12739 character: 22,
12740 },
12741 end: lsp::Position {
12742 line: 0,
12743 character: 22,
12744 },
12745 },
12746 new_text: "Some(2)".to_string(),
12747 })),
12748 additional_text_edits: Some(vec![lsp::TextEdit {
12749 range: lsp::Range {
12750 start: lsp::Position {
12751 line: 0,
12752 character: 20,
12753 },
12754 end: lsp::Position {
12755 line: 0,
12756 character: 22,
12757 },
12758 },
12759 new_text: "".to_string(),
12760 }]),
12761 ..Default::default()
12762 };
12763
12764 let closure_completion_item = completion_item.clone();
12765 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12766 let task_completion_item = closure_completion_item.clone();
12767 async move {
12768 Ok(Some(lsp::CompletionResponse::Array(vec![
12769 task_completion_item,
12770 ])))
12771 }
12772 });
12773
12774 request.next().await;
12775
12776 cx.condition(|editor, _| editor.context_menu_visible())
12777 .await;
12778 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12779 editor
12780 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12781 .unwrap()
12782 });
12783 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12784
12785 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12786 let task_completion_item = completion_item.clone();
12787 async move { Ok(task_completion_item) }
12788 })
12789 .next()
12790 .await
12791 .unwrap();
12792 apply_additional_edits.await.unwrap();
12793 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12794}
12795
12796#[gpui::test]
12797async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12798 init_test(cx, |_| {});
12799
12800 let mut cx = EditorLspTestContext::new_rust(
12801 lsp::ServerCapabilities {
12802 completion_provider: Some(lsp::CompletionOptions {
12803 trigger_characters: Some(vec![".".to_string()]),
12804 resolve_provider: Some(true),
12805 ..Default::default()
12806 }),
12807 ..Default::default()
12808 },
12809 cx,
12810 )
12811 .await;
12812
12813 cx.set_state("fn main() { let a = 2ˇ; }");
12814 cx.simulate_keystroke(".");
12815
12816 let item1 = lsp::CompletionItem {
12817 label: "method id()".to_string(),
12818 filter_text: Some("id".to_string()),
12819 detail: None,
12820 documentation: None,
12821 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12822 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12823 new_text: ".id".to_string(),
12824 })),
12825 ..lsp::CompletionItem::default()
12826 };
12827
12828 let item2 = lsp::CompletionItem {
12829 label: "other".to_string(),
12830 filter_text: Some("other".to_string()),
12831 detail: None,
12832 documentation: None,
12833 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12834 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12835 new_text: ".other".to_string(),
12836 })),
12837 ..lsp::CompletionItem::default()
12838 };
12839
12840 let item1 = item1.clone();
12841 cx.set_request_handler::<lsp::request::Completion, _, _>({
12842 let item1 = item1.clone();
12843 move |_, _, _| {
12844 let item1 = item1.clone();
12845 let item2 = item2.clone();
12846 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12847 }
12848 })
12849 .next()
12850 .await;
12851
12852 cx.condition(|editor, _| editor.context_menu_visible())
12853 .await;
12854 cx.update_editor(|editor, _, _| {
12855 let context_menu = editor.context_menu.borrow_mut();
12856 let context_menu = context_menu
12857 .as_ref()
12858 .expect("Should have the context menu deployed");
12859 match context_menu {
12860 CodeContextMenu::Completions(completions_menu) => {
12861 let completions = completions_menu.completions.borrow_mut();
12862 assert_eq!(
12863 completions
12864 .iter()
12865 .map(|completion| &completion.label.text)
12866 .collect::<Vec<_>>(),
12867 vec!["method id()", "other"]
12868 )
12869 }
12870 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12871 }
12872 });
12873
12874 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12875 let item1 = item1.clone();
12876 move |_, item_to_resolve, _| {
12877 let item1 = item1.clone();
12878 async move {
12879 if item1 == item_to_resolve {
12880 Ok(lsp::CompletionItem {
12881 label: "method id()".to_string(),
12882 filter_text: Some("id".to_string()),
12883 detail: Some("Now resolved!".to_string()),
12884 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12885 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12886 range: lsp::Range::new(
12887 lsp::Position::new(0, 22),
12888 lsp::Position::new(0, 22),
12889 ),
12890 new_text: ".id".to_string(),
12891 })),
12892 ..lsp::CompletionItem::default()
12893 })
12894 } else {
12895 Ok(item_to_resolve)
12896 }
12897 }
12898 }
12899 })
12900 .next()
12901 .await
12902 .unwrap();
12903 cx.run_until_parked();
12904
12905 cx.update_editor(|editor, window, cx| {
12906 editor.context_menu_next(&Default::default(), window, cx);
12907 });
12908
12909 cx.update_editor(|editor, _, _| {
12910 let context_menu = editor.context_menu.borrow_mut();
12911 let context_menu = context_menu
12912 .as_ref()
12913 .expect("Should have the context menu deployed");
12914 match context_menu {
12915 CodeContextMenu::Completions(completions_menu) => {
12916 let completions = completions_menu.completions.borrow_mut();
12917 assert_eq!(
12918 completions
12919 .iter()
12920 .map(|completion| &completion.label.text)
12921 .collect::<Vec<_>>(),
12922 vec!["method id() Now resolved!", "other"],
12923 "Should update first completion label, but not second as the filter text did not match."
12924 );
12925 }
12926 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12927 }
12928 });
12929}
12930
12931#[gpui::test]
12932async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12933 init_test(cx, |_| {});
12934
12935 let mut cx = EditorLspTestContext::new_rust(
12936 lsp::ServerCapabilities {
12937 completion_provider: Some(lsp::CompletionOptions {
12938 trigger_characters: Some(vec![".".to_string()]),
12939 resolve_provider: Some(true),
12940 ..Default::default()
12941 }),
12942 ..Default::default()
12943 },
12944 cx,
12945 )
12946 .await;
12947
12948 cx.set_state("fn main() { let a = 2ˇ; }");
12949 cx.simulate_keystroke(".");
12950
12951 let unresolved_item_1 = lsp::CompletionItem {
12952 label: "id".to_string(),
12953 filter_text: Some("id".to_string()),
12954 detail: None,
12955 documentation: None,
12956 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12957 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12958 new_text: ".id".to_string(),
12959 })),
12960 ..lsp::CompletionItem::default()
12961 };
12962 let resolved_item_1 = lsp::CompletionItem {
12963 additional_text_edits: Some(vec![lsp::TextEdit {
12964 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12965 new_text: "!!".to_string(),
12966 }]),
12967 ..unresolved_item_1.clone()
12968 };
12969 let unresolved_item_2 = lsp::CompletionItem {
12970 label: "other".to_string(),
12971 filter_text: Some("other".to_string()),
12972 detail: None,
12973 documentation: None,
12974 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12975 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12976 new_text: ".other".to_string(),
12977 })),
12978 ..lsp::CompletionItem::default()
12979 };
12980 let resolved_item_2 = lsp::CompletionItem {
12981 additional_text_edits: Some(vec![lsp::TextEdit {
12982 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12983 new_text: "??".to_string(),
12984 }]),
12985 ..unresolved_item_2.clone()
12986 };
12987
12988 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12989 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12990 cx.lsp
12991 .server
12992 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12993 let unresolved_item_1 = unresolved_item_1.clone();
12994 let resolved_item_1 = resolved_item_1.clone();
12995 let unresolved_item_2 = unresolved_item_2.clone();
12996 let resolved_item_2 = resolved_item_2.clone();
12997 let resolve_requests_1 = resolve_requests_1.clone();
12998 let resolve_requests_2 = resolve_requests_2.clone();
12999 move |unresolved_request, _| {
13000 let unresolved_item_1 = unresolved_item_1.clone();
13001 let resolved_item_1 = resolved_item_1.clone();
13002 let unresolved_item_2 = unresolved_item_2.clone();
13003 let resolved_item_2 = resolved_item_2.clone();
13004 let resolve_requests_1 = resolve_requests_1.clone();
13005 let resolve_requests_2 = resolve_requests_2.clone();
13006 async move {
13007 if unresolved_request == unresolved_item_1 {
13008 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13009 Ok(resolved_item_1.clone())
13010 } else if unresolved_request == unresolved_item_2 {
13011 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13012 Ok(resolved_item_2.clone())
13013 } else {
13014 panic!("Unexpected completion item {unresolved_request:?}")
13015 }
13016 }
13017 }
13018 })
13019 .detach();
13020
13021 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13022 let unresolved_item_1 = unresolved_item_1.clone();
13023 let unresolved_item_2 = unresolved_item_2.clone();
13024 async move {
13025 Ok(Some(lsp::CompletionResponse::Array(vec![
13026 unresolved_item_1,
13027 unresolved_item_2,
13028 ])))
13029 }
13030 })
13031 .next()
13032 .await;
13033
13034 cx.condition(|editor, _| editor.context_menu_visible())
13035 .await;
13036 cx.update_editor(|editor, _, _| {
13037 let context_menu = editor.context_menu.borrow_mut();
13038 let context_menu = context_menu
13039 .as_ref()
13040 .expect("Should have the context menu deployed");
13041 match context_menu {
13042 CodeContextMenu::Completions(completions_menu) => {
13043 let completions = completions_menu.completions.borrow_mut();
13044 assert_eq!(
13045 completions
13046 .iter()
13047 .map(|completion| &completion.label.text)
13048 .collect::<Vec<_>>(),
13049 vec!["id", "other"]
13050 )
13051 }
13052 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13053 }
13054 });
13055 cx.run_until_parked();
13056
13057 cx.update_editor(|editor, window, cx| {
13058 editor.context_menu_next(&ContextMenuNext, window, cx);
13059 });
13060 cx.run_until_parked();
13061 cx.update_editor(|editor, window, cx| {
13062 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13063 });
13064 cx.run_until_parked();
13065 cx.update_editor(|editor, window, cx| {
13066 editor.context_menu_next(&ContextMenuNext, window, cx);
13067 });
13068 cx.run_until_parked();
13069 cx.update_editor(|editor, window, cx| {
13070 editor
13071 .compose_completion(&ComposeCompletion::default(), window, cx)
13072 .expect("No task returned")
13073 })
13074 .await
13075 .expect("Completion failed");
13076 cx.run_until_parked();
13077
13078 cx.update_editor(|editor, _, cx| {
13079 assert_eq!(
13080 resolve_requests_1.load(atomic::Ordering::Acquire),
13081 1,
13082 "Should always resolve once despite multiple selections"
13083 );
13084 assert_eq!(
13085 resolve_requests_2.load(atomic::Ordering::Acquire),
13086 1,
13087 "Should always resolve once after multiple selections and applying the completion"
13088 );
13089 assert_eq!(
13090 editor.text(cx),
13091 "fn main() { let a = ??.other; }",
13092 "Should use resolved data when applying the completion"
13093 );
13094 });
13095}
13096
13097#[gpui::test]
13098async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13099 init_test(cx, |_| {});
13100
13101 let item_0 = lsp::CompletionItem {
13102 label: "abs".into(),
13103 insert_text: Some("abs".into()),
13104 data: Some(json!({ "very": "special"})),
13105 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13106 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13107 lsp::InsertReplaceEdit {
13108 new_text: "abs".to_string(),
13109 insert: lsp::Range::default(),
13110 replace: lsp::Range::default(),
13111 },
13112 )),
13113 ..lsp::CompletionItem::default()
13114 };
13115 let items = iter::once(item_0.clone())
13116 .chain((11..51).map(|i| lsp::CompletionItem {
13117 label: format!("item_{}", i),
13118 insert_text: Some(format!("item_{}", i)),
13119 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13120 ..lsp::CompletionItem::default()
13121 }))
13122 .collect::<Vec<_>>();
13123
13124 let default_commit_characters = vec!["?".to_string()];
13125 let default_data = json!({ "default": "data"});
13126 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13127 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13128 let default_edit_range = lsp::Range {
13129 start: lsp::Position {
13130 line: 0,
13131 character: 5,
13132 },
13133 end: lsp::Position {
13134 line: 0,
13135 character: 5,
13136 },
13137 };
13138
13139 let mut cx = EditorLspTestContext::new_rust(
13140 lsp::ServerCapabilities {
13141 completion_provider: Some(lsp::CompletionOptions {
13142 trigger_characters: Some(vec![".".to_string()]),
13143 resolve_provider: Some(true),
13144 ..Default::default()
13145 }),
13146 ..Default::default()
13147 },
13148 cx,
13149 )
13150 .await;
13151
13152 cx.set_state("fn main() { let a = 2ˇ; }");
13153 cx.simulate_keystroke(".");
13154
13155 let completion_data = default_data.clone();
13156 let completion_characters = default_commit_characters.clone();
13157 let completion_items = items.clone();
13158 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13159 let default_data = completion_data.clone();
13160 let default_commit_characters = completion_characters.clone();
13161 let items = completion_items.clone();
13162 async move {
13163 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13164 items,
13165 item_defaults: Some(lsp::CompletionListItemDefaults {
13166 data: Some(default_data.clone()),
13167 commit_characters: Some(default_commit_characters.clone()),
13168 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13169 default_edit_range,
13170 )),
13171 insert_text_format: Some(default_insert_text_format),
13172 insert_text_mode: Some(default_insert_text_mode),
13173 }),
13174 ..lsp::CompletionList::default()
13175 })))
13176 }
13177 })
13178 .next()
13179 .await;
13180
13181 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13182 cx.lsp
13183 .server
13184 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13185 let closure_resolved_items = resolved_items.clone();
13186 move |item_to_resolve, _| {
13187 let closure_resolved_items = closure_resolved_items.clone();
13188 async move {
13189 closure_resolved_items.lock().push(item_to_resolve.clone());
13190 Ok(item_to_resolve)
13191 }
13192 }
13193 })
13194 .detach();
13195
13196 cx.condition(|editor, _| editor.context_menu_visible())
13197 .await;
13198 cx.run_until_parked();
13199 cx.update_editor(|editor, _, _| {
13200 let menu = editor.context_menu.borrow_mut();
13201 match menu.as_ref().expect("should have the completions menu") {
13202 CodeContextMenu::Completions(completions_menu) => {
13203 assert_eq!(
13204 completions_menu
13205 .entries
13206 .borrow()
13207 .iter()
13208 .map(|mat| mat.string.clone())
13209 .collect::<Vec<String>>(),
13210 items
13211 .iter()
13212 .map(|completion| completion.label.clone())
13213 .collect::<Vec<String>>()
13214 );
13215 }
13216 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13217 }
13218 });
13219 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13220 // with 4 from the end.
13221 assert_eq!(
13222 *resolved_items.lock(),
13223 [&items[0..16], &items[items.len() - 4..items.len()]]
13224 .concat()
13225 .iter()
13226 .cloned()
13227 .map(|mut item| {
13228 if item.data.is_none() {
13229 item.data = Some(default_data.clone());
13230 }
13231 item
13232 })
13233 .collect::<Vec<lsp::CompletionItem>>(),
13234 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13235 );
13236 resolved_items.lock().clear();
13237
13238 cx.update_editor(|editor, window, cx| {
13239 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13240 });
13241 cx.run_until_parked();
13242 // Completions that have already been resolved are skipped.
13243 assert_eq!(
13244 *resolved_items.lock(),
13245 items[items.len() - 16..items.len() - 4]
13246 .iter()
13247 .cloned()
13248 .map(|mut item| {
13249 if item.data.is_none() {
13250 item.data = Some(default_data.clone());
13251 }
13252 item
13253 })
13254 .collect::<Vec<lsp::CompletionItem>>()
13255 );
13256 resolved_items.lock().clear();
13257}
13258
13259#[gpui::test]
13260async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13261 init_test(cx, |_| {});
13262
13263 let mut cx = EditorLspTestContext::new(
13264 Language::new(
13265 LanguageConfig {
13266 matcher: LanguageMatcher {
13267 path_suffixes: vec!["jsx".into()],
13268 ..Default::default()
13269 },
13270 overrides: [(
13271 "element".into(),
13272 LanguageConfigOverride {
13273 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13274 ..Default::default()
13275 },
13276 )]
13277 .into_iter()
13278 .collect(),
13279 ..Default::default()
13280 },
13281 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13282 )
13283 .with_override_query("(jsx_self_closing_element) @element")
13284 .unwrap(),
13285 lsp::ServerCapabilities {
13286 completion_provider: Some(lsp::CompletionOptions {
13287 trigger_characters: Some(vec![":".to_string()]),
13288 ..Default::default()
13289 }),
13290 ..Default::default()
13291 },
13292 cx,
13293 )
13294 .await;
13295
13296 cx.lsp
13297 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13298 Ok(Some(lsp::CompletionResponse::Array(vec![
13299 lsp::CompletionItem {
13300 label: "bg-blue".into(),
13301 ..Default::default()
13302 },
13303 lsp::CompletionItem {
13304 label: "bg-red".into(),
13305 ..Default::default()
13306 },
13307 lsp::CompletionItem {
13308 label: "bg-yellow".into(),
13309 ..Default::default()
13310 },
13311 ])))
13312 });
13313
13314 cx.set_state(r#"<p class="bgˇ" />"#);
13315
13316 // Trigger completion when typing a dash, because the dash is an extra
13317 // word character in the 'element' scope, which contains the cursor.
13318 cx.simulate_keystroke("-");
13319 cx.executor().run_until_parked();
13320 cx.update_editor(|editor, _, _| {
13321 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13322 {
13323 assert_eq!(
13324 completion_menu_entries(&menu),
13325 &["bg-red", "bg-blue", "bg-yellow"]
13326 );
13327 } else {
13328 panic!("expected completion menu to be open");
13329 }
13330 });
13331
13332 cx.simulate_keystroke("l");
13333 cx.executor().run_until_parked();
13334 cx.update_editor(|editor, _, _| {
13335 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13336 {
13337 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13338 } else {
13339 panic!("expected completion menu to be open");
13340 }
13341 });
13342
13343 // When filtering completions, consider the character after the '-' to
13344 // be the start of a subword.
13345 cx.set_state(r#"<p class="yelˇ" />"#);
13346 cx.simulate_keystroke("l");
13347 cx.executor().run_until_parked();
13348 cx.update_editor(|editor, _, _| {
13349 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13350 {
13351 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13352 } else {
13353 panic!("expected completion menu to be open");
13354 }
13355 });
13356}
13357
13358fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13359 let entries = menu.entries.borrow();
13360 entries.iter().map(|mat| mat.string.clone()).collect()
13361}
13362
13363#[gpui::test]
13364async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13365 init_test(cx, |settings| {
13366 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13367 FormatterList(vec![Formatter::Prettier].into()),
13368 ))
13369 });
13370
13371 let fs = FakeFs::new(cx.executor());
13372 fs.insert_file(path!("/file.ts"), Default::default()).await;
13373
13374 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13375 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13376
13377 language_registry.add(Arc::new(Language::new(
13378 LanguageConfig {
13379 name: "TypeScript".into(),
13380 matcher: LanguageMatcher {
13381 path_suffixes: vec!["ts".to_string()],
13382 ..Default::default()
13383 },
13384 ..Default::default()
13385 },
13386 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13387 )));
13388 update_test_language_settings(cx, |settings| {
13389 settings.defaults.prettier = Some(PrettierSettings {
13390 allowed: true,
13391 ..PrettierSettings::default()
13392 });
13393 });
13394
13395 let test_plugin = "test_plugin";
13396 let _ = language_registry.register_fake_lsp(
13397 "TypeScript",
13398 FakeLspAdapter {
13399 prettier_plugins: vec![test_plugin],
13400 ..Default::default()
13401 },
13402 );
13403
13404 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13405 let buffer = project
13406 .update(cx, |project, cx| {
13407 project.open_local_buffer(path!("/file.ts"), cx)
13408 })
13409 .await
13410 .unwrap();
13411
13412 let buffer_text = "one\ntwo\nthree\n";
13413 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13414 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13415 editor.update_in(cx, |editor, window, cx| {
13416 editor.set_text(buffer_text, window, cx)
13417 });
13418
13419 editor
13420 .update_in(cx, |editor, window, cx| {
13421 editor.perform_format(
13422 project.clone(),
13423 FormatTrigger::Manual,
13424 FormatTarget::Buffers,
13425 window,
13426 cx,
13427 )
13428 })
13429 .unwrap()
13430 .await;
13431 assert_eq!(
13432 editor.update(cx, |editor, cx| editor.text(cx)),
13433 buffer_text.to_string() + prettier_format_suffix,
13434 "Test prettier formatting was not applied to the original buffer text",
13435 );
13436
13437 update_test_language_settings(cx, |settings| {
13438 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13439 });
13440 let format = editor.update_in(cx, |editor, window, cx| {
13441 editor.perform_format(
13442 project.clone(),
13443 FormatTrigger::Manual,
13444 FormatTarget::Buffers,
13445 window,
13446 cx,
13447 )
13448 });
13449 format.await.unwrap();
13450 assert_eq!(
13451 editor.update(cx, |editor, cx| editor.text(cx)),
13452 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13453 "Autoformatting (via test prettier) was not applied to the original buffer text",
13454 );
13455}
13456
13457#[gpui::test]
13458async fn test_addition_reverts(cx: &mut TestAppContext) {
13459 init_test(cx, |_| {});
13460 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13461 let base_text = indoc! {r#"
13462 struct Row;
13463 struct Row1;
13464 struct Row2;
13465
13466 struct Row4;
13467 struct Row5;
13468 struct Row6;
13469
13470 struct Row8;
13471 struct Row9;
13472 struct Row10;"#};
13473
13474 // When addition hunks are not adjacent to carets, no hunk revert is performed
13475 assert_hunk_revert(
13476 indoc! {r#"struct Row;
13477 struct Row1;
13478 struct Row1.1;
13479 struct Row1.2;
13480 struct Row2;ˇ
13481
13482 struct Row4;
13483 struct Row5;
13484 struct Row6;
13485
13486 struct Row8;
13487 ˇstruct Row9;
13488 struct Row9.1;
13489 struct Row9.2;
13490 struct Row9.3;
13491 struct Row10;"#},
13492 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13493 indoc! {r#"struct Row;
13494 struct Row1;
13495 struct Row1.1;
13496 struct Row1.2;
13497 struct Row2;ˇ
13498
13499 struct Row4;
13500 struct Row5;
13501 struct Row6;
13502
13503 struct Row8;
13504 ˇstruct Row9;
13505 struct Row9.1;
13506 struct Row9.2;
13507 struct Row9.3;
13508 struct Row10;"#},
13509 base_text,
13510 &mut cx,
13511 );
13512 // Same for selections
13513 assert_hunk_revert(
13514 indoc! {r#"struct Row;
13515 struct Row1;
13516 struct Row2;
13517 struct Row2.1;
13518 struct Row2.2;
13519 «ˇ
13520 struct Row4;
13521 struct» Row5;
13522 «struct Row6;
13523 ˇ»
13524 struct Row9.1;
13525 struct Row9.2;
13526 struct Row9.3;
13527 struct Row8;
13528 struct Row9;
13529 struct Row10;"#},
13530 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13531 indoc! {r#"struct Row;
13532 struct Row1;
13533 struct Row2;
13534 struct Row2.1;
13535 struct Row2.2;
13536 «ˇ
13537 struct Row4;
13538 struct» Row5;
13539 «struct Row6;
13540 ˇ»
13541 struct Row9.1;
13542 struct Row9.2;
13543 struct Row9.3;
13544 struct Row8;
13545 struct Row9;
13546 struct Row10;"#},
13547 base_text,
13548 &mut cx,
13549 );
13550
13551 // When carets and selections intersect the addition hunks, those are reverted.
13552 // Adjacent carets got merged.
13553 assert_hunk_revert(
13554 indoc! {r#"struct Row;
13555 ˇ// something on the top
13556 struct Row1;
13557 struct Row2;
13558 struct Roˇw3.1;
13559 struct Row2.2;
13560 struct Row2.3;ˇ
13561
13562 struct Row4;
13563 struct ˇRow5.1;
13564 struct Row5.2;
13565 struct «Rowˇ»5.3;
13566 struct Row5;
13567 struct Row6;
13568 ˇ
13569 struct Row9.1;
13570 struct «Rowˇ»9.2;
13571 struct «ˇRow»9.3;
13572 struct Row8;
13573 struct Row9;
13574 «ˇ// something on bottom»
13575 struct Row10;"#},
13576 vec![
13577 DiffHunkStatusKind::Added,
13578 DiffHunkStatusKind::Added,
13579 DiffHunkStatusKind::Added,
13580 DiffHunkStatusKind::Added,
13581 DiffHunkStatusKind::Added,
13582 ],
13583 indoc! {r#"struct Row;
13584 ˇstruct Row1;
13585 struct Row2;
13586 ˇ
13587 struct Row4;
13588 ˇstruct Row5;
13589 struct Row6;
13590 ˇ
13591 ˇstruct Row8;
13592 struct Row9;
13593 ˇstruct Row10;"#},
13594 base_text,
13595 &mut cx,
13596 );
13597}
13598
13599#[gpui::test]
13600async fn test_modification_reverts(cx: &mut TestAppContext) {
13601 init_test(cx, |_| {});
13602 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13603 let base_text = indoc! {r#"
13604 struct Row;
13605 struct Row1;
13606 struct Row2;
13607
13608 struct Row4;
13609 struct Row5;
13610 struct Row6;
13611
13612 struct Row8;
13613 struct Row9;
13614 struct Row10;"#};
13615
13616 // Modification hunks behave the same as the addition ones.
13617 assert_hunk_revert(
13618 indoc! {r#"struct Row;
13619 struct Row1;
13620 struct Row33;
13621 ˇ
13622 struct Row4;
13623 struct Row5;
13624 struct Row6;
13625 ˇ
13626 struct Row99;
13627 struct Row9;
13628 struct Row10;"#},
13629 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13630 indoc! {r#"struct Row;
13631 struct Row1;
13632 struct Row33;
13633 ˇ
13634 struct Row4;
13635 struct Row5;
13636 struct Row6;
13637 ˇ
13638 struct Row99;
13639 struct Row9;
13640 struct Row10;"#},
13641 base_text,
13642 &mut cx,
13643 );
13644 assert_hunk_revert(
13645 indoc! {r#"struct Row;
13646 struct Row1;
13647 struct Row33;
13648 «ˇ
13649 struct Row4;
13650 struct» Row5;
13651 «struct Row6;
13652 ˇ»
13653 struct Row99;
13654 struct Row9;
13655 struct Row10;"#},
13656 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13657 indoc! {r#"struct Row;
13658 struct Row1;
13659 struct Row33;
13660 «ˇ
13661 struct Row4;
13662 struct» Row5;
13663 «struct Row6;
13664 ˇ»
13665 struct Row99;
13666 struct Row9;
13667 struct Row10;"#},
13668 base_text,
13669 &mut cx,
13670 );
13671
13672 assert_hunk_revert(
13673 indoc! {r#"ˇstruct Row1.1;
13674 struct Row1;
13675 «ˇstr»uct Row22;
13676
13677 struct ˇRow44;
13678 struct Row5;
13679 struct «Rˇ»ow66;ˇ
13680
13681 «struˇ»ct Row88;
13682 struct Row9;
13683 struct Row1011;ˇ"#},
13684 vec![
13685 DiffHunkStatusKind::Modified,
13686 DiffHunkStatusKind::Modified,
13687 DiffHunkStatusKind::Modified,
13688 DiffHunkStatusKind::Modified,
13689 DiffHunkStatusKind::Modified,
13690 DiffHunkStatusKind::Modified,
13691 ],
13692 indoc! {r#"struct Row;
13693 ˇstruct Row1;
13694 struct Row2;
13695 ˇ
13696 struct Row4;
13697 ˇstruct Row5;
13698 struct Row6;
13699 ˇ
13700 struct Row8;
13701 ˇstruct Row9;
13702 struct Row10;ˇ"#},
13703 base_text,
13704 &mut cx,
13705 );
13706}
13707
13708#[gpui::test]
13709async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13710 init_test(cx, |_| {});
13711 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13712 let base_text = indoc! {r#"
13713 one
13714
13715 two
13716 three
13717 "#};
13718
13719 cx.set_head_text(base_text);
13720 cx.set_state("\nˇ\n");
13721 cx.executor().run_until_parked();
13722 cx.update_editor(|editor, _window, cx| {
13723 editor.expand_selected_diff_hunks(cx);
13724 });
13725 cx.executor().run_until_parked();
13726 cx.update_editor(|editor, window, cx| {
13727 editor.backspace(&Default::default(), window, cx);
13728 });
13729 cx.run_until_parked();
13730 cx.assert_state_with_diff(
13731 indoc! {r#"
13732
13733 - two
13734 - threeˇ
13735 +
13736 "#}
13737 .to_string(),
13738 );
13739}
13740
13741#[gpui::test]
13742async fn test_deletion_reverts(cx: &mut TestAppContext) {
13743 init_test(cx, |_| {});
13744 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13745 let base_text = indoc! {r#"struct Row;
13746struct Row1;
13747struct Row2;
13748
13749struct Row4;
13750struct Row5;
13751struct Row6;
13752
13753struct Row8;
13754struct Row9;
13755struct Row10;"#};
13756
13757 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13758 assert_hunk_revert(
13759 indoc! {r#"struct Row;
13760 struct Row2;
13761
13762 ˇstruct Row4;
13763 struct Row5;
13764 struct Row6;
13765 ˇ
13766 struct Row8;
13767 struct Row10;"#},
13768 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13769 indoc! {r#"struct Row;
13770 struct Row2;
13771
13772 ˇstruct Row4;
13773 struct Row5;
13774 struct Row6;
13775 ˇ
13776 struct Row8;
13777 struct Row10;"#},
13778 base_text,
13779 &mut cx,
13780 );
13781 assert_hunk_revert(
13782 indoc! {r#"struct Row;
13783 struct Row2;
13784
13785 «ˇstruct Row4;
13786 struct» Row5;
13787 «struct Row6;
13788 ˇ»
13789 struct Row8;
13790 struct Row10;"#},
13791 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13792 indoc! {r#"struct Row;
13793 struct Row2;
13794
13795 «ˇstruct Row4;
13796 struct» Row5;
13797 «struct Row6;
13798 ˇ»
13799 struct Row8;
13800 struct Row10;"#},
13801 base_text,
13802 &mut cx,
13803 );
13804
13805 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13806 assert_hunk_revert(
13807 indoc! {r#"struct Row;
13808 ˇstruct Row2;
13809
13810 struct Row4;
13811 struct Row5;
13812 struct Row6;
13813
13814 struct Row8;ˇ
13815 struct Row10;"#},
13816 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13817 indoc! {r#"struct Row;
13818 struct Row1;
13819 ˇstruct Row2;
13820
13821 struct Row4;
13822 struct Row5;
13823 struct Row6;
13824
13825 struct Row8;ˇ
13826 struct Row9;
13827 struct Row10;"#},
13828 base_text,
13829 &mut cx,
13830 );
13831 assert_hunk_revert(
13832 indoc! {r#"struct Row;
13833 struct Row2«ˇ;
13834 struct Row4;
13835 struct» Row5;
13836 «struct Row6;
13837
13838 struct Row8;ˇ»
13839 struct Row10;"#},
13840 vec![
13841 DiffHunkStatusKind::Deleted,
13842 DiffHunkStatusKind::Deleted,
13843 DiffHunkStatusKind::Deleted,
13844 ],
13845 indoc! {r#"struct Row;
13846 struct Row1;
13847 struct Row2«ˇ;
13848
13849 struct Row4;
13850 struct» Row5;
13851 «struct Row6;
13852
13853 struct Row8;ˇ»
13854 struct Row9;
13855 struct Row10;"#},
13856 base_text,
13857 &mut cx,
13858 );
13859}
13860
13861#[gpui::test]
13862async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13863 init_test(cx, |_| {});
13864
13865 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13866 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13867 let base_text_3 =
13868 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13869
13870 let text_1 = edit_first_char_of_every_line(base_text_1);
13871 let text_2 = edit_first_char_of_every_line(base_text_2);
13872 let text_3 = edit_first_char_of_every_line(base_text_3);
13873
13874 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13875 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13876 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13877
13878 let multibuffer = cx.new(|cx| {
13879 let mut multibuffer = MultiBuffer::new(ReadWrite);
13880 multibuffer.push_excerpts(
13881 buffer_1.clone(),
13882 [
13883 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13884 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13885 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13886 ],
13887 cx,
13888 );
13889 multibuffer.push_excerpts(
13890 buffer_2.clone(),
13891 [
13892 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13893 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13894 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13895 ],
13896 cx,
13897 );
13898 multibuffer.push_excerpts(
13899 buffer_3.clone(),
13900 [
13901 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13902 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13903 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13904 ],
13905 cx,
13906 );
13907 multibuffer
13908 });
13909
13910 let fs = FakeFs::new(cx.executor());
13911 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13912 let (editor, cx) = cx
13913 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13914 editor.update_in(cx, |editor, _window, cx| {
13915 for (buffer, diff_base) in [
13916 (buffer_1.clone(), base_text_1),
13917 (buffer_2.clone(), base_text_2),
13918 (buffer_3.clone(), base_text_3),
13919 ] {
13920 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13921 editor
13922 .buffer
13923 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13924 }
13925 });
13926 cx.executor().run_until_parked();
13927
13928 editor.update_in(cx, |editor, window, cx| {
13929 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}");
13930 editor.select_all(&SelectAll, window, cx);
13931 editor.git_restore(&Default::default(), window, cx);
13932 });
13933 cx.executor().run_until_parked();
13934
13935 // When all ranges are selected, all buffer hunks are reverted.
13936 editor.update(cx, |editor, cx| {
13937 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");
13938 });
13939 buffer_1.update(cx, |buffer, _| {
13940 assert_eq!(buffer.text(), base_text_1);
13941 });
13942 buffer_2.update(cx, |buffer, _| {
13943 assert_eq!(buffer.text(), base_text_2);
13944 });
13945 buffer_3.update(cx, |buffer, _| {
13946 assert_eq!(buffer.text(), base_text_3);
13947 });
13948
13949 editor.update_in(cx, |editor, window, cx| {
13950 editor.undo(&Default::default(), window, cx);
13951 });
13952
13953 editor.update_in(cx, |editor, window, cx| {
13954 editor.change_selections(None, window, cx, |s| {
13955 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13956 });
13957 editor.git_restore(&Default::default(), window, cx);
13958 });
13959
13960 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13961 // but not affect buffer_2 and its related excerpts.
13962 editor.update(cx, |editor, cx| {
13963 assert_eq!(
13964 editor.text(cx),
13965 "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}"
13966 );
13967 });
13968 buffer_1.update(cx, |buffer, _| {
13969 assert_eq!(buffer.text(), base_text_1);
13970 });
13971 buffer_2.update(cx, |buffer, _| {
13972 assert_eq!(
13973 buffer.text(),
13974 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13975 );
13976 });
13977 buffer_3.update(cx, |buffer, _| {
13978 assert_eq!(
13979 buffer.text(),
13980 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13981 );
13982 });
13983
13984 fn edit_first_char_of_every_line(text: &str) -> String {
13985 text.split('\n')
13986 .map(|line| format!("X{}", &line[1..]))
13987 .collect::<Vec<_>>()
13988 .join("\n")
13989 }
13990}
13991
13992#[gpui::test]
13993async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13994 init_test(cx, |_| {});
13995
13996 let cols = 4;
13997 let rows = 10;
13998 let sample_text_1 = sample_text(rows, cols, 'a');
13999 assert_eq!(
14000 sample_text_1,
14001 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14002 );
14003 let sample_text_2 = sample_text(rows, cols, 'l');
14004 assert_eq!(
14005 sample_text_2,
14006 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14007 );
14008 let sample_text_3 = sample_text(rows, cols, 'v');
14009 assert_eq!(
14010 sample_text_3,
14011 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14012 );
14013
14014 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14015 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14016 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14017
14018 let multi_buffer = cx.new(|cx| {
14019 let mut multibuffer = MultiBuffer::new(ReadWrite);
14020 multibuffer.push_excerpts(
14021 buffer_1.clone(),
14022 [
14023 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14024 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14025 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14026 ],
14027 cx,
14028 );
14029 multibuffer.push_excerpts(
14030 buffer_2.clone(),
14031 [
14032 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14033 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14034 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14035 ],
14036 cx,
14037 );
14038 multibuffer.push_excerpts(
14039 buffer_3.clone(),
14040 [
14041 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14042 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14043 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14044 ],
14045 cx,
14046 );
14047 multibuffer
14048 });
14049
14050 let fs = FakeFs::new(cx.executor());
14051 fs.insert_tree(
14052 "/a",
14053 json!({
14054 "main.rs": sample_text_1,
14055 "other.rs": sample_text_2,
14056 "lib.rs": sample_text_3,
14057 }),
14058 )
14059 .await;
14060 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14061 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14062 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14063 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14064 Editor::new(
14065 EditorMode::Full,
14066 multi_buffer,
14067 Some(project.clone()),
14068 window,
14069 cx,
14070 )
14071 });
14072 let multibuffer_item_id = workspace
14073 .update(cx, |workspace, window, cx| {
14074 assert!(
14075 workspace.active_item(cx).is_none(),
14076 "active item should be None before the first item is added"
14077 );
14078 workspace.add_item_to_active_pane(
14079 Box::new(multi_buffer_editor.clone()),
14080 None,
14081 true,
14082 window,
14083 cx,
14084 );
14085 let active_item = workspace
14086 .active_item(cx)
14087 .expect("should have an active item after adding the multi buffer");
14088 assert!(
14089 !active_item.is_singleton(cx),
14090 "A multi buffer was expected to active after adding"
14091 );
14092 active_item.item_id()
14093 })
14094 .unwrap();
14095 cx.executor().run_until_parked();
14096
14097 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14098 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14099 s.select_ranges(Some(1..2))
14100 });
14101 editor.open_excerpts(&OpenExcerpts, window, cx);
14102 });
14103 cx.executor().run_until_parked();
14104 let first_item_id = workspace
14105 .update(cx, |workspace, window, cx| {
14106 let active_item = workspace
14107 .active_item(cx)
14108 .expect("should have an active item after navigating into the 1st buffer");
14109 let first_item_id = active_item.item_id();
14110 assert_ne!(
14111 first_item_id, multibuffer_item_id,
14112 "Should navigate into the 1st buffer and activate it"
14113 );
14114 assert!(
14115 active_item.is_singleton(cx),
14116 "New active item should be a singleton buffer"
14117 );
14118 assert_eq!(
14119 active_item
14120 .act_as::<Editor>(cx)
14121 .expect("should have navigated into an editor for the 1st buffer")
14122 .read(cx)
14123 .text(cx),
14124 sample_text_1
14125 );
14126
14127 workspace
14128 .go_back(workspace.active_pane().downgrade(), window, cx)
14129 .detach_and_log_err(cx);
14130
14131 first_item_id
14132 })
14133 .unwrap();
14134 cx.executor().run_until_parked();
14135 workspace
14136 .update(cx, |workspace, _, cx| {
14137 let active_item = workspace
14138 .active_item(cx)
14139 .expect("should have an active item after navigating back");
14140 assert_eq!(
14141 active_item.item_id(),
14142 multibuffer_item_id,
14143 "Should navigate back to the multi buffer"
14144 );
14145 assert!(!active_item.is_singleton(cx));
14146 })
14147 .unwrap();
14148
14149 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14150 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14151 s.select_ranges(Some(39..40))
14152 });
14153 editor.open_excerpts(&OpenExcerpts, window, cx);
14154 });
14155 cx.executor().run_until_parked();
14156 let second_item_id = workspace
14157 .update(cx, |workspace, window, cx| {
14158 let active_item = workspace
14159 .active_item(cx)
14160 .expect("should have an active item after navigating into the 2nd buffer");
14161 let second_item_id = active_item.item_id();
14162 assert_ne!(
14163 second_item_id, multibuffer_item_id,
14164 "Should navigate away from the multibuffer"
14165 );
14166 assert_ne!(
14167 second_item_id, first_item_id,
14168 "Should navigate into the 2nd buffer and activate it"
14169 );
14170 assert!(
14171 active_item.is_singleton(cx),
14172 "New active item should be a singleton buffer"
14173 );
14174 assert_eq!(
14175 active_item
14176 .act_as::<Editor>(cx)
14177 .expect("should have navigated into an editor")
14178 .read(cx)
14179 .text(cx),
14180 sample_text_2
14181 );
14182
14183 workspace
14184 .go_back(workspace.active_pane().downgrade(), window, cx)
14185 .detach_and_log_err(cx);
14186
14187 second_item_id
14188 })
14189 .unwrap();
14190 cx.executor().run_until_parked();
14191 workspace
14192 .update(cx, |workspace, _, cx| {
14193 let active_item = workspace
14194 .active_item(cx)
14195 .expect("should have an active item after navigating back from the 2nd buffer");
14196 assert_eq!(
14197 active_item.item_id(),
14198 multibuffer_item_id,
14199 "Should navigate back from the 2nd buffer to the multi buffer"
14200 );
14201 assert!(!active_item.is_singleton(cx));
14202 })
14203 .unwrap();
14204
14205 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14206 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14207 s.select_ranges(Some(70..70))
14208 });
14209 editor.open_excerpts(&OpenExcerpts, window, cx);
14210 });
14211 cx.executor().run_until_parked();
14212 workspace
14213 .update(cx, |workspace, window, cx| {
14214 let active_item = workspace
14215 .active_item(cx)
14216 .expect("should have an active item after navigating into the 3rd buffer");
14217 let third_item_id = active_item.item_id();
14218 assert_ne!(
14219 third_item_id, multibuffer_item_id,
14220 "Should navigate into the 3rd buffer and activate it"
14221 );
14222 assert_ne!(third_item_id, first_item_id);
14223 assert_ne!(third_item_id, second_item_id);
14224 assert!(
14225 active_item.is_singleton(cx),
14226 "New active item should be a singleton buffer"
14227 );
14228 assert_eq!(
14229 active_item
14230 .act_as::<Editor>(cx)
14231 .expect("should have navigated into an editor")
14232 .read(cx)
14233 .text(cx),
14234 sample_text_3
14235 );
14236
14237 workspace
14238 .go_back(workspace.active_pane().downgrade(), window, cx)
14239 .detach_and_log_err(cx);
14240 })
14241 .unwrap();
14242 cx.executor().run_until_parked();
14243 workspace
14244 .update(cx, |workspace, _, cx| {
14245 let active_item = workspace
14246 .active_item(cx)
14247 .expect("should have an active item after navigating back from the 3rd buffer");
14248 assert_eq!(
14249 active_item.item_id(),
14250 multibuffer_item_id,
14251 "Should navigate back from the 3rd buffer to the multi buffer"
14252 );
14253 assert!(!active_item.is_singleton(cx));
14254 })
14255 .unwrap();
14256}
14257
14258#[gpui::test]
14259async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14260 init_test(cx, |_| {});
14261
14262 let mut cx = EditorTestContext::new(cx).await;
14263
14264 let diff_base = r#"
14265 use some::mod;
14266
14267 const A: u32 = 42;
14268
14269 fn main() {
14270 println!("hello");
14271
14272 println!("world");
14273 }
14274 "#
14275 .unindent();
14276
14277 cx.set_state(
14278 &r#"
14279 use some::modified;
14280
14281 ˇ
14282 fn main() {
14283 println!("hello there");
14284
14285 println!("around the");
14286 println!("world");
14287 }
14288 "#
14289 .unindent(),
14290 );
14291
14292 cx.set_head_text(&diff_base);
14293 executor.run_until_parked();
14294
14295 cx.update_editor(|editor, window, cx| {
14296 editor.go_to_next_hunk(&GoToHunk, window, cx);
14297 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14298 });
14299 executor.run_until_parked();
14300 cx.assert_state_with_diff(
14301 r#"
14302 use some::modified;
14303
14304
14305 fn main() {
14306 - println!("hello");
14307 + ˇ println!("hello there");
14308
14309 println!("around the");
14310 println!("world");
14311 }
14312 "#
14313 .unindent(),
14314 );
14315
14316 cx.update_editor(|editor, window, cx| {
14317 for _ in 0..2 {
14318 editor.go_to_next_hunk(&GoToHunk, window, cx);
14319 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14320 }
14321 });
14322 executor.run_until_parked();
14323 cx.assert_state_with_diff(
14324 r#"
14325 - use some::mod;
14326 + ˇuse some::modified;
14327
14328
14329 fn main() {
14330 - println!("hello");
14331 + println!("hello there");
14332
14333 + println!("around the");
14334 println!("world");
14335 }
14336 "#
14337 .unindent(),
14338 );
14339
14340 cx.update_editor(|editor, window, cx| {
14341 editor.go_to_next_hunk(&GoToHunk, window, cx);
14342 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14343 });
14344 executor.run_until_parked();
14345 cx.assert_state_with_diff(
14346 r#"
14347 - use some::mod;
14348 + use some::modified;
14349
14350 - const A: u32 = 42;
14351 ˇ
14352 fn main() {
14353 - println!("hello");
14354 + println!("hello there");
14355
14356 + println!("around the");
14357 println!("world");
14358 }
14359 "#
14360 .unindent(),
14361 );
14362
14363 cx.update_editor(|editor, window, cx| {
14364 editor.cancel(&Cancel, window, cx);
14365 });
14366
14367 cx.assert_state_with_diff(
14368 r#"
14369 use some::modified;
14370
14371 ˇ
14372 fn main() {
14373 println!("hello there");
14374
14375 println!("around the");
14376 println!("world");
14377 }
14378 "#
14379 .unindent(),
14380 );
14381}
14382
14383#[gpui::test]
14384async fn test_diff_base_change_with_expanded_diff_hunks(
14385 executor: BackgroundExecutor,
14386 cx: &mut TestAppContext,
14387) {
14388 init_test(cx, |_| {});
14389
14390 let mut cx = EditorTestContext::new(cx).await;
14391
14392 let diff_base = r#"
14393 use some::mod1;
14394 use some::mod2;
14395
14396 const A: u32 = 42;
14397 const B: u32 = 42;
14398 const C: u32 = 42;
14399
14400 fn main() {
14401 println!("hello");
14402
14403 println!("world");
14404 }
14405 "#
14406 .unindent();
14407
14408 cx.set_state(
14409 &r#"
14410 use some::mod2;
14411
14412 const A: u32 = 42;
14413 const C: u32 = 42;
14414
14415 fn main(ˇ) {
14416 //println!("hello");
14417
14418 println!("world");
14419 //
14420 //
14421 }
14422 "#
14423 .unindent(),
14424 );
14425
14426 cx.set_head_text(&diff_base);
14427 executor.run_until_parked();
14428
14429 cx.update_editor(|editor, window, cx| {
14430 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14431 });
14432 executor.run_until_parked();
14433 cx.assert_state_with_diff(
14434 r#"
14435 - use some::mod1;
14436 use some::mod2;
14437
14438 const A: u32 = 42;
14439 - const B: u32 = 42;
14440 const C: u32 = 42;
14441
14442 fn main(ˇ) {
14443 - println!("hello");
14444 + //println!("hello");
14445
14446 println!("world");
14447 + //
14448 + //
14449 }
14450 "#
14451 .unindent(),
14452 );
14453
14454 cx.set_head_text("new diff base!");
14455 executor.run_until_parked();
14456 cx.assert_state_with_diff(
14457 r#"
14458 - new diff base!
14459 + use some::mod2;
14460 +
14461 + const A: u32 = 42;
14462 + const C: u32 = 42;
14463 +
14464 + fn main(ˇ) {
14465 + //println!("hello");
14466 +
14467 + println!("world");
14468 + //
14469 + //
14470 + }
14471 "#
14472 .unindent(),
14473 );
14474}
14475
14476#[gpui::test]
14477async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14478 init_test(cx, |_| {});
14479
14480 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14481 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14482 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14483 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14484 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14485 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14486
14487 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14488 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14489 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14490
14491 let multi_buffer = cx.new(|cx| {
14492 let mut multibuffer = MultiBuffer::new(ReadWrite);
14493 multibuffer.push_excerpts(
14494 buffer_1.clone(),
14495 [
14496 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14497 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14498 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14499 ],
14500 cx,
14501 );
14502 multibuffer.push_excerpts(
14503 buffer_2.clone(),
14504 [
14505 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14506 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14507 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14508 ],
14509 cx,
14510 );
14511 multibuffer.push_excerpts(
14512 buffer_3.clone(),
14513 [
14514 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14515 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14516 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14517 ],
14518 cx,
14519 );
14520 multibuffer
14521 });
14522
14523 let editor =
14524 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14525 editor
14526 .update(cx, |editor, _window, cx| {
14527 for (buffer, diff_base) in [
14528 (buffer_1.clone(), file_1_old),
14529 (buffer_2.clone(), file_2_old),
14530 (buffer_3.clone(), file_3_old),
14531 ] {
14532 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14533 editor
14534 .buffer
14535 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14536 }
14537 })
14538 .unwrap();
14539
14540 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14541 cx.run_until_parked();
14542
14543 cx.assert_editor_state(
14544 &"
14545 ˇaaa
14546 ccc
14547 ddd
14548
14549 ggg
14550 hhh
14551
14552
14553 lll
14554 mmm
14555 NNN
14556
14557 qqq
14558 rrr
14559
14560 uuu
14561 111
14562 222
14563 333
14564
14565 666
14566 777
14567
14568 000
14569 !!!"
14570 .unindent(),
14571 );
14572
14573 cx.update_editor(|editor, window, cx| {
14574 editor.select_all(&SelectAll, window, cx);
14575 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14576 });
14577 cx.executor().run_until_parked();
14578
14579 cx.assert_state_with_diff(
14580 "
14581 «aaa
14582 - bbb
14583 ccc
14584 ddd
14585
14586 ggg
14587 hhh
14588
14589
14590 lll
14591 mmm
14592 - nnn
14593 + NNN
14594
14595 qqq
14596 rrr
14597
14598 uuu
14599 111
14600 222
14601 333
14602
14603 + 666
14604 777
14605
14606 000
14607 !!!ˇ»"
14608 .unindent(),
14609 );
14610}
14611
14612#[gpui::test]
14613async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14614 init_test(cx, |_| {});
14615
14616 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14617 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14618
14619 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14620 let multi_buffer = cx.new(|cx| {
14621 let mut multibuffer = MultiBuffer::new(ReadWrite);
14622 multibuffer.push_excerpts(
14623 buffer.clone(),
14624 [
14625 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
14626 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
14627 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
14628 ],
14629 cx,
14630 );
14631 multibuffer
14632 });
14633
14634 let editor =
14635 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14636 editor
14637 .update(cx, |editor, _window, cx| {
14638 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14639 editor
14640 .buffer
14641 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14642 })
14643 .unwrap();
14644
14645 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14646 cx.run_until_parked();
14647
14648 cx.update_editor(|editor, window, cx| {
14649 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14650 });
14651 cx.executor().run_until_parked();
14652
14653 // When the start of a hunk coincides with the start of its excerpt,
14654 // the hunk is expanded. When the start of a a hunk is earlier than
14655 // the start of its excerpt, the hunk is not expanded.
14656 cx.assert_state_with_diff(
14657 "
14658 ˇaaa
14659 - bbb
14660 + BBB
14661
14662 - ddd
14663 - eee
14664 + DDD
14665 + EEE
14666 fff
14667
14668 iii
14669 "
14670 .unindent(),
14671 );
14672}
14673
14674#[gpui::test]
14675async fn test_edits_around_expanded_insertion_hunks(
14676 executor: BackgroundExecutor,
14677 cx: &mut TestAppContext,
14678) {
14679 init_test(cx, |_| {});
14680
14681 let mut cx = EditorTestContext::new(cx).await;
14682
14683 let diff_base = r#"
14684 use some::mod1;
14685 use some::mod2;
14686
14687 const A: u32 = 42;
14688
14689 fn main() {
14690 println!("hello");
14691
14692 println!("world");
14693 }
14694 "#
14695 .unindent();
14696 executor.run_until_parked();
14697 cx.set_state(
14698 &r#"
14699 use some::mod1;
14700 use some::mod2;
14701
14702 const A: u32 = 42;
14703 const B: u32 = 42;
14704 const C: u32 = 42;
14705 ˇ
14706
14707 fn main() {
14708 println!("hello");
14709
14710 println!("world");
14711 }
14712 "#
14713 .unindent(),
14714 );
14715
14716 cx.set_head_text(&diff_base);
14717 executor.run_until_parked();
14718
14719 cx.update_editor(|editor, window, cx| {
14720 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14721 });
14722 executor.run_until_parked();
14723
14724 cx.assert_state_with_diff(
14725 r#"
14726 use some::mod1;
14727 use some::mod2;
14728
14729 const A: u32 = 42;
14730 + const B: u32 = 42;
14731 + const C: u32 = 42;
14732 + ˇ
14733
14734 fn main() {
14735 println!("hello");
14736
14737 println!("world");
14738 }
14739 "#
14740 .unindent(),
14741 );
14742
14743 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14744 executor.run_until_parked();
14745
14746 cx.assert_state_with_diff(
14747 r#"
14748 use some::mod1;
14749 use some::mod2;
14750
14751 const A: u32 = 42;
14752 + const B: u32 = 42;
14753 + const C: u32 = 42;
14754 + const D: u32 = 42;
14755 + ˇ
14756
14757 fn main() {
14758 println!("hello");
14759
14760 println!("world");
14761 }
14762 "#
14763 .unindent(),
14764 );
14765
14766 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14767 executor.run_until_parked();
14768
14769 cx.assert_state_with_diff(
14770 r#"
14771 use some::mod1;
14772 use some::mod2;
14773
14774 const A: u32 = 42;
14775 + const B: u32 = 42;
14776 + const C: u32 = 42;
14777 + const D: u32 = 42;
14778 + const E: u32 = 42;
14779 + ˇ
14780
14781 fn main() {
14782 println!("hello");
14783
14784 println!("world");
14785 }
14786 "#
14787 .unindent(),
14788 );
14789
14790 cx.update_editor(|editor, window, cx| {
14791 editor.delete_line(&DeleteLine, window, cx);
14792 });
14793 executor.run_until_parked();
14794
14795 cx.assert_state_with_diff(
14796 r#"
14797 use some::mod1;
14798 use some::mod2;
14799
14800 const A: u32 = 42;
14801 + const B: u32 = 42;
14802 + const C: u32 = 42;
14803 + const D: u32 = 42;
14804 + const E: u32 = 42;
14805 ˇ
14806 fn main() {
14807 println!("hello");
14808
14809 println!("world");
14810 }
14811 "#
14812 .unindent(),
14813 );
14814
14815 cx.update_editor(|editor, window, cx| {
14816 editor.move_up(&MoveUp, window, cx);
14817 editor.delete_line(&DeleteLine, window, cx);
14818 editor.move_up(&MoveUp, window, cx);
14819 editor.delete_line(&DeleteLine, window, cx);
14820 editor.move_up(&MoveUp, window, cx);
14821 editor.delete_line(&DeleteLine, window, cx);
14822 });
14823 executor.run_until_parked();
14824 cx.assert_state_with_diff(
14825 r#"
14826 use some::mod1;
14827 use some::mod2;
14828
14829 const A: u32 = 42;
14830 + const B: u32 = 42;
14831 ˇ
14832 fn main() {
14833 println!("hello");
14834
14835 println!("world");
14836 }
14837 "#
14838 .unindent(),
14839 );
14840
14841 cx.update_editor(|editor, window, cx| {
14842 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14843 editor.delete_line(&DeleteLine, window, cx);
14844 });
14845 executor.run_until_parked();
14846 cx.assert_state_with_diff(
14847 r#"
14848 ˇ
14849 fn main() {
14850 println!("hello");
14851
14852 println!("world");
14853 }
14854 "#
14855 .unindent(),
14856 );
14857}
14858
14859#[gpui::test]
14860async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14861 init_test(cx, |_| {});
14862
14863 let mut cx = EditorTestContext::new(cx).await;
14864 cx.set_head_text(indoc! { "
14865 one
14866 two
14867 three
14868 four
14869 five
14870 "
14871 });
14872 cx.set_state(indoc! { "
14873 one
14874 ˇthree
14875 five
14876 "});
14877 cx.run_until_parked();
14878 cx.update_editor(|editor, window, cx| {
14879 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14880 });
14881 cx.assert_state_with_diff(
14882 indoc! { "
14883 one
14884 - two
14885 ˇthree
14886 - four
14887 five
14888 "}
14889 .to_string(),
14890 );
14891 cx.update_editor(|editor, window, cx| {
14892 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14893 });
14894
14895 cx.assert_state_with_diff(
14896 indoc! { "
14897 one
14898 ˇthree
14899 five
14900 "}
14901 .to_string(),
14902 );
14903
14904 cx.set_state(indoc! { "
14905 one
14906 ˇTWO
14907 three
14908 four
14909 five
14910 "});
14911 cx.run_until_parked();
14912 cx.update_editor(|editor, window, cx| {
14913 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14914 });
14915
14916 cx.assert_state_with_diff(
14917 indoc! { "
14918 one
14919 - two
14920 + ˇTWO
14921 three
14922 four
14923 five
14924 "}
14925 .to_string(),
14926 );
14927 cx.update_editor(|editor, window, cx| {
14928 editor.move_up(&Default::default(), window, cx);
14929 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14930 });
14931 cx.assert_state_with_diff(
14932 indoc! { "
14933 one
14934 ˇTWO
14935 three
14936 four
14937 five
14938 "}
14939 .to_string(),
14940 );
14941}
14942
14943#[gpui::test]
14944async fn test_edits_around_expanded_deletion_hunks(
14945 executor: BackgroundExecutor,
14946 cx: &mut TestAppContext,
14947) {
14948 init_test(cx, |_| {});
14949
14950 let mut cx = EditorTestContext::new(cx).await;
14951
14952 let diff_base = r#"
14953 use some::mod1;
14954 use some::mod2;
14955
14956 const A: u32 = 42;
14957 const B: u32 = 42;
14958 const C: u32 = 42;
14959
14960
14961 fn main() {
14962 println!("hello");
14963
14964 println!("world");
14965 }
14966 "#
14967 .unindent();
14968 executor.run_until_parked();
14969 cx.set_state(
14970 &r#"
14971 use some::mod1;
14972 use some::mod2;
14973
14974 ˇconst B: u32 = 42;
14975 const C: u32 = 42;
14976
14977
14978 fn main() {
14979 println!("hello");
14980
14981 println!("world");
14982 }
14983 "#
14984 .unindent(),
14985 );
14986
14987 cx.set_head_text(&diff_base);
14988 executor.run_until_parked();
14989
14990 cx.update_editor(|editor, window, cx| {
14991 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14992 });
14993 executor.run_until_parked();
14994
14995 cx.assert_state_with_diff(
14996 r#"
14997 use some::mod1;
14998 use some::mod2;
14999
15000 - const A: u32 = 42;
15001 ˇconst B: u32 = 42;
15002 const C: u32 = 42;
15003
15004
15005 fn main() {
15006 println!("hello");
15007
15008 println!("world");
15009 }
15010 "#
15011 .unindent(),
15012 );
15013
15014 cx.update_editor(|editor, window, cx| {
15015 editor.delete_line(&DeleteLine, window, cx);
15016 });
15017 executor.run_until_parked();
15018 cx.assert_state_with_diff(
15019 r#"
15020 use some::mod1;
15021 use some::mod2;
15022
15023 - const A: u32 = 42;
15024 - const B: u32 = 42;
15025 ˇconst C: u32 = 42;
15026
15027
15028 fn main() {
15029 println!("hello");
15030
15031 println!("world");
15032 }
15033 "#
15034 .unindent(),
15035 );
15036
15037 cx.update_editor(|editor, window, cx| {
15038 editor.delete_line(&DeleteLine, window, cx);
15039 });
15040 executor.run_until_parked();
15041 cx.assert_state_with_diff(
15042 r#"
15043 use some::mod1;
15044 use some::mod2;
15045
15046 - const A: u32 = 42;
15047 - const B: u32 = 42;
15048 - const C: u32 = 42;
15049 ˇ
15050
15051 fn main() {
15052 println!("hello");
15053
15054 println!("world");
15055 }
15056 "#
15057 .unindent(),
15058 );
15059
15060 cx.update_editor(|editor, window, cx| {
15061 editor.handle_input("replacement", window, cx);
15062 });
15063 executor.run_until_parked();
15064 cx.assert_state_with_diff(
15065 r#"
15066 use some::mod1;
15067 use some::mod2;
15068
15069 - const A: u32 = 42;
15070 - const B: u32 = 42;
15071 - const C: u32 = 42;
15072 -
15073 + replacementˇ
15074
15075 fn main() {
15076 println!("hello");
15077
15078 println!("world");
15079 }
15080 "#
15081 .unindent(),
15082 );
15083}
15084
15085#[gpui::test]
15086async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15087 init_test(cx, |_| {});
15088
15089 let mut cx = EditorTestContext::new(cx).await;
15090
15091 let base_text = r#"
15092 one
15093 two
15094 three
15095 four
15096 five
15097 "#
15098 .unindent();
15099 executor.run_until_parked();
15100 cx.set_state(
15101 &r#"
15102 one
15103 two
15104 fˇour
15105 five
15106 "#
15107 .unindent(),
15108 );
15109
15110 cx.set_head_text(&base_text);
15111 executor.run_until_parked();
15112
15113 cx.update_editor(|editor, window, cx| {
15114 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15115 });
15116 executor.run_until_parked();
15117
15118 cx.assert_state_with_diff(
15119 r#"
15120 one
15121 two
15122 - three
15123 fˇour
15124 five
15125 "#
15126 .unindent(),
15127 );
15128
15129 cx.update_editor(|editor, window, cx| {
15130 editor.backspace(&Backspace, window, cx);
15131 editor.backspace(&Backspace, window, cx);
15132 });
15133 executor.run_until_parked();
15134 cx.assert_state_with_diff(
15135 r#"
15136 one
15137 two
15138 - threeˇ
15139 - four
15140 + our
15141 five
15142 "#
15143 .unindent(),
15144 );
15145}
15146
15147#[gpui::test]
15148async fn test_edit_after_expanded_modification_hunk(
15149 executor: BackgroundExecutor,
15150 cx: &mut TestAppContext,
15151) {
15152 init_test(cx, |_| {});
15153
15154 let mut cx = EditorTestContext::new(cx).await;
15155
15156 let diff_base = r#"
15157 use some::mod1;
15158 use some::mod2;
15159
15160 const A: u32 = 42;
15161 const B: u32 = 42;
15162 const C: u32 = 42;
15163 const D: u32 = 42;
15164
15165
15166 fn main() {
15167 println!("hello");
15168
15169 println!("world");
15170 }"#
15171 .unindent();
15172
15173 cx.set_state(
15174 &r#"
15175 use some::mod1;
15176 use some::mod2;
15177
15178 const A: u32 = 42;
15179 const B: u32 = 42;
15180 const C: u32 = 43ˇ
15181 const D: u32 = 42;
15182
15183
15184 fn main() {
15185 println!("hello");
15186
15187 println!("world");
15188 }"#
15189 .unindent(),
15190 );
15191
15192 cx.set_head_text(&diff_base);
15193 executor.run_until_parked();
15194 cx.update_editor(|editor, window, cx| {
15195 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15196 });
15197 executor.run_until_parked();
15198
15199 cx.assert_state_with_diff(
15200 r#"
15201 use some::mod1;
15202 use some::mod2;
15203
15204 const A: u32 = 42;
15205 const B: u32 = 42;
15206 - const C: u32 = 42;
15207 + const C: u32 = 43ˇ
15208 const D: u32 = 42;
15209
15210
15211 fn main() {
15212 println!("hello");
15213
15214 println!("world");
15215 }"#
15216 .unindent(),
15217 );
15218
15219 cx.update_editor(|editor, window, cx| {
15220 editor.handle_input("\nnew_line\n", window, cx);
15221 });
15222 executor.run_until_parked();
15223
15224 cx.assert_state_with_diff(
15225 r#"
15226 use some::mod1;
15227 use some::mod2;
15228
15229 const A: u32 = 42;
15230 const B: u32 = 42;
15231 - const C: u32 = 42;
15232 + const C: u32 = 43
15233 + new_line
15234 + ˇ
15235 const D: u32 = 42;
15236
15237
15238 fn main() {
15239 println!("hello");
15240
15241 println!("world");
15242 }"#
15243 .unindent(),
15244 );
15245}
15246
15247#[gpui::test]
15248async fn test_stage_and_unstage_added_file_hunk(
15249 executor: BackgroundExecutor,
15250 cx: &mut TestAppContext,
15251) {
15252 init_test(cx, |_| {});
15253
15254 let mut cx = EditorTestContext::new(cx).await;
15255 cx.update_editor(|editor, _, cx| {
15256 editor.set_expand_all_diff_hunks(cx);
15257 });
15258
15259 let working_copy = r#"
15260 ˇfn main() {
15261 println!("hello, world!");
15262 }
15263 "#
15264 .unindent();
15265
15266 cx.set_state(&working_copy);
15267 executor.run_until_parked();
15268
15269 cx.assert_state_with_diff(
15270 r#"
15271 + ˇfn main() {
15272 + println!("hello, world!");
15273 + }
15274 "#
15275 .unindent(),
15276 );
15277 cx.assert_index_text(None);
15278
15279 cx.update_editor(|editor, window, cx| {
15280 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15281 });
15282 executor.run_until_parked();
15283 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15284 cx.assert_state_with_diff(
15285 r#"
15286 + ˇfn main() {
15287 + println!("hello, world!");
15288 + }
15289 "#
15290 .unindent(),
15291 );
15292
15293 cx.update_editor(|editor, window, cx| {
15294 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15295 });
15296 executor.run_until_parked();
15297 cx.assert_index_text(None);
15298}
15299
15300async fn setup_indent_guides_editor(
15301 text: &str,
15302 cx: &mut TestAppContext,
15303) -> (BufferId, EditorTestContext) {
15304 init_test(cx, |_| {});
15305
15306 let mut cx = EditorTestContext::new(cx).await;
15307
15308 let buffer_id = cx.update_editor(|editor, window, cx| {
15309 editor.set_text(text, window, cx);
15310 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15311
15312 buffer_ids[0]
15313 });
15314
15315 (buffer_id, cx)
15316}
15317
15318fn assert_indent_guides(
15319 range: Range<u32>,
15320 expected: Vec<IndentGuide>,
15321 active_indices: Option<Vec<usize>>,
15322 cx: &mut EditorTestContext,
15323) {
15324 let indent_guides = cx.update_editor(|editor, window, cx| {
15325 let snapshot = editor.snapshot(window, cx).display_snapshot;
15326 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15327 editor,
15328 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15329 true,
15330 &snapshot,
15331 cx,
15332 );
15333
15334 indent_guides.sort_by(|a, b| {
15335 a.depth.cmp(&b.depth).then(
15336 a.start_row
15337 .cmp(&b.start_row)
15338 .then(a.end_row.cmp(&b.end_row)),
15339 )
15340 });
15341 indent_guides
15342 });
15343
15344 if let Some(expected) = active_indices {
15345 let active_indices = cx.update_editor(|editor, window, cx| {
15346 let snapshot = editor.snapshot(window, cx).display_snapshot;
15347 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15348 });
15349
15350 assert_eq!(
15351 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15352 expected,
15353 "Active indent guide indices do not match"
15354 );
15355 }
15356
15357 assert_eq!(indent_guides, expected, "Indent guides do not match");
15358}
15359
15360fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15361 IndentGuide {
15362 buffer_id,
15363 start_row: MultiBufferRow(start_row),
15364 end_row: MultiBufferRow(end_row),
15365 depth,
15366 tab_size: 4,
15367 settings: IndentGuideSettings {
15368 enabled: true,
15369 line_width: 1,
15370 active_line_width: 1,
15371 ..Default::default()
15372 },
15373 }
15374}
15375
15376#[gpui::test]
15377async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15378 let (buffer_id, mut cx) = setup_indent_guides_editor(
15379 &"
15380 fn main() {
15381 let a = 1;
15382 }"
15383 .unindent(),
15384 cx,
15385 )
15386 .await;
15387
15388 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15389}
15390
15391#[gpui::test]
15392async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15393 let (buffer_id, mut cx) = setup_indent_guides_editor(
15394 &"
15395 fn main() {
15396 let a = 1;
15397 let b = 2;
15398 }"
15399 .unindent(),
15400 cx,
15401 )
15402 .await;
15403
15404 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15405}
15406
15407#[gpui::test]
15408async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15409 let (buffer_id, mut cx) = setup_indent_guides_editor(
15410 &"
15411 fn main() {
15412 let a = 1;
15413 if a == 3 {
15414 let b = 2;
15415 } else {
15416 let c = 3;
15417 }
15418 }"
15419 .unindent(),
15420 cx,
15421 )
15422 .await;
15423
15424 assert_indent_guides(
15425 0..8,
15426 vec![
15427 indent_guide(buffer_id, 1, 6, 0),
15428 indent_guide(buffer_id, 3, 3, 1),
15429 indent_guide(buffer_id, 5, 5, 1),
15430 ],
15431 None,
15432 &mut cx,
15433 );
15434}
15435
15436#[gpui::test]
15437async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15438 let (buffer_id, mut cx) = setup_indent_guides_editor(
15439 &"
15440 fn main() {
15441 let a = 1;
15442 let b = 2;
15443 let c = 3;
15444 }"
15445 .unindent(),
15446 cx,
15447 )
15448 .await;
15449
15450 assert_indent_guides(
15451 0..5,
15452 vec![
15453 indent_guide(buffer_id, 1, 3, 0),
15454 indent_guide(buffer_id, 2, 2, 1),
15455 ],
15456 None,
15457 &mut cx,
15458 );
15459}
15460
15461#[gpui::test]
15462async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15463 let (buffer_id, mut cx) = setup_indent_guides_editor(
15464 &"
15465 fn main() {
15466 let a = 1;
15467
15468 let c = 3;
15469 }"
15470 .unindent(),
15471 cx,
15472 )
15473 .await;
15474
15475 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15476}
15477
15478#[gpui::test]
15479async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15480 let (buffer_id, mut cx) = setup_indent_guides_editor(
15481 &"
15482 fn main() {
15483 let a = 1;
15484
15485 let c = 3;
15486
15487 if a == 3 {
15488 let b = 2;
15489 } else {
15490 let c = 3;
15491 }
15492 }"
15493 .unindent(),
15494 cx,
15495 )
15496 .await;
15497
15498 assert_indent_guides(
15499 0..11,
15500 vec![
15501 indent_guide(buffer_id, 1, 9, 0),
15502 indent_guide(buffer_id, 6, 6, 1),
15503 indent_guide(buffer_id, 8, 8, 1),
15504 ],
15505 None,
15506 &mut cx,
15507 );
15508}
15509
15510#[gpui::test]
15511async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15512 let (buffer_id, mut cx) = setup_indent_guides_editor(
15513 &"
15514 fn main() {
15515 let a = 1;
15516
15517 let c = 3;
15518
15519 if a == 3 {
15520 let b = 2;
15521 } else {
15522 let c = 3;
15523 }
15524 }"
15525 .unindent(),
15526 cx,
15527 )
15528 .await;
15529
15530 assert_indent_guides(
15531 1..11,
15532 vec![
15533 indent_guide(buffer_id, 1, 9, 0),
15534 indent_guide(buffer_id, 6, 6, 1),
15535 indent_guide(buffer_id, 8, 8, 1),
15536 ],
15537 None,
15538 &mut cx,
15539 );
15540}
15541
15542#[gpui::test]
15543async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15544 let (buffer_id, mut cx) = setup_indent_guides_editor(
15545 &"
15546 fn main() {
15547 let a = 1;
15548
15549 let c = 3;
15550
15551 if a == 3 {
15552 let b = 2;
15553 } else {
15554 let c = 3;
15555 }
15556 }"
15557 .unindent(),
15558 cx,
15559 )
15560 .await;
15561
15562 assert_indent_guides(
15563 1..10,
15564 vec![
15565 indent_guide(buffer_id, 1, 9, 0),
15566 indent_guide(buffer_id, 6, 6, 1),
15567 indent_guide(buffer_id, 8, 8, 1),
15568 ],
15569 None,
15570 &mut cx,
15571 );
15572}
15573
15574#[gpui::test]
15575async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15576 let (buffer_id, mut cx) = setup_indent_guides_editor(
15577 &"
15578 block1
15579 block2
15580 block3
15581 block4
15582 block2
15583 block1
15584 block1"
15585 .unindent(),
15586 cx,
15587 )
15588 .await;
15589
15590 assert_indent_guides(
15591 1..10,
15592 vec![
15593 indent_guide(buffer_id, 1, 4, 0),
15594 indent_guide(buffer_id, 2, 3, 1),
15595 indent_guide(buffer_id, 3, 3, 2),
15596 ],
15597 None,
15598 &mut cx,
15599 );
15600}
15601
15602#[gpui::test]
15603async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15604 let (buffer_id, mut cx) = setup_indent_guides_editor(
15605 &"
15606 block1
15607 block2
15608 block3
15609
15610 block1
15611 block1"
15612 .unindent(),
15613 cx,
15614 )
15615 .await;
15616
15617 assert_indent_guides(
15618 0..6,
15619 vec![
15620 indent_guide(buffer_id, 1, 2, 0),
15621 indent_guide(buffer_id, 2, 2, 1),
15622 ],
15623 None,
15624 &mut cx,
15625 );
15626}
15627
15628#[gpui::test]
15629async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15630 let (buffer_id, mut cx) = setup_indent_guides_editor(
15631 &"
15632 block1
15633
15634
15635
15636 block2
15637 "
15638 .unindent(),
15639 cx,
15640 )
15641 .await;
15642
15643 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15644}
15645
15646#[gpui::test]
15647async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15648 let (buffer_id, mut cx) = setup_indent_guides_editor(
15649 &"
15650 def a:
15651 \tb = 3
15652 \tif True:
15653 \t\tc = 4
15654 \t\td = 5
15655 \tprint(b)
15656 "
15657 .unindent(),
15658 cx,
15659 )
15660 .await;
15661
15662 assert_indent_guides(
15663 0..6,
15664 vec![
15665 indent_guide(buffer_id, 1, 6, 0),
15666 indent_guide(buffer_id, 3, 4, 1),
15667 ],
15668 None,
15669 &mut cx,
15670 );
15671}
15672
15673#[gpui::test]
15674async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15675 let (buffer_id, mut cx) = setup_indent_guides_editor(
15676 &"
15677 fn main() {
15678 let a = 1;
15679 }"
15680 .unindent(),
15681 cx,
15682 )
15683 .await;
15684
15685 cx.update_editor(|editor, window, cx| {
15686 editor.change_selections(None, window, cx, |s| {
15687 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15688 });
15689 });
15690
15691 assert_indent_guides(
15692 0..3,
15693 vec![indent_guide(buffer_id, 1, 1, 0)],
15694 Some(vec![0]),
15695 &mut cx,
15696 );
15697}
15698
15699#[gpui::test]
15700async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15701 let (buffer_id, mut cx) = setup_indent_guides_editor(
15702 &"
15703 fn main() {
15704 if 1 == 2 {
15705 let a = 1;
15706 }
15707 }"
15708 .unindent(),
15709 cx,
15710 )
15711 .await;
15712
15713 cx.update_editor(|editor, window, cx| {
15714 editor.change_selections(None, window, cx, |s| {
15715 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15716 });
15717 });
15718
15719 assert_indent_guides(
15720 0..4,
15721 vec![
15722 indent_guide(buffer_id, 1, 3, 0),
15723 indent_guide(buffer_id, 2, 2, 1),
15724 ],
15725 Some(vec![1]),
15726 &mut cx,
15727 );
15728
15729 cx.update_editor(|editor, window, cx| {
15730 editor.change_selections(None, window, cx, |s| {
15731 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15732 });
15733 });
15734
15735 assert_indent_guides(
15736 0..4,
15737 vec![
15738 indent_guide(buffer_id, 1, 3, 0),
15739 indent_guide(buffer_id, 2, 2, 1),
15740 ],
15741 Some(vec![1]),
15742 &mut cx,
15743 );
15744
15745 cx.update_editor(|editor, window, cx| {
15746 editor.change_selections(None, window, cx, |s| {
15747 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15748 });
15749 });
15750
15751 assert_indent_guides(
15752 0..4,
15753 vec![
15754 indent_guide(buffer_id, 1, 3, 0),
15755 indent_guide(buffer_id, 2, 2, 1),
15756 ],
15757 Some(vec![0]),
15758 &mut cx,
15759 );
15760}
15761
15762#[gpui::test]
15763async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15764 let (buffer_id, mut cx) = setup_indent_guides_editor(
15765 &"
15766 fn main() {
15767 let a = 1;
15768
15769 let b = 2;
15770 }"
15771 .unindent(),
15772 cx,
15773 )
15774 .await;
15775
15776 cx.update_editor(|editor, window, cx| {
15777 editor.change_selections(None, window, cx, |s| {
15778 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15779 });
15780 });
15781
15782 assert_indent_guides(
15783 0..5,
15784 vec![indent_guide(buffer_id, 1, 3, 0)],
15785 Some(vec![0]),
15786 &mut cx,
15787 );
15788}
15789
15790#[gpui::test]
15791async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15792 let (buffer_id, mut cx) = setup_indent_guides_editor(
15793 &"
15794 def m:
15795 a = 1
15796 pass"
15797 .unindent(),
15798 cx,
15799 )
15800 .await;
15801
15802 cx.update_editor(|editor, window, cx| {
15803 editor.change_selections(None, window, cx, |s| {
15804 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15805 });
15806 });
15807
15808 assert_indent_guides(
15809 0..3,
15810 vec![indent_guide(buffer_id, 1, 2, 0)],
15811 Some(vec![0]),
15812 &mut cx,
15813 );
15814}
15815
15816#[gpui::test]
15817async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15818 init_test(cx, |_| {});
15819 let mut cx = EditorTestContext::new(cx).await;
15820 let text = indoc! {
15821 "
15822 impl A {
15823 fn b() {
15824 0;
15825 3;
15826 5;
15827 6;
15828 7;
15829 }
15830 }
15831 "
15832 };
15833 let base_text = indoc! {
15834 "
15835 impl A {
15836 fn b() {
15837 0;
15838 1;
15839 2;
15840 3;
15841 4;
15842 }
15843 fn c() {
15844 5;
15845 6;
15846 7;
15847 }
15848 }
15849 "
15850 };
15851
15852 cx.update_editor(|editor, window, cx| {
15853 editor.set_text(text, window, cx);
15854
15855 editor.buffer().update(cx, |multibuffer, cx| {
15856 let buffer = multibuffer.as_singleton().unwrap();
15857 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15858
15859 multibuffer.set_all_diff_hunks_expanded(cx);
15860 multibuffer.add_diff(diff, cx);
15861
15862 buffer.read(cx).remote_id()
15863 })
15864 });
15865 cx.run_until_parked();
15866
15867 cx.assert_state_with_diff(
15868 indoc! { "
15869 impl A {
15870 fn b() {
15871 0;
15872 - 1;
15873 - 2;
15874 3;
15875 - 4;
15876 - }
15877 - fn c() {
15878 5;
15879 6;
15880 7;
15881 }
15882 }
15883 ˇ"
15884 }
15885 .to_string(),
15886 );
15887
15888 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15889 editor
15890 .snapshot(window, cx)
15891 .buffer_snapshot
15892 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15893 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15894 .collect::<Vec<_>>()
15895 });
15896 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15897 assert_eq!(
15898 actual_guides,
15899 vec![
15900 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15901 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15902 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15903 ]
15904 );
15905}
15906
15907#[gpui::test]
15908async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15909 init_test(cx, |_| {});
15910 let mut cx = EditorTestContext::new(cx).await;
15911
15912 let diff_base = r#"
15913 a
15914 b
15915 c
15916 "#
15917 .unindent();
15918
15919 cx.set_state(
15920 &r#"
15921 ˇA
15922 b
15923 C
15924 "#
15925 .unindent(),
15926 );
15927 cx.set_head_text(&diff_base);
15928 cx.update_editor(|editor, window, cx| {
15929 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15930 });
15931 executor.run_until_parked();
15932
15933 let both_hunks_expanded = r#"
15934 - a
15935 + ˇA
15936 b
15937 - c
15938 + C
15939 "#
15940 .unindent();
15941
15942 cx.assert_state_with_diff(both_hunks_expanded.clone());
15943
15944 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15945 let snapshot = editor.snapshot(window, cx);
15946 let hunks = editor
15947 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15948 .collect::<Vec<_>>();
15949 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15950 let buffer_id = hunks[0].buffer_id;
15951 hunks
15952 .into_iter()
15953 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15954 .collect::<Vec<_>>()
15955 });
15956 assert_eq!(hunk_ranges.len(), 2);
15957
15958 cx.update_editor(|editor, _, cx| {
15959 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15960 });
15961 executor.run_until_parked();
15962
15963 let second_hunk_expanded = r#"
15964 ˇA
15965 b
15966 - c
15967 + C
15968 "#
15969 .unindent();
15970
15971 cx.assert_state_with_diff(second_hunk_expanded);
15972
15973 cx.update_editor(|editor, _, cx| {
15974 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15975 });
15976 executor.run_until_parked();
15977
15978 cx.assert_state_with_diff(both_hunks_expanded.clone());
15979
15980 cx.update_editor(|editor, _, cx| {
15981 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15982 });
15983 executor.run_until_parked();
15984
15985 let first_hunk_expanded = r#"
15986 - a
15987 + ˇA
15988 b
15989 C
15990 "#
15991 .unindent();
15992
15993 cx.assert_state_with_diff(first_hunk_expanded);
15994
15995 cx.update_editor(|editor, _, cx| {
15996 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15997 });
15998 executor.run_until_parked();
15999
16000 cx.assert_state_with_diff(both_hunks_expanded);
16001
16002 cx.set_state(
16003 &r#"
16004 ˇA
16005 b
16006 "#
16007 .unindent(),
16008 );
16009 cx.run_until_parked();
16010
16011 // TODO this cursor position seems bad
16012 cx.assert_state_with_diff(
16013 r#"
16014 - ˇa
16015 + A
16016 b
16017 "#
16018 .unindent(),
16019 );
16020
16021 cx.update_editor(|editor, window, cx| {
16022 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16023 });
16024
16025 cx.assert_state_with_diff(
16026 r#"
16027 - ˇa
16028 + A
16029 b
16030 - c
16031 "#
16032 .unindent(),
16033 );
16034
16035 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16036 let snapshot = editor.snapshot(window, cx);
16037 let hunks = editor
16038 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16039 .collect::<Vec<_>>();
16040 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16041 let buffer_id = hunks[0].buffer_id;
16042 hunks
16043 .into_iter()
16044 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16045 .collect::<Vec<_>>()
16046 });
16047 assert_eq!(hunk_ranges.len(), 2);
16048
16049 cx.update_editor(|editor, _, cx| {
16050 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16051 });
16052 executor.run_until_parked();
16053
16054 cx.assert_state_with_diff(
16055 r#"
16056 - ˇa
16057 + A
16058 b
16059 "#
16060 .unindent(),
16061 );
16062}
16063
16064#[gpui::test]
16065async fn test_toggle_deletion_hunk_at_start_of_file(
16066 executor: BackgroundExecutor,
16067 cx: &mut TestAppContext,
16068) {
16069 init_test(cx, |_| {});
16070 let mut cx = EditorTestContext::new(cx).await;
16071
16072 let diff_base = r#"
16073 a
16074 b
16075 c
16076 "#
16077 .unindent();
16078
16079 cx.set_state(
16080 &r#"
16081 ˇb
16082 c
16083 "#
16084 .unindent(),
16085 );
16086 cx.set_head_text(&diff_base);
16087 cx.update_editor(|editor, window, cx| {
16088 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16089 });
16090 executor.run_until_parked();
16091
16092 let hunk_expanded = r#"
16093 - a
16094 ˇb
16095 c
16096 "#
16097 .unindent();
16098
16099 cx.assert_state_with_diff(hunk_expanded.clone());
16100
16101 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16102 let snapshot = editor.snapshot(window, cx);
16103 let hunks = editor
16104 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16105 .collect::<Vec<_>>();
16106 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16107 let buffer_id = hunks[0].buffer_id;
16108 hunks
16109 .into_iter()
16110 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16111 .collect::<Vec<_>>()
16112 });
16113 assert_eq!(hunk_ranges.len(), 1);
16114
16115 cx.update_editor(|editor, _, cx| {
16116 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16117 });
16118 executor.run_until_parked();
16119
16120 let hunk_collapsed = r#"
16121 ˇb
16122 c
16123 "#
16124 .unindent();
16125
16126 cx.assert_state_with_diff(hunk_collapsed);
16127
16128 cx.update_editor(|editor, _, cx| {
16129 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16130 });
16131 executor.run_until_parked();
16132
16133 cx.assert_state_with_diff(hunk_expanded.clone());
16134}
16135
16136#[gpui::test]
16137async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16138 init_test(cx, |_| {});
16139
16140 let fs = FakeFs::new(cx.executor());
16141 fs.insert_tree(
16142 path!("/test"),
16143 json!({
16144 ".git": {},
16145 "file-1": "ONE\n",
16146 "file-2": "TWO\n",
16147 "file-3": "THREE\n",
16148 }),
16149 )
16150 .await;
16151
16152 fs.set_head_for_repo(
16153 path!("/test/.git").as_ref(),
16154 &[
16155 ("file-1".into(), "one\n".into()),
16156 ("file-2".into(), "two\n".into()),
16157 ("file-3".into(), "three\n".into()),
16158 ],
16159 );
16160
16161 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16162 let mut buffers = vec![];
16163 for i in 1..=3 {
16164 let buffer = project
16165 .update(cx, |project, cx| {
16166 let path = format!(path!("/test/file-{}"), i);
16167 project.open_local_buffer(path, cx)
16168 })
16169 .await
16170 .unwrap();
16171 buffers.push(buffer);
16172 }
16173
16174 let multibuffer = cx.new(|cx| {
16175 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16176 multibuffer.set_all_diff_hunks_expanded(cx);
16177 for buffer in &buffers {
16178 let snapshot = buffer.read(cx).snapshot();
16179 multibuffer.set_excerpts_for_path(
16180 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16181 buffer.clone(),
16182 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16183 DEFAULT_MULTIBUFFER_CONTEXT,
16184 cx,
16185 );
16186 }
16187 multibuffer
16188 });
16189
16190 let editor = cx.add_window(|window, cx| {
16191 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16192 });
16193 cx.run_until_parked();
16194
16195 let snapshot = editor
16196 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16197 .unwrap();
16198 let hunks = snapshot
16199 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16200 .map(|hunk| match hunk {
16201 DisplayDiffHunk::Unfolded {
16202 display_row_range, ..
16203 } => display_row_range,
16204 DisplayDiffHunk::Folded { .. } => unreachable!(),
16205 })
16206 .collect::<Vec<_>>();
16207 assert_eq!(
16208 hunks,
16209 [
16210 DisplayRow(2)..DisplayRow(4),
16211 DisplayRow(7)..DisplayRow(9),
16212 DisplayRow(12)..DisplayRow(14),
16213 ]
16214 );
16215}
16216
16217#[gpui::test]
16218async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16219 init_test(cx, |_| {});
16220
16221 let mut cx = EditorTestContext::new(cx).await;
16222 cx.set_head_text(indoc! { "
16223 one
16224 two
16225 three
16226 four
16227 five
16228 "
16229 });
16230 cx.set_index_text(indoc! { "
16231 one
16232 two
16233 three
16234 four
16235 five
16236 "
16237 });
16238 cx.set_state(indoc! {"
16239 one
16240 TWO
16241 ˇTHREE
16242 FOUR
16243 five
16244 "});
16245 cx.run_until_parked();
16246 cx.update_editor(|editor, window, cx| {
16247 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16248 });
16249 cx.run_until_parked();
16250 cx.assert_index_text(Some(indoc! {"
16251 one
16252 TWO
16253 THREE
16254 FOUR
16255 five
16256 "}));
16257 cx.set_state(indoc! { "
16258 one
16259 TWO
16260 ˇTHREE-HUNDRED
16261 FOUR
16262 five
16263 "});
16264 cx.run_until_parked();
16265 cx.update_editor(|editor, window, cx| {
16266 let snapshot = editor.snapshot(window, cx);
16267 let hunks = editor
16268 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16269 .collect::<Vec<_>>();
16270 assert_eq!(hunks.len(), 1);
16271 assert_eq!(
16272 hunks[0].status(),
16273 DiffHunkStatus {
16274 kind: DiffHunkStatusKind::Modified,
16275 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16276 }
16277 );
16278
16279 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16280 });
16281 cx.run_until_parked();
16282 cx.assert_index_text(Some(indoc! {"
16283 one
16284 TWO
16285 THREE-HUNDRED
16286 FOUR
16287 five
16288 "}));
16289}
16290
16291#[gpui::test]
16292fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16293 init_test(cx, |_| {});
16294
16295 let editor = cx.add_window(|window, cx| {
16296 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16297 build_editor(buffer, window, cx)
16298 });
16299
16300 let render_args = Arc::new(Mutex::new(None));
16301 let snapshot = editor
16302 .update(cx, |editor, window, cx| {
16303 let snapshot = editor.buffer().read(cx).snapshot(cx);
16304 let range =
16305 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16306
16307 struct RenderArgs {
16308 row: MultiBufferRow,
16309 folded: bool,
16310 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16311 }
16312
16313 let crease = Crease::inline(
16314 range,
16315 FoldPlaceholder::test(),
16316 {
16317 let toggle_callback = render_args.clone();
16318 move |row, folded, callback, _window, _cx| {
16319 *toggle_callback.lock() = Some(RenderArgs {
16320 row,
16321 folded,
16322 callback,
16323 });
16324 div()
16325 }
16326 },
16327 |_row, _folded, _window, _cx| div(),
16328 );
16329
16330 editor.insert_creases(Some(crease), cx);
16331 let snapshot = editor.snapshot(window, cx);
16332 let _div = snapshot.render_crease_toggle(
16333 MultiBufferRow(1),
16334 false,
16335 cx.entity().clone(),
16336 window,
16337 cx,
16338 );
16339 snapshot
16340 })
16341 .unwrap();
16342
16343 let render_args = render_args.lock().take().unwrap();
16344 assert_eq!(render_args.row, MultiBufferRow(1));
16345 assert!(!render_args.folded);
16346 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16347
16348 cx.update_window(*editor, |_, window, cx| {
16349 (render_args.callback)(true, window, cx)
16350 })
16351 .unwrap();
16352 let snapshot = editor
16353 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16354 .unwrap();
16355 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16356
16357 cx.update_window(*editor, |_, window, cx| {
16358 (render_args.callback)(false, window, cx)
16359 })
16360 .unwrap();
16361 let snapshot = editor
16362 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16363 .unwrap();
16364 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16365}
16366
16367#[gpui::test]
16368async fn test_input_text(cx: &mut TestAppContext) {
16369 init_test(cx, |_| {});
16370 let mut cx = EditorTestContext::new(cx).await;
16371
16372 cx.set_state(
16373 &r#"ˇone
16374 two
16375
16376 three
16377 fourˇ
16378 five
16379
16380 siˇx"#
16381 .unindent(),
16382 );
16383
16384 cx.dispatch_action(HandleInput(String::new()));
16385 cx.assert_editor_state(
16386 &r#"ˇone
16387 two
16388
16389 three
16390 fourˇ
16391 five
16392
16393 siˇx"#
16394 .unindent(),
16395 );
16396
16397 cx.dispatch_action(HandleInput("AAAA".to_string()));
16398 cx.assert_editor_state(
16399 &r#"AAAAˇone
16400 two
16401
16402 three
16403 fourAAAAˇ
16404 five
16405
16406 siAAAAˇx"#
16407 .unindent(),
16408 );
16409}
16410
16411#[gpui::test]
16412async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16413 init_test(cx, |_| {});
16414
16415 let mut cx = EditorTestContext::new(cx).await;
16416 cx.set_state(
16417 r#"let foo = 1;
16418let foo = 2;
16419let foo = 3;
16420let fooˇ = 4;
16421let foo = 5;
16422let foo = 6;
16423let foo = 7;
16424let foo = 8;
16425let foo = 9;
16426let foo = 10;
16427let foo = 11;
16428let foo = 12;
16429let foo = 13;
16430let foo = 14;
16431let foo = 15;"#,
16432 );
16433
16434 cx.update_editor(|e, window, cx| {
16435 assert_eq!(
16436 e.next_scroll_position,
16437 NextScrollCursorCenterTopBottom::Center,
16438 "Default next scroll direction is center",
16439 );
16440
16441 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16442 assert_eq!(
16443 e.next_scroll_position,
16444 NextScrollCursorCenterTopBottom::Top,
16445 "After center, next scroll direction should be top",
16446 );
16447
16448 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16449 assert_eq!(
16450 e.next_scroll_position,
16451 NextScrollCursorCenterTopBottom::Bottom,
16452 "After top, next scroll direction should be bottom",
16453 );
16454
16455 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16456 assert_eq!(
16457 e.next_scroll_position,
16458 NextScrollCursorCenterTopBottom::Center,
16459 "After bottom, scrolling should start over",
16460 );
16461
16462 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16463 assert_eq!(
16464 e.next_scroll_position,
16465 NextScrollCursorCenterTopBottom::Top,
16466 "Scrolling continues if retriggered fast enough"
16467 );
16468 });
16469
16470 cx.executor()
16471 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16472 cx.executor().run_until_parked();
16473 cx.update_editor(|e, _, _| {
16474 assert_eq!(
16475 e.next_scroll_position,
16476 NextScrollCursorCenterTopBottom::Center,
16477 "If scrolling is not triggered fast enough, it should reset"
16478 );
16479 });
16480}
16481
16482#[gpui::test]
16483async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16484 init_test(cx, |_| {});
16485 let mut cx = EditorLspTestContext::new_rust(
16486 lsp::ServerCapabilities {
16487 definition_provider: Some(lsp::OneOf::Left(true)),
16488 references_provider: Some(lsp::OneOf::Left(true)),
16489 ..lsp::ServerCapabilities::default()
16490 },
16491 cx,
16492 )
16493 .await;
16494
16495 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16496 let go_to_definition = cx
16497 .lsp
16498 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16499 move |params, _| async move {
16500 if empty_go_to_definition {
16501 Ok(None)
16502 } else {
16503 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16504 uri: params.text_document_position_params.text_document.uri,
16505 range: lsp::Range::new(
16506 lsp::Position::new(4, 3),
16507 lsp::Position::new(4, 6),
16508 ),
16509 })))
16510 }
16511 },
16512 );
16513 let references = cx
16514 .lsp
16515 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16516 Ok(Some(vec![lsp::Location {
16517 uri: params.text_document_position.text_document.uri,
16518 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16519 }]))
16520 });
16521 (go_to_definition, references)
16522 };
16523
16524 cx.set_state(
16525 &r#"fn one() {
16526 let mut a = ˇtwo();
16527 }
16528
16529 fn two() {}"#
16530 .unindent(),
16531 );
16532 set_up_lsp_handlers(false, &mut cx);
16533 let navigated = cx
16534 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16535 .await
16536 .expect("Failed to navigate to definition");
16537 assert_eq!(
16538 navigated,
16539 Navigated::Yes,
16540 "Should have navigated to definition from the GetDefinition response"
16541 );
16542 cx.assert_editor_state(
16543 &r#"fn one() {
16544 let mut a = two();
16545 }
16546
16547 fn «twoˇ»() {}"#
16548 .unindent(),
16549 );
16550
16551 let editors = cx.update_workspace(|workspace, _, cx| {
16552 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16553 });
16554 cx.update_editor(|_, _, test_editor_cx| {
16555 assert_eq!(
16556 editors.len(),
16557 1,
16558 "Initially, only one, test, editor should be open in the workspace"
16559 );
16560 assert_eq!(
16561 test_editor_cx.entity(),
16562 editors.last().expect("Asserted len is 1").clone()
16563 );
16564 });
16565
16566 set_up_lsp_handlers(true, &mut cx);
16567 let navigated = cx
16568 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16569 .await
16570 .expect("Failed to navigate to lookup references");
16571 assert_eq!(
16572 navigated,
16573 Navigated::Yes,
16574 "Should have navigated to references as a fallback after empty GoToDefinition response"
16575 );
16576 // We should not change the selections in the existing file,
16577 // if opening another milti buffer with the references
16578 cx.assert_editor_state(
16579 &r#"fn one() {
16580 let mut a = two();
16581 }
16582
16583 fn «twoˇ»() {}"#
16584 .unindent(),
16585 );
16586 let editors = cx.update_workspace(|workspace, _, cx| {
16587 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16588 });
16589 cx.update_editor(|_, _, test_editor_cx| {
16590 assert_eq!(
16591 editors.len(),
16592 2,
16593 "After falling back to references search, we open a new editor with the results"
16594 );
16595 let references_fallback_text = editors
16596 .into_iter()
16597 .find(|new_editor| *new_editor != test_editor_cx.entity())
16598 .expect("Should have one non-test editor now")
16599 .read(test_editor_cx)
16600 .text(test_editor_cx);
16601 assert_eq!(
16602 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16603 "Should use the range from the references response and not the GoToDefinition one"
16604 );
16605 });
16606}
16607
16608#[gpui::test]
16609async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16610 init_test(cx, |_| {});
16611 cx.update(|cx| {
16612 let mut editor_settings = EditorSettings::get_global(cx).clone();
16613 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16614 EditorSettings::override_global(editor_settings, cx);
16615 });
16616 let mut cx = EditorLspTestContext::new_rust(
16617 lsp::ServerCapabilities {
16618 definition_provider: Some(lsp::OneOf::Left(true)),
16619 references_provider: Some(lsp::OneOf::Left(true)),
16620 ..lsp::ServerCapabilities::default()
16621 },
16622 cx,
16623 )
16624 .await;
16625 let original_state = r#"fn one() {
16626 let mut a = ˇtwo();
16627 }
16628
16629 fn two() {}"#
16630 .unindent();
16631 cx.set_state(&original_state);
16632
16633 let mut go_to_definition = cx
16634 .lsp
16635 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16636 move |_, _| async move { Ok(None) },
16637 );
16638 let _references = cx
16639 .lsp
16640 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16641 panic!("Should not call for references with no go to definition fallback")
16642 });
16643
16644 let navigated = cx
16645 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16646 .await
16647 .expect("Failed to navigate to lookup references");
16648 go_to_definition
16649 .next()
16650 .await
16651 .expect("Should have called the go_to_definition handler");
16652
16653 assert_eq!(
16654 navigated,
16655 Navigated::No,
16656 "Should have navigated to references as a fallback after empty GoToDefinition response"
16657 );
16658 cx.assert_editor_state(&original_state);
16659 let editors = cx.update_workspace(|workspace, _, cx| {
16660 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16661 });
16662 cx.update_editor(|_, _, _| {
16663 assert_eq!(
16664 editors.len(),
16665 1,
16666 "After unsuccessful fallback, no other editor should have been opened"
16667 );
16668 });
16669}
16670
16671#[gpui::test]
16672async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16673 init_test(cx, |_| {});
16674
16675 let language = Arc::new(Language::new(
16676 LanguageConfig::default(),
16677 Some(tree_sitter_rust::LANGUAGE.into()),
16678 ));
16679
16680 let text = r#"
16681 #[cfg(test)]
16682 mod tests() {
16683 #[test]
16684 fn runnable_1() {
16685 let a = 1;
16686 }
16687
16688 #[test]
16689 fn runnable_2() {
16690 let a = 1;
16691 let b = 2;
16692 }
16693 }
16694 "#
16695 .unindent();
16696
16697 let fs = FakeFs::new(cx.executor());
16698 fs.insert_file("/file.rs", Default::default()).await;
16699
16700 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16701 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16702 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16703 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16704 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16705
16706 let editor = cx.new_window_entity(|window, cx| {
16707 Editor::new(
16708 EditorMode::Full,
16709 multi_buffer,
16710 Some(project.clone()),
16711 window,
16712 cx,
16713 )
16714 });
16715
16716 editor.update_in(cx, |editor, window, cx| {
16717 let snapshot = editor.buffer().read(cx).snapshot(cx);
16718 editor.tasks.insert(
16719 (buffer.read(cx).remote_id(), 3),
16720 RunnableTasks {
16721 templates: vec![],
16722 offset: snapshot.anchor_before(43),
16723 column: 0,
16724 extra_variables: HashMap::default(),
16725 context_range: BufferOffset(43)..BufferOffset(85),
16726 },
16727 );
16728 editor.tasks.insert(
16729 (buffer.read(cx).remote_id(), 8),
16730 RunnableTasks {
16731 templates: vec![],
16732 offset: snapshot.anchor_before(86),
16733 column: 0,
16734 extra_variables: HashMap::default(),
16735 context_range: BufferOffset(86)..BufferOffset(191),
16736 },
16737 );
16738
16739 // Test finding task when cursor is inside function body
16740 editor.change_selections(None, window, cx, |s| {
16741 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16742 });
16743 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16744 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16745
16746 // Test finding task when cursor is on function name
16747 editor.change_selections(None, window, cx, |s| {
16748 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16749 });
16750 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16751 assert_eq!(row, 8, "Should find task when cursor is on function name");
16752 });
16753}
16754
16755#[gpui::test]
16756async fn test_folding_buffers(cx: &mut TestAppContext) {
16757 init_test(cx, |_| {});
16758
16759 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16760 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16761 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16762
16763 let fs = FakeFs::new(cx.executor());
16764 fs.insert_tree(
16765 path!("/a"),
16766 json!({
16767 "first.rs": sample_text_1,
16768 "second.rs": sample_text_2,
16769 "third.rs": sample_text_3,
16770 }),
16771 )
16772 .await;
16773 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16774 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16775 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16776 let worktree = project.update(cx, |project, cx| {
16777 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16778 assert_eq!(worktrees.len(), 1);
16779 worktrees.pop().unwrap()
16780 });
16781 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16782
16783 let buffer_1 = project
16784 .update(cx, |project, cx| {
16785 project.open_buffer((worktree_id, "first.rs"), cx)
16786 })
16787 .await
16788 .unwrap();
16789 let buffer_2 = project
16790 .update(cx, |project, cx| {
16791 project.open_buffer((worktree_id, "second.rs"), cx)
16792 })
16793 .await
16794 .unwrap();
16795 let buffer_3 = project
16796 .update(cx, |project, cx| {
16797 project.open_buffer((worktree_id, "third.rs"), cx)
16798 })
16799 .await
16800 .unwrap();
16801
16802 let multi_buffer = cx.new(|cx| {
16803 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16804 multi_buffer.push_excerpts(
16805 buffer_1.clone(),
16806 [
16807 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16808 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16809 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16810 ],
16811 cx,
16812 );
16813 multi_buffer.push_excerpts(
16814 buffer_2.clone(),
16815 [
16816 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16817 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16818 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16819 ],
16820 cx,
16821 );
16822 multi_buffer.push_excerpts(
16823 buffer_3.clone(),
16824 [
16825 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16826 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16827 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16828 ],
16829 cx,
16830 );
16831 multi_buffer
16832 });
16833 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16834 Editor::new(
16835 EditorMode::Full,
16836 multi_buffer.clone(),
16837 Some(project.clone()),
16838 window,
16839 cx,
16840 )
16841 });
16842
16843 assert_eq!(
16844 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16845 "\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",
16846 );
16847
16848 multi_buffer_editor.update(cx, |editor, cx| {
16849 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16850 });
16851 assert_eq!(
16852 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16853 "\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",
16854 "After folding the first buffer, its text should not be displayed"
16855 );
16856
16857 multi_buffer_editor.update(cx, |editor, cx| {
16858 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16859 });
16860 assert_eq!(
16861 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16862 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16863 "After folding the second buffer, its text should not be displayed"
16864 );
16865
16866 multi_buffer_editor.update(cx, |editor, cx| {
16867 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16868 });
16869 assert_eq!(
16870 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16871 "\n\n\n\n\n",
16872 "After folding the third buffer, its text should not be displayed"
16873 );
16874
16875 // Emulate selection inside the fold logic, that should work
16876 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16877 editor
16878 .snapshot(window, cx)
16879 .next_line_boundary(Point::new(0, 4));
16880 });
16881
16882 multi_buffer_editor.update(cx, |editor, cx| {
16883 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16884 });
16885 assert_eq!(
16886 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16887 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16888 "After unfolding the second buffer, its text should be displayed"
16889 );
16890
16891 // Typing inside of buffer 1 causes that buffer to be unfolded.
16892 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16893 assert_eq!(
16894 multi_buffer
16895 .read(cx)
16896 .snapshot(cx)
16897 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16898 .collect::<String>(),
16899 "bbbb"
16900 );
16901 editor.change_selections(None, window, cx, |selections| {
16902 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16903 });
16904 editor.handle_input("B", window, cx);
16905 });
16906
16907 assert_eq!(
16908 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16909 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16910 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16911 );
16912
16913 multi_buffer_editor.update(cx, |editor, cx| {
16914 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16915 });
16916 assert_eq!(
16917 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16918 "\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",
16919 "After unfolding the all buffers, all original text should be displayed"
16920 );
16921}
16922
16923#[gpui::test]
16924async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16925 init_test(cx, |_| {});
16926
16927 let sample_text_1 = "1111\n2222\n3333".to_string();
16928 let sample_text_2 = "4444\n5555\n6666".to_string();
16929 let sample_text_3 = "7777\n8888\n9999".to_string();
16930
16931 let fs = FakeFs::new(cx.executor());
16932 fs.insert_tree(
16933 path!("/a"),
16934 json!({
16935 "first.rs": sample_text_1,
16936 "second.rs": sample_text_2,
16937 "third.rs": sample_text_3,
16938 }),
16939 )
16940 .await;
16941 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16943 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16944 let worktree = project.update(cx, |project, cx| {
16945 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16946 assert_eq!(worktrees.len(), 1);
16947 worktrees.pop().unwrap()
16948 });
16949 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16950
16951 let buffer_1 = project
16952 .update(cx, |project, cx| {
16953 project.open_buffer((worktree_id, "first.rs"), cx)
16954 })
16955 .await
16956 .unwrap();
16957 let buffer_2 = project
16958 .update(cx, |project, cx| {
16959 project.open_buffer((worktree_id, "second.rs"), cx)
16960 })
16961 .await
16962 .unwrap();
16963 let buffer_3 = project
16964 .update(cx, |project, cx| {
16965 project.open_buffer((worktree_id, "third.rs"), cx)
16966 })
16967 .await
16968 .unwrap();
16969
16970 let multi_buffer = cx.new(|cx| {
16971 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16972 multi_buffer.push_excerpts(
16973 buffer_1.clone(),
16974 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16975 cx,
16976 );
16977 multi_buffer.push_excerpts(
16978 buffer_2.clone(),
16979 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16980 cx,
16981 );
16982 multi_buffer.push_excerpts(
16983 buffer_3.clone(),
16984 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16985 cx,
16986 );
16987 multi_buffer
16988 });
16989
16990 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16991 Editor::new(
16992 EditorMode::Full,
16993 multi_buffer,
16994 Some(project.clone()),
16995 window,
16996 cx,
16997 )
16998 });
16999
17000 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17001 assert_eq!(
17002 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17003 full_text,
17004 );
17005
17006 multi_buffer_editor.update(cx, |editor, cx| {
17007 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17008 });
17009 assert_eq!(
17010 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17011 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17012 "After folding the first buffer, its text should not be displayed"
17013 );
17014
17015 multi_buffer_editor.update(cx, |editor, cx| {
17016 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17017 });
17018
17019 assert_eq!(
17020 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17021 "\n\n\n\n\n\n7777\n8888\n9999",
17022 "After folding the second buffer, its text should not be displayed"
17023 );
17024
17025 multi_buffer_editor.update(cx, |editor, cx| {
17026 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17027 });
17028 assert_eq!(
17029 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17030 "\n\n\n\n\n",
17031 "After folding the third buffer, its text should not be displayed"
17032 );
17033
17034 multi_buffer_editor.update(cx, |editor, cx| {
17035 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17036 });
17037 assert_eq!(
17038 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17039 "\n\n\n\n4444\n5555\n6666\n\n",
17040 "After unfolding the second buffer, its text should be displayed"
17041 );
17042
17043 multi_buffer_editor.update(cx, |editor, cx| {
17044 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17045 });
17046 assert_eq!(
17047 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17048 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17049 "After unfolding the first buffer, its text should be displayed"
17050 );
17051
17052 multi_buffer_editor.update(cx, |editor, cx| {
17053 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17054 });
17055 assert_eq!(
17056 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17057 full_text,
17058 "After unfolding all buffers, all original text should be displayed"
17059 );
17060}
17061
17062#[gpui::test]
17063async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17064 init_test(cx, |_| {});
17065
17066 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17067
17068 let fs = FakeFs::new(cx.executor());
17069 fs.insert_tree(
17070 path!("/a"),
17071 json!({
17072 "main.rs": sample_text,
17073 }),
17074 )
17075 .await;
17076 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17077 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17078 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17079 let worktree = project.update(cx, |project, cx| {
17080 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17081 assert_eq!(worktrees.len(), 1);
17082 worktrees.pop().unwrap()
17083 });
17084 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17085
17086 let buffer_1 = project
17087 .update(cx, |project, cx| {
17088 project.open_buffer((worktree_id, "main.rs"), cx)
17089 })
17090 .await
17091 .unwrap();
17092
17093 let multi_buffer = cx.new(|cx| {
17094 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17095 multi_buffer.push_excerpts(
17096 buffer_1.clone(),
17097 [ExcerptRange::new(
17098 Point::new(0, 0)
17099 ..Point::new(
17100 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17101 0,
17102 ),
17103 )],
17104 cx,
17105 );
17106 multi_buffer
17107 });
17108 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17109 Editor::new(
17110 EditorMode::Full,
17111 multi_buffer,
17112 Some(project.clone()),
17113 window,
17114 cx,
17115 )
17116 });
17117
17118 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17119 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17120 enum TestHighlight {}
17121 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17122 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17123 editor.highlight_text::<TestHighlight>(
17124 vec![highlight_range.clone()],
17125 HighlightStyle::color(Hsla::green()),
17126 cx,
17127 );
17128 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17129 });
17130
17131 let full_text = format!("\n\n{sample_text}");
17132 assert_eq!(
17133 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17134 full_text,
17135 );
17136}
17137
17138#[gpui::test]
17139async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17140 init_test(cx, |_| {});
17141 cx.update(|cx| {
17142 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17143 "keymaps/default-linux.json",
17144 cx,
17145 )
17146 .unwrap();
17147 cx.bind_keys(default_key_bindings);
17148 });
17149
17150 let (editor, cx) = cx.add_window_view(|window, cx| {
17151 let multi_buffer = MultiBuffer::build_multi(
17152 [
17153 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17154 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17155 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17156 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17157 ],
17158 cx,
17159 );
17160 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17161
17162 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17163 // fold all but the second buffer, so that we test navigating between two
17164 // adjacent folded buffers, as well as folded buffers at the start and
17165 // end the multibuffer
17166 editor.fold_buffer(buffer_ids[0], cx);
17167 editor.fold_buffer(buffer_ids[2], cx);
17168 editor.fold_buffer(buffer_ids[3], cx);
17169
17170 editor
17171 });
17172 cx.simulate_resize(size(px(1000.), px(1000.)));
17173
17174 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17175 cx.assert_excerpts_with_selections(indoc! {"
17176 [EXCERPT]
17177 ˇ[FOLDED]
17178 [EXCERPT]
17179 a1
17180 b1
17181 [EXCERPT]
17182 [FOLDED]
17183 [EXCERPT]
17184 [FOLDED]
17185 "
17186 });
17187 cx.simulate_keystroke("down");
17188 cx.assert_excerpts_with_selections(indoc! {"
17189 [EXCERPT]
17190 [FOLDED]
17191 [EXCERPT]
17192 ˇa1
17193 b1
17194 [EXCERPT]
17195 [FOLDED]
17196 [EXCERPT]
17197 [FOLDED]
17198 "
17199 });
17200 cx.simulate_keystroke("down");
17201 cx.assert_excerpts_with_selections(indoc! {"
17202 [EXCERPT]
17203 [FOLDED]
17204 [EXCERPT]
17205 a1
17206 ˇb1
17207 [EXCERPT]
17208 [FOLDED]
17209 [EXCERPT]
17210 [FOLDED]
17211 "
17212 });
17213 cx.simulate_keystroke("down");
17214 cx.assert_excerpts_with_selections(indoc! {"
17215 [EXCERPT]
17216 [FOLDED]
17217 [EXCERPT]
17218 a1
17219 b1
17220 ˇ[EXCERPT]
17221 [FOLDED]
17222 [EXCERPT]
17223 [FOLDED]
17224 "
17225 });
17226 cx.simulate_keystroke("down");
17227 cx.assert_excerpts_with_selections(indoc! {"
17228 [EXCERPT]
17229 [FOLDED]
17230 [EXCERPT]
17231 a1
17232 b1
17233 [EXCERPT]
17234 ˇ[FOLDED]
17235 [EXCERPT]
17236 [FOLDED]
17237 "
17238 });
17239 for _ in 0..5 {
17240 cx.simulate_keystroke("down");
17241 cx.assert_excerpts_with_selections(indoc! {"
17242 [EXCERPT]
17243 [FOLDED]
17244 [EXCERPT]
17245 a1
17246 b1
17247 [EXCERPT]
17248 [FOLDED]
17249 [EXCERPT]
17250 ˇ[FOLDED]
17251 "
17252 });
17253 }
17254
17255 cx.simulate_keystroke("up");
17256 cx.assert_excerpts_with_selections(indoc! {"
17257 [EXCERPT]
17258 [FOLDED]
17259 [EXCERPT]
17260 a1
17261 b1
17262 [EXCERPT]
17263 ˇ[FOLDED]
17264 [EXCERPT]
17265 [FOLDED]
17266 "
17267 });
17268 cx.simulate_keystroke("up");
17269 cx.assert_excerpts_with_selections(indoc! {"
17270 [EXCERPT]
17271 [FOLDED]
17272 [EXCERPT]
17273 a1
17274 b1
17275 ˇ[EXCERPT]
17276 [FOLDED]
17277 [EXCERPT]
17278 [FOLDED]
17279 "
17280 });
17281 cx.simulate_keystroke("up");
17282 cx.assert_excerpts_with_selections(indoc! {"
17283 [EXCERPT]
17284 [FOLDED]
17285 [EXCERPT]
17286 a1
17287 ˇb1
17288 [EXCERPT]
17289 [FOLDED]
17290 [EXCERPT]
17291 [FOLDED]
17292 "
17293 });
17294 cx.simulate_keystroke("up");
17295 cx.assert_excerpts_with_selections(indoc! {"
17296 [EXCERPT]
17297 [FOLDED]
17298 [EXCERPT]
17299 ˇa1
17300 b1
17301 [EXCERPT]
17302 [FOLDED]
17303 [EXCERPT]
17304 [FOLDED]
17305 "
17306 });
17307 for _ in 0..5 {
17308 cx.simulate_keystroke("up");
17309 cx.assert_excerpts_with_selections(indoc! {"
17310 [EXCERPT]
17311 ˇ[FOLDED]
17312 [EXCERPT]
17313 a1
17314 b1
17315 [EXCERPT]
17316 [FOLDED]
17317 [EXCERPT]
17318 [FOLDED]
17319 "
17320 });
17321 }
17322}
17323
17324#[gpui::test]
17325async fn test_inline_completion_text(cx: &mut TestAppContext) {
17326 init_test(cx, |_| {});
17327
17328 // Simple insertion
17329 assert_highlighted_edits(
17330 "Hello, world!",
17331 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17332 true,
17333 cx,
17334 |highlighted_edits, cx| {
17335 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17336 assert_eq!(highlighted_edits.highlights.len(), 1);
17337 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17338 assert_eq!(
17339 highlighted_edits.highlights[0].1.background_color,
17340 Some(cx.theme().status().created_background)
17341 );
17342 },
17343 )
17344 .await;
17345
17346 // Replacement
17347 assert_highlighted_edits(
17348 "This is a test.",
17349 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17350 false,
17351 cx,
17352 |highlighted_edits, cx| {
17353 assert_eq!(highlighted_edits.text, "That is a test.");
17354 assert_eq!(highlighted_edits.highlights.len(), 1);
17355 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17356 assert_eq!(
17357 highlighted_edits.highlights[0].1.background_color,
17358 Some(cx.theme().status().created_background)
17359 );
17360 },
17361 )
17362 .await;
17363
17364 // Multiple edits
17365 assert_highlighted_edits(
17366 "Hello, world!",
17367 vec![
17368 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17369 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17370 ],
17371 false,
17372 cx,
17373 |highlighted_edits, cx| {
17374 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17375 assert_eq!(highlighted_edits.highlights.len(), 2);
17376 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17377 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17378 assert_eq!(
17379 highlighted_edits.highlights[0].1.background_color,
17380 Some(cx.theme().status().created_background)
17381 );
17382 assert_eq!(
17383 highlighted_edits.highlights[1].1.background_color,
17384 Some(cx.theme().status().created_background)
17385 );
17386 },
17387 )
17388 .await;
17389
17390 // Multiple lines with edits
17391 assert_highlighted_edits(
17392 "First line\nSecond line\nThird line\nFourth line",
17393 vec![
17394 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17395 (
17396 Point::new(2, 0)..Point::new(2, 10),
17397 "New third line".to_string(),
17398 ),
17399 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17400 ],
17401 false,
17402 cx,
17403 |highlighted_edits, cx| {
17404 assert_eq!(
17405 highlighted_edits.text,
17406 "Second modified\nNew third line\nFourth updated line"
17407 );
17408 assert_eq!(highlighted_edits.highlights.len(), 3);
17409 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17410 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17411 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17412 for highlight in &highlighted_edits.highlights {
17413 assert_eq!(
17414 highlight.1.background_color,
17415 Some(cx.theme().status().created_background)
17416 );
17417 }
17418 },
17419 )
17420 .await;
17421}
17422
17423#[gpui::test]
17424async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17425 init_test(cx, |_| {});
17426
17427 // Deletion
17428 assert_highlighted_edits(
17429 "Hello, world!",
17430 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17431 true,
17432 cx,
17433 |highlighted_edits, cx| {
17434 assert_eq!(highlighted_edits.text, "Hello, world!");
17435 assert_eq!(highlighted_edits.highlights.len(), 1);
17436 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17437 assert_eq!(
17438 highlighted_edits.highlights[0].1.background_color,
17439 Some(cx.theme().status().deleted_background)
17440 );
17441 },
17442 )
17443 .await;
17444
17445 // Insertion
17446 assert_highlighted_edits(
17447 "Hello, world!",
17448 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17449 true,
17450 cx,
17451 |highlighted_edits, cx| {
17452 assert_eq!(highlighted_edits.highlights.len(), 1);
17453 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17454 assert_eq!(
17455 highlighted_edits.highlights[0].1.background_color,
17456 Some(cx.theme().status().created_background)
17457 );
17458 },
17459 )
17460 .await;
17461}
17462
17463async fn assert_highlighted_edits(
17464 text: &str,
17465 edits: Vec<(Range<Point>, String)>,
17466 include_deletions: bool,
17467 cx: &mut TestAppContext,
17468 assertion_fn: impl Fn(HighlightedText, &App),
17469) {
17470 let window = cx.add_window(|window, cx| {
17471 let buffer = MultiBuffer::build_simple(text, cx);
17472 Editor::new(EditorMode::Full, buffer, None, window, cx)
17473 });
17474 let cx = &mut VisualTestContext::from_window(*window, cx);
17475
17476 let (buffer, snapshot) = window
17477 .update(cx, |editor, _window, cx| {
17478 (
17479 editor.buffer().clone(),
17480 editor.buffer().read(cx).snapshot(cx),
17481 )
17482 })
17483 .unwrap();
17484
17485 let edits = edits
17486 .into_iter()
17487 .map(|(range, edit)| {
17488 (
17489 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17490 edit,
17491 )
17492 })
17493 .collect::<Vec<_>>();
17494
17495 let text_anchor_edits = edits
17496 .clone()
17497 .into_iter()
17498 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17499 .collect::<Vec<_>>();
17500
17501 let edit_preview = window
17502 .update(cx, |_, _window, cx| {
17503 buffer
17504 .read(cx)
17505 .as_singleton()
17506 .unwrap()
17507 .read(cx)
17508 .preview_edits(text_anchor_edits.into(), cx)
17509 })
17510 .unwrap()
17511 .await;
17512
17513 cx.update(|_window, cx| {
17514 let highlighted_edits = inline_completion_edit_text(
17515 &snapshot.as_singleton().unwrap().2,
17516 &edits,
17517 &edit_preview,
17518 include_deletions,
17519 cx,
17520 );
17521 assertion_fn(highlighted_edits, cx)
17522 });
17523}
17524
17525#[track_caller]
17526fn assert_breakpoint(
17527 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17528 path: &Arc<Path>,
17529 expected: Vec<(u32, Breakpoint)>,
17530) {
17531 if expected.len() == 0usize {
17532 assert!(!breakpoints.contains_key(path), "{}", path.display());
17533 } else {
17534 let mut breakpoint = breakpoints
17535 .get(path)
17536 .unwrap()
17537 .into_iter()
17538 .map(|breakpoint| {
17539 (
17540 breakpoint.row,
17541 Breakpoint {
17542 message: breakpoint.message.clone(),
17543 state: breakpoint.state,
17544 condition: breakpoint.condition.clone(),
17545 hit_condition: breakpoint.hit_condition.clone(),
17546 },
17547 )
17548 })
17549 .collect::<Vec<_>>();
17550
17551 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17552
17553 assert_eq!(expected, breakpoint);
17554 }
17555}
17556
17557fn add_log_breakpoint_at_cursor(
17558 editor: &mut Editor,
17559 log_message: &str,
17560 window: &mut Window,
17561 cx: &mut Context<Editor>,
17562) {
17563 let (anchor, bp) = editor
17564 .breakpoint_at_cursor_head(window, cx)
17565 .unwrap_or_else(|| {
17566 let cursor_position: Point = editor.selections.newest(cx).head();
17567
17568 let breakpoint_position = editor
17569 .snapshot(window, cx)
17570 .display_snapshot
17571 .buffer_snapshot
17572 .anchor_before(Point::new(cursor_position.row, 0));
17573
17574 (breakpoint_position, Breakpoint::new_log(&log_message))
17575 });
17576
17577 editor.edit_breakpoint_at_anchor(
17578 anchor,
17579 bp,
17580 BreakpointEditAction::EditLogMessage(log_message.into()),
17581 cx,
17582 );
17583}
17584
17585#[gpui::test]
17586async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17587 init_test(cx, |_| {});
17588
17589 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17590 let fs = FakeFs::new(cx.executor());
17591 fs.insert_tree(
17592 path!("/a"),
17593 json!({
17594 "main.rs": sample_text,
17595 }),
17596 )
17597 .await;
17598 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17599 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17600 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17601
17602 let fs = FakeFs::new(cx.executor());
17603 fs.insert_tree(
17604 path!("/a"),
17605 json!({
17606 "main.rs": sample_text,
17607 }),
17608 )
17609 .await;
17610 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17611 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17612 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17613 let worktree_id = workspace
17614 .update(cx, |workspace, _window, cx| {
17615 workspace.project().update(cx, |project, cx| {
17616 project.worktrees(cx).next().unwrap().read(cx).id()
17617 })
17618 })
17619 .unwrap();
17620
17621 let buffer = project
17622 .update(cx, |project, cx| {
17623 project.open_buffer((worktree_id, "main.rs"), cx)
17624 })
17625 .await
17626 .unwrap();
17627
17628 let (editor, cx) = cx.add_window_view(|window, cx| {
17629 Editor::new(
17630 EditorMode::Full,
17631 MultiBuffer::build_from_buffer(buffer, cx),
17632 Some(project.clone()),
17633 window,
17634 cx,
17635 )
17636 });
17637
17638 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17639 let abs_path = project.read_with(cx, |project, cx| {
17640 project
17641 .absolute_path(&project_path, cx)
17642 .map(|path_buf| Arc::from(path_buf.to_owned()))
17643 .unwrap()
17644 });
17645
17646 // assert we can add breakpoint on the first line
17647 editor.update_in(cx, |editor, window, cx| {
17648 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17649 editor.move_to_end(&MoveToEnd, window, cx);
17650 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17651 });
17652
17653 let breakpoints = editor.update(cx, |editor, cx| {
17654 editor
17655 .breakpoint_store()
17656 .as_ref()
17657 .unwrap()
17658 .read(cx)
17659 .all_breakpoints(cx)
17660 .clone()
17661 });
17662
17663 assert_eq!(1, breakpoints.len());
17664 assert_breakpoint(
17665 &breakpoints,
17666 &abs_path,
17667 vec![
17668 (0, Breakpoint::new_standard()),
17669 (3, Breakpoint::new_standard()),
17670 ],
17671 );
17672
17673 editor.update_in(cx, |editor, window, cx| {
17674 editor.move_to_beginning(&MoveToBeginning, window, cx);
17675 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17676 });
17677
17678 let breakpoints = editor.update(cx, |editor, cx| {
17679 editor
17680 .breakpoint_store()
17681 .as_ref()
17682 .unwrap()
17683 .read(cx)
17684 .all_breakpoints(cx)
17685 .clone()
17686 });
17687
17688 assert_eq!(1, breakpoints.len());
17689 assert_breakpoint(
17690 &breakpoints,
17691 &abs_path,
17692 vec![(3, Breakpoint::new_standard())],
17693 );
17694
17695 editor.update_in(cx, |editor, window, cx| {
17696 editor.move_to_end(&MoveToEnd, window, cx);
17697 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17698 });
17699
17700 let breakpoints = editor.update(cx, |editor, cx| {
17701 editor
17702 .breakpoint_store()
17703 .as_ref()
17704 .unwrap()
17705 .read(cx)
17706 .all_breakpoints(cx)
17707 .clone()
17708 });
17709
17710 assert_eq!(0, breakpoints.len());
17711 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17712}
17713
17714#[gpui::test]
17715async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17716 init_test(cx, |_| {});
17717
17718 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17719
17720 let fs = FakeFs::new(cx.executor());
17721 fs.insert_tree(
17722 path!("/a"),
17723 json!({
17724 "main.rs": sample_text,
17725 }),
17726 )
17727 .await;
17728 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17729 let (workspace, cx) =
17730 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17731
17732 let worktree_id = workspace.update(cx, |workspace, cx| {
17733 workspace.project().update(cx, |project, cx| {
17734 project.worktrees(cx).next().unwrap().read(cx).id()
17735 })
17736 });
17737
17738 let buffer = project
17739 .update(cx, |project, cx| {
17740 project.open_buffer((worktree_id, "main.rs"), cx)
17741 })
17742 .await
17743 .unwrap();
17744
17745 let (editor, cx) = cx.add_window_view(|window, cx| {
17746 Editor::new(
17747 EditorMode::Full,
17748 MultiBuffer::build_from_buffer(buffer, cx),
17749 Some(project.clone()),
17750 window,
17751 cx,
17752 )
17753 });
17754
17755 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17756 let abs_path = project.read_with(cx, |project, cx| {
17757 project
17758 .absolute_path(&project_path, cx)
17759 .map(|path_buf| Arc::from(path_buf.to_owned()))
17760 .unwrap()
17761 });
17762
17763 editor.update_in(cx, |editor, window, cx| {
17764 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17765 });
17766
17767 let breakpoints = editor.update(cx, |editor, cx| {
17768 editor
17769 .breakpoint_store()
17770 .as_ref()
17771 .unwrap()
17772 .read(cx)
17773 .all_breakpoints(cx)
17774 .clone()
17775 });
17776
17777 assert_breakpoint(
17778 &breakpoints,
17779 &abs_path,
17780 vec![(0, Breakpoint::new_log("hello world"))],
17781 );
17782
17783 // Removing a log message from a log breakpoint should remove it
17784 editor.update_in(cx, |editor, window, cx| {
17785 add_log_breakpoint_at_cursor(editor, "", window, cx);
17786 });
17787
17788 let breakpoints = editor.update(cx, |editor, cx| {
17789 editor
17790 .breakpoint_store()
17791 .as_ref()
17792 .unwrap()
17793 .read(cx)
17794 .all_breakpoints(cx)
17795 .clone()
17796 });
17797
17798 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17799
17800 editor.update_in(cx, |editor, window, cx| {
17801 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17802 editor.move_to_end(&MoveToEnd, window, cx);
17803 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17804 // Not adding a log message to a standard breakpoint shouldn't remove it
17805 add_log_breakpoint_at_cursor(editor, "", window, cx);
17806 });
17807
17808 let breakpoints = editor.update(cx, |editor, cx| {
17809 editor
17810 .breakpoint_store()
17811 .as_ref()
17812 .unwrap()
17813 .read(cx)
17814 .all_breakpoints(cx)
17815 .clone()
17816 });
17817
17818 assert_breakpoint(
17819 &breakpoints,
17820 &abs_path,
17821 vec![
17822 (0, Breakpoint::new_standard()),
17823 (3, Breakpoint::new_standard()),
17824 ],
17825 );
17826
17827 editor.update_in(cx, |editor, window, cx| {
17828 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17829 });
17830
17831 let breakpoints = editor.update(cx, |editor, cx| {
17832 editor
17833 .breakpoint_store()
17834 .as_ref()
17835 .unwrap()
17836 .read(cx)
17837 .all_breakpoints(cx)
17838 .clone()
17839 });
17840
17841 assert_breakpoint(
17842 &breakpoints,
17843 &abs_path,
17844 vec![
17845 (0, Breakpoint::new_standard()),
17846 (3, Breakpoint::new_log("hello world")),
17847 ],
17848 );
17849
17850 editor.update_in(cx, |editor, window, cx| {
17851 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17852 });
17853
17854 let breakpoints = editor.update(cx, |editor, cx| {
17855 editor
17856 .breakpoint_store()
17857 .as_ref()
17858 .unwrap()
17859 .read(cx)
17860 .all_breakpoints(cx)
17861 .clone()
17862 });
17863
17864 assert_breakpoint(
17865 &breakpoints,
17866 &abs_path,
17867 vec![
17868 (0, Breakpoint::new_standard()),
17869 (3, Breakpoint::new_log("hello Earth!!")),
17870 ],
17871 );
17872}
17873
17874/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17875/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17876/// or when breakpoints were placed out of order. This tests for a regression too
17877#[gpui::test]
17878async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17879 init_test(cx, |_| {});
17880
17881 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17882 let fs = FakeFs::new(cx.executor());
17883 fs.insert_tree(
17884 path!("/a"),
17885 json!({
17886 "main.rs": sample_text,
17887 }),
17888 )
17889 .await;
17890 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17891 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17892 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17893
17894 let fs = FakeFs::new(cx.executor());
17895 fs.insert_tree(
17896 path!("/a"),
17897 json!({
17898 "main.rs": sample_text,
17899 }),
17900 )
17901 .await;
17902 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17903 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17904 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17905 let worktree_id = workspace
17906 .update(cx, |workspace, _window, cx| {
17907 workspace.project().update(cx, |project, cx| {
17908 project.worktrees(cx).next().unwrap().read(cx).id()
17909 })
17910 })
17911 .unwrap();
17912
17913 let buffer = project
17914 .update(cx, |project, cx| {
17915 project.open_buffer((worktree_id, "main.rs"), cx)
17916 })
17917 .await
17918 .unwrap();
17919
17920 let (editor, cx) = cx.add_window_view(|window, cx| {
17921 Editor::new(
17922 EditorMode::Full,
17923 MultiBuffer::build_from_buffer(buffer, cx),
17924 Some(project.clone()),
17925 window,
17926 cx,
17927 )
17928 });
17929
17930 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17931 let abs_path = project.read_with(cx, |project, cx| {
17932 project
17933 .absolute_path(&project_path, cx)
17934 .map(|path_buf| Arc::from(path_buf.to_owned()))
17935 .unwrap()
17936 });
17937
17938 // assert we can add breakpoint on the first line
17939 editor.update_in(cx, |editor, window, cx| {
17940 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17941 editor.move_to_end(&MoveToEnd, window, cx);
17942 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17943 editor.move_up(&MoveUp, window, cx);
17944 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17945 });
17946
17947 let breakpoints = editor.update(cx, |editor, cx| {
17948 editor
17949 .breakpoint_store()
17950 .as_ref()
17951 .unwrap()
17952 .read(cx)
17953 .all_breakpoints(cx)
17954 .clone()
17955 });
17956
17957 assert_eq!(1, breakpoints.len());
17958 assert_breakpoint(
17959 &breakpoints,
17960 &abs_path,
17961 vec![
17962 (0, Breakpoint::new_standard()),
17963 (2, Breakpoint::new_standard()),
17964 (3, Breakpoint::new_standard()),
17965 ],
17966 );
17967
17968 editor.update_in(cx, |editor, window, cx| {
17969 editor.move_to_beginning(&MoveToBeginning, window, cx);
17970 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17971 editor.move_to_end(&MoveToEnd, window, cx);
17972 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17973 // Disabling a breakpoint that doesn't exist should do nothing
17974 editor.move_up(&MoveUp, window, cx);
17975 editor.move_up(&MoveUp, window, cx);
17976 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17977 });
17978
17979 let breakpoints = editor.update(cx, |editor, cx| {
17980 editor
17981 .breakpoint_store()
17982 .as_ref()
17983 .unwrap()
17984 .read(cx)
17985 .all_breakpoints(cx)
17986 .clone()
17987 });
17988
17989 let disable_breakpoint = {
17990 let mut bp = Breakpoint::new_standard();
17991 bp.state = BreakpointState::Disabled;
17992 bp
17993 };
17994
17995 assert_eq!(1, breakpoints.len());
17996 assert_breakpoint(
17997 &breakpoints,
17998 &abs_path,
17999 vec![
18000 (0, disable_breakpoint.clone()),
18001 (2, Breakpoint::new_standard()),
18002 (3, disable_breakpoint.clone()),
18003 ],
18004 );
18005
18006 editor.update_in(cx, |editor, window, cx| {
18007 editor.move_to_beginning(&MoveToBeginning, window, cx);
18008 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18009 editor.move_to_end(&MoveToEnd, window, cx);
18010 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18011 editor.move_up(&MoveUp, window, cx);
18012 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18013 });
18014
18015 let breakpoints = editor.update(cx, |editor, cx| {
18016 editor
18017 .breakpoint_store()
18018 .as_ref()
18019 .unwrap()
18020 .read(cx)
18021 .all_breakpoints(cx)
18022 .clone()
18023 });
18024
18025 assert_eq!(1, breakpoints.len());
18026 assert_breakpoint(
18027 &breakpoints,
18028 &abs_path,
18029 vec![
18030 (0, Breakpoint::new_standard()),
18031 (2, disable_breakpoint),
18032 (3, Breakpoint::new_standard()),
18033 ],
18034 );
18035}
18036
18037#[gpui::test]
18038async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18039 init_test(cx, |_| {});
18040 let capabilities = lsp::ServerCapabilities {
18041 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18042 prepare_provider: Some(true),
18043 work_done_progress_options: Default::default(),
18044 })),
18045 ..Default::default()
18046 };
18047 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18048
18049 cx.set_state(indoc! {"
18050 struct Fˇoo {}
18051 "});
18052
18053 cx.update_editor(|editor, _, cx| {
18054 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18055 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18056 editor.highlight_background::<DocumentHighlightRead>(
18057 &[highlight_range],
18058 |c| c.editor_document_highlight_read_background,
18059 cx,
18060 );
18061 });
18062
18063 let mut prepare_rename_handler = cx
18064 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18065 move |_, _, _| async move {
18066 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18067 start: lsp::Position {
18068 line: 0,
18069 character: 7,
18070 },
18071 end: lsp::Position {
18072 line: 0,
18073 character: 10,
18074 },
18075 })))
18076 },
18077 );
18078 let prepare_rename_task = cx
18079 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18080 .expect("Prepare rename was not started");
18081 prepare_rename_handler.next().await.unwrap();
18082 prepare_rename_task.await.expect("Prepare rename failed");
18083
18084 let mut rename_handler =
18085 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18086 let edit = lsp::TextEdit {
18087 range: lsp::Range {
18088 start: lsp::Position {
18089 line: 0,
18090 character: 7,
18091 },
18092 end: lsp::Position {
18093 line: 0,
18094 character: 10,
18095 },
18096 },
18097 new_text: "FooRenamed".to_string(),
18098 };
18099 Ok(Some(lsp::WorkspaceEdit::new(
18100 // Specify the same edit twice
18101 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18102 )))
18103 });
18104 let rename_task = cx
18105 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18106 .expect("Confirm rename was not started");
18107 rename_handler.next().await.unwrap();
18108 rename_task.await.expect("Confirm rename failed");
18109 cx.run_until_parked();
18110
18111 // Despite two edits, only one is actually applied as those are identical
18112 cx.assert_editor_state(indoc! {"
18113 struct FooRenamedˇ {}
18114 "});
18115}
18116
18117#[gpui::test]
18118async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18119 init_test(cx, |_| {});
18120 // These capabilities indicate that the server does not support prepare rename.
18121 let capabilities = lsp::ServerCapabilities {
18122 rename_provider: Some(lsp::OneOf::Left(true)),
18123 ..Default::default()
18124 };
18125 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18126
18127 cx.set_state(indoc! {"
18128 struct Fˇoo {}
18129 "});
18130
18131 cx.update_editor(|editor, _window, cx| {
18132 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18133 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18134 editor.highlight_background::<DocumentHighlightRead>(
18135 &[highlight_range],
18136 |c| c.editor_document_highlight_read_background,
18137 cx,
18138 );
18139 });
18140
18141 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18142 .expect("Prepare rename was not started")
18143 .await
18144 .expect("Prepare rename failed");
18145
18146 let mut rename_handler =
18147 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18148 let edit = lsp::TextEdit {
18149 range: lsp::Range {
18150 start: lsp::Position {
18151 line: 0,
18152 character: 7,
18153 },
18154 end: lsp::Position {
18155 line: 0,
18156 character: 10,
18157 },
18158 },
18159 new_text: "FooRenamed".to_string(),
18160 };
18161 Ok(Some(lsp::WorkspaceEdit::new(
18162 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18163 )))
18164 });
18165 let rename_task = cx
18166 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18167 .expect("Confirm rename was not started");
18168 rename_handler.next().await.unwrap();
18169 rename_task.await.expect("Confirm rename failed");
18170 cx.run_until_parked();
18171
18172 // Correct range is renamed, as `surrounding_word` is used to find it.
18173 cx.assert_editor_state(indoc! {"
18174 struct FooRenamedˇ {}
18175 "});
18176}
18177
18178#[gpui::test]
18179async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18180 init_test(cx, |_| {});
18181 let mut cx = EditorTestContext::new(cx).await;
18182
18183 let language = Arc::new(
18184 Language::new(
18185 LanguageConfig::default(),
18186 Some(tree_sitter_html::LANGUAGE.into()),
18187 )
18188 .with_brackets_query(
18189 r#"
18190 ("<" @open "/>" @close)
18191 ("</" @open ">" @close)
18192 ("<" @open ">" @close)
18193 ("\"" @open "\"" @close)
18194 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18195 "#,
18196 )
18197 .unwrap(),
18198 );
18199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18200
18201 cx.set_state(indoc! {"
18202 <span>ˇ</span>
18203 "});
18204 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18205 cx.assert_editor_state(indoc! {"
18206 <span>
18207 ˇ
18208 </span>
18209 "});
18210
18211 cx.set_state(indoc! {"
18212 <span><span></span>ˇ</span>
18213 "});
18214 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18215 cx.assert_editor_state(indoc! {"
18216 <span><span></span>
18217 ˇ</span>
18218 "});
18219
18220 cx.set_state(indoc! {"
18221 <span>ˇ
18222 </span>
18223 "});
18224 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18225 cx.assert_editor_state(indoc! {"
18226 <span>
18227 ˇ
18228 </span>
18229 "});
18230}
18231
18232#[gpui::test(iterations = 10)]
18233async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18234 init_test(cx, |_| {});
18235
18236 let fs = FakeFs::new(cx.executor());
18237 fs.insert_tree(
18238 path!("/dir"),
18239 json!({
18240 "a.ts": "a",
18241 }),
18242 )
18243 .await;
18244
18245 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18246 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18247 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18248
18249 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18250 language_registry.add(Arc::new(Language::new(
18251 LanguageConfig {
18252 name: "TypeScript".into(),
18253 matcher: LanguageMatcher {
18254 path_suffixes: vec!["ts".to_string()],
18255 ..Default::default()
18256 },
18257 ..Default::default()
18258 },
18259 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18260 )));
18261 let mut fake_language_servers = language_registry.register_fake_lsp(
18262 "TypeScript",
18263 FakeLspAdapter {
18264 capabilities: lsp::ServerCapabilities {
18265 code_lens_provider: Some(lsp::CodeLensOptions {
18266 resolve_provider: Some(true),
18267 }),
18268 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18269 commands: vec!["_the/command".to_string()],
18270 ..lsp::ExecuteCommandOptions::default()
18271 }),
18272 ..lsp::ServerCapabilities::default()
18273 },
18274 ..FakeLspAdapter::default()
18275 },
18276 );
18277
18278 let (buffer, _handle) = project
18279 .update(cx, |p, cx| {
18280 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18281 })
18282 .await
18283 .unwrap();
18284 cx.executor().run_until_parked();
18285
18286 let fake_server = fake_language_servers.next().await.unwrap();
18287
18288 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18289 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18290 drop(buffer_snapshot);
18291 let actions = cx
18292 .update_window(*workspace, |_, window, cx| {
18293 project.code_actions(&buffer, anchor..anchor, window, cx)
18294 })
18295 .unwrap();
18296
18297 fake_server
18298 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18299 Ok(Some(vec![
18300 lsp::CodeLens {
18301 range: lsp::Range::default(),
18302 command: Some(lsp::Command {
18303 title: "Code lens command".to_owned(),
18304 command: "_the/command".to_owned(),
18305 arguments: None,
18306 }),
18307 data: None,
18308 },
18309 lsp::CodeLens {
18310 range: lsp::Range::default(),
18311 command: Some(lsp::Command {
18312 title: "Command not in capabilities".to_owned(),
18313 command: "not in capabilities".to_owned(),
18314 arguments: None,
18315 }),
18316 data: None,
18317 },
18318 lsp::CodeLens {
18319 range: lsp::Range {
18320 start: lsp::Position {
18321 line: 1,
18322 character: 1,
18323 },
18324 end: lsp::Position {
18325 line: 1,
18326 character: 1,
18327 },
18328 },
18329 command: Some(lsp::Command {
18330 title: "Command not in range".to_owned(),
18331 command: "_the/command".to_owned(),
18332 arguments: None,
18333 }),
18334 data: None,
18335 },
18336 ]))
18337 })
18338 .next()
18339 .await;
18340
18341 let actions = actions.await.unwrap();
18342 assert_eq!(
18343 actions.len(),
18344 1,
18345 "Should have only one valid action for the 0..0 range"
18346 );
18347 let action = actions[0].clone();
18348 let apply = project.update(cx, |project, cx| {
18349 project.apply_code_action(buffer.clone(), action, true, cx)
18350 });
18351
18352 // Resolving the code action does not populate its edits. In absence of
18353 // edits, we must execute the given command.
18354 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18355 |mut lens, _| async move {
18356 let lens_command = lens.command.as_mut().expect("should have a command");
18357 assert_eq!(lens_command.title, "Code lens command");
18358 lens_command.arguments = Some(vec![json!("the-argument")]);
18359 Ok(lens)
18360 },
18361 );
18362
18363 // While executing the command, the language server sends the editor
18364 // a `workspaceEdit` request.
18365 fake_server
18366 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18367 let fake = fake_server.clone();
18368 move |params, _| {
18369 assert_eq!(params.command, "_the/command");
18370 let fake = fake.clone();
18371 async move {
18372 fake.server
18373 .request::<lsp::request::ApplyWorkspaceEdit>(
18374 lsp::ApplyWorkspaceEditParams {
18375 label: None,
18376 edit: lsp::WorkspaceEdit {
18377 changes: Some(
18378 [(
18379 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18380 vec![lsp::TextEdit {
18381 range: lsp::Range::new(
18382 lsp::Position::new(0, 0),
18383 lsp::Position::new(0, 0),
18384 ),
18385 new_text: "X".into(),
18386 }],
18387 )]
18388 .into_iter()
18389 .collect(),
18390 ),
18391 ..Default::default()
18392 },
18393 },
18394 )
18395 .await
18396 .unwrap();
18397 Ok(Some(json!(null)))
18398 }
18399 }
18400 })
18401 .next()
18402 .await;
18403
18404 // Applying the code lens command returns a project transaction containing the edits
18405 // sent by the language server in its `workspaceEdit` request.
18406 let transaction = apply.await.unwrap();
18407 assert!(transaction.0.contains_key(&buffer));
18408 buffer.update(cx, |buffer, cx| {
18409 assert_eq!(buffer.text(), "Xa");
18410 buffer.undo(cx);
18411 assert_eq!(buffer.text(), "a");
18412 });
18413}
18414
18415#[gpui::test]
18416async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18417 init_test(cx, |_| {});
18418
18419 let fs = FakeFs::new(cx.executor());
18420 let main_text = r#"fn main() {
18421println!("1");
18422println!("2");
18423println!("3");
18424println!("4");
18425println!("5");
18426}"#;
18427 let lib_text = "mod foo {}";
18428 fs.insert_tree(
18429 path!("/a"),
18430 json!({
18431 "lib.rs": lib_text,
18432 "main.rs": main_text,
18433 }),
18434 )
18435 .await;
18436
18437 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18438 let (workspace, cx) =
18439 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18440 let worktree_id = workspace.update(cx, |workspace, cx| {
18441 workspace.project().update(cx, |project, cx| {
18442 project.worktrees(cx).next().unwrap().read(cx).id()
18443 })
18444 });
18445
18446 let expected_ranges = vec![
18447 Point::new(0, 0)..Point::new(0, 0),
18448 Point::new(1, 0)..Point::new(1, 1),
18449 Point::new(2, 0)..Point::new(2, 2),
18450 Point::new(3, 0)..Point::new(3, 3),
18451 ];
18452
18453 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18454 let editor_1 = workspace
18455 .update_in(cx, |workspace, window, cx| {
18456 workspace.open_path(
18457 (worktree_id, "main.rs"),
18458 Some(pane_1.downgrade()),
18459 true,
18460 window,
18461 cx,
18462 )
18463 })
18464 .unwrap()
18465 .await
18466 .downcast::<Editor>()
18467 .unwrap();
18468 pane_1.update(cx, |pane, cx| {
18469 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18470 open_editor.update(cx, |editor, cx| {
18471 assert_eq!(
18472 editor.display_text(cx),
18473 main_text,
18474 "Original main.rs text on initial open",
18475 );
18476 assert_eq!(
18477 editor
18478 .selections
18479 .all::<Point>(cx)
18480 .into_iter()
18481 .map(|s| s.range())
18482 .collect::<Vec<_>>(),
18483 vec![Point::zero()..Point::zero()],
18484 "Default selections on initial open",
18485 );
18486 })
18487 });
18488 editor_1.update_in(cx, |editor, window, cx| {
18489 editor.change_selections(None, window, cx, |s| {
18490 s.select_ranges(expected_ranges.clone());
18491 });
18492 });
18493
18494 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18495 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18496 });
18497 let editor_2 = workspace
18498 .update_in(cx, |workspace, window, cx| {
18499 workspace.open_path(
18500 (worktree_id, "main.rs"),
18501 Some(pane_2.downgrade()),
18502 true,
18503 window,
18504 cx,
18505 )
18506 })
18507 .unwrap()
18508 .await
18509 .downcast::<Editor>()
18510 .unwrap();
18511 pane_2.update(cx, |pane, cx| {
18512 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18513 open_editor.update(cx, |editor, cx| {
18514 assert_eq!(
18515 editor.display_text(cx),
18516 main_text,
18517 "Original main.rs text on initial open in another panel",
18518 );
18519 assert_eq!(
18520 editor
18521 .selections
18522 .all::<Point>(cx)
18523 .into_iter()
18524 .map(|s| s.range())
18525 .collect::<Vec<_>>(),
18526 vec![Point::zero()..Point::zero()],
18527 "Default selections on initial open in another panel",
18528 );
18529 })
18530 });
18531
18532 editor_2.update_in(cx, |editor, window, cx| {
18533 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18534 });
18535
18536 let _other_editor_1 = workspace
18537 .update_in(cx, |workspace, window, cx| {
18538 workspace.open_path(
18539 (worktree_id, "lib.rs"),
18540 Some(pane_1.downgrade()),
18541 true,
18542 window,
18543 cx,
18544 )
18545 })
18546 .unwrap()
18547 .await
18548 .downcast::<Editor>()
18549 .unwrap();
18550 pane_1
18551 .update_in(cx, |pane, window, cx| {
18552 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18553 .unwrap()
18554 })
18555 .await
18556 .unwrap();
18557 drop(editor_1);
18558 pane_1.update(cx, |pane, cx| {
18559 pane.active_item()
18560 .unwrap()
18561 .downcast::<Editor>()
18562 .unwrap()
18563 .update(cx, |editor, cx| {
18564 assert_eq!(
18565 editor.display_text(cx),
18566 lib_text,
18567 "Other file should be open and active",
18568 );
18569 });
18570 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18571 });
18572
18573 let _other_editor_2 = workspace
18574 .update_in(cx, |workspace, window, cx| {
18575 workspace.open_path(
18576 (worktree_id, "lib.rs"),
18577 Some(pane_2.downgrade()),
18578 true,
18579 window,
18580 cx,
18581 )
18582 })
18583 .unwrap()
18584 .await
18585 .downcast::<Editor>()
18586 .unwrap();
18587 pane_2
18588 .update_in(cx, |pane, window, cx| {
18589 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18590 .unwrap()
18591 })
18592 .await
18593 .unwrap();
18594 drop(editor_2);
18595 pane_2.update(cx, |pane, cx| {
18596 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18597 open_editor.update(cx, |editor, cx| {
18598 assert_eq!(
18599 editor.display_text(cx),
18600 lib_text,
18601 "Other file should be open and active in another panel too",
18602 );
18603 });
18604 assert_eq!(
18605 pane.items().count(),
18606 1,
18607 "No other editors should be open in another pane",
18608 );
18609 });
18610
18611 let _editor_1_reopened = workspace
18612 .update_in(cx, |workspace, window, cx| {
18613 workspace.open_path(
18614 (worktree_id, "main.rs"),
18615 Some(pane_1.downgrade()),
18616 true,
18617 window,
18618 cx,
18619 )
18620 })
18621 .unwrap()
18622 .await
18623 .downcast::<Editor>()
18624 .unwrap();
18625 let _editor_2_reopened = workspace
18626 .update_in(cx, |workspace, window, cx| {
18627 workspace.open_path(
18628 (worktree_id, "main.rs"),
18629 Some(pane_2.downgrade()),
18630 true,
18631 window,
18632 cx,
18633 )
18634 })
18635 .unwrap()
18636 .await
18637 .downcast::<Editor>()
18638 .unwrap();
18639 pane_1.update(cx, |pane, cx| {
18640 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18641 open_editor.update(cx, |editor, cx| {
18642 assert_eq!(
18643 editor.display_text(cx),
18644 main_text,
18645 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18646 );
18647 assert_eq!(
18648 editor
18649 .selections
18650 .all::<Point>(cx)
18651 .into_iter()
18652 .map(|s| s.range())
18653 .collect::<Vec<_>>(),
18654 expected_ranges,
18655 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18656 );
18657 })
18658 });
18659 pane_2.update(cx, |pane, cx| {
18660 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18661 open_editor.update(cx, |editor, cx| {
18662 assert_eq!(
18663 editor.display_text(cx),
18664 r#"fn main() {
18665⋯rintln!("1");
18666⋯intln!("2");
18667⋯ntln!("3");
18668println!("4");
18669println!("5");
18670}"#,
18671 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18672 );
18673 assert_eq!(
18674 editor
18675 .selections
18676 .all::<Point>(cx)
18677 .into_iter()
18678 .map(|s| s.range())
18679 .collect::<Vec<_>>(),
18680 vec![Point::zero()..Point::zero()],
18681 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18682 );
18683 })
18684 });
18685}
18686
18687#[gpui::test]
18688async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18689 init_test(cx, |_| {});
18690
18691 let fs = FakeFs::new(cx.executor());
18692 let main_text = r#"fn main() {
18693println!("1");
18694println!("2");
18695println!("3");
18696println!("4");
18697println!("5");
18698}"#;
18699 let lib_text = "mod foo {}";
18700 fs.insert_tree(
18701 path!("/a"),
18702 json!({
18703 "lib.rs": lib_text,
18704 "main.rs": main_text,
18705 }),
18706 )
18707 .await;
18708
18709 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18710 let (workspace, cx) =
18711 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18712 let worktree_id = workspace.update(cx, |workspace, cx| {
18713 workspace.project().update(cx, |project, cx| {
18714 project.worktrees(cx).next().unwrap().read(cx).id()
18715 })
18716 });
18717
18718 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18719 let editor = workspace
18720 .update_in(cx, |workspace, window, cx| {
18721 workspace.open_path(
18722 (worktree_id, "main.rs"),
18723 Some(pane.downgrade()),
18724 true,
18725 window,
18726 cx,
18727 )
18728 })
18729 .unwrap()
18730 .await
18731 .downcast::<Editor>()
18732 .unwrap();
18733 pane.update(cx, |pane, cx| {
18734 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18735 open_editor.update(cx, |editor, cx| {
18736 assert_eq!(
18737 editor.display_text(cx),
18738 main_text,
18739 "Original main.rs text on initial open",
18740 );
18741 })
18742 });
18743 editor.update_in(cx, |editor, window, cx| {
18744 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18745 });
18746
18747 cx.update_global(|store: &mut SettingsStore, cx| {
18748 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18749 s.restore_on_file_reopen = Some(false);
18750 });
18751 });
18752 editor.update_in(cx, |editor, window, cx| {
18753 editor.fold_ranges(
18754 vec![
18755 Point::new(1, 0)..Point::new(1, 1),
18756 Point::new(2, 0)..Point::new(2, 2),
18757 Point::new(3, 0)..Point::new(3, 3),
18758 ],
18759 false,
18760 window,
18761 cx,
18762 );
18763 });
18764 pane.update_in(cx, |pane, window, cx| {
18765 pane.close_all_items(&CloseAllItems::default(), window, cx)
18766 .unwrap()
18767 })
18768 .await
18769 .unwrap();
18770 pane.update(cx, |pane, _| {
18771 assert!(pane.active_item().is_none());
18772 });
18773 cx.update_global(|store: &mut SettingsStore, cx| {
18774 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18775 s.restore_on_file_reopen = Some(true);
18776 });
18777 });
18778
18779 let _editor_reopened = workspace
18780 .update_in(cx, |workspace, window, cx| {
18781 workspace.open_path(
18782 (worktree_id, "main.rs"),
18783 Some(pane.downgrade()),
18784 true,
18785 window,
18786 cx,
18787 )
18788 })
18789 .unwrap()
18790 .await
18791 .downcast::<Editor>()
18792 .unwrap();
18793 pane.update(cx, |pane, cx| {
18794 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18795 open_editor.update(cx, |editor, cx| {
18796 assert_eq!(
18797 editor.display_text(cx),
18798 main_text,
18799 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18800 );
18801 })
18802 });
18803}
18804
18805fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18806 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18807 point..point
18808}
18809
18810fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18811 let (text, ranges) = marked_text_ranges(marked_text, true);
18812 assert_eq!(editor.text(cx), text);
18813 assert_eq!(
18814 editor.selections.ranges(cx),
18815 ranges,
18816 "Assert selections are {}",
18817 marked_text
18818 );
18819}
18820
18821pub fn handle_signature_help_request(
18822 cx: &mut EditorLspTestContext,
18823 mocked_response: lsp::SignatureHelp,
18824) -> impl Future<Output = ()> + use<> {
18825 let mut request =
18826 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18827 let mocked_response = mocked_response.clone();
18828 async move { Ok(Some(mocked_response)) }
18829 });
18830
18831 async move {
18832 request.next().await;
18833 }
18834}
18835
18836/// Handle completion request passing a marked string specifying where the completion
18837/// should be triggered from using '|' character, what range should be replaced, and what completions
18838/// should be returned using '<' and '>' to delimit the range.
18839///
18840/// Also see `handle_completion_request_with_insert_and_replace`.
18841#[track_caller]
18842pub fn handle_completion_request(
18843 cx: &mut EditorLspTestContext,
18844 marked_string: &str,
18845 completions: Vec<&'static str>,
18846 counter: Arc<AtomicUsize>,
18847) -> impl Future<Output = ()> {
18848 let complete_from_marker: TextRangeMarker = '|'.into();
18849 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18850 let (_, mut marked_ranges) = marked_text_ranges_by(
18851 marked_string,
18852 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18853 );
18854
18855 let complete_from_position =
18856 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18857 let replace_range =
18858 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18859
18860 let mut request =
18861 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18862 let completions = completions.clone();
18863 counter.fetch_add(1, atomic::Ordering::Release);
18864 async move {
18865 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18866 assert_eq!(
18867 params.text_document_position.position,
18868 complete_from_position
18869 );
18870 Ok(Some(lsp::CompletionResponse::Array(
18871 completions
18872 .iter()
18873 .map(|completion_text| lsp::CompletionItem {
18874 label: completion_text.to_string(),
18875 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18876 range: replace_range,
18877 new_text: completion_text.to_string(),
18878 })),
18879 ..Default::default()
18880 })
18881 .collect(),
18882 )))
18883 }
18884 });
18885
18886 async move {
18887 request.next().await;
18888 }
18889}
18890
18891/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
18892/// given instead, which also contains an `insert` range.
18893///
18894/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
18895/// that is, `replace_range.start..cursor_pos`.
18896pub fn handle_completion_request_with_insert_and_replace(
18897 cx: &mut EditorLspTestContext,
18898 marked_string: &str,
18899 completions: Vec<&'static str>,
18900 counter: Arc<AtomicUsize>,
18901) -> impl Future<Output = ()> {
18902 let complete_from_marker: TextRangeMarker = '|'.into();
18903 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18904 let (_, mut marked_ranges) = marked_text_ranges_by(
18905 marked_string,
18906 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18907 );
18908
18909 let complete_from_position =
18910 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18911 let replace_range =
18912 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18913
18914 let mut request =
18915 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18916 let completions = completions.clone();
18917 counter.fetch_add(1, atomic::Ordering::Release);
18918 async move {
18919 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18920 assert_eq!(
18921 params.text_document_position.position, complete_from_position,
18922 "marker `|` position doesn't match",
18923 );
18924 Ok(Some(lsp::CompletionResponse::Array(
18925 completions
18926 .iter()
18927 .map(|completion_text| lsp::CompletionItem {
18928 label: completion_text.to_string(),
18929 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18930 lsp::InsertReplaceEdit {
18931 insert: lsp::Range {
18932 start: replace_range.start,
18933 end: complete_from_position,
18934 },
18935 replace: replace_range,
18936 new_text: completion_text.to_string(),
18937 },
18938 )),
18939 ..Default::default()
18940 })
18941 .collect(),
18942 )))
18943 }
18944 });
18945
18946 async move {
18947 request.next().await;
18948 }
18949}
18950
18951fn handle_resolve_completion_request(
18952 cx: &mut EditorLspTestContext,
18953 edits: Option<Vec<(&'static str, &'static str)>>,
18954) -> impl Future<Output = ()> {
18955 let edits = edits.map(|edits| {
18956 edits
18957 .iter()
18958 .map(|(marked_string, new_text)| {
18959 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18960 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18961 lsp::TextEdit::new(replace_range, new_text.to_string())
18962 })
18963 .collect::<Vec<_>>()
18964 });
18965
18966 let mut request =
18967 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18968 let edits = edits.clone();
18969 async move {
18970 Ok(lsp::CompletionItem {
18971 additional_text_edits: edits,
18972 ..Default::default()
18973 })
18974 }
18975 });
18976
18977 async move {
18978 request.next().await;
18979 }
18980}
18981
18982pub(crate) fn update_test_language_settings(
18983 cx: &mut TestAppContext,
18984 f: impl Fn(&mut AllLanguageSettingsContent),
18985) {
18986 cx.update(|cx| {
18987 SettingsStore::update_global(cx, |store, cx| {
18988 store.update_user_settings::<AllLanguageSettings>(cx, f);
18989 });
18990 });
18991}
18992
18993pub(crate) fn update_test_project_settings(
18994 cx: &mut TestAppContext,
18995 f: impl Fn(&mut ProjectSettings),
18996) {
18997 cx.update(|cx| {
18998 SettingsStore::update_global(cx, |store, cx| {
18999 store.update_user_settings::<ProjectSettings>(cx, f);
19000 });
19001 });
19002}
19003
19004pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19005 cx.update(|cx| {
19006 assets::Assets.load_test_fonts(cx);
19007 let store = SettingsStore::test(cx);
19008 cx.set_global(store);
19009 theme::init(theme::LoadThemes::JustBase, cx);
19010 release_channel::init(SemanticVersion::default(), cx);
19011 client::init_settings(cx);
19012 language::init(cx);
19013 Project::init_settings(cx);
19014 workspace::init_settings(cx);
19015 crate::init(cx);
19016 });
19017
19018 update_test_language_settings(cx, f);
19019}
19020
19021#[track_caller]
19022fn assert_hunk_revert(
19023 not_reverted_text_with_selections: &str,
19024 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19025 expected_reverted_text_with_selections: &str,
19026 base_text: &str,
19027 cx: &mut EditorLspTestContext,
19028) {
19029 cx.set_state(not_reverted_text_with_selections);
19030 cx.set_head_text(base_text);
19031 cx.executor().run_until_parked();
19032
19033 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19034 let snapshot = editor.snapshot(window, cx);
19035 let reverted_hunk_statuses = snapshot
19036 .buffer_snapshot
19037 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19038 .map(|hunk| hunk.status().kind)
19039 .collect::<Vec<_>>();
19040
19041 editor.git_restore(&Default::default(), window, cx);
19042 reverted_hunk_statuses
19043 });
19044 cx.executor().run_until_parked();
19045 cx.assert_editor_state(expected_reverted_text_with_selections);
19046 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19047}