1use super::*;
2use crate::{
3 JoinLines,
4 inline_completion_tests::FakeInlineCompletionProvider,
5 linked_editing_ranges::LinkedEditingRanges,
6 scroll::scroll_amount::ScrollAmount,
7 test::{
8 assert_text_with_selections, build_editor,
9 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
10 editor_test_context::EditorTestContext,
11 select_ranges,
12 },
13};
14use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
15use futures::StreamExt;
16use gpui::{
17 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
18 VisualTestContext, WindowBounds, WindowOptions, div,
19};
20use indoc::indoc;
21use language::{
22 BracketPairConfig,
23 Capability::ReadWrite,
24 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
25 Override, Point,
26 language_settings::{
27 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
28 LanguageSettingsContent, LspInsertMode, PrettierSettings,
29 },
30 tree_sitter_python,
31};
32use language_settings::{Formatter, FormatterList, IndentGuideSettings};
33use lsp::CompletionParams;
34use multi_buffer::{IndentGuide, PathKey};
35use parking_lot::Mutex;
36use pretty_assertions::{assert_eq, assert_ne};
37use project::{
38 FakeFs,
39 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
40 project_settings::{LspSettings, ProjectSettings},
41};
42use serde_json::{self, json};
43use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
44use std::{
45 iter,
46 sync::atomic::{self, AtomicUsize},
47};
48use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
49use text::ToPoint as _;
50use unindent::Unindent;
51use util::{
52 assert_set_eq, path,
53 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
54 uri,
55};
56use workspace::{
57 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
58 item::{FollowEvent, FollowableItem, Item, ItemHandle},
59};
60
61#[gpui::test]
62fn test_edit_events(cx: &mut TestAppContext) {
63 init_test(cx, |_| {});
64
65 let buffer = cx.new(|cx| {
66 let mut buffer = language::Buffer::local("123456", cx);
67 buffer.set_group_interval(Duration::from_secs(1));
68 buffer
69 });
70
71 let events = Rc::new(RefCell::new(Vec::new()));
72 let editor1 = cx.add_window({
73 let events = events.clone();
74 |window, cx| {
75 let entity = cx.entity().clone();
76 cx.subscribe_in(
77 &entity,
78 window,
79 move |_, _, event: &EditorEvent, _, _| match event {
80 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
81 EditorEvent::BufferEdited => {
82 events.borrow_mut().push(("editor1", "buffer edited"))
83 }
84 _ => {}
85 },
86 )
87 .detach();
88 Editor::for_buffer(buffer.clone(), None, window, cx)
89 }
90 });
91
92 let editor2 = cx.add_window({
93 let events = events.clone();
94 |window, cx| {
95 cx.subscribe_in(
96 &cx.entity().clone(),
97 window,
98 move |_, _, event: &EditorEvent, _, _| match event {
99 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
100 EditorEvent::BufferEdited => {
101 events.borrow_mut().push(("editor2", "buffer edited"))
102 }
103 _ => {}
104 },
105 )
106 .detach();
107 Editor::for_buffer(buffer.clone(), None, window, cx)
108 }
109 });
110
111 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
112
113 // Mutating editor 1 will emit an `Edited` event only for that editor.
114 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
115 assert_eq!(
116 mem::take(&mut *events.borrow_mut()),
117 [
118 ("editor1", "edited"),
119 ("editor1", "buffer edited"),
120 ("editor2", "buffer edited"),
121 ]
122 );
123
124 // Mutating editor 2 will emit an `Edited` event only for that editor.
125 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
126 assert_eq!(
127 mem::take(&mut *events.borrow_mut()),
128 [
129 ("editor2", "edited"),
130 ("editor1", "buffer edited"),
131 ("editor2", "buffer edited"),
132 ]
133 );
134
135 // Undoing on editor 1 will emit an `Edited` event only for that editor.
136 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
137 assert_eq!(
138 mem::take(&mut *events.borrow_mut()),
139 [
140 ("editor1", "edited"),
141 ("editor1", "buffer edited"),
142 ("editor2", "buffer edited"),
143 ]
144 );
145
146 // Redoing on editor 1 will emit an `Edited` event only for that editor.
147 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
148 assert_eq!(
149 mem::take(&mut *events.borrow_mut()),
150 [
151 ("editor1", "edited"),
152 ("editor1", "buffer edited"),
153 ("editor2", "buffer edited"),
154 ]
155 );
156
157 // Undoing on editor 2 will emit an `Edited` event only for that editor.
158 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
159 assert_eq!(
160 mem::take(&mut *events.borrow_mut()),
161 [
162 ("editor2", "edited"),
163 ("editor1", "buffer edited"),
164 ("editor2", "buffer edited"),
165 ]
166 );
167
168 // Redoing on editor 2 will emit an `Edited` event only for that editor.
169 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
170 assert_eq!(
171 mem::take(&mut *events.borrow_mut()),
172 [
173 ("editor2", "edited"),
174 ("editor1", "buffer edited"),
175 ("editor2", "buffer edited"),
176 ]
177 );
178
179 // No event is emitted when the mutation is a no-op.
180 _ = editor2.update(cx, |editor, window, cx| {
181 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
182
183 editor.backspace(&Backspace, window, cx);
184 });
185 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
186}
187
188#[gpui::test]
189fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
190 init_test(cx, |_| {});
191
192 let mut now = Instant::now();
193 let group_interval = Duration::from_millis(1);
194 let buffer = cx.new(|cx| {
195 let mut buf = language::Buffer::local("123456", cx);
196 buf.set_group_interval(group_interval);
197 buf
198 });
199 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
200 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
201
202 _ = editor.update(cx, |editor, window, cx| {
203 editor.start_transaction_at(now, window, cx);
204 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
205
206 editor.insert("cd", window, cx);
207 editor.end_transaction_at(now, cx);
208 assert_eq!(editor.text(cx), "12cd56");
209 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
210
211 editor.start_transaction_at(now, window, cx);
212 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
213 editor.insert("e", window, cx);
214 editor.end_transaction_at(now, cx);
215 assert_eq!(editor.text(cx), "12cde6");
216 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
217
218 now += group_interval + Duration::from_millis(1);
219 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
220
221 // Simulate an edit in another editor
222 buffer.update(cx, |buffer, cx| {
223 buffer.start_transaction_at(now, cx);
224 buffer.edit([(0..1, "a")], None, cx);
225 buffer.edit([(1..1, "b")], None, cx);
226 buffer.end_transaction_at(now, cx);
227 });
228
229 assert_eq!(editor.text(cx), "ab2cde6");
230 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
231
232 // Last transaction happened past the group interval in a different editor.
233 // Undo it individually and don't restore selections.
234 editor.undo(&Undo, window, cx);
235 assert_eq!(editor.text(cx), "12cde6");
236 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
237
238 // First two transactions happened within the group interval in this editor.
239 // Undo them together and restore selections.
240 editor.undo(&Undo, window, cx);
241 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
242 assert_eq!(editor.text(cx), "123456");
243 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
244
245 // Redo the first two transactions together.
246 editor.redo(&Redo, window, cx);
247 assert_eq!(editor.text(cx), "12cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
249
250 // Redo the last transaction on its own.
251 editor.redo(&Redo, window, cx);
252 assert_eq!(editor.text(cx), "ab2cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
254
255 // Test empty transactions.
256 editor.start_transaction_at(now, window, cx);
257 editor.end_transaction_at(now, cx);
258 editor.undo(&Undo, window, cx);
259 assert_eq!(editor.text(cx), "12cde6");
260 });
261}
262
263#[gpui::test]
264fn test_ime_composition(cx: &mut TestAppContext) {
265 init_test(cx, |_| {});
266
267 let buffer = cx.new(|cx| {
268 let mut buffer = language::Buffer::local("abcde", cx);
269 // Ensure automatic grouping doesn't occur.
270 buffer.set_group_interval(Duration::ZERO);
271 buffer
272 });
273
274 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
275 cx.add_window(|window, cx| {
276 let mut editor = build_editor(buffer.clone(), window, cx);
277
278 // Start a new IME composition.
279 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
280 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
281 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
282 assert_eq!(editor.text(cx), "äbcde");
283 assert_eq!(
284 editor.marked_text_ranges(cx),
285 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
286 );
287
288 // Finalize IME composition.
289 editor.replace_text_in_range(None, "ā", window, cx);
290 assert_eq!(editor.text(cx), "ābcde");
291 assert_eq!(editor.marked_text_ranges(cx), None);
292
293 // IME composition edits are grouped and are undone/redone at once.
294 editor.undo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "abcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297 editor.redo(&Default::default(), window, cx);
298 assert_eq!(editor.text(cx), "ābcde");
299 assert_eq!(editor.marked_text_ranges(cx), None);
300
301 // Start a new IME composition.
302 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
303 assert_eq!(
304 editor.marked_text_ranges(cx),
305 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
306 );
307
308 // Undoing during an IME composition cancels it.
309 editor.undo(&Default::default(), window, cx);
310 assert_eq!(editor.text(cx), "ābcde");
311 assert_eq!(editor.marked_text_ranges(cx), None);
312
313 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
314 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
315 assert_eq!(editor.text(cx), "ābcdè");
316 assert_eq!(
317 editor.marked_text_ranges(cx),
318 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
319 );
320
321 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
322 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
323 assert_eq!(editor.text(cx), "ābcdę");
324 assert_eq!(editor.marked_text_ranges(cx), None);
325
326 // Start a new IME composition with multiple cursors.
327 editor.change_selections(None, window, cx, |s| {
328 s.select_ranges([
329 OffsetUtf16(1)..OffsetUtf16(1),
330 OffsetUtf16(3)..OffsetUtf16(3),
331 OffsetUtf16(5)..OffsetUtf16(5),
332 ])
333 });
334 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
335 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
336 assert_eq!(
337 editor.marked_text_ranges(cx),
338 Some(vec![
339 OffsetUtf16(0)..OffsetUtf16(3),
340 OffsetUtf16(4)..OffsetUtf16(7),
341 OffsetUtf16(8)..OffsetUtf16(11)
342 ])
343 );
344
345 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
346 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
347 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
348 assert_eq!(
349 editor.marked_text_ranges(cx),
350 Some(vec![
351 OffsetUtf16(1)..OffsetUtf16(2),
352 OffsetUtf16(5)..OffsetUtf16(6),
353 OffsetUtf16(9)..OffsetUtf16(10)
354 ])
355 );
356
357 // Finalize IME composition with multiple cursors.
358 editor.replace_text_in_range(Some(9..10), "2", window, cx);
359 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
360 assert_eq!(editor.marked_text_ranges(cx), None);
361
362 editor
363 });
364}
365
366#[gpui::test]
367fn test_selection_with_mouse(cx: &mut TestAppContext) {
368 init_test(cx, |_| {});
369
370 let editor = cx.add_window(|window, cx| {
371 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
372 build_editor(buffer, window, cx)
373 });
374
375 _ = editor.update(cx, |editor, window, cx| {
376 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
377 });
378 assert_eq!(
379 editor
380 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
381 .unwrap(),
382 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
383 );
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.update_selection(
387 DisplayPoint::new(DisplayRow(3), 3),
388 0,
389 gpui::Point::<f32>::default(),
390 window,
391 cx,
392 );
393 });
394
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(1), 1),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.end_selection(window, cx);
421 editor.update_selection(
422 DisplayPoint::new(DisplayRow(3), 3),
423 0,
424 gpui::Point::<f32>::default(),
425 window,
426 cx,
427 );
428 });
429
430 assert_eq!(
431 editor
432 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
433 .unwrap(),
434 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
435 );
436
437 _ = editor.update(cx, |editor, window, cx| {
438 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
439 editor.update_selection(
440 DisplayPoint::new(DisplayRow(0), 0),
441 0,
442 gpui::Point::<f32>::default(),
443 window,
444 cx,
445 );
446 });
447
448 assert_eq!(
449 editor
450 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
451 .unwrap(),
452 [
453 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
454 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
455 ]
456 );
457
458 _ = editor.update(cx, |editor, window, cx| {
459 editor.end_selection(window, cx);
460 });
461
462 assert_eq!(
463 editor
464 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
465 .unwrap(),
466 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
467 );
468}
469
470#[gpui::test]
471fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
472 init_test(cx, |_| {});
473
474 let editor = cx.add_window(|window, cx| {
475 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
476 build_editor(buffer, window, cx)
477 });
478
479 _ = editor.update(cx, |editor, window, cx| {
480 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
481 });
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 });
486
487 _ = editor.update(cx, |editor, window, cx| {
488 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
489 });
490
491 _ = editor.update(cx, |editor, window, cx| {
492 editor.end_selection(window, cx);
493 });
494
495 assert_eq!(
496 editor
497 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
498 .unwrap(),
499 [
500 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
501 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
502 ]
503 );
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
507 });
508
509 _ = editor.update(cx, |editor, window, cx| {
510 editor.end_selection(window, cx);
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
516 .unwrap(),
517 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
518 );
519}
520
521#[gpui::test]
522fn test_canceling_pending_selection(cx: &mut TestAppContext) {
523 init_test(cx, |_| {});
524
525 let editor = cx.add_window(|window, cx| {
526 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
527 build_editor(buffer, window, cx)
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
532 assert_eq!(
533 editor.selections.display_ranges(cx),
534 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
535 );
536 });
537
538 _ = editor.update(cx, |editor, window, cx| {
539 editor.update_selection(
540 DisplayPoint::new(DisplayRow(3), 3),
541 0,
542 gpui::Point::<f32>::default(),
543 window,
544 cx,
545 );
546 assert_eq!(
547 editor.selections.display_ranges(cx),
548 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
549 );
550 });
551
552 _ = editor.update(cx, |editor, window, cx| {
553 editor.cancel(&Cancel, window, cx);
554 editor.update_selection(
555 DisplayPoint::new(DisplayRow(1), 1),
556 0,
557 gpui::Point::<f32>::default(),
558 window,
559 cx,
560 );
561 assert_eq!(
562 editor.selections.display_ranges(cx),
563 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
564 );
565 });
566}
567
568#[gpui::test]
569fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
570 init_test(cx, |_| {});
571
572 let editor = cx.add_window(|window, cx| {
573 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
574 build_editor(buffer, window, cx)
575 });
576
577 _ = editor.update(cx, |editor, window, cx| {
578 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
579 assert_eq!(
580 editor.selections.display_ranges(cx),
581 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
582 );
583
584 editor.move_down(&Default::default(), window, cx);
585 assert_eq!(
586 editor.selections.display_ranges(cx),
587 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
588 );
589
590 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
591 assert_eq!(
592 editor.selections.display_ranges(cx),
593 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
594 );
595
596 editor.move_up(&Default::default(), window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
600 );
601 });
602}
603
604#[gpui::test]
605fn test_clone(cx: &mut TestAppContext) {
606 init_test(cx, |_| {});
607
608 let (text, selection_ranges) = marked_text_ranges(
609 indoc! {"
610 one
611 two
612 threeˇ
613 four
614 fiveˇ
615 "},
616 true,
617 );
618
619 let editor = cx.add_window(|window, cx| {
620 let buffer = MultiBuffer::build_simple(&text, cx);
621 build_editor(buffer, window, cx)
622 });
623
624 _ = editor.update(cx, |editor, window, cx| {
625 editor.change_selections(None, window, cx, |s| {
626 s.select_ranges(selection_ranges.clone())
627 });
628 editor.fold_creases(
629 vec![
630 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
631 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
632 ],
633 true,
634 window,
635 cx,
636 );
637 });
638
639 let cloned_editor = editor
640 .update(cx, |editor, _, cx| {
641 cx.open_window(Default::default(), |window, cx| {
642 cx.new(|cx| editor.clone(window, cx))
643 })
644 })
645 .unwrap()
646 .unwrap();
647
648 let snapshot = editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651 let cloned_snapshot = cloned_editor
652 .update(cx, |e, window, cx| e.snapshot(window, cx))
653 .unwrap();
654
655 assert_eq!(
656 cloned_editor
657 .update(cx, |e, _, cx| e.display_text(cx))
658 .unwrap(),
659 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
660 );
661 assert_eq!(
662 cloned_snapshot
663 .folds_in_range(0..text.len())
664 .collect::<Vec<_>>(),
665 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
666 );
667 assert_set_eq!(
668 cloned_editor
669 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
670 .unwrap(),
671 editor
672 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
673 .unwrap()
674 );
675 assert_set_eq!(
676 cloned_editor
677 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
678 .unwrap(),
679 editor
680 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
681 .unwrap()
682 );
683}
684
685#[gpui::test]
686async fn test_navigation_history(cx: &mut TestAppContext) {
687 init_test(cx, |_| {});
688
689 use workspace::item::Item;
690
691 let fs = FakeFs::new(cx.executor());
692 let project = Project::test(fs, [], cx).await;
693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
694 let pane = workspace
695 .update(cx, |workspace, _, _| workspace.active_pane().clone())
696 .unwrap();
697
698 _ = workspace.update(cx, |_v, window, cx| {
699 cx.new(|cx| {
700 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
701 let mut editor = build_editor(buffer.clone(), window, cx);
702 let handle = cx.entity();
703 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
704
705 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
706 editor.nav_history.as_mut().unwrap().pop_backward(cx)
707 }
708
709 // Move the cursor a small distance.
710 // Nothing is added to the navigation history.
711 editor.change_selections(None, window, cx, |s| {
712 s.select_display_ranges([
713 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
714 ])
715 });
716 editor.change_selections(None, window, cx, |s| {
717 s.select_display_ranges([
718 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
719 ])
720 });
721 assert!(pop_history(&mut editor, cx).is_none());
722
723 // Move the cursor a large distance.
724 // The history can jump back to the previous position.
725 editor.change_selections(None, window, cx, |s| {
726 s.select_display_ranges([
727 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
728 ])
729 });
730 let nav_entry = pop_history(&mut editor, cx).unwrap();
731 editor.navigate(nav_entry.data.unwrap(), window, cx);
732 assert_eq!(nav_entry.item.id(), cx.entity_id());
733 assert_eq!(
734 editor.selections.display_ranges(cx),
735 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
736 );
737 assert!(pop_history(&mut editor, cx).is_none());
738
739 // Move the cursor a small distance via the mouse.
740 // Nothing is added to the navigation history.
741 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
742 editor.end_selection(window, cx);
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a large distance via the mouse.
750 // The history can jump back to the previous position.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
756 );
757 let nav_entry = pop_history(&mut editor, cx).unwrap();
758 editor.navigate(nav_entry.data.unwrap(), window, cx);
759 assert_eq!(nav_entry.item.id(), cx.entity_id());
760 assert_eq!(
761 editor.selections.display_ranges(cx),
762 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
763 );
764 assert!(pop_history(&mut editor, cx).is_none());
765
766 // Set scroll position to check later
767 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
768 let original_scroll_position = editor.scroll_manager.anchor();
769
770 // Jump to the end of the document and adjust scroll
771 editor.move_to_end(&MoveToEnd, window, cx);
772 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
773 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
774
775 let nav_entry = pop_history(&mut editor, cx).unwrap();
776 editor.navigate(nav_entry.data.unwrap(), window, cx);
777 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
778
779 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
780 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
781 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
782 let invalid_point = Point::new(9999, 0);
783 editor.navigate(
784 Box::new(NavigationData {
785 cursor_anchor: invalid_anchor,
786 cursor_position: invalid_point,
787 scroll_anchor: ScrollAnchor {
788 anchor: invalid_anchor,
789 offset: Default::default(),
790 },
791 scroll_top_row: invalid_point.row,
792 }),
793 window,
794 cx,
795 );
796 assert_eq!(
797 editor.selections.display_ranges(cx),
798 &[editor.max_point(cx)..editor.max_point(cx)]
799 );
800 assert_eq!(
801 editor.scroll_position(cx),
802 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
803 );
804
805 editor
806 })
807 });
808}
809
810#[gpui::test]
811fn test_cancel(cx: &mut TestAppContext) {
812 init_test(cx, |_| {});
813
814 let editor = cx.add_window(|window, cx| {
815 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
816 build_editor(buffer, window, cx)
817 });
818
819 _ = editor.update(cx, |editor, window, cx| {
820 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
821 editor.update_selection(
822 DisplayPoint::new(DisplayRow(1), 1),
823 0,
824 gpui::Point::<f32>::default(),
825 window,
826 cx,
827 );
828 editor.end_selection(window, cx);
829
830 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(0), 3),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839 assert_eq!(
840 editor.selections.display_ranges(cx),
841 [
842 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
843 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
844 ]
845 );
846 });
847
848 _ = editor.update(cx, |editor, window, cx| {
849 editor.cancel(&Cancel, window, cx);
850 assert_eq!(
851 editor.selections.display_ranges(cx),
852 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
853 );
854 });
855
856 _ = editor.update(cx, |editor, window, cx| {
857 editor.cancel(&Cancel, window, cx);
858 assert_eq!(
859 editor.selections.display_ranges(cx),
860 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
861 );
862 });
863}
864
865#[gpui::test]
866fn test_fold_action(cx: &mut TestAppContext) {
867 init_test(cx, |_| {});
868
869 let editor = cx.add_window(|window, cx| {
870 let buffer = MultiBuffer::build_simple(
871 &"
872 impl Foo {
873 // Hello!
874
875 fn a() {
876 1
877 }
878
879 fn b() {
880 2
881 }
882
883 fn c() {
884 3
885 }
886 }
887 "
888 .unindent(),
889 cx,
890 );
891 build_editor(buffer.clone(), window, cx)
892 });
893
894 _ = editor.update(cx, |editor, window, cx| {
895 editor.change_selections(None, window, cx, |s| {
896 s.select_display_ranges([
897 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
898 ]);
899 });
900 editor.fold(&Fold, window, cx);
901 assert_eq!(
902 editor.display_text(cx),
903 "
904 impl Foo {
905 // Hello!
906
907 fn a() {
908 1
909 }
910
911 fn b() {⋯
912 }
913
914 fn c() {⋯
915 }
916 }
917 "
918 .unindent(),
919 );
920
921 editor.fold(&Fold, window, cx);
922 assert_eq!(
923 editor.display_text(cx),
924 "
925 impl Foo {⋯
926 }
927 "
928 .unindent(),
929 );
930
931 editor.unfold_lines(&UnfoldLines, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {
936 // Hello!
937
938 fn a() {
939 1
940 }
941
942 fn b() {⋯
943 }
944
945 fn c() {⋯
946 }
947 }
948 "
949 .unindent(),
950 );
951
952 editor.unfold_lines(&UnfoldLines, window, cx);
953 assert_eq!(
954 editor.display_text(cx),
955 editor.buffer.read(cx).read(cx).text()
956 );
957 });
958}
959
960#[gpui::test]
961fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
962 init_test(cx, |_| {});
963
964 let editor = cx.add_window(|window, cx| {
965 let buffer = MultiBuffer::build_simple(
966 &"
967 class Foo:
968 # Hello!
969
970 def a():
971 print(1)
972
973 def b():
974 print(2)
975
976 def c():
977 print(3)
978 "
979 .unindent(),
980 cx,
981 );
982 build_editor(buffer.clone(), window, cx)
983 });
984
985 _ = editor.update(cx, |editor, window, cx| {
986 editor.change_selections(None, window, cx, |s| {
987 s.select_display_ranges([
988 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
989 ]);
990 });
991 editor.fold(&Fold, window, cx);
992 assert_eq!(
993 editor.display_text(cx),
994 "
995 class Foo:
996 # Hello!
997
998 def a():
999 print(1)
1000
1001 def b():⋯
1002
1003 def c():⋯
1004 "
1005 .unindent(),
1006 );
1007
1008 editor.fold(&Fold, window, cx);
1009 assert_eq!(
1010 editor.display_text(cx),
1011 "
1012 class Foo:⋯
1013 "
1014 .unindent(),
1015 );
1016
1017 editor.unfold_lines(&UnfoldLines, window, cx);
1018 assert_eq!(
1019 editor.display_text(cx),
1020 "
1021 class Foo:
1022 # Hello!
1023
1024 def a():
1025 print(1)
1026
1027 def b():⋯
1028
1029 def c():⋯
1030 "
1031 .unindent(),
1032 );
1033
1034 editor.unfold_lines(&UnfoldLines, window, cx);
1035 assert_eq!(
1036 editor.display_text(cx),
1037 editor.buffer.read(cx).read(cx).text()
1038 );
1039 });
1040}
1041
1042#[gpui::test]
1043fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1044 init_test(cx, |_| {});
1045
1046 let editor = cx.add_window(|window, cx| {
1047 let buffer = MultiBuffer::build_simple(
1048 &"
1049 class Foo:
1050 # Hello!
1051
1052 def a():
1053 print(1)
1054
1055 def b():
1056 print(2)
1057
1058
1059 def c():
1060 print(3)
1061
1062
1063 "
1064 .unindent(),
1065 cx,
1066 );
1067 build_editor(buffer.clone(), window, cx)
1068 });
1069
1070 _ = editor.update(cx, |editor, window, cx| {
1071 editor.change_selections(None, window, cx, |s| {
1072 s.select_display_ranges([
1073 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1074 ]);
1075 });
1076 editor.fold(&Fold, window, cx);
1077 assert_eq!(
1078 editor.display_text(cx),
1079 "
1080 class Foo:
1081 # Hello!
1082
1083 def a():
1084 print(1)
1085
1086 def b():⋯
1087
1088
1089 def c():⋯
1090
1091
1092 "
1093 .unindent(),
1094 );
1095
1096 editor.fold(&Fold, window, cx);
1097 assert_eq!(
1098 editor.display_text(cx),
1099 "
1100 class Foo:⋯
1101
1102
1103 "
1104 .unindent(),
1105 );
1106
1107 editor.unfold_lines(&UnfoldLines, window, cx);
1108 assert_eq!(
1109 editor.display_text(cx),
1110 "
1111 class Foo:
1112 # Hello!
1113
1114 def a():
1115 print(1)
1116
1117 def b():⋯
1118
1119
1120 def c():⋯
1121
1122
1123 "
1124 .unindent(),
1125 );
1126
1127 editor.unfold_lines(&UnfoldLines, window, cx);
1128 assert_eq!(
1129 editor.display_text(cx),
1130 editor.buffer.read(cx).read(cx).text()
1131 );
1132 });
1133}
1134
1135#[gpui::test]
1136fn test_fold_at_level(cx: &mut TestAppContext) {
1137 init_test(cx, |_| {});
1138
1139 let editor = cx.add_window(|window, cx| {
1140 let buffer = MultiBuffer::build_simple(
1141 &"
1142 class Foo:
1143 # Hello!
1144
1145 def a():
1146 print(1)
1147
1148 def b():
1149 print(2)
1150
1151
1152 class Bar:
1153 # World!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 "
1163 .unindent(),
1164 cx,
1165 );
1166 build_editor(buffer.clone(), window, cx)
1167 });
1168
1169 _ = editor.update(cx, |editor, window, cx| {
1170 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1171 assert_eq!(
1172 editor.display_text(cx),
1173 "
1174 class Foo:
1175 # Hello!
1176
1177 def a():⋯
1178
1179 def b():⋯
1180
1181
1182 class Bar:
1183 # World!
1184
1185 def a():⋯
1186
1187 def b():⋯
1188
1189
1190 "
1191 .unindent(),
1192 );
1193
1194 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1195 assert_eq!(
1196 editor.display_text(cx),
1197 "
1198 class Foo:⋯
1199
1200
1201 class Bar:⋯
1202
1203
1204 "
1205 .unindent(),
1206 );
1207
1208 editor.unfold_all(&UnfoldAll, window, cx);
1209 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1210 assert_eq!(
1211 editor.display_text(cx),
1212 "
1213 class Foo:
1214 # Hello!
1215
1216 def a():
1217 print(1)
1218
1219 def b():
1220 print(2)
1221
1222
1223 class Bar:
1224 # World!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 "
1234 .unindent(),
1235 );
1236
1237 assert_eq!(
1238 editor.display_text(cx),
1239 editor.buffer.read(cx).read(cx).text()
1240 );
1241 });
1242}
1243
1244#[gpui::test]
1245fn test_move_cursor(cx: &mut TestAppContext) {
1246 init_test(cx, |_| {});
1247
1248 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1249 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1250
1251 buffer.update(cx, |buffer, cx| {
1252 buffer.edit(
1253 vec![
1254 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1255 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1256 ],
1257 None,
1258 cx,
1259 );
1260 });
1261 _ = editor.update(cx, |editor, window, cx| {
1262 assert_eq!(
1263 editor.selections.display_ranges(cx),
1264 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1265 );
1266
1267 editor.move_down(&MoveDown, window, cx);
1268 assert_eq!(
1269 editor.selections.display_ranges(cx),
1270 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1271 );
1272
1273 editor.move_right(&MoveRight, window, cx);
1274 assert_eq!(
1275 editor.selections.display_ranges(cx),
1276 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1277 );
1278
1279 editor.move_left(&MoveLeft, window, cx);
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1283 );
1284
1285 editor.move_up(&MoveUp, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1289 );
1290
1291 editor.move_to_end(&MoveToEnd, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1295 );
1296
1297 editor.move_to_beginning(&MoveToBeginning, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1301 );
1302
1303 editor.change_selections(None, window, cx, |s| {
1304 s.select_display_ranges([
1305 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1306 ]);
1307 });
1308 editor.select_to_beginning(&SelectToBeginning, window, cx);
1309 assert_eq!(
1310 editor.selections.display_ranges(cx),
1311 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1312 );
1313
1314 editor.select_to_end(&SelectToEnd, window, cx);
1315 assert_eq!(
1316 editor.selections.display_ranges(cx),
1317 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1318 );
1319 });
1320}
1321
1322#[gpui::test]
1323fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1324 init_test(cx, |_| {});
1325
1326 let editor = cx.add_window(|window, cx| {
1327 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1328 build_editor(buffer.clone(), window, cx)
1329 });
1330
1331 assert_eq!('🟥'.len_utf8(), 4);
1332 assert_eq!('α'.len_utf8(), 2);
1333
1334 _ = editor.update(cx, |editor, window, cx| {
1335 editor.fold_creases(
1336 vec![
1337 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1340 ],
1341 true,
1342 window,
1343 cx,
1344 );
1345 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1346
1347 editor.move_right(&MoveRight, window, cx);
1348 assert_eq!(
1349 editor.selections.display_ranges(cx),
1350 &[empty_range(0, "🟥".len())]
1351 );
1352 editor.move_right(&MoveRight, window, cx);
1353 assert_eq!(
1354 editor.selections.display_ranges(cx),
1355 &[empty_range(0, "🟥🟧".len())]
1356 );
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥🟧⋯".len())]
1361 );
1362
1363 editor.move_down(&MoveDown, window, cx);
1364 assert_eq!(
1365 editor.selections.display_ranges(cx),
1366 &[empty_range(1, "ab⋯e".len())]
1367 );
1368 editor.move_left(&MoveLeft, window, cx);
1369 assert_eq!(
1370 editor.selections.display_ranges(cx),
1371 &[empty_range(1, "ab⋯".len())]
1372 );
1373 editor.move_left(&MoveLeft, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "a".len())]
1382 );
1383
1384 editor.move_down(&MoveDown, window, cx);
1385 assert_eq!(
1386 editor.selections.display_ranges(cx),
1387 &[empty_range(2, "α".len())]
1388 );
1389 editor.move_right(&MoveRight, window, cx);
1390 assert_eq!(
1391 editor.selections.display_ranges(cx),
1392 &[empty_range(2, "αβ".len())]
1393 );
1394 editor.move_right(&MoveRight, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "αβ⋯".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ⋯ε".len())]
1403 );
1404
1405 editor.move_up(&MoveUp, window, cx);
1406 assert_eq!(
1407 editor.selections.display_ranges(cx),
1408 &[empty_range(1, "ab⋯e".len())]
1409 );
1410 editor.move_down(&MoveDown, window, cx);
1411 assert_eq!(
1412 editor.selections.display_ranges(cx),
1413 &[empty_range(2, "αβ⋯ε".len())]
1414 );
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420
1421 editor.move_up(&MoveUp, window, cx);
1422 assert_eq!(
1423 editor.selections.display_ranges(cx),
1424 &[empty_range(0, "🟥🟧".len())]
1425 );
1426 editor.move_left(&MoveLeft, window, cx);
1427 assert_eq!(
1428 editor.selections.display_ranges(cx),
1429 &[empty_range(0, "🟥".len())]
1430 );
1431 editor.move_left(&MoveLeft, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "".len())]
1435 );
1436 });
1437}
1438
1439#[gpui::test]
1440fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1441 init_test(cx, |_| {});
1442
1443 let editor = cx.add_window(|window, cx| {
1444 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1445 build_editor(buffer.clone(), window, cx)
1446 });
1447 _ = editor.update(cx, |editor, window, cx| {
1448 editor.change_selections(None, window, cx, |s| {
1449 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1450 });
1451
1452 // moving above start of document should move selection to start of document,
1453 // but the next move down should still be at the original goal_x
1454 editor.move_up(&MoveUp, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[empty_range(0, "".len())]
1458 );
1459
1460 editor.move_down(&MoveDown, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[empty_range(1, "abcd".len())]
1464 );
1465
1466 editor.move_down(&MoveDown, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(2, "αβγ".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(3, "abcd".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1482 );
1483
1484 // moving past end of document should not change goal_x
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(5, "".len())]
1489 );
1490
1491 editor.move_down(&MoveDown, window, cx);
1492 assert_eq!(
1493 editor.selections.display_ranges(cx),
1494 &[empty_range(5, "".len())]
1495 );
1496
1497 editor.move_up(&MoveUp, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1501 );
1502
1503 editor.move_up(&MoveUp, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(3, "abcd".len())]
1507 );
1508
1509 editor.move_up(&MoveUp, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(2, "αβγ".len())]
1513 );
1514 });
1515}
1516
1517#[gpui::test]
1518fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1519 init_test(cx, |_| {});
1520 let move_to_beg = MoveToBeginningOfLine {
1521 stop_at_soft_wraps: true,
1522 stop_at_indent: true,
1523 };
1524
1525 let delete_to_beg = DeleteToBeginningOfLine {
1526 stop_at_indent: false,
1527 };
1528
1529 let move_to_end = MoveToEndOfLine {
1530 stop_at_soft_wraps: true,
1531 };
1532
1533 let editor = cx.add_window(|window, cx| {
1534 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1535 build_editor(buffer, window, cx)
1536 });
1537 _ = editor.update(cx, |editor, window, cx| {
1538 editor.change_selections(None, window, cx, |s| {
1539 s.select_display_ranges([
1540 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1541 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1542 ]);
1543 });
1544 });
1545
1546 _ = editor.update(cx, |editor, window, cx| {
1547 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1548 assert_eq!(
1549 editor.selections.display_ranges(cx),
1550 &[
1551 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1552 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1553 ]
1554 );
1555 });
1556
1557 _ = editor.update(cx, |editor, window, cx| {
1558 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1559 assert_eq!(
1560 editor.selections.display_ranges(cx),
1561 &[
1562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1563 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1564 ]
1565 );
1566 });
1567
1568 _ = editor.update(cx, |editor, window, cx| {
1569 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1570 assert_eq!(
1571 editor.selections.display_ranges(cx),
1572 &[
1573 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1574 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1575 ]
1576 );
1577 });
1578
1579 _ = editor.update(cx, |editor, window, cx| {
1580 editor.move_to_end_of_line(&move_to_end, window, cx);
1581 assert_eq!(
1582 editor.selections.display_ranges(cx),
1583 &[
1584 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1585 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1586 ]
1587 );
1588 });
1589
1590 // Moving to the end of line again is a no-op.
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_to_end_of_line(&move_to_end, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[
1596 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1597 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1598 ]
1599 );
1600 });
1601
1602 _ = editor.update(cx, |editor, window, cx| {
1603 editor.move_left(&MoveLeft, window, cx);
1604 editor.select_to_beginning_of_line(
1605 &SelectToBeginningOfLine {
1606 stop_at_soft_wraps: true,
1607 stop_at_indent: true,
1608 },
1609 window,
1610 cx,
1611 );
1612 assert_eq!(
1613 editor.selections.display_ranges(cx),
1614 &[
1615 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1616 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1617 ]
1618 );
1619 });
1620
1621 _ = editor.update(cx, |editor, window, cx| {
1622 editor.select_to_beginning_of_line(
1623 &SelectToBeginningOfLine {
1624 stop_at_soft_wraps: true,
1625 stop_at_indent: true,
1626 },
1627 window,
1628 cx,
1629 );
1630 assert_eq!(
1631 editor.selections.display_ranges(cx),
1632 &[
1633 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1634 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1635 ]
1636 );
1637 });
1638
1639 _ = editor.update(cx, |editor, window, cx| {
1640 editor.select_to_beginning_of_line(
1641 &SelectToBeginningOfLine {
1642 stop_at_soft_wraps: true,
1643 stop_at_indent: true,
1644 },
1645 window,
1646 cx,
1647 );
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[
1651 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1652 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1653 ]
1654 );
1655 });
1656
1657 _ = editor.update(cx, |editor, window, cx| {
1658 editor.select_to_end_of_line(
1659 &SelectToEndOfLine {
1660 stop_at_soft_wraps: true,
1661 },
1662 window,
1663 cx,
1664 );
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[
1668 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1669 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1670 ]
1671 );
1672 });
1673
1674 _ = editor.update(cx, |editor, window, cx| {
1675 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1676 assert_eq!(editor.display_text(cx), "ab\n de");
1677 assert_eq!(
1678 editor.selections.display_ranges(cx),
1679 &[
1680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1681 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1682 ]
1683 );
1684 });
1685
1686 _ = editor.update(cx, |editor, window, cx| {
1687 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1688 assert_eq!(editor.display_text(cx), "\n");
1689 assert_eq!(
1690 editor.selections.display_ranges(cx),
1691 &[
1692 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1693 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1694 ]
1695 );
1696 });
1697}
1698
1699#[gpui::test]
1700fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1701 init_test(cx, |_| {});
1702 let move_to_beg = MoveToBeginningOfLine {
1703 stop_at_soft_wraps: false,
1704 stop_at_indent: false,
1705 };
1706
1707 let move_to_end = MoveToEndOfLine {
1708 stop_at_soft_wraps: false,
1709 };
1710
1711 let editor = cx.add_window(|window, cx| {
1712 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1713 build_editor(buffer, window, cx)
1714 });
1715
1716 _ = editor.update(cx, |editor, window, cx| {
1717 editor.set_wrap_width(Some(140.0.into()), cx);
1718
1719 // We expect the following lines after wrapping
1720 // ```
1721 // thequickbrownfox
1722 // jumpedoverthelazydo
1723 // gs
1724 // ```
1725 // The final `gs` was soft-wrapped onto a new line.
1726 assert_eq!(
1727 "thequickbrownfox\njumpedoverthelaz\nydogs",
1728 editor.display_text(cx),
1729 );
1730
1731 // First, let's assert behavior on the first line, that was not soft-wrapped.
1732 // Start the cursor at the `k` on the first line
1733 editor.change_selections(None, window, cx, |s| {
1734 s.select_display_ranges([
1735 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1736 ]);
1737 });
1738
1739 // Moving to the beginning of the line should put us at the beginning of the line.
1740 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1741 assert_eq!(
1742 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1743 editor.selections.display_ranges(cx)
1744 );
1745
1746 // Moving to the end of the line should put us at the end of the line.
1747 editor.move_to_end_of_line(&move_to_end, window, cx);
1748 assert_eq!(
1749 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1750 editor.selections.display_ranges(cx)
1751 );
1752
1753 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1754 // Start the cursor at the last line (`y` that was wrapped to a new line)
1755 editor.change_selections(None, window, cx, |s| {
1756 s.select_display_ranges([
1757 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1758 ]);
1759 });
1760
1761 // Moving to the beginning of the line should put us at the start of the second line of
1762 // display text, i.e., the `j`.
1763 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Moving to the beginning of the line again should be a no-op.
1770 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1771 assert_eq!(
1772 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1773 editor.selections.display_ranges(cx)
1774 );
1775
1776 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1777 // next display line.
1778 editor.move_to_end_of_line(&move_to_end, window, cx);
1779 assert_eq!(
1780 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1781 editor.selections.display_ranges(cx)
1782 );
1783
1784 // Moving to the end of the line again should be a no-op.
1785 editor.move_to_end_of_line(&move_to_end, window, cx);
1786 assert_eq!(
1787 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1788 editor.selections.display_ranges(cx)
1789 );
1790 });
1791}
1792
1793#[gpui::test]
1794fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1795 init_test(cx, |_| {});
1796
1797 let move_to_beg = MoveToBeginningOfLine {
1798 stop_at_soft_wraps: true,
1799 stop_at_indent: true,
1800 };
1801
1802 let select_to_beg = SelectToBeginningOfLine {
1803 stop_at_soft_wraps: true,
1804 stop_at_indent: true,
1805 };
1806
1807 let delete_to_beg = DeleteToBeginningOfLine {
1808 stop_at_indent: true,
1809 };
1810
1811 let move_to_end = MoveToEndOfLine {
1812 stop_at_soft_wraps: false,
1813 };
1814
1815 let editor = cx.add_window(|window, cx| {
1816 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1817 build_editor(buffer, window, cx)
1818 });
1819
1820 _ = editor.update(cx, |editor, window, cx| {
1821 editor.change_selections(None, window, cx, |s| {
1822 s.select_display_ranges([
1823 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1824 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1825 ]);
1826 });
1827
1828 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1829 // and the second cursor at the first non-whitespace character in the line.
1830 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1831 assert_eq!(
1832 editor.selections.display_ranges(cx),
1833 &[
1834 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1835 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1836 ]
1837 );
1838
1839 // Moving to the beginning of the line again should be a no-op for the first cursor,
1840 // and should move the second cursor to the beginning of the line.
1841 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1842 assert_eq!(
1843 editor.selections.display_ranges(cx),
1844 &[
1845 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1846 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1847 ]
1848 );
1849
1850 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1851 // and should move the second cursor back to the first non-whitespace character in the line.
1852 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1853 assert_eq!(
1854 editor.selections.display_ranges(cx),
1855 &[
1856 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1857 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1858 ]
1859 );
1860
1861 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1862 // and to the first non-whitespace character in the line for the second cursor.
1863 editor.move_to_end_of_line(&move_to_end, window, cx);
1864 editor.move_left(&MoveLeft, window, cx);
1865 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1866 assert_eq!(
1867 editor.selections.display_ranges(cx),
1868 &[
1869 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1870 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1871 ]
1872 );
1873
1874 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1875 // and should select to the beginning of the line for the second cursor.
1876 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1877 assert_eq!(
1878 editor.selections.display_ranges(cx),
1879 &[
1880 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1881 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1882 ]
1883 );
1884
1885 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1886 // and should delete to the first non-whitespace character in the line for the second cursor.
1887 editor.move_to_end_of_line(&move_to_end, window, cx);
1888 editor.move_left(&MoveLeft, window, cx);
1889 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1890 assert_eq!(editor.text(cx), "c\n f");
1891 });
1892}
1893
1894#[gpui::test]
1895fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1896 init_test(cx, |_| {});
1897
1898 let editor = cx.add_window(|window, cx| {
1899 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1900 build_editor(buffer, window, cx)
1901 });
1902 _ = editor.update(cx, |editor, window, cx| {
1903 editor.change_selections(None, window, cx, |s| {
1904 s.select_display_ranges([
1905 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1906 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1907 ])
1908 });
1909
1910 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1911 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1912
1913 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1914 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1915
1916 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1917 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1918
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1924
1925 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1926 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1929 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1932 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1933
1934 editor.move_right(&MoveRight, window, cx);
1935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1936 assert_selection_ranges(
1937 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1938 editor,
1939 cx,
1940 );
1941
1942 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1943 assert_selection_ranges(
1944 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1945 editor,
1946 cx,
1947 );
1948
1949 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1950 assert_selection_ranges(
1951 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1952 editor,
1953 cx,
1954 );
1955 });
1956}
1957
1958#[gpui::test]
1959fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1960 init_test(cx, |_| {});
1961
1962 let editor = cx.add_window(|window, cx| {
1963 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1964 build_editor(buffer, window, cx)
1965 });
1966
1967 _ = editor.update(cx, |editor, window, cx| {
1968 editor.set_wrap_width(Some(140.0.into()), cx);
1969 assert_eq!(
1970 editor.display_text(cx),
1971 "use one::{\n two::three::\n four::five\n};"
1972 );
1973
1974 editor.change_selections(None, window, cx, |s| {
1975 s.select_display_ranges([
1976 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1977 ]);
1978 });
1979
1980 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1981 assert_eq!(
1982 editor.selections.display_ranges(cx),
1983 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1984 );
1985
1986 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1987 assert_eq!(
1988 editor.selections.display_ranges(cx),
1989 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1990 );
1991
1992 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1996 );
1997
1998 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1999 assert_eq!(
2000 editor.selections.display_ranges(cx),
2001 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2002 );
2003
2004 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2005 assert_eq!(
2006 editor.selections.display_ranges(cx),
2007 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2008 );
2009
2010 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2011 assert_eq!(
2012 editor.selections.display_ranges(cx),
2013 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2014 );
2015 });
2016}
2017
2018#[gpui::test]
2019async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2020 init_test(cx, |_| {});
2021 let mut cx = EditorTestContext::new(cx).await;
2022
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2031
2032 cx.set_state(
2033 &r#"ˇone
2034 two
2035
2036 three
2037 fourˇ
2038 five
2039
2040 six"#
2041 .unindent(),
2042 );
2043
2044 cx.update_editor(|editor, window, cx| {
2045 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2046 });
2047 cx.assert_editor_state(
2048 &r#"one
2049 two
2050 ˇ
2051 three
2052 four
2053 five
2054 ˇ
2055 six"#
2056 .unindent(),
2057 );
2058
2059 cx.update_editor(|editor, window, cx| {
2060 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2061 });
2062 cx.assert_editor_state(
2063 &r#"one
2064 two
2065
2066 three
2067 four
2068 five
2069 ˇ
2070 sixˇ"#
2071 .unindent(),
2072 );
2073
2074 cx.update_editor(|editor, window, cx| {
2075 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2076 });
2077 cx.assert_editor_state(
2078 &r#"one
2079 two
2080
2081 three
2082 four
2083 five
2084
2085 sixˇ"#
2086 .unindent(),
2087 );
2088
2089 cx.update_editor(|editor, window, cx| {
2090 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2091 });
2092 cx.assert_editor_state(
2093 &r#"one
2094 two
2095
2096 three
2097 four
2098 five
2099 ˇ
2100 six"#
2101 .unindent(),
2102 );
2103
2104 cx.update_editor(|editor, window, cx| {
2105 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2106 });
2107 cx.assert_editor_state(
2108 &r#"one
2109 two
2110 ˇ
2111 three
2112 four
2113 five
2114
2115 six"#
2116 .unindent(),
2117 );
2118
2119 cx.update_editor(|editor, window, cx| {
2120 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2121 });
2122 cx.assert_editor_state(
2123 &r#"ˇone
2124 two
2125
2126 three
2127 four
2128 five
2129
2130 six"#
2131 .unindent(),
2132 );
2133}
2134
2135#[gpui::test]
2136async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2137 init_test(cx, |_| {});
2138 let mut cx = EditorTestContext::new(cx).await;
2139 let line_height = cx.editor(|editor, window, _| {
2140 editor
2141 .style()
2142 .unwrap()
2143 .text
2144 .line_height_in_pixels(window.rem_size())
2145 });
2146 let window = cx.window;
2147 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2148
2149 cx.set_state(
2150 r#"ˇone
2151 two
2152 three
2153 four
2154 five
2155 six
2156 seven
2157 eight
2158 nine
2159 ten
2160 "#,
2161 );
2162
2163 cx.update_editor(|editor, window, cx| {
2164 assert_eq!(
2165 editor.snapshot(window, cx).scroll_position(),
2166 gpui::Point::new(0., 0.)
2167 );
2168 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2169 assert_eq!(
2170 editor.snapshot(window, cx).scroll_position(),
2171 gpui::Point::new(0., 3.)
2172 );
2173 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2174 assert_eq!(
2175 editor.snapshot(window, cx).scroll_position(),
2176 gpui::Point::new(0., 6.)
2177 );
2178 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2179 assert_eq!(
2180 editor.snapshot(window, cx).scroll_position(),
2181 gpui::Point::new(0., 3.)
2182 );
2183
2184 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2185 assert_eq!(
2186 editor.snapshot(window, cx).scroll_position(),
2187 gpui::Point::new(0., 1.)
2188 );
2189 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2190 assert_eq!(
2191 editor.snapshot(window, cx).scroll_position(),
2192 gpui::Point::new(0., 3.)
2193 );
2194 });
2195}
2196
2197#[gpui::test]
2198async fn test_autoscroll(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201
2202 let line_height = cx.update_editor(|editor, window, cx| {
2203 editor.set_vertical_scroll_margin(2, cx);
2204 editor
2205 .style()
2206 .unwrap()
2207 .text
2208 .line_height_in_pixels(window.rem_size())
2209 });
2210 let window = cx.window;
2211 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2212
2213 cx.set_state(
2214 r#"ˇone
2215 two
2216 three
2217 four
2218 five
2219 six
2220 seven
2221 eight
2222 nine
2223 ten
2224 "#,
2225 );
2226 cx.update_editor(|editor, window, cx| {
2227 assert_eq!(
2228 editor.snapshot(window, cx).scroll_position(),
2229 gpui::Point::new(0., 0.0)
2230 );
2231 });
2232
2233 // Add a cursor below the visible area. Since both cursors cannot fit
2234 // on screen, the editor autoscrolls to reveal the newest cursor, and
2235 // allows the vertical scroll margin below that cursor.
2236 cx.update_editor(|editor, window, cx| {
2237 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2238 selections.select_ranges([
2239 Point::new(0, 0)..Point::new(0, 0),
2240 Point::new(6, 0)..Point::new(6, 0),
2241 ]);
2242 })
2243 });
2244 cx.update_editor(|editor, window, cx| {
2245 assert_eq!(
2246 editor.snapshot(window, cx).scroll_position(),
2247 gpui::Point::new(0., 3.0)
2248 );
2249 });
2250
2251 // Move down. The editor cursor scrolls down to track the newest cursor.
2252 cx.update_editor(|editor, window, cx| {
2253 editor.move_down(&Default::default(), window, cx);
2254 });
2255 cx.update_editor(|editor, window, cx| {
2256 assert_eq!(
2257 editor.snapshot(window, cx).scroll_position(),
2258 gpui::Point::new(0., 4.0)
2259 );
2260 });
2261
2262 // Add a cursor above the visible area. Since both cursors fit on screen,
2263 // the editor scrolls to show both.
2264 cx.update_editor(|editor, window, cx| {
2265 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2266 selections.select_ranges([
2267 Point::new(1, 0)..Point::new(1, 0),
2268 Point::new(6, 0)..Point::new(6, 0),
2269 ]);
2270 })
2271 });
2272 cx.update_editor(|editor, window, cx| {
2273 assert_eq!(
2274 editor.snapshot(window, cx).scroll_position(),
2275 gpui::Point::new(0., 1.0)
2276 );
2277 });
2278}
2279
2280#[gpui::test]
2281async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2282 init_test(cx, |_| {});
2283 let mut cx = EditorTestContext::new(cx).await;
2284
2285 let line_height = cx.editor(|editor, window, _cx| {
2286 editor
2287 .style()
2288 .unwrap()
2289 .text
2290 .line_height_in_pixels(window.rem_size())
2291 });
2292 let window = cx.window;
2293 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2294 cx.set_state(
2295 &r#"
2296 ˇone
2297 two
2298 threeˇ
2299 four
2300 five
2301 six
2302 seven
2303 eight
2304 nine
2305 ten
2306 "#
2307 .unindent(),
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 editor.move_page_down(&MovePageDown::default(), window, cx)
2312 });
2313 cx.assert_editor_state(
2314 &r#"
2315 one
2316 two
2317 three
2318 ˇfour
2319 five
2320 sixˇ
2321 seven
2322 eight
2323 nine
2324 ten
2325 "#
2326 .unindent(),
2327 );
2328
2329 cx.update_editor(|editor, window, cx| {
2330 editor.move_page_down(&MovePageDown::default(), window, cx)
2331 });
2332 cx.assert_editor_state(
2333 &r#"
2334 one
2335 two
2336 three
2337 four
2338 five
2339 six
2340 ˇseven
2341 eight
2342 nineˇ
2343 ten
2344 "#
2345 .unindent(),
2346 );
2347
2348 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2349 cx.assert_editor_state(
2350 &r#"
2351 one
2352 two
2353 three
2354 ˇfour
2355 five
2356 sixˇ
2357 seven
2358 eight
2359 nine
2360 ten
2361 "#
2362 .unindent(),
2363 );
2364
2365 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2366 cx.assert_editor_state(
2367 &r#"
2368 ˇone
2369 two
2370 threeˇ
2371 four
2372 five
2373 six
2374 seven
2375 eight
2376 nine
2377 ten
2378 "#
2379 .unindent(),
2380 );
2381
2382 // Test select collapsing
2383 cx.update_editor(|editor, window, cx| {
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 seven
2397 eight
2398 nine
2399 ˇten
2400 ˇ"#
2401 .unindent(),
2402 );
2403}
2404
2405#[gpui::test]
2406async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2407 init_test(cx, |_| {});
2408 let mut cx = EditorTestContext::new(cx).await;
2409 cx.set_state("one «two threeˇ» four");
2410 cx.update_editor(|editor, window, cx| {
2411 editor.delete_to_beginning_of_line(
2412 &DeleteToBeginningOfLine {
2413 stop_at_indent: false,
2414 },
2415 window,
2416 cx,
2417 );
2418 assert_eq!(editor.text(cx), " four");
2419 });
2420}
2421
2422#[gpui::test]
2423fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2424 init_test(cx, |_| {});
2425
2426 let editor = cx.add_window(|window, cx| {
2427 let buffer = MultiBuffer::build_simple("one two three four", cx);
2428 build_editor(buffer.clone(), window, cx)
2429 });
2430
2431 _ = editor.update(cx, |editor, window, cx| {
2432 editor.change_selections(None, window, cx, |s| {
2433 s.select_display_ranges([
2434 // an empty selection - the preceding word fragment is deleted
2435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2436 // characters selected - they are deleted
2437 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2438 ])
2439 });
2440 editor.delete_to_previous_word_start(
2441 &DeleteToPreviousWordStart {
2442 ignore_newlines: false,
2443 },
2444 window,
2445 cx,
2446 );
2447 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2448 });
2449
2450 _ = editor.update(cx, |editor, window, cx| {
2451 editor.change_selections(None, window, cx, |s| {
2452 s.select_display_ranges([
2453 // an empty selection - the following word fragment is deleted
2454 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2455 // characters selected - they are deleted
2456 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2457 ])
2458 });
2459 editor.delete_to_next_word_end(
2460 &DeleteToNextWordEnd {
2461 ignore_newlines: false,
2462 },
2463 window,
2464 cx,
2465 );
2466 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2467 });
2468}
2469
2470#[gpui::test]
2471fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2472 init_test(cx, |_| {});
2473
2474 let editor = cx.add_window(|window, cx| {
2475 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2476 build_editor(buffer.clone(), window, cx)
2477 });
2478 let del_to_prev_word_start = DeleteToPreviousWordStart {
2479 ignore_newlines: false,
2480 };
2481 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2482 ignore_newlines: true,
2483 };
2484
2485 _ = editor.update(cx, |editor, window, cx| {
2486 editor.change_selections(None, window, cx, |s| {
2487 s.select_display_ranges([
2488 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2489 ])
2490 });
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2501 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2502 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2503 });
2504}
2505
2506#[gpui::test]
2507fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2508 init_test(cx, |_| {});
2509
2510 let editor = cx.add_window(|window, cx| {
2511 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2512 build_editor(buffer.clone(), window, cx)
2513 });
2514 let del_to_next_word_end = DeleteToNextWordEnd {
2515 ignore_newlines: false,
2516 };
2517 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2518 ignore_newlines: true,
2519 };
2520
2521 _ = editor.update(cx, |editor, window, cx| {
2522 editor.change_selections(None, window, cx, |s| {
2523 s.select_display_ranges([
2524 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2525 ])
2526 });
2527 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2528 assert_eq!(
2529 editor.buffer.read(cx).read(cx).text(),
2530 "one\n two\nthree\n four"
2531 );
2532 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2533 assert_eq!(
2534 editor.buffer.read(cx).read(cx).text(),
2535 "\n two\nthree\n four"
2536 );
2537 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2538 assert_eq!(
2539 editor.buffer.read(cx).read(cx).text(),
2540 "two\nthree\n four"
2541 );
2542 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2546 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2547 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2548 });
2549}
2550
2551#[gpui::test]
2552fn test_newline(cx: &mut TestAppContext) {
2553 init_test(cx, |_| {});
2554
2555 let editor = cx.add_window(|window, cx| {
2556 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2557 build_editor(buffer.clone(), window, cx)
2558 });
2559
2560 _ = editor.update(cx, |editor, window, cx| {
2561 editor.change_selections(None, window, cx, |s| {
2562 s.select_display_ranges([
2563 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2565 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2566 ])
2567 });
2568
2569 editor.newline(&Newline, window, cx);
2570 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2571 });
2572}
2573
2574#[gpui::test]
2575fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2576 init_test(cx, |_| {});
2577
2578 let editor = cx.add_window(|window, cx| {
2579 let buffer = MultiBuffer::build_simple(
2580 "
2581 a
2582 b(
2583 X
2584 )
2585 c(
2586 X
2587 )
2588 "
2589 .unindent()
2590 .as_str(),
2591 cx,
2592 );
2593 let mut editor = build_editor(buffer.clone(), window, cx);
2594 editor.change_selections(None, window, cx, |s| {
2595 s.select_ranges([
2596 Point::new(2, 4)..Point::new(2, 5),
2597 Point::new(5, 4)..Point::new(5, 5),
2598 ])
2599 });
2600 editor
2601 });
2602
2603 _ = editor.update(cx, |editor, window, cx| {
2604 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2605 editor.buffer.update(cx, |buffer, cx| {
2606 buffer.edit(
2607 [
2608 (Point::new(1, 2)..Point::new(3, 0), ""),
2609 (Point::new(4, 2)..Point::new(6, 0), ""),
2610 ],
2611 None,
2612 cx,
2613 );
2614 assert_eq!(
2615 buffer.read(cx).text(),
2616 "
2617 a
2618 b()
2619 c()
2620 "
2621 .unindent()
2622 );
2623 });
2624 assert_eq!(
2625 editor.selections.ranges(cx),
2626 &[
2627 Point::new(1, 2)..Point::new(1, 2),
2628 Point::new(2, 2)..Point::new(2, 2),
2629 ],
2630 );
2631
2632 editor.newline(&Newline, window, cx);
2633 assert_eq!(
2634 editor.text(cx),
2635 "
2636 a
2637 b(
2638 )
2639 c(
2640 )
2641 "
2642 .unindent()
2643 );
2644
2645 // The selections are moved after the inserted newlines
2646 assert_eq!(
2647 editor.selections.ranges(cx),
2648 &[
2649 Point::new(2, 0)..Point::new(2, 0),
2650 Point::new(4, 0)..Point::new(4, 0),
2651 ],
2652 );
2653 });
2654}
2655
2656#[gpui::test]
2657async fn test_newline_above(cx: &mut TestAppContext) {
2658 init_test(cx, |settings| {
2659 settings.defaults.tab_size = NonZeroU32::new(4)
2660 });
2661
2662 let language = Arc::new(
2663 Language::new(
2664 LanguageConfig::default(),
2665 Some(tree_sitter_rust::LANGUAGE.into()),
2666 )
2667 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2668 .unwrap(),
2669 );
2670
2671 let mut cx = EditorTestContext::new(cx).await;
2672 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2673 cx.set_state(indoc! {"
2674 const a: ˇA = (
2675 (ˇ
2676 «const_functionˇ»(ˇ),
2677 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2678 )ˇ
2679 ˇ);ˇ
2680 "});
2681
2682 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2683 cx.assert_editor_state(indoc! {"
2684 ˇ
2685 const a: A = (
2686 ˇ
2687 (
2688 ˇ
2689 ˇ
2690 const_function(),
2691 ˇ
2692 ˇ
2693 ˇ
2694 ˇ
2695 something_else,
2696 ˇ
2697 )
2698 ˇ
2699 ˇ
2700 );
2701 "});
2702}
2703
2704#[gpui::test]
2705async fn test_newline_below(cx: &mut TestAppContext) {
2706 init_test(cx, |settings| {
2707 settings.defaults.tab_size = NonZeroU32::new(4)
2708 });
2709
2710 let language = Arc::new(
2711 Language::new(
2712 LanguageConfig::default(),
2713 Some(tree_sitter_rust::LANGUAGE.into()),
2714 )
2715 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2716 .unwrap(),
2717 );
2718
2719 let mut cx = EditorTestContext::new(cx).await;
2720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2721 cx.set_state(indoc! {"
2722 const a: ˇA = (
2723 (ˇ
2724 «const_functionˇ»(ˇ),
2725 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2726 )ˇ
2727 ˇ);ˇ
2728 "});
2729
2730 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2731 cx.assert_editor_state(indoc! {"
2732 const a: A = (
2733 ˇ
2734 (
2735 ˇ
2736 const_function(),
2737 ˇ
2738 ˇ
2739 something_else,
2740 ˇ
2741 ˇ
2742 ˇ
2743 ˇ
2744 )
2745 ˇ
2746 );
2747 ˇ
2748 ˇ
2749 "});
2750}
2751
2752#[gpui::test]
2753async fn test_newline_comments(cx: &mut TestAppContext) {
2754 init_test(cx, |settings| {
2755 settings.defaults.tab_size = NonZeroU32::new(4)
2756 });
2757
2758 let language = Arc::new(Language::new(
2759 LanguageConfig {
2760 line_comments: vec!["// ".into()],
2761 ..LanguageConfig::default()
2762 },
2763 None,
2764 ));
2765 {
2766 let mut cx = EditorTestContext::new(cx).await;
2767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2768 cx.set_state(indoc! {"
2769 // Fooˇ
2770 "});
2771
2772 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2773 cx.assert_editor_state(indoc! {"
2774 // Foo
2775 // ˇ
2776 "});
2777 // Ensure that we add comment prefix when existing line contains space
2778 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2779 cx.assert_editor_state(
2780 indoc! {"
2781 // Foo
2782 //s
2783 // ˇ
2784 "}
2785 .replace("s", " ") // s is used as space placeholder to prevent format on save
2786 .as_str(),
2787 );
2788 // Ensure that we add comment prefix when existing line does not contain space
2789 cx.set_state(indoc! {"
2790 // Foo
2791 //ˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 //
2797 // ˇ
2798 "});
2799 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2800 cx.set_state(indoc! {"
2801 ˇ// Foo
2802 "});
2803 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2804 cx.assert_editor_state(indoc! {"
2805
2806 ˇ// Foo
2807 "});
2808 }
2809 // Ensure that comment continuations can be disabled.
2810 update_test_language_settings(cx, |settings| {
2811 settings.defaults.extend_comment_on_newline = Some(false);
2812 });
2813 let mut cx = EditorTestContext::new(cx).await;
2814 cx.set_state(indoc! {"
2815 // Fooˇ
2816 "});
2817 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2818 cx.assert_editor_state(indoc! {"
2819 // Foo
2820 ˇ
2821 "});
2822}
2823
2824#[gpui::test]
2825async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2826 init_test(cx, |settings| {
2827 settings.defaults.tab_size = NonZeroU32::new(4)
2828 });
2829
2830 let language = Arc::new(Language::new(
2831 LanguageConfig {
2832 line_comments: vec!["// ".into(), "/// ".into()],
2833 ..LanguageConfig::default()
2834 },
2835 None,
2836 ));
2837 {
2838 let mut cx = EditorTestContext::new(cx).await;
2839 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2840 cx.set_state(indoc! {"
2841 //ˇ
2842 "});
2843 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2844 cx.assert_editor_state(indoc! {"
2845 //
2846 // ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 ///ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 ///
2855 /// ˇ
2856 "});
2857 }
2858}
2859
2860#[gpui::test]
2861async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2862 init_test(cx, |settings| {
2863 settings.defaults.tab_size = NonZeroU32::new(4)
2864 });
2865
2866 let language = Arc::new(Language::new(
2867 LanguageConfig {
2868 documentation: Some(language::DocumentationConfig {
2869 start: "/**".into(),
2870 end: "*/".into(),
2871 prefix: "* ".into(),
2872 tab_size: NonZeroU32::new(1).unwrap(),
2873 }),
2874 ..LanguageConfig::default()
2875 },
2876 None,
2877 ));
2878 {
2879 let mut cx = EditorTestContext::new(cx).await;
2880 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2881 cx.set_state(indoc! {"
2882 /**ˇ
2883 "});
2884
2885 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2886 cx.assert_editor_state(indoc! {"
2887 /**
2888 * ˇ
2889 "});
2890 // Ensure that if cursor is before the comment start,
2891 // we do not actually insert a comment prefix.
2892 cx.set_state(indoc! {"
2893 ˇ/**
2894 "});
2895 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2896 cx.assert_editor_state(indoc! {"
2897
2898 ˇ/**
2899 "});
2900 // Ensure that if cursor is between it doesn't add comment prefix.
2901 cx.set_state(indoc! {"
2902 /*ˇ*
2903 "});
2904 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2905 cx.assert_editor_state(indoc! {"
2906 /*
2907 ˇ*
2908 "});
2909 // Ensure that if suffix exists on same line after cursor it adds new line.
2910 cx.set_state(indoc! {"
2911 /**ˇ*/
2912 "});
2913 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2914 cx.assert_editor_state(indoc! {"
2915 /**
2916 * ˇ
2917 */
2918 "});
2919 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2920 cx.set_state(indoc! {"
2921 /**ˇ */
2922 "});
2923 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2924 cx.assert_editor_state(indoc! {"
2925 /**
2926 * ˇ
2927 */
2928 "});
2929 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2930 cx.set_state(indoc! {"
2931 /** ˇ*/
2932 "});
2933 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2934 cx.assert_editor_state(
2935 indoc! {"
2936 /**s
2937 * ˇ
2938 */
2939 "}
2940 .replace("s", " ") // s is used as space placeholder to prevent format on save
2941 .as_str(),
2942 );
2943 // Ensure that delimiter space is preserved when newline on already
2944 // spaced delimiter.
2945 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2946 cx.assert_editor_state(
2947 indoc! {"
2948 /**s
2949 *s
2950 * ˇ
2951 */
2952 "}
2953 .replace("s", " ") // s is used as space placeholder to prevent format on save
2954 .as_str(),
2955 );
2956 // Ensure that delimiter space is preserved when space is not
2957 // on existing delimiter.
2958 cx.set_state(indoc! {"
2959 /**
2960 *ˇ
2961 */
2962 "});
2963 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2964 cx.assert_editor_state(indoc! {"
2965 /**
2966 *
2967 * ˇ
2968 */
2969 "});
2970 // Ensure that if suffix exists on same line after cursor it
2971 // doesn't add extra new line if prefix is not on same line.
2972 cx.set_state(indoc! {"
2973 /**
2974 ˇ*/
2975 "});
2976 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2977 cx.assert_editor_state(indoc! {"
2978 /**
2979
2980 ˇ*/
2981 "});
2982 // Ensure that it detects suffix after existing prefix.
2983 cx.set_state(indoc! {"
2984 /**ˇ/
2985 "});
2986 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2987 cx.assert_editor_state(indoc! {"
2988 /**
2989 ˇ/
2990 "});
2991 // Ensure that if suffix exists on same line before
2992 // cursor it does not add comment prefix.
2993 cx.set_state(indoc! {"
2994 /** */ˇ
2995 "});
2996 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2997 cx.assert_editor_state(indoc! {"
2998 /** */
2999 ˇ
3000 "});
3001 // Ensure that if suffix exists on same line before
3002 // cursor it does not add comment prefix.
3003 cx.set_state(indoc! {"
3004 /**
3005 *
3006 */ˇ
3007 "});
3008 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3009 cx.assert_editor_state(indoc! {"
3010 /**
3011 *
3012 */
3013 ˇ
3014 "});
3015 }
3016 // Ensure that comment continuations can be disabled.
3017 update_test_language_settings(cx, |settings| {
3018 settings.defaults.extend_comment_on_newline = Some(false);
3019 });
3020 let mut cx = EditorTestContext::new(cx).await;
3021 cx.set_state(indoc! {"
3022 /**ˇ
3023 "});
3024 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3025 cx.assert_editor_state(indoc! {"
3026 /**
3027 ˇ
3028 "});
3029}
3030
3031#[gpui::test]
3032fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3033 init_test(cx, |_| {});
3034
3035 let editor = cx.add_window(|window, cx| {
3036 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3037 let mut editor = build_editor(buffer.clone(), window, cx);
3038 editor.change_selections(None, window, cx, |s| {
3039 s.select_ranges([3..4, 11..12, 19..20])
3040 });
3041 editor
3042 });
3043
3044 _ = editor.update(cx, |editor, window, cx| {
3045 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3046 editor.buffer.update(cx, |buffer, cx| {
3047 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3048 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3049 });
3050 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3051
3052 editor.insert("Z", window, cx);
3053 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3054
3055 // The selections are moved after the inserted characters
3056 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3057 });
3058}
3059
3060#[gpui::test]
3061async fn test_tab(cx: &mut TestAppContext) {
3062 init_test(cx, |settings| {
3063 settings.defaults.tab_size = NonZeroU32::new(3)
3064 });
3065
3066 let mut cx = EditorTestContext::new(cx).await;
3067 cx.set_state(indoc! {"
3068 ˇabˇc
3069 ˇ🏀ˇ🏀ˇefg
3070 dˇ
3071 "});
3072 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3073 cx.assert_editor_state(indoc! {"
3074 ˇab ˇc
3075 ˇ🏀 ˇ🏀 ˇefg
3076 d ˇ
3077 "});
3078
3079 cx.set_state(indoc! {"
3080 a
3081 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3082 "});
3083 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3084 cx.assert_editor_state(indoc! {"
3085 a
3086 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3087 "});
3088}
3089
3090#[gpui::test]
3091async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3092 init_test(cx, |_| {});
3093
3094 let mut cx = EditorTestContext::new(cx).await;
3095 let language = Arc::new(
3096 Language::new(
3097 LanguageConfig::default(),
3098 Some(tree_sitter_rust::LANGUAGE.into()),
3099 )
3100 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3101 .unwrap(),
3102 );
3103 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3104
3105 // test when all cursors are not at suggested indent
3106 // then simply move to their suggested indent location
3107 cx.set_state(indoc! {"
3108 const a: B = (
3109 c(
3110 ˇ
3111 ˇ )
3112 );
3113 "});
3114 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3115 cx.assert_editor_state(indoc! {"
3116 const a: B = (
3117 c(
3118 ˇ
3119 ˇ)
3120 );
3121 "});
3122
3123 // test cursor already at suggested indent not moving when
3124 // other cursors are yet to reach their suggested indents
3125 cx.set_state(indoc! {"
3126 ˇ
3127 const a: B = (
3128 c(
3129 d(
3130 ˇ
3131 )
3132 ˇ
3133 ˇ )
3134 );
3135 "});
3136 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3137 cx.assert_editor_state(indoc! {"
3138 ˇ
3139 const a: B = (
3140 c(
3141 d(
3142 ˇ
3143 )
3144 ˇ
3145 ˇ)
3146 );
3147 "});
3148 // test when all cursors are at suggested indent then tab is inserted
3149 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3150 cx.assert_editor_state(indoc! {"
3151 ˇ
3152 const a: B = (
3153 c(
3154 d(
3155 ˇ
3156 )
3157 ˇ
3158 ˇ)
3159 );
3160 "});
3161
3162 // test when current indent is less than suggested indent,
3163 // we adjust line to match suggested indent and move cursor to it
3164 //
3165 // when no other cursor is at word boundary, all of them should move
3166 cx.set_state(indoc! {"
3167 const a: B = (
3168 c(
3169 d(
3170 ˇ
3171 ˇ )
3172 ˇ )
3173 );
3174 "});
3175 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3176 cx.assert_editor_state(indoc! {"
3177 const a: B = (
3178 c(
3179 d(
3180 ˇ
3181 ˇ)
3182 ˇ)
3183 );
3184 "});
3185
3186 // test when current indent is less than suggested indent,
3187 // we adjust line to match suggested indent and move cursor to it
3188 //
3189 // when some other cursor is at word boundary, it should not move
3190 cx.set_state(indoc! {"
3191 const a: B = (
3192 c(
3193 d(
3194 ˇ
3195 ˇ )
3196 ˇ)
3197 );
3198 "});
3199 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3200 cx.assert_editor_state(indoc! {"
3201 const a: B = (
3202 c(
3203 d(
3204 ˇ
3205 ˇ)
3206 ˇ)
3207 );
3208 "});
3209
3210 // test when current indent is more than suggested indent,
3211 // we just move cursor to current indent instead of suggested indent
3212 //
3213 // when no other cursor is at word boundary, all of them should move
3214 cx.set_state(indoc! {"
3215 const a: B = (
3216 c(
3217 d(
3218 ˇ
3219 ˇ )
3220 ˇ )
3221 );
3222 "});
3223 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3224 cx.assert_editor_state(indoc! {"
3225 const a: B = (
3226 c(
3227 d(
3228 ˇ
3229 ˇ)
3230 ˇ)
3231 );
3232 "});
3233 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3234 cx.assert_editor_state(indoc! {"
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 ˇ)
3240 ˇ)
3241 );
3242 "});
3243
3244 // test when current indent is more than suggested indent,
3245 // we just move cursor to current indent instead of suggested indent
3246 //
3247 // when some other cursor is at word boundary, it doesn't move
3248 cx.set_state(indoc! {"
3249 const a: B = (
3250 c(
3251 d(
3252 ˇ
3253 ˇ )
3254 ˇ)
3255 );
3256 "});
3257 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3258 cx.assert_editor_state(indoc! {"
3259 const a: B = (
3260 c(
3261 d(
3262 ˇ
3263 ˇ)
3264 ˇ)
3265 );
3266 "});
3267
3268 // handle auto-indent when there are multiple cursors on the same line
3269 cx.set_state(indoc! {"
3270 const a: B = (
3271 c(
3272 ˇ ˇ
3273 ˇ )
3274 );
3275 "});
3276 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3277 cx.assert_editor_state(indoc! {"
3278 const a: B = (
3279 c(
3280 ˇ
3281 ˇ)
3282 );
3283 "});
3284}
3285
3286#[gpui::test]
3287async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3288 init_test(cx, |settings| {
3289 settings.defaults.tab_size = NonZeroU32::new(3)
3290 });
3291
3292 let mut cx = EditorTestContext::new(cx).await;
3293 cx.set_state(indoc! {"
3294 ˇ
3295 \t ˇ
3296 \t ˇ
3297 \t ˇ
3298 \t \t\t \t \t\t \t\t \t \t ˇ
3299 "});
3300
3301 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3302 cx.assert_editor_state(indoc! {"
3303 ˇ
3304 \t ˇ
3305 \t ˇ
3306 \t ˇ
3307 \t \t\t \t \t\t \t\t \t \t ˇ
3308 "});
3309}
3310
3311#[gpui::test]
3312async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3313 init_test(cx, |settings| {
3314 settings.defaults.tab_size = NonZeroU32::new(4)
3315 });
3316
3317 let language = Arc::new(
3318 Language::new(
3319 LanguageConfig::default(),
3320 Some(tree_sitter_rust::LANGUAGE.into()),
3321 )
3322 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3323 .unwrap(),
3324 );
3325
3326 let mut cx = EditorTestContext::new(cx).await;
3327 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3328 cx.set_state(indoc! {"
3329 fn a() {
3330 if b {
3331 \t ˇc
3332 }
3333 }
3334 "});
3335
3336 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3337 cx.assert_editor_state(indoc! {"
3338 fn a() {
3339 if b {
3340 ˇc
3341 }
3342 }
3343 "});
3344}
3345
3346#[gpui::test]
3347async fn test_indent_outdent(cx: &mut TestAppContext) {
3348 init_test(cx, |settings| {
3349 settings.defaults.tab_size = NonZeroU32::new(4);
3350 });
3351
3352 let mut cx = EditorTestContext::new(cx).await;
3353
3354 cx.set_state(indoc! {"
3355 «oneˇ» «twoˇ»
3356 three
3357 four
3358 "});
3359 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3360 cx.assert_editor_state(indoc! {"
3361 «oneˇ» «twoˇ»
3362 three
3363 four
3364 "});
3365
3366 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3367 cx.assert_editor_state(indoc! {"
3368 «oneˇ» «twoˇ»
3369 three
3370 four
3371 "});
3372
3373 // select across line ending
3374 cx.set_state(indoc! {"
3375 one two
3376 t«hree
3377 ˇ» four
3378 "});
3379 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381 one two
3382 t«hree
3383 ˇ» four
3384 "});
3385
3386 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3387 cx.assert_editor_state(indoc! {"
3388 one two
3389 t«hree
3390 ˇ» four
3391 "});
3392
3393 // Ensure that indenting/outdenting works when the cursor is at column 0.
3394 cx.set_state(indoc! {"
3395 one two
3396 ˇthree
3397 four
3398 "});
3399 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3400 cx.assert_editor_state(indoc! {"
3401 one two
3402 ˇthree
3403 four
3404 "});
3405
3406 cx.set_state(indoc! {"
3407 one two
3408 ˇ three
3409 four
3410 "});
3411 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3412 cx.assert_editor_state(indoc! {"
3413 one two
3414 ˇthree
3415 four
3416 "});
3417}
3418
3419#[gpui::test]
3420async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3421 init_test(cx, |settings| {
3422 settings.defaults.hard_tabs = Some(true);
3423 });
3424
3425 let mut cx = EditorTestContext::new(cx).await;
3426
3427 // select two ranges on one line
3428 cx.set_state(indoc! {"
3429 «oneˇ» «twoˇ»
3430 three
3431 four
3432 "});
3433 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3434 cx.assert_editor_state(indoc! {"
3435 \t«oneˇ» «twoˇ»
3436 three
3437 four
3438 "});
3439 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3440 cx.assert_editor_state(indoc! {"
3441 \t\t«oneˇ» «twoˇ»
3442 three
3443 four
3444 "});
3445 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3446 cx.assert_editor_state(indoc! {"
3447 \t«oneˇ» «twoˇ»
3448 three
3449 four
3450 "});
3451 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3452 cx.assert_editor_state(indoc! {"
3453 «oneˇ» «twoˇ»
3454 three
3455 four
3456 "});
3457
3458 // select across a line ending
3459 cx.set_state(indoc! {"
3460 one two
3461 t«hree
3462 ˇ»four
3463 "});
3464 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3465 cx.assert_editor_state(indoc! {"
3466 one two
3467 \tt«hree
3468 ˇ»four
3469 "});
3470 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3471 cx.assert_editor_state(indoc! {"
3472 one two
3473 \t\tt«hree
3474 ˇ»four
3475 "});
3476 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478 one two
3479 \tt«hree
3480 ˇ»four
3481 "});
3482 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 one two
3485 t«hree
3486 ˇ»four
3487 "});
3488
3489 // Ensure that indenting/outdenting works when the cursor is at column 0.
3490 cx.set_state(indoc! {"
3491 one two
3492 ˇthree
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 one two
3498 ˇthree
3499 four
3500 "});
3501 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3502 cx.assert_editor_state(indoc! {"
3503 one two
3504 \tˇthree
3505 four
3506 "});
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 ˇthree
3511 four
3512 "});
3513}
3514
3515#[gpui::test]
3516fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3517 init_test(cx, |settings| {
3518 settings.languages.extend([
3519 (
3520 "TOML".into(),
3521 LanguageSettingsContent {
3522 tab_size: NonZeroU32::new(2),
3523 ..Default::default()
3524 },
3525 ),
3526 (
3527 "Rust".into(),
3528 LanguageSettingsContent {
3529 tab_size: NonZeroU32::new(4),
3530 ..Default::default()
3531 },
3532 ),
3533 ]);
3534 });
3535
3536 let toml_language = Arc::new(Language::new(
3537 LanguageConfig {
3538 name: "TOML".into(),
3539 ..Default::default()
3540 },
3541 None,
3542 ));
3543 let rust_language = Arc::new(Language::new(
3544 LanguageConfig {
3545 name: "Rust".into(),
3546 ..Default::default()
3547 },
3548 None,
3549 ));
3550
3551 let toml_buffer =
3552 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3553 let rust_buffer =
3554 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3555 let multibuffer = cx.new(|cx| {
3556 let mut multibuffer = MultiBuffer::new(ReadWrite);
3557 multibuffer.push_excerpts(
3558 toml_buffer.clone(),
3559 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3560 cx,
3561 );
3562 multibuffer.push_excerpts(
3563 rust_buffer.clone(),
3564 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3565 cx,
3566 );
3567 multibuffer
3568 });
3569
3570 cx.add_window(|window, cx| {
3571 let mut editor = build_editor(multibuffer, window, cx);
3572
3573 assert_eq!(
3574 editor.text(cx),
3575 indoc! {"
3576 a = 1
3577 b = 2
3578
3579 const c: usize = 3;
3580 "}
3581 );
3582
3583 select_ranges(
3584 &mut editor,
3585 indoc! {"
3586 «aˇ» = 1
3587 b = 2
3588
3589 «const c:ˇ» usize = 3;
3590 "},
3591 window,
3592 cx,
3593 );
3594
3595 editor.tab(&Tab, window, cx);
3596 assert_text_with_selections(
3597 &mut editor,
3598 indoc! {"
3599 «aˇ» = 1
3600 b = 2
3601
3602 «const c:ˇ» usize = 3;
3603 "},
3604 cx,
3605 );
3606 editor.backtab(&Backtab, window, cx);
3607 assert_text_with_selections(
3608 &mut editor,
3609 indoc! {"
3610 «aˇ» = 1
3611 b = 2
3612
3613 «const c:ˇ» usize = 3;
3614 "},
3615 cx,
3616 );
3617
3618 editor
3619 });
3620}
3621
3622#[gpui::test]
3623async fn test_backspace(cx: &mut TestAppContext) {
3624 init_test(cx, |_| {});
3625
3626 let mut cx = EditorTestContext::new(cx).await;
3627
3628 // Basic backspace
3629 cx.set_state(indoc! {"
3630 onˇe two three
3631 fou«rˇ» five six
3632 seven «ˇeight nine
3633 »ten
3634 "});
3635 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3636 cx.assert_editor_state(indoc! {"
3637 oˇe two three
3638 fouˇ five six
3639 seven ˇten
3640 "});
3641
3642 // Test backspace inside and around indents
3643 cx.set_state(indoc! {"
3644 zero
3645 ˇone
3646 ˇtwo
3647 ˇ ˇ ˇ three
3648 ˇ ˇ four
3649 "});
3650 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 zero
3653 ˇone
3654 ˇtwo
3655 ˇ threeˇ four
3656 "});
3657}
3658
3659#[gpui::test]
3660async fn test_delete(cx: &mut TestAppContext) {
3661 init_test(cx, |_| {});
3662
3663 let mut cx = EditorTestContext::new(cx).await;
3664 cx.set_state(indoc! {"
3665 onˇe two three
3666 fou«rˇ» five six
3667 seven «ˇeight nine
3668 »ten
3669 "});
3670 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3671 cx.assert_editor_state(indoc! {"
3672 onˇ two three
3673 fouˇ five six
3674 seven ˇten
3675 "});
3676}
3677
3678#[gpui::test]
3679fn test_delete_line(cx: &mut TestAppContext) {
3680 init_test(cx, |_| {});
3681
3682 let editor = cx.add_window(|window, cx| {
3683 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3684 build_editor(buffer, window, cx)
3685 });
3686 _ = editor.update(cx, |editor, window, cx| {
3687 editor.change_selections(None, window, cx, |s| {
3688 s.select_display_ranges([
3689 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3691 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3692 ])
3693 });
3694 editor.delete_line(&DeleteLine, window, cx);
3695 assert_eq!(editor.display_text(cx), "ghi");
3696 assert_eq!(
3697 editor.selections.display_ranges(cx),
3698 vec![
3699 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3700 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3701 ]
3702 );
3703 });
3704
3705 let editor = cx.add_window(|window, cx| {
3706 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3707 build_editor(buffer, window, cx)
3708 });
3709 _ = editor.update(cx, |editor, window, cx| {
3710 editor.change_selections(None, window, cx, |s| {
3711 s.select_display_ranges([
3712 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3713 ])
3714 });
3715 editor.delete_line(&DeleteLine, window, cx);
3716 assert_eq!(editor.display_text(cx), "ghi\n");
3717 assert_eq!(
3718 editor.selections.display_ranges(cx),
3719 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3720 );
3721 });
3722}
3723
3724#[gpui::test]
3725fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3726 init_test(cx, |_| {});
3727
3728 cx.add_window(|window, cx| {
3729 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3730 let mut editor = build_editor(buffer.clone(), window, cx);
3731 let buffer = buffer.read(cx).as_singleton().unwrap();
3732
3733 assert_eq!(
3734 editor.selections.ranges::<Point>(cx),
3735 &[Point::new(0, 0)..Point::new(0, 0)]
3736 );
3737
3738 // When on single line, replace newline at end by space
3739 editor.join_lines(&JoinLines, window, cx);
3740 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3741 assert_eq!(
3742 editor.selections.ranges::<Point>(cx),
3743 &[Point::new(0, 3)..Point::new(0, 3)]
3744 );
3745
3746 // When multiple lines are selected, remove newlines that are spanned by the selection
3747 editor.change_selections(None, window, cx, |s| {
3748 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3749 });
3750 editor.join_lines(&JoinLines, window, cx);
3751 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3752 assert_eq!(
3753 editor.selections.ranges::<Point>(cx),
3754 &[Point::new(0, 11)..Point::new(0, 11)]
3755 );
3756
3757 // Undo should be transactional
3758 editor.undo(&Undo, window, cx);
3759 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3760 assert_eq!(
3761 editor.selections.ranges::<Point>(cx),
3762 &[Point::new(0, 5)..Point::new(2, 2)]
3763 );
3764
3765 // When joining an empty line don't insert a space
3766 editor.change_selections(None, window, cx, |s| {
3767 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3768 });
3769 editor.join_lines(&JoinLines, window, cx);
3770 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3771 assert_eq!(
3772 editor.selections.ranges::<Point>(cx),
3773 [Point::new(2, 3)..Point::new(2, 3)]
3774 );
3775
3776 // We can remove trailing newlines
3777 editor.join_lines(&JoinLines, window, cx);
3778 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3779 assert_eq!(
3780 editor.selections.ranges::<Point>(cx),
3781 [Point::new(2, 3)..Point::new(2, 3)]
3782 );
3783
3784 // We don't blow up on the last line
3785 editor.join_lines(&JoinLines, window, cx);
3786 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3787 assert_eq!(
3788 editor.selections.ranges::<Point>(cx),
3789 [Point::new(2, 3)..Point::new(2, 3)]
3790 );
3791
3792 // reset to test indentation
3793 editor.buffer.update(cx, |buffer, cx| {
3794 buffer.edit(
3795 [
3796 (Point::new(1, 0)..Point::new(1, 2), " "),
3797 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3798 ],
3799 None,
3800 cx,
3801 )
3802 });
3803
3804 // We remove any leading spaces
3805 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3806 editor.change_selections(None, window, cx, |s| {
3807 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3808 });
3809 editor.join_lines(&JoinLines, window, cx);
3810 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3811
3812 // We don't insert a space for a line containing only spaces
3813 editor.join_lines(&JoinLines, window, cx);
3814 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3815
3816 // We ignore any leading tabs
3817 editor.join_lines(&JoinLines, window, cx);
3818 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3819
3820 editor
3821 });
3822}
3823
3824#[gpui::test]
3825fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3826 init_test(cx, |_| {});
3827
3828 cx.add_window(|window, cx| {
3829 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3830 let mut editor = build_editor(buffer.clone(), window, cx);
3831 let buffer = buffer.read(cx).as_singleton().unwrap();
3832
3833 editor.change_selections(None, window, cx, |s| {
3834 s.select_ranges([
3835 Point::new(0, 2)..Point::new(1, 1),
3836 Point::new(1, 2)..Point::new(1, 2),
3837 Point::new(3, 1)..Point::new(3, 2),
3838 ])
3839 });
3840
3841 editor.join_lines(&JoinLines, window, cx);
3842 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3843
3844 assert_eq!(
3845 editor.selections.ranges::<Point>(cx),
3846 [
3847 Point::new(0, 7)..Point::new(0, 7),
3848 Point::new(1, 3)..Point::new(1, 3)
3849 ]
3850 );
3851 editor
3852 });
3853}
3854
3855#[gpui::test]
3856async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3857 init_test(cx, |_| {});
3858
3859 let mut cx = EditorTestContext::new(cx).await;
3860
3861 let diff_base = r#"
3862 Line 0
3863 Line 1
3864 Line 2
3865 Line 3
3866 "#
3867 .unindent();
3868
3869 cx.set_state(
3870 &r#"
3871 ˇLine 0
3872 Line 1
3873 Line 2
3874 Line 3
3875 "#
3876 .unindent(),
3877 );
3878
3879 cx.set_head_text(&diff_base);
3880 executor.run_until_parked();
3881
3882 // Join lines
3883 cx.update_editor(|editor, window, cx| {
3884 editor.join_lines(&JoinLines, window, cx);
3885 });
3886 executor.run_until_parked();
3887
3888 cx.assert_editor_state(
3889 &r#"
3890 Line 0ˇ Line 1
3891 Line 2
3892 Line 3
3893 "#
3894 .unindent(),
3895 );
3896 // Join again
3897 cx.update_editor(|editor, window, cx| {
3898 editor.join_lines(&JoinLines, window, cx);
3899 });
3900 executor.run_until_parked();
3901
3902 cx.assert_editor_state(
3903 &r#"
3904 Line 0 Line 1ˇ Line 2
3905 Line 3
3906 "#
3907 .unindent(),
3908 );
3909}
3910
3911#[gpui::test]
3912async fn test_custom_newlines_cause_no_false_positive_diffs(
3913 executor: BackgroundExecutor,
3914 cx: &mut TestAppContext,
3915) {
3916 init_test(cx, |_| {});
3917 let mut cx = EditorTestContext::new(cx).await;
3918 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3919 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3920 executor.run_until_parked();
3921
3922 cx.update_editor(|editor, window, cx| {
3923 let snapshot = editor.snapshot(window, cx);
3924 assert_eq!(
3925 snapshot
3926 .buffer_snapshot
3927 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3928 .collect::<Vec<_>>(),
3929 Vec::new(),
3930 "Should not have any diffs for files with custom newlines"
3931 );
3932 });
3933}
3934
3935#[gpui::test]
3936async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3937 init_test(cx, |_| {});
3938
3939 let mut cx = EditorTestContext::new(cx).await;
3940
3941 // Test sort_lines_case_insensitive()
3942 cx.set_state(indoc! {"
3943 «z
3944 y
3945 x
3946 Z
3947 Y
3948 Xˇ»
3949 "});
3950 cx.update_editor(|e, window, cx| {
3951 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3952 });
3953 cx.assert_editor_state(indoc! {"
3954 «x
3955 X
3956 y
3957 Y
3958 z
3959 Zˇ»
3960 "});
3961
3962 // Test reverse_lines()
3963 cx.set_state(indoc! {"
3964 «5
3965 4
3966 3
3967 2
3968 1ˇ»
3969 "});
3970 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3971 cx.assert_editor_state(indoc! {"
3972 «1
3973 2
3974 3
3975 4
3976 5ˇ»
3977 "});
3978
3979 // Skip testing shuffle_line()
3980
3981 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3982 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3983
3984 // Don't manipulate when cursor is on single line, but expand the selection
3985 cx.set_state(indoc! {"
3986 ddˇdd
3987 ccc
3988 bb
3989 a
3990 "});
3991 cx.update_editor(|e, window, cx| {
3992 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3993 });
3994 cx.assert_editor_state(indoc! {"
3995 «ddddˇ»
3996 ccc
3997 bb
3998 a
3999 "});
4000
4001 // Basic manipulate case
4002 // Start selection moves to column 0
4003 // End of selection shrinks to fit shorter line
4004 cx.set_state(indoc! {"
4005 dd«d
4006 ccc
4007 bb
4008 aaaaaˇ»
4009 "});
4010 cx.update_editor(|e, window, cx| {
4011 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4012 });
4013 cx.assert_editor_state(indoc! {"
4014 «aaaaa
4015 bb
4016 ccc
4017 dddˇ»
4018 "});
4019
4020 // Manipulate case with newlines
4021 cx.set_state(indoc! {"
4022 dd«d
4023 ccc
4024
4025 bb
4026 aaaaa
4027
4028 ˇ»
4029 "});
4030 cx.update_editor(|e, window, cx| {
4031 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4032 });
4033 cx.assert_editor_state(indoc! {"
4034 «
4035
4036 aaaaa
4037 bb
4038 ccc
4039 dddˇ»
4040
4041 "});
4042
4043 // Adding new line
4044 cx.set_state(indoc! {"
4045 aa«a
4046 bbˇ»b
4047 "});
4048 cx.update_editor(|e, window, cx| {
4049 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4050 });
4051 cx.assert_editor_state(indoc! {"
4052 «aaa
4053 bbb
4054 added_lineˇ»
4055 "});
4056
4057 // Removing line
4058 cx.set_state(indoc! {"
4059 aa«a
4060 bbbˇ»
4061 "});
4062 cx.update_editor(|e, window, cx| {
4063 e.manipulate_lines(window, cx, |lines| {
4064 lines.pop();
4065 })
4066 });
4067 cx.assert_editor_state(indoc! {"
4068 «aaaˇ»
4069 "});
4070
4071 // Removing all lines
4072 cx.set_state(indoc! {"
4073 aa«a
4074 bbbˇ»
4075 "});
4076 cx.update_editor(|e, window, cx| {
4077 e.manipulate_lines(window, cx, |lines| {
4078 lines.drain(..);
4079 })
4080 });
4081 cx.assert_editor_state(indoc! {"
4082 ˇ
4083 "});
4084}
4085
4086#[gpui::test]
4087async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4088 init_test(cx, |_| {});
4089
4090 let mut cx = EditorTestContext::new(cx).await;
4091
4092 // Consider continuous selection as single selection
4093 cx.set_state(indoc! {"
4094 Aaa«aa
4095 cˇ»c«c
4096 bb
4097 aaaˇ»aa
4098 "});
4099 cx.update_editor(|e, window, cx| {
4100 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4101 });
4102 cx.assert_editor_state(indoc! {"
4103 «Aaaaa
4104 ccc
4105 bb
4106 aaaaaˇ»
4107 "});
4108
4109 cx.set_state(indoc! {"
4110 Aaa«aa
4111 cˇ»c«c
4112 bb
4113 aaaˇ»aa
4114 "});
4115 cx.update_editor(|e, window, cx| {
4116 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4117 });
4118 cx.assert_editor_state(indoc! {"
4119 «Aaaaa
4120 ccc
4121 bbˇ»
4122 "});
4123
4124 // Consider non continuous selection as distinct dedup operations
4125 cx.set_state(indoc! {"
4126 «aaaaa
4127 bb
4128 aaaaa
4129 aaaaaˇ»
4130
4131 aaa«aaˇ»
4132 "});
4133 cx.update_editor(|e, window, cx| {
4134 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4135 });
4136 cx.assert_editor_state(indoc! {"
4137 «aaaaa
4138 bbˇ»
4139
4140 «aaaaaˇ»
4141 "});
4142}
4143
4144#[gpui::test]
4145async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4146 init_test(cx, |_| {});
4147
4148 let mut cx = EditorTestContext::new(cx).await;
4149
4150 cx.set_state(indoc! {"
4151 «Aaa
4152 aAa
4153 Aaaˇ»
4154 "});
4155 cx.update_editor(|e, window, cx| {
4156 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4157 });
4158 cx.assert_editor_state(indoc! {"
4159 «Aaa
4160 aAaˇ»
4161 "});
4162
4163 cx.set_state(indoc! {"
4164 «Aaa
4165 aAa
4166 aaAˇ»
4167 "});
4168 cx.update_editor(|e, window, cx| {
4169 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4170 });
4171 cx.assert_editor_state(indoc! {"
4172 «Aaaˇ»
4173 "});
4174}
4175
4176#[gpui::test]
4177async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4178 init_test(cx, |_| {});
4179
4180 let mut cx = EditorTestContext::new(cx).await;
4181
4182 // Manipulate with multiple selections on a single line
4183 cx.set_state(indoc! {"
4184 dd«dd
4185 cˇ»c«c
4186 bb
4187 aaaˇ»aa
4188 "});
4189 cx.update_editor(|e, window, cx| {
4190 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4191 });
4192 cx.assert_editor_state(indoc! {"
4193 «aaaaa
4194 bb
4195 ccc
4196 ddddˇ»
4197 "});
4198
4199 // Manipulate with multiple disjoin selections
4200 cx.set_state(indoc! {"
4201 5«
4202 4
4203 3
4204 2
4205 1ˇ»
4206
4207 dd«dd
4208 ccc
4209 bb
4210 aaaˇ»aa
4211 "});
4212 cx.update_editor(|e, window, cx| {
4213 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4214 });
4215 cx.assert_editor_state(indoc! {"
4216 «1
4217 2
4218 3
4219 4
4220 5ˇ»
4221
4222 «aaaaa
4223 bb
4224 ccc
4225 ddddˇ»
4226 "});
4227
4228 // Adding lines on each selection
4229 cx.set_state(indoc! {"
4230 2«
4231 1ˇ»
4232
4233 bb«bb
4234 aaaˇ»aa
4235 "});
4236 cx.update_editor(|e, window, cx| {
4237 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4238 });
4239 cx.assert_editor_state(indoc! {"
4240 «2
4241 1
4242 added lineˇ»
4243
4244 «bbbb
4245 aaaaa
4246 added lineˇ»
4247 "});
4248
4249 // Removing lines on each selection
4250 cx.set_state(indoc! {"
4251 2«
4252 1ˇ»
4253
4254 bb«bb
4255 aaaˇ»aa
4256 "});
4257 cx.update_editor(|e, window, cx| {
4258 e.manipulate_lines(window, cx, |lines| {
4259 lines.pop();
4260 })
4261 });
4262 cx.assert_editor_state(indoc! {"
4263 «2ˇ»
4264
4265 «bbbbˇ»
4266 "});
4267}
4268
4269#[gpui::test]
4270async fn test_toggle_case(cx: &mut TestAppContext) {
4271 init_test(cx, |_| {});
4272
4273 let mut cx = EditorTestContext::new(cx).await;
4274
4275 // If all lower case -> upper case
4276 cx.set_state(indoc! {"
4277 «hello worldˇ»
4278 "});
4279 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4280 cx.assert_editor_state(indoc! {"
4281 «HELLO WORLDˇ»
4282 "});
4283
4284 // If all upper case -> lower case
4285 cx.set_state(indoc! {"
4286 «HELLO WORLDˇ»
4287 "});
4288 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4289 cx.assert_editor_state(indoc! {"
4290 «hello worldˇ»
4291 "});
4292
4293 // If any upper case characters are identified -> lower case
4294 // This matches JetBrains IDEs
4295 cx.set_state(indoc! {"
4296 «hEllo worldˇ»
4297 "});
4298 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4299 cx.assert_editor_state(indoc! {"
4300 «hello worldˇ»
4301 "});
4302}
4303
4304#[gpui::test]
4305async fn test_manipulate_text(cx: &mut TestAppContext) {
4306 init_test(cx, |_| {});
4307
4308 let mut cx = EditorTestContext::new(cx).await;
4309
4310 // Test convert_to_upper_case()
4311 cx.set_state(indoc! {"
4312 «hello worldˇ»
4313 "});
4314 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4315 cx.assert_editor_state(indoc! {"
4316 «HELLO WORLDˇ»
4317 "});
4318
4319 // Test convert_to_lower_case()
4320 cx.set_state(indoc! {"
4321 «HELLO WORLDˇ»
4322 "});
4323 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4324 cx.assert_editor_state(indoc! {"
4325 «hello worldˇ»
4326 "});
4327
4328 // Test multiple line, single selection case
4329 cx.set_state(indoc! {"
4330 «The quick brown
4331 fox jumps over
4332 the lazy dogˇ»
4333 "});
4334 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4335 cx.assert_editor_state(indoc! {"
4336 «The Quick Brown
4337 Fox Jumps Over
4338 The Lazy Dogˇ»
4339 "});
4340
4341 // Test multiple line, single selection case
4342 cx.set_state(indoc! {"
4343 «The quick brown
4344 fox jumps over
4345 the lazy dogˇ»
4346 "});
4347 cx.update_editor(|e, window, cx| {
4348 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4349 });
4350 cx.assert_editor_state(indoc! {"
4351 «TheQuickBrown
4352 FoxJumpsOver
4353 TheLazyDogˇ»
4354 "});
4355
4356 // From here on out, test more complex cases of manipulate_text()
4357
4358 // Test no selection case - should affect words cursors are in
4359 // Cursor at beginning, middle, and end of word
4360 cx.set_state(indoc! {"
4361 ˇhello big beauˇtiful worldˇ
4362 "});
4363 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4364 cx.assert_editor_state(indoc! {"
4365 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4366 "});
4367
4368 // Test multiple selections on a single line and across multiple lines
4369 cx.set_state(indoc! {"
4370 «Theˇ» quick «brown
4371 foxˇ» jumps «overˇ»
4372 the «lazyˇ» dog
4373 "});
4374 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4375 cx.assert_editor_state(indoc! {"
4376 «THEˇ» quick «BROWN
4377 FOXˇ» jumps «OVERˇ»
4378 the «LAZYˇ» dog
4379 "});
4380
4381 // Test case where text length grows
4382 cx.set_state(indoc! {"
4383 «tschüߡ»
4384 "});
4385 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4386 cx.assert_editor_state(indoc! {"
4387 «TSCHÜSSˇ»
4388 "});
4389
4390 // Test to make sure we don't crash when text shrinks
4391 cx.set_state(indoc! {"
4392 aaa_bbbˇ
4393 "});
4394 cx.update_editor(|e, window, cx| {
4395 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4396 });
4397 cx.assert_editor_state(indoc! {"
4398 «aaaBbbˇ»
4399 "});
4400
4401 // Test to make sure we all aware of the fact that each word can grow and shrink
4402 // Final selections should be aware of this fact
4403 cx.set_state(indoc! {"
4404 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4405 "});
4406 cx.update_editor(|e, window, cx| {
4407 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4408 });
4409 cx.assert_editor_state(indoc! {"
4410 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4411 "});
4412
4413 cx.set_state(indoc! {"
4414 «hElLo, WoRld!ˇ»
4415 "});
4416 cx.update_editor(|e, window, cx| {
4417 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4418 });
4419 cx.assert_editor_state(indoc! {"
4420 «HeLlO, wOrLD!ˇ»
4421 "});
4422}
4423
4424#[gpui::test]
4425fn test_duplicate_line(cx: &mut TestAppContext) {
4426 init_test(cx, |_| {});
4427
4428 let editor = cx.add_window(|window, cx| {
4429 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4430 build_editor(buffer, window, cx)
4431 });
4432 _ = editor.update(cx, |editor, window, cx| {
4433 editor.change_selections(None, window, cx, |s| {
4434 s.select_display_ranges([
4435 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4436 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4437 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4438 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4439 ])
4440 });
4441 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4442 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4443 assert_eq!(
4444 editor.selections.display_ranges(cx),
4445 vec![
4446 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4447 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4448 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4449 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4450 ]
4451 );
4452 });
4453
4454 let editor = cx.add_window(|window, cx| {
4455 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4456 build_editor(buffer, window, cx)
4457 });
4458 _ = editor.update(cx, |editor, window, cx| {
4459 editor.change_selections(None, window, cx, |s| {
4460 s.select_display_ranges([
4461 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4462 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4463 ])
4464 });
4465 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4466 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4467 assert_eq!(
4468 editor.selections.display_ranges(cx),
4469 vec![
4470 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4471 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4472 ]
4473 );
4474 });
4475
4476 // With `move_upwards` the selections stay in place, except for
4477 // the lines inserted above them
4478 let editor = cx.add_window(|window, cx| {
4479 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4480 build_editor(buffer, window, cx)
4481 });
4482 _ = editor.update(cx, |editor, window, cx| {
4483 editor.change_selections(None, window, cx, |s| {
4484 s.select_display_ranges([
4485 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4486 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4487 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4488 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4489 ])
4490 });
4491 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4492 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4493 assert_eq!(
4494 editor.selections.display_ranges(cx),
4495 vec![
4496 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4497 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4498 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4499 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4500 ]
4501 );
4502 });
4503
4504 let editor = cx.add_window(|window, cx| {
4505 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4506 build_editor(buffer, window, cx)
4507 });
4508 _ = editor.update(cx, |editor, window, cx| {
4509 editor.change_selections(None, window, cx, |s| {
4510 s.select_display_ranges([
4511 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4512 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4513 ])
4514 });
4515 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4516 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4517 assert_eq!(
4518 editor.selections.display_ranges(cx),
4519 vec![
4520 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4521 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4522 ]
4523 );
4524 });
4525
4526 let editor = cx.add_window(|window, cx| {
4527 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4528 build_editor(buffer, window, cx)
4529 });
4530 _ = editor.update(cx, |editor, window, cx| {
4531 editor.change_selections(None, window, cx, |s| {
4532 s.select_display_ranges([
4533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4534 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4535 ])
4536 });
4537 editor.duplicate_selection(&DuplicateSelection, window, cx);
4538 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4539 assert_eq!(
4540 editor.selections.display_ranges(cx),
4541 vec![
4542 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4543 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4544 ]
4545 );
4546 });
4547}
4548
4549#[gpui::test]
4550fn test_move_line_up_down(cx: &mut TestAppContext) {
4551 init_test(cx, |_| {});
4552
4553 let editor = cx.add_window(|window, cx| {
4554 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4555 build_editor(buffer, window, cx)
4556 });
4557 _ = editor.update(cx, |editor, window, cx| {
4558 editor.fold_creases(
4559 vec![
4560 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4561 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4562 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4563 ],
4564 true,
4565 window,
4566 cx,
4567 );
4568 editor.change_selections(None, window, cx, |s| {
4569 s.select_display_ranges([
4570 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4571 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4572 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4573 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4574 ])
4575 });
4576 assert_eq!(
4577 editor.display_text(cx),
4578 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4579 );
4580
4581 editor.move_line_up(&MoveLineUp, window, cx);
4582 assert_eq!(
4583 editor.display_text(cx),
4584 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4585 );
4586 assert_eq!(
4587 editor.selections.display_ranges(cx),
4588 vec![
4589 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4590 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4591 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4592 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4593 ]
4594 );
4595 });
4596
4597 _ = editor.update(cx, |editor, window, cx| {
4598 editor.move_line_down(&MoveLineDown, window, cx);
4599 assert_eq!(
4600 editor.display_text(cx),
4601 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4602 );
4603 assert_eq!(
4604 editor.selections.display_ranges(cx),
4605 vec![
4606 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4607 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4608 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4609 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4610 ]
4611 );
4612 });
4613
4614 _ = editor.update(cx, |editor, window, cx| {
4615 editor.move_line_down(&MoveLineDown, window, cx);
4616 assert_eq!(
4617 editor.display_text(cx),
4618 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4619 );
4620 assert_eq!(
4621 editor.selections.display_ranges(cx),
4622 vec![
4623 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4624 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4625 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4626 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4627 ]
4628 );
4629 });
4630
4631 _ = editor.update(cx, |editor, window, cx| {
4632 editor.move_line_up(&MoveLineUp, window, cx);
4633 assert_eq!(
4634 editor.display_text(cx),
4635 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4636 );
4637 assert_eq!(
4638 editor.selections.display_ranges(cx),
4639 vec![
4640 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4641 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4642 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4643 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4644 ]
4645 );
4646 });
4647}
4648
4649#[gpui::test]
4650fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4651 init_test(cx, |_| {});
4652
4653 let editor = cx.add_window(|window, cx| {
4654 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4655 build_editor(buffer, window, cx)
4656 });
4657 _ = editor.update(cx, |editor, window, cx| {
4658 let snapshot = editor.buffer.read(cx).snapshot(cx);
4659 editor.insert_blocks(
4660 [BlockProperties {
4661 style: BlockStyle::Fixed,
4662 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4663 height: Some(1),
4664 render: Arc::new(|_| div().into_any()),
4665 priority: 0,
4666 render_in_minimap: true,
4667 }],
4668 Some(Autoscroll::fit()),
4669 cx,
4670 );
4671 editor.change_selections(None, window, cx, |s| {
4672 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4673 });
4674 editor.move_line_down(&MoveLineDown, window, cx);
4675 });
4676}
4677
4678#[gpui::test]
4679async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4680 init_test(cx, |_| {});
4681
4682 let mut cx = EditorTestContext::new(cx).await;
4683 cx.set_state(
4684 &"
4685 ˇzero
4686 one
4687 two
4688 three
4689 four
4690 five
4691 "
4692 .unindent(),
4693 );
4694
4695 // Create a four-line block that replaces three lines of text.
4696 cx.update_editor(|editor, window, cx| {
4697 let snapshot = editor.snapshot(window, cx);
4698 let snapshot = &snapshot.buffer_snapshot;
4699 let placement = BlockPlacement::Replace(
4700 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4701 );
4702 editor.insert_blocks(
4703 [BlockProperties {
4704 placement,
4705 height: Some(4),
4706 style: BlockStyle::Sticky,
4707 render: Arc::new(|_| gpui::div().into_any_element()),
4708 priority: 0,
4709 render_in_minimap: true,
4710 }],
4711 None,
4712 cx,
4713 );
4714 });
4715
4716 // Move down so that the cursor touches the block.
4717 cx.update_editor(|editor, window, cx| {
4718 editor.move_down(&Default::default(), window, cx);
4719 });
4720 cx.assert_editor_state(
4721 &"
4722 zero
4723 «one
4724 two
4725 threeˇ»
4726 four
4727 five
4728 "
4729 .unindent(),
4730 );
4731
4732 // Move down past the block.
4733 cx.update_editor(|editor, window, cx| {
4734 editor.move_down(&Default::default(), window, cx);
4735 });
4736 cx.assert_editor_state(
4737 &"
4738 zero
4739 one
4740 two
4741 three
4742 ˇfour
4743 five
4744 "
4745 .unindent(),
4746 );
4747}
4748
4749#[gpui::test]
4750fn test_transpose(cx: &mut TestAppContext) {
4751 init_test(cx, |_| {});
4752
4753 _ = cx.add_window(|window, cx| {
4754 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4755 editor.set_style(EditorStyle::default(), window, cx);
4756 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4757 editor.transpose(&Default::default(), window, cx);
4758 assert_eq!(editor.text(cx), "bac");
4759 assert_eq!(editor.selections.ranges(cx), [2..2]);
4760
4761 editor.transpose(&Default::default(), window, cx);
4762 assert_eq!(editor.text(cx), "bca");
4763 assert_eq!(editor.selections.ranges(cx), [3..3]);
4764
4765 editor.transpose(&Default::default(), window, cx);
4766 assert_eq!(editor.text(cx), "bac");
4767 assert_eq!(editor.selections.ranges(cx), [3..3]);
4768
4769 editor
4770 });
4771
4772 _ = cx.add_window(|window, cx| {
4773 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4774 editor.set_style(EditorStyle::default(), window, cx);
4775 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4776 editor.transpose(&Default::default(), window, cx);
4777 assert_eq!(editor.text(cx), "acb\nde");
4778 assert_eq!(editor.selections.ranges(cx), [3..3]);
4779
4780 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4781 editor.transpose(&Default::default(), window, cx);
4782 assert_eq!(editor.text(cx), "acbd\ne");
4783 assert_eq!(editor.selections.ranges(cx), [5..5]);
4784
4785 editor.transpose(&Default::default(), window, cx);
4786 assert_eq!(editor.text(cx), "acbde\n");
4787 assert_eq!(editor.selections.ranges(cx), [6..6]);
4788
4789 editor.transpose(&Default::default(), window, cx);
4790 assert_eq!(editor.text(cx), "acbd\ne");
4791 assert_eq!(editor.selections.ranges(cx), [6..6]);
4792
4793 editor
4794 });
4795
4796 _ = cx.add_window(|window, cx| {
4797 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4798 editor.set_style(EditorStyle::default(), window, cx);
4799 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4800 editor.transpose(&Default::default(), window, cx);
4801 assert_eq!(editor.text(cx), "bacd\ne");
4802 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4803
4804 editor.transpose(&Default::default(), window, cx);
4805 assert_eq!(editor.text(cx), "bcade\n");
4806 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4807
4808 editor.transpose(&Default::default(), window, cx);
4809 assert_eq!(editor.text(cx), "bcda\ne");
4810 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4811
4812 editor.transpose(&Default::default(), window, cx);
4813 assert_eq!(editor.text(cx), "bcade\n");
4814 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4815
4816 editor.transpose(&Default::default(), window, cx);
4817 assert_eq!(editor.text(cx), "bcaed\n");
4818 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4819
4820 editor
4821 });
4822
4823 _ = cx.add_window(|window, cx| {
4824 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4825 editor.set_style(EditorStyle::default(), window, cx);
4826 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4827 editor.transpose(&Default::default(), window, cx);
4828 assert_eq!(editor.text(cx), "🏀🍐✋");
4829 assert_eq!(editor.selections.ranges(cx), [8..8]);
4830
4831 editor.transpose(&Default::default(), window, cx);
4832 assert_eq!(editor.text(cx), "🏀✋🍐");
4833 assert_eq!(editor.selections.ranges(cx), [11..11]);
4834
4835 editor.transpose(&Default::default(), window, cx);
4836 assert_eq!(editor.text(cx), "🏀🍐✋");
4837 assert_eq!(editor.selections.ranges(cx), [11..11]);
4838
4839 editor
4840 });
4841}
4842
4843#[gpui::test]
4844async fn test_rewrap(cx: &mut TestAppContext) {
4845 init_test(cx, |settings| {
4846 settings.languages.extend([
4847 (
4848 "Markdown".into(),
4849 LanguageSettingsContent {
4850 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4851 ..Default::default()
4852 },
4853 ),
4854 (
4855 "Plain Text".into(),
4856 LanguageSettingsContent {
4857 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4858 ..Default::default()
4859 },
4860 ),
4861 ])
4862 });
4863
4864 let mut cx = EditorTestContext::new(cx).await;
4865
4866 let language_with_c_comments = Arc::new(Language::new(
4867 LanguageConfig {
4868 line_comments: vec!["// ".into()],
4869 ..LanguageConfig::default()
4870 },
4871 None,
4872 ));
4873 let language_with_pound_comments = Arc::new(Language::new(
4874 LanguageConfig {
4875 line_comments: vec!["# ".into()],
4876 ..LanguageConfig::default()
4877 },
4878 None,
4879 ));
4880 let markdown_language = Arc::new(Language::new(
4881 LanguageConfig {
4882 name: "Markdown".into(),
4883 ..LanguageConfig::default()
4884 },
4885 None,
4886 ));
4887 let language_with_doc_comments = Arc::new(Language::new(
4888 LanguageConfig {
4889 line_comments: vec!["// ".into(), "/// ".into()],
4890 ..LanguageConfig::default()
4891 },
4892 Some(tree_sitter_rust::LANGUAGE.into()),
4893 ));
4894
4895 let plaintext_language = Arc::new(Language::new(
4896 LanguageConfig {
4897 name: "Plain Text".into(),
4898 ..LanguageConfig::default()
4899 },
4900 None,
4901 ));
4902
4903 assert_rewrap(
4904 indoc! {"
4905 // ˇ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.
4906 "},
4907 indoc! {"
4908 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4909 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4910 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4911 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4912 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4913 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4914 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4915 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4916 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4917 // porttitor id. Aliquam id accumsan eros.
4918 "},
4919 language_with_c_comments.clone(),
4920 &mut cx,
4921 );
4922
4923 // Test that rewrapping works inside of a selection
4924 assert_rewrap(
4925 indoc! {"
4926 «// 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.ˇ»
4927 "},
4928 indoc! {"
4929 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4930 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4931 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4932 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4933 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4934 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4935 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4936 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4937 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4938 // porttitor id. Aliquam id accumsan eros.ˇ»
4939 "},
4940 language_with_c_comments.clone(),
4941 &mut cx,
4942 );
4943
4944 // Test that cursors that expand to the same region are collapsed.
4945 assert_rewrap(
4946 indoc! {"
4947 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4948 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4949 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4950 // ˇ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.
4951 "},
4952 indoc! {"
4953 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4954 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4955 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4956 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4957 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4958 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4959 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4960 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4961 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4962 // porttitor id. Aliquam id accumsan eros.
4963 "},
4964 language_with_c_comments.clone(),
4965 &mut cx,
4966 );
4967
4968 // Test that non-contiguous selections are treated separately.
4969 assert_rewrap(
4970 indoc! {"
4971 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4972 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4973 //
4974 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4975 // ˇ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.
4976 "},
4977 indoc! {"
4978 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4979 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4980 // auctor, eu lacinia sapien scelerisque.
4981 //
4982 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4983 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4984 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4985 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4986 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4987 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4988 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4989 "},
4990 language_with_c_comments.clone(),
4991 &mut cx,
4992 );
4993
4994 // Test that different comment prefixes are supported.
4995 assert_rewrap(
4996 indoc! {"
4997 # ˇ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.
4998 "},
4999 indoc! {"
5000 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5001 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5002 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5003 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5004 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5005 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5006 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5007 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5008 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5009 # accumsan eros.
5010 "},
5011 language_with_pound_comments.clone(),
5012 &mut cx,
5013 );
5014
5015 // Test that rewrapping is ignored outside of comments in most languages.
5016 assert_rewrap(
5017 indoc! {"
5018 /// Adds two numbers.
5019 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5020 fn add(a: u32, b: u32) -> u32 {
5021 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ˇ
5022 }
5023 "},
5024 indoc! {"
5025 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5026 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5027 fn add(a: u32, b: u32) -> u32 {
5028 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ˇ
5029 }
5030 "},
5031 language_with_doc_comments.clone(),
5032 &mut cx,
5033 );
5034
5035 // Test that rewrapping works in Markdown and Plain Text languages.
5036 assert_rewrap(
5037 indoc! {"
5038 # Hello
5039
5040 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.
5041 "},
5042 indoc! {"
5043 # Hello
5044
5045 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5046 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5047 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5048 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5049 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5050 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5051 Integer sit amet scelerisque nisi.
5052 "},
5053 markdown_language,
5054 &mut cx,
5055 );
5056
5057 assert_rewrap(
5058 indoc! {"
5059 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.
5060 "},
5061 indoc! {"
5062 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5063 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5064 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5065 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5066 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5067 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5068 Integer sit amet scelerisque nisi.
5069 "},
5070 plaintext_language,
5071 &mut cx,
5072 );
5073
5074 // Test rewrapping unaligned comments in a selection.
5075 assert_rewrap(
5076 indoc! {"
5077 fn foo() {
5078 if true {
5079 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5080 // Praesent semper egestas tellus id dignissim.ˇ»
5081 do_something();
5082 } else {
5083 //
5084 }
5085 }
5086 "},
5087 indoc! {"
5088 fn foo() {
5089 if true {
5090 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5091 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5092 // egestas tellus id dignissim.ˇ»
5093 do_something();
5094 } else {
5095 //
5096 }
5097 }
5098 "},
5099 language_with_doc_comments.clone(),
5100 &mut cx,
5101 );
5102
5103 assert_rewrap(
5104 indoc! {"
5105 fn foo() {
5106 if true {
5107 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5108 // Praesent semper egestas tellus id dignissim.»
5109 do_something();
5110 } else {
5111 //
5112 }
5113
5114 }
5115 "},
5116 indoc! {"
5117 fn foo() {
5118 if true {
5119 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5120 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5121 // egestas tellus id dignissim.»
5122 do_something();
5123 } else {
5124 //
5125 }
5126
5127 }
5128 "},
5129 language_with_doc_comments.clone(),
5130 &mut cx,
5131 );
5132
5133 #[track_caller]
5134 fn assert_rewrap(
5135 unwrapped_text: &str,
5136 wrapped_text: &str,
5137 language: Arc<Language>,
5138 cx: &mut EditorTestContext,
5139 ) {
5140 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5141 cx.set_state(unwrapped_text);
5142 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5143 cx.assert_editor_state(wrapped_text);
5144 }
5145}
5146
5147#[gpui::test]
5148async fn test_hard_wrap(cx: &mut TestAppContext) {
5149 init_test(cx, |_| {});
5150 let mut cx = EditorTestContext::new(cx).await;
5151
5152 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5153 cx.update_editor(|editor, _, cx| {
5154 editor.set_hard_wrap(Some(14), cx);
5155 });
5156
5157 cx.set_state(indoc!(
5158 "
5159 one two three ˇ
5160 "
5161 ));
5162 cx.simulate_input("four");
5163 cx.run_until_parked();
5164
5165 cx.assert_editor_state(indoc!(
5166 "
5167 one two three
5168 fourˇ
5169 "
5170 ));
5171
5172 cx.update_editor(|editor, window, cx| {
5173 editor.newline(&Default::default(), window, cx);
5174 });
5175 cx.run_until_parked();
5176 cx.assert_editor_state(indoc!(
5177 "
5178 one two three
5179 four
5180 ˇ
5181 "
5182 ));
5183
5184 cx.simulate_input("five");
5185 cx.run_until_parked();
5186 cx.assert_editor_state(indoc!(
5187 "
5188 one two three
5189 four
5190 fiveˇ
5191 "
5192 ));
5193
5194 cx.update_editor(|editor, window, cx| {
5195 editor.newline(&Default::default(), window, cx);
5196 });
5197 cx.run_until_parked();
5198 cx.simulate_input("# ");
5199 cx.run_until_parked();
5200 cx.assert_editor_state(indoc!(
5201 "
5202 one two three
5203 four
5204 five
5205 # ˇ
5206 "
5207 ));
5208
5209 cx.update_editor(|editor, window, cx| {
5210 editor.newline(&Default::default(), window, cx);
5211 });
5212 cx.run_until_parked();
5213 cx.assert_editor_state(indoc!(
5214 "
5215 one two three
5216 four
5217 five
5218 #\x20
5219 #ˇ
5220 "
5221 ));
5222
5223 cx.simulate_input(" 6");
5224 cx.run_until_parked();
5225 cx.assert_editor_state(indoc!(
5226 "
5227 one two three
5228 four
5229 five
5230 #
5231 # 6ˇ
5232 "
5233 ));
5234}
5235
5236#[gpui::test]
5237async fn test_clipboard(cx: &mut TestAppContext) {
5238 init_test(cx, |_| {});
5239
5240 let mut cx = EditorTestContext::new(cx).await;
5241
5242 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5243 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5244 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5245
5246 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5247 cx.set_state("two ˇfour ˇsix ˇ");
5248 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5249 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5250
5251 // Paste again but with only two cursors. Since the number of cursors doesn't
5252 // match the number of slices in the clipboard, the entire clipboard text
5253 // is pasted at each cursor.
5254 cx.set_state("ˇtwo one✅ four three six five ˇ");
5255 cx.update_editor(|e, window, cx| {
5256 e.handle_input("( ", window, cx);
5257 e.paste(&Paste, window, cx);
5258 e.handle_input(") ", window, cx);
5259 });
5260 cx.assert_editor_state(
5261 &([
5262 "( one✅ ",
5263 "three ",
5264 "five ) ˇtwo one✅ four three six five ( one✅ ",
5265 "three ",
5266 "five ) ˇ",
5267 ]
5268 .join("\n")),
5269 );
5270
5271 // Cut with three selections, one of which is full-line.
5272 cx.set_state(indoc! {"
5273 1«2ˇ»3
5274 4ˇ567
5275 «8ˇ»9"});
5276 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5277 cx.assert_editor_state(indoc! {"
5278 1ˇ3
5279 ˇ9"});
5280
5281 // Paste with three selections, noticing how the copied selection that was full-line
5282 // gets inserted before the second cursor.
5283 cx.set_state(indoc! {"
5284 1ˇ3
5285 9ˇ
5286 «oˇ»ne"});
5287 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5288 cx.assert_editor_state(indoc! {"
5289 12ˇ3
5290 4567
5291 9ˇ
5292 8ˇne"});
5293
5294 // Copy with a single cursor only, which writes the whole line into the clipboard.
5295 cx.set_state(indoc! {"
5296 The quick brown
5297 fox juˇmps over
5298 the lazy dog"});
5299 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5300 assert_eq!(
5301 cx.read_from_clipboard()
5302 .and_then(|item| item.text().as_deref().map(str::to_string)),
5303 Some("fox jumps over\n".to_string())
5304 );
5305
5306 // Paste with three selections, noticing how the copied full-line selection is inserted
5307 // before the empty selections but replaces the selection that is non-empty.
5308 cx.set_state(indoc! {"
5309 Tˇhe quick brown
5310 «foˇ»x jumps over
5311 tˇhe lazy dog"});
5312 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5313 cx.assert_editor_state(indoc! {"
5314 fox jumps over
5315 Tˇhe quick brown
5316 fox jumps over
5317 ˇx jumps over
5318 fox jumps over
5319 tˇhe lazy dog"});
5320}
5321
5322#[gpui::test]
5323async fn test_copy_trim(cx: &mut TestAppContext) {
5324 init_test(cx, |_| {});
5325
5326 let mut cx = EditorTestContext::new(cx).await;
5327 cx.set_state(
5328 r#" «for selection in selections.iter() {
5329 let mut start = selection.start;
5330 let mut end = selection.end;
5331 let is_entire_line = selection.is_empty();
5332 if is_entire_line {
5333 start = Point::new(start.row, 0);ˇ»
5334 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5335 }
5336 "#,
5337 );
5338 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5339 assert_eq!(
5340 cx.read_from_clipboard()
5341 .and_then(|item| item.text().as_deref().map(str::to_string)),
5342 Some(
5343 "for selection in selections.iter() {
5344 let mut start = selection.start;
5345 let mut end = selection.end;
5346 let is_entire_line = selection.is_empty();
5347 if is_entire_line {
5348 start = Point::new(start.row, 0);"
5349 .to_string()
5350 ),
5351 "Regular copying preserves all indentation selected",
5352 );
5353 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5354 assert_eq!(
5355 cx.read_from_clipboard()
5356 .and_then(|item| item.text().as_deref().map(str::to_string)),
5357 Some(
5358 "for selection in selections.iter() {
5359let mut start = selection.start;
5360let mut end = selection.end;
5361let is_entire_line = selection.is_empty();
5362if is_entire_line {
5363 start = Point::new(start.row, 0);"
5364 .to_string()
5365 ),
5366 "Copying with stripping should strip all leading whitespaces"
5367 );
5368
5369 cx.set_state(
5370 r#" « for selection in selections.iter() {
5371 let mut start = selection.start;
5372 let mut end = selection.end;
5373 let is_entire_line = selection.is_empty();
5374 if is_entire_line {
5375 start = Point::new(start.row, 0);ˇ»
5376 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5377 }
5378 "#,
5379 );
5380 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5381 assert_eq!(
5382 cx.read_from_clipboard()
5383 .and_then(|item| item.text().as_deref().map(str::to_string)),
5384 Some(
5385 " for selection in selections.iter() {
5386 let mut start = selection.start;
5387 let mut end = selection.end;
5388 let is_entire_line = selection.is_empty();
5389 if is_entire_line {
5390 start = Point::new(start.row, 0);"
5391 .to_string()
5392 ),
5393 "Regular copying preserves all indentation selected",
5394 );
5395 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5396 assert_eq!(
5397 cx.read_from_clipboard()
5398 .and_then(|item| item.text().as_deref().map(str::to_string)),
5399 Some(
5400 "for selection in selections.iter() {
5401let mut start = selection.start;
5402let mut end = selection.end;
5403let is_entire_line = selection.is_empty();
5404if is_entire_line {
5405 start = Point::new(start.row, 0);"
5406 .to_string()
5407 ),
5408 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5409 );
5410
5411 cx.set_state(
5412 r#" «ˇ for selection in selections.iter() {
5413 let mut start = selection.start;
5414 let mut end = selection.end;
5415 let is_entire_line = selection.is_empty();
5416 if is_entire_line {
5417 start = Point::new(start.row, 0);»
5418 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5419 }
5420 "#,
5421 );
5422 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5423 assert_eq!(
5424 cx.read_from_clipboard()
5425 .and_then(|item| item.text().as_deref().map(str::to_string)),
5426 Some(
5427 " for selection in selections.iter() {
5428 let mut start = selection.start;
5429 let mut end = selection.end;
5430 let is_entire_line = selection.is_empty();
5431 if is_entire_line {
5432 start = Point::new(start.row, 0);"
5433 .to_string()
5434 ),
5435 "Regular copying for reverse selection works the same",
5436 );
5437 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5438 assert_eq!(
5439 cx.read_from_clipboard()
5440 .and_then(|item| item.text().as_deref().map(str::to_string)),
5441 Some(
5442 "for selection in selections.iter() {
5443let mut start = selection.start;
5444let mut end = selection.end;
5445let is_entire_line = selection.is_empty();
5446if is_entire_line {
5447 start = Point::new(start.row, 0);"
5448 .to_string()
5449 ),
5450 "Copying with stripping for reverse selection works the same"
5451 );
5452
5453 cx.set_state(
5454 r#" for selection «in selections.iter() {
5455 let mut start = selection.start;
5456 let mut end = selection.end;
5457 let is_entire_line = selection.is_empty();
5458 if is_entire_line {
5459 start = Point::new(start.row, 0);ˇ»
5460 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5461 }
5462 "#,
5463 );
5464 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5465 assert_eq!(
5466 cx.read_from_clipboard()
5467 .and_then(|item| item.text().as_deref().map(str::to_string)),
5468 Some(
5469 "in selections.iter() {
5470 let mut start = selection.start;
5471 let mut end = selection.end;
5472 let is_entire_line = selection.is_empty();
5473 if is_entire_line {
5474 start = Point::new(start.row, 0);"
5475 .to_string()
5476 ),
5477 "When selecting past the indent, the copying works as usual",
5478 );
5479 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5480 assert_eq!(
5481 cx.read_from_clipboard()
5482 .and_then(|item| item.text().as_deref().map(str::to_string)),
5483 Some(
5484 "in selections.iter() {
5485 let mut start = selection.start;
5486 let mut end = selection.end;
5487 let is_entire_line = selection.is_empty();
5488 if is_entire_line {
5489 start = Point::new(start.row, 0);"
5490 .to_string()
5491 ),
5492 "When selecting past the indent, nothing is trimmed"
5493 );
5494
5495 cx.set_state(
5496 r#" «for selection in selections.iter() {
5497 let mut start = selection.start;
5498
5499 let mut end = selection.end;
5500 let is_entire_line = selection.is_empty();
5501 if is_entire_line {
5502 start = Point::new(start.row, 0);
5503ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5504 }
5505 "#,
5506 );
5507 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5508 assert_eq!(
5509 cx.read_from_clipboard()
5510 .and_then(|item| item.text().as_deref().map(str::to_string)),
5511 Some(
5512 "for selection in selections.iter() {
5513let mut start = selection.start;
5514
5515let mut end = selection.end;
5516let is_entire_line = selection.is_empty();
5517if is_entire_line {
5518 start = Point::new(start.row, 0);
5519"
5520 .to_string()
5521 ),
5522 "Copying with stripping should ignore empty lines"
5523 );
5524}
5525
5526#[gpui::test]
5527async fn test_paste_multiline(cx: &mut TestAppContext) {
5528 init_test(cx, |_| {});
5529
5530 let mut cx = EditorTestContext::new(cx).await;
5531 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5532
5533 // Cut an indented block, without the leading whitespace.
5534 cx.set_state(indoc! {"
5535 const a: B = (
5536 c(),
5537 «d(
5538 e,
5539 f
5540 )ˇ»
5541 );
5542 "});
5543 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5544 cx.assert_editor_state(indoc! {"
5545 const a: B = (
5546 c(),
5547 ˇ
5548 );
5549 "});
5550
5551 // Paste it at the same position.
5552 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5553 cx.assert_editor_state(indoc! {"
5554 const a: B = (
5555 c(),
5556 d(
5557 e,
5558 f
5559 )ˇ
5560 );
5561 "});
5562
5563 // Paste it at a line with a lower indent level.
5564 cx.set_state(indoc! {"
5565 ˇ
5566 const a: B = (
5567 c(),
5568 );
5569 "});
5570 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5571 cx.assert_editor_state(indoc! {"
5572 d(
5573 e,
5574 f
5575 )ˇ
5576 const a: B = (
5577 c(),
5578 );
5579 "});
5580
5581 // Cut an indented block, with the leading whitespace.
5582 cx.set_state(indoc! {"
5583 const a: B = (
5584 c(),
5585 « d(
5586 e,
5587 f
5588 )
5589 ˇ»);
5590 "});
5591 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5592 cx.assert_editor_state(indoc! {"
5593 const a: B = (
5594 c(),
5595 ˇ);
5596 "});
5597
5598 // Paste it at the same position.
5599 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5600 cx.assert_editor_state(indoc! {"
5601 const a: B = (
5602 c(),
5603 d(
5604 e,
5605 f
5606 )
5607 ˇ);
5608 "});
5609
5610 // Paste it at a line with a higher indent level.
5611 cx.set_state(indoc! {"
5612 const a: B = (
5613 c(),
5614 d(
5615 e,
5616 fˇ
5617 )
5618 );
5619 "});
5620 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5621 cx.assert_editor_state(indoc! {"
5622 const a: B = (
5623 c(),
5624 d(
5625 e,
5626 f d(
5627 e,
5628 f
5629 )
5630 ˇ
5631 )
5632 );
5633 "});
5634
5635 // Copy an indented block, starting mid-line
5636 cx.set_state(indoc! {"
5637 const a: B = (
5638 c(),
5639 somethin«g(
5640 e,
5641 f
5642 )ˇ»
5643 );
5644 "});
5645 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5646
5647 // Paste it on a line with a lower indent level
5648 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5649 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5650 cx.assert_editor_state(indoc! {"
5651 const a: B = (
5652 c(),
5653 something(
5654 e,
5655 f
5656 )
5657 );
5658 g(
5659 e,
5660 f
5661 )ˇ"});
5662}
5663
5664#[gpui::test]
5665async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5666 init_test(cx, |_| {});
5667
5668 cx.write_to_clipboard(ClipboardItem::new_string(
5669 " d(\n e\n );\n".into(),
5670 ));
5671
5672 let mut cx = EditorTestContext::new(cx).await;
5673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5674
5675 cx.set_state(indoc! {"
5676 fn a() {
5677 b();
5678 if c() {
5679 ˇ
5680 }
5681 }
5682 "});
5683
5684 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5685 cx.assert_editor_state(indoc! {"
5686 fn a() {
5687 b();
5688 if c() {
5689 d(
5690 e
5691 );
5692 ˇ
5693 }
5694 }
5695 "});
5696
5697 cx.set_state(indoc! {"
5698 fn a() {
5699 b();
5700 ˇ
5701 }
5702 "});
5703
5704 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5705 cx.assert_editor_state(indoc! {"
5706 fn a() {
5707 b();
5708 d(
5709 e
5710 );
5711 ˇ
5712 }
5713 "});
5714}
5715
5716#[gpui::test]
5717fn test_select_all(cx: &mut TestAppContext) {
5718 init_test(cx, |_| {});
5719
5720 let editor = cx.add_window(|window, cx| {
5721 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5722 build_editor(buffer, window, cx)
5723 });
5724 _ = editor.update(cx, |editor, window, cx| {
5725 editor.select_all(&SelectAll, window, cx);
5726 assert_eq!(
5727 editor.selections.display_ranges(cx),
5728 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5729 );
5730 });
5731}
5732
5733#[gpui::test]
5734fn test_select_line(cx: &mut TestAppContext) {
5735 init_test(cx, |_| {});
5736
5737 let editor = cx.add_window(|window, cx| {
5738 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5739 build_editor(buffer, window, cx)
5740 });
5741 _ = editor.update(cx, |editor, window, cx| {
5742 editor.change_selections(None, window, cx, |s| {
5743 s.select_display_ranges([
5744 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5745 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5746 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5747 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5748 ])
5749 });
5750 editor.select_line(&SelectLine, window, cx);
5751 assert_eq!(
5752 editor.selections.display_ranges(cx),
5753 vec![
5754 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5755 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5756 ]
5757 );
5758 });
5759
5760 _ = editor.update(cx, |editor, window, cx| {
5761 editor.select_line(&SelectLine, window, cx);
5762 assert_eq!(
5763 editor.selections.display_ranges(cx),
5764 vec![
5765 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5766 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5767 ]
5768 );
5769 });
5770
5771 _ = editor.update(cx, |editor, window, cx| {
5772 editor.select_line(&SelectLine, window, cx);
5773 assert_eq!(
5774 editor.selections.display_ranges(cx),
5775 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5776 );
5777 });
5778}
5779
5780#[gpui::test]
5781async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5782 init_test(cx, |_| {});
5783 let mut cx = EditorTestContext::new(cx).await;
5784
5785 #[track_caller]
5786 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5787 cx.set_state(initial_state);
5788 cx.update_editor(|e, window, cx| {
5789 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5790 });
5791 cx.assert_editor_state(expected_state);
5792 }
5793
5794 // Selection starts and ends at the middle of lines, left-to-right
5795 test(
5796 &mut cx,
5797 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5798 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5799 );
5800 // Same thing, right-to-left
5801 test(
5802 &mut cx,
5803 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5804 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5805 );
5806
5807 // Whole buffer, left-to-right, last line *doesn't* end with newline
5808 test(
5809 &mut cx,
5810 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5811 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5812 );
5813 // Same thing, right-to-left
5814 test(
5815 &mut cx,
5816 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5817 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5818 );
5819
5820 // Whole buffer, left-to-right, last line ends with newline
5821 test(
5822 &mut cx,
5823 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5824 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5825 );
5826 // Same thing, right-to-left
5827 test(
5828 &mut cx,
5829 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5830 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5831 );
5832
5833 // Starts at the end of a line, ends at the start of another
5834 test(
5835 &mut cx,
5836 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5837 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5838 );
5839}
5840
5841#[gpui::test]
5842async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5843 init_test(cx, |_| {});
5844
5845 let editor = cx.add_window(|window, cx| {
5846 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5847 build_editor(buffer, window, cx)
5848 });
5849
5850 // setup
5851 _ = editor.update(cx, |editor, window, cx| {
5852 editor.fold_creases(
5853 vec![
5854 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5855 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5856 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5857 ],
5858 true,
5859 window,
5860 cx,
5861 );
5862 assert_eq!(
5863 editor.display_text(cx),
5864 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5865 );
5866 });
5867
5868 _ = editor.update(cx, |editor, window, cx| {
5869 editor.change_selections(None, window, cx, |s| {
5870 s.select_display_ranges([
5871 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5872 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5873 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5874 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5875 ])
5876 });
5877 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5878 assert_eq!(
5879 editor.display_text(cx),
5880 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5881 );
5882 });
5883 EditorTestContext::for_editor(editor, cx)
5884 .await
5885 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5886
5887 _ = editor.update(cx, |editor, window, cx| {
5888 editor.change_selections(None, window, cx, |s| {
5889 s.select_display_ranges([
5890 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5891 ])
5892 });
5893 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5894 assert_eq!(
5895 editor.display_text(cx),
5896 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5897 );
5898 assert_eq!(
5899 editor.selections.display_ranges(cx),
5900 [
5901 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5902 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5903 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5904 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5905 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5906 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5907 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5908 ]
5909 );
5910 });
5911 EditorTestContext::for_editor(editor, cx)
5912 .await
5913 .assert_editor_state(
5914 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5915 );
5916}
5917
5918#[gpui::test]
5919async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5920 init_test(cx, |_| {});
5921
5922 let mut cx = EditorTestContext::new(cx).await;
5923
5924 cx.set_state(indoc!(
5925 r#"abc
5926 defˇghi
5927
5928 jk
5929 nlmo
5930 "#
5931 ));
5932
5933 cx.update_editor(|editor, window, cx| {
5934 editor.add_selection_above(&Default::default(), window, cx);
5935 });
5936
5937 cx.assert_editor_state(indoc!(
5938 r#"abcˇ
5939 defˇghi
5940
5941 jk
5942 nlmo
5943 "#
5944 ));
5945
5946 cx.update_editor(|editor, window, cx| {
5947 editor.add_selection_above(&Default::default(), window, cx);
5948 });
5949
5950 cx.assert_editor_state(indoc!(
5951 r#"abcˇ
5952 defˇghi
5953
5954 jk
5955 nlmo
5956 "#
5957 ));
5958
5959 cx.update_editor(|editor, window, cx| {
5960 editor.add_selection_below(&Default::default(), window, cx);
5961 });
5962
5963 cx.assert_editor_state(indoc!(
5964 r#"abc
5965 defˇghi
5966
5967 jk
5968 nlmo
5969 "#
5970 ));
5971
5972 cx.update_editor(|editor, window, cx| {
5973 editor.undo_selection(&Default::default(), window, cx);
5974 });
5975
5976 cx.assert_editor_state(indoc!(
5977 r#"abcˇ
5978 defˇghi
5979
5980 jk
5981 nlmo
5982 "#
5983 ));
5984
5985 cx.update_editor(|editor, window, cx| {
5986 editor.redo_selection(&Default::default(), window, cx);
5987 });
5988
5989 cx.assert_editor_state(indoc!(
5990 r#"abc
5991 defˇghi
5992
5993 jk
5994 nlmo
5995 "#
5996 ));
5997
5998 cx.update_editor(|editor, window, cx| {
5999 editor.add_selection_below(&Default::default(), window, cx);
6000 });
6001
6002 cx.assert_editor_state(indoc!(
6003 r#"abc
6004 defˇghi
6005 ˇ
6006 jk
6007 nlmo
6008 "#
6009 ));
6010
6011 cx.update_editor(|editor, window, cx| {
6012 editor.add_selection_below(&Default::default(), window, cx);
6013 });
6014
6015 cx.assert_editor_state(indoc!(
6016 r#"abc
6017 defˇghi
6018 ˇ
6019 jkˇ
6020 nlmo
6021 "#
6022 ));
6023
6024 cx.update_editor(|editor, window, cx| {
6025 editor.add_selection_below(&Default::default(), window, cx);
6026 });
6027
6028 cx.assert_editor_state(indoc!(
6029 r#"abc
6030 defˇghi
6031 ˇ
6032 jkˇ
6033 nlmˇo
6034 "#
6035 ));
6036
6037 cx.update_editor(|editor, window, cx| {
6038 editor.add_selection_below(&Default::default(), window, cx);
6039 });
6040
6041 cx.assert_editor_state(indoc!(
6042 r#"abc
6043 defˇghi
6044 ˇ
6045 jkˇ
6046 nlmˇo
6047 ˇ"#
6048 ));
6049
6050 // change selections
6051 cx.set_state(indoc!(
6052 r#"abc
6053 def«ˇg»hi
6054
6055 jk
6056 nlmo
6057 "#
6058 ));
6059
6060 cx.update_editor(|editor, window, cx| {
6061 editor.add_selection_below(&Default::default(), window, cx);
6062 });
6063
6064 cx.assert_editor_state(indoc!(
6065 r#"abc
6066 def«ˇg»hi
6067
6068 jk
6069 nlm«ˇo»
6070 "#
6071 ));
6072
6073 cx.update_editor(|editor, window, cx| {
6074 editor.add_selection_below(&Default::default(), window, cx);
6075 });
6076
6077 cx.assert_editor_state(indoc!(
6078 r#"abc
6079 def«ˇg»hi
6080
6081 jk
6082 nlm«ˇo»
6083 "#
6084 ));
6085
6086 cx.update_editor(|editor, window, cx| {
6087 editor.add_selection_above(&Default::default(), window, cx);
6088 });
6089
6090 cx.assert_editor_state(indoc!(
6091 r#"abc
6092 def«ˇg»hi
6093
6094 jk
6095 nlmo
6096 "#
6097 ));
6098
6099 cx.update_editor(|editor, window, cx| {
6100 editor.add_selection_above(&Default::default(), window, cx);
6101 });
6102
6103 cx.assert_editor_state(indoc!(
6104 r#"abc
6105 def«ˇg»hi
6106
6107 jk
6108 nlmo
6109 "#
6110 ));
6111
6112 // Change selections again
6113 cx.set_state(indoc!(
6114 r#"a«bc
6115 defgˇ»hi
6116
6117 jk
6118 nlmo
6119 "#
6120 ));
6121
6122 cx.update_editor(|editor, window, cx| {
6123 editor.add_selection_below(&Default::default(), window, cx);
6124 });
6125
6126 cx.assert_editor_state(indoc!(
6127 r#"a«bcˇ»
6128 d«efgˇ»hi
6129
6130 j«kˇ»
6131 nlmo
6132 "#
6133 ));
6134
6135 cx.update_editor(|editor, window, cx| {
6136 editor.add_selection_below(&Default::default(), window, cx);
6137 });
6138 cx.assert_editor_state(indoc!(
6139 r#"a«bcˇ»
6140 d«efgˇ»hi
6141
6142 j«kˇ»
6143 n«lmoˇ»
6144 "#
6145 ));
6146 cx.update_editor(|editor, window, cx| {
6147 editor.add_selection_above(&Default::default(), window, cx);
6148 });
6149
6150 cx.assert_editor_state(indoc!(
6151 r#"a«bcˇ»
6152 d«efgˇ»hi
6153
6154 j«kˇ»
6155 nlmo
6156 "#
6157 ));
6158
6159 // Change selections again
6160 cx.set_state(indoc!(
6161 r#"abc
6162 d«ˇefghi
6163
6164 jk
6165 nlm»o
6166 "#
6167 ));
6168
6169 cx.update_editor(|editor, window, cx| {
6170 editor.add_selection_above(&Default::default(), window, cx);
6171 });
6172
6173 cx.assert_editor_state(indoc!(
6174 r#"a«ˇbc»
6175 d«ˇef»ghi
6176
6177 j«ˇk»
6178 n«ˇlm»o
6179 "#
6180 ));
6181
6182 cx.update_editor(|editor, window, cx| {
6183 editor.add_selection_below(&Default::default(), window, cx);
6184 });
6185
6186 cx.assert_editor_state(indoc!(
6187 r#"abc
6188 d«ˇef»ghi
6189
6190 j«ˇk»
6191 n«ˇlm»o
6192 "#
6193 ));
6194}
6195
6196#[gpui::test]
6197async fn test_select_next(cx: &mut TestAppContext) {
6198 init_test(cx, |_| {});
6199
6200 let mut cx = EditorTestContext::new(cx).await;
6201 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6202
6203 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6204 .unwrap();
6205 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6206
6207 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6208 .unwrap();
6209 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6210
6211 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6212 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6213
6214 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6215 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6216
6217 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6218 .unwrap();
6219 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6220
6221 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6222 .unwrap();
6223 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6224
6225 // Test selection direction should be preserved
6226 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6227
6228 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6229 .unwrap();
6230 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6231}
6232
6233#[gpui::test]
6234async fn test_select_all_matches(cx: &mut TestAppContext) {
6235 init_test(cx, |_| {});
6236
6237 let mut cx = EditorTestContext::new(cx).await;
6238
6239 // Test caret-only selections
6240 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6241 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6242 .unwrap();
6243 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6244
6245 // Test left-to-right selections
6246 cx.set_state("abc\n«abcˇ»\nabc");
6247 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6248 .unwrap();
6249 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6250
6251 // Test right-to-left selections
6252 cx.set_state("abc\n«ˇabc»\nabc");
6253 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6254 .unwrap();
6255 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6256
6257 // Test selecting whitespace with caret selection
6258 cx.set_state("abc\nˇ abc\nabc");
6259 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6260 .unwrap();
6261 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6262
6263 // Test selecting whitespace with left-to-right selection
6264 cx.set_state("abc\n«ˇ »abc\nabc");
6265 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6266 .unwrap();
6267 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6268
6269 // Test no matches with right-to-left selection
6270 cx.set_state("abc\n« ˇ»abc\nabc");
6271 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6272 .unwrap();
6273 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6274}
6275
6276#[gpui::test]
6277async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6278 init_test(cx, |_| {});
6279
6280 let mut cx = EditorTestContext::new(cx).await;
6281
6282 let large_body_1 = "\nd".repeat(200);
6283 let large_body_2 = "\ne".repeat(200);
6284
6285 cx.set_state(&format!(
6286 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6287 ));
6288 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6289 let scroll_position = editor.scroll_position(cx);
6290 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6291 scroll_position
6292 });
6293
6294 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6295 .unwrap();
6296 cx.assert_editor_state(&format!(
6297 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6298 ));
6299 let scroll_position_after_selection =
6300 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6301 assert_eq!(
6302 initial_scroll_position, scroll_position_after_selection,
6303 "Scroll position should not change after selecting all matches"
6304 );
6305}
6306
6307#[gpui::test]
6308async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6309 init_test(cx, |_| {});
6310
6311 let mut cx = EditorLspTestContext::new_rust(
6312 lsp::ServerCapabilities {
6313 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6314 ..Default::default()
6315 },
6316 cx,
6317 )
6318 .await;
6319
6320 cx.set_state(indoc! {"
6321 line 1
6322 line 2
6323 linˇe 3
6324 line 4
6325 line 5
6326 "});
6327
6328 // Make an edit
6329 cx.update_editor(|editor, window, cx| {
6330 editor.handle_input("X", window, cx);
6331 });
6332
6333 // Move cursor to a different position
6334 cx.update_editor(|editor, window, cx| {
6335 editor.change_selections(None, window, cx, |s| {
6336 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6337 });
6338 });
6339
6340 cx.assert_editor_state(indoc! {"
6341 line 1
6342 line 2
6343 linXe 3
6344 line 4
6345 liˇne 5
6346 "});
6347
6348 cx.lsp
6349 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6350 Ok(Some(vec![lsp::TextEdit::new(
6351 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6352 "PREFIX ".to_string(),
6353 )]))
6354 });
6355
6356 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6357 .unwrap()
6358 .await
6359 .unwrap();
6360
6361 cx.assert_editor_state(indoc! {"
6362 PREFIX line 1
6363 line 2
6364 linXe 3
6365 line 4
6366 liˇne 5
6367 "});
6368
6369 // Undo formatting
6370 cx.update_editor(|editor, window, cx| {
6371 editor.undo(&Default::default(), window, cx);
6372 });
6373
6374 // Verify cursor moved back to position after edit
6375 cx.assert_editor_state(indoc! {"
6376 line 1
6377 line 2
6378 linXˇe 3
6379 line 4
6380 line 5
6381 "});
6382}
6383
6384#[gpui::test]
6385async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6386 init_test(cx, |_| {});
6387
6388 let mut cx = EditorTestContext::new(cx).await;
6389
6390 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6391 cx.update_editor(|editor, window, cx| {
6392 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6393 });
6394
6395 cx.set_state(indoc! {"
6396 line 1
6397 line 2
6398 linˇe 3
6399 line 4
6400 line 5
6401 line 6
6402 line 7
6403 line 8
6404 line 9
6405 line 10
6406 "});
6407
6408 let snapshot = cx.buffer_snapshot();
6409 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6410
6411 cx.update(|_, cx| {
6412 provider.update(cx, |provider, _| {
6413 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6414 id: None,
6415 edits: vec![(edit_position..edit_position, "X".into())],
6416 edit_preview: None,
6417 }))
6418 })
6419 });
6420
6421 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6422 cx.update_editor(|editor, window, cx| {
6423 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6424 });
6425
6426 cx.assert_editor_state(indoc! {"
6427 line 1
6428 line 2
6429 lineXˇ 3
6430 line 4
6431 line 5
6432 line 6
6433 line 7
6434 line 8
6435 line 9
6436 line 10
6437 "});
6438
6439 cx.update_editor(|editor, window, cx| {
6440 editor.change_selections(None, window, cx, |s| {
6441 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6442 });
6443 });
6444
6445 cx.assert_editor_state(indoc! {"
6446 line 1
6447 line 2
6448 lineX 3
6449 line 4
6450 line 5
6451 line 6
6452 line 7
6453 line 8
6454 line 9
6455 liˇne 10
6456 "});
6457
6458 cx.update_editor(|editor, window, cx| {
6459 editor.undo(&Default::default(), window, cx);
6460 });
6461
6462 cx.assert_editor_state(indoc! {"
6463 line 1
6464 line 2
6465 lineˇ 3
6466 line 4
6467 line 5
6468 line 6
6469 line 7
6470 line 8
6471 line 9
6472 line 10
6473 "});
6474}
6475
6476#[gpui::test]
6477async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6478 init_test(cx, |_| {});
6479
6480 let mut cx = EditorTestContext::new(cx).await;
6481 cx.set_state(
6482 r#"let foo = 2;
6483lˇet foo = 2;
6484let fooˇ = 2;
6485let foo = 2;
6486let foo = ˇ2;"#,
6487 );
6488
6489 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6490 .unwrap();
6491 cx.assert_editor_state(
6492 r#"let foo = 2;
6493«letˇ» foo = 2;
6494let «fooˇ» = 2;
6495let foo = 2;
6496let foo = «2ˇ»;"#,
6497 );
6498
6499 // noop for multiple selections with different contents
6500 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6501 .unwrap();
6502 cx.assert_editor_state(
6503 r#"let foo = 2;
6504«letˇ» foo = 2;
6505let «fooˇ» = 2;
6506let foo = 2;
6507let foo = «2ˇ»;"#,
6508 );
6509
6510 // Test last selection direction should be preserved
6511 cx.set_state(
6512 r#"let foo = 2;
6513let foo = 2;
6514let «fooˇ» = 2;
6515let «ˇfoo» = 2;
6516let foo = 2;"#,
6517 );
6518
6519 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6520 .unwrap();
6521 cx.assert_editor_state(
6522 r#"let foo = 2;
6523let foo = 2;
6524let «fooˇ» = 2;
6525let «ˇfoo» = 2;
6526let «ˇfoo» = 2;"#,
6527 );
6528}
6529
6530#[gpui::test]
6531async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6532 init_test(cx, |_| {});
6533
6534 let mut cx =
6535 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6536
6537 cx.assert_editor_state(indoc! {"
6538 ˇbbb
6539 ccc
6540
6541 bbb
6542 ccc
6543 "});
6544 cx.dispatch_action(SelectPrevious::default());
6545 cx.assert_editor_state(indoc! {"
6546 «bbbˇ»
6547 ccc
6548
6549 bbb
6550 ccc
6551 "});
6552 cx.dispatch_action(SelectPrevious::default());
6553 cx.assert_editor_state(indoc! {"
6554 «bbbˇ»
6555 ccc
6556
6557 «bbbˇ»
6558 ccc
6559 "});
6560}
6561
6562#[gpui::test]
6563async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6564 init_test(cx, |_| {});
6565
6566 let mut cx = EditorTestContext::new(cx).await;
6567 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6568
6569 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6570 .unwrap();
6571 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6572
6573 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6574 .unwrap();
6575 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6576
6577 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6578 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6579
6580 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6581 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6582
6583 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6584 .unwrap();
6585 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6586
6587 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6588 .unwrap();
6589 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6590}
6591
6592#[gpui::test]
6593async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6594 init_test(cx, |_| {});
6595
6596 let mut cx = EditorTestContext::new(cx).await;
6597 cx.set_state("aˇ");
6598
6599 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6600 .unwrap();
6601 cx.assert_editor_state("«aˇ»");
6602 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6603 .unwrap();
6604 cx.assert_editor_state("«aˇ»");
6605}
6606
6607#[gpui::test]
6608async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6609 init_test(cx, |_| {});
6610
6611 let mut cx = EditorTestContext::new(cx).await;
6612 cx.set_state(
6613 r#"let foo = 2;
6614lˇet foo = 2;
6615let fooˇ = 2;
6616let foo = 2;
6617let foo = ˇ2;"#,
6618 );
6619
6620 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6621 .unwrap();
6622 cx.assert_editor_state(
6623 r#"let foo = 2;
6624«letˇ» foo = 2;
6625let «fooˇ» = 2;
6626let foo = 2;
6627let foo = «2ˇ»;"#,
6628 );
6629
6630 // noop for multiple selections with different contents
6631 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6632 .unwrap();
6633 cx.assert_editor_state(
6634 r#"let foo = 2;
6635«letˇ» foo = 2;
6636let «fooˇ» = 2;
6637let foo = 2;
6638let foo = «2ˇ»;"#,
6639 );
6640}
6641
6642#[gpui::test]
6643async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6644 init_test(cx, |_| {});
6645
6646 let mut cx = EditorTestContext::new(cx).await;
6647 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6648
6649 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6650 .unwrap();
6651 // selection direction is preserved
6652 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6653
6654 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6655 .unwrap();
6656 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6657
6658 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6659 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6660
6661 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6662 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6663
6664 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6665 .unwrap();
6666 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6667
6668 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6669 .unwrap();
6670 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6671}
6672
6673#[gpui::test]
6674async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6675 init_test(cx, |_| {});
6676
6677 let language = Arc::new(Language::new(
6678 LanguageConfig::default(),
6679 Some(tree_sitter_rust::LANGUAGE.into()),
6680 ));
6681
6682 let text = r#"
6683 use mod1::mod2::{mod3, mod4};
6684
6685 fn fn_1(param1: bool, param2: &str) {
6686 let var1 = "text";
6687 }
6688 "#
6689 .unindent();
6690
6691 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6692 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6693 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6694
6695 editor
6696 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6697 .await;
6698
6699 editor.update_in(cx, |editor, window, cx| {
6700 editor.change_selections(None, window, cx, |s| {
6701 s.select_display_ranges([
6702 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6703 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6704 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6705 ]);
6706 });
6707 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6708 });
6709 editor.update(cx, |editor, cx| {
6710 assert_text_with_selections(
6711 editor,
6712 indoc! {r#"
6713 use mod1::mod2::{mod3, «mod4ˇ»};
6714
6715 fn fn_1«ˇ(param1: bool, param2: &str)» {
6716 let var1 = "«ˇtext»";
6717 }
6718 "#},
6719 cx,
6720 );
6721 });
6722
6723 editor.update_in(cx, |editor, window, cx| {
6724 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6725 });
6726 editor.update(cx, |editor, cx| {
6727 assert_text_with_selections(
6728 editor,
6729 indoc! {r#"
6730 use mod1::mod2::«{mod3, mod4}ˇ»;
6731
6732 «ˇfn fn_1(param1: bool, param2: &str) {
6733 let var1 = "text";
6734 }»
6735 "#},
6736 cx,
6737 );
6738 });
6739
6740 editor.update_in(cx, |editor, window, cx| {
6741 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6742 });
6743 assert_eq!(
6744 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6745 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6746 );
6747
6748 // Trying to expand the selected syntax node one more time has no effect.
6749 editor.update_in(cx, |editor, window, cx| {
6750 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6751 });
6752 assert_eq!(
6753 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6754 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6755 );
6756
6757 editor.update_in(cx, |editor, window, cx| {
6758 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6759 });
6760 editor.update(cx, |editor, cx| {
6761 assert_text_with_selections(
6762 editor,
6763 indoc! {r#"
6764 use mod1::mod2::«{mod3, mod4}ˇ»;
6765
6766 «ˇfn fn_1(param1: bool, param2: &str) {
6767 let var1 = "text";
6768 }»
6769 "#},
6770 cx,
6771 );
6772 });
6773
6774 editor.update_in(cx, |editor, window, cx| {
6775 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6776 });
6777 editor.update(cx, |editor, cx| {
6778 assert_text_with_selections(
6779 editor,
6780 indoc! {r#"
6781 use mod1::mod2::{mod3, «mod4ˇ»};
6782
6783 fn fn_1«ˇ(param1: bool, param2: &str)» {
6784 let var1 = "«ˇtext»";
6785 }
6786 "#},
6787 cx,
6788 );
6789 });
6790
6791 editor.update_in(cx, |editor, window, cx| {
6792 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6793 });
6794 editor.update(cx, |editor, cx| {
6795 assert_text_with_selections(
6796 editor,
6797 indoc! {r#"
6798 use mod1::mod2::{mod3, mo«ˇ»d4};
6799
6800 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6801 let var1 = "te«ˇ»xt";
6802 }
6803 "#},
6804 cx,
6805 );
6806 });
6807
6808 // Trying to shrink the selected syntax node one more time has no effect.
6809 editor.update_in(cx, |editor, window, cx| {
6810 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6811 });
6812 editor.update_in(cx, |editor, _, cx| {
6813 assert_text_with_selections(
6814 editor,
6815 indoc! {r#"
6816 use mod1::mod2::{mod3, mo«ˇ»d4};
6817
6818 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6819 let var1 = "te«ˇ»xt";
6820 }
6821 "#},
6822 cx,
6823 );
6824 });
6825
6826 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6827 // a fold.
6828 editor.update_in(cx, |editor, window, cx| {
6829 editor.fold_creases(
6830 vec![
6831 Crease::simple(
6832 Point::new(0, 21)..Point::new(0, 24),
6833 FoldPlaceholder::test(),
6834 ),
6835 Crease::simple(
6836 Point::new(3, 20)..Point::new(3, 22),
6837 FoldPlaceholder::test(),
6838 ),
6839 ],
6840 true,
6841 window,
6842 cx,
6843 );
6844 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6845 });
6846 editor.update(cx, |editor, cx| {
6847 assert_text_with_selections(
6848 editor,
6849 indoc! {r#"
6850 use mod1::mod2::«{mod3, mod4}ˇ»;
6851
6852 fn fn_1«ˇ(param1: bool, param2: &str)» {
6853 let var1 = "«ˇtext»";
6854 }
6855 "#},
6856 cx,
6857 );
6858 });
6859}
6860
6861#[gpui::test]
6862async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6863 init_test(cx, |_| {});
6864
6865 let language = Arc::new(Language::new(
6866 LanguageConfig::default(),
6867 Some(tree_sitter_rust::LANGUAGE.into()),
6868 ));
6869
6870 let text = "let a = 2;";
6871
6872 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6873 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6874 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6875
6876 editor
6877 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6878 .await;
6879
6880 // Test case 1: Cursor at end of word
6881 editor.update_in(cx, |editor, window, cx| {
6882 editor.change_selections(None, window, cx, |s| {
6883 s.select_display_ranges([
6884 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6885 ]);
6886 });
6887 });
6888 editor.update(cx, |editor, cx| {
6889 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6890 });
6891 editor.update_in(cx, |editor, window, cx| {
6892 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6893 });
6894 editor.update(cx, |editor, cx| {
6895 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6896 });
6897 editor.update_in(cx, |editor, window, cx| {
6898 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6899 });
6900 editor.update(cx, |editor, cx| {
6901 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6902 });
6903
6904 // Test case 2: Cursor at end of statement
6905 editor.update_in(cx, |editor, window, cx| {
6906 editor.change_selections(None, window, cx, |s| {
6907 s.select_display_ranges([
6908 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6909 ]);
6910 });
6911 });
6912 editor.update(cx, |editor, cx| {
6913 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6914 });
6915 editor.update_in(cx, |editor, window, cx| {
6916 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6917 });
6918 editor.update(cx, |editor, cx| {
6919 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6920 });
6921}
6922
6923#[gpui::test]
6924async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6925 init_test(cx, |_| {});
6926
6927 let language = Arc::new(Language::new(
6928 LanguageConfig::default(),
6929 Some(tree_sitter_rust::LANGUAGE.into()),
6930 ));
6931
6932 let text = r#"
6933 use mod1::mod2::{mod3, mod4};
6934
6935 fn fn_1(param1: bool, param2: &str) {
6936 let var1 = "hello world";
6937 }
6938 "#
6939 .unindent();
6940
6941 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6942 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6943 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6944
6945 editor
6946 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6947 .await;
6948
6949 // Test 1: Cursor on a letter of a string word
6950 editor.update_in(cx, |editor, window, cx| {
6951 editor.change_selections(None, window, cx, |s| {
6952 s.select_display_ranges([
6953 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6954 ]);
6955 });
6956 });
6957 editor.update_in(cx, |editor, window, cx| {
6958 assert_text_with_selections(
6959 editor,
6960 indoc! {r#"
6961 use mod1::mod2::{mod3, mod4};
6962
6963 fn fn_1(param1: bool, param2: &str) {
6964 let var1 = "hˇello world";
6965 }
6966 "#},
6967 cx,
6968 );
6969 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6970 assert_text_with_selections(
6971 editor,
6972 indoc! {r#"
6973 use mod1::mod2::{mod3, mod4};
6974
6975 fn fn_1(param1: bool, param2: &str) {
6976 let var1 = "«ˇhello» world";
6977 }
6978 "#},
6979 cx,
6980 );
6981 });
6982
6983 // Test 2: Partial selection within a word
6984 editor.update_in(cx, |editor, window, cx| {
6985 editor.change_selections(None, window, cx, |s| {
6986 s.select_display_ranges([
6987 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6988 ]);
6989 });
6990 });
6991 editor.update_in(cx, |editor, window, cx| {
6992 assert_text_with_selections(
6993 editor,
6994 indoc! {r#"
6995 use mod1::mod2::{mod3, mod4};
6996
6997 fn fn_1(param1: bool, param2: &str) {
6998 let var1 = "h«elˇ»lo world";
6999 }
7000 "#},
7001 cx,
7002 );
7003 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7004 assert_text_with_selections(
7005 editor,
7006 indoc! {r#"
7007 use mod1::mod2::{mod3, mod4};
7008
7009 fn fn_1(param1: bool, param2: &str) {
7010 let var1 = "«ˇhello» world";
7011 }
7012 "#},
7013 cx,
7014 );
7015 });
7016
7017 // Test 3: Complete word already selected
7018 editor.update_in(cx, |editor, window, cx| {
7019 editor.change_selections(None, window, cx, |s| {
7020 s.select_display_ranges([
7021 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7022 ]);
7023 });
7024 });
7025 editor.update_in(cx, |editor, window, cx| {
7026 assert_text_with_selections(
7027 editor,
7028 indoc! {r#"
7029 use mod1::mod2::{mod3, mod4};
7030
7031 fn fn_1(param1: bool, param2: &str) {
7032 let var1 = "«helloˇ» world";
7033 }
7034 "#},
7035 cx,
7036 );
7037 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7038 assert_text_with_selections(
7039 editor,
7040 indoc! {r#"
7041 use mod1::mod2::{mod3, mod4};
7042
7043 fn fn_1(param1: bool, param2: &str) {
7044 let var1 = "«hello worldˇ»";
7045 }
7046 "#},
7047 cx,
7048 );
7049 });
7050
7051 // Test 4: Selection spanning across words
7052 editor.update_in(cx, |editor, window, cx| {
7053 editor.change_selections(None, window, cx, |s| {
7054 s.select_display_ranges([
7055 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7056 ]);
7057 });
7058 });
7059 editor.update_in(cx, |editor, window, cx| {
7060 assert_text_with_selections(
7061 editor,
7062 indoc! {r#"
7063 use mod1::mod2::{mod3, mod4};
7064
7065 fn fn_1(param1: bool, param2: &str) {
7066 let var1 = "hel«lo woˇ»rld";
7067 }
7068 "#},
7069 cx,
7070 );
7071 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7072 assert_text_with_selections(
7073 editor,
7074 indoc! {r#"
7075 use mod1::mod2::{mod3, mod4};
7076
7077 fn fn_1(param1: bool, param2: &str) {
7078 let var1 = "«ˇhello world»";
7079 }
7080 "#},
7081 cx,
7082 );
7083 });
7084
7085 // Test 5: Expansion beyond string
7086 editor.update_in(cx, |editor, window, cx| {
7087 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7088 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7089 assert_text_with_selections(
7090 editor,
7091 indoc! {r#"
7092 use mod1::mod2::{mod3, mod4};
7093
7094 fn fn_1(param1: bool, param2: &str) {
7095 «ˇlet var1 = "hello world";»
7096 }
7097 "#},
7098 cx,
7099 );
7100 });
7101}
7102
7103#[gpui::test]
7104async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7105 init_test(cx, |_| {});
7106
7107 let base_text = r#"
7108 impl A {
7109 // this is an uncommitted comment
7110
7111 fn b() {
7112 c();
7113 }
7114
7115 // this is another uncommitted comment
7116
7117 fn d() {
7118 // e
7119 // f
7120 }
7121 }
7122
7123 fn g() {
7124 // h
7125 }
7126 "#
7127 .unindent();
7128
7129 let text = r#"
7130 ˇimpl A {
7131
7132 fn b() {
7133 c();
7134 }
7135
7136 fn d() {
7137 // e
7138 // f
7139 }
7140 }
7141
7142 fn g() {
7143 // h
7144 }
7145 "#
7146 .unindent();
7147
7148 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7149 cx.set_state(&text);
7150 cx.set_head_text(&base_text);
7151 cx.update_editor(|editor, window, cx| {
7152 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7153 });
7154
7155 cx.assert_state_with_diff(
7156 "
7157 ˇimpl A {
7158 - // this is an uncommitted comment
7159
7160 fn b() {
7161 c();
7162 }
7163
7164 - // this is another uncommitted comment
7165 -
7166 fn d() {
7167 // e
7168 // f
7169 }
7170 }
7171
7172 fn g() {
7173 // h
7174 }
7175 "
7176 .unindent(),
7177 );
7178
7179 let expected_display_text = "
7180 impl A {
7181 // this is an uncommitted comment
7182
7183 fn b() {
7184 ⋯
7185 }
7186
7187 // this is another uncommitted comment
7188
7189 fn d() {
7190 ⋯
7191 }
7192 }
7193
7194 fn g() {
7195 ⋯
7196 }
7197 "
7198 .unindent();
7199
7200 cx.update_editor(|editor, window, cx| {
7201 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7202 assert_eq!(editor.display_text(cx), expected_display_text);
7203 });
7204}
7205
7206#[gpui::test]
7207async fn test_autoindent(cx: &mut TestAppContext) {
7208 init_test(cx, |_| {});
7209
7210 let language = Arc::new(
7211 Language::new(
7212 LanguageConfig {
7213 brackets: BracketPairConfig {
7214 pairs: vec![
7215 BracketPair {
7216 start: "{".to_string(),
7217 end: "}".to_string(),
7218 close: false,
7219 surround: false,
7220 newline: true,
7221 },
7222 BracketPair {
7223 start: "(".to_string(),
7224 end: ")".to_string(),
7225 close: false,
7226 surround: false,
7227 newline: true,
7228 },
7229 ],
7230 ..Default::default()
7231 },
7232 ..Default::default()
7233 },
7234 Some(tree_sitter_rust::LANGUAGE.into()),
7235 )
7236 .with_indents_query(
7237 r#"
7238 (_ "(" ")" @end) @indent
7239 (_ "{" "}" @end) @indent
7240 "#,
7241 )
7242 .unwrap(),
7243 );
7244
7245 let text = "fn a() {}";
7246
7247 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7248 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7249 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7250 editor
7251 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7252 .await;
7253
7254 editor.update_in(cx, |editor, window, cx| {
7255 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7256 editor.newline(&Newline, window, cx);
7257 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7258 assert_eq!(
7259 editor.selections.ranges(cx),
7260 &[
7261 Point::new(1, 4)..Point::new(1, 4),
7262 Point::new(3, 4)..Point::new(3, 4),
7263 Point::new(5, 0)..Point::new(5, 0)
7264 ]
7265 );
7266 });
7267}
7268
7269#[gpui::test]
7270async fn test_autoindent_selections(cx: &mut TestAppContext) {
7271 init_test(cx, |_| {});
7272
7273 {
7274 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7275 cx.set_state(indoc! {"
7276 impl A {
7277
7278 fn b() {}
7279
7280 «fn c() {
7281
7282 }ˇ»
7283 }
7284 "});
7285
7286 cx.update_editor(|editor, window, cx| {
7287 editor.autoindent(&Default::default(), window, cx);
7288 });
7289
7290 cx.assert_editor_state(indoc! {"
7291 impl A {
7292
7293 fn b() {}
7294
7295 «fn c() {
7296
7297 }ˇ»
7298 }
7299 "});
7300 }
7301
7302 {
7303 let mut cx = EditorTestContext::new_multibuffer(
7304 cx,
7305 [indoc! { "
7306 impl A {
7307 «
7308 // a
7309 fn b(){}
7310 »
7311 «
7312 }
7313 fn c(){}
7314 »
7315 "}],
7316 );
7317
7318 let buffer = cx.update_editor(|editor, _, cx| {
7319 let buffer = editor.buffer().update(cx, |buffer, _| {
7320 buffer.all_buffers().iter().next().unwrap().clone()
7321 });
7322 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7323 buffer
7324 });
7325
7326 cx.run_until_parked();
7327 cx.update_editor(|editor, window, cx| {
7328 editor.select_all(&Default::default(), window, cx);
7329 editor.autoindent(&Default::default(), window, cx)
7330 });
7331 cx.run_until_parked();
7332
7333 cx.update(|_, cx| {
7334 assert_eq!(
7335 buffer.read(cx).text(),
7336 indoc! { "
7337 impl A {
7338
7339 // a
7340 fn b(){}
7341
7342
7343 }
7344 fn c(){}
7345
7346 " }
7347 )
7348 });
7349 }
7350}
7351
7352#[gpui::test]
7353async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7354 init_test(cx, |_| {});
7355
7356 let mut cx = EditorTestContext::new(cx).await;
7357
7358 let language = Arc::new(Language::new(
7359 LanguageConfig {
7360 brackets: BracketPairConfig {
7361 pairs: vec![
7362 BracketPair {
7363 start: "{".to_string(),
7364 end: "}".to_string(),
7365 close: true,
7366 surround: true,
7367 newline: true,
7368 },
7369 BracketPair {
7370 start: "(".to_string(),
7371 end: ")".to_string(),
7372 close: true,
7373 surround: true,
7374 newline: true,
7375 },
7376 BracketPair {
7377 start: "/*".to_string(),
7378 end: " */".to_string(),
7379 close: true,
7380 surround: true,
7381 newline: true,
7382 },
7383 BracketPair {
7384 start: "[".to_string(),
7385 end: "]".to_string(),
7386 close: false,
7387 surround: false,
7388 newline: true,
7389 },
7390 BracketPair {
7391 start: "\"".to_string(),
7392 end: "\"".to_string(),
7393 close: true,
7394 surround: true,
7395 newline: false,
7396 },
7397 BracketPair {
7398 start: "<".to_string(),
7399 end: ">".to_string(),
7400 close: false,
7401 surround: true,
7402 newline: true,
7403 },
7404 ],
7405 ..Default::default()
7406 },
7407 autoclose_before: "})]".to_string(),
7408 ..Default::default()
7409 },
7410 Some(tree_sitter_rust::LANGUAGE.into()),
7411 ));
7412
7413 cx.language_registry().add(language.clone());
7414 cx.update_buffer(|buffer, cx| {
7415 buffer.set_language(Some(language), cx);
7416 });
7417
7418 cx.set_state(
7419 &r#"
7420 🏀ˇ
7421 εˇ
7422 ❤️ˇ
7423 "#
7424 .unindent(),
7425 );
7426
7427 // autoclose multiple nested brackets at multiple cursors
7428 cx.update_editor(|editor, window, cx| {
7429 editor.handle_input("{", window, cx);
7430 editor.handle_input("{", window, cx);
7431 editor.handle_input("{", window, cx);
7432 });
7433 cx.assert_editor_state(
7434 &"
7435 🏀{{{ˇ}}}
7436 ε{{{ˇ}}}
7437 ❤️{{{ˇ}}}
7438 "
7439 .unindent(),
7440 );
7441
7442 // insert a different closing bracket
7443 cx.update_editor(|editor, window, cx| {
7444 editor.handle_input(")", window, cx);
7445 });
7446 cx.assert_editor_state(
7447 &"
7448 🏀{{{)ˇ}}}
7449 ε{{{)ˇ}}}
7450 ❤️{{{)ˇ}}}
7451 "
7452 .unindent(),
7453 );
7454
7455 // skip over the auto-closed brackets when typing a closing bracket
7456 cx.update_editor(|editor, window, cx| {
7457 editor.move_right(&MoveRight, window, cx);
7458 editor.handle_input("}", window, cx);
7459 editor.handle_input("}", window, cx);
7460 editor.handle_input("}", window, cx);
7461 });
7462 cx.assert_editor_state(
7463 &"
7464 🏀{{{)}}}}ˇ
7465 ε{{{)}}}}ˇ
7466 ❤️{{{)}}}}ˇ
7467 "
7468 .unindent(),
7469 );
7470
7471 // autoclose multi-character pairs
7472 cx.set_state(
7473 &"
7474 ˇ
7475 ˇ
7476 "
7477 .unindent(),
7478 );
7479 cx.update_editor(|editor, window, cx| {
7480 editor.handle_input("/", window, cx);
7481 editor.handle_input("*", window, cx);
7482 });
7483 cx.assert_editor_state(
7484 &"
7485 /*ˇ */
7486 /*ˇ */
7487 "
7488 .unindent(),
7489 );
7490
7491 // one cursor autocloses a multi-character pair, one cursor
7492 // does not autoclose.
7493 cx.set_state(
7494 &"
7495 /ˇ
7496 ˇ
7497 "
7498 .unindent(),
7499 );
7500 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7501 cx.assert_editor_state(
7502 &"
7503 /*ˇ */
7504 *ˇ
7505 "
7506 .unindent(),
7507 );
7508
7509 // Don't autoclose if the next character isn't whitespace and isn't
7510 // listed in the language's "autoclose_before" section.
7511 cx.set_state("ˇa b");
7512 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7513 cx.assert_editor_state("{ˇa b");
7514
7515 // Don't autoclose if `close` is false for the bracket pair
7516 cx.set_state("ˇ");
7517 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7518 cx.assert_editor_state("[ˇ");
7519
7520 // Surround with brackets if text is selected
7521 cx.set_state("«aˇ» b");
7522 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7523 cx.assert_editor_state("{«aˇ»} b");
7524
7525 // Autoclose when not immediately after a word character
7526 cx.set_state("a ˇ");
7527 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7528 cx.assert_editor_state("a \"ˇ\"");
7529
7530 // Autoclose pair where the start and end characters are the same
7531 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7532 cx.assert_editor_state("a \"\"ˇ");
7533
7534 // Don't autoclose when immediately after a word character
7535 cx.set_state("aˇ");
7536 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7537 cx.assert_editor_state("a\"ˇ");
7538
7539 // Do autoclose when after a non-word character
7540 cx.set_state("{ˇ");
7541 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7542 cx.assert_editor_state("{\"ˇ\"");
7543
7544 // Non identical pairs autoclose regardless of preceding character
7545 cx.set_state("aˇ");
7546 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7547 cx.assert_editor_state("a{ˇ}");
7548
7549 // Don't autoclose pair if autoclose is disabled
7550 cx.set_state("ˇ");
7551 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7552 cx.assert_editor_state("<ˇ");
7553
7554 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7555 cx.set_state("«aˇ» b");
7556 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7557 cx.assert_editor_state("<«aˇ»> b");
7558}
7559
7560#[gpui::test]
7561async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7562 init_test(cx, |settings| {
7563 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7564 });
7565
7566 let mut cx = EditorTestContext::new(cx).await;
7567
7568 let language = Arc::new(Language::new(
7569 LanguageConfig {
7570 brackets: BracketPairConfig {
7571 pairs: vec![
7572 BracketPair {
7573 start: "{".to_string(),
7574 end: "}".to_string(),
7575 close: true,
7576 surround: true,
7577 newline: true,
7578 },
7579 BracketPair {
7580 start: "(".to_string(),
7581 end: ")".to_string(),
7582 close: true,
7583 surround: true,
7584 newline: true,
7585 },
7586 BracketPair {
7587 start: "[".to_string(),
7588 end: "]".to_string(),
7589 close: false,
7590 surround: false,
7591 newline: true,
7592 },
7593 ],
7594 ..Default::default()
7595 },
7596 autoclose_before: "})]".to_string(),
7597 ..Default::default()
7598 },
7599 Some(tree_sitter_rust::LANGUAGE.into()),
7600 ));
7601
7602 cx.language_registry().add(language.clone());
7603 cx.update_buffer(|buffer, cx| {
7604 buffer.set_language(Some(language), cx);
7605 });
7606
7607 cx.set_state(
7608 &"
7609 ˇ
7610 ˇ
7611 ˇ
7612 "
7613 .unindent(),
7614 );
7615
7616 // ensure only matching closing brackets are skipped over
7617 cx.update_editor(|editor, window, cx| {
7618 editor.handle_input("}", window, cx);
7619 editor.move_left(&MoveLeft, window, cx);
7620 editor.handle_input(")", window, cx);
7621 editor.move_left(&MoveLeft, window, cx);
7622 });
7623 cx.assert_editor_state(
7624 &"
7625 ˇ)}
7626 ˇ)}
7627 ˇ)}
7628 "
7629 .unindent(),
7630 );
7631
7632 // skip-over closing brackets at multiple cursors
7633 cx.update_editor(|editor, window, cx| {
7634 editor.handle_input(")", window, cx);
7635 editor.handle_input("}", window, cx);
7636 });
7637 cx.assert_editor_state(
7638 &"
7639 )}ˇ
7640 )}ˇ
7641 )}ˇ
7642 "
7643 .unindent(),
7644 );
7645
7646 // ignore non-close brackets
7647 cx.update_editor(|editor, window, cx| {
7648 editor.handle_input("]", window, cx);
7649 editor.move_left(&MoveLeft, window, cx);
7650 editor.handle_input("]", window, cx);
7651 });
7652 cx.assert_editor_state(
7653 &"
7654 )}]ˇ]
7655 )}]ˇ]
7656 )}]ˇ]
7657 "
7658 .unindent(),
7659 );
7660}
7661
7662#[gpui::test]
7663async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7664 init_test(cx, |_| {});
7665
7666 let mut cx = EditorTestContext::new(cx).await;
7667
7668 let html_language = Arc::new(
7669 Language::new(
7670 LanguageConfig {
7671 name: "HTML".into(),
7672 brackets: BracketPairConfig {
7673 pairs: vec![
7674 BracketPair {
7675 start: "<".into(),
7676 end: ">".into(),
7677 close: true,
7678 ..Default::default()
7679 },
7680 BracketPair {
7681 start: "{".into(),
7682 end: "}".into(),
7683 close: true,
7684 ..Default::default()
7685 },
7686 BracketPair {
7687 start: "(".into(),
7688 end: ")".into(),
7689 close: true,
7690 ..Default::default()
7691 },
7692 ],
7693 ..Default::default()
7694 },
7695 autoclose_before: "})]>".into(),
7696 ..Default::default()
7697 },
7698 Some(tree_sitter_html::LANGUAGE.into()),
7699 )
7700 .with_injection_query(
7701 r#"
7702 (script_element
7703 (raw_text) @injection.content
7704 (#set! injection.language "javascript"))
7705 "#,
7706 )
7707 .unwrap(),
7708 );
7709
7710 let javascript_language = Arc::new(Language::new(
7711 LanguageConfig {
7712 name: "JavaScript".into(),
7713 brackets: BracketPairConfig {
7714 pairs: vec![
7715 BracketPair {
7716 start: "/*".into(),
7717 end: " */".into(),
7718 close: true,
7719 ..Default::default()
7720 },
7721 BracketPair {
7722 start: "{".into(),
7723 end: "}".into(),
7724 close: true,
7725 ..Default::default()
7726 },
7727 BracketPair {
7728 start: "(".into(),
7729 end: ")".into(),
7730 close: true,
7731 ..Default::default()
7732 },
7733 ],
7734 ..Default::default()
7735 },
7736 autoclose_before: "})]>".into(),
7737 ..Default::default()
7738 },
7739 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7740 ));
7741
7742 cx.language_registry().add(html_language.clone());
7743 cx.language_registry().add(javascript_language.clone());
7744
7745 cx.update_buffer(|buffer, cx| {
7746 buffer.set_language(Some(html_language), cx);
7747 });
7748
7749 cx.set_state(
7750 &r#"
7751 <body>ˇ
7752 <script>
7753 var x = 1;ˇ
7754 </script>
7755 </body>ˇ
7756 "#
7757 .unindent(),
7758 );
7759
7760 // Precondition: different languages are active at different locations.
7761 cx.update_editor(|editor, window, cx| {
7762 let snapshot = editor.snapshot(window, cx);
7763 let cursors = editor.selections.ranges::<usize>(cx);
7764 let languages = cursors
7765 .iter()
7766 .map(|c| snapshot.language_at(c.start).unwrap().name())
7767 .collect::<Vec<_>>();
7768 assert_eq!(
7769 languages,
7770 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7771 );
7772 });
7773
7774 // Angle brackets autoclose in HTML, but not JavaScript.
7775 cx.update_editor(|editor, window, cx| {
7776 editor.handle_input("<", window, cx);
7777 editor.handle_input("a", window, cx);
7778 });
7779 cx.assert_editor_state(
7780 &r#"
7781 <body><aˇ>
7782 <script>
7783 var x = 1;<aˇ
7784 </script>
7785 </body><aˇ>
7786 "#
7787 .unindent(),
7788 );
7789
7790 // Curly braces and parens autoclose in both HTML and JavaScript.
7791 cx.update_editor(|editor, window, cx| {
7792 editor.handle_input(" b=", window, cx);
7793 editor.handle_input("{", window, cx);
7794 editor.handle_input("c", window, cx);
7795 editor.handle_input("(", window, cx);
7796 });
7797 cx.assert_editor_state(
7798 &r#"
7799 <body><a b={c(ˇ)}>
7800 <script>
7801 var x = 1;<a b={c(ˇ)}
7802 </script>
7803 </body><a b={c(ˇ)}>
7804 "#
7805 .unindent(),
7806 );
7807
7808 // Brackets that were already autoclosed are skipped.
7809 cx.update_editor(|editor, window, cx| {
7810 editor.handle_input(")", window, cx);
7811 editor.handle_input("d", window, cx);
7812 editor.handle_input("}", window, cx);
7813 });
7814 cx.assert_editor_state(
7815 &r#"
7816 <body><a b={c()d}ˇ>
7817 <script>
7818 var x = 1;<a b={c()d}ˇ
7819 </script>
7820 </body><a b={c()d}ˇ>
7821 "#
7822 .unindent(),
7823 );
7824 cx.update_editor(|editor, window, cx| {
7825 editor.handle_input(">", window, cx);
7826 });
7827 cx.assert_editor_state(
7828 &r#"
7829 <body><a b={c()d}>ˇ
7830 <script>
7831 var x = 1;<a b={c()d}>ˇ
7832 </script>
7833 </body><a b={c()d}>ˇ
7834 "#
7835 .unindent(),
7836 );
7837
7838 // Reset
7839 cx.set_state(
7840 &r#"
7841 <body>ˇ
7842 <script>
7843 var x = 1;ˇ
7844 </script>
7845 </body>ˇ
7846 "#
7847 .unindent(),
7848 );
7849
7850 cx.update_editor(|editor, window, cx| {
7851 editor.handle_input("<", window, cx);
7852 });
7853 cx.assert_editor_state(
7854 &r#"
7855 <body><ˇ>
7856 <script>
7857 var x = 1;<ˇ
7858 </script>
7859 </body><ˇ>
7860 "#
7861 .unindent(),
7862 );
7863
7864 // When backspacing, the closing angle brackets are removed.
7865 cx.update_editor(|editor, window, cx| {
7866 editor.backspace(&Backspace, window, cx);
7867 });
7868 cx.assert_editor_state(
7869 &r#"
7870 <body>ˇ
7871 <script>
7872 var x = 1;ˇ
7873 </script>
7874 </body>ˇ
7875 "#
7876 .unindent(),
7877 );
7878
7879 // Block comments autoclose in JavaScript, but not HTML.
7880 cx.update_editor(|editor, window, cx| {
7881 editor.handle_input("/", window, cx);
7882 editor.handle_input("*", window, cx);
7883 });
7884 cx.assert_editor_state(
7885 &r#"
7886 <body>/*ˇ
7887 <script>
7888 var x = 1;/*ˇ */
7889 </script>
7890 </body>/*ˇ
7891 "#
7892 .unindent(),
7893 );
7894}
7895
7896#[gpui::test]
7897async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7898 init_test(cx, |_| {});
7899
7900 let mut cx = EditorTestContext::new(cx).await;
7901
7902 let rust_language = Arc::new(
7903 Language::new(
7904 LanguageConfig {
7905 name: "Rust".into(),
7906 brackets: serde_json::from_value(json!([
7907 { "start": "{", "end": "}", "close": true, "newline": true },
7908 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7909 ]))
7910 .unwrap(),
7911 autoclose_before: "})]>".into(),
7912 ..Default::default()
7913 },
7914 Some(tree_sitter_rust::LANGUAGE.into()),
7915 )
7916 .with_override_query("(string_literal) @string")
7917 .unwrap(),
7918 );
7919
7920 cx.language_registry().add(rust_language.clone());
7921 cx.update_buffer(|buffer, cx| {
7922 buffer.set_language(Some(rust_language), cx);
7923 });
7924
7925 cx.set_state(
7926 &r#"
7927 let x = ˇ
7928 "#
7929 .unindent(),
7930 );
7931
7932 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7933 cx.update_editor(|editor, window, cx| {
7934 editor.handle_input("\"", window, cx);
7935 });
7936 cx.assert_editor_state(
7937 &r#"
7938 let x = "ˇ"
7939 "#
7940 .unindent(),
7941 );
7942
7943 // Inserting another quotation mark. The cursor moves across the existing
7944 // automatically-inserted quotation mark.
7945 cx.update_editor(|editor, window, cx| {
7946 editor.handle_input("\"", window, cx);
7947 });
7948 cx.assert_editor_state(
7949 &r#"
7950 let x = ""ˇ
7951 "#
7952 .unindent(),
7953 );
7954
7955 // Reset
7956 cx.set_state(
7957 &r#"
7958 let x = ˇ
7959 "#
7960 .unindent(),
7961 );
7962
7963 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7964 cx.update_editor(|editor, window, cx| {
7965 editor.handle_input("\"", window, cx);
7966 editor.handle_input(" ", window, cx);
7967 editor.move_left(&Default::default(), window, cx);
7968 editor.handle_input("\\", window, cx);
7969 editor.handle_input("\"", window, cx);
7970 });
7971 cx.assert_editor_state(
7972 &r#"
7973 let x = "\"ˇ "
7974 "#
7975 .unindent(),
7976 );
7977
7978 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7979 // mark. Nothing is inserted.
7980 cx.update_editor(|editor, window, cx| {
7981 editor.move_right(&Default::default(), window, cx);
7982 editor.handle_input("\"", window, cx);
7983 });
7984 cx.assert_editor_state(
7985 &r#"
7986 let x = "\" "ˇ
7987 "#
7988 .unindent(),
7989 );
7990}
7991
7992#[gpui::test]
7993async fn test_surround_with_pair(cx: &mut TestAppContext) {
7994 init_test(cx, |_| {});
7995
7996 let language = Arc::new(Language::new(
7997 LanguageConfig {
7998 brackets: BracketPairConfig {
7999 pairs: vec![
8000 BracketPair {
8001 start: "{".to_string(),
8002 end: "}".to_string(),
8003 close: true,
8004 surround: true,
8005 newline: true,
8006 },
8007 BracketPair {
8008 start: "/* ".to_string(),
8009 end: "*/".to_string(),
8010 close: true,
8011 surround: true,
8012 ..Default::default()
8013 },
8014 ],
8015 ..Default::default()
8016 },
8017 ..Default::default()
8018 },
8019 Some(tree_sitter_rust::LANGUAGE.into()),
8020 ));
8021
8022 let text = r#"
8023 a
8024 b
8025 c
8026 "#
8027 .unindent();
8028
8029 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8030 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8031 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8032 editor
8033 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8034 .await;
8035
8036 editor.update_in(cx, |editor, window, cx| {
8037 editor.change_selections(None, window, cx, |s| {
8038 s.select_display_ranges([
8039 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8040 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8041 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8042 ])
8043 });
8044
8045 editor.handle_input("{", window, cx);
8046 editor.handle_input("{", window, cx);
8047 editor.handle_input("{", window, cx);
8048 assert_eq!(
8049 editor.text(cx),
8050 "
8051 {{{a}}}
8052 {{{b}}}
8053 {{{c}}}
8054 "
8055 .unindent()
8056 );
8057 assert_eq!(
8058 editor.selections.display_ranges(cx),
8059 [
8060 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8061 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8062 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8063 ]
8064 );
8065
8066 editor.undo(&Undo, window, cx);
8067 editor.undo(&Undo, window, cx);
8068 editor.undo(&Undo, window, cx);
8069 assert_eq!(
8070 editor.text(cx),
8071 "
8072 a
8073 b
8074 c
8075 "
8076 .unindent()
8077 );
8078 assert_eq!(
8079 editor.selections.display_ranges(cx),
8080 [
8081 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8082 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8083 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8084 ]
8085 );
8086
8087 // Ensure inserting the first character of a multi-byte bracket pair
8088 // doesn't surround the selections with the bracket.
8089 editor.handle_input("/", window, cx);
8090 assert_eq!(
8091 editor.text(cx),
8092 "
8093 /
8094 /
8095 /
8096 "
8097 .unindent()
8098 );
8099 assert_eq!(
8100 editor.selections.display_ranges(cx),
8101 [
8102 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8103 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8104 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8105 ]
8106 );
8107
8108 editor.undo(&Undo, window, cx);
8109 assert_eq!(
8110 editor.text(cx),
8111 "
8112 a
8113 b
8114 c
8115 "
8116 .unindent()
8117 );
8118 assert_eq!(
8119 editor.selections.display_ranges(cx),
8120 [
8121 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8122 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8123 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8124 ]
8125 );
8126
8127 // Ensure inserting the last character of a multi-byte bracket pair
8128 // doesn't surround the selections with the bracket.
8129 editor.handle_input("*", window, cx);
8130 assert_eq!(
8131 editor.text(cx),
8132 "
8133 *
8134 *
8135 *
8136 "
8137 .unindent()
8138 );
8139 assert_eq!(
8140 editor.selections.display_ranges(cx),
8141 [
8142 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8143 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8144 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8145 ]
8146 );
8147 });
8148}
8149
8150#[gpui::test]
8151async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8152 init_test(cx, |_| {});
8153
8154 let language = Arc::new(Language::new(
8155 LanguageConfig {
8156 brackets: BracketPairConfig {
8157 pairs: vec![BracketPair {
8158 start: "{".to_string(),
8159 end: "}".to_string(),
8160 close: true,
8161 surround: true,
8162 newline: true,
8163 }],
8164 ..Default::default()
8165 },
8166 autoclose_before: "}".to_string(),
8167 ..Default::default()
8168 },
8169 Some(tree_sitter_rust::LANGUAGE.into()),
8170 ));
8171
8172 let text = r#"
8173 a
8174 b
8175 c
8176 "#
8177 .unindent();
8178
8179 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8180 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8181 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8182 editor
8183 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8184 .await;
8185
8186 editor.update_in(cx, |editor, window, cx| {
8187 editor.change_selections(None, window, cx, |s| {
8188 s.select_ranges([
8189 Point::new(0, 1)..Point::new(0, 1),
8190 Point::new(1, 1)..Point::new(1, 1),
8191 Point::new(2, 1)..Point::new(2, 1),
8192 ])
8193 });
8194
8195 editor.handle_input("{", window, cx);
8196 editor.handle_input("{", window, cx);
8197 editor.handle_input("_", window, cx);
8198 assert_eq!(
8199 editor.text(cx),
8200 "
8201 a{{_}}
8202 b{{_}}
8203 c{{_}}
8204 "
8205 .unindent()
8206 );
8207 assert_eq!(
8208 editor.selections.ranges::<Point>(cx),
8209 [
8210 Point::new(0, 4)..Point::new(0, 4),
8211 Point::new(1, 4)..Point::new(1, 4),
8212 Point::new(2, 4)..Point::new(2, 4)
8213 ]
8214 );
8215
8216 editor.backspace(&Default::default(), window, cx);
8217 editor.backspace(&Default::default(), window, cx);
8218 assert_eq!(
8219 editor.text(cx),
8220 "
8221 a{}
8222 b{}
8223 c{}
8224 "
8225 .unindent()
8226 );
8227 assert_eq!(
8228 editor.selections.ranges::<Point>(cx),
8229 [
8230 Point::new(0, 2)..Point::new(0, 2),
8231 Point::new(1, 2)..Point::new(1, 2),
8232 Point::new(2, 2)..Point::new(2, 2)
8233 ]
8234 );
8235
8236 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8237 assert_eq!(
8238 editor.text(cx),
8239 "
8240 a
8241 b
8242 c
8243 "
8244 .unindent()
8245 );
8246 assert_eq!(
8247 editor.selections.ranges::<Point>(cx),
8248 [
8249 Point::new(0, 1)..Point::new(0, 1),
8250 Point::new(1, 1)..Point::new(1, 1),
8251 Point::new(2, 1)..Point::new(2, 1)
8252 ]
8253 );
8254 });
8255}
8256
8257#[gpui::test]
8258async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8259 init_test(cx, |settings| {
8260 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8261 });
8262
8263 let mut cx = EditorTestContext::new(cx).await;
8264
8265 let language = Arc::new(Language::new(
8266 LanguageConfig {
8267 brackets: BracketPairConfig {
8268 pairs: vec![
8269 BracketPair {
8270 start: "{".to_string(),
8271 end: "}".to_string(),
8272 close: true,
8273 surround: true,
8274 newline: true,
8275 },
8276 BracketPair {
8277 start: "(".to_string(),
8278 end: ")".to_string(),
8279 close: true,
8280 surround: true,
8281 newline: true,
8282 },
8283 BracketPair {
8284 start: "[".to_string(),
8285 end: "]".to_string(),
8286 close: false,
8287 surround: true,
8288 newline: true,
8289 },
8290 ],
8291 ..Default::default()
8292 },
8293 autoclose_before: "})]".to_string(),
8294 ..Default::default()
8295 },
8296 Some(tree_sitter_rust::LANGUAGE.into()),
8297 ));
8298
8299 cx.language_registry().add(language.clone());
8300 cx.update_buffer(|buffer, cx| {
8301 buffer.set_language(Some(language), cx);
8302 });
8303
8304 cx.set_state(
8305 &"
8306 {(ˇ)}
8307 [[ˇ]]
8308 {(ˇ)}
8309 "
8310 .unindent(),
8311 );
8312
8313 cx.update_editor(|editor, window, cx| {
8314 editor.backspace(&Default::default(), window, cx);
8315 editor.backspace(&Default::default(), window, cx);
8316 });
8317
8318 cx.assert_editor_state(
8319 &"
8320 ˇ
8321 ˇ]]
8322 ˇ
8323 "
8324 .unindent(),
8325 );
8326
8327 cx.update_editor(|editor, window, cx| {
8328 editor.handle_input("{", window, cx);
8329 editor.handle_input("{", window, cx);
8330 editor.move_right(&MoveRight, window, cx);
8331 editor.move_right(&MoveRight, window, cx);
8332 editor.move_left(&MoveLeft, window, cx);
8333 editor.move_left(&MoveLeft, window, cx);
8334 editor.backspace(&Default::default(), window, cx);
8335 });
8336
8337 cx.assert_editor_state(
8338 &"
8339 {ˇ}
8340 {ˇ}]]
8341 {ˇ}
8342 "
8343 .unindent(),
8344 );
8345
8346 cx.update_editor(|editor, window, cx| {
8347 editor.backspace(&Default::default(), window, cx);
8348 });
8349
8350 cx.assert_editor_state(
8351 &"
8352 ˇ
8353 ˇ]]
8354 ˇ
8355 "
8356 .unindent(),
8357 );
8358}
8359
8360#[gpui::test]
8361async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8362 init_test(cx, |_| {});
8363
8364 let language = Arc::new(Language::new(
8365 LanguageConfig::default(),
8366 Some(tree_sitter_rust::LANGUAGE.into()),
8367 ));
8368
8369 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8370 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8371 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8372 editor
8373 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8374 .await;
8375
8376 editor.update_in(cx, |editor, window, cx| {
8377 editor.set_auto_replace_emoji_shortcode(true);
8378
8379 editor.handle_input("Hello ", window, cx);
8380 editor.handle_input(":wave", window, cx);
8381 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8382
8383 editor.handle_input(":", window, cx);
8384 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8385
8386 editor.handle_input(" :smile", window, cx);
8387 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8388
8389 editor.handle_input(":", window, cx);
8390 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8391
8392 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8393 editor.handle_input(":wave", window, cx);
8394 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8395
8396 editor.handle_input(":", window, cx);
8397 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8398
8399 editor.handle_input(":1", window, cx);
8400 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8401
8402 editor.handle_input(":", window, cx);
8403 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8404
8405 // Ensure shortcode does not get replaced when it is part of a word
8406 editor.handle_input(" Test:wave", window, cx);
8407 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8408
8409 editor.handle_input(":", window, cx);
8410 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8411
8412 editor.set_auto_replace_emoji_shortcode(false);
8413
8414 // Ensure shortcode does not get replaced when auto replace is off
8415 editor.handle_input(" :wave", window, cx);
8416 assert_eq!(
8417 editor.text(cx),
8418 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8419 );
8420
8421 editor.handle_input(":", window, cx);
8422 assert_eq!(
8423 editor.text(cx),
8424 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8425 );
8426 });
8427}
8428
8429#[gpui::test]
8430async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8431 init_test(cx, |_| {});
8432
8433 let (text, insertion_ranges) = marked_text_ranges(
8434 indoc! {"
8435 ˇ
8436 "},
8437 false,
8438 );
8439
8440 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8441 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8442
8443 _ = editor.update_in(cx, |editor, window, cx| {
8444 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8445
8446 editor
8447 .insert_snippet(&insertion_ranges, snippet, window, cx)
8448 .unwrap();
8449
8450 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8451 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8452 assert_eq!(editor.text(cx), expected_text);
8453 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8454 }
8455
8456 assert(
8457 editor,
8458 cx,
8459 indoc! {"
8460 type «» =•
8461 "},
8462 );
8463
8464 assert!(editor.context_menu_visible(), "There should be a matches");
8465 });
8466}
8467
8468#[gpui::test]
8469async fn test_snippets(cx: &mut TestAppContext) {
8470 init_test(cx, |_| {});
8471
8472 let (text, insertion_ranges) = marked_text_ranges(
8473 indoc! {"
8474 a.ˇ b
8475 a.ˇ b
8476 a.ˇ b
8477 "},
8478 false,
8479 );
8480
8481 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8482 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8483
8484 editor.update_in(cx, |editor, window, cx| {
8485 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8486
8487 editor
8488 .insert_snippet(&insertion_ranges, snippet, window, cx)
8489 .unwrap();
8490
8491 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8492 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8493 assert_eq!(editor.text(cx), expected_text);
8494 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8495 }
8496
8497 assert(
8498 editor,
8499 cx,
8500 indoc! {"
8501 a.f(«one», two, «three») b
8502 a.f(«one», two, «three») b
8503 a.f(«one», two, «three») b
8504 "},
8505 );
8506
8507 // Can't move earlier than the first tab stop
8508 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8509 assert(
8510 editor,
8511 cx,
8512 indoc! {"
8513 a.f(«one», two, «three») b
8514 a.f(«one», two, «three») b
8515 a.f(«one», two, «three») b
8516 "},
8517 );
8518
8519 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8520 assert(
8521 editor,
8522 cx,
8523 indoc! {"
8524 a.f(one, «two», three) b
8525 a.f(one, «two», three) b
8526 a.f(one, «two», three) b
8527 "},
8528 );
8529
8530 editor.move_to_prev_snippet_tabstop(window, cx);
8531 assert(
8532 editor,
8533 cx,
8534 indoc! {"
8535 a.f(«one», two, «three») b
8536 a.f(«one», two, «three») b
8537 a.f(«one», two, «three») b
8538 "},
8539 );
8540
8541 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8542 assert(
8543 editor,
8544 cx,
8545 indoc! {"
8546 a.f(one, «two», three) b
8547 a.f(one, «two», three) b
8548 a.f(one, «two», three) b
8549 "},
8550 );
8551 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8552 assert(
8553 editor,
8554 cx,
8555 indoc! {"
8556 a.f(one, two, three)ˇ b
8557 a.f(one, two, three)ˇ b
8558 a.f(one, two, three)ˇ b
8559 "},
8560 );
8561
8562 // As soon as the last tab stop is reached, snippet state is gone
8563 editor.move_to_prev_snippet_tabstop(window, cx);
8564 assert(
8565 editor,
8566 cx,
8567 indoc! {"
8568 a.f(one, two, three)ˇ b
8569 a.f(one, two, three)ˇ b
8570 a.f(one, two, three)ˇ b
8571 "},
8572 );
8573 });
8574}
8575
8576#[gpui::test]
8577async fn test_document_format_during_save(cx: &mut TestAppContext) {
8578 init_test(cx, |_| {});
8579
8580 let fs = FakeFs::new(cx.executor());
8581 fs.insert_file(path!("/file.rs"), Default::default()).await;
8582
8583 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8584
8585 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8586 language_registry.add(rust_lang());
8587 let mut fake_servers = language_registry.register_fake_lsp(
8588 "Rust",
8589 FakeLspAdapter {
8590 capabilities: lsp::ServerCapabilities {
8591 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8592 ..Default::default()
8593 },
8594 ..Default::default()
8595 },
8596 );
8597
8598 let buffer = project
8599 .update(cx, |project, cx| {
8600 project.open_local_buffer(path!("/file.rs"), cx)
8601 })
8602 .await
8603 .unwrap();
8604
8605 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8606 let (editor, cx) = cx.add_window_view(|window, cx| {
8607 build_editor_with_project(project.clone(), buffer, window, cx)
8608 });
8609 editor.update_in(cx, |editor, window, cx| {
8610 editor.set_text("one\ntwo\nthree\n", window, cx)
8611 });
8612 assert!(cx.read(|cx| editor.is_dirty(cx)));
8613
8614 cx.executor().start_waiting();
8615 let fake_server = fake_servers.next().await.unwrap();
8616
8617 {
8618 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8619 move |params, _| async move {
8620 assert_eq!(
8621 params.text_document.uri,
8622 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8623 );
8624 assert_eq!(params.options.tab_size, 4);
8625 Ok(Some(vec![lsp::TextEdit::new(
8626 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8627 ", ".to_string(),
8628 )]))
8629 },
8630 );
8631 let save = editor
8632 .update_in(cx, |editor, window, cx| {
8633 editor.save(true, project.clone(), window, cx)
8634 })
8635 .unwrap();
8636 cx.executor().start_waiting();
8637 save.await;
8638
8639 assert_eq!(
8640 editor.update(cx, |editor, cx| editor.text(cx)),
8641 "one, two\nthree\n"
8642 );
8643 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8644 }
8645
8646 {
8647 editor.update_in(cx, |editor, window, cx| {
8648 editor.set_text("one\ntwo\nthree\n", window, cx)
8649 });
8650 assert!(cx.read(|cx| editor.is_dirty(cx)));
8651
8652 // Ensure we can still save even if formatting hangs.
8653 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8654 move |params, _| async move {
8655 assert_eq!(
8656 params.text_document.uri,
8657 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8658 );
8659 futures::future::pending::<()>().await;
8660 unreachable!()
8661 },
8662 );
8663 let save = editor
8664 .update_in(cx, |editor, window, cx| {
8665 editor.save(true, project.clone(), window, cx)
8666 })
8667 .unwrap();
8668 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8669 cx.executor().start_waiting();
8670 save.await;
8671 assert_eq!(
8672 editor.update(cx, |editor, cx| editor.text(cx)),
8673 "one\ntwo\nthree\n"
8674 );
8675 }
8676
8677 // For non-dirty buffer, no formatting request should be sent
8678 {
8679 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8680
8681 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8682 panic!("Should not be invoked on non-dirty buffer");
8683 });
8684 let save = editor
8685 .update_in(cx, |editor, window, cx| {
8686 editor.save(true, project.clone(), window, cx)
8687 })
8688 .unwrap();
8689 cx.executor().start_waiting();
8690 save.await;
8691 }
8692
8693 // Set rust language override and assert overridden tabsize is sent to language server
8694 update_test_language_settings(cx, |settings| {
8695 settings.languages.insert(
8696 "Rust".into(),
8697 LanguageSettingsContent {
8698 tab_size: NonZeroU32::new(8),
8699 ..Default::default()
8700 },
8701 );
8702 });
8703
8704 {
8705 editor.update_in(cx, |editor, window, cx| {
8706 editor.set_text("somehting_new\n", window, cx)
8707 });
8708 assert!(cx.read(|cx| editor.is_dirty(cx)));
8709 let _formatting_request_signal = fake_server
8710 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8711 assert_eq!(
8712 params.text_document.uri,
8713 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8714 );
8715 assert_eq!(params.options.tab_size, 8);
8716 Ok(Some(vec![]))
8717 });
8718 let save = editor
8719 .update_in(cx, |editor, window, cx| {
8720 editor.save(true, project.clone(), window, cx)
8721 })
8722 .unwrap();
8723 cx.executor().start_waiting();
8724 save.await;
8725 }
8726}
8727
8728#[gpui::test]
8729async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8730 init_test(cx, |_| {});
8731
8732 let cols = 4;
8733 let rows = 10;
8734 let sample_text_1 = sample_text(rows, cols, 'a');
8735 assert_eq!(
8736 sample_text_1,
8737 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8738 );
8739 let sample_text_2 = sample_text(rows, cols, 'l');
8740 assert_eq!(
8741 sample_text_2,
8742 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8743 );
8744 let sample_text_3 = sample_text(rows, cols, 'v');
8745 assert_eq!(
8746 sample_text_3,
8747 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8748 );
8749
8750 let fs = FakeFs::new(cx.executor());
8751 fs.insert_tree(
8752 path!("/a"),
8753 json!({
8754 "main.rs": sample_text_1,
8755 "other.rs": sample_text_2,
8756 "lib.rs": sample_text_3,
8757 }),
8758 )
8759 .await;
8760
8761 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8762 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8763 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8764
8765 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8766 language_registry.add(rust_lang());
8767 let mut fake_servers = language_registry.register_fake_lsp(
8768 "Rust",
8769 FakeLspAdapter {
8770 capabilities: lsp::ServerCapabilities {
8771 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8772 ..Default::default()
8773 },
8774 ..Default::default()
8775 },
8776 );
8777
8778 let worktree = project.update(cx, |project, cx| {
8779 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8780 assert_eq!(worktrees.len(), 1);
8781 worktrees.pop().unwrap()
8782 });
8783 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8784
8785 let buffer_1 = project
8786 .update(cx, |project, cx| {
8787 project.open_buffer((worktree_id, "main.rs"), cx)
8788 })
8789 .await
8790 .unwrap();
8791 let buffer_2 = project
8792 .update(cx, |project, cx| {
8793 project.open_buffer((worktree_id, "other.rs"), cx)
8794 })
8795 .await
8796 .unwrap();
8797 let buffer_3 = project
8798 .update(cx, |project, cx| {
8799 project.open_buffer((worktree_id, "lib.rs"), cx)
8800 })
8801 .await
8802 .unwrap();
8803
8804 let multi_buffer = cx.new(|cx| {
8805 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8806 multi_buffer.push_excerpts(
8807 buffer_1.clone(),
8808 [
8809 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8810 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8811 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8812 ],
8813 cx,
8814 );
8815 multi_buffer.push_excerpts(
8816 buffer_2.clone(),
8817 [
8818 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8819 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8820 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8821 ],
8822 cx,
8823 );
8824 multi_buffer.push_excerpts(
8825 buffer_3.clone(),
8826 [
8827 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8828 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8829 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8830 ],
8831 cx,
8832 );
8833 multi_buffer
8834 });
8835 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8836 Editor::new(
8837 EditorMode::full(),
8838 multi_buffer,
8839 Some(project.clone()),
8840 window,
8841 cx,
8842 )
8843 });
8844
8845 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8846 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8847 s.select_ranges(Some(1..2))
8848 });
8849 editor.insert("|one|two|three|", window, cx);
8850 });
8851 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8852 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8853 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8854 s.select_ranges(Some(60..70))
8855 });
8856 editor.insert("|four|five|six|", window, cx);
8857 });
8858 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8859
8860 // First two buffers should be edited, but not the third one.
8861 assert_eq!(
8862 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8863 "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}",
8864 );
8865 buffer_1.update(cx, |buffer, _| {
8866 assert!(buffer.is_dirty());
8867 assert_eq!(
8868 buffer.text(),
8869 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8870 )
8871 });
8872 buffer_2.update(cx, |buffer, _| {
8873 assert!(buffer.is_dirty());
8874 assert_eq!(
8875 buffer.text(),
8876 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8877 )
8878 });
8879 buffer_3.update(cx, |buffer, _| {
8880 assert!(!buffer.is_dirty());
8881 assert_eq!(buffer.text(), sample_text_3,)
8882 });
8883 cx.executor().run_until_parked();
8884
8885 cx.executor().start_waiting();
8886 let save = multi_buffer_editor
8887 .update_in(cx, |editor, window, cx| {
8888 editor.save(true, project.clone(), window, cx)
8889 })
8890 .unwrap();
8891
8892 let fake_server = fake_servers.next().await.unwrap();
8893 fake_server
8894 .server
8895 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8896 Ok(Some(vec![lsp::TextEdit::new(
8897 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8898 format!("[{} formatted]", params.text_document.uri),
8899 )]))
8900 })
8901 .detach();
8902 save.await;
8903
8904 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8905 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8906 assert_eq!(
8907 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8908 uri!(
8909 "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}"
8910 ),
8911 );
8912 buffer_1.update(cx, |buffer, _| {
8913 assert!(!buffer.is_dirty());
8914 assert_eq!(
8915 buffer.text(),
8916 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8917 )
8918 });
8919 buffer_2.update(cx, |buffer, _| {
8920 assert!(!buffer.is_dirty());
8921 assert_eq!(
8922 buffer.text(),
8923 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8924 )
8925 });
8926 buffer_3.update(cx, |buffer, _| {
8927 assert!(!buffer.is_dirty());
8928 assert_eq!(buffer.text(), sample_text_3,)
8929 });
8930}
8931
8932#[gpui::test]
8933async fn test_range_format_during_save(cx: &mut TestAppContext) {
8934 init_test(cx, |_| {});
8935
8936 let fs = FakeFs::new(cx.executor());
8937 fs.insert_file(path!("/file.rs"), Default::default()).await;
8938
8939 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8940
8941 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8942 language_registry.add(rust_lang());
8943 let mut fake_servers = language_registry.register_fake_lsp(
8944 "Rust",
8945 FakeLspAdapter {
8946 capabilities: lsp::ServerCapabilities {
8947 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8948 ..Default::default()
8949 },
8950 ..Default::default()
8951 },
8952 );
8953
8954 let buffer = project
8955 .update(cx, |project, cx| {
8956 project.open_local_buffer(path!("/file.rs"), cx)
8957 })
8958 .await
8959 .unwrap();
8960
8961 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8962 let (editor, cx) = cx.add_window_view(|window, cx| {
8963 build_editor_with_project(project.clone(), buffer, window, cx)
8964 });
8965 editor.update_in(cx, |editor, window, cx| {
8966 editor.set_text("one\ntwo\nthree\n", window, cx)
8967 });
8968 assert!(cx.read(|cx| editor.is_dirty(cx)));
8969
8970 cx.executor().start_waiting();
8971 let fake_server = fake_servers.next().await.unwrap();
8972
8973 let save = editor
8974 .update_in(cx, |editor, window, cx| {
8975 editor.save(true, project.clone(), window, cx)
8976 })
8977 .unwrap();
8978 fake_server
8979 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8980 assert_eq!(
8981 params.text_document.uri,
8982 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8983 );
8984 assert_eq!(params.options.tab_size, 4);
8985 Ok(Some(vec![lsp::TextEdit::new(
8986 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8987 ", ".to_string(),
8988 )]))
8989 })
8990 .next()
8991 .await;
8992 cx.executor().start_waiting();
8993 save.await;
8994 assert_eq!(
8995 editor.update(cx, |editor, cx| editor.text(cx)),
8996 "one, two\nthree\n"
8997 );
8998 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8999
9000 editor.update_in(cx, |editor, window, cx| {
9001 editor.set_text("one\ntwo\nthree\n", window, cx)
9002 });
9003 assert!(cx.read(|cx| editor.is_dirty(cx)));
9004
9005 // Ensure we can still save even if formatting hangs.
9006 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9007 move |params, _| async move {
9008 assert_eq!(
9009 params.text_document.uri,
9010 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9011 );
9012 futures::future::pending::<()>().await;
9013 unreachable!()
9014 },
9015 );
9016 let save = editor
9017 .update_in(cx, |editor, window, cx| {
9018 editor.save(true, project.clone(), window, cx)
9019 })
9020 .unwrap();
9021 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9022 cx.executor().start_waiting();
9023 save.await;
9024 assert_eq!(
9025 editor.update(cx, |editor, cx| editor.text(cx)),
9026 "one\ntwo\nthree\n"
9027 );
9028 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9029
9030 // For non-dirty buffer, no formatting request should be sent
9031 let save = editor
9032 .update_in(cx, |editor, window, cx| {
9033 editor.save(true, project.clone(), window, cx)
9034 })
9035 .unwrap();
9036 let _pending_format_request = fake_server
9037 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9038 panic!("Should not be invoked on non-dirty buffer");
9039 })
9040 .next();
9041 cx.executor().start_waiting();
9042 save.await;
9043
9044 // Set Rust language override and assert overridden tabsize is sent to language server
9045 update_test_language_settings(cx, |settings| {
9046 settings.languages.insert(
9047 "Rust".into(),
9048 LanguageSettingsContent {
9049 tab_size: NonZeroU32::new(8),
9050 ..Default::default()
9051 },
9052 );
9053 });
9054
9055 editor.update_in(cx, |editor, window, cx| {
9056 editor.set_text("somehting_new\n", window, cx)
9057 });
9058 assert!(cx.read(|cx| editor.is_dirty(cx)));
9059 let save = editor
9060 .update_in(cx, |editor, window, cx| {
9061 editor.save(true, project.clone(), window, cx)
9062 })
9063 .unwrap();
9064 fake_server
9065 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9066 assert_eq!(
9067 params.text_document.uri,
9068 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9069 );
9070 assert_eq!(params.options.tab_size, 8);
9071 Ok(Some(vec![]))
9072 })
9073 .next()
9074 .await;
9075 cx.executor().start_waiting();
9076 save.await;
9077}
9078
9079#[gpui::test]
9080async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9081 init_test(cx, |settings| {
9082 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9083 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9084 ))
9085 });
9086
9087 let fs = FakeFs::new(cx.executor());
9088 fs.insert_file(path!("/file.rs"), Default::default()).await;
9089
9090 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9091
9092 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9093 language_registry.add(Arc::new(Language::new(
9094 LanguageConfig {
9095 name: "Rust".into(),
9096 matcher: LanguageMatcher {
9097 path_suffixes: vec!["rs".to_string()],
9098 ..Default::default()
9099 },
9100 ..LanguageConfig::default()
9101 },
9102 Some(tree_sitter_rust::LANGUAGE.into()),
9103 )));
9104 update_test_language_settings(cx, |settings| {
9105 // Enable Prettier formatting for the same buffer, and ensure
9106 // LSP is called instead of Prettier.
9107 settings.defaults.prettier = Some(PrettierSettings {
9108 allowed: true,
9109 ..PrettierSettings::default()
9110 });
9111 });
9112 let mut fake_servers = language_registry.register_fake_lsp(
9113 "Rust",
9114 FakeLspAdapter {
9115 capabilities: lsp::ServerCapabilities {
9116 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9117 ..Default::default()
9118 },
9119 ..Default::default()
9120 },
9121 );
9122
9123 let buffer = project
9124 .update(cx, |project, cx| {
9125 project.open_local_buffer(path!("/file.rs"), cx)
9126 })
9127 .await
9128 .unwrap();
9129
9130 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9131 let (editor, cx) = cx.add_window_view(|window, cx| {
9132 build_editor_with_project(project.clone(), buffer, window, cx)
9133 });
9134 editor.update_in(cx, |editor, window, cx| {
9135 editor.set_text("one\ntwo\nthree\n", window, cx)
9136 });
9137
9138 cx.executor().start_waiting();
9139 let fake_server = fake_servers.next().await.unwrap();
9140
9141 let format = editor
9142 .update_in(cx, |editor, window, cx| {
9143 editor.perform_format(
9144 project.clone(),
9145 FormatTrigger::Manual,
9146 FormatTarget::Buffers,
9147 window,
9148 cx,
9149 )
9150 })
9151 .unwrap();
9152 fake_server
9153 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9154 assert_eq!(
9155 params.text_document.uri,
9156 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9157 );
9158 assert_eq!(params.options.tab_size, 4);
9159 Ok(Some(vec![lsp::TextEdit::new(
9160 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9161 ", ".to_string(),
9162 )]))
9163 })
9164 .next()
9165 .await;
9166 cx.executor().start_waiting();
9167 format.await;
9168 assert_eq!(
9169 editor.update(cx, |editor, cx| editor.text(cx)),
9170 "one, two\nthree\n"
9171 );
9172
9173 editor.update_in(cx, |editor, window, cx| {
9174 editor.set_text("one\ntwo\nthree\n", window, cx)
9175 });
9176 // Ensure we don't lock if formatting hangs.
9177 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9178 move |params, _| async move {
9179 assert_eq!(
9180 params.text_document.uri,
9181 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9182 );
9183 futures::future::pending::<()>().await;
9184 unreachable!()
9185 },
9186 );
9187 let format = editor
9188 .update_in(cx, |editor, window, cx| {
9189 editor.perform_format(
9190 project,
9191 FormatTrigger::Manual,
9192 FormatTarget::Buffers,
9193 window,
9194 cx,
9195 )
9196 })
9197 .unwrap();
9198 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9199 cx.executor().start_waiting();
9200 format.await;
9201 assert_eq!(
9202 editor.update(cx, |editor, cx| editor.text(cx)),
9203 "one\ntwo\nthree\n"
9204 );
9205}
9206
9207#[gpui::test]
9208async fn test_multiple_formatters(cx: &mut TestAppContext) {
9209 init_test(cx, |settings| {
9210 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9211 settings.defaults.formatter =
9212 Some(language_settings::SelectedFormatter::List(FormatterList(
9213 vec![
9214 Formatter::LanguageServer { name: None },
9215 Formatter::CodeActions(
9216 [
9217 ("code-action-1".into(), true),
9218 ("code-action-2".into(), true),
9219 ]
9220 .into_iter()
9221 .collect(),
9222 ),
9223 ]
9224 .into(),
9225 )))
9226 });
9227
9228 let fs = FakeFs::new(cx.executor());
9229 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9230 .await;
9231
9232 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9233 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9234 language_registry.add(rust_lang());
9235
9236 let mut fake_servers = language_registry.register_fake_lsp(
9237 "Rust",
9238 FakeLspAdapter {
9239 capabilities: lsp::ServerCapabilities {
9240 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9241 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9242 commands: vec!["the-command-for-code-action-1".into()],
9243 ..Default::default()
9244 }),
9245 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9246 ..Default::default()
9247 },
9248 ..Default::default()
9249 },
9250 );
9251
9252 let buffer = project
9253 .update(cx, |project, cx| {
9254 project.open_local_buffer(path!("/file.rs"), cx)
9255 })
9256 .await
9257 .unwrap();
9258
9259 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9260 let (editor, cx) = cx.add_window_view(|window, cx| {
9261 build_editor_with_project(project.clone(), buffer, window, cx)
9262 });
9263
9264 cx.executor().start_waiting();
9265
9266 let fake_server = fake_servers.next().await.unwrap();
9267 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9268 move |_params, _| async move {
9269 Ok(Some(vec![lsp::TextEdit::new(
9270 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9271 "applied-formatting\n".to_string(),
9272 )]))
9273 },
9274 );
9275 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9276 move |params, _| async move {
9277 assert_eq!(
9278 params.context.only,
9279 Some(vec!["code-action-1".into(), "code-action-2".into()])
9280 );
9281 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9282 Ok(Some(vec![
9283 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9284 kind: Some("code-action-1".into()),
9285 edit: Some(lsp::WorkspaceEdit::new(
9286 [(
9287 uri.clone(),
9288 vec![lsp::TextEdit::new(
9289 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9290 "applied-code-action-1-edit\n".to_string(),
9291 )],
9292 )]
9293 .into_iter()
9294 .collect(),
9295 )),
9296 command: Some(lsp::Command {
9297 command: "the-command-for-code-action-1".into(),
9298 ..Default::default()
9299 }),
9300 ..Default::default()
9301 }),
9302 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9303 kind: Some("code-action-2".into()),
9304 edit: Some(lsp::WorkspaceEdit::new(
9305 [(
9306 uri.clone(),
9307 vec![lsp::TextEdit::new(
9308 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9309 "applied-code-action-2-edit\n".to_string(),
9310 )],
9311 )]
9312 .into_iter()
9313 .collect(),
9314 )),
9315 ..Default::default()
9316 }),
9317 ]))
9318 },
9319 );
9320
9321 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9322 move |params, _| async move { Ok(params) }
9323 });
9324
9325 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9326 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9327 let fake = fake_server.clone();
9328 let lock = command_lock.clone();
9329 move |params, _| {
9330 assert_eq!(params.command, "the-command-for-code-action-1");
9331 let fake = fake.clone();
9332 let lock = lock.clone();
9333 async move {
9334 lock.lock().await;
9335 fake.server
9336 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9337 label: None,
9338 edit: lsp::WorkspaceEdit {
9339 changes: Some(
9340 [(
9341 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9342 vec![lsp::TextEdit {
9343 range: lsp::Range::new(
9344 lsp::Position::new(0, 0),
9345 lsp::Position::new(0, 0),
9346 ),
9347 new_text: "applied-code-action-1-command\n".into(),
9348 }],
9349 )]
9350 .into_iter()
9351 .collect(),
9352 ),
9353 ..Default::default()
9354 },
9355 })
9356 .await
9357 .into_response()
9358 .unwrap();
9359 Ok(Some(json!(null)))
9360 }
9361 }
9362 });
9363
9364 cx.executor().start_waiting();
9365 editor
9366 .update_in(cx, |editor, window, cx| {
9367 editor.perform_format(
9368 project.clone(),
9369 FormatTrigger::Manual,
9370 FormatTarget::Buffers,
9371 window,
9372 cx,
9373 )
9374 })
9375 .unwrap()
9376 .await;
9377 editor.update(cx, |editor, cx| {
9378 assert_eq!(
9379 editor.text(cx),
9380 r#"
9381 applied-code-action-2-edit
9382 applied-code-action-1-command
9383 applied-code-action-1-edit
9384 applied-formatting
9385 one
9386 two
9387 three
9388 "#
9389 .unindent()
9390 );
9391 });
9392
9393 editor.update_in(cx, |editor, window, cx| {
9394 editor.undo(&Default::default(), window, cx);
9395 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9396 });
9397
9398 // Perform a manual edit while waiting for an LSP command
9399 // that's being run as part of a formatting code action.
9400 let lock_guard = command_lock.lock().await;
9401 let format = editor
9402 .update_in(cx, |editor, window, cx| {
9403 editor.perform_format(
9404 project.clone(),
9405 FormatTrigger::Manual,
9406 FormatTarget::Buffers,
9407 window,
9408 cx,
9409 )
9410 })
9411 .unwrap();
9412 cx.run_until_parked();
9413 editor.update(cx, |editor, cx| {
9414 assert_eq!(
9415 editor.text(cx),
9416 r#"
9417 applied-code-action-1-edit
9418 applied-formatting
9419 one
9420 two
9421 three
9422 "#
9423 .unindent()
9424 );
9425
9426 editor.buffer.update(cx, |buffer, cx| {
9427 let ix = buffer.len(cx);
9428 buffer.edit([(ix..ix, "edited\n")], None, cx);
9429 });
9430 });
9431
9432 // Allow the LSP command to proceed. Because the buffer was edited,
9433 // the second code action will not be run.
9434 drop(lock_guard);
9435 format.await;
9436 editor.update_in(cx, |editor, window, cx| {
9437 assert_eq!(
9438 editor.text(cx),
9439 r#"
9440 applied-code-action-1-command
9441 applied-code-action-1-edit
9442 applied-formatting
9443 one
9444 two
9445 three
9446 edited
9447 "#
9448 .unindent()
9449 );
9450
9451 // The manual edit is undone first, because it is the last thing the user did
9452 // (even though the command completed afterwards).
9453 editor.undo(&Default::default(), window, cx);
9454 assert_eq!(
9455 editor.text(cx),
9456 r#"
9457 applied-code-action-1-command
9458 applied-code-action-1-edit
9459 applied-formatting
9460 one
9461 two
9462 three
9463 "#
9464 .unindent()
9465 );
9466
9467 // All the formatting (including the command, which completed after the manual edit)
9468 // is undone together.
9469 editor.undo(&Default::default(), window, cx);
9470 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9471 });
9472}
9473
9474#[gpui::test]
9475async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9476 init_test(cx, |settings| {
9477 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9478 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9479 ))
9480 });
9481
9482 let fs = FakeFs::new(cx.executor());
9483 fs.insert_file(path!("/file.ts"), Default::default()).await;
9484
9485 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9486
9487 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9488 language_registry.add(Arc::new(Language::new(
9489 LanguageConfig {
9490 name: "TypeScript".into(),
9491 matcher: LanguageMatcher {
9492 path_suffixes: vec!["ts".to_string()],
9493 ..Default::default()
9494 },
9495 ..LanguageConfig::default()
9496 },
9497 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9498 )));
9499 update_test_language_settings(cx, |settings| {
9500 settings.defaults.prettier = Some(PrettierSettings {
9501 allowed: true,
9502 ..PrettierSettings::default()
9503 });
9504 });
9505 let mut fake_servers = language_registry.register_fake_lsp(
9506 "TypeScript",
9507 FakeLspAdapter {
9508 capabilities: lsp::ServerCapabilities {
9509 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9510 ..Default::default()
9511 },
9512 ..Default::default()
9513 },
9514 );
9515
9516 let buffer = project
9517 .update(cx, |project, cx| {
9518 project.open_local_buffer(path!("/file.ts"), cx)
9519 })
9520 .await
9521 .unwrap();
9522
9523 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9524 let (editor, cx) = cx.add_window_view(|window, cx| {
9525 build_editor_with_project(project.clone(), buffer, window, cx)
9526 });
9527 editor.update_in(cx, |editor, window, cx| {
9528 editor.set_text(
9529 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9530 window,
9531 cx,
9532 )
9533 });
9534
9535 cx.executor().start_waiting();
9536 let fake_server = fake_servers.next().await.unwrap();
9537
9538 let format = editor
9539 .update_in(cx, |editor, window, cx| {
9540 editor.perform_code_action_kind(
9541 project.clone(),
9542 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9543 window,
9544 cx,
9545 )
9546 })
9547 .unwrap();
9548 fake_server
9549 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9550 assert_eq!(
9551 params.text_document.uri,
9552 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9553 );
9554 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9555 lsp::CodeAction {
9556 title: "Organize Imports".to_string(),
9557 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9558 edit: Some(lsp::WorkspaceEdit {
9559 changes: Some(
9560 [(
9561 params.text_document.uri.clone(),
9562 vec![lsp::TextEdit::new(
9563 lsp::Range::new(
9564 lsp::Position::new(1, 0),
9565 lsp::Position::new(2, 0),
9566 ),
9567 "".to_string(),
9568 )],
9569 )]
9570 .into_iter()
9571 .collect(),
9572 ),
9573 ..Default::default()
9574 }),
9575 ..Default::default()
9576 },
9577 )]))
9578 })
9579 .next()
9580 .await;
9581 cx.executor().start_waiting();
9582 format.await;
9583 assert_eq!(
9584 editor.update(cx, |editor, cx| editor.text(cx)),
9585 "import { a } from 'module';\n\nconst x = a;\n"
9586 );
9587
9588 editor.update_in(cx, |editor, window, cx| {
9589 editor.set_text(
9590 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9591 window,
9592 cx,
9593 )
9594 });
9595 // Ensure we don't lock if code action hangs.
9596 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9597 move |params, _| async move {
9598 assert_eq!(
9599 params.text_document.uri,
9600 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9601 );
9602 futures::future::pending::<()>().await;
9603 unreachable!()
9604 },
9605 );
9606 let format = editor
9607 .update_in(cx, |editor, window, cx| {
9608 editor.perform_code_action_kind(
9609 project,
9610 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9611 window,
9612 cx,
9613 )
9614 })
9615 .unwrap();
9616 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9617 cx.executor().start_waiting();
9618 format.await;
9619 assert_eq!(
9620 editor.update(cx, |editor, cx| editor.text(cx)),
9621 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9622 );
9623}
9624
9625#[gpui::test]
9626async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9627 init_test(cx, |_| {});
9628
9629 let mut cx = EditorLspTestContext::new_rust(
9630 lsp::ServerCapabilities {
9631 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9632 ..Default::default()
9633 },
9634 cx,
9635 )
9636 .await;
9637
9638 cx.set_state(indoc! {"
9639 one.twoˇ
9640 "});
9641
9642 // The format request takes a long time. When it completes, it inserts
9643 // a newline and an indent before the `.`
9644 cx.lsp
9645 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9646 let executor = cx.background_executor().clone();
9647 async move {
9648 executor.timer(Duration::from_millis(100)).await;
9649 Ok(Some(vec![lsp::TextEdit {
9650 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9651 new_text: "\n ".into(),
9652 }]))
9653 }
9654 });
9655
9656 // Submit a format request.
9657 let format_1 = cx
9658 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9659 .unwrap();
9660 cx.executor().run_until_parked();
9661
9662 // Submit a second format request.
9663 let format_2 = cx
9664 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9665 .unwrap();
9666 cx.executor().run_until_parked();
9667
9668 // Wait for both format requests to complete
9669 cx.executor().advance_clock(Duration::from_millis(200));
9670 cx.executor().start_waiting();
9671 format_1.await.unwrap();
9672 cx.executor().start_waiting();
9673 format_2.await.unwrap();
9674
9675 // The formatting edits only happens once.
9676 cx.assert_editor_state(indoc! {"
9677 one
9678 .twoˇ
9679 "});
9680}
9681
9682#[gpui::test]
9683async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9684 init_test(cx, |settings| {
9685 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9686 });
9687
9688 let mut cx = EditorLspTestContext::new_rust(
9689 lsp::ServerCapabilities {
9690 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9691 ..Default::default()
9692 },
9693 cx,
9694 )
9695 .await;
9696
9697 // Set up a buffer white some trailing whitespace and no trailing newline.
9698 cx.set_state(
9699 &[
9700 "one ", //
9701 "twoˇ", //
9702 "three ", //
9703 "four", //
9704 ]
9705 .join("\n"),
9706 );
9707
9708 // Submit a format request.
9709 let format = cx
9710 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9711 .unwrap();
9712
9713 // Record which buffer changes have been sent to the language server
9714 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9715 cx.lsp
9716 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9717 let buffer_changes = buffer_changes.clone();
9718 move |params, _| {
9719 buffer_changes.lock().extend(
9720 params
9721 .content_changes
9722 .into_iter()
9723 .map(|e| (e.range.unwrap(), e.text)),
9724 );
9725 }
9726 });
9727
9728 // Handle formatting requests to the language server.
9729 cx.lsp
9730 .set_request_handler::<lsp::request::Formatting, _, _>({
9731 let buffer_changes = buffer_changes.clone();
9732 move |_, _| {
9733 // When formatting is requested, trailing whitespace has already been stripped,
9734 // and the trailing newline has already been added.
9735 assert_eq!(
9736 &buffer_changes.lock()[1..],
9737 &[
9738 (
9739 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9740 "".into()
9741 ),
9742 (
9743 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9744 "".into()
9745 ),
9746 (
9747 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9748 "\n".into()
9749 ),
9750 ]
9751 );
9752
9753 // Insert blank lines between each line of the buffer.
9754 async move {
9755 Ok(Some(vec![
9756 lsp::TextEdit {
9757 range: lsp::Range::new(
9758 lsp::Position::new(1, 0),
9759 lsp::Position::new(1, 0),
9760 ),
9761 new_text: "\n".into(),
9762 },
9763 lsp::TextEdit {
9764 range: lsp::Range::new(
9765 lsp::Position::new(2, 0),
9766 lsp::Position::new(2, 0),
9767 ),
9768 new_text: "\n".into(),
9769 },
9770 ]))
9771 }
9772 }
9773 });
9774
9775 // After formatting the buffer, the trailing whitespace is stripped,
9776 // a newline is appended, and the edits provided by the language server
9777 // have been applied.
9778 format.await.unwrap();
9779 cx.assert_editor_state(
9780 &[
9781 "one", //
9782 "", //
9783 "twoˇ", //
9784 "", //
9785 "three", //
9786 "four", //
9787 "", //
9788 ]
9789 .join("\n"),
9790 );
9791
9792 // Undoing the formatting undoes the trailing whitespace removal, the
9793 // trailing newline, and the LSP edits.
9794 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9795 cx.assert_editor_state(
9796 &[
9797 "one ", //
9798 "twoˇ", //
9799 "three ", //
9800 "four", //
9801 ]
9802 .join("\n"),
9803 );
9804}
9805
9806#[gpui::test]
9807async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9808 cx: &mut TestAppContext,
9809) {
9810 init_test(cx, |_| {});
9811
9812 cx.update(|cx| {
9813 cx.update_global::<SettingsStore, _>(|settings, cx| {
9814 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9815 settings.auto_signature_help = Some(true);
9816 });
9817 });
9818 });
9819
9820 let mut cx = EditorLspTestContext::new_rust(
9821 lsp::ServerCapabilities {
9822 signature_help_provider: Some(lsp::SignatureHelpOptions {
9823 ..Default::default()
9824 }),
9825 ..Default::default()
9826 },
9827 cx,
9828 )
9829 .await;
9830
9831 let language = Language::new(
9832 LanguageConfig {
9833 name: "Rust".into(),
9834 brackets: BracketPairConfig {
9835 pairs: vec![
9836 BracketPair {
9837 start: "{".to_string(),
9838 end: "}".to_string(),
9839 close: true,
9840 surround: true,
9841 newline: true,
9842 },
9843 BracketPair {
9844 start: "(".to_string(),
9845 end: ")".to_string(),
9846 close: true,
9847 surround: true,
9848 newline: true,
9849 },
9850 BracketPair {
9851 start: "/*".to_string(),
9852 end: " */".to_string(),
9853 close: true,
9854 surround: true,
9855 newline: true,
9856 },
9857 BracketPair {
9858 start: "[".to_string(),
9859 end: "]".to_string(),
9860 close: false,
9861 surround: false,
9862 newline: true,
9863 },
9864 BracketPair {
9865 start: "\"".to_string(),
9866 end: "\"".to_string(),
9867 close: true,
9868 surround: true,
9869 newline: false,
9870 },
9871 BracketPair {
9872 start: "<".to_string(),
9873 end: ">".to_string(),
9874 close: false,
9875 surround: true,
9876 newline: true,
9877 },
9878 ],
9879 ..Default::default()
9880 },
9881 autoclose_before: "})]".to_string(),
9882 ..Default::default()
9883 },
9884 Some(tree_sitter_rust::LANGUAGE.into()),
9885 );
9886 let language = Arc::new(language);
9887
9888 cx.language_registry().add(language.clone());
9889 cx.update_buffer(|buffer, cx| {
9890 buffer.set_language(Some(language), cx);
9891 });
9892
9893 cx.set_state(
9894 &r#"
9895 fn main() {
9896 sampleˇ
9897 }
9898 "#
9899 .unindent(),
9900 );
9901
9902 cx.update_editor(|editor, window, cx| {
9903 editor.handle_input("(", window, cx);
9904 });
9905 cx.assert_editor_state(
9906 &"
9907 fn main() {
9908 sample(ˇ)
9909 }
9910 "
9911 .unindent(),
9912 );
9913
9914 let mocked_response = lsp::SignatureHelp {
9915 signatures: vec![lsp::SignatureInformation {
9916 label: "fn sample(param1: u8, param2: u8)".to_string(),
9917 documentation: None,
9918 parameters: Some(vec![
9919 lsp::ParameterInformation {
9920 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9921 documentation: None,
9922 },
9923 lsp::ParameterInformation {
9924 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9925 documentation: None,
9926 },
9927 ]),
9928 active_parameter: None,
9929 }],
9930 active_signature: Some(0),
9931 active_parameter: Some(0),
9932 };
9933 handle_signature_help_request(&mut cx, mocked_response).await;
9934
9935 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9936 .await;
9937
9938 cx.editor(|editor, _, _| {
9939 let signature_help_state = editor.signature_help_state.popover().cloned();
9940 assert_eq!(
9941 signature_help_state.unwrap().label,
9942 "param1: u8, param2: u8"
9943 );
9944 });
9945}
9946
9947#[gpui::test]
9948async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9949 init_test(cx, |_| {});
9950
9951 cx.update(|cx| {
9952 cx.update_global::<SettingsStore, _>(|settings, cx| {
9953 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9954 settings.auto_signature_help = Some(false);
9955 settings.show_signature_help_after_edits = Some(false);
9956 });
9957 });
9958 });
9959
9960 let mut cx = EditorLspTestContext::new_rust(
9961 lsp::ServerCapabilities {
9962 signature_help_provider: Some(lsp::SignatureHelpOptions {
9963 ..Default::default()
9964 }),
9965 ..Default::default()
9966 },
9967 cx,
9968 )
9969 .await;
9970
9971 let language = Language::new(
9972 LanguageConfig {
9973 name: "Rust".into(),
9974 brackets: BracketPairConfig {
9975 pairs: vec![
9976 BracketPair {
9977 start: "{".to_string(),
9978 end: "}".to_string(),
9979 close: true,
9980 surround: true,
9981 newline: true,
9982 },
9983 BracketPair {
9984 start: "(".to_string(),
9985 end: ")".to_string(),
9986 close: true,
9987 surround: true,
9988 newline: true,
9989 },
9990 BracketPair {
9991 start: "/*".to_string(),
9992 end: " */".to_string(),
9993 close: true,
9994 surround: true,
9995 newline: true,
9996 },
9997 BracketPair {
9998 start: "[".to_string(),
9999 end: "]".to_string(),
10000 close: false,
10001 surround: false,
10002 newline: true,
10003 },
10004 BracketPair {
10005 start: "\"".to_string(),
10006 end: "\"".to_string(),
10007 close: true,
10008 surround: true,
10009 newline: false,
10010 },
10011 BracketPair {
10012 start: "<".to_string(),
10013 end: ">".to_string(),
10014 close: false,
10015 surround: true,
10016 newline: true,
10017 },
10018 ],
10019 ..Default::default()
10020 },
10021 autoclose_before: "})]".to_string(),
10022 ..Default::default()
10023 },
10024 Some(tree_sitter_rust::LANGUAGE.into()),
10025 );
10026 let language = Arc::new(language);
10027
10028 cx.language_registry().add(language.clone());
10029 cx.update_buffer(|buffer, cx| {
10030 buffer.set_language(Some(language), cx);
10031 });
10032
10033 // Ensure that signature_help is not called when no signature help is enabled.
10034 cx.set_state(
10035 &r#"
10036 fn main() {
10037 sampleˇ
10038 }
10039 "#
10040 .unindent(),
10041 );
10042 cx.update_editor(|editor, window, cx| {
10043 editor.handle_input("(", window, cx);
10044 });
10045 cx.assert_editor_state(
10046 &"
10047 fn main() {
10048 sample(ˇ)
10049 }
10050 "
10051 .unindent(),
10052 );
10053 cx.editor(|editor, _, _| {
10054 assert!(editor.signature_help_state.task().is_none());
10055 });
10056
10057 let mocked_response = lsp::SignatureHelp {
10058 signatures: vec![lsp::SignatureInformation {
10059 label: "fn sample(param1: u8, param2: u8)".to_string(),
10060 documentation: None,
10061 parameters: Some(vec![
10062 lsp::ParameterInformation {
10063 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10064 documentation: None,
10065 },
10066 lsp::ParameterInformation {
10067 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10068 documentation: None,
10069 },
10070 ]),
10071 active_parameter: None,
10072 }],
10073 active_signature: Some(0),
10074 active_parameter: Some(0),
10075 };
10076
10077 // Ensure that signature_help is called when enabled afte edits
10078 cx.update(|_, cx| {
10079 cx.update_global::<SettingsStore, _>(|settings, cx| {
10080 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10081 settings.auto_signature_help = Some(false);
10082 settings.show_signature_help_after_edits = Some(true);
10083 });
10084 });
10085 });
10086 cx.set_state(
10087 &r#"
10088 fn main() {
10089 sampleˇ
10090 }
10091 "#
10092 .unindent(),
10093 );
10094 cx.update_editor(|editor, window, cx| {
10095 editor.handle_input("(", window, cx);
10096 });
10097 cx.assert_editor_state(
10098 &"
10099 fn main() {
10100 sample(ˇ)
10101 }
10102 "
10103 .unindent(),
10104 );
10105 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10106 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10107 .await;
10108 cx.update_editor(|editor, _, _| {
10109 let signature_help_state = editor.signature_help_state.popover().cloned();
10110 assert!(signature_help_state.is_some());
10111 assert_eq!(
10112 signature_help_state.unwrap().label,
10113 "param1: u8, param2: u8"
10114 );
10115 editor.signature_help_state = SignatureHelpState::default();
10116 });
10117
10118 // Ensure that signature_help is called when auto signature help override is enabled
10119 cx.update(|_, cx| {
10120 cx.update_global::<SettingsStore, _>(|settings, cx| {
10121 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10122 settings.auto_signature_help = Some(true);
10123 settings.show_signature_help_after_edits = Some(false);
10124 });
10125 });
10126 });
10127 cx.set_state(
10128 &r#"
10129 fn main() {
10130 sampleˇ
10131 }
10132 "#
10133 .unindent(),
10134 );
10135 cx.update_editor(|editor, window, cx| {
10136 editor.handle_input("(", window, cx);
10137 });
10138 cx.assert_editor_state(
10139 &"
10140 fn main() {
10141 sample(ˇ)
10142 }
10143 "
10144 .unindent(),
10145 );
10146 handle_signature_help_request(&mut cx, mocked_response).await;
10147 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10148 .await;
10149 cx.editor(|editor, _, _| {
10150 let signature_help_state = editor.signature_help_state.popover().cloned();
10151 assert!(signature_help_state.is_some());
10152 assert_eq!(
10153 signature_help_state.unwrap().label,
10154 "param1: u8, param2: u8"
10155 );
10156 });
10157}
10158
10159#[gpui::test]
10160async fn test_signature_help(cx: &mut TestAppContext) {
10161 init_test(cx, |_| {});
10162 cx.update(|cx| {
10163 cx.update_global::<SettingsStore, _>(|settings, cx| {
10164 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10165 settings.auto_signature_help = Some(true);
10166 });
10167 });
10168 });
10169
10170 let mut cx = EditorLspTestContext::new_rust(
10171 lsp::ServerCapabilities {
10172 signature_help_provider: Some(lsp::SignatureHelpOptions {
10173 ..Default::default()
10174 }),
10175 ..Default::default()
10176 },
10177 cx,
10178 )
10179 .await;
10180
10181 // A test that directly calls `show_signature_help`
10182 cx.update_editor(|editor, window, cx| {
10183 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10184 });
10185
10186 let mocked_response = lsp::SignatureHelp {
10187 signatures: vec![lsp::SignatureInformation {
10188 label: "fn sample(param1: u8, param2: u8)".to_string(),
10189 documentation: None,
10190 parameters: Some(vec![
10191 lsp::ParameterInformation {
10192 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10193 documentation: None,
10194 },
10195 lsp::ParameterInformation {
10196 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10197 documentation: None,
10198 },
10199 ]),
10200 active_parameter: None,
10201 }],
10202 active_signature: Some(0),
10203 active_parameter: Some(0),
10204 };
10205 handle_signature_help_request(&mut cx, mocked_response).await;
10206
10207 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10208 .await;
10209
10210 cx.editor(|editor, _, _| {
10211 let signature_help_state = editor.signature_help_state.popover().cloned();
10212 assert!(signature_help_state.is_some());
10213 assert_eq!(
10214 signature_help_state.unwrap().label,
10215 "param1: u8, param2: u8"
10216 );
10217 });
10218
10219 // When exiting outside from inside the brackets, `signature_help` is closed.
10220 cx.set_state(indoc! {"
10221 fn main() {
10222 sample(ˇ);
10223 }
10224
10225 fn sample(param1: u8, param2: u8) {}
10226 "});
10227
10228 cx.update_editor(|editor, window, cx| {
10229 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10230 });
10231
10232 let mocked_response = lsp::SignatureHelp {
10233 signatures: Vec::new(),
10234 active_signature: None,
10235 active_parameter: None,
10236 };
10237 handle_signature_help_request(&mut cx, mocked_response).await;
10238
10239 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10240 .await;
10241
10242 cx.editor(|editor, _, _| {
10243 assert!(!editor.signature_help_state.is_shown());
10244 });
10245
10246 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10247 cx.set_state(indoc! {"
10248 fn main() {
10249 sample(ˇ);
10250 }
10251
10252 fn sample(param1: u8, param2: u8) {}
10253 "});
10254
10255 let mocked_response = lsp::SignatureHelp {
10256 signatures: vec![lsp::SignatureInformation {
10257 label: "fn sample(param1: u8, param2: u8)".to_string(),
10258 documentation: None,
10259 parameters: Some(vec![
10260 lsp::ParameterInformation {
10261 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10262 documentation: None,
10263 },
10264 lsp::ParameterInformation {
10265 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10266 documentation: None,
10267 },
10268 ]),
10269 active_parameter: None,
10270 }],
10271 active_signature: Some(0),
10272 active_parameter: Some(0),
10273 };
10274 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10275 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10276 .await;
10277 cx.editor(|editor, _, _| {
10278 assert!(editor.signature_help_state.is_shown());
10279 });
10280
10281 // Restore the popover with more parameter input
10282 cx.set_state(indoc! {"
10283 fn main() {
10284 sample(param1, param2ˇ);
10285 }
10286
10287 fn sample(param1: u8, param2: u8) {}
10288 "});
10289
10290 let mocked_response = lsp::SignatureHelp {
10291 signatures: vec![lsp::SignatureInformation {
10292 label: "fn sample(param1: u8, param2: u8)".to_string(),
10293 documentation: None,
10294 parameters: Some(vec![
10295 lsp::ParameterInformation {
10296 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10297 documentation: None,
10298 },
10299 lsp::ParameterInformation {
10300 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10301 documentation: None,
10302 },
10303 ]),
10304 active_parameter: None,
10305 }],
10306 active_signature: Some(0),
10307 active_parameter: Some(1),
10308 };
10309 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10310 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10311 .await;
10312
10313 // When selecting a range, the popover is gone.
10314 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10315 cx.update_editor(|editor, window, cx| {
10316 editor.change_selections(None, window, cx, |s| {
10317 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10318 })
10319 });
10320 cx.assert_editor_state(indoc! {"
10321 fn main() {
10322 sample(param1, «ˇparam2»);
10323 }
10324
10325 fn sample(param1: u8, param2: u8) {}
10326 "});
10327 cx.editor(|editor, _, _| {
10328 assert!(!editor.signature_help_state.is_shown());
10329 });
10330
10331 // When unselecting again, the popover is back if within the brackets.
10332 cx.update_editor(|editor, window, cx| {
10333 editor.change_selections(None, window, cx, |s| {
10334 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10335 })
10336 });
10337 cx.assert_editor_state(indoc! {"
10338 fn main() {
10339 sample(param1, ˇparam2);
10340 }
10341
10342 fn sample(param1: u8, param2: u8) {}
10343 "});
10344 handle_signature_help_request(&mut cx, mocked_response).await;
10345 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10346 .await;
10347 cx.editor(|editor, _, _| {
10348 assert!(editor.signature_help_state.is_shown());
10349 });
10350
10351 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10352 cx.update_editor(|editor, window, cx| {
10353 editor.change_selections(None, window, cx, |s| {
10354 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10355 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10356 })
10357 });
10358 cx.assert_editor_state(indoc! {"
10359 fn main() {
10360 sample(param1, ˇparam2);
10361 }
10362
10363 fn sample(param1: u8, param2: u8) {}
10364 "});
10365
10366 let mocked_response = lsp::SignatureHelp {
10367 signatures: vec![lsp::SignatureInformation {
10368 label: "fn sample(param1: u8, param2: u8)".to_string(),
10369 documentation: None,
10370 parameters: Some(vec![
10371 lsp::ParameterInformation {
10372 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10373 documentation: None,
10374 },
10375 lsp::ParameterInformation {
10376 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10377 documentation: None,
10378 },
10379 ]),
10380 active_parameter: None,
10381 }],
10382 active_signature: Some(0),
10383 active_parameter: Some(1),
10384 };
10385 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10386 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10387 .await;
10388 cx.update_editor(|editor, _, cx| {
10389 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10390 });
10391 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10392 .await;
10393 cx.update_editor(|editor, window, cx| {
10394 editor.change_selections(None, window, cx, |s| {
10395 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10396 })
10397 });
10398 cx.assert_editor_state(indoc! {"
10399 fn main() {
10400 sample(param1, «ˇparam2»);
10401 }
10402
10403 fn sample(param1: u8, param2: u8) {}
10404 "});
10405 cx.update_editor(|editor, window, cx| {
10406 editor.change_selections(None, window, cx, |s| {
10407 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10408 })
10409 });
10410 cx.assert_editor_state(indoc! {"
10411 fn main() {
10412 sample(param1, ˇparam2);
10413 }
10414
10415 fn sample(param1: u8, param2: u8) {}
10416 "});
10417 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10418 .await;
10419}
10420
10421#[gpui::test]
10422async fn test_completion_mode(cx: &mut TestAppContext) {
10423 init_test(cx, |_| {});
10424 let mut cx = EditorLspTestContext::new_rust(
10425 lsp::ServerCapabilities {
10426 completion_provider: Some(lsp::CompletionOptions {
10427 resolve_provider: Some(true),
10428 ..Default::default()
10429 }),
10430 ..Default::default()
10431 },
10432 cx,
10433 )
10434 .await;
10435
10436 struct Run {
10437 run_description: &'static str,
10438 initial_state: String,
10439 buffer_marked_text: String,
10440 completion_text: &'static str,
10441 expected_with_insert_mode: String,
10442 expected_with_replace_mode: String,
10443 expected_with_replace_subsequence_mode: String,
10444 expected_with_replace_suffix_mode: String,
10445 }
10446
10447 let runs = [
10448 Run {
10449 run_description: "Start of word matches completion text",
10450 initial_state: "before ediˇ after".into(),
10451 buffer_marked_text: "before <edi|> after".into(),
10452 completion_text: "editor",
10453 expected_with_insert_mode: "before editorˇ after".into(),
10454 expected_with_replace_mode: "before editorˇ after".into(),
10455 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10456 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10457 },
10458 Run {
10459 run_description: "Accept same text at the middle of the word",
10460 initial_state: "before ediˇtor after".into(),
10461 buffer_marked_text: "before <edi|tor> after".into(),
10462 completion_text: "editor",
10463 expected_with_insert_mode: "before editorˇtor after".into(),
10464 expected_with_replace_mode: "before editorˇ after".into(),
10465 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10466 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10467 },
10468 Run {
10469 run_description: "End of word matches completion text -- cursor at end",
10470 initial_state: "before torˇ after".into(),
10471 buffer_marked_text: "before <tor|> after".into(),
10472 completion_text: "editor",
10473 expected_with_insert_mode: "before editorˇ after".into(),
10474 expected_with_replace_mode: "before editorˇ after".into(),
10475 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10476 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10477 },
10478 Run {
10479 run_description: "End of word matches completion text -- cursor at start",
10480 initial_state: "before ˇtor after".into(),
10481 buffer_marked_text: "before <|tor> after".into(),
10482 completion_text: "editor",
10483 expected_with_insert_mode: "before editorˇtor after".into(),
10484 expected_with_replace_mode: "before editorˇ after".into(),
10485 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10486 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10487 },
10488 Run {
10489 run_description: "Prepend text containing whitespace",
10490 initial_state: "pˇfield: bool".into(),
10491 buffer_marked_text: "<p|field>: bool".into(),
10492 completion_text: "pub ",
10493 expected_with_insert_mode: "pub ˇfield: bool".into(),
10494 expected_with_replace_mode: "pub ˇ: bool".into(),
10495 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10496 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10497 },
10498 Run {
10499 run_description: "Add element to start of list",
10500 initial_state: "[element_ˇelement_2]".into(),
10501 buffer_marked_text: "[<element_|element_2>]".into(),
10502 completion_text: "element_1",
10503 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10504 expected_with_replace_mode: "[element_1ˇ]".into(),
10505 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10506 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10507 },
10508 Run {
10509 run_description: "Add element to start of list -- first and second elements are equal",
10510 initial_state: "[elˇelement]".into(),
10511 buffer_marked_text: "[<el|element>]".into(),
10512 completion_text: "element",
10513 expected_with_insert_mode: "[elementˇelement]".into(),
10514 expected_with_replace_mode: "[elementˇ]".into(),
10515 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10516 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10517 },
10518 Run {
10519 run_description: "Ends with matching suffix",
10520 initial_state: "SubˇError".into(),
10521 buffer_marked_text: "<Sub|Error>".into(),
10522 completion_text: "SubscriptionError",
10523 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10524 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10525 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10526 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10527 },
10528 Run {
10529 run_description: "Suffix is a subsequence -- contiguous",
10530 initial_state: "SubˇErr".into(),
10531 buffer_marked_text: "<Sub|Err>".into(),
10532 completion_text: "SubscriptionError",
10533 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10534 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10535 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10536 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10537 },
10538 Run {
10539 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10540 initial_state: "Suˇscrirr".into(),
10541 buffer_marked_text: "<Su|scrirr>".into(),
10542 completion_text: "SubscriptionError",
10543 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10544 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10545 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10546 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10547 },
10548 Run {
10549 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10550 initial_state: "foo(indˇix)".into(),
10551 buffer_marked_text: "foo(<ind|ix>)".into(),
10552 completion_text: "node_index",
10553 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10554 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10555 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10556 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10557 },
10558 ];
10559
10560 for run in runs {
10561 let run_variations = [
10562 (LspInsertMode::Insert, run.expected_with_insert_mode),
10563 (LspInsertMode::Replace, run.expected_with_replace_mode),
10564 (
10565 LspInsertMode::ReplaceSubsequence,
10566 run.expected_with_replace_subsequence_mode,
10567 ),
10568 (
10569 LspInsertMode::ReplaceSuffix,
10570 run.expected_with_replace_suffix_mode,
10571 ),
10572 ];
10573
10574 for (lsp_insert_mode, expected_text) in run_variations {
10575 eprintln!(
10576 "run = {:?}, mode = {lsp_insert_mode:.?}",
10577 run.run_description,
10578 );
10579
10580 update_test_language_settings(&mut cx, |settings| {
10581 settings.defaults.completions = Some(CompletionSettings {
10582 lsp_insert_mode,
10583 words: WordsCompletionMode::Disabled,
10584 lsp: true,
10585 lsp_fetch_timeout_ms: 0,
10586 });
10587 });
10588
10589 cx.set_state(&run.initial_state);
10590 cx.update_editor(|editor, window, cx| {
10591 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10592 });
10593
10594 let counter = Arc::new(AtomicUsize::new(0));
10595 handle_completion_request_with_insert_and_replace(
10596 &mut cx,
10597 &run.buffer_marked_text,
10598 vec![run.completion_text],
10599 counter.clone(),
10600 )
10601 .await;
10602 cx.condition(|editor, _| editor.context_menu_visible())
10603 .await;
10604 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10605
10606 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10607 editor
10608 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10609 .unwrap()
10610 });
10611 cx.assert_editor_state(&expected_text);
10612 handle_resolve_completion_request(&mut cx, None).await;
10613 apply_additional_edits.await.unwrap();
10614 }
10615 }
10616}
10617
10618#[gpui::test]
10619async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10620 init_test(cx, |_| {});
10621 let mut cx = EditorLspTestContext::new_rust(
10622 lsp::ServerCapabilities {
10623 completion_provider: Some(lsp::CompletionOptions {
10624 resolve_provider: Some(true),
10625 ..Default::default()
10626 }),
10627 ..Default::default()
10628 },
10629 cx,
10630 )
10631 .await;
10632
10633 let initial_state = "SubˇError";
10634 let buffer_marked_text = "<Sub|Error>";
10635 let completion_text = "SubscriptionError";
10636 let expected_with_insert_mode = "SubscriptionErrorˇError";
10637 let expected_with_replace_mode = "SubscriptionErrorˇ";
10638
10639 update_test_language_settings(&mut cx, |settings| {
10640 settings.defaults.completions = Some(CompletionSettings {
10641 words: WordsCompletionMode::Disabled,
10642 // set the opposite here to ensure that the action is overriding the default behavior
10643 lsp_insert_mode: LspInsertMode::Insert,
10644 lsp: true,
10645 lsp_fetch_timeout_ms: 0,
10646 });
10647 });
10648
10649 cx.set_state(initial_state);
10650 cx.update_editor(|editor, window, cx| {
10651 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10652 });
10653
10654 let counter = Arc::new(AtomicUsize::new(0));
10655 handle_completion_request_with_insert_and_replace(
10656 &mut cx,
10657 &buffer_marked_text,
10658 vec![completion_text],
10659 counter.clone(),
10660 )
10661 .await;
10662 cx.condition(|editor, _| editor.context_menu_visible())
10663 .await;
10664 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10665
10666 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10667 editor
10668 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10669 .unwrap()
10670 });
10671 cx.assert_editor_state(&expected_with_replace_mode);
10672 handle_resolve_completion_request(&mut cx, None).await;
10673 apply_additional_edits.await.unwrap();
10674
10675 update_test_language_settings(&mut cx, |settings| {
10676 settings.defaults.completions = Some(CompletionSettings {
10677 words: WordsCompletionMode::Disabled,
10678 // set the opposite here to ensure that the action is overriding the default behavior
10679 lsp_insert_mode: LspInsertMode::Replace,
10680 lsp: true,
10681 lsp_fetch_timeout_ms: 0,
10682 });
10683 });
10684
10685 cx.set_state(initial_state);
10686 cx.update_editor(|editor, window, cx| {
10687 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10688 });
10689 handle_completion_request_with_insert_and_replace(
10690 &mut cx,
10691 &buffer_marked_text,
10692 vec![completion_text],
10693 counter.clone(),
10694 )
10695 .await;
10696 cx.condition(|editor, _| editor.context_menu_visible())
10697 .await;
10698 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10699
10700 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10701 editor
10702 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10703 .unwrap()
10704 });
10705 cx.assert_editor_state(&expected_with_insert_mode);
10706 handle_resolve_completion_request(&mut cx, None).await;
10707 apply_additional_edits.await.unwrap();
10708}
10709
10710#[gpui::test]
10711async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10712 init_test(cx, |_| {});
10713 let mut cx = EditorLspTestContext::new_rust(
10714 lsp::ServerCapabilities {
10715 completion_provider: Some(lsp::CompletionOptions {
10716 resolve_provider: Some(true),
10717 ..Default::default()
10718 }),
10719 ..Default::default()
10720 },
10721 cx,
10722 )
10723 .await;
10724
10725 // scenario: surrounding text matches completion text
10726 let completion_text = "to_offset";
10727 let initial_state = indoc! {"
10728 1. buf.to_offˇsuffix
10729 2. buf.to_offˇsuf
10730 3. buf.to_offˇfix
10731 4. buf.to_offˇ
10732 5. into_offˇensive
10733 6. ˇsuffix
10734 7. let ˇ //
10735 8. aaˇzz
10736 9. buf.to_off«zzzzzˇ»suffix
10737 10. buf.«ˇzzzzz»suffix
10738 11. to_off«ˇzzzzz»
10739
10740 buf.to_offˇsuffix // newest cursor
10741 "};
10742 let completion_marked_buffer = indoc! {"
10743 1. buf.to_offsuffix
10744 2. buf.to_offsuf
10745 3. buf.to_offfix
10746 4. buf.to_off
10747 5. into_offensive
10748 6. suffix
10749 7. let //
10750 8. aazz
10751 9. buf.to_offzzzzzsuffix
10752 10. buf.zzzzzsuffix
10753 11. to_offzzzzz
10754
10755 buf.<to_off|suffix> // newest cursor
10756 "};
10757 let expected = indoc! {"
10758 1. buf.to_offsetˇ
10759 2. buf.to_offsetˇsuf
10760 3. buf.to_offsetˇfix
10761 4. buf.to_offsetˇ
10762 5. into_offsetˇensive
10763 6. to_offsetˇsuffix
10764 7. let to_offsetˇ //
10765 8. aato_offsetˇzz
10766 9. buf.to_offsetˇ
10767 10. buf.to_offsetˇsuffix
10768 11. to_offsetˇ
10769
10770 buf.to_offsetˇ // newest cursor
10771 "};
10772 cx.set_state(initial_state);
10773 cx.update_editor(|editor, window, cx| {
10774 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10775 });
10776 handle_completion_request_with_insert_and_replace(
10777 &mut cx,
10778 completion_marked_buffer,
10779 vec![completion_text],
10780 Arc::new(AtomicUsize::new(0)),
10781 )
10782 .await;
10783 cx.condition(|editor, _| editor.context_menu_visible())
10784 .await;
10785 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10786 editor
10787 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10788 .unwrap()
10789 });
10790 cx.assert_editor_state(expected);
10791 handle_resolve_completion_request(&mut cx, None).await;
10792 apply_additional_edits.await.unwrap();
10793
10794 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10795 let completion_text = "foo_and_bar";
10796 let initial_state = indoc! {"
10797 1. ooanbˇ
10798 2. zooanbˇ
10799 3. ooanbˇz
10800 4. zooanbˇz
10801 5. ooanˇ
10802 6. oanbˇ
10803
10804 ooanbˇ
10805 "};
10806 let completion_marked_buffer = indoc! {"
10807 1. ooanb
10808 2. zooanb
10809 3. ooanbz
10810 4. zooanbz
10811 5. ooan
10812 6. oanb
10813
10814 <ooanb|>
10815 "};
10816 let expected = indoc! {"
10817 1. foo_and_barˇ
10818 2. zfoo_and_barˇ
10819 3. foo_and_barˇz
10820 4. zfoo_and_barˇz
10821 5. ooanfoo_and_barˇ
10822 6. oanbfoo_and_barˇ
10823
10824 foo_and_barˇ
10825 "};
10826 cx.set_state(initial_state);
10827 cx.update_editor(|editor, window, cx| {
10828 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10829 });
10830 handle_completion_request_with_insert_and_replace(
10831 &mut cx,
10832 completion_marked_buffer,
10833 vec![completion_text],
10834 Arc::new(AtomicUsize::new(0)),
10835 )
10836 .await;
10837 cx.condition(|editor, _| editor.context_menu_visible())
10838 .await;
10839 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10840 editor
10841 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10842 .unwrap()
10843 });
10844 cx.assert_editor_state(expected);
10845 handle_resolve_completion_request(&mut cx, None).await;
10846 apply_additional_edits.await.unwrap();
10847
10848 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10849 // (expects the same as if it was inserted at the end)
10850 let completion_text = "foo_and_bar";
10851 let initial_state = indoc! {"
10852 1. ooˇanb
10853 2. zooˇanb
10854 3. ooˇanbz
10855 4. zooˇanbz
10856
10857 ooˇanb
10858 "};
10859 let completion_marked_buffer = indoc! {"
10860 1. ooanb
10861 2. zooanb
10862 3. ooanbz
10863 4. zooanbz
10864
10865 <oo|anb>
10866 "};
10867 let expected = indoc! {"
10868 1. foo_and_barˇ
10869 2. zfoo_and_barˇ
10870 3. foo_and_barˇz
10871 4. zfoo_and_barˇz
10872
10873 foo_and_barˇ
10874 "};
10875 cx.set_state(initial_state);
10876 cx.update_editor(|editor, window, cx| {
10877 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10878 });
10879 handle_completion_request_with_insert_and_replace(
10880 &mut cx,
10881 completion_marked_buffer,
10882 vec![completion_text],
10883 Arc::new(AtomicUsize::new(0)),
10884 )
10885 .await;
10886 cx.condition(|editor, _| editor.context_menu_visible())
10887 .await;
10888 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10889 editor
10890 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10891 .unwrap()
10892 });
10893 cx.assert_editor_state(expected);
10894 handle_resolve_completion_request(&mut cx, None).await;
10895 apply_additional_edits.await.unwrap();
10896}
10897
10898// This used to crash
10899#[gpui::test]
10900async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10901 init_test(cx, |_| {});
10902
10903 let buffer_text = indoc! {"
10904 fn main() {
10905 10.satu;
10906
10907 //
10908 // separate cursors so they open in different excerpts (manually reproducible)
10909 //
10910
10911 10.satu20;
10912 }
10913 "};
10914 let multibuffer_text_with_selections = indoc! {"
10915 fn main() {
10916 10.satuˇ;
10917
10918 //
10919
10920 //
10921
10922 10.satuˇ20;
10923 }
10924 "};
10925 let expected_multibuffer = indoc! {"
10926 fn main() {
10927 10.saturating_sub()ˇ;
10928
10929 //
10930
10931 //
10932
10933 10.saturating_sub()ˇ;
10934 }
10935 "};
10936
10937 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10938 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10939
10940 let fs = FakeFs::new(cx.executor());
10941 fs.insert_tree(
10942 path!("/a"),
10943 json!({
10944 "main.rs": buffer_text,
10945 }),
10946 )
10947 .await;
10948
10949 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10950 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10951 language_registry.add(rust_lang());
10952 let mut fake_servers = language_registry.register_fake_lsp(
10953 "Rust",
10954 FakeLspAdapter {
10955 capabilities: lsp::ServerCapabilities {
10956 completion_provider: Some(lsp::CompletionOptions {
10957 resolve_provider: None,
10958 ..lsp::CompletionOptions::default()
10959 }),
10960 ..lsp::ServerCapabilities::default()
10961 },
10962 ..FakeLspAdapter::default()
10963 },
10964 );
10965 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10966 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10967 let buffer = project
10968 .update(cx, |project, cx| {
10969 project.open_local_buffer(path!("/a/main.rs"), cx)
10970 })
10971 .await
10972 .unwrap();
10973
10974 let multi_buffer = cx.new(|cx| {
10975 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10976 multi_buffer.push_excerpts(
10977 buffer.clone(),
10978 [ExcerptRange::new(0..first_excerpt_end)],
10979 cx,
10980 );
10981 multi_buffer.push_excerpts(
10982 buffer.clone(),
10983 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10984 cx,
10985 );
10986 multi_buffer
10987 });
10988
10989 let editor = workspace
10990 .update(cx, |_, window, cx| {
10991 cx.new(|cx| {
10992 Editor::new(
10993 EditorMode::Full {
10994 scale_ui_elements_with_buffer_font_size: false,
10995 show_active_line_background: false,
10996 sized_by_content: false,
10997 },
10998 multi_buffer.clone(),
10999 Some(project.clone()),
11000 window,
11001 cx,
11002 )
11003 })
11004 })
11005 .unwrap();
11006
11007 let pane = workspace
11008 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11009 .unwrap();
11010 pane.update_in(cx, |pane, window, cx| {
11011 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11012 });
11013
11014 let fake_server = fake_servers.next().await.unwrap();
11015
11016 editor.update_in(cx, |editor, window, cx| {
11017 editor.change_selections(None, window, cx, |s| {
11018 s.select_ranges([
11019 Point::new(1, 11)..Point::new(1, 11),
11020 Point::new(7, 11)..Point::new(7, 11),
11021 ])
11022 });
11023
11024 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11025 });
11026
11027 editor.update_in(cx, |editor, window, cx| {
11028 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11029 });
11030
11031 fake_server
11032 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11033 let completion_item = lsp::CompletionItem {
11034 label: "saturating_sub()".into(),
11035 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11036 lsp::InsertReplaceEdit {
11037 new_text: "saturating_sub()".to_owned(),
11038 insert: lsp::Range::new(
11039 lsp::Position::new(7, 7),
11040 lsp::Position::new(7, 11),
11041 ),
11042 replace: lsp::Range::new(
11043 lsp::Position::new(7, 7),
11044 lsp::Position::new(7, 13),
11045 ),
11046 },
11047 )),
11048 ..lsp::CompletionItem::default()
11049 };
11050
11051 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11052 })
11053 .next()
11054 .await
11055 .unwrap();
11056
11057 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11058 .await;
11059
11060 editor
11061 .update_in(cx, |editor, window, cx| {
11062 editor
11063 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11064 .unwrap()
11065 })
11066 .await
11067 .unwrap();
11068
11069 editor.update(cx, |editor, cx| {
11070 assert_text_with_selections(editor, expected_multibuffer, cx);
11071 })
11072}
11073
11074#[gpui::test]
11075async fn test_completion(cx: &mut TestAppContext) {
11076 init_test(cx, |_| {});
11077
11078 let mut cx = EditorLspTestContext::new_rust(
11079 lsp::ServerCapabilities {
11080 completion_provider: Some(lsp::CompletionOptions {
11081 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11082 resolve_provider: Some(true),
11083 ..Default::default()
11084 }),
11085 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11086 ..Default::default()
11087 },
11088 cx,
11089 )
11090 .await;
11091 let counter = Arc::new(AtomicUsize::new(0));
11092
11093 cx.set_state(indoc! {"
11094 oneˇ
11095 two
11096 three
11097 "});
11098 cx.simulate_keystroke(".");
11099 handle_completion_request(
11100 &mut cx,
11101 indoc! {"
11102 one.|<>
11103 two
11104 three
11105 "},
11106 vec!["first_completion", "second_completion"],
11107 counter.clone(),
11108 )
11109 .await;
11110 cx.condition(|editor, _| editor.context_menu_visible())
11111 .await;
11112 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11113
11114 let _handler = handle_signature_help_request(
11115 &mut cx,
11116 lsp::SignatureHelp {
11117 signatures: vec![lsp::SignatureInformation {
11118 label: "test signature".to_string(),
11119 documentation: None,
11120 parameters: Some(vec![lsp::ParameterInformation {
11121 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11122 documentation: None,
11123 }]),
11124 active_parameter: None,
11125 }],
11126 active_signature: None,
11127 active_parameter: None,
11128 },
11129 );
11130 cx.update_editor(|editor, window, cx| {
11131 assert!(
11132 !editor.signature_help_state.is_shown(),
11133 "No signature help was called for"
11134 );
11135 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11136 });
11137 cx.run_until_parked();
11138 cx.update_editor(|editor, _, _| {
11139 assert!(
11140 !editor.signature_help_state.is_shown(),
11141 "No signature help should be shown when completions menu is open"
11142 );
11143 });
11144
11145 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11146 editor.context_menu_next(&Default::default(), window, cx);
11147 editor
11148 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11149 .unwrap()
11150 });
11151 cx.assert_editor_state(indoc! {"
11152 one.second_completionˇ
11153 two
11154 three
11155 "});
11156
11157 handle_resolve_completion_request(
11158 &mut cx,
11159 Some(vec![
11160 (
11161 //This overlaps with the primary completion edit which is
11162 //misbehavior from the LSP spec, test that we filter it out
11163 indoc! {"
11164 one.second_ˇcompletion
11165 two
11166 threeˇ
11167 "},
11168 "overlapping additional edit",
11169 ),
11170 (
11171 indoc! {"
11172 one.second_completion
11173 two
11174 threeˇ
11175 "},
11176 "\nadditional edit",
11177 ),
11178 ]),
11179 )
11180 .await;
11181 apply_additional_edits.await.unwrap();
11182 cx.assert_editor_state(indoc! {"
11183 one.second_completionˇ
11184 two
11185 three
11186 additional edit
11187 "});
11188
11189 cx.set_state(indoc! {"
11190 one.second_completion
11191 twoˇ
11192 threeˇ
11193 additional edit
11194 "});
11195 cx.simulate_keystroke(" ");
11196 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11197 cx.simulate_keystroke("s");
11198 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11199
11200 cx.assert_editor_state(indoc! {"
11201 one.second_completion
11202 two sˇ
11203 three sˇ
11204 additional edit
11205 "});
11206 handle_completion_request(
11207 &mut cx,
11208 indoc! {"
11209 one.second_completion
11210 two s
11211 three <s|>
11212 additional edit
11213 "},
11214 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11215 counter.clone(),
11216 )
11217 .await;
11218 cx.condition(|editor, _| editor.context_menu_visible())
11219 .await;
11220 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11221
11222 cx.simulate_keystroke("i");
11223
11224 handle_completion_request(
11225 &mut cx,
11226 indoc! {"
11227 one.second_completion
11228 two si
11229 three <si|>
11230 additional edit
11231 "},
11232 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11233 counter.clone(),
11234 )
11235 .await;
11236 cx.condition(|editor, _| editor.context_menu_visible())
11237 .await;
11238 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11239
11240 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11241 editor
11242 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11243 .unwrap()
11244 });
11245 cx.assert_editor_state(indoc! {"
11246 one.second_completion
11247 two sixth_completionˇ
11248 three sixth_completionˇ
11249 additional edit
11250 "});
11251
11252 apply_additional_edits.await.unwrap();
11253
11254 update_test_language_settings(&mut cx, |settings| {
11255 settings.defaults.show_completions_on_input = Some(false);
11256 });
11257 cx.set_state("editorˇ");
11258 cx.simulate_keystroke(".");
11259 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11260 cx.simulate_keystrokes("c l o");
11261 cx.assert_editor_state("editor.cloˇ");
11262 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11263 cx.update_editor(|editor, window, cx| {
11264 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11265 });
11266 handle_completion_request(
11267 &mut cx,
11268 "editor.<clo|>",
11269 vec!["close", "clobber"],
11270 counter.clone(),
11271 )
11272 .await;
11273 cx.condition(|editor, _| editor.context_menu_visible())
11274 .await;
11275 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11276
11277 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11278 editor
11279 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11280 .unwrap()
11281 });
11282 cx.assert_editor_state("editor.closeˇ");
11283 handle_resolve_completion_request(&mut cx, None).await;
11284 apply_additional_edits.await.unwrap();
11285}
11286
11287#[gpui::test]
11288async fn test_word_completion(cx: &mut TestAppContext) {
11289 let lsp_fetch_timeout_ms = 10;
11290 init_test(cx, |language_settings| {
11291 language_settings.defaults.completions = Some(CompletionSettings {
11292 words: WordsCompletionMode::Fallback,
11293 lsp: true,
11294 lsp_fetch_timeout_ms: 10,
11295 lsp_insert_mode: LspInsertMode::Insert,
11296 });
11297 });
11298
11299 let mut cx = EditorLspTestContext::new_rust(
11300 lsp::ServerCapabilities {
11301 completion_provider: Some(lsp::CompletionOptions {
11302 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11303 ..lsp::CompletionOptions::default()
11304 }),
11305 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11306 ..lsp::ServerCapabilities::default()
11307 },
11308 cx,
11309 )
11310 .await;
11311
11312 let throttle_completions = Arc::new(AtomicBool::new(false));
11313
11314 let lsp_throttle_completions = throttle_completions.clone();
11315 let _completion_requests_handler =
11316 cx.lsp
11317 .server
11318 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11319 let lsp_throttle_completions = lsp_throttle_completions.clone();
11320 let cx = cx.clone();
11321 async move {
11322 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11323 cx.background_executor()
11324 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11325 .await;
11326 }
11327 Ok(Some(lsp::CompletionResponse::Array(vec![
11328 lsp::CompletionItem {
11329 label: "first".into(),
11330 ..lsp::CompletionItem::default()
11331 },
11332 lsp::CompletionItem {
11333 label: "last".into(),
11334 ..lsp::CompletionItem::default()
11335 },
11336 ])))
11337 }
11338 });
11339
11340 cx.set_state(indoc! {"
11341 oneˇ
11342 two
11343 three
11344 "});
11345 cx.simulate_keystroke(".");
11346 cx.executor().run_until_parked();
11347 cx.condition(|editor, _| editor.context_menu_visible())
11348 .await;
11349 cx.update_editor(|editor, window, cx| {
11350 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11351 {
11352 assert_eq!(
11353 completion_menu_entries(&menu),
11354 &["first", "last"],
11355 "When LSP server is fast to reply, no fallback word completions are used"
11356 );
11357 } else {
11358 panic!("expected completion menu to be open");
11359 }
11360 editor.cancel(&Cancel, window, cx);
11361 });
11362 cx.executor().run_until_parked();
11363 cx.condition(|editor, _| !editor.context_menu_visible())
11364 .await;
11365
11366 throttle_completions.store(true, atomic::Ordering::Release);
11367 cx.simulate_keystroke(".");
11368 cx.executor()
11369 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11370 cx.executor().run_until_parked();
11371 cx.condition(|editor, _| editor.context_menu_visible())
11372 .await;
11373 cx.update_editor(|editor, _, _| {
11374 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11375 {
11376 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11377 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11378 } else {
11379 panic!("expected completion menu to be open");
11380 }
11381 });
11382}
11383
11384#[gpui::test]
11385async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11386 init_test(cx, |language_settings| {
11387 language_settings.defaults.completions = Some(CompletionSettings {
11388 words: WordsCompletionMode::Enabled,
11389 lsp: true,
11390 lsp_fetch_timeout_ms: 0,
11391 lsp_insert_mode: LspInsertMode::Insert,
11392 });
11393 });
11394
11395 let mut cx = EditorLspTestContext::new_rust(
11396 lsp::ServerCapabilities {
11397 completion_provider: Some(lsp::CompletionOptions {
11398 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11399 ..lsp::CompletionOptions::default()
11400 }),
11401 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11402 ..lsp::ServerCapabilities::default()
11403 },
11404 cx,
11405 )
11406 .await;
11407
11408 let _completion_requests_handler =
11409 cx.lsp
11410 .server
11411 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11412 Ok(Some(lsp::CompletionResponse::Array(vec![
11413 lsp::CompletionItem {
11414 label: "first".into(),
11415 ..lsp::CompletionItem::default()
11416 },
11417 lsp::CompletionItem {
11418 label: "last".into(),
11419 ..lsp::CompletionItem::default()
11420 },
11421 ])))
11422 });
11423
11424 cx.set_state(indoc! {"ˇ
11425 first
11426 last
11427 second
11428 "});
11429 cx.simulate_keystroke(".");
11430 cx.executor().run_until_parked();
11431 cx.condition(|editor, _| editor.context_menu_visible())
11432 .await;
11433 cx.update_editor(|editor, _, _| {
11434 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11435 {
11436 assert_eq!(
11437 completion_menu_entries(&menu),
11438 &["first", "last", "second"],
11439 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11440 );
11441 } else {
11442 panic!("expected completion menu to be open");
11443 }
11444 });
11445}
11446
11447#[gpui::test]
11448async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11449 init_test(cx, |language_settings| {
11450 language_settings.defaults.completions = Some(CompletionSettings {
11451 words: WordsCompletionMode::Disabled,
11452 lsp: true,
11453 lsp_fetch_timeout_ms: 0,
11454 lsp_insert_mode: LspInsertMode::Insert,
11455 });
11456 });
11457
11458 let mut cx = EditorLspTestContext::new_rust(
11459 lsp::ServerCapabilities {
11460 completion_provider: Some(lsp::CompletionOptions {
11461 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11462 ..lsp::CompletionOptions::default()
11463 }),
11464 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11465 ..lsp::ServerCapabilities::default()
11466 },
11467 cx,
11468 )
11469 .await;
11470
11471 let _completion_requests_handler =
11472 cx.lsp
11473 .server
11474 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11475 panic!("LSP completions should not be queried when dealing with word completions")
11476 });
11477
11478 cx.set_state(indoc! {"ˇ
11479 first
11480 last
11481 second
11482 "});
11483 cx.update_editor(|editor, window, cx| {
11484 editor.show_word_completions(&ShowWordCompletions, window, cx);
11485 });
11486 cx.executor().run_until_parked();
11487 cx.condition(|editor, _| editor.context_menu_visible())
11488 .await;
11489 cx.update_editor(|editor, _, _| {
11490 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11491 {
11492 assert_eq!(
11493 completion_menu_entries(&menu),
11494 &["first", "last", "second"],
11495 "`ShowWordCompletions` action should show word completions"
11496 );
11497 } else {
11498 panic!("expected completion menu to be open");
11499 }
11500 });
11501
11502 cx.simulate_keystroke("l");
11503 cx.executor().run_until_parked();
11504 cx.condition(|editor, _| editor.context_menu_visible())
11505 .await;
11506 cx.update_editor(|editor, _, _| {
11507 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11508 {
11509 assert_eq!(
11510 completion_menu_entries(&menu),
11511 &["last"],
11512 "After showing word completions, further editing should filter them and not query the LSP"
11513 );
11514 } else {
11515 panic!("expected completion menu to be open");
11516 }
11517 });
11518}
11519
11520#[gpui::test]
11521async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11522 init_test(cx, |language_settings| {
11523 language_settings.defaults.completions = Some(CompletionSettings {
11524 words: WordsCompletionMode::Fallback,
11525 lsp: false,
11526 lsp_fetch_timeout_ms: 0,
11527 lsp_insert_mode: LspInsertMode::Insert,
11528 });
11529 });
11530
11531 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11532
11533 cx.set_state(indoc! {"ˇ
11534 0_usize
11535 let
11536 33
11537 4.5f32
11538 "});
11539 cx.update_editor(|editor, window, cx| {
11540 editor.show_completions(&ShowCompletions::default(), window, cx);
11541 });
11542 cx.executor().run_until_parked();
11543 cx.condition(|editor, _| editor.context_menu_visible())
11544 .await;
11545 cx.update_editor(|editor, window, cx| {
11546 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11547 {
11548 assert_eq!(
11549 completion_menu_entries(&menu),
11550 &["let"],
11551 "With no digits in the completion query, no digits should be in the word completions"
11552 );
11553 } else {
11554 panic!("expected completion menu to be open");
11555 }
11556 editor.cancel(&Cancel, window, cx);
11557 });
11558
11559 cx.set_state(indoc! {"3ˇ
11560 0_usize
11561 let
11562 3
11563 33.35f32
11564 "});
11565 cx.update_editor(|editor, window, cx| {
11566 editor.show_completions(&ShowCompletions::default(), window, cx);
11567 });
11568 cx.executor().run_until_parked();
11569 cx.condition(|editor, _| editor.context_menu_visible())
11570 .await;
11571 cx.update_editor(|editor, _, _| {
11572 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11573 {
11574 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11575 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11576 } else {
11577 panic!("expected completion menu to be open");
11578 }
11579 });
11580}
11581
11582fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11583 let position = || lsp::Position {
11584 line: params.text_document_position.position.line,
11585 character: params.text_document_position.position.character,
11586 };
11587 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11588 range: lsp::Range {
11589 start: position(),
11590 end: position(),
11591 },
11592 new_text: text.to_string(),
11593 }))
11594}
11595
11596#[gpui::test]
11597async fn test_multiline_completion(cx: &mut TestAppContext) {
11598 init_test(cx, |_| {});
11599
11600 let fs = FakeFs::new(cx.executor());
11601 fs.insert_tree(
11602 path!("/a"),
11603 json!({
11604 "main.ts": "a",
11605 }),
11606 )
11607 .await;
11608
11609 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11610 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11611 let typescript_language = Arc::new(Language::new(
11612 LanguageConfig {
11613 name: "TypeScript".into(),
11614 matcher: LanguageMatcher {
11615 path_suffixes: vec!["ts".to_string()],
11616 ..LanguageMatcher::default()
11617 },
11618 line_comments: vec!["// ".into()],
11619 ..LanguageConfig::default()
11620 },
11621 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11622 ));
11623 language_registry.add(typescript_language.clone());
11624 let mut fake_servers = language_registry.register_fake_lsp(
11625 "TypeScript",
11626 FakeLspAdapter {
11627 capabilities: lsp::ServerCapabilities {
11628 completion_provider: Some(lsp::CompletionOptions {
11629 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11630 ..lsp::CompletionOptions::default()
11631 }),
11632 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11633 ..lsp::ServerCapabilities::default()
11634 },
11635 // Emulate vtsls label generation
11636 label_for_completion: Some(Box::new(|item, _| {
11637 let text = if let Some(description) = item
11638 .label_details
11639 .as_ref()
11640 .and_then(|label_details| label_details.description.as_ref())
11641 {
11642 format!("{} {}", item.label, description)
11643 } else if let Some(detail) = &item.detail {
11644 format!("{} {}", item.label, detail)
11645 } else {
11646 item.label.clone()
11647 };
11648 let len = text.len();
11649 Some(language::CodeLabel {
11650 text,
11651 runs: Vec::new(),
11652 filter_range: 0..len,
11653 })
11654 })),
11655 ..FakeLspAdapter::default()
11656 },
11657 );
11658 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11659 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11660 let worktree_id = workspace
11661 .update(cx, |workspace, _window, cx| {
11662 workspace.project().update(cx, |project, cx| {
11663 project.worktrees(cx).next().unwrap().read(cx).id()
11664 })
11665 })
11666 .unwrap();
11667 let _buffer = project
11668 .update(cx, |project, cx| {
11669 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11670 })
11671 .await
11672 .unwrap();
11673 let editor = workspace
11674 .update(cx, |workspace, window, cx| {
11675 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11676 })
11677 .unwrap()
11678 .await
11679 .unwrap()
11680 .downcast::<Editor>()
11681 .unwrap();
11682 let fake_server = fake_servers.next().await.unwrap();
11683
11684 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11685 let multiline_label_2 = "a\nb\nc\n";
11686 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11687 let multiline_description = "d\ne\nf\n";
11688 let multiline_detail_2 = "g\nh\ni\n";
11689
11690 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11691 move |params, _| async move {
11692 Ok(Some(lsp::CompletionResponse::Array(vec![
11693 lsp::CompletionItem {
11694 label: multiline_label.to_string(),
11695 text_edit: gen_text_edit(¶ms, "new_text_1"),
11696 ..lsp::CompletionItem::default()
11697 },
11698 lsp::CompletionItem {
11699 label: "single line label 1".to_string(),
11700 detail: Some(multiline_detail.to_string()),
11701 text_edit: gen_text_edit(¶ms, "new_text_2"),
11702 ..lsp::CompletionItem::default()
11703 },
11704 lsp::CompletionItem {
11705 label: "single line label 2".to_string(),
11706 label_details: Some(lsp::CompletionItemLabelDetails {
11707 description: Some(multiline_description.to_string()),
11708 detail: None,
11709 }),
11710 text_edit: gen_text_edit(¶ms, "new_text_2"),
11711 ..lsp::CompletionItem::default()
11712 },
11713 lsp::CompletionItem {
11714 label: multiline_label_2.to_string(),
11715 detail: Some(multiline_detail_2.to_string()),
11716 text_edit: gen_text_edit(¶ms, "new_text_3"),
11717 ..lsp::CompletionItem::default()
11718 },
11719 lsp::CompletionItem {
11720 label: "Label with many spaces and \t but without newlines".to_string(),
11721 detail: Some(
11722 "Details with many spaces and \t but without newlines".to_string(),
11723 ),
11724 text_edit: gen_text_edit(¶ms, "new_text_4"),
11725 ..lsp::CompletionItem::default()
11726 },
11727 ])))
11728 },
11729 );
11730
11731 editor.update_in(cx, |editor, window, cx| {
11732 cx.focus_self(window);
11733 editor.move_to_end(&MoveToEnd, window, cx);
11734 editor.handle_input(".", window, cx);
11735 });
11736 cx.run_until_parked();
11737 completion_handle.next().await.unwrap();
11738
11739 editor.update(cx, |editor, _| {
11740 assert!(editor.context_menu_visible());
11741 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11742 {
11743 let completion_labels = menu
11744 .completions
11745 .borrow()
11746 .iter()
11747 .map(|c| c.label.text.clone())
11748 .collect::<Vec<_>>();
11749 assert_eq!(
11750 completion_labels,
11751 &[
11752 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11753 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11754 "single line label 2 d e f ",
11755 "a b c g h i ",
11756 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11757 ],
11758 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11759 );
11760
11761 for completion in menu
11762 .completions
11763 .borrow()
11764 .iter() {
11765 assert_eq!(
11766 completion.label.filter_range,
11767 0..completion.label.text.len(),
11768 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11769 );
11770 }
11771 } else {
11772 panic!("expected completion menu to be open");
11773 }
11774 });
11775}
11776
11777#[gpui::test]
11778async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11779 init_test(cx, |_| {});
11780 let mut cx = EditorLspTestContext::new_rust(
11781 lsp::ServerCapabilities {
11782 completion_provider: Some(lsp::CompletionOptions {
11783 trigger_characters: Some(vec![".".to_string()]),
11784 ..Default::default()
11785 }),
11786 ..Default::default()
11787 },
11788 cx,
11789 )
11790 .await;
11791 cx.lsp
11792 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11793 Ok(Some(lsp::CompletionResponse::Array(vec![
11794 lsp::CompletionItem {
11795 label: "first".into(),
11796 ..Default::default()
11797 },
11798 lsp::CompletionItem {
11799 label: "last".into(),
11800 ..Default::default()
11801 },
11802 ])))
11803 });
11804 cx.set_state("variableˇ");
11805 cx.simulate_keystroke(".");
11806 cx.executor().run_until_parked();
11807
11808 cx.update_editor(|editor, _, _| {
11809 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11810 {
11811 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11812 } else {
11813 panic!("expected completion menu to be open");
11814 }
11815 });
11816
11817 cx.update_editor(|editor, window, cx| {
11818 editor.move_page_down(&MovePageDown::default(), window, cx);
11819 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11820 {
11821 assert!(
11822 menu.selected_item == 1,
11823 "expected PageDown to select the last item from the context menu"
11824 );
11825 } else {
11826 panic!("expected completion menu to stay open after PageDown");
11827 }
11828 });
11829
11830 cx.update_editor(|editor, window, cx| {
11831 editor.move_page_up(&MovePageUp::default(), window, cx);
11832 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11833 {
11834 assert!(
11835 menu.selected_item == 0,
11836 "expected PageUp to select the first item from the context menu"
11837 );
11838 } else {
11839 panic!("expected completion menu to stay open after PageUp");
11840 }
11841 });
11842}
11843
11844#[gpui::test]
11845async fn test_as_is_completions(cx: &mut TestAppContext) {
11846 init_test(cx, |_| {});
11847 let mut cx = EditorLspTestContext::new_rust(
11848 lsp::ServerCapabilities {
11849 completion_provider: Some(lsp::CompletionOptions {
11850 ..Default::default()
11851 }),
11852 ..Default::default()
11853 },
11854 cx,
11855 )
11856 .await;
11857 cx.lsp
11858 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11859 Ok(Some(lsp::CompletionResponse::Array(vec![
11860 lsp::CompletionItem {
11861 label: "unsafe".into(),
11862 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11863 range: lsp::Range {
11864 start: lsp::Position {
11865 line: 1,
11866 character: 2,
11867 },
11868 end: lsp::Position {
11869 line: 1,
11870 character: 3,
11871 },
11872 },
11873 new_text: "unsafe".to_string(),
11874 })),
11875 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11876 ..Default::default()
11877 },
11878 ])))
11879 });
11880 cx.set_state("fn a() {}\n nˇ");
11881 cx.executor().run_until_parked();
11882 cx.update_editor(|editor, window, cx| {
11883 editor.show_completions(
11884 &ShowCompletions {
11885 trigger: Some("\n".into()),
11886 },
11887 window,
11888 cx,
11889 );
11890 });
11891 cx.executor().run_until_parked();
11892
11893 cx.update_editor(|editor, window, cx| {
11894 editor.confirm_completion(&Default::default(), window, cx)
11895 });
11896 cx.executor().run_until_parked();
11897 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11898}
11899
11900#[gpui::test]
11901async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11902 init_test(cx, |_| {});
11903
11904 let mut cx = EditorLspTestContext::new_rust(
11905 lsp::ServerCapabilities {
11906 completion_provider: Some(lsp::CompletionOptions {
11907 trigger_characters: Some(vec![".".to_string()]),
11908 resolve_provider: Some(true),
11909 ..Default::default()
11910 }),
11911 ..Default::default()
11912 },
11913 cx,
11914 )
11915 .await;
11916
11917 cx.set_state("fn main() { let a = 2ˇ; }");
11918 cx.simulate_keystroke(".");
11919 let completion_item = lsp::CompletionItem {
11920 label: "Some".into(),
11921 kind: Some(lsp::CompletionItemKind::SNIPPET),
11922 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11923 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11924 kind: lsp::MarkupKind::Markdown,
11925 value: "```rust\nSome(2)\n```".to_string(),
11926 })),
11927 deprecated: Some(false),
11928 sort_text: Some("Some".to_string()),
11929 filter_text: Some("Some".to_string()),
11930 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11931 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11932 range: lsp::Range {
11933 start: lsp::Position {
11934 line: 0,
11935 character: 22,
11936 },
11937 end: lsp::Position {
11938 line: 0,
11939 character: 22,
11940 },
11941 },
11942 new_text: "Some(2)".to_string(),
11943 })),
11944 additional_text_edits: Some(vec![lsp::TextEdit {
11945 range: lsp::Range {
11946 start: lsp::Position {
11947 line: 0,
11948 character: 20,
11949 },
11950 end: lsp::Position {
11951 line: 0,
11952 character: 22,
11953 },
11954 },
11955 new_text: "".to_string(),
11956 }]),
11957 ..Default::default()
11958 };
11959
11960 let closure_completion_item = completion_item.clone();
11961 let counter = Arc::new(AtomicUsize::new(0));
11962 let counter_clone = counter.clone();
11963 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11964 let task_completion_item = closure_completion_item.clone();
11965 counter_clone.fetch_add(1, atomic::Ordering::Release);
11966 async move {
11967 Ok(Some(lsp::CompletionResponse::Array(vec![
11968 task_completion_item,
11969 ])))
11970 }
11971 });
11972
11973 cx.condition(|editor, _| editor.context_menu_visible())
11974 .await;
11975 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11976 assert!(request.next().await.is_some());
11977 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11978
11979 cx.simulate_keystrokes("S o m");
11980 cx.condition(|editor, _| editor.context_menu_visible())
11981 .await;
11982 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11983 assert!(request.next().await.is_some());
11984 assert!(request.next().await.is_some());
11985 assert!(request.next().await.is_some());
11986 request.close();
11987 assert!(request.next().await.is_none());
11988 assert_eq!(
11989 counter.load(atomic::Ordering::Acquire),
11990 4,
11991 "With the completions menu open, only one LSP request should happen per input"
11992 );
11993}
11994
11995#[gpui::test]
11996async fn test_toggle_comment(cx: &mut TestAppContext) {
11997 init_test(cx, |_| {});
11998 let mut cx = EditorTestContext::new(cx).await;
11999 let language = Arc::new(Language::new(
12000 LanguageConfig {
12001 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12002 ..Default::default()
12003 },
12004 Some(tree_sitter_rust::LANGUAGE.into()),
12005 ));
12006 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12007
12008 // If multiple selections intersect a line, the line is only toggled once.
12009 cx.set_state(indoc! {"
12010 fn a() {
12011 «//b();
12012 ˇ»// «c();
12013 //ˇ» d();
12014 }
12015 "});
12016
12017 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12018
12019 cx.assert_editor_state(indoc! {"
12020 fn a() {
12021 «b();
12022 c();
12023 ˇ» d();
12024 }
12025 "});
12026
12027 // The comment prefix is inserted at the same column for every line in a
12028 // selection.
12029 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12030
12031 cx.assert_editor_state(indoc! {"
12032 fn a() {
12033 // «b();
12034 // c();
12035 ˇ»// d();
12036 }
12037 "});
12038
12039 // If a selection ends at the beginning of a line, that line is not toggled.
12040 cx.set_selections_state(indoc! {"
12041 fn a() {
12042 // b();
12043 «// c();
12044 ˇ» // d();
12045 }
12046 "});
12047
12048 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12049
12050 cx.assert_editor_state(indoc! {"
12051 fn a() {
12052 // b();
12053 «c();
12054 ˇ» // d();
12055 }
12056 "});
12057
12058 // If a selection span a single line and is empty, the line is toggled.
12059 cx.set_state(indoc! {"
12060 fn a() {
12061 a();
12062 b();
12063 ˇ
12064 }
12065 "});
12066
12067 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12068
12069 cx.assert_editor_state(indoc! {"
12070 fn a() {
12071 a();
12072 b();
12073 //•ˇ
12074 }
12075 "});
12076
12077 // If a selection span multiple lines, empty lines are not toggled.
12078 cx.set_state(indoc! {"
12079 fn a() {
12080 «a();
12081
12082 c();ˇ»
12083 }
12084 "});
12085
12086 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12087
12088 cx.assert_editor_state(indoc! {"
12089 fn a() {
12090 // «a();
12091
12092 // c();ˇ»
12093 }
12094 "});
12095
12096 // If a selection includes multiple comment prefixes, all lines are uncommented.
12097 cx.set_state(indoc! {"
12098 fn a() {
12099 «// a();
12100 /// b();
12101 //! c();ˇ»
12102 }
12103 "});
12104
12105 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12106
12107 cx.assert_editor_state(indoc! {"
12108 fn a() {
12109 «a();
12110 b();
12111 c();ˇ»
12112 }
12113 "});
12114}
12115
12116#[gpui::test]
12117async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12118 init_test(cx, |_| {});
12119 let mut cx = EditorTestContext::new(cx).await;
12120 let language = Arc::new(Language::new(
12121 LanguageConfig {
12122 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12123 ..Default::default()
12124 },
12125 Some(tree_sitter_rust::LANGUAGE.into()),
12126 ));
12127 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12128
12129 let toggle_comments = &ToggleComments {
12130 advance_downwards: false,
12131 ignore_indent: true,
12132 };
12133
12134 // If multiple selections intersect a line, the line is only toggled once.
12135 cx.set_state(indoc! {"
12136 fn a() {
12137 // «b();
12138 // c();
12139 // ˇ» d();
12140 }
12141 "});
12142
12143 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12144
12145 cx.assert_editor_state(indoc! {"
12146 fn a() {
12147 «b();
12148 c();
12149 ˇ» d();
12150 }
12151 "});
12152
12153 // The comment prefix is inserted at the beginning of each line
12154 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12155
12156 cx.assert_editor_state(indoc! {"
12157 fn a() {
12158 // «b();
12159 // c();
12160 // ˇ» d();
12161 }
12162 "});
12163
12164 // If a selection ends at the beginning of a line, that line is not toggled.
12165 cx.set_selections_state(indoc! {"
12166 fn a() {
12167 // b();
12168 // «c();
12169 ˇ»// d();
12170 }
12171 "});
12172
12173 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12174
12175 cx.assert_editor_state(indoc! {"
12176 fn a() {
12177 // b();
12178 «c();
12179 ˇ»// d();
12180 }
12181 "});
12182
12183 // If a selection span a single line and is empty, the line is toggled.
12184 cx.set_state(indoc! {"
12185 fn a() {
12186 a();
12187 b();
12188 ˇ
12189 }
12190 "});
12191
12192 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12193
12194 cx.assert_editor_state(indoc! {"
12195 fn a() {
12196 a();
12197 b();
12198 //ˇ
12199 }
12200 "});
12201
12202 // If a selection span multiple lines, empty lines are not toggled.
12203 cx.set_state(indoc! {"
12204 fn a() {
12205 «a();
12206
12207 c();ˇ»
12208 }
12209 "});
12210
12211 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12212
12213 cx.assert_editor_state(indoc! {"
12214 fn a() {
12215 // «a();
12216
12217 // c();ˇ»
12218 }
12219 "});
12220
12221 // If a selection includes multiple comment prefixes, all lines are uncommented.
12222 cx.set_state(indoc! {"
12223 fn a() {
12224 // «a();
12225 /// b();
12226 //! c();ˇ»
12227 }
12228 "});
12229
12230 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12231
12232 cx.assert_editor_state(indoc! {"
12233 fn a() {
12234 «a();
12235 b();
12236 c();ˇ»
12237 }
12238 "});
12239}
12240
12241#[gpui::test]
12242async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12243 init_test(cx, |_| {});
12244
12245 let language = Arc::new(Language::new(
12246 LanguageConfig {
12247 line_comments: vec!["// ".into()],
12248 ..Default::default()
12249 },
12250 Some(tree_sitter_rust::LANGUAGE.into()),
12251 ));
12252
12253 let mut cx = EditorTestContext::new(cx).await;
12254
12255 cx.language_registry().add(language.clone());
12256 cx.update_buffer(|buffer, cx| {
12257 buffer.set_language(Some(language), cx);
12258 });
12259
12260 let toggle_comments = &ToggleComments {
12261 advance_downwards: true,
12262 ignore_indent: false,
12263 };
12264
12265 // Single cursor on one line -> advance
12266 // Cursor moves horizontally 3 characters as well on non-blank line
12267 cx.set_state(indoc!(
12268 "fn a() {
12269 ˇdog();
12270 cat();
12271 }"
12272 ));
12273 cx.update_editor(|editor, window, cx| {
12274 editor.toggle_comments(toggle_comments, window, cx);
12275 });
12276 cx.assert_editor_state(indoc!(
12277 "fn a() {
12278 // dog();
12279 catˇ();
12280 }"
12281 ));
12282
12283 // Single selection on one line -> don't advance
12284 cx.set_state(indoc!(
12285 "fn a() {
12286 «dog()ˇ»;
12287 cat();
12288 }"
12289 ));
12290 cx.update_editor(|editor, window, cx| {
12291 editor.toggle_comments(toggle_comments, window, cx);
12292 });
12293 cx.assert_editor_state(indoc!(
12294 "fn a() {
12295 // «dog()ˇ»;
12296 cat();
12297 }"
12298 ));
12299
12300 // Multiple cursors on one line -> advance
12301 cx.set_state(indoc!(
12302 "fn a() {
12303 ˇdˇog();
12304 cat();
12305 }"
12306 ));
12307 cx.update_editor(|editor, window, cx| {
12308 editor.toggle_comments(toggle_comments, window, cx);
12309 });
12310 cx.assert_editor_state(indoc!(
12311 "fn a() {
12312 // dog();
12313 catˇ(ˇ);
12314 }"
12315 ));
12316
12317 // Multiple cursors on one line, with selection -> don't advance
12318 cx.set_state(indoc!(
12319 "fn a() {
12320 ˇdˇog«()ˇ»;
12321 cat();
12322 }"
12323 ));
12324 cx.update_editor(|editor, window, cx| {
12325 editor.toggle_comments(toggle_comments, window, cx);
12326 });
12327 cx.assert_editor_state(indoc!(
12328 "fn a() {
12329 // ˇdˇog«()ˇ»;
12330 cat();
12331 }"
12332 ));
12333
12334 // Single cursor on one line -> advance
12335 // Cursor moves to column 0 on blank line
12336 cx.set_state(indoc!(
12337 "fn a() {
12338 ˇdog();
12339
12340 cat();
12341 }"
12342 ));
12343 cx.update_editor(|editor, window, cx| {
12344 editor.toggle_comments(toggle_comments, window, cx);
12345 });
12346 cx.assert_editor_state(indoc!(
12347 "fn a() {
12348 // dog();
12349 ˇ
12350 cat();
12351 }"
12352 ));
12353
12354 // Single cursor on one line -> advance
12355 // Cursor starts and ends at column 0
12356 cx.set_state(indoc!(
12357 "fn a() {
12358 ˇ dog();
12359 cat();
12360 }"
12361 ));
12362 cx.update_editor(|editor, window, cx| {
12363 editor.toggle_comments(toggle_comments, window, cx);
12364 });
12365 cx.assert_editor_state(indoc!(
12366 "fn a() {
12367 // dog();
12368 ˇ cat();
12369 }"
12370 ));
12371}
12372
12373#[gpui::test]
12374async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12375 init_test(cx, |_| {});
12376
12377 let mut cx = EditorTestContext::new(cx).await;
12378
12379 let html_language = Arc::new(
12380 Language::new(
12381 LanguageConfig {
12382 name: "HTML".into(),
12383 block_comment: Some(("<!-- ".into(), " -->".into())),
12384 ..Default::default()
12385 },
12386 Some(tree_sitter_html::LANGUAGE.into()),
12387 )
12388 .with_injection_query(
12389 r#"
12390 (script_element
12391 (raw_text) @injection.content
12392 (#set! injection.language "javascript"))
12393 "#,
12394 )
12395 .unwrap(),
12396 );
12397
12398 let javascript_language = Arc::new(Language::new(
12399 LanguageConfig {
12400 name: "JavaScript".into(),
12401 line_comments: vec!["// ".into()],
12402 ..Default::default()
12403 },
12404 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12405 ));
12406
12407 cx.language_registry().add(html_language.clone());
12408 cx.language_registry().add(javascript_language.clone());
12409 cx.update_buffer(|buffer, cx| {
12410 buffer.set_language(Some(html_language), cx);
12411 });
12412
12413 // Toggle comments for empty selections
12414 cx.set_state(
12415 &r#"
12416 <p>A</p>ˇ
12417 <p>B</p>ˇ
12418 <p>C</p>ˇ
12419 "#
12420 .unindent(),
12421 );
12422 cx.update_editor(|editor, window, cx| {
12423 editor.toggle_comments(&ToggleComments::default(), window, cx)
12424 });
12425 cx.assert_editor_state(
12426 &r#"
12427 <!-- <p>A</p>ˇ -->
12428 <!-- <p>B</p>ˇ -->
12429 <!-- <p>C</p>ˇ -->
12430 "#
12431 .unindent(),
12432 );
12433 cx.update_editor(|editor, window, cx| {
12434 editor.toggle_comments(&ToggleComments::default(), window, cx)
12435 });
12436 cx.assert_editor_state(
12437 &r#"
12438 <p>A</p>ˇ
12439 <p>B</p>ˇ
12440 <p>C</p>ˇ
12441 "#
12442 .unindent(),
12443 );
12444
12445 // Toggle comments for mixture of empty and non-empty selections, where
12446 // multiple selections occupy a given line.
12447 cx.set_state(
12448 &r#"
12449 <p>A«</p>
12450 <p>ˇ»B</p>ˇ
12451 <p>C«</p>
12452 <p>ˇ»D</p>ˇ
12453 "#
12454 .unindent(),
12455 );
12456
12457 cx.update_editor(|editor, window, cx| {
12458 editor.toggle_comments(&ToggleComments::default(), window, cx)
12459 });
12460 cx.assert_editor_state(
12461 &r#"
12462 <!-- <p>A«</p>
12463 <p>ˇ»B</p>ˇ -->
12464 <!-- <p>C«</p>
12465 <p>ˇ»D</p>ˇ -->
12466 "#
12467 .unindent(),
12468 );
12469 cx.update_editor(|editor, window, cx| {
12470 editor.toggle_comments(&ToggleComments::default(), window, cx)
12471 });
12472 cx.assert_editor_state(
12473 &r#"
12474 <p>A«</p>
12475 <p>ˇ»B</p>ˇ
12476 <p>C«</p>
12477 <p>ˇ»D</p>ˇ
12478 "#
12479 .unindent(),
12480 );
12481
12482 // Toggle comments when different languages are active for different
12483 // selections.
12484 cx.set_state(
12485 &r#"
12486 ˇ<script>
12487 ˇvar x = new Y();
12488 ˇ</script>
12489 "#
12490 .unindent(),
12491 );
12492 cx.executor().run_until_parked();
12493 cx.update_editor(|editor, window, cx| {
12494 editor.toggle_comments(&ToggleComments::default(), window, cx)
12495 });
12496 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12497 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12498 cx.assert_editor_state(
12499 &r#"
12500 <!-- ˇ<script> -->
12501 // ˇvar x = new Y();
12502 <!-- ˇ</script> -->
12503 "#
12504 .unindent(),
12505 );
12506}
12507
12508#[gpui::test]
12509fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12510 init_test(cx, |_| {});
12511
12512 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12513 let multibuffer = cx.new(|cx| {
12514 let mut multibuffer = MultiBuffer::new(ReadWrite);
12515 multibuffer.push_excerpts(
12516 buffer.clone(),
12517 [
12518 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12519 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12520 ],
12521 cx,
12522 );
12523 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12524 multibuffer
12525 });
12526
12527 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12528 editor.update_in(cx, |editor, window, cx| {
12529 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12530 editor.change_selections(None, window, cx, |s| {
12531 s.select_ranges([
12532 Point::new(0, 0)..Point::new(0, 0),
12533 Point::new(1, 0)..Point::new(1, 0),
12534 ])
12535 });
12536
12537 editor.handle_input("X", window, cx);
12538 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12539 assert_eq!(
12540 editor.selections.ranges(cx),
12541 [
12542 Point::new(0, 1)..Point::new(0, 1),
12543 Point::new(1, 1)..Point::new(1, 1),
12544 ]
12545 );
12546
12547 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12548 editor.change_selections(None, window, cx, |s| {
12549 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12550 });
12551 editor.backspace(&Default::default(), window, cx);
12552 assert_eq!(editor.text(cx), "Xa\nbbb");
12553 assert_eq!(
12554 editor.selections.ranges(cx),
12555 [Point::new(1, 0)..Point::new(1, 0)]
12556 );
12557
12558 editor.change_selections(None, window, cx, |s| {
12559 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12560 });
12561 editor.backspace(&Default::default(), window, cx);
12562 assert_eq!(editor.text(cx), "X\nbb");
12563 assert_eq!(
12564 editor.selections.ranges(cx),
12565 [Point::new(0, 1)..Point::new(0, 1)]
12566 );
12567 });
12568}
12569
12570#[gpui::test]
12571fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12572 init_test(cx, |_| {});
12573
12574 let markers = vec![('[', ']').into(), ('(', ')').into()];
12575 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12576 indoc! {"
12577 [aaaa
12578 (bbbb]
12579 cccc)",
12580 },
12581 markers.clone(),
12582 );
12583 let excerpt_ranges = markers.into_iter().map(|marker| {
12584 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12585 ExcerptRange::new(context.clone())
12586 });
12587 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12588 let multibuffer = cx.new(|cx| {
12589 let mut multibuffer = MultiBuffer::new(ReadWrite);
12590 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12591 multibuffer
12592 });
12593
12594 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12595 editor.update_in(cx, |editor, window, cx| {
12596 let (expected_text, selection_ranges) = marked_text_ranges(
12597 indoc! {"
12598 aaaa
12599 bˇbbb
12600 bˇbbˇb
12601 cccc"
12602 },
12603 true,
12604 );
12605 assert_eq!(editor.text(cx), expected_text);
12606 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12607
12608 editor.handle_input("X", window, cx);
12609
12610 let (expected_text, expected_selections) = marked_text_ranges(
12611 indoc! {"
12612 aaaa
12613 bXˇbbXb
12614 bXˇbbXˇb
12615 cccc"
12616 },
12617 false,
12618 );
12619 assert_eq!(editor.text(cx), expected_text);
12620 assert_eq!(editor.selections.ranges(cx), expected_selections);
12621
12622 editor.newline(&Newline, window, cx);
12623 let (expected_text, expected_selections) = marked_text_ranges(
12624 indoc! {"
12625 aaaa
12626 bX
12627 ˇbbX
12628 b
12629 bX
12630 ˇbbX
12631 ˇb
12632 cccc"
12633 },
12634 false,
12635 );
12636 assert_eq!(editor.text(cx), expected_text);
12637 assert_eq!(editor.selections.ranges(cx), expected_selections);
12638 });
12639}
12640
12641#[gpui::test]
12642fn test_refresh_selections(cx: &mut TestAppContext) {
12643 init_test(cx, |_| {});
12644
12645 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12646 let mut excerpt1_id = None;
12647 let multibuffer = cx.new(|cx| {
12648 let mut multibuffer = MultiBuffer::new(ReadWrite);
12649 excerpt1_id = multibuffer
12650 .push_excerpts(
12651 buffer.clone(),
12652 [
12653 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12654 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12655 ],
12656 cx,
12657 )
12658 .into_iter()
12659 .next();
12660 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12661 multibuffer
12662 });
12663
12664 let editor = cx.add_window(|window, cx| {
12665 let mut editor = build_editor(multibuffer.clone(), window, cx);
12666 let snapshot = editor.snapshot(window, cx);
12667 editor.change_selections(None, window, cx, |s| {
12668 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12669 });
12670 editor.begin_selection(
12671 Point::new(2, 1).to_display_point(&snapshot),
12672 true,
12673 1,
12674 window,
12675 cx,
12676 );
12677 assert_eq!(
12678 editor.selections.ranges(cx),
12679 [
12680 Point::new(1, 3)..Point::new(1, 3),
12681 Point::new(2, 1)..Point::new(2, 1),
12682 ]
12683 );
12684 editor
12685 });
12686
12687 // Refreshing selections is a no-op when excerpts haven't changed.
12688 _ = editor.update(cx, |editor, window, cx| {
12689 editor.change_selections(None, window, cx, |s| s.refresh());
12690 assert_eq!(
12691 editor.selections.ranges(cx),
12692 [
12693 Point::new(1, 3)..Point::new(1, 3),
12694 Point::new(2, 1)..Point::new(2, 1),
12695 ]
12696 );
12697 });
12698
12699 multibuffer.update(cx, |multibuffer, cx| {
12700 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12701 });
12702 _ = editor.update(cx, |editor, window, cx| {
12703 // Removing an excerpt causes the first selection to become degenerate.
12704 assert_eq!(
12705 editor.selections.ranges(cx),
12706 [
12707 Point::new(0, 0)..Point::new(0, 0),
12708 Point::new(0, 1)..Point::new(0, 1)
12709 ]
12710 );
12711
12712 // Refreshing selections will relocate the first selection to the original buffer
12713 // location.
12714 editor.change_selections(None, window, cx, |s| s.refresh());
12715 assert_eq!(
12716 editor.selections.ranges(cx),
12717 [
12718 Point::new(0, 1)..Point::new(0, 1),
12719 Point::new(0, 3)..Point::new(0, 3)
12720 ]
12721 );
12722 assert!(editor.selections.pending_anchor().is_some());
12723 });
12724}
12725
12726#[gpui::test]
12727fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12728 init_test(cx, |_| {});
12729
12730 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12731 let mut excerpt1_id = None;
12732 let multibuffer = cx.new(|cx| {
12733 let mut multibuffer = MultiBuffer::new(ReadWrite);
12734 excerpt1_id = multibuffer
12735 .push_excerpts(
12736 buffer.clone(),
12737 [
12738 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12739 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12740 ],
12741 cx,
12742 )
12743 .into_iter()
12744 .next();
12745 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12746 multibuffer
12747 });
12748
12749 let editor = cx.add_window(|window, cx| {
12750 let mut editor = build_editor(multibuffer.clone(), window, cx);
12751 let snapshot = editor.snapshot(window, cx);
12752 editor.begin_selection(
12753 Point::new(1, 3).to_display_point(&snapshot),
12754 false,
12755 1,
12756 window,
12757 cx,
12758 );
12759 assert_eq!(
12760 editor.selections.ranges(cx),
12761 [Point::new(1, 3)..Point::new(1, 3)]
12762 );
12763 editor
12764 });
12765
12766 multibuffer.update(cx, |multibuffer, cx| {
12767 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12768 });
12769 _ = editor.update(cx, |editor, window, cx| {
12770 assert_eq!(
12771 editor.selections.ranges(cx),
12772 [Point::new(0, 0)..Point::new(0, 0)]
12773 );
12774
12775 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12776 editor.change_selections(None, window, cx, |s| s.refresh());
12777 assert_eq!(
12778 editor.selections.ranges(cx),
12779 [Point::new(0, 3)..Point::new(0, 3)]
12780 );
12781 assert!(editor.selections.pending_anchor().is_some());
12782 });
12783}
12784
12785#[gpui::test]
12786async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12787 init_test(cx, |_| {});
12788
12789 let language = Arc::new(
12790 Language::new(
12791 LanguageConfig {
12792 brackets: BracketPairConfig {
12793 pairs: vec![
12794 BracketPair {
12795 start: "{".to_string(),
12796 end: "}".to_string(),
12797 close: true,
12798 surround: true,
12799 newline: true,
12800 },
12801 BracketPair {
12802 start: "/* ".to_string(),
12803 end: " */".to_string(),
12804 close: true,
12805 surround: true,
12806 newline: true,
12807 },
12808 ],
12809 ..Default::default()
12810 },
12811 ..Default::default()
12812 },
12813 Some(tree_sitter_rust::LANGUAGE.into()),
12814 )
12815 .with_indents_query("")
12816 .unwrap(),
12817 );
12818
12819 let text = concat!(
12820 "{ }\n", //
12821 " x\n", //
12822 " /* */\n", //
12823 "x\n", //
12824 "{{} }\n", //
12825 );
12826
12827 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12828 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12829 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12830 editor
12831 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12832 .await;
12833
12834 editor.update_in(cx, |editor, window, cx| {
12835 editor.change_selections(None, window, cx, |s| {
12836 s.select_display_ranges([
12837 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12838 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12839 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12840 ])
12841 });
12842 editor.newline(&Newline, window, cx);
12843
12844 assert_eq!(
12845 editor.buffer().read(cx).read(cx).text(),
12846 concat!(
12847 "{ \n", // Suppress rustfmt
12848 "\n", //
12849 "}\n", //
12850 " x\n", //
12851 " /* \n", //
12852 " \n", //
12853 " */\n", //
12854 "x\n", //
12855 "{{} \n", //
12856 "}\n", //
12857 )
12858 );
12859 });
12860}
12861
12862#[gpui::test]
12863fn test_highlighted_ranges(cx: &mut TestAppContext) {
12864 init_test(cx, |_| {});
12865
12866 let editor = cx.add_window(|window, cx| {
12867 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12868 build_editor(buffer.clone(), window, cx)
12869 });
12870
12871 _ = editor.update(cx, |editor, window, cx| {
12872 struct Type1;
12873 struct Type2;
12874
12875 let buffer = editor.buffer.read(cx).snapshot(cx);
12876
12877 let anchor_range =
12878 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12879
12880 editor.highlight_background::<Type1>(
12881 &[
12882 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12883 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12884 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12885 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12886 ],
12887 |_| Hsla::red(),
12888 cx,
12889 );
12890 editor.highlight_background::<Type2>(
12891 &[
12892 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12893 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12894 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12895 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12896 ],
12897 |_| Hsla::green(),
12898 cx,
12899 );
12900
12901 let snapshot = editor.snapshot(window, cx);
12902 let mut highlighted_ranges = editor.background_highlights_in_range(
12903 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12904 &snapshot,
12905 cx.theme().colors(),
12906 );
12907 // Enforce a consistent ordering based on color without relying on the ordering of the
12908 // highlight's `TypeId` which is non-executor.
12909 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12910 assert_eq!(
12911 highlighted_ranges,
12912 &[
12913 (
12914 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12915 Hsla::red(),
12916 ),
12917 (
12918 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12919 Hsla::red(),
12920 ),
12921 (
12922 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12923 Hsla::green(),
12924 ),
12925 (
12926 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12927 Hsla::green(),
12928 ),
12929 ]
12930 );
12931 assert_eq!(
12932 editor.background_highlights_in_range(
12933 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12934 &snapshot,
12935 cx.theme().colors(),
12936 ),
12937 &[(
12938 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12939 Hsla::red(),
12940 )]
12941 );
12942 });
12943}
12944
12945#[gpui::test]
12946async fn test_following(cx: &mut TestAppContext) {
12947 init_test(cx, |_| {});
12948
12949 let fs = FakeFs::new(cx.executor());
12950 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12951
12952 let buffer = project.update(cx, |project, cx| {
12953 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12954 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12955 });
12956 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12957 let follower = cx.update(|cx| {
12958 cx.open_window(
12959 WindowOptions {
12960 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12961 gpui::Point::new(px(0.), px(0.)),
12962 gpui::Point::new(px(10.), px(80.)),
12963 ))),
12964 ..Default::default()
12965 },
12966 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12967 )
12968 .unwrap()
12969 });
12970
12971 let is_still_following = Rc::new(RefCell::new(true));
12972 let follower_edit_event_count = Rc::new(RefCell::new(0));
12973 let pending_update = Rc::new(RefCell::new(None));
12974 let leader_entity = leader.root(cx).unwrap();
12975 let follower_entity = follower.root(cx).unwrap();
12976 _ = follower.update(cx, {
12977 let update = pending_update.clone();
12978 let is_still_following = is_still_following.clone();
12979 let follower_edit_event_count = follower_edit_event_count.clone();
12980 |_, window, cx| {
12981 cx.subscribe_in(
12982 &leader_entity,
12983 window,
12984 move |_, leader, event, window, cx| {
12985 leader.read(cx).add_event_to_update_proto(
12986 event,
12987 &mut update.borrow_mut(),
12988 window,
12989 cx,
12990 );
12991 },
12992 )
12993 .detach();
12994
12995 cx.subscribe_in(
12996 &follower_entity,
12997 window,
12998 move |_, _, event: &EditorEvent, _window, _cx| {
12999 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13000 *is_still_following.borrow_mut() = false;
13001 }
13002
13003 if let EditorEvent::BufferEdited = event {
13004 *follower_edit_event_count.borrow_mut() += 1;
13005 }
13006 },
13007 )
13008 .detach();
13009 }
13010 });
13011
13012 // Update the selections only
13013 _ = leader.update(cx, |leader, window, cx| {
13014 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13015 });
13016 follower
13017 .update(cx, |follower, window, cx| {
13018 follower.apply_update_proto(
13019 &project,
13020 pending_update.borrow_mut().take().unwrap(),
13021 window,
13022 cx,
13023 )
13024 })
13025 .unwrap()
13026 .await
13027 .unwrap();
13028 _ = follower.update(cx, |follower, _, cx| {
13029 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13030 });
13031 assert!(*is_still_following.borrow());
13032 assert_eq!(*follower_edit_event_count.borrow(), 0);
13033
13034 // Update the scroll position only
13035 _ = leader.update(cx, |leader, window, cx| {
13036 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13037 });
13038 follower
13039 .update(cx, |follower, window, cx| {
13040 follower.apply_update_proto(
13041 &project,
13042 pending_update.borrow_mut().take().unwrap(),
13043 window,
13044 cx,
13045 )
13046 })
13047 .unwrap()
13048 .await
13049 .unwrap();
13050 assert_eq!(
13051 follower
13052 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13053 .unwrap(),
13054 gpui::Point::new(1.5, 3.5)
13055 );
13056 assert!(*is_still_following.borrow());
13057 assert_eq!(*follower_edit_event_count.borrow(), 0);
13058
13059 // Update the selections and scroll position. The follower's scroll position is updated
13060 // via autoscroll, not via the leader's exact scroll position.
13061 _ = leader.update(cx, |leader, window, cx| {
13062 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13063 leader.request_autoscroll(Autoscroll::newest(), cx);
13064 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13065 });
13066 follower
13067 .update(cx, |follower, window, cx| {
13068 follower.apply_update_proto(
13069 &project,
13070 pending_update.borrow_mut().take().unwrap(),
13071 window,
13072 cx,
13073 )
13074 })
13075 .unwrap()
13076 .await
13077 .unwrap();
13078 _ = follower.update(cx, |follower, _, cx| {
13079 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13080 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13081 });
13082 assert!(*is_still_following.borrow());
13083
13084 // Creating a pending selection that precedes another selection
13085 _ = leader.update(cx, |leader, window, cx| {
13086 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13087 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13088 });
13089 follower
13090 .update(cx, |follower, window, cx| {
13091 follower.apply_update_proto(
13092 &project,
13093 pending_update.borrow_mut().take().unwrap(),
13094 window,
13095 cx,
13096 )
13097 })
13098 .unwrap()
13099 .await
13100 .unwrap();
13101 _ = follower.update(cx, |follower, _, cx| {
13102 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13103 });
13104 assert!(*is_still_following.borrow());
13105
13106 // Extend the pending selection so that it surrounds another selection
13107 _ = leader.update(cx, |leader, window, cx| {
13108 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13109 });
13110 follower
13111 .update(cx, |follower, window, cx| {
13112 follower.apply_update_proto(
13113 &project,
13114 pending_update.borrow_mut().take().unwrap(),
13115 window,
13116 cx,
13117 )
13118 })
13119 .unwrap()
13120 .await
13121 .unwrap();
13122 _ = follower.update(cx, |follower, _, cx| {
13123 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13124 });
13125
13126 // Scrolling locally breaks the follow
13127 _ = follower.update(cx, |follower, window, cx| {
13128 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13129 follower.set_scroll_anchor(
13130 ScrollAnchor {
13131 anchor: top_anchor,
13132 offset: gpui::Point::new(0.0, 0.5),
13133 },
13134 window,
13135 cx,
13136 );
13137 });
13138 assert!(!(*is_still_following.borrow()));
13139}
13140
13141#[gpui::test]
13142async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13143 init_test(cx, |_| {});
13144
13145 let fs = FakeFs::new(cx.executor());
13146 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13147 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13148 let pane = workspace
13149 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13150 .unwrap();
13151
13152 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13153
13154 let leader = pane.update_in(cx, |_, window, cx| {
13155 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13156 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13157 });
13158
13159 // Start following the editor when it has no excerpts.
13160 let mut state_message =
13161 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13162 let workspace_entity = workspace.root(cx).unwrap();
13163 let follower_1 = cx
13164 .update_window(*workspace.deref(), |_, window, cx| {
13165 Editor::from_state_proto(
13166 workspace_entity,
13167 ViewId {
13168 creator: CollaboratorId::PeerId(PeerId::default()),
13169 id: 0,
13170 },
13171 &mut state_message,
13172 window,
13173 cx,
13174 )
13175 })
13176 .unwrap()
13177 .unwrap()
13178 .await
13179 .unwrap();
13180
13181 let update_message = Rc::new(RefCell::new(None));
13182 follower_1.update_in(cx, {
13183 let update = update_message.clone();
13184 |_, window, cx| {
13185 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13186 leader.read(cx).add_event_to_update_proto(
13187 event,
13188 &mut update.borrow_mut(),
13189 window,
13190 cx,
13191 );
13192 })
13193 .detach();
13194 }
13195 });
13196
13197 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13198 (
13199 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13200 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13201 )
13202 });
13203
13204 // Insert some excerpts.
13205 leader.update(cx, |leader, cx| {
13206 leader.buffer.update(cx, |multibuffer, cx| {
13207 multibuffer.set_excerpts_for_path(
13208 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13209 buffer_1.clone(),
13210 vec![
13211 Point::row_range(0..3),
13212 Point::row_range(1..6),
13213 Point::row_range(12..15),
13214 ],
13215 0,
13216 cx,
13217 );
13218 multibuffer.set_excerpts_for_path(
13219 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13220 buffer_2.clone(),
13221 vec![Point::row_range(0..6), Point::row_range(8..12)],
13222 0,
13223 cx,
13224 );
13225 });
13226 });
13227
13228 // Apply the update of adding the excerpts.
13229 follower_1
13230 .update_in(cx, |follower, window, cx| {
13231 follower.apply_update_proto(
13232 &project,
13233 update_message.borrow().clone().unwrap(),
13234 window,
13235 cx,
13236 )
13237 })
13238 .await
13239 .unwrap();
13240 assert_eq!(
13241 follower_1.update(cx, |editor, cx| editor.text(cx)),
13242 leader.update(cx, |editor, cx| editor.text(cx))
13243 );
13244 update_message.borrow_mut().take();
13245
13246 // Start following separately after it already has excerpts.
13247 let mut state_message =
13248 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13249 let workspace_entity = workspace.root(cx).unwrap();
13250 let follower_2 = cx
13251 .update_window(*workspace.deref(), |_, window, cx| {
13252 Editor::from_state_proto(
13253 workspace_entity,
13254 ViewId {
13255 creator: CollaboratorId::PeerId(PeerId::default()),
13256 id: 0,
13257 },
13258 &mut state_message,
13259 window,
13260 cx,
13261 )
13262 })
13263 .unwrap()
13264 .unwrap()
13265 .await
13266 .unwrap();
13267 assert_eq!(
13268 follower_2.update(cx, |editor, cx| editor.text(cx)),
13269 leader.update(cx, |editor, cx| editor.text(cx))
13270 );
13271
13272 // Remove some excerpts.
13273 leader.update(cx, |leader, cx| {
13274 leader.buffer.update(cx, |multibuffer, cx| {
13275 let excerpt_ids = multibuffer.excerpt_ids();
13276 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13277 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13278 });
13279 });
13280
13281 // Apply the update of removing the excerpts.
13282 follower_1
13283 .update_in(cx, |follower, window, cx| {
13284 follower.apply_update_proto(
13285 &project,
13286 update_message.borrow().clone().unwrap(),
13287 window,
13288 cx,
13289 )
13290 })
13291 .await
13292 .unwrap();
13293 follower_2
13294 .update_in(cx, |follower, window, cx| {
13295 follower.apply_update_proto(
13296 &project,
13297 update_message.borrow().clone().unwrap(),
13298 window,
13299 cx,
13300 )
13301 })
13302 .await
13303 .unwrap();
13304 update_message.borrow_mut().take();
13305 assert_eq!(
13306 follower_1.update(cx, |editor, cx| editor.text(cx)),
13307 leader.update(cx, |editor, cx| editor.text(cx))
13308 );
13309}
13310
13311#[gpui::test]
13312async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13313 init_test(cx, |_| {});
13314
13315 let mut cx = EditorTestContext::new(cx).await;
13316 let lsp_store =
13317 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13318
13319 cx.set_state(indoc! {"
13320 ˇfn func(abc def: i32) -> u32 {
13321 }
13322 "});
13323
13324 cx.update(|_, cx| {
13325 lsp_store.update(cx, |lsp_store, cx| {
13326 lsp_store
13327 .update_diagnostics(
13328 LanguageServerId(0),
13329 lsp::PublishDiagnosticsParams {
13330 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13331 version: None,
13332 diagnostics: vec![
13333 lsp::Diagnostic {
13334 range: lsp::Range::new(
13335 lsp::Position::new(0, 11),
13336 lsp::Position::new(0, 12),
13337 ),
13338 severity: Some(lsp::DiagnosticSeverity::ERROR),
13339 ..Default::default()
13340 },
13341 lsp::Diagnostic {
13342 range: lsp::Range::new(
13343 lsp::Position::new(0, 12),
13344 lsp::Position::new(0, 15),
13345 ),
13346 severity: Some(lsp::DiagnosticSeverity::ERROR),
13347 ..Default::default()
13348 },
13349 lsp::Diagnostic {
13350 range: lsp::Range::new(
13351 lsp::Position::new(0, 25),
13352 lsp::Position::new(0, 28),
13353 ),
13354 severity: Some(lsp::DiagnosticSeverity::ERROR),
13355 ..Default::default()
13356 },
13357 ],
13358 },
13359 &[],
13360 cx,
13361 )
13362 .unwrap()
13363 });
13364 });
13365
13366 executor.run_until_parked();
13367
13368 cx.update_editor(|editor, window, cx| {
13369 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13370 });
13371
13372 cx.assert_editor_state(indoc! {"
13373 fn func(abc def: i32) -> ˇu32 {
13374 }
13375 "});
13376
13377 cx.update_editor(|editor, window, cx| {
13378 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13379 });
13380
13381 cx.assert_editor_state(indoc! {"
13382 fn func(abc ˇdef: i32) -> u32 {
13383 }
13384 "});
13385
13386 cx.update_editor(|editor, window, cx| {
13387 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13388 });
13389
13390 cx.assert_editor_state(indoc! {"
13391 fn func(abcˇ def: i32) -> u32 {
13392 }
13393 "});
13394
13395 cx.update_editor(|editor, window, cx| {
13396 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13397 });
13398
13399 cx.assert_editor_state(indoc! {"
13400 fn func(abc def: i32) -> ˇu32 {
13401 }
13402 "});
13403}
13404
13405#[gpui::test]
13406async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13407 init_test(cx, |_| {});
13408
13409 let mut cx = EditorTestContext::new(cx).await;
13410
13411 let diff_base = r#"
13412 use some::mod;
13413
13414 const A: u32 = 42;
13415
13416 fn main() {
13417 println!("hello");
13418
13419 println!("world");
13420 }
13421 "#
13422 .unindent();
13423
13424 // Edits are modified, removed, modified, added
13425 cx.set_state(
13426 &r#"
13427 use some::modified;
13428
13429 ˇ
13430 fn main() {
13431 println!("hello there");
13432
13433 println!("around the");
13434 println!("world");
13435 }
13436 "#
13437 .unindent(),
13438 );
13439
13440 cx.set_head_text(&diff_base);
13441 executor.run_until_parked();
13442
13443 cx.update_editor(|editor, window, cx| {
13444 //Wrap around the bottom of the buffer
13445 for _ in 0..3 {
13446 editor.go_to_next_hunk(&GoToHunk, window, cx);
13447 }
13448 });
13449
13450 cx.assert_editor_state(
13451 &r#"
13452 ˇuse some::modified;
13453
13454
13455 fn main() {
13456 println!("hello there");
13457
13458 println!("around the");
13459 println!("world");
13460 }
13461 "#
13462 .unindent(),
13463 );
13464
13465 cx.update_editor(|editor, window, cx| {
13466 //Wrap around the top of the buffer
13467 for _ in 0..2 {
13468 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13469 }
13470 });
13471
13472 cx.assert_editor_state(
13473 &r#"
13474 use some::modified;
13475
13476
13477 fn main() {
13478 ˇ println!("hello there");
13479
13480 println!("around the");
13481 println!("world");
13482 }
13483 "#
13484 .unindent(),
13485 );
13486
13487 cx.update_editor(|editor, window, cx| {
13488 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13489 });
13490
13491 cx.assert_editor_state(
13492 &r#"
13493 use some::modified;
13494
13495 ˇ
13496 fn main() {
13497 println!("hello there");
13498
13499 println!("around the");
13500 println!("world");
13501 }
13502 "#
13503 .unindent(),
13504 );
13505
13506 cx.update_editor(|editor, window, cx| {
13507 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13508 });
13509
13510 cx.assert_editor_state(
13511 &r#"
13512 ˇuse some::modified;
13513
13514
13515 fn main() {
13516 println!("hello there");
13517
13518 println!("around the");
13519 println!("world");
13520 }
13521 "#
13522 .unindent(),
13523 );
13524
13525 cx.update_editor(|editor, window, cx| {
13526 for _ in 0..2 {
13527 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13528 }
13529 });
13530
13531 cx.assert_editor_state(
13532 &r#"
13533 use some::modified;
13534
13535
13536 fn main() {
13537 ˇ println!("hello there");
13538
13539 println!("around the");
13540 println!("world");
13541 }
13542 "#
13543 .unindent(),
13544 );
13545
13546 cx.update_editor(|editor, window, cx| {
13547 editor.fold(&Fold, window, cx);
13548 });
13549
13550 cx.update_editor(|editor, window, cx| {
13551 editor.go_to_next_hunk(&GoToHunk, window, cx);
13552 });
13553
13554 cx.assert_editor_state(
13555 &r#"
13556 ˇuse some::modified;
13557
13558
13559 fn main() {
13560 println!("hello there");
13561
13562 println!("around the");
13563 println!("world");
13564 }
13565 "#
13566 .unindent(),
13567 );
13568}
13569
13570#[test]
13571fn test_split_words() {
13572 fn split(text: &str) -> Vec<&str> {
13573 split_words(text).collect()
13574 }
13575
13576 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13577 assert_eq!(split("hello_world"), &["hello_", "world"]);
13578 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13579 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13580 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13581 assert_eq!(split("helloworld"), &["helloworld"]);
13582
13583 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13584}
13585
13586#[gpui::test]
13587async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13588 init_test(cx, |_| {});
13589
13590 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13591 let mut assert = |before, after| {
13592 let _state_context = cx.set_state(before);
13593 cx.run_until_parked();
13594 cx.update_editor(|editor, window, cx| {
13595 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13596 });
13597 cx.run_until_parked();
13598 cx.assert_editor_state(after);
13599 };
13600
13601 // Outside bracket jumps to outside of matching bracket
13602 assert("console.logˇ(var);", "console.log(var)ˇ;");
13603 assert("console.log(var)ˇ;", "console.logˇ(var);");
13604
13605 // Inside bracket jumps to inside of matching bracket
13606 assert("console.log(ˇvar);", "console.log(varˇ);");
13607 assert("console.log(varˇ);", "console.log(ˇvar);");
13608
13609 // When outside a bracket and inside, favor jumping to the inside bracket
13610 assert(
13611 "console.log('foo', [1, 2, 3]ˇ);",
13612 "console.log(ˇ'foo', [1, 2, 3]);",
13613 );
13614 assert(
13615 "console.log(ˇ'foo', [1, 2, 3]);",
13616 "console.log('foo', [1, 2, 3]ˇ);",
13617 );
13618
13619 // Bias forward if two options are equally likely
13620 assert(
13621 "let result = curried_fun()ˇ();",
13622 "let result = curried_fun()()ˇ;",
13623 );
13624
13625 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13626 assert(
13627 indoc! {"
13628 function test() {
13629 console.log('test')ˇ
13630 }"},
13631 indoc! {"
13632 function test() {
13633 console.logˇ('test')
13634 }"},
13635 );
13636}
13637
13638#[gpui::test]
13639async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13640 init_test(cx, |_| {});
13641
13642 let fs = FakeFs::new(cx.executor());
13643 fs.insert_tree(
13644 path!("/a"),
13645 json!({
13646 "main.rs": "fn main() { let a = 5; }",
13647 "other.rs": "// Test file",
13648 }),
13649 )
13650 .await;
13651 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13652
13653 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13654 language_registry.add(Arc::new(Language::new(
13655 LanguageConfig {
13656 name: "Rust".into(),
13657 matcher: LanguageMatcher {
13658 path_suffixes: vec!["rs".to_string()],
13659 ..Default::default()
13660 },
13661 brackets: BracketPairConfig {
13662 pairs: vec![BracketPair {
13663 start: "{".to_string(),
13664 end: "}".to_string(),
13665 close: true,
13666 surround: true,
13667 newline: true,
13668 }],
13669 disabled_scopes_by_bracket_ix: Vec::new(),
13670 },
13671 ..Default::default()
13672 },
13673 Some(tree_sitter_rust::LANGUAGE.into()),
13674 )));
13675 let mut fake_servers = language_registry.register_fake_lsp(
13676 "Rust",
13677 FakeLspAdapter {
13678 capabilities: lsp::ServerCapabilities {
13679 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13680 first_trigger_character: "{".to_string(),
13681 more_trigger_character: None,
13682 }),
13683 ..Default::default()
13684 },
13685 ..Default::default()
13686 },
13687 );
13688
13689 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13690
13691 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13692
13693 let worktree_id = workspace
13694 .update(cx, |workspace, _, cx| {
13695 workspace.project().update(cx, |project, cx| {
13696 project.worktrees(cx).next().unwrap().read(cx).id()
13697 })
13698 })
13699 .unwrap();
13700
13701 let buffer = project
13702 .update(cx, |project, cx| {
13703 project.open_local_buffer(path!("/a/main.rs"), cx)
13704 })
13705 .await
13706 .unwrap();
13707 let editor_handle = workspace
13708 .update(cx, |workspace, window, cx| {
13709 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13710 })
13711 .unwrap()
13712 .await
13713 .unwrap()
13714 .downcast::<Editor>()
13715 .unwrap();
13716
13717 cx.executor().start_waiting();
13718 let fake_server = fake_servers.next().await.unwrap();
13719
13720 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13721 |params, _| async move {
13722 assert_eq!(
13723 params.text_document_position.text_document.uri,
13724 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13725 );
13726 assert_eq!(
13727 params.text_document_position.position,
13728 lsp::Position::new(0, 21),
13729 );
13730
13731 Ok(Some(vec![lsp::TextEdit {
13732 new_text: "]".to_string(),
13733 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13734 }]))
13735 },
13736 );
13737
13738 editor_handle.update_in(cx, |editor, window, cx| {
13739 window.focus(&editor.focus_handle(cx));
13740 editor.change_selections(None, window, cx, |s| {
13741 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13742 });
13743 editor.handle_input("{", window, cx);
13744 });
13745
13746 cx.executor().run_until_parked();
13747
13748 buffer.update(cx, |buffer, _| {
13749 assert_eq!(
13750 buffer.text(),
13751 "fn main() { let a = {5}; }",
13752 "No extra braces from on type formatting should appear in the buffer"
13753 )
13754 });
13755}
13756
13757#[gpui::test]
13758async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13759 init_test(cx, |_| {});
13760
13761 let fs = FakeFs::new(cx.executor());
13762 fs.insert_tree(
13763 path!("/a"),
13764 json!({
13765 "main.rs": "fn main() { let a = 5; }",
13766 "other.rs": "// Test file",
13767 }),
13768 )
13769 .await;
13770
13771 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13772
13773 let server_restarts = Arc::new(AtomicUsize::new(0));
13774 let closure_restarts = Arc::clone(&server_restarts);
13775 let language_server_name = "test language server";
13776 let language_name: LanguageName = "Rust".into();
13777
13778 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13779 language_registry.add(Arc::new(Language::new(
13780 LanguageConfig {
13781 name: language_name.clone(),
13782 matcher: LanguageMatcher {
13783 path_suffixes: vec!["rs".to_string()],
13784 ..Default::default()
13785 },
13786 ..Default::default()
13787 },
13788 Some(tree_sitter_rust::LANGUAGE.into()),
13789 )));
13790 let mut fake_servers = language_registry.register_fake_lsp(
13791 "Rust",
13792 FakeLspAdapter {
13793 name: language_server_name,
13794 initialization_options: Some(json!({
13795 "testOptionValue": true
13796 })),
13797 initializer: Some(Box::new(move |fake_server| {
13798 let task_restarts = Arc::clone(&closure_restarts);
13799 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13800 task_restarts.fetch_add(1, atomic::Ordering::Release);
13801 futures::future::ready(Ok(()))
13802 });
13803 })),
13804 ..Default::default()
13805 },
13806 );
13807
13808 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13809 let _buffer = project
13810 .update(cx, |project, cx| {
13811 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13812 })
13813 .await
13814 .unwrap();
13815 let _fake_server = fake_servers.next().await.unwrap();
13816 update_test_language_settings(cx, |language_settings| {
13817 language_settings.languages.insert(
13818 language_name.clone(),
13819 LanguageSettingsContent {
13820 tab_size: NonZeroU32::new(8),
13821 ..Default::default()
13822 },
13823 );
13824 });
13825 cx.executor().run_until_parked();
13826 assert_eq!(
13827 server_restarts.load(atomic::Ordering::Acquire),
13828 0,
13829 "Should not restart LSP server on an unrelated change"
13830 );
13831
13832 update_test_project_settings(cx, |project_settings| {
13833 project_settings.lsp.insert(
13834 "Some other server name".into(),
13835 LspSettings {
13836 binary: None,
13837 settings: None,
13838 initialization_options: Some(json!({
13839 "some other init value": false
13840 })),
13841 enable_lsp_tasks: false,
13842 },
13843 );
13844 });
13845 cx.executor().run_until_parked();
13846 assert_eq!(
13847 server_restarts.load(atomic::Ordering::Acquire),
13848 0,
13849 "Should not restart LSP server on an unrelated LSP settings change"
13850 );
13851
13852 update_test_project_settings(cx, |project_settings| {
13853 project_settings.lsp.insert(
13854 language_server_name.into(),
13855 LspSettings {
13856 binary: None,
13857 settings: None,
13858 initialization_options: Some(json!({
13859 "anotherInitValue": false
13860 })),
13861 enable_lsp_tasks: false,
13862 },
13863 );
13864 });
13865 cx.executor().run_until_parked();
13866 assert_eq!(
13867 server_restarts.load(atomic::Ordering::Acquire),
13868 1,
13869 "Should restart LSP server on a related LSP settings change"
13870 );
13871
13872 update_test_project_settings(cx, |project_settings| {
13873 project_settings.lsp.insert(
13874 language_server_name.into(),
13875 LspSettings {
13876 binary: None,
13877 settings: None,
13878 initialization_options: Some(json!({
13879 "anotherInitValue": false
13880 })),
13881 enable_lsp_tasks: false,
13882 },
13883 );
13884 });
13885 cx.executor().run_until_parked();
13886 assert_eq!(
13887 server_restarts.load(atomic::Ordering::Acquire),
13888 1,
13889 "Should not restart LSP server on a related LSP settings change that is the same"
13890 );
13891
13892 update_test_project_settings(cx, |project_settings| {
13893 project_settings.lsp.insert(
13894 language_server_name.into(),
13895 LspSettings {
13896 binary: None,
13897 settings: None,
13898 initialization_options: None,
13899 enable_lsp_tasks: false,
13900 },
13901 );
13902 });
13903 cx.executor().run_until_parked();
13904 assert_eq!(
13905 server_restarts.load(atomic::Ordering::Acquire),
13906 2,
13907 "Should restart LSP server on another related LSP settings change"
13908 );
13909}
13910
13911#[gpui::test]
13912async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13913 init_test(cx, |_| {});
13914
13915 let mut cx = EditorLspTestContext::new_rust(
13916 lsp::ServerCapabilities {
13917 completion_provider: Some(lsp::CompletionOptions {
13918 trigger_characters: Some(vec![".".to_string()]),
13919 resolve_provider: Some(true),
13920 ..Default::default()
13921 }),
13922 ..Default::default()
13923 },
13924 cx,
13925 )
13926 .await;
13927
13928 cx.set_state("fn main() { let a = 2ˇ; }");
13929 cx.simulate_keystroke(".");
13930 let completion_item = lsp::CompletionItem {
13931 label: "some".into(),
13932 kind: Some(lsp::CompletionItemKind::SNIPPET),
13933 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13934 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13935 kind: lsp::MarkupKind::Markdown,
13936 value: "```rust\nSome(2)\n```".to_string(),
13937 })),
13938 deprecated: Some(false),
13939 sort_text: Some("fffffff2".to_string()),
13940 filter_text: Some("some".to_string()),
13941 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13942 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13943 range: lsp::Range {
13944 start: lsp::Position {
13945 line: 0,
13946 character: 22,
13947 },
13948 end: lsp::Position {
13949 line: 0,
13950 character: 22,
13951 },
13952 },
13953 new_text: "Some(2)".to_string(),
13954 })),
13955 additional_text_edits: Some(vec![lsp::TextEdit {
13956 range: lsp::Range {
13957 start: lsp::Position {
13958 line: 0,
13959 character: 20,
13960 },
13961 end: lsp::Position {
13962 line: 0,
13963 character: 22,
13964 },
13965 },
13966 new_text: "".to_string(),
13967 }]),
13968 ..Default::default()
13969 };
13970
13971 let closure_completion_item = completion_item.clone();
13972 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13973 let task_completion_item = closure_completion_item.clone();
13974 async move {
13975 Ok(Some(lsp::CompletionResponse::Array(vec![
13976 task_completion_item,
13977 ])))
13978 }
13979 });
13980
13981 request.next().await;
13982
13983 cx.condition(|editor, _| editor.context_menu_visible())
13984 .await;
13985 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13986 editor
13987 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13988 .unwrap()
13989 });
13990 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13991
13992 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13993 let task_completion_item = completion_item.clone();
13994 async move { Ok(task_completion_item) }
13995 })
13996 .next()
13997 .await
13998 .unwrap();
13999 apply_additional_edits.await.unwrap();
14000 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14001}
14002
14003#[gpui::test]
14004async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14005 init_test(cx, |_| {});
14006
14007 let mut cx = EditorLspTestContext::new_rust(
14008 lsp::ServerCapabilities {
14009 completion_provider: Some(lsp::CompletionOptions {
14010 trigger_characters: Some(vec![".".to_string()]),
14011 resolve_provider: Some(true),
14012 ..Default::default()
14013 }),
14014 ..Default::default()
14015 },
14016 cx,
14017 )
14018 .await;
14019
14020 cx.set_state("fn main() { let a = 2ˇ; }");
14021 cx.simulate_keystroke(".");
14022
14023 let item1 = lsp::CompletionItem {
14024 label: "method id()".to_string(),
14025 filter_text: Some("id".to_string()),
14026 detail: None,
14027 documentation: None,
14028 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14029 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14030 new_text: ".id".to_string(),
14031 })),
14032 ..lsp::CompletionItem::default()
14033 };
14034
14035 let item2 = lsp::CompletionItem {
14036 label: "other".to_string(),
14037 filter_text: Some("other".to_string()),
14038 detail: None,
14039 documentation: None,
14040 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14041 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14042 new_text: ".other".to_string(),
14043 })),
14044 ..lsp::CompletionItem::default()
14045 };
14046
14047 let item1 = item1.clone();
14048 cx.set_request_handler::<lsp::request::Completion, _, _>({
14049 let item1 = item1.clone();
14050 move |_, _, _| {
14051 let item1 = item1.clone();
14052 let item2 = item2.clone();
14053 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14054 }
14055 })
14056 .next()
14057 .await;
14058
14059 cx.condition(|editor, _| editor.context_menu_visible())
14060 .await;
14061 cx.update_editor(|editor, _, _| {
14062 let context_menu = editor.context_menu.borrow_mut();
14063 let context_menu = context_menu
14064 .as_ref()
14065 .expect("Should have the context menu deployed");
14066 match context_menu {
14067 CodeContextMenu::Completions(completions_menu) => {
14068 let completions = completions_menu.completions.borrow_mut();
14069 assert_eq!(
14070 completions
14071 .iter()
14072 .map(|completion| &completion.label.text)
14073 .collect::<Vec<_>>(),
14074 vec!["method id()", "other"]
14075 )
14076 }
14077 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14078 }
14079 });
14080
14081 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14082 let item1 = item1.clone();
14083 move |_, item_to_resolve, _| {
14084 let item1 = item1.clone();
14085 async move {
14086 if item1 == item_to_resolve {
14087 Ok(lsp::CompletionItem {
14088 label: "method id()".to_string(),
14089 filter_text: Some("id".to_string()),
14090 detail: Some("Now resolved!".to_string()),
14091 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14092 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14093 range: lsp::Range::new(
14094 lsp::Position::new(0, 22),
14095 lsp::Position::new(0, 22),
14096 ),
14097 new_text: ".id".to_string(),
14098 })),
14099 ..lsp::CompletionItem::default()
14100 })
14101 } else {
14102 Ok(item_to_resolve)
14103 }
14104 }
14105 }
14106 })
14107 .next()
14108 .await
14109 .unwrap();
14110 cx.run_until_parked();
14111
14112 cx.update_editor(|editor, window, cx| {
14113 editor.context_menu_next(&Default::default(), window, cx);
14114 });
14115
14116 cx.update_editor(|editor, _, _| {
14117 let context_menu = editor.context_menu.borrow_mut();
14118 let context_menu = context_menu
14119 .as_ref()
14120 .expect("Should have the context menu deployed");
14121 match context_menu {
14122 CodeContextMenu::Completions(completions_menu) => {
14123 let completions = completions_menu.completions.borrow_mut();
14124 assert_eq!(
14125 completions
14126 .iter()
14127 .map(|completion| &completion.label.text)
14128 .collect::<Vec<_>>(),
14129 vec!["method id() Now resolved!", "other"],
14130 "Should update first completion label, but not second as the filter text did not match."
14131 );
14132 }
14133 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14134 }
14135 });
14136}
14137
14138#[gpui::test]
14139async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14140 init_test(cx, |_| {});
14141 let mut cx = EditorLspTestContext::new_rust(
14142 lsp::ServerCapabilities {
14143 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14144 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14145 completion_provider: Some(lsp::CompletionOptions {
14146 resolve_provider: Some(true),
14147 ..Default::default()
14148 }),
14149 ..Default::default()
14150 },
14151 cx,
14152 )
14153 .await;
14154 cx.set_state(indoc! {"
14155 struct TestStruct {
14156 field: i32
14157 }
14158
14159 fn mainˇ() {
14160 let unused_var = 42;
14161 let test_struct = TestStruct { field: 42 };
14162 }
14163 "});
14164 let symbol_range = cx.lsp_range(indoc! {"
14165 struct TestStruct {
14166 field: i32
14167 }
14168
14169 «fn main»() {
14170 let unused_var = 42;
14171 let test_struct = TestStruct { field: 42 };
14172 }
14173 "});
14174 let mut hover_requests =
14175 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14176 Ok(Some(lsp::Hover {
14177 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14178 kind: lsp::MarkupKind::Markdown,
14179 value: "Function documentation".to_string(),
14180 }),
14181 range: Some(symbol_range),
14182 }))
14183 });
14184
14185 // Case 1: Test that code action menu hide hover popover
14186 cx.dispatch_action(Hover);
14187 hover_requests.next().await;
14188 cx.condition(|editor, _| editor.hover_state.visible()).await;
14189 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14190 move |_, _, _| async move {
14191 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14192 lsp::CodeAction {
14193 title: "Remove unused variable".to_string(),
14194 kind: Some(CodeActionKind::QUICKFIX),
14195 edit: Some(lsp::WorkspaceEdit {
14196 changes: Some(
14197 [(
14198 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14199 vec![lsp::TextEdit {
14200 range: lsp::Range::new(
14201 lsp::Position::new(5, 4),
14202 lsp::Position::new(5, 27),
14203 ),
14204 new_text: "".to_string(),
14205 }],
14206 )]
14207 .into_iter()
14208 .collect(),
14209 ),
14210 ..Default::default()
14211 }),
14212 ..Default::default()
14213 },
14214 )]))
14215 },
14216 );
14217 cx.update_editor(|editor, window, cx| {
14218 editor.toggle_code_actions(
14219 &ToggleCodeActions {
14220 deployed_from_indicator: None,
14221 quick_launch: false,
14222 },
14223 window,
14224 cx,
14225 );
14226 });
14227 code_action_requests.next().await;
14228 cx.run_until_parked();
14229 cx.condition(|editor, _| editor.context_menu_visible())
14230 .await;
14231 cx.update_editor(|editor, _, _| {
14232 assert!(
14233 !editor.hover_state.visible(),
14234 "Hover popover should be hidden when code action menu is shown"
14235 );
14236 // Hide code actions
14237 editor.context_menu.take();
14238 });
14239
14240 // Case 2: Test that code completions hide hover popover
14241 cx.dispatch_action(Hover);
14242 hover_requests.next().await;
14243 cx.condition(|editor, _| editor.hover_state.visible()).await;
14244 let counter = Arc::new(AtomicUsize::new(0));
14245 let mut completion_requests =
14246 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14247 let counter = counter.clone();
14248 async move {
14249 counter.fetch_add(1, atomic::Ordering::Release);
14250 Ok(Some(lsp::CompletionResponse::Array(vec![
14251 lsp::CompletionItem {
14252 label: "main".into(),
14253 kind: Some(lsp::CompletionItemKind::FUNCTION),
14254 detail: Some("() -> ()".to_string()),
14255 ..Default::default()
14256 },
14257 lsp::CompletionItem {
14258 label: "TestStruct".into(),
14259 kind: Some(lsp::CompletionItemKind::STRUCT),
14260 detail: Some("struct TestStruct".to_string()),
14261 ..Default::default()
14262 },
14263 ])))
14264 }
14265 });
14266 cx.update_editor(|editor, window, cx| {
14267 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14268 });
14269 completion_requests.next().await;
14270 cx.condition(|editor, _| editor.context_menu_visible())
14271 .await;
14272 cx.update_editor(|editor, _, _| {
14273 assert!(
14274 !editor.hover_state.visible(),
14275 "Hover popover should be hidden when completion menu is shown"
14276 );
14277 });
14278}
14279
14280#[gpui::test]
14281async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14282 init_test(cx, |_| {});
14283
14284 let mut cx = EditorLspTestContext::new_rust(
14285 lsp::ServerCapabilities {
14286 completion_provider: Some(lsp::CompletionOptions {
14287 trigger_characters: Some(vec![".".to_string()]),
14288 resolve_provider: Some(true),
14289 ..Default::default()
14290 }),
14291 ..Default::default()
14292 },
14293 cx,
14294 )
14295 .await;
14296
14297 cx.set_state("fn main() { let a = 2ˇ; }");
14298 cx.simulate_keystroke(".");
14299
14300 let unresolved_item_1 = lsp::CompletionItem {
14301 label: "id".to_string(),
14302 filter_text: Some("id".to_string()),
14303 detail: None,
14304 documentation: None,
14305 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14306 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14307 new_text: ".id".to_string(),
14308 })),
14309 ..lsp::CompletionItem::default()
14310 };
14311 let resolved_item_1 = lsp::CompletionItem {
14312 additional_text_edits: Some(vec![lsp::TextEdit {
14313 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14314 new_text: "!!".to_string(),
14315 }]),
14316 ..unresolved_item_1.clone()
14317 };
14318 let unresolved_item_2 = lsp::CompletionItem {
14319 label: "other".to_string(),
14320 filter_text: Some("other".to_string()),
14321 detail: None,
14322 documentation: None,
14323 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14324 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14325 new_text: ".other".to_string(),
14326 })),
14327 ..lsp::CompletionItem::default()
14328 };
14329 let resolved_item_2 = lsp::CompletionItem {
14330 additional_text_edits: Some(vec![lsp::TextEdit {
14331 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14332 new_text: "??".to_string(),
14333 }]),
14334 ..unresolved_item_2.clone()
14335 };
14336
14337 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14338 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14339 cx.lsp
14340 .server
14341 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14342 let unresolved_item_1 = unresolved_item_1.clone();
14343 let resolved_item_1 = resolved_item_1.clone();
14344 let unresolved_item_2 = unresolved_item_2.clone();
14345 let resolved_item_2 = resolved_item_2.clone();
14346 let resolve_requests_1 = resolve_requests_1.clone();
14347 let resolve_requests_2 = resolve_requests_2.clone();
14348 move |unresolved_request, _| {
14349 let unresolved_item_1 = unresolved_item_1.clone();
14350 let resolved_item_1 = resolved_item_1.clone();
14351 let unresolved_item_2 = unresolved_item_2.clone();
14352 let resolved_item_2 = resolved_item_2.clone();
14353 let resolve_requests_1 = resolve_requests_1.clone();
14354 let resolve_requests_2 = resolve_requests_2.clone();
14355 async move {
14356 if unresolved_request == unresolved_item_1 {
14357 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14358 Ok(resolved_item_1.clone())
14359 } else if unresolved_request == unresolved_item_2 {
14360 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14361 Ok(resolved_item_2.clone())
14362 } else {
14363 panic!("Unexpected completion item {unresolved_request:?}")
14364 }
14365 }
14366 }
14367 })
14368 .detach();
14369
14370 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14371 let unresolved_item_1 = unresolved_item_1.clone();
14372 let unresolved_item_2 = unresolved_item_2.clone();
14373 async move {
14374 Ok(Some(lsp::CompletionResponse::Array(vec![
14375 unresolved_item_1,
14376 unresolved_item_2,
14377 ])))
14378 }
14379 })
14380 .next()
14381 .await;
14382
14383 cx.condition(|editor, _| editor.context_menu_visible())
14384 .await;
14385 cx.update_editor(|editor, _, _| {
14386 let context_menu = editor.context_menu.borrow_mut();
14387 let context_menu = context_menu
14388 .as_ref()
14389 .expect("Should have the context menu deployed");
14390 match context_menu {
14391 CodeContextMenu::Completions(completions_menu) => {
14392 let completions = completions_menu.completions.borrow_mut();
14393 assert_eq!(
14394 completions
14395 .iter()
14396 .map(|completion| &completion.label.text)
14397 .collect::<Vec<_>>(),
14398 vec!["id", "other"]
14399 )
14400 }
14401 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14402 }
14403 });
14404 cx.run_until_parked();
14405
14406 cx.update_editor(|editor, window, cx| {
14407 editor.context_menu_next(&ContextMenuNext, window, cx);
14408 });
14409 cx.run_until_parked();
14410 cx.update_editor(|editor, window, cx| {
14411 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14412 });
14413 cx.run_until_parked();
14414 cx.update_editor(|editor, window, cx| {
14415 editor.context_menu_next(&ContextMenuNext, window, cx);
14416 });
14417 cx.run_until_parked();
14418 cx.update_editor(|editor, window, cx| {
14419 editor
14420 .compose_completion(&ComposeCompletion::default(), window, cx)
14421 .expect("No task returned")
14422 })
14423 .await
14424 .expect("Completion failed");
14425 cx.run_until_parked();
14426
14427 cx.update_editor(|editor, _, cx| {
14428 assert_eq!(
14429 resolve_requests_1.load(atomic::Ordering::Acquire),
14430 1,
14431 "Should always resolve once despite multiple selections"
14432 );
14433 assert_eq!(
14434 resolve_requests_2.load(atomic::Ordering::Acquire),
14435 1,
14436 "Should always resolve once after multiple selections and applying the completion"
14437 );
14438 assert_eq!(
14439 editor.text(cx),
14440 "fn main() { let a = ??.other; }",
14441 "Should use resolved data when applying the completion"
14442 );
14443 });
14444}
14445
14446#[gpui::test]
14447async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14448 init_test(cx, |_| {});
14449
14450 let item_0 = lsp::CompletionItem {
14451 label: "abs".into(),
14452 insert_text: Some("abs".into()),
14453 data: Some(json!({ "very": "special"})),
14454 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14455 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14456 lsp::InsertReplaceEdit {
14457 new_text: "abs".to_string(),
14458 insert: lsp::Range::default(),
14459 replace: lsp::Range::default(),
14460 },
14461 )),
14462 ..lsp::CompletionItem::default()
14463 };
14464 let items = iter::once(item_0.clone())
14465 .chain((11..51).map(|i| lsp::CompletionItem {
14466 label: format!("item_{}", i),
14467 insert_text: Some(format!("item_{}", i)),
14468 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14469 ..lsp::CompletionItem::default()
14470 }))
14471 .collect::<Vec<_>>();
14472
14473 let default_commit_characters = vec!["?".to_string()];
14474 let default_data = json!({ "default": "data"});
14475 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14476 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14477 let default_edit_range = lsp::Range {
14478 start: lsp::Position {
14479 line: 0,
14480 character: 5,
14481 },
14482 end: lsp::Position {
14483 line: 0,
14484 character: 5,
14485 },
14486 };
14487
14488 let mut cx = EditorLspTestContext::new_rust(
14489 lsp::ServerCapabilities {
14490 completion_provider: Some(lsp::CompletionOptions {
14491 trigger_characters: Some(vec![".".to_string()]),
14492 resolve_provider: Some(true),
14493 ..Default::default()
14494 }),
14495 ..Default::default()
14496 },
14497 cx,
14498 )
14499 .await;
14500
14501 cx.set_state("fn main() { let a = 2ˇ; }");
14502 cx.simulate_keystroke(".");
14503
14504 let completion_data = default_data.clone();
14505 let completion_characters = default_commit_characters.clone();
14506 let completion_items = items.clone();
14507 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14508 let default_data = completion_data.clone();
14509 let default_commit_characters = completion_characters.clone();
14510 let items = completion_items.clone();
14511 async move {
14512 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14513 items,
14514 item_defaults: Some(lsp::CompletionListItemDefaults {
14515 data: Some(default_data.clone()),
14516 commit_characters: Some(default_commit_characters.clone()),
14517 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14518 default_edit_range,
14519 )),
14520 insert_text_format: Some(default_insert_text_format),
14521 insert_text_mode: Some(default_insert_text_mode),
14522 }),
14523 ..lsp::CompletionList::default()
14524 })))
14525 }
14526 })
14527 .next()
14528 .await;
14529
14530 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14531 cx.lsp
14532 .server
14533 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14534 let closure_resolved_items = resolved_items.clone();
14535 move |item_to_resolve, _| {
14536 let closure_resolved_items = closure_resolved_items.clone();
14537 async move {
14538 closure_resolved_items.lock().push(item_to_resolve.clone());
14539 Ok(item_to_resolve)
14540 }
14541 }
14542 })
14543 .detach();
14544
14545 cx.condition(|editor, _| editor.context_menu_visible())
14546 .await;
14547 cx.run_until_parked();
14548 cx.update_editor(|editor, _, _| {
14549 let menu = editor.context_menu.borrow_mut();
14550 match menu.as_ref().expect("should have the completions menu") {
14551 CodeContextMenu::Completions(completions_menu) => {
14552 assert_eq!(
14553 completions_menu
14554 .entries
14555 .borrow()
14556 .iter()
14557 .map(|mat| mat.string.clone())
14558 .collect::<Vec<String>>(),
14559 items
14560 .iter()
14561 .map(|completion| completion.label.clone())
14562 .collect::<Vec<String>>()
14563 );
14564 }
14565 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14566 }
14567 });
14568 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14569 // with 4 from the end.
14570 assert_eq!(
14571 *resolved_items.lock(),
14572 [&items[0..16], &items[items.len() - 4..items.len()]]
14573 .concat()
14574 .iter()
14575 .cloned()
14576 .map(|mut item| {
14577 if item.data.is_none() {
14578 item.data = Some(default_data.clone());
14579 }
14580 item
14581 })
14582 .collect::<Vec<lsp::CompletionItem>>(),
14583 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14584 );
14585 resolved_items.lock().clear();
14586
14587 cx.update_editor(|editor, window, cx| {
14588 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14589 });
14590 cx.run_until_parked();
14591 // Completions that have already been resolved are skipped.
14592 assert_eq!(
14593 *resolved_items.lock(),
14594 items[items.len() - 16..items.len() - 4]
14595 .iter()
14596 .cloned()
14597 .map(|mut item| {
14598 if item.data.is_none() {
14599 item.data = Some(default_data.clone());
14600 }
14601 item
14602 })
14603 .collect::<Vec<lsp::CompletionItem>>()
14604 );
14605 resolved_items.lock().clear();
14606}
14607
14608#[gpui::test]
14609async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14610 init_test(cx, |_| {});
14611
14612 let mut cx = EditorLspTestContext::new(
14613 Language::new(
14614 LanguageConfig {
14615 matcher: LanguageMatcher {
14616 path_suffixes: vec!["jsx".into()],
14617 ..Default::default()
14618 },
14619 overrides: [(
14620 "element".into(),
14621 LanguageConfigOverride {
14622 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14623 ..Default::default()
14624 },
14625 )]
14626 .into_iter()
14627 .collect(),
14628 ..Default::default()
14629 },
14630 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14631 )
14632 .with_override_query("(jsx_self_closing_element) @element")
14633 .unwrap(),
14634 lsp::ServerCapabilities {
14635 completion_provider: Some(lsp::CompletionOptions {
14636 trigger_characters: Some(vec![":".to_string()]),
14637 ..Default::default()
14638 }),
14639 ..Default::default()
14640 },
14641 cx,
14642 )
14643 .await;
14644
14645 cx.lsp
14646 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14647 Ok(Some(lsp::CompletionResponse::Array(vec![
14648 lsp::CompletionItem {
14649 label: "bg-blue".into(),
14650 ..Default::default()
14651 },
14652 lsp::CompletionItem {
14653 label: "bg-red".into(),
14654 ..Default::default()
14655 },
14656 lsp::CompletionItem {
14657 label: "bg-yellow".into(),
14658 ..Default::default()
14659 },
14660 ])))
14661 });
14662
14663 cx.set_state(r#"<p class="bgˇ" />"#);
14664
14665 // Trigger completion when typing a dash, because the dash is an extra
14666 // word character in the 'element' scope, which contains the cursor.
14667 cx.simulate_keystroke("-");
14668 cx.executor().run_until_parked();
14669 cx.update_editor(|editor, _, _| {
14670 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14671 {
14672 assert_eq!(
14673 completion_menu_entries(&menu),
14674 &["bg-red", "bg-blue", "bg-yellow"]
14675 );
14676 } else {
14677 panic!("expected completion menu to be open");
14678 }
14679 });
14680
14681 cx.simulate_keystroke("l");
14682 cx.executor().run_until_parked();
14683 cx.update_editor(|editor, _, _| {
14684 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14685 {
14686 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14687 } else {
14688 panic!("expected completion menu to be open");
14689 }
14690 });
14691
14692 // When filtering completions, consider the character after the '-' to
14693 // be the start of a subword.
14694 cx.set_state(r#"<p class="yelˇ" />"#);
14695 cx.simulate_keystroke("l");
14696 cx.executor().run_until_parked();
14697 cx.update_editor(|editor, _, _| {
14698 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14699 {
14700 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14701 } else {
14702 panic!("expected completion menu to be open");
14703 }
14704 });
14705}
14706
14707fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14708 let entries = menu.entries.borrow();
14709 entries.iter().map(|mat| mat.string.clone()).collect()
14710}
14711
14712#[gpui::test]
14713async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14714 init_test(cx, |settings| {
14715 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14716 FormatterList(vec![Formatter::Prettier].into()),
14717 ))
14718 });
14719
14720 let fs = FakeFs::new(cx.executor());
14721 fs.insert_file(path!("/file.ts"), Default::default()).await;
14722
14723 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14724 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14725
14726 language_registry.add(Arc::new(Language::new(
14727 LanguageConfig {
14728 name: "TypeScript".into(),
14729 matcher: LanguageMatcher {
14730 path_suffixes: vec!["ts".to_string()],
14731 ..Default::default()
14732 },
14733 ..Default::default()
14734 },
14735 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14736 )));
14737 update_test_language_settings(cx, |settings| {
14738 settings.defaults.prettier = Some(PrettierSettings {
14739 allowed: true,
14740 ..PrettierSettings::default()
14741 });
14742 });
14743
14744 let test_plugin = "test_plugin";
14745 let _ = language_registry.register_fake_lsp(
14746 "TypeScript",
14747 FakeLspAdapter {
14748 prettier_plugins: vec![test_plugin],
14749 ..Default::default()
14750 },
14751 );
14752
14753 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14754 let buffer = project
14755 .update(cx, |project, cx| {
14756 project.open_local_buffer(path!("/file.ts"), cx)
14757 })
14758 .await
14759 .unwrap();
14760
14761 let buffer_text = "one\ntwo\nthree\n";
14762 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14763 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14764 editor.update_in(cx, |editor, window, cx| {
14765 editor.set_text(buffer_text, window, cx)
14766 });
14767
14768 editor
14769 .update_in(cx, |editor, window, cx| {
14770 editor.perform_format(
14771 project.clone(),
14772 FormatTrigger::Manual,
14773 FormatTarget::Buffers,
14774 window,
14775 cx,
14776 )
14777 })
14778 .unwrap()
14779 .await;
14780 assert_eq!(
14781 editor.update(cx, |editor, cx| editor.text(cx)),
14782 buffer_text.to_string() + prettier_format_suffix,
14783 "Test prettier formatting was not applied to the original buffer text",
14784 );
14785
14786 update_test_language_settings(cx, |settings| {
14787 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14788 });
14789 let format = editor.update_in(cx, |editor, window, cx| {
14790 editor.perform_format(
14791 project.clone(),
14792 FormatTrigger::Manual,
14793 FormatTarget::Buffers,
14794 window,
14795 cx,
14796 )
14797 });
14798 format.await.unwrap();
14799 assert_eq!(
14800 editor.update(cx, |editor, cx| editor.text(cx)),
14801 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14802 "Autoformatting (via test prettier) was not applied to the original buffer text",
14803 );
14804}
14805
14806#[gpui::test]
14807async fn test_addition_reverts(cx: &mut TestAppContext) {
14808 init_test(cx, |_| {});
14809 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14810 let base_text = indoc! {r#"
14811 struct Row;
14812 struct Row1;
14813 struct Row2;
14814
14815 struct Row4;
14816 struct Row5;
14817 struct Row6;
14818
14819 struct Row8;
14820 struct Row9;
14821 struct Row10;"#};
14822
14823 // When addition hunks are not adjacent to carets, no hunk revert is performed
14824 assert_hunk_revert(
14825 indoc! {r#"struct Row;
14826 struct Row1;
14827 struct Row1.1;
14828 struct Row1.2;
14829 struct Row2;ˇ
14830
14831 struct Row4;
14832 struct Row5;
14833 struct Row6;
14834
14835 struct Row8;
14836 ˇstruct Row9;
14837 struct Row9.1;
14838 struct Row9.2;
14839 struct Row9.3;
14840 struct Row10;"#},
14841 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14842 indoc! {r#"struct Row;
14843 struct Row1;
14844 struct Row1.1;
14845 struct Row1.2;
14846 struct Row2;ˇ
14847
14848 struct Row4;
14849 struct Row5;
14850 struct Row6;
14851
14852 struct Row8;
14853 ˇstruct Row9;
14854 struct Row9.1;
14855 struct Row9.2;
14856 struct Row9.3;
14857 struct Row10;"#},
14858 base_text,
14859 &mut cx,
14860 );
14861 // Same for selections
14862 assert_hunk_revert(
14863 indoc! {r#"struct Row;
14864 struct Row1;
14865 struct Row2;
14866 struct Row2.1;
14867 struct Row2.2;
14868 «ˇ
14869 struct Row4;
14870 struct» Row5;
14871 «struct Row6;
14872 ˇ»
14873 struct Row9.1;
14874 struct Row9.2;
14875 struct Row9.3;
14876 struct Row8;
14877 struct Row9;
14878 struct Row10;"#},
14879 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14880 indoc! {r#"struct Row;
14881 struct Row1;
14882 struct Row2;
14883 struct Row2.1;
14884 struct Row2.2;
14885 «ˇ
14886 struct Row4;
14887 struct» Row5;
14888 «struct Row6;
14889 ˇ»
14890 struct Row9.1;
14891 struct Row9.2;
14892 struct Row9.3;
14893 struct Row8;
14894 struct Row9;
14895 struct Row10;"#},
14896 base_text,
14897 &mut cx,
14898 );
14899
14900 // When carets and selections intersect the addition hunks, those are reverted.
14901 // Adjacent carets got merged.
14902 assert_hunk_revert(
14903 indoc! {r#"struct Row;
14904 ˇ// something on the top
14905 struct Row1;
14906 struct Row2;
14907 struct Roˇw3.1;
14908 struct Row2.2;
14909 struct Row2.3;ˇ
14910
14911 struct Row4;
14912 struct ˇRow5.1;
14913 struct Row5.2;
14914 struct «Rowˇ»5.3;
14915 struct Row5;
14916 struct Row6;
14917 ˇ
14918 struct Row9.1;
14919 struct «Rowˇ»9.2;
14920 struct «ˇRow»9.3;
14921 struct Row8;
14922 struct Row9;
14923 «ˇ// something on bottom»
14924 struct Row10;"#},
14925 vec![
14926 DiffHunkStatusKind::Added,
14927 DiffHunkStatusKind::Added,
14928 DiffHunkStatusKind::Added,
14929 DiffHunkStatusKind::Added,
14930 DiffHunkStatusKind::Added,
14931 ],
14932 indoc! {r#"struct Row;
14933 ˇstruct Row1;
14934 struct Row2;
14935 ˇ
14936 struct Row4;
14937 ˇstruct Row5;
14938 struct Row6;
14939 ˇ
14940 ˇstruct Row8;
14941 struct Row9;
14942 ˇstruct Row10;"#},
14943 base_text,
14944 &mut cx,
14945 );
14946}
14947
14948#[gpui::test]
14949async fn test_modification_reverts(cx: &mut TestAppContext) {
14950 init_test(cx, |_| {});
14951 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14952 let base_text = indoc! {r#"
14953 struct Row;
14954 struct Row1;
14955 struct Row2;
14956
14957 struct Row4;
14958 struct Row5;
14959 struct Row6;
14960
14961 struct Row8;
14962 struct Row9;
14963 struct Row10;"#};
14964
14965 // Modification hunks behave the same as the addition ones.
14966 assert_hunk_revert(
14967 indoc! {r#"struct Row;
14968 struct Row1;
14969 struct Row33;
14970 ˇ
14971 struct Row4;
14972 struct Row5;
14973 struct Row6;
14974 ˇ
14975 struct Row99;
14976 struct Row9;
14977 struct Row10;"#},
14978 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14979 indoc! {r#"struct Row;
14980 struct Row1;
14981 struct Row33;
14982 ˇ
14983 struct Row4;
14984 struct Row5;
14985 struct Row6;
14986 ˇ
14987 struct Row99;
14988 struct Row9;
14989 struct Row10;"#},
14990 base_text,
14991 &mut cx,
14992 );
14993 assert_hunk_revert(
14994 indoc! {r#"struct Row;
14995 struct Row1;
14996 struct Row33;
14997 «ˇ
14998 struct Row4;
14999 struct» Row5;
15000 «struct Row6;
15001 ˇ»
15002 struct Row99;
15003 struct Row9;
15004 struct Row10;"#},
15005 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15006 indoc! {r#"struct Row;
15007 struct Row1;
15008 struct Row33;
15009 «ˇ
15010 struct Row4;
15011 struct» Row5;
15012 «struct Row6;
15013 ˇ»
15014 struct Row99;
15015 struct Row9;
15016 struct Row10;"#},
15017 base_text,
15018 &mut cx,
15019 );
15020
15021 assert_hunk_revert(
15022 indoc! {r#"ˇstruct Row1.1;
15023 struct Row1;
15024 «ˇstr»uct Row22;
15025
15026 struct ˇRow44;
15027 struct Row5;
15028 struct «Rˇ»ow66;ˇ
15029
15030 «struˇ»ct Row88;
15031 struct Row9;
15032 struct Row1011;ˇ"#},
15033 vec![
15034 DiffHunkStatusKind::Modified,
15035 DiffHunkStatusKind::Modified,
15036 DiffHunkStatusKind::Modified,
15037 DiffHunkStatusKind::Modified,
15038 DiffHunkStatusKind::Modified,
15039 DiffHunkStatusKind::Modified,
15040 ],
15041 indoc! {r#"struct Row;
15042 ˇstruct Row1;
15043 struct Row2;
15044 ˇ
15045 struct Row4;
15046 ˇstruct Row5;
15047 struct Row6;
15048 ˇ
15049 struct Row8;
15050 ˇstruct Row9;
15051 struct Row10;ˇ"#},
15052 base_text,
15053 &mut cx,
15054 );
15055}
15056
15057#[gpui::test]
15058async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15059 init_test(cx, |_| {});
15060 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15061 let base_text = indoc! {r#"
15062 one
15063
15064 two
15065 three
15066 "#};
15067
15068 cx.set_head_text(base_text);
15069 cx.set_state("\nˇ\n");
15070 cx.executor().run_until_parked();
15071 cx.update_editor(|editor, _window, cx| {
15072 editor.expand_selected_diff_hunks(cx);
15073 });
15074 cx.executor().run_until_parked();
15075 cx.update_editor(|editor, window, cx| {
15076 editor.backspace(&Default::default(), window, cx);
15077 });
15078 cx.run_until_parked();
15079 cx.assert_state_with_diff(
15080 indoc! {r#"
15081
15082 - two
15083 - threeˇ
15084 +
15085 "#}
15086 .to_string(),
15087 );
15088}
15089
15090#[gpui::test]
15091async fn test_deletion_reverts(cx: &mut TestAppContext) {
15092 init_test(cx, |_| {});
15093 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15094 let base_text = indoc! {r#"struct Row;
15095struct Row1;
15096struct Row2;
15097
15098struct Row4;
15099struct Row5;
15100struct Row6;
15101
15102struct Row8;
15103struct Row9;
15104struct Row10;"#};
15105
15106 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15107 assert_hunk_revert(
15108 indoc! {r#"struct Row;
15109 struct Row2;
15110
15111 ˇstruct Row4;
15112 struct Row5;
15113 struct Row6;
15114 ˇ
15115 struct Row8;
15116 struct Row10;"#},
15117 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15118 indoc! {r#"struct Row;
15119 struct Row2;
15120
15121 ˇstruct Row4;
15122 struct Row5;
15123 struct Row6;
15124 ˇ
15125 struct Row8;
15126 struct Row10;"#},
15127 base_text,
15128 &mut cx,
15129 );
15130 assert_hunk_revert(
15131 indoc! {r#"struct Row;
15132 struct Row2;
15133
15134 «ˇstruct Row4;
15135 struct» Row5;
15136 «struct Row6;
15137 ˇ»
15138 struct Row8;
15139 struct Row10;"#},
15140 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15141 indoc! {r#"struct Row;
15142 struct Row2;
15143
15144 «ˇstruct Row4;
15145 struct» Row5;
15146 «struct Row6;
15147 ˇ»
15148 struct Row8;
15149 struct Row10;"#},
15150 base_text,
15151 &mut cx,
15152 );
15153
15154 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15155 assert_hunk_revert(
15156 indoc! {r#"struct Row;
15157 ˇstruct Row2;
15158
15159 struct Row4;
15160 struct Row5;
15161 struct Row6;
15162
15163 struct Row8;ˇ
15164 struct Row10;"#},
15165 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15166 indoc! {r#"struct Row;
15167 struct Row1;
15168 ˇstruct Row2;
15169
15170 struct Row4;
15171 struct Row5;
15172 struct Row6;
15173
15174 struct Row8;ˇ
15175 struct Row9;
15176 struct Row10;"#},
15177 base_text,
15178 &mut cx,
15179 );
15180 assert_hunk_revert(
15181 indoc! {r#"struct Row;
15182 struct Row2«ˇ;
15183 struct Row4;
15184 struct» Row5;
15185 «struct Row6;
15186
15187 struct Row8;ˇ»
15188 struct Row10;"#},
15189 vec![
15190 DiffHunkStatusKind::Deleted,
15191 DiffHunkStatusKind::Deleted,
15192 DiffHunkStatusKind::Deleted,
15193 ],
15194 indoc! {r#"struct Row;
15195 struct Row1;
15196 struct Row2«ˇ;
15197
15198 struct Row4;
15199 struct» Row5;
15200 «struct Row6;
15201
15202 struct Row8;ˇ»
15203 struct Row9;
15204 struct Row10;"#},
15205 base_text,
15206 &mut cx,
15207 );
15208}
15209
15210#[gpui::test]
15211async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15212 init_test(cx, |_| {});
15213
15214 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15215 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15216 let base_text_3 =
15217 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15218
15219 let text_1 = edit_first_char_of_every_line(base_text_1);
15220 let text_2 = edit_first_char_of_every_line(base_text_2);
15221 let text_3 = edit_first_char_of_every_line(base_text_3);
15222
15223 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15224 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15225 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15226
15227 let multibuffer = cx.new(|cx| {
15228 let mut multibuffer = MultiBuffer::new(ReadWrite);
15229 multibuffer.push_excerpts(
15230 buffer_1.clone(),
15231 [
15232 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15233 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15234 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15235 ],
15236 cx,
15237 );
15238 multibuffer.push_excerpts(
15239 buffer_2.clone(),
15240 [
15241 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15242 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15243 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15244 ],
15245 cx,
15246 );
15247 multibuffer.push_excerpts(
15248 buffer_3.clone(),
15249 [
15250 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15251 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15252 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15253 ],
15254 cx,
15255 );
15256 multibuffer
15257 });
15258
15259 let fs = FakeFs::new(cx.executor());
15260 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15261 let (editor, cx) = cx
15262 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15263 editor.update_in(cx, |editor, _window, cx| {
15264 for (buffer, diff_base) in [
15265 (buffer_1.clone(), base_text_1),
15266 (buffer_2.clone(), base_text_2),
15267 (buffer_3.clone(), base_text_3),
15268 ] {
15269 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15270 editor
15271 .buffer
15272 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15273 }
15274 });
15275 cx.executor().run_until_parked();
15276
15277 editor.update_in(cx, |editor, window, cx| {
15278 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}");
15279 editor.select_all(&SelectAll, window, cx);
15280 editor.git_restore(&Default::default(), window, cx);
15281 });
15282 cx.executor().run_until_parked();
15283
15284 // When all ranges are selected, all buffer hunks are reverted.
15285 editor.update(cx, |editor, cx| {
15286 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");
15287 });
15288 buffer_1.update(cx, |buffer, _| {
15289 assert_eq!(buffer.text(), base_text_1);
15290 });
15291 buffer_2.update(cx, |buffer, _| {
15292 assert_eq!(buffer.text(), base_text_2);
15293 });
15294 buffer_3.update(cx, |buffer, _| {
15295 assert_eq!(buffer.text(), base_text_3);
15296 });
15297
15298 editor.update_in(cx, |editor, window, cx| {
15299 editor.undo(&Default::default(), window, cx);
15300 });
15301
15302 editor.update_in(cx, |editor, window, cx| {
15303 editor.change_selections(None, window, cx, |s| {
15304 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15305 });
15306 editor.git_restore(&Default::default(), window, cx);
15307 });
15308
15309 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15310 // but not affect buffer_2 and its related excerpts.
15311 editor.update(cx, |editor, cx| {
15312 assert_eq!(
15313 editor.text(cx),
15314 "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}"
15315 );
15316 });
15317 buffer_1.update(cx, |buffer, _| {
15318 assert_eq!(buffer.text(), base_text_1);
15319 });
15320 buffer_2.update(cx, |buffer, _| {
15321 assert_eq!(
15322 buffer.text(),
15323 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15324 );
15325 });
15326 buffer_3.update(cx, |buffer, _| {
15327 assert_eq!(
15328 buffer.text(),
15329 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15330 );
15331 });
15332
15333 fn edit_first_char_of_every_line(text: &str) -> String {
15334 text.split('\n')
15335 .map(|line| format!("X{}", &line[1..]))
15336 .collect::<Vec<_>>()
15337 .join("\n")
15338 }
15339}
15340
15341#[gpui::test]
15342async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15343 init_test(cx, |_| {});
15344
15345 let cols = 4;
15346 let rows = 10;
15347 let sample_text_1 = sample_text(rows, cols, 'a');
15348 assert_eq!(
15349 sample_text_1,
15350 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15351 );
15352 let sample_text_2 = sample_text(rows, cols, 'l');
15353 assert_eq!(
15354 sample_text_2,
15355 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15356 );
15357 let sample_text_3 = sample_text(rows, cols, 'v');
15358 assert_eq!(
15359 sample_text_3,
15360 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15361 );
15362
15363 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15364 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15365 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15366
15367 let multi_buffer = cx.new(|cx| {
15368 let mut multibuffer = MultiBuffer::new(ReadWrite);
15369 multibuffer.push_excerpts(
15370 buffer_1.clone(),
15371 [
15372 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15373 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15374 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15375 ],
15376 cx,
15377 );
15378 multibuffer.push_excerpts(
15379 buffer_2.clone(),
15380 [
15381 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15382 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15383 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15384 ],
15385 cx,
15386 );
15387 multibuffer.push_excerpts(
15388 buffer_3.clone(),
15389 [
15390 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15391 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15392 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15393 ],
15394 cx,
15395 );
15396 multibuffer
15397 });
15398
15399 let fs = FakeFs::new(cx.executor());
15400 fs.insert_tree(
15401 "/a",
15402 json!({
15403 "main.rs": sample_text_1,
15404 "other.rs": sample_text_2,
15405 "lib.rs": sample_text_3,
15406 }),
15407 )
15408 .await;
15409 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15410 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15411 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15412 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15413 Editor::new(
15414 EditorMode::full(),
15415 multi_buffer,
15416 Some(project.clone()),
15417 window,
15418 cx,
15419 )
15420 });
15421 let multibuffer_item_id = workspace
15422 .update(cx, |workspace, window, cx| {
15423 assert!(
15424 workspace.active_item(cx).is_none(),
15425 "active item should be None before the first item is added"
15426 );
15427 workspace.add_item_to_active_pane(
15428 Box::new(multi_buffer_editor.clone()),
15429 None,
15430 true,
15431 window,
15432 cx,
15433 );
15434 let active_item = workspace
15435 .active_item(cx)
15436 .expect("should have an active item after adding the multi buffer");
15437 assert!(
15438 !active_item.is_singleton(cx),
15439 "A multi buffer was expected to active after adding"
15440 );
15441 active_item.item_id()
15442 })
15443 .unwrap();
15444 cx.executor().run_until_parked();
15445
15446 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15447 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15448 s.select_ranges(Some(1..2))
15449 });
15450 editor.open_excerpts(&OpenExcerpts, window, cx);
15451 });
15452 cx.executor().run_until_parked();
15453 let first_item_id = workspace
15454 .update(cx, |workspace, window, cx| {
15455 let active_item = workspace
15456 .active_item(cx)
15457 .expect("should have an active item after navigating into the 1st buffer");
15458 let first_item_id = active_item.item_id();
15459 assert_ne!(
15460 first_item_id, multibuffer_item_id,
15461 "Should navigate into the 1st buffer and activate it"
15462 );
15463 assert!(
15464 active_item.is_singleton(cx),
15465 "New active item should be a singleton buffer"
15466 );
15467 assert_eq!(
15468 active_item
15469 .act_as::<Editor>(cx)
15470 .expect("should have navigated into an editor for the 1st buffer")
15471 .read(cx)
15472 .text(cx),
15473 sample_text_1
15474 );
15475
15476 workspace
15477 .go_back(workspace.active_pane().downgrade(), window, cx)
15478 .detach_and_log_err(cx);
15479
15480 first_item_id
15481 })
15482 .unwrap();
15483 cx.executor().run_until_parked();
15484 workspace
15485 .update(cx, |workspace, _, cx| {
15486 let active_item = workspace
15487 .active_item(cx)
15488 .expect("should have an active item after navigating back");
15489 assert_eq!(
15490 active_item.item_id(),
15491 multibuffer_item_id,
15492 "Should navigate back to the multi buffer"
15493 );
15494 assert!(!active_item.is_singleton(cx));
15495 })
15496 .unwrap();
15497
15498 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15499 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15500 s.select_ranges(Some(39..40))
15501 });
15502 editor.open_excerpts(&OpenExcerpts, window, cx);
15503 });
15504 cx.executor().run_until_parked();
15505 let second_item_id = workspace
15506 .update(cx, |workspace, window, cx| {
15507 let active_item = workspace
15508 .active_item(cx)
15509 .expect("should have an active item after navigating into the 2nd buffer");
15510 let second_item_id = active_item.item_id();
15511 assert_ne!(
15512 second_item_id, multibuffer_item_id,
15513 "Should navigate away from the multibuffer"
15514 );
15515 assert_ne!(
15516 second_item_id, first_item_id,
15517 "Should navigate into the 2nd buffer and activate it"
15518 );
15519 assert!(
15520 active_item.is_singleton(cx),
15521 "New active item should be a singleton buffer"
15522 );
15523 assert_eq!(
15524 active_item
15525 .act_as::<Editor>(cx)
15526 .expect("should have navigated into an editor")
15527 .read(cx)
15528 .text(cx),
15529 sample_text_2
15530 );
15531
15532 workspace
15533 .go_back(workspace.active_pane().downgrade(), window, cx)
15534 .detach_and_log_err(cx);
15535
15536 second_item_id
15537 })
15538 .unwrap();
15539 cx.executor().run_until_parked();
15540 workspace
15541 .update(cx, |workspace, _, cx| {
15542 let active_item = workspace
15543 .active_item(cx)
15544 .expect("should have an active item after navigating back from the 2nd buffer");
15545 assert_eq!(
15546 active_item.item_id(),
15547 multibuffer_item_id,
15548 "Should navigate back from the 2nd buffer to the multi buffer"
15549 );
15550 assert!(!active_item.is_singleton(cx));
15551 })
15552 .unwrap();
15553
15554 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15555 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15556 s.select_ranges(Some(70..70))
15557 });
15558 editor.open_excerpts(&OpenExcerpts, window, cx);
15559 });
15560 cx.executor().run_until_parked();
15561 workspace
15562 .update(cx, |workspace, window, cx| {
15563 let active_item = workspace
15564 .active_item(cx)
15565 .expect("should have an active item after navigating into the 3rd buffer");
15566 let third_item_id = active_item.item_id();
15567 assert_ne!(
15568 third_item_id, multibuffer_item_id,
15569 "Should navigate into the 3rd buffer and activate it"
15570 );
15571 assert_ne!(third_item_id, first_item_id);
15572 assert_ne!(third_item_id, second_item_id);
15573 assert!(
15574 active_item.is_singleton(cx),
15575 "New active item should be a singleton buffer"
15576 );
15577 assert_eq!(
15578 active_item
15579 .act_as::<Editor>(cx)
15580 .expect("should have navigated into an editor")
15581 .read(cx)
15582 .text(cx),
15583 sample_text_3
15584 );
15585
15586 workspace
15587 .go_back(workspace.active_pane().downgrade(), window, cx)
15588 .detach_and_log_err(cx);
15589 })
15590 .unwrap();
15591 cx.executor().run_until_parked();
15592 workspace
15593 .update(cx, |workspace, _, cx| {
15594 let active_item = workspace
15595 .active_item(cx)
15596 .expect("should have an active item after navigating back from the 3rd buffer");
15597 assert_eq!(
15598 active_item.item_id(),
15599 multibuffer_item_id,
15600 "Should navigate back from the 3rd buffer to the multi buffer"
15601 );
15602 assert!(!active_item.is_singleton(cx));
15603 })
15604 .unwrap();
15605}
15606
15607#[gpui::test]
15608async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15609 init_test(cx, |_| {});
15610
15611 let mut cx = EditorTestContext::new(cx).await;
15612
15613 let diff_base = r#"
15614 use some::mod;
15615
15616 const A: u32 = 42;
15617
15618 fn main() {
15619 println!("hello");
15620
15621 println!("world");
15622 }
15623 "#
15624 .unindent();
15625
15626 cx.set_state(
15627 &r#"
15628 use some::modified;
15629
15630 ˇ
15631 fn main() {
15632 println!("hello there");
15633
15634 println!("around the");
15635 println!("world");
15636 }
15637 "#
15638 .unindent(),
15639 );
15640
15641 cx.set_head_text(&diff_base);
15642 executor.run_until_parked();
15643
15644 cx.update_editor(|editor, window, cx| {
15645 editor.go_to_next_hunk(&GoToHunk, window, cx);
15646 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15647 });
15648 executor.run_until_parked();
15649 cx.assert_state_with_diff(
15650 r#"
15651 use some::modified;
15652
15653
15654 fn main() {
15655 - println!("hello");
15656 + ˇ println!("hello there");
15657
15658 println!("around the");
15659 println!("world");
15660 }
15661 "#
15662 .unindent(),
15663 );
15664
15665 cx.update_editor(|editor, window, cx| {
15666 for _ in 0..2 {
15667 editor.go_to_next_hunk(&GoToHunk, window, cx);
15668 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15669 }
15670 });
15671 executor.run_until_parked();
15672 cx.assert_state_with_diff(
15673 r#"
15674 - use some::mod;
15675 + ˇuse some::modified;
15676
15677
15678 fn main() {
15679 - println!("hello");
15680 + println!("hello there");
15681
15682 + println!("around the");
15683 println!("world");
15684 }
15685 "#
15686 .unindent(),
15687 );
15688
15689 cx.update_editor(|editor, window, cx| {
15690 editor.go_to_next_hunk(&GoToHunk, window, cx);
15691 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15692 });
15693 executor.run_until_parked();
15694 cx.assert_state_with_diff(
15695 r#"
15696 - use some::mod;
15697 + use some::modified;
15698
15699 - const A: u32 = 42;
15700 ˇ
15701 fn main() {
15702 - println!("hello");
15703 + println!("hello there");
15704
15705 + println!("around the");
15706 println!("world");
15707 }
15708 "#
15709 .unindent(),
15710 );
15711
15712 cx.update_editor(|editor, window, cx| {
15713 editor.cancel(&Cancel, window, cx);
15714 });
15715
15716 cx.assert_state_with_diff(
15717 r#"
15718 use some::modified;
15719
15720 ˇ
15721 fn main() {
15722 println!("hello there");
15723
15724 println!("around the");
15725 println!("world");
15726 }
15727 "#
15728 .unindent(),
15729 );
15730}
15731
15732#[gpui::test]
15733async fn test_diff_base_change_with_expanded_diff_hunks(
15734 executor: BackgroundExecutor,
15735 cx: &mut TestAppContext,
15736) {
15737 init_test(cx, |_| {});
15738
15739 let mut cx = EditorTestContext::new(cx).await;
15740
15741 let diff_base = r#"
15742 use some::mod1;
15743 use some::mod2;
15744
15745 const A: u32 = 42;
15746 const B: u32 = 42;
15747 const C: u32 = 42;
15748
15749 fn main() {
15750 println!("hello");
15751
15752 println!("world");
15753 }
15754 "#
15755 .unindent();
15756
15757 cx.set_state(
15758 &r#"
15759 use some::mod2;
15760
15761 const A: u32 = 42;
15762 const C: u32 = 42;
15763
15764 fn main(ˇ) {
15765 //println!("hello");
15766
15767 println!("world");
15768 //
15769 //
15770 }
15771 "#
15772 .unindent(),
15773 );
15774
15775 cx.set_head_text(&diff_base);
15776 executor.run_until_parked();
15777
15778 cx.update_editor(|editor, window, cx| {
15779 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15780 });
15781 executor.run_until_parked();
15782 cx.assert_state_with_diff(
15783 r#"
15784 - use some::mod1;
15785 use some::mod2;
15786
15787 const A: u32 = 42;
15788 - const B: u32 = 42;
15789 const C: u32 = 42;
15790
15791 fn main(ˇ) {
15792 - println!("hello");
15793 + //println!("hello");
15794
15795 println!("world");
15796 + //
15797 + //
15798 }
15799 "#
15800 .unindent(),
15801 );
15802
15803 cx.set_head_text("new diff base!");
15804 executor.run_until_parked();
15805 cx.assert_state_with_diff(
15806 r#"
15807 - new diff base!
15808 + use some::mod2;
15809 +
15810 + const A: u32 = 42;
15811 + const C: u32 = 42;
15812 +
15813 + fn main(ˇ) {
15814 + //println!("hello");
15815 +
15816 + println!("world");
15817 + //
15818 + //
15819 + }
15820 "#
15821 .unindent(),
15822 );
15823}
15824
15825#[gpui::test]
15826async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15827 init_test(cx, |_| {});
15828
15829 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15830 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15831 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15832 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15833 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15834 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15835
15836 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15837 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15838 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15839
15840 let multi_buffer = cx.new(|cx| {
15841 let mut multibuffer = MultiBuffer::new(ReadWrite);
15842 multibuffer.push_excerpts(
15843 buffer_1.clone(),
15844 [
15845 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15846 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15847 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15848 ],
15849 cx,
15850 );
15851 multibuffer.push_excerpts(
15852 buffer_2.clone(),
15853 [
15854 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15855 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15856 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15857 ],
15858 cx,
15859 );
15860 multibuffer.push_excerpts(
15861 buffer_3.clone(),
15862 [
15863 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15864 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15865 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15866 ],
15867 cx,
15868 );
15869 multibuffer
15870 });
15871
15872 let editor =
15873 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15874 editor
15875 .update(cx, |editor, _window, cx| {
15876 for (buffer, diff_base) in [
15877 (buffer_1.clone(), file_1_old),
15878 (buffer_2.clone(), file_2_old),
15879 (buffer_3.clone(), file_3_old),
15880 ] {
15881 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15882 editor
15883 .buffer
15884 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15885 }
15886 })
15887 .unwrap();
15888
15889 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15890 cx.run_until_parked();
15891
15892 cx.assert_editor_state(
15893 &"
15894 ˇaaa
15895 ccc
15896 ddd
15897
15898 ggg
15899 hhh
15900
15901
15902 lll
15903 mmm
15904 NNN
15905
15906 qqq
15907 rrr
15908
15909 uuu
15910 111
15911 222
15912 333
15913
15914 666
15915 777
15916
15917 000
15918 !!!"
15919 .unindent(),
15920 );
15921
15922 cx.update_editor(|editor, window, cx| {
15923 editor.select_all(&SelectAll, window, cx);
15924 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15925 });
15926 cx.executor().run_until_parked();
15927
15928 cx.assert_state_with_diff(
15929 "
15930 «aaa
15931 - bbb
15932 ccc
15933 ddd
15934
15935 ggg
15936 hhh
15937
15938
15939 lll
15940 mmm
15941 - nnn
15942 + NNN
15943
15944 qqq
15945 rrr
15946
15947 uuu
15948 111
15949 222
15950 333
15951
15952 + 666
15953 777
15954
15955 000
15956 !!!ˇ»"
15957 .unindent(),
15958 );
15959}
15960
15961#[gpui::test]
15962async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15963 init_test(cx, |_| {});
15964
15965 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15966 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15967
15968 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15969 let multi_buffer = cx.new(|cx| {
15970 let mut multibuffer = MultiBuffer::new(ReadWrite);
15971 multibuffer.push_excerpts(
15972 buffer.clone(),
15973 [
15974 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15975 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15976 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15977 ],
15978 cx,
15979 );
15980 multibuffer
15981 });
15982
15983 let editor =
15984 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15985 editor
15986 .update(cx, |editor, _window, cx| {
15987 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15988 editor
15989 .buffer
15990 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15991 })
15992 .unwrap();
15993
15994 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15995 cx.run_until_parked();
15996
15997 cx.update_editor(|editor, window, cx| {
15998 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15999 });
16000 cx.executor().run_until_parked();
16001
16002 // When the start of a hunk coincides with the start of its excerpt,
16003 // the hunk is expanded. When the start of a a hunk is earlier than
16004 // the start of its excerpt, the hunk is not expanded.
16005 cx.assert_state_with_diff(
16006 "
16007 ˇaaa
16008 - bbb
16009 + BBB
16010
16011 - ddd
16012 - eee
16013 + DDD
16014 + EEE
16015 fff
16016
16017 iii
16018 "
16019 .unindent(),
16020 );
16021}
16022
16023#[gpui::test]
16024async fn test_edits_around_expanded_insertion_hunks(
16025 executor: BackgroundExecutor,
16026 cx: &mut TestAppContext,
16027) {
16028 init_test(cx, |_| {});
16029
16030 let mut cx = EditorTestContext::new(cx).await;
16031
16032 let diff_base = r#"
16033 use some::mod1;
16034 use some::mod2;
16035
16036 const A: u32 = 42;
16037
16038 fn main() {
16039 println!("hello");
16040
16041 println!("world");
16042 }
16043 "#
16044 .unindent();
16045 executor.run_until_parked();
16046 cx.set_state(
16047 &r#"
16048 use some::mod1;
16049 use some::mod2;
16050
16051 const A: u32 = 42;
16052 const B: u32 = 42;
16053 const C: u32 = 42;
16054 ˇ
16055
16056 fn main() {
16057 println!("hello");
16058
16059 println!("world");
16060 }
16061 "#
16062 .unindent(),
16063 );
16064
16065 cx.set_head_text(&diff_base);
16066 executor.run_until_parked();
16067
16068 cx.update_editor(|editor, window, cx| {
16069 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16070 });
16071 executor.run_until_parked();
16072
16073 cx.assert_state_with_diff(
16074 r#"
16075 use some::mod1;
16076 use some::mod2;
16077
16078 const A: u32 = 42;
16079 + const B: u32 = 42;
16080 + const C: u32 = 42;
16081 + ˇ
16082
16083 fn main() {
16084 println!("hello");
16085
16086 println!("world");
16087 }
16088 "#
16089 .unindent(),
16090 );
16091
16092 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16093 executor.run_until_parked();
16094
16095 cx.assert_state_with_diff(
16096 r#"
16097 use some::mod1;
16098 use some::mod2;
16099
16100 const A: u32 = 42;
16101 + const B: u32 = 42;
16102 + const C: u32 = 42;
16103 + const D: u32 = 42;
16104 + ˇ
16105
16106 fn main() {
16107 println!("hello");
16108
16109 println!("world");
16110 }
16111 "#
16112 .unindent(),
16113 );
16114
16115 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16116 executor.run_until_parked();
16117
16118 cx.assert_state_with_diff(
16119 r#"
16120 use some::mod1;
16121 use some::mod2;
16122
16123 const A: u32 = 42;
16124 + const B: u32 = 42;
16125 + const C: u32 = 42;
16126 + const D: u32 = 42;
16127 + const E: u32 = 42;
16128 + ˇ
16129
16130 fn main() {
16131 println!("hello");
16132
16133 println!("world");
16134 }
16135 "#
16136 .unindent(),
16137 );
16138
16139 cx.update_editor(|editor, window, cx| {
16140 editor.delete_line(&DeleteLine, window, cx);
16141 });
16142 executor.run_until_parked();
16143
16144 cx.assert_state_with_diff(
16145 r#"
16146 use some::mod1;
16147 use some::mod2;
16148
16149 const A: u32 = 42;
16150 + const B: u32 = 42;
16151 + const C: u32 = 42;
16152 + const D: u32 = 42;
16153 + const E: u32 = 42;
16154 ˇ
16155 fn main() {
16156 println!("hello");
16157
16158 println!("world");
16159 }
16160 "#
16161 .unindent(),
16162 );
16163
16164 cx.update_editor(|editor, window, cx| {
16165 editor.move_up(&MoveUp, window, cx);
16166 editor.delete_line(&DeleteLine, window, cx);
16167 editor.move_up(&MoveUp, window, cx);
16168 editor.delete_line(&DeleteLine, window, cx);
16169 editor.move_up(&MoveUp, window, cx);
16170 editor.delete_line(&DeleteLine, window, cx);
16171 });
16172 executor.run_until_parked();
16173 cx.assert_state_with_diff(
16174 r#"
16175 use some::mod1;
16176 use some::mod2;
16177
16178 const A: u32 = 42;
16179 + const B: u32 = 42;
16180 ˇ
16181 fn main() {
16182 println!("hello");
16183
16184 println!("world");
16185 }
16186 "#
16187 .unindent(),
16188 );
16189
16190 cx.update_editor(|editor, window, cx| {
16191 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16192 editor.delete_line(&DeleteLine, window, cx);
16193 });
16194 executor.run_until_parked();
16195 cx.assert_state_with_diff(
16196 r#"
16197 ˇ
16198 fn main() {
16199 println!("hello");
16200
16201 println!("world");
16202 }
16203 "#
16204 .unindent(),
16205 );
16206}
16207
16208#[gpui::test]
16209async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16210 init_test(cx, |_| {});
16211
16212 let mut cx = EditorTestContext::new(cx).await;
16213 cx.set_head_text(indoc! { "
16214 one
16215 two
16216 three
16217 four
16218 five
16219 "
16220 });
16221 cx.set_state(indoc! { "
16222 one
16223 ˇthree
16224 five
16225 "});
16226 cx.run_until_parked();
16227 cx.update_editor(|editor, window, cx| {
16228 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16229 });
16230 cx.assert_state_with_diff(
16231 indoc! { "
16232 one
16233 - two
16234 ˇthree
16235 - four
16236 five
16237 "}
16238 .to_string(),
16239 );
16240 cx.update_editor(|editor, window, cx| {
16241 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16242 });
16243
16244 cx.assert_state_with_diff(
16245 indoc! { "
16246 one
16247 ˇthree
16248 five
16249 "}
16250 .to_string(),
16251 );
16252
16253 cx.set_state(indoc! { "
16254 one
16255 ˇTWO
16256 three
16257 four
16258 five
16259 "});
16260 cx.run_until_parked();
16261 cx.update_editor(|editor, window, cx| {
16262 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16263 });
16264
16265 cx.assert_state_with_diff(
16266 indoc! { "
16267 one
16268 - two
16269 + ˇTWO
16270 three
16271 four
16272 five
16273 "}
16274 .to_string(),
16275 );
16276 cx.update_editor(|editor, window, cx| {
16277 editor.move_up(&Default::default(), window, cx);
16278 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16279 });
16280 cx.assert_state_with_diff(
16281 indoc! { "
16282 one
16283 ˇTWO
16284 three
16285 four
16286 five
16287 "}
16288 .to_string(),
16289 );
16290}
16291
16292#[gpui::test]
16293async fn test_edits_around_expanded_deletion_hunks(
16294 executor: BackgroundExecutor,
16295 cx: &mut TestAppContext,
16296) {
16297 init_test(cx, |_| {});
16298
16299 let mut cx = EditorTestContext::new(cx).await;
16300
16301 let diff_base = r#"
16302 use some::mod1;
16303 use some::mod2;
16304
16305 const A: u32 = 42;
16306 const B: u32 = 42;
16307 const C: u32 = 42;
16308
16309
16310 fn main() {
16311 println!("hello");
16312
16313 println!("world");
16314 }
16315 "#
16316 .unindent();
16317 executor.run_until_parked();
16318 cx.set_state(
16319 &r#"
16320 use some::mod1;
16321 use some::mod2;
16322
16323 ˇconst B: u32 = 42;
16324 const C: u32 = 42;
16325
16326
16327 fn main() {
16328 println!("hello");
16329
16330 println!("world");
16331 }
16332 "#
16333 .unindent(),
16334 );
16335
16336 cx.set_head_text(&diff_base);
16337 executor.run_until_parked();
16338
16339 cx.update_editor(|editor, window, cx| {
16340 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16341 });
16342 executor.run_until_parked();
16343
16344 cx.assert_state_with_diff(
16345 r#"
16346 use some::mod1;
16347 use some::mod2;
16348
16349 - const A: u32 = 42;
16350 ˇconst B: u32 = 42;
16351 const C: u32 = 42;
16352
16353
16354 fn main() {
16355 println!("hello");
16356
16357 println!("world");
16358 }
16359 "#
16360 .unindent(),
16361 );
16362
16363 cx.update_editor(|editor, window, cx| {
16364 editor.delete_line(&DeleteLine, window, cx);
16365 });
16366 executor.run_until_parked();
16367 cx.assert_state_with_diff(
16368 r#"
16369 use some::mod1;
16370 use some::mod2;
16371
16372 - const A: u32 = 42;
16373 - const B: u32 = 42;
16374 ˇconst C: u32 = 42;
16375
16376
16377 fn main() {
16378 println!("hello");
16379
16380 println!("world");
16381 }
16382 "#
16383 .unindent(),
16384 );
16385
16386 cx.update_editor(|editor, window, cx| {
16387 editor.delete_line(&DeleteLine, window, cx);
16388 });
16389 executor.run_until_parked();
16390 cx.assert_state_with_diff(
16391 r#"
16392 use some::mod1;
16393 use some::mod2;
16394
16395 - const A: u32 = 42;
16396 - const B: u32 = 42;
16397 - const C: u32 = 42;
16398 ˇ
16399
16400 fn main() {
16401 println!("hello");
16402
16403 println!("world");
16404 }
16405 "#
16406 .unindent(),
16407 );
16408
16409 cx.update_editor(|editor, window, cx| {
16410 editor.handle_input("replacement", window, cx);
16411 });
16412 executor.run_until_parked();
16413 cx.assert_state_with_diff(
16414 r#"
16415 use some::mod1;
16416 use some::mod2;
16417
16418 - const A: u32 = 42;
16419 - const B: u32 = 42;
16420 - const C: u32 = 42;
16421 -
16422 + replacementˇ
16423
16424 fn main() {
16425 println!("hello");
16426
16427 println!("world");
16428 }
16429 "#
16430 .unindent(),
16431 );
16432}
16433
16434#[gpui::test]
16435async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16436 init_test(cx, |_| {});
16437
16438 let mut cx = EditorTestContext::new(cx).await;
16439
16440 let base_text = r#"
16441 one
16442 two
16443 three
16444 four
16445 five
16446 "#
16447 .unindent();
16448 executor.run_until_parked();
16449 cx.set_state(
16450 &r#"
16451 one
16452 two
16453 fˇour
16454 five
16455 "#
16456 .unindent(),
16457 );
16458
16459 cx.set_head_text(&base_text);
16460 executor.run_until_parked();
16461
16462 cx.update_editor(|editor, window, cx| {
16463 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16464 });
16465 executor.run_until_parked();
16466
16467 cx.assert_state_with_diff(
16468 r#"
16469 one
16470 two
16471 - three
16472 fˇour
16473 five
16474 "#
16475 .unindent(),
16476 );
16477
16478 cx.update_editor(|editor, window, cx| {
16479 editor.backspace(&Backspace, window, cx);
16480 editor.backspace(&Backspace, window, cx);
16481 });
16482 executor.run_until_parked();
16483 cx.assert_state_with_diff(
16484 r#"
16485 one
16486 two
16487 - threeˇ
16488 - four
16489 + our
16490 five
16491 "#
16492 .unindent(),
16493 );
16494}
16495
16496#[gpui::test]
16497async fn test_edit_after_expanded_modification_hunk(
16498 executor: BackgroundExecutor,
16499 cx: &mut TestAppContext,
16500) {
16501 init_test(cx, |_| {});
16502
16503 let mut cx = EditorTestContext::new(cx).await;
16504
16505 let diff_base = r#"
16506 use some::mod1;
16507 use some::mod2;
16508
16509 const A: u32 = 42;
16510 const B: u32 = 42;
16511 const C: u32 = 42;
16512 const D: u32 = 42;
16513
16514
16515 fn main() {
16516 println!("hello");
16517
16518 println!("world");
16519 }"#
16520 .unindent();
16521
16522 cx.set_state(
16523 &r#"
16524 use some::mod1;
16525 use some::mod2;
16526
16527 const A: u32 = 42;
16528 const B: u32 = 42;
16529 const C: u32 = 43ˇ
16530 const D: u32 = 42;
16531
16532
16533 fn main() {
16534 println!("hello");
16535
16536 println!("world");
16537 }"#
16538 .unindent(),
16539 );
16540
16541 cx.set_head_text(&diff_base);
16542 executor.run_until_parked();
16543 cx.update_editor(|editor, window, cx| {
16544 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16545 });
16546 executor.run_until_parked();
16547
16548 cx.assert_state_with_diff(
16549 r#"
16550 use some::mod1;
16551 use some::mod2;
16552
16553 const A: u32 = 42;
16554 const B: u32 = 42;
16555 - const C: u32 = 42;
16556 + const C: u32 = 43ˇ
16557 const D: u32 = 42;
16558
16559
16560 fn main() {
16561 println!("hello");
16562
16563 println!("world");
16564 }"#
16565 .unindent(),
16566 );
16567
16568 cx.update_editor(|editor, window, cx| {
16569 editor.handle_input("\nnew_line\n", window, cx);
16570 });
16571 executor.run_until_parked();
16572
16573 cx.assert_state_with_diff(
16574 r#"
16575 use some::mod1;
16576 use some::mod2;
16577
16578 const A: u32 = 42;
16579 const B: u32 = 42;
16580 - const C: u32 = 42;
16581 + const C: u32 = 43
16582 + new_line
16583 + ˇ
16584 const D: u32 = 42;
16585
16586
16587 fn main() {
16588 println!("hello");
16589
16590 println!("world");
16591 }"#
16592 .unindent(),
16593 );
16594}
16595
16596#[gpui::test]
16597async fn test_stage_and_unstage_added_file_hunk(
16598 executor: BackgroundExecutor,
16599 cx: &mut TestAppContext,
16600) {
16601 init_test(cx, |_| {});
16602
16603 let mut cx = EditorTestContext::new(cx).await;
16604 cx.update_editor(|editor, _, cx| {
16605 editor.set_expand_all_diff_hunks(cx);
16606 });
16607
16608 let working_copy = r#"
16609 ˇfn main() {
16610 println!("hello, world!");
16611 }
16612 "#
16613 .unindent();
16614
16615 cx.set_state(&working_copy);
16616 executor.run_until_parked();
16617
16618 cx.assert_state_with_diff(
16619 r#"
16620 + ˇfn main() {
16621 + println!("hello, world!");
16622 + }
16623 "#
16624 .unindent(),
16625 );
16626 cx.assert_index_text(None);
16627
16628 cx.update_editor(|editor, window, cx| {
16629 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16630 });
16631 executor.run_until_parked();
16632 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16633 cx.assert_state_with_diff(
16634 r#"
16635 + ˇfn main() {
16636 + println!("hello, world!");
16637 + }
16638 "#
16639 .unindent(),
16640 );
16641
16642 cx.update_editor(|editor, window, cx| {
16643 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16644 });
16645 executor.run_until_parked();
16646 cx.assert_index_text(None);
16647}
16648
16649async fn setup_indent_guides_editor(
16650 text: &str,
16651 cx: &mut TestAppContext,
16652) -> (BufferId, EditorTestContext) {
16653 init_test(cx, |_| {});
16654
16655 let mut cx = EditorTestContext::new(cx).await;
16656
16657 let buffer_id = cx.update_editor(|editor, window, cx| {
16658 editor.set_text(text, window, cx);
16659 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16660
16661 buffer_ids[0]
16662 });
16663
16664 (buffer_id, cx)
16665}
16666
16667fn assert_indent_guides(
16668 range: Range<u32>,
16669 expected: Vec<IndentGuide>,
16670 active_indices: Option<Vec<usize>>,
16671 cx: &mut EditorTestContext,
16672) {
16673 let indent_guides = cx.update_editor(|editor, window, cx| {
16674 let snapshot = editor.snapshot(window, cx).display_snapshot;
16675 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16676 editor,
16677 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16678 true,
16679 &snapshot,
16680 cx,
16681 );
16682
16683 indent_guides.sort_by(|a, b| {
16684 a.depth.cmp(&b.depth).then(
16685 a.start_row
16686 .cmp(&b.start_row)
16687 .then(a.end_row.cmp(&b.end_row)),
16688 )
16689 });
16690 indent_guides
16691 });
16692
16693 if let Some(expected) = active_indices {
16694 let active_indices = cx.update_editor(|editor, window, cx| {
16695 let snapshot = editor.snapshot(window, cx).display_snapshot;
16696 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16697 });
16698
16699 assert_eq!(
16700 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16701 expected,
16702 "Active indent guide indices do not match"
16703 );
16704 }
16705
16706 assert_eq!(indent_guides, expected, "Indent guides do not match");
16707}
16708
16709fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16710 IndentGuide {
16711 buffer_id,
16712 start_row: MultiBufferRow(start_row),
16713 end_row: MultiBufferRow(end_row),
16714 depth,
16715 tab_size: 4,
16716 settings: IndentGuideSettings {
16717 enabled: true,
16718 line_width: 1,
16719 active_line_width: 1,
16720 ..Default::default()
16721 },
16722 }
16723}
16724
16725#[gpui::test]
16726async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16727 let (buffer_id, mut cx) = setup_indent_guides_editor(
16728 &"
16729 fn main() {
16730 let a = 1;
16731 }"
16732 .unindent(),
16733 cx,
16734 )
16735 .await;
16736
16737 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16738}
16739
16740#[gpui::test]
16741async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16742 let (buffer_id, mut cx) = setup_indent_guides_editor(
16743 &"
16744 fn main() {
16745 let a = 1;
16746 let b = 2;
16747 }"
16748 .unindent(),
16749 cx,
16750 )
16751 .await;
16752
16753 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16754}
16755
16756#[gpui::test]
16757async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16758 let (buffer_id, mut cx) = setup_indent_guides_editor(
16759 &"
16760 fn main() {
16761 let a = 1;
16762 if a == 3 {
16763 let b = 2;
16764 } else {
16765 let c = 3;
16766 }
16767 }"
16768 .unindent(),
16769 cx,
16770 )
16771 .await;
16772
16773 assert_indent_guides(
16774 0..8,
16775 vec![
16776 indent_guide(buffer_id, 1, 6, 0),
16777 indent_guide(buffer_id, 3, 3, 1),
16778 indent_guide(buffer_id, 5, 5, 1),
16779 ],
16780 None,
16781 &mut cx,
16782 );
16783}
16784
16785#[gpui::test]
16786async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16787 let (buffer_id, mut cx) = setup_indent_guides_editor(
16788 &"
16789 fn main() {
16790 let a = 1;
16791 let b = 2;
16792 let c = 3;
16793 }"
16794 .unindent(),
16795 cx,
16796 )
16797 .await;
16798
16799 assert_indent_guides(
16800 0..5,
16801 vec![
16802 indent_guide(buffer_id, 1, 3, 0),
16803 indent_guide(buffer_id, 2, 2, 1),
16804 ],
16805 None,
16806 &mut cx,
16807 );
16808}
16809
16810#[gpui::test]
16811async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16812 let (buffer_id, mut cx) = setup_indent_guides_editor(
16813 &"
16814 fn main() {
16815 let a = 1;
16816
16817 let c = 3;
16818 }"
16819 .unindent(),
16820 cx,
16821 )
16822 .await;
16823
16824 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16825}
16826
16827#[gpui::test]
16828async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16829 let (buffer_id, mut cx) = setup_indent_guides_editor(
16830 &"
16831 fn main() {
16832 let a = 1;
16833
16834 let c = 3;
16835
16836 if a == 3 {
16837 let b = 2;
16838 } else {
16839 let c = 3;
16840 }
16841 }"
16842 .unindent(),
16843 cx,
16844 )
16845 .await;
16846
16847 assert_indent_guides(
16848 0..11,
16849 vec![
16850 indent_guide(buffer_id, 1, 9, 0),
16851 indent_guide(buffer_id, 6, 6, 1),
16852 indent_guide(buffer_id, 8, 8, 1),
16853 ],
16854 None,
16855 &mut cx,
16856 );
16857}
16858
16859#[gpui::test]
16860async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16861 let (buffer_id, mut cx) = setup_indent_guides_editor(
16862 &"
16863 fn main() {
16864 let a = 1;
16865
16866 let c = 3;
16867
16868 if a == 3 {
16869 let b = 2;
16870 } else {
16871 let c = 3;
16872 }
16873 }"
16874 .unindent(),
16875 cx,
16876 )
16877 .await;
16878
16879 assert_indent_guides(
16880 1..11,
16881 vec![
16882 indent_guide(buffer_id, 1, 9, 0),
16883 indent_guide(buffer_id, 6, 6, 1),
16884 indent_guide(buffer_id, 8, 8, 1),
16885 ],
16886 None,
16887 &mut cx,
16888 );
16889}
16890
16891#[gpui::test]
16892async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16893 let (buffer_id, mut cx) = setup_indent_guides_editor(
16894 &"
16895 fn main() {
16896 let a = 1;
16897
16898 let c = 3;
16899
16900 if a == 3 {
16901 let b = 2;
16902 } else {
16903 let c = 3;
16904 }
16905 }"
16906 .unindent(),
16907 cx,
16908 )
16909 .await;
16910
16911 assert_indent_guides(
16912 1..10,
16913 vec![
16914 indent_guide(buffer_id, 1, 9, 0),
16915 indent_guide(buffer_id, 6, 6, 1),
16916 indent_guide(buffer_id, 8, 8, 1),
16917 ],
16918 None,
16919 &mut cx,
16920 );
16921}
16922
16923#[gpui::test]
16924async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16925 let (buffer_id, mut cx) = setup_indent_guides_editor(
16926 &"
16927 block1
16928 block2
16929 block3
16930 block4
16931 block2
16932 block1
16933 block1"
16934 .unindent(),
16935 cx,
16936 )
16937 .await;
16938
16939 assert_indent_guides(
16940 1..10,
16941 vec![
16942 indent_guide(buffer_id, 1, 4, 0),
16943 indent_guide(buffer_id, 2, 3, 1),
16944 indent_guide(buffer_id, 3, 3, 2),
16945 ],
16946 None,
16947 &mut cx,
16948 );
16949}
16950
16951#[gpui::test]
16952async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16953 let (buffer_id, mut cx) = setup_indent_guides_editor(
16954 &"
16955 block1
16956 block2
16957 block3
16958
16959 block1
16960 block1"
16961 .unindent(),
16962 cx,
16963 )
16964 .await;
16965
16966 assert_indent_guides(
16967 0..6,
16968 vec![
16969 indent_guide(buffer_id, 1, 2, 0),
16970 indent_guide(buffer_id, 2, 2, 1),
16971 ],
16972 None,
16973 &mut cx,
16974 );
16975}
16976
16977#[gpui::test]
16978async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16979 let (buffer_id, mut cx) = setup_indent_guides_editor(
16980 &"
16981 block1
16982
16983
16984
16985 block2
16986 "
16987 .unindent(),
16988 cx,
16989 )
16990 .await;
16991
16992 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16993}
16994
16995#[gpui::test]
16996async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16997 let (buffer_id, mut cx) = setup_indent_guides_editor(
16998 &"
16999 def a:
17000 \tb = 3
17001 \tif True:
17002 \t\tc = 4
17003 \t\td = 5
17004 \tprint(b)
17005 "
17006 .unindent(),
17007 cx,
17008 )
17009 .await;
17010
17011 assert_indent_guides(
17012 0..6,
17013 vec![
17014 indent_guide(buffer_id, 1, 5, 0),
17015 indent_guide(buffer_id, 3, 4, 1),
17016 ],
17017 None,
17018 &mut cx,
17019 );
17020}
17021
17022#[gpui::test]
17023async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17024 let (buffer_id, mut cx) = setup_indent_guides_editor(
17025 &"
17026 fn main() {
17027 let a = 1;
17028 }"
17029 .unindent(),
17030 cx,
17031 )
17032 .await;
17033
17034 cx.update_editor(|editor, window, cx| {
17035 editor.change_selections(None, window, cx, |s| {
17036 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17037 });
17038 });
17039
17040 assert_indent_guides(
17041 0..3,
17042 vec![indent_guide(buffer_id, 1, 1, 0)],
17043 Some(vec![0]),
17044 &mut cx,
17045 );
17046}
17047
17048#[gpui::test]
17049async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17050 let (buffer_id, mut cx) = setup_indent_guides_editor(
17051 &"
17052 fn main() {
17053 if 1 == 2 {
17054 let a = 1;
17055 }
17056 }"
17057 .unindent(),
17058 cx,
17059 )
17060 .await;
17061
17062 cx.update_editor(|editor, window, cx| {
17063 editor.change_selections(None, window, cx, |s| {
17064 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17065 });
17066 });
17067
17068 assert_indent_guides(
17069 0..4,
17070 vec![
17071 indent_guide(buffer_id, 1, 3, 0),
17072 indent_guide(buffer_id, 2, 2, 1),
17073 ],
17074 Some(vec![1]),
17075 &mut cx,
17076 );
17077
17078 cx.update_editor(|editor, window, cx| {
17079 editor.change_selections(None, window, cx, |s| {
17080 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17081 });
17082 });
17083
17084 assert_indent_guides(
17085 0..4,
17086 vec![
17087 indent_guide(buffer_id, 1, 3, 0),
17088 indent_guide(buffer_id, 2, 2, 1),
17089 ],
17090 Some(vec![1]),
17091 &mut cx,
17092 );
17093
17094 cx.update_editor(|editor, window, cx| {
17095 editor.change_selections(None, window, cx, |s| {
17096 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17097 });
17098 });
17099
17100 assert_indent_guides(
17101 0..4,
17102 vec![
17103 indent_guide(buffer_id, 1, 3, 0),
17104 indent_guide(buffer_id, 2, 2, 1),
17105 ],
17106 Some(vec![0]),
17107 &mut cx,
17108 );
17109}
17110
17111#[gpui::test]
17112async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17113 let (buffer_id, mut cx) = setup_indent_guides_editor(
17114 &"
17115 fn main() {
17116 let a = 1;
17117
17118 let b = 2;
17119 }"
17120 .unindent(),
17121 cx,
17122 )
17123 .await;
17124
17125 cx.update_editor(|editor, window, cx| {
17126 editor.change_selections(None, window, cx, |s| {
17127 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17128 });
17129 });
17130
17131 assert_indent_guides(
17132 0..5,
17133 vec![indent_guide(buffer_id, 1, 3, 0)],
17134 Some(vec![0]),
17135 &mut cx,
17136 );
17137}
17138
17139#[gpui::test]
17140async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17141 let (buffer_id, mut cx) = setup_indent_guides_editor(
17142 &"
17143 def m:
17144 a = 1
17145 pass"
17146 .unindent(),
17147 cx,
17148 )
17149 .await;
17150
17151 cx.update_editor(|editor, window, cx| {
17152 editor.change_selections(None, window, cx, |s| {
17153 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17154 });
17155 });
17156
17157 assert_indent_guides(
17158 0..3,
17159 vec![indent_guide(buffer_id, 1, 2, 0)],
17160 Some(vec![0]),
17161 &mut cx,
17162 );
17163}
17164
17165#[gpui::test]
17166async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17167 init_test(cx, |_| {});
17168 let mut cx = EditorTestContext::new(cx).await;
17169 let text = indoc! {
17170 "
17171 impl A {
17172 fn b() {
17173 0;
17174 3;
17175 5;
17176 6;
17177 7;
17178 }
17179 }
17180 "
17181 };
17182 let base_text = indoc! {
17183 "
17184 impl A {
17185 fn b() {
17186 0;
17187 1;
17188 2;
17189 3;
17190 4;
17191 }
17192 fn c() {
17193 5;
17194 6;
17195 7;
17196 }
17197 }
17198 "
17199 };
17200
17201 cx.update_editor(|editor, window, cx| {
17202 editor.set_text(text, window, cx);
17203
17204 editor.buffer().update(cx, |multibuffer, cx| {
17205 let buffer = multibuffer.as_singleton().unwrap();
17206 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17207
17208 multibuffer.set_all_diff_hunks_expanded(cx);
17209 multibuffer.add_diff(diff, cx);
17210
17211 buffer.read(cx).remote_id()
17212 })
17213 });
17214 cx.run_until_parked();
17215
17216 cx.assert_state_with_diff(
17217 indoc! { "
17218 impl A {
17219 fn b() {
17220 0;
17221 - 1;
17222 - 2;
17223 3;
17224 - 4;
17225 - }
17226 - fn c() {
17227 5;
17228 6;
17229 7;
17230 }
17231 }
17232 ˇ"
17233 }
17234 .to_string(),
17235 );
17236
17237 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17238 editor
17239 .snapshot(window, cx)
17240 .buffer_snapshot
17241 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17242 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17243 .collect::<Vec<_>>()
17244 });
17245 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17246 assert_eq!(
17247 actual_guides,
17248 vec![
17249 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17250 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17251 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17252 ]
17253 );
17254}
17255
17256#[gpui::test]
17257async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17258 init_test(cx, |_| {});
17259 let mut cx = EditorTestContext::new(cx).await;
17260
17261 let diff_base = r#"
17262 a
17263 b
17264 c
17265 "#
17266 .unindent();
17267
17268 cx.set_state(
17269 &r#"
17270 ˇA
17271 b
17272 C
17273 "#
17274 .unindent(),
17275 );
17276 cx.set_head_text(&diff_base);
17277 cx.update_editor(|editor, window, cx| {
17278 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17279 });
17280 executor.run_until_parked();
17281
17282 let both_hunks_expanded = r#"
17283 - a
17284 + ˇA
17285 b
17286 - c
17287 + C
17288 "#
17289 .unindent();
17290
17291 cx.assert_state_with_diff(both_hunks_expanded.clone());
17292
17293 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17294 let snapshot = editor.snapshot(window, cx);
17295 let hunks = editor
17296 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17297 .collect::<Vec<_>>();
17298 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17299 let buffer_id = hunks[0].buffer_id;
17300 hunks
17301 .into_iter()
17302 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17303 .collect::<Vec<_>>()
17304 });
17305 assert_eq!(hunk_ranges.len(), 2);
17306
17307 cx.update_editor(|editor, _, cx| {
17308 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17309 });
17310 executor.run_until_parked();
17311
17312 let second_hunk_expanded = r#"
17313 ˇA
17314 b
17315 - c
17316 + C
17317 "#
17318 .unindent();
17319
17320 cx.assert_state_with_diff(second_hunk_expanded);
17321
17322 cx.update_editor(|editor, _, cx| {
17323 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17324 });
17325 executor.run_until_parked();
17326
17327 cx.assert_state_with_diff(both_hunks_expanded.clone());
17328
17329 cx.update_editor(|editor, _, cx| {
17330 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17331 });
17332 executor.run_until_parked();
17333
17334 let first_hunk_expanded = r#"
17335 - a
17336 + ˇA
17337 b
17338 C
17339 "#
17340 .unindent();
17341
17342 cx.assert_state_with_diff(first_hunk_expanded);
17343
17344 cx.update_editor(|editor, _, cx| {
17345 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17346 });
17347 executor.run_until_parked();
17348
17349 cx.assert_state_with_diff(both_hunks_expanded);
17350
17351 cx.set_state(
17352 &r#"
17353 ˇA
17354 b
17355 "#
17356 .unindent(),
17357 );
17358 cx.run_until_parked();
17359
17360 // TODO this cursor position seems bad
17361 cx.assert_state_with_diff(
17362 r#"
17363 - ˇa
17364 + A
17365 b
17366 "#
17367 .unindent(),
17368 );
17369
17370 cx.update_editor(|editor, window, cx| {
17371 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17372 });
17373
17374 cx.assert_state_with_diff(
17375 r#"
17376 - ˇa
17377 + A
17378 b
17379 - c
17380 "#
17381 .unindent(),
17382 );
17383
17384 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17385 let snapshot = editor.snapshot(window, cx);
17386 let hunks = editor
17387 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17388 .collect::<Vec<_>>();
17389 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17390 let buffer_id = hunks[0].buffer_id;
17391 hunks
17392 .into_iter()
17393 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17394 .collect::<Vec<_>>()
17395 });
17396 assert_eq!(hunk_ranges.len(), 2);
17397
17398 cx.update_editor(|editor, _, cx| {
17399 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17400 });
17401 executor.run_until_parked();
17402
17403 cx.assert_state_with_diff(
17404 r#"
17405 - ˇa
17406 + A
17407 b
17408 "#
17409 .unindent(),
17410 );
17411}
17412
17413#[gpui::test]
17414async fn test_toggle_deletion_hunk_at_start_of_file(
17415 executor: BackgroundExecutor,
17416 cx: &mut TestAppContext,
17417) {
17418 init_test(cx, |_| {});
17419 let mut cx = EditorTestContext::new(cx).await;
17420
17421 let diff_base = r#"
17422 a
17423 b
17424 c
17425 "#
17426 .unindent();
17427
17428 cx.set_state(
17429 &r#"
17430 ˇb
17431 c
17432 "#
17433 .unindent(),
17434 );
17435 cx.set_head_text(&diff_base);
17436 cx.update_editor(|editor, window, cx| {
17437 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17438 });
17439 executor.run_until_parked();
17440
17441 let hunk_expanded = r#"
17442 - a
17443 ˇb
17444 c
17445 "#
17446 .unindent();
17447
17448 cx.assert_state_with_diff(hunk_expanded.clone());
17449
17450 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17451 let snapshot = editor.snapshot(window, cx);
17452 let hunks = editor
17453 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17454 .collect::<Vec<_>>();
17455 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17456 let buffer_id = hunks[0].buffer_id;
17457 hunks
17458 .into_iter()
17459 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17460 .collect::<Vec<_>>()
17461 });
17462 assert_eq!(hunk_ranges.len(), 1);
17463
17464 cx.update_editor(|editor, _, cx| {
17465 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17466 });
17467 executor.run_until_parked();
17468
17469 let hunk_collapsed = r#"
17470 ˇb
17471 c
17472 "#
17473 .unindent();
17474
17475 cx.assert_state_with_diff(hunk_collapsed);
17476
17477 cx.update_editor(|editor, _, cx| {
17478 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17479 });
17480 executor.run_until_parked();
17481
17482 cx.assert_state_with_diff(hunk_expanded.clone());
17483}
17484
17485#[gpui::test]
17486async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17487 init_test(cx, |_| {});
17488
17489 let fs = FakeFs::new(cx.executor());
17490 fs.insert_tree(
17491 path!("/test"),
17492 json!({
17493 ".git": {},
17494 "file-1": "ONE\n",
17495 "file-2": "TWO\n",
17496 "file-3": "THREE\n",
17497 }),
17498 )
17499 .await;
17500
17501 fs.set_head_for_repo(
17502 path!("/test/.git").as_ref(),
17503 &[
17504 ("file-1".into(), "one\n".into()),
17505 ("file-2".into(), "two\n".into()),
17506 ("file-3".into(), "three\n".into()),
17507 ],
17508 );
17509
17510 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17511 let mut buffers = vec![];
17512 for i in 1..=3 {
17513 let buffer = project
17514 .update(cx, |project, cx| {
17515 let path = format!(path!("/test/file-{}"), i);
17516 project.open_local_buffer(path, cx)
17517 })
17518 .await
17519 .unwrap();
17520 buffers.push(buffer);
17521 }
17522
17523 let multibuffer = cx.new(|cx| {
17524 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17525 multibuffer.set_all_diff_hunks_expanded(cx);
17526 for buffer in &buffers {
17527 let snapshot = buffer.read(cx).snapshot();
17528 multibuffer.set_excerpts_for_path(
17529 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17530 buffer.clone(),
17531 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17532 DEFAULT_MULTIBUFFER_CONTEXT,
17533 cx,
17534 );
17535 }
17536 multibuffer
17537 });
17538
17539 let editor = cx.add_window(|window, cx| {
17540 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17541 });
17542 cx.run_until_parked();
17543
17544 let snapshot = editor
17545 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17546 .unwrap();
17547 let hunks = snapshot
17548 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17549 .map(|hunk| match hunk {
17550 DisplayDiffHunk::Unfolded {
17551 display_row_range, ..
17552 } => display_row_range,
17553 DisplayDiffHunk::Folded { .. } => unreachable!(),
17554 })
17555 .collect::<Vec<_>>();
17556 assert_eq!(
17557 hunks,
17558 [
17559 DisplayRow(2)..DisplayRow(4),
17560 DisplayRow(7)..DisplayRow(9),
17561 DisplayRow(12)..DisplayRow(14),
17562 ]
17563 );
17564}
17565
17566#[gpui::test]
17567async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17568 init_test(cx, |_| {});
17569
17570 let mut cx = EditorTestContext::new(cx).await;
17571 cx.set_head_text(indoc! { "
17572 one
17573 two
17574 three
17575 four
17576 five
17577 "
17578 });
17579 cx.set_index_text(indoc! { "
17580 one
17581 two
17582 three
17583 four
17584 five
17585 "
17586 });
17587 cx.set_state(indoc! {"
17588 one
17589 TWO
17590 ˇTHREE
17591 FOUR
17592 five
17593 "});
17594 cx.run_until_parked();
17595 cx.update_editor(|editor, window, cx| {
17596 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17597 });
17598 cx.run_until_parked();
17599 cx.assert_index_text(Some(indoc! {"
17600 one
17601 TWO
17602 THREE
17603 FOUR
17604 five
17605 "}));
17606 cx.set_state(indoc! { "
17607 one
17608 TWO
17609 ˇTHREE-HUNDRED
17610 FOUR
17611 five
17612 "});
17613 cx.run_until_parked();
17614 cx.update_editor(|editor, window, cx| {
17615 let snapshot = editor.snapshot(window, cx);
17616 let hunks = editor
17617 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17618 .collect::<Vec<_>>();
17619 assert_eq!(hunks.len(), 1);
17620 assert_eq!(
17621 hunks[0].status(),
17622 DiffHunkStatus {
17623 kind: DiffHunkStatusKind::Modified,
17624 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17625 }
17626 );
17627
17628 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17629 });
17630 cx.run_until_parked();
17631 cx.assert_index_text(Some(indoc! {"
17632 one
17633 TWO
17634 THREE-HUNDRED
17635 FOUR
17636 five
17637 "}));
17638}
17639
17640#[gpui::test]
17641fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17642 init_test(cx, |_| {});
17643
17644 let editor = cx.add_window(|window, cx| {
17645 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17646 build_editor(buffer, window, cx)
17647 });
17648
17649 let render_args = Arc::new(Mutex::new(None));
17650 let snapshot = editor
17651 .update(cx, |editor, window, cx| {
17652 let snapshot = editor.buffer().read(cx).snapshot(cx);
17653 let range =
17654 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17655
17656 struct RenderArgs {
17657 row: MultiBufferRow,
17658 folded: bool,
17659 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17660 }
17661
17662 let crease = Crease::inline(
17663 range,
17664 FoldPlaceholder::test(),
17665 {
17666 let toggle_callback = render_args.clone();
17667 move |row, folded, callback, _window, _cx| {
17668 *toggle_callback.lock() = Some(RenderArgs {
17669 row,
17670 folded,
17671 callback,
17672 });
17673 div()
17674 }
17675 },
17676 |_row, _folded, _window, _cx| div(),
17677 );
17678
17679 editor.insert_creases(Some(crease), cx);
17680 let snapshot = editor.snapshot(window, cx);
17681 let _div = snapshot.render_crease_toggle(
17682 MultiBufferRow(1),
17683 false,
17684 cx.entity().clone(),
17685 window,
17686 cx,
17687 );
17688 snapshot
17689 })
17690 .unwrap();
17691
17692 let render_args = render_args.lock().take().unwrap();
17693 assert_eq!(render_args.row, MultiBufferRow(1));
17694 assert!(!render_args.folded);
17695 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17696
17697 cx.update_window(*editor, |_, window, cx| {
17698 (render_args.callback)(true, window, cx)
17699 })
17700 .unwrap();
17701 let snapshot = editor
17702 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17703 .unwrap();
17704 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17705
17706 cx.update_window(*editor, |_, window, cx| {
17707 (render_args.callback)(false, window, cx)
17708 })
17709 .unwrap();
17710 let snapshot = editor
17711 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17712 .unwrap();
17713 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17714}
17715
17716#[gpui::test]
17717async fn test_input_text(cx: &mut TestAppContext) {
17718 init_test(cx, |_| {});
17719 let mut cx = EditorTestContext::new(cx).await;
17720
17721 cx.set_state(
17722 &r#"ˇone
17723 two
17724
17725 three
17726 fourˇ
17727 five
17728
17729 siˇx"#
17730 .unindent(),
17731 );
17732
17733 cx.dispatch_action(HandleInput(String::new()));
17734 cx.assert_editor_state(
17735 &r#"ˇone
17736 two
17737
17738 three
17739 fourˇ
17740 five
17741
17742 siˇx"#
17743 .unindent(),
17744 );
17745
17746 cx.dispatch_action(HandleInput("AAAA".to_string()));
17747 cx.assert_editor_state(
17748 &r#"AAAAˇone
17749 two
17750
17751 three
17752 fourAAAAˇ
17753 five
17754
17755 siAAAAˇx"#
17756 .unindent(),
17757 );
17758}
17759
17760#[gpui::test]
17761async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17762 init_test(cx, |_| {});
17763
17764 let mut cx = EditorTestContext::new(cx).await;
17765 cx.set_state(
17766 r#"let foo = 1;
17767let foo = 2;
17768let foo = 3;
17769let fooˇ = 4;
17770let foo = 5;
17771let foo = 6;
17772let foo = 7;
17773let foo = 8;
17774let foo = 9;
17775let foo = 10;
17776let foo = 11;
17777let foo = 12;
17778let foo = 13;
17779let foo = 14;
17780let foo = 15;"#,
17781 );
17782
17783 cx.update_editor(|e, window, cx| {
17784 assert_eq!(
17785 e.next_scroll_position,
17786 NextScrollCursorCenterTopBottom::Center,
17787 "Default next scroll direction is center",
17788 );
17789
17790 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17791 assert_eq!(
17792 e.next_scroll_position,
17793 NextScrollCursorCenterTopBottom::Top,
17794 "After center, next scroll direction should be top",
17795 );
17796
17797 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17798 assert_eq!(
17799 e.next_scroll_position,
17800 NextScrollCursorCenterTopBottom::Bottom,
17801 "After top, next scroll direction should be bottom",
17802 );
17803
17804 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17805 assert_eq!(
17806 e.next_scroll_position,
17807 NextScrollCursorCenterTopBottom::Center,
17808 "After bottom, scrolling should start over",
17809 );
17810
17811 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17812 assert_eq!(
17813 e.next_scroll_position,
17814 NextScrollCursorCenterTopBottom::Top,
17815 "Scrolling continues if retriggered fast enough"
17816 );
17817 });
17818
17819 cx.executor()
17820 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17821 cx.executor().run_until_parked();
17822 cx.update_editor(|e, _, _| {
17823 assert_eq!(
17824 e.next_scroll_position,
17825 NextScrollCursorCenterTopBottom::Center,
17826 "If scrolling is not triggered fast enough, it should reset"
17827 );
17828 });
17829}
17830
17831#[gpui::test]
17832async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17833 init_test(cx, |_| {});
17834 let mut cx = EditorLspTestContext::new_rust(
17835 lsp::ServerCapabilities {
17836 definition_provider: Some(lsp::OneOf::Left(true)),
17837 references_provider: Some(lsp::OneOf::Left(true)),
17838 ..lsp::ServerCapabilities::default()
17839 },
17840 cx,
17841 )
17842 .await;
17843
17844 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17845 let go_to_definition = cx
17846 .lsp
17847 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17848 move |params, _| async move {
17849 if empty_go_to_definition {
17850 Ok(None)
17851 } else {
17852 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17853 uri: params.text_document_position_params.text_document.uri,
17854 range: lsp::Range::new(
17855 lsp::Position::new(4, 3),
17856 lsp::Position::new(4, 6),
17857 ),
17858 })))
17859 }
17860 },
17861 );
17862 let references = cx
17863 .lsp
17864 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17865 Ok(Some(vec![lsp::Location {
17866 uri: params.text_document_position.text_document.uri,
17867 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17868 }]))
17869 });
17870 (go_to_definition, references)
17871 };
17872
17873 cx.set_state(
17874 &r#"fn one() {
17875 let mut a = ˇtwo();
17876 }
17877
17878 fn two() {}"#
17879 .unindent(),
17880 );
17881 set_up_lsp_handlers(false, &mut cx);
17882 let navigated = cx
17883 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17884 .await
17885 .expect("Failed to navigate to definition");
17886 assert_eq!(
17887 navigated,
17888 Navigated::Yes,
17889 "Should have navigated to definition from the GetDefinition response"
17890 );
17891 cx.assert_editor_state(
17892 &r#"fn one() {
17893 let mut a = two();
17894 }
17895
17896 fn «twoˇ»() {}"#
17897 .unindent(),
17898 );
17899
17900 let editors = cx.update_workspace(|workspace, _, cx| {
17901 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17902 });
17903 cx.update_editor(|_, _, test_editor_cx| {
17904 assert_eq!(
17905 editors.len(),
17906 1,
17907 "Initially, only one, test, editor should be open in the workspace"
17908 );
17909 assert_eq!(
17910 test_editor_cx.entity(),
17911 editors.last().expect("Asserted len is 1").clone()
17912 );
17913 });
17914
17915 set_up_lsp_handlers(true, &mut cx);
17916 let navigated = cx
17917 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17918 .await
17919 .expect("Failed to navigate to lookup references");
17920 assert_eq!(
17921 navigated,
17922 Navigated::Yes,
17923 "Should have navigated to references as a fallback after empty GoToDefinition response"
17924 );
17925 // We should not change the selections in the existing file,
17926 // if opening another milti buffer with the references
17927 cx.assert_editor_state(
17928 &r#"fn one() {
17929 let mut a = two();
17930 }
17931
17932 fn «twoˇ»() {}"#
17933 .unindent(),
17934 );
17935 let editors = cx.update_workspace(|workspace, _, cx| {
17936 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17937 });
17938 cx.update_editor(|_, _, test_editor_cx| {
17939 assert_eq!(
17940 editors.len(),
17941 2,
17942 "After falling back to references search, we open a new editor with the results"
17943 );
17944 let references_fallback_text = editors
17945 .into_iter()
17946 .find(|new_editor| *new_editor != test_editor_cx.entity())
17947 .expect("Should have one non-test editor now")
17948 .read(test_editor_cx)
17949 .text(test_editor_cx);
17950 assert_eq!(
17951 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17952 "Should use the range from the references response and not the GoToDefinition one"
17953 );
17954 });
17955}
17956
17957#[gpui::test]
17958async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17959 init_test(cx, |_| {});
17960 cx.update(|cx| {
17961 let mut editor_settings = EditorSettings::get_global(cx).clone();
17962 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17963 EditorSettings::override_global(editor_settings, cx);
17964 });
17965 let mut cx = EditorLspTestContext::new_rust(
17966 lsp::ServerCapabilities {
17967 definition_provider: Some(lsp::OneOf::Left(true)),
17968 references_provider: Some(lsp::OneOf::Left(true)),
17969 ..lsp::ServerCapabilities::default()
17970 },
17971 cx,
17972 )
17973 .await;
17974 let original_state = r#"fn one() {
17975 let mut a = ˇtwo();
17976 }
17977
17978 fn two() {}"#
17979 .unindent();
17980 cx.set_state(&original_state);
17981
17982 let mut go_to_definition = cx
17983 .lsp
17984 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17985 move |_, _| async move { Ok(None) },
17986 );
17987 let _references = cx
17988 .lsp
17989 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17990 panic!("Should not call for references with no go to definition fallback")
17991 });
17992
17993 let navigated = cx
17994 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17995 .await
17996 .expect("Failed to navigate to lookup references");
17997 go_to_definition
17998 .next()
17999 .await
18000 .expect("Should have called the go_to_definition handler");
18001
18002 assert_eq!(
18003 navigated,
18004 Navigated::No,
18005 "Should have navigated to references as a fallback after empty GoToDefinition response"
18006 );
18007 cx.assert_editor_state(&original_state);
18008 let editors = cx.update_workspace(|workspace, _, cx| {
18009 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18010 });
18011 cx.update_editor(|_, _, _| {
18012 assert_eq!(
18013 editors.len(),
18014 1,
18015 "After unsuccessful fallback, no other editor should have been opened"
18016 );
18017 });
18018}
18019
18020#[gpui::test]
18021async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18022 init_test(cx, |_| {});
18023
18024 let language = Arc::new(Language::new(
18025 LanguageConfig::default(),
18026 Some(tree_sitter_rust::LANGUAGE.into()),
18027 ));
18028
18029 let text = r#"
18030 #[cfg(test)]
18031 mod tests() {
18032 #[test]
18033 fn runnable_1() {
18034 let a = 1;
18035 }
18036
18037 #[test]
18038 fn runnable_2() {
18039 let a = 1;
18040 let b = 2;
18041 }
18042 }
18043 "#
18044 .unindent();
18045
18046 let fs = FakeFs::new(cx.executor());
18047 fs.insert_file("/file.rs", Default::default()).await;
18048
18049 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18050 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18051 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18052 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18053 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18054
18055 let editor = cx.new_window_entity(|window, cx| {
18056 Editor::new(
18057 EditorMode::full(),
18058 multi_buffer,
18059 Some(project.clone()),
18060 window,
18061 cx,
18062 )
18063 });
18064
18065 editor.update_in(cx, |editor, window, cx| {
18066 let snapshot = editor.buffer().read(cx).snapshot(cx);
18067 editor.tasks.insert(
18068 (buffer.read(cx).remote_id(), 3),
18069 RunnableTasks {
18070 templates: vec![],
18071 offset: snapshot.anchor_before(43),
18072 column: 0,
18073 extra_variables: HashMap::default(),
18074 context_range: BufferOffset(43)..BufferOffset(85),
18075 },
18076 );
18077 editor.tasks.insert(
18078 (buffer.read(cx).remote_id(), 8),
18079 RunnableTasks {
18080 templates: vec![],
18081 offset: snapshot.anchor_before(86),
18082 column: 0,
18083 extra_variables: HashMap::default(),
18084 context_range: BufferOffset(86)..BufferOffset(191),
18085 },
18086 );
18087
18088 // Test finding task when cursor is inside function body
18089 editor.change_selections(None, window, cx, |s| {
18090 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18091 });
18092 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18093 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18094
18095 // Test finding task when cursor is on function name
18096 editor.change_selections(None, window, cx, |s| {
18097 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18098 });
18099 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18100 assert_eq!(row, 8, "Should find task when cursor is on function name");
18101 });
18102}
18103
18104#[gpui::test]
18105async fn test_folding_buffers(cx: &mut TestAppContext) {
18106 init_test(cx, |_| {});
18107
18108 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18109 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18110 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18111
18112 let fs = FakeFs::new(cx.executor());
18113 fs.insert_tree(
18114 path!("/a"),
18115 json!({
18116 "first.rs": sample_text_1,
18117 "second.rs": sample_text_2,
18118 "third.rs": sample_text_3,
18119 }),
18120 )
18121 .await;
18122 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18123 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18124 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18125 let worktree = project.update(cx, |project, cx| {
18126 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18127 assert_eq!(worktrees.len(), 1);
18128 worktrees.pop().unwrap()
18129 });
18130 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18131
18132 let buffer_1 = project
18133 .update(cx, |project, cx| {
18134 project.open_buffer((worktree_id, "first.rs"), cx)
18135 })
18136 .await
18137 .unwrap();
18138 let buffer_2 = project
18139 .update(cx, |project, cx| {
18140 project.open_buffer((worktree_id, "second.rs"), cx)
18141 })
18142 .await
18143 .unwrap();
18144 let buffer_3 = project
18145 .update(cx, |project, cx| {
18146 project.open_buffer((worktree_id, "third.rs"), cx)
18147 })
18148 .await
18149 .unwrap();
18150
18151 let multi_buffer = cx.new(|cx| {
18152 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18153 multi_buffer.push_excerpts(
18154 buffer_1.clone(),
18155 [
18156 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18157 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18158 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18159 ],
18160 cx,
18161 );
18162 multi_buffer.push_excerpts(
18163 buffer_2.clone(),
18164 [
18165 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18166 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18167 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18168 ],
18169 cx,
18170 );
18171 multi_buffer.push_excerpts(
18172 buffer_3.clone(),
18173 [
18174 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18175 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18176 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18177 ],
18178 cx,
18179 );
18180 multi_buffer
18181 });
18182 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18183 Editor::new(
18184 EditorMode::full(),
18185 multi_buffer.clone(),
18186 Some(project.clone()),
18187 window,
18188 cx,
18189 )
18190 });
18191
18192 assert_eq!(
18193 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18194 "\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",
18195 );
18196
18197 multi_buffer_editor.update(cx, |editor, cx| {
18198 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18199 });
18200 assert_eq!(
18201 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18202 "\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",
18203 "After folding the first buffer, its text should not be displayed"
18204 );
18205
18206 multi_buffer_editor.update(cx, |editor, cx| {
18207 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18208 });
18209 assert_eq!(
18210 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18211 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18212 "After folding the second buffer, its text should not be displayed"
18213 );
18214
18215 multi_buffer_editor.update(cx, |editor, cx| {
18216 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18217 });
18218 assert_eq!(
18219 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18220 "\n\n\n\n\n",
18221 "After folding the third buffer, its text should not be displayed"
18222 );
18223
18224 // Emulate selection inside the fold logic, that should work
18225 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18226 editor
18227 .snapshot(window, cx)
18228 .next_line_boundary(Point::new(0, 4));
18229 });
18230
18231 multi_buffer_editor.update(cx, |editor, cx| {
18232 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18233 });
18234 assert_eq!(
18235 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18236 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18237 "After unfolding the second buffer, its text should be displayed"
18238 );
18239
18240 // Typing inside of buffer 1 causes that buffer to be unfolded.
18241 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18242 assert_eq!(
18243 multi_buffer
18244 .read(cx)
18245 .snapshot(cx)
18246 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18247 .collect::<String>(),
18248 "bbbb"
18249 );
18250 editor.change_selections(None, window, cx, |selections| {
18251 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18252 });
18253 editor.handle_input("B", window, cx);
18254 });
18255
18256 assert_eq!(
18257 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18258 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18259 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18260 );
18261
18262 multi_buffer_editor.update(cx, |editor, cx| {
18263 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18264 });
18265 assert_eq!(
18266 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18267 "\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",
18268 "After unfolding the all buffers, all original text should be displayed"
18269 );
18270}
18271
18272#[gpui::test]
18273async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18274 init_test(cx, |_| {});
18275
18276 let sample_text_1 = "1111\n2222\n3333".to_string();
18277 let sample_text_2 = "4444\n5555\n6666".to_string();
18278 let sample_text_3 = "7777\n8888\n9999".to_string();
18279
18280 let fs = FakeFs::new(cx.executor());
18281 fs.insert_tree(
18282 path!("/a"),
18283 json!({
18284 "first.rs": sample_text_1,
18285 "second.rs": sample_text_2,
18286 "third.rs": sample_text_3,
18287 }),
18288 )
18289 .await;
18290 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18291 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18292 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18293 let worktree = project.update(cx, |project, cx| {
18294 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18295 assert_eq!(worktrees.len(), 1);
18296 worktrees.pop().unwrap()
18297 });
18298 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18299
18300 let buffer_1 = project
18301 .update(cx, |project, cx| {
18302 project.open_buffer((worktree_id, "first.rs"), cx)
18303 })
18304 .await
18305 .unwrap();
18306 let buffer_2 = project
18307 .update(cx, |project, cx| {
18308 project.open_buffer((worktree_id, "second.rs"), cx)
18309 })
18310 .await
18311 .unwrap();
18312 let buffer_3 = project
18313 .update(cx, |project, cx| {
18314 project.open_buffer((worktree_id, "third.rs"), cx)
18315 })
18316 .await
18317 .unwrap();
18318
18319 let multi_buffer = cx.new(|cx| {
18320 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18321 multi_buffer.push_excerpts(
18322 buffer_1.clone(),
18323 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18324 cx,
18325 );
18326 multi_buffer.push_excerpts(
18327 buffer_2.clone(),
18328 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18329 cx,
18330 );
18331 multi_buffer.push_excerpts(
18332 buffer_3.clone(),
18333 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18334 cx,
18335 );
18336 multi_buffer
18337 });
18338
18339 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18340 Editor::new(
18341 EditorMode::full(),
18342 multi_buffer,
18343 Some(project.clone()),
18344 window,
18345 cx,
18346 )
18347 });
18348
18349 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18350 assert_eq!(
18351 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18352 full_text,
18353 );
18354
18355 multi_buffer_editor.update(cx, |editor, cx| {
18356 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18357 });
18358 assert_eq!(
18359 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18360 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18361 "After folding the first buffer, its text should not be displayed"
18362 );
18363
18364 multi_buffer_editor.update(cx, |editor, cx| {
18365 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18366 });
18367
18368 assert_eq!(
18369 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18370 "\n\n\n\n\n\n7777\n8888\n9999",
18371 "After folding the second buffer, its text should not be displayed"
18372 );
18373
18374 multi_buffer_editor.update(cx, |editor, cx| {
18375 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18376 });
18377 assert_eq!(
18378 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18379 "\n\n\n\n\n",
18380 "After folding the third buffer, its text should not be displayed"
18381 );
18382
18383 multi_buffer_editor.update(cx, |editor, cx| {
18384 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18385 });
18386 assert_eq!(
18387 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18388 "\n\n\n\n4444\n5555\n6666\n\n",
18389 "After unfolding the second buffer, its text should be displayed"
18390 );
18391
18392 multi_buffer_editor.update(cx, |editor, cx| {
18393 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18394 });
18395 assert_eq!(
18396 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18397 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18398 "After unfolding the first buffer, its text should be displayed"
18399 );
18400
18401 multi_buffer_editor.update(cx, |editor, cx| {
18402 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18403 });
18404 assert_eq!(
18405 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18406 full_text,
18407 "After unfolding all buffers, all original text should be displayed"
18408 );
18409}
18410
18411#[gpui::test]
18412async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18413 init_test(cx, |_| {});
18414
18415 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18416
18417 let fs = FakeFs::new(cx.executor());
18418 fs.insert_tree(
18419 path!("/a"),
18420 json!({
18421 "main.rs": sample_text,
18422 }),
18423 )
18424 .await;
18425 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18426 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18427 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18428 let worktree = project.update(cx, |project, cx| {
18429 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18430 assert_eq!(worktrees.len(), 1);
18431 worktrees.pop().unwrap()
18432 });
18433 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18434
18435 let buffer_1 = project
18436 .update(cx, |project, cx| {
18437 project.open_buffer((worktree_id, "main.rs"), cx)
18438 })
18439 .await
18440 .unwrap();
18441
18442 let multi_buffer = cx.new(|cx| {
18443 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18444 multi_buffer.push_excerpts(
18445 buffer_1.clone(),
18446 [ExcerptRange::new(
18447 Point::new(0, 0)
18448 ..Point::new(
18449 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18450 0,
18451 ),
18452 )],
18453 cx,
18454 );
18455 multi_buffer
18456 });
18457 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18458 Editor::new(
18459 EditorMode::full(),
18460 multi_buffer,
18461 Some(project.clone()),
18462 window,
18463 cx,
18464 )
18465 });
18466
18467 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18468 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18469 enum TestHighlight {}
18470 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18471 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18472 editor.highlight_text::<TestHighlight>(
18473 vec![highlight_range.clone()],
18474 HighlightStyle::color(Hsla::green()),
18475 cx,
18476 );
18477 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18478 });
18479
18480 let full_text = format!("\n\n{sample_text}");
18481 assert_eq!(
18482 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18483 full_text,
18484 );
18485}
18486
18487#[gpui::test]
18488async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18489 init_test(cx, |_| {});
18490 cx.update(|cx| {
18491 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18492 "keymaps/default-linux.json",
18493 cx,
18494 )
18495 .unwrap();
18496 cx.bind_keys(default_key_bindings);
18497 });
18498
18499 let (editor, cx) = cx.add_window_view(|window, cx| {
18500 let multi_buffer = MultiBuffer::build_multi(
18501 [
18502 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18503 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18504 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18505 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18506 ],
18507 cx,
18508 );
18509 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18510
18511 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18512 // fold all but the second buffer, so that we test navigating between two
18513 // adjacent folded buffers, as well as folded buffers at the start and
18514 // end the multibuffer
18515 editor.fold_buffer(buffer_ids[0], cx);
18516 editor.fold_buffer(buffer_ids[2], cx);
18517 editor.fold_buffer(buffer_ids[3], cx);
18518
18519 editor
18520 });
18521 cx.simulate_resize(size(px(1000.), px(1000.)));
18522
18523 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18524 cx.assert_excerpts_with_selections(indoc! {"
18525 [EXCERPT]
18526 ˇ[FOLDED]
18527 [EXCERPT]
18528 a1
18529 b1
18530 [EXCERPT]
18531 [FOLDED]
18532 [EXCERPT]
18533 [FOLDED]
18534 "
18535 });
18536 cx.simulate_keystroke("down");
18537 cx.assert_excerpts_with_selections(indoc! {"
18538 [EXCERPT]
18539 [FOLDED]
18540 [EXCERPT]
18541 ˇa1
18542 b1
18543 [EXCERPT]
18544 [FOLDED]
18545 [EXCERPT]
18546 [FOLDED]
18547 "
18548 });
18549 cx.simulate_keystroke("down");
18550 cx.assert_excerpts_with_selections(indoc! {"
18551 [EXCERPT]
18552 [FOLDED]
18553 [EXCERPT]
18554 a1
18555 ˇb1
18556 [EXCERPT]
18557 [FOLDED]
18558 [EXCERPT]
18559 [FOLDED]
18560 "
18561 });
18562 cx.simulate_keystroke("down");
18563 cx.assert_excerpts_with_selections(indoc! {"
18564 [EXCERPT]
18565 [FOLDED]
18566 [EXCERPT]
18567 a1
18568 b1
18569 ˇ[EXCERPT]
18570 [FOLDED]
18571 [EXCERPT]
18572 [FOLDED]
18573 "
18574 });
18575 cx.simulate_keystroke("down");
18576 cx.assert_excerpts_with_selections(indoc! {"
18577 [EXCERPT]
18578 [FOLDED]
18579 [EXCERPT]
18580 a1
18581 b1
18582 [EXCERPT]
18583 ˇ[FOLDED]
18584 [EXCERPT]
18585 [FOLDED]
18586 "
18587 });
18588 for _ in 0..5 {
18589 cx.simulate_keystroke("down");
18590 cx.assert_excerpts_with_selections(indoc! {"
18591 [EXCERPT]
18592 [FOLDED]
18593 [EXCERPT]
18594 a1
18595 b1
18596 [EXCERPT]
18597 [FOLDED]
18598 [EXCERPT]
18599 ˇ[FOLDED]
18600 "
18601 });
18602 }
18603
18604 cx.simulate_keystroke("up");
18605 cx.assert_excerpts_with_selections(indoc! {"
18606 [EXCERPT]
18607 [FOLDED]
18608 [EXCERPT]
18609 a1
18610 b1
18611 [EXCERPT]
18612 ˇ[FOLDED]
18613 [EXCERPT]
18614 [FOLDED]
18615 "
18616 });
18617 cx.simulate_keystroke("up");
18618 cx.assert_excerpts_with_selections(indoc! {"
18619 [EXCERPT]
18620 [FOLDED]
18621 [EXCERPT]
18622 a1
18623 b1
18624 ˇ[EXCERPT]
18625 [FOLDED]
18626 [EXCERPT]
18627 [FOLDED]
18628 "
18629 });
18630 cx.simulate_keystroke("up");
18631 cx.assert_excerpts_with_selections(indoc! {"
18632 [EXCERPT]
18633 [FOLDED]
18634 [EXCERPT]
18635 a1
18636 ˇb1
18637 [EXCERPT]
18638 [FOLDED]
18639 [EXCERPT]
18640 [FOLDED]
18641 "
18642 });
18643 cx.simulate_keystroke("up");
18644 cx.assert_excerpts_with_selections(indoc! {"
18645 [EXCERPT]
18646 [FOLDED]
18647 [EXCERPT]
18648 ˇa1
18649 b1
18650 [EXCERPT]
18651 [FOLDED]
18652 [EXCERPT]
18653 [FOLDED]
18654 "
18655 });
18656 for _ in 0..5 {
18657 cx.simulate_keystroke("up");
18658 cx.assert_excerpts_with_selections(indoc! {"
18659 [EXCERPT]
18660 ˇ[FOLDED]
18661 [EXCERPT]
18662 a1
18663 b1
18664 [EXCERPT]
18665 [FOLDED]
18666 [EXCERPT]
18667 [FOLDED]
18668 "
18669 });
18670 }
18671}
18672
18673#[gpui::test]
18674async fn test_inline_completion_text(cx: &mut TestAppContext) {
18675 init_test(cx, |_| {});
18676
18677 // Simple insertion
18678 assert_highlighted_edits(
18679 "Hello, world!",
18680 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18681 true,
18682 cx,
18683 |highlighted_edits, cx| {
18684 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18685 assert_eq!(highlighted_edits.highlights.len(), 1);
18686 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18687 assert_eq!(
18688 highlighted_edits.highlights[0].1.background_color,
18689 Some(cx.theme().status().created_background)
18690 );
18691 },
18692 )
18693 .await;
18694
18695 // Replacement
18696 assert_highlighted_edits(
18697 "This is a test.",
18698 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18699 false,
18700 cx,
18701 |highlighted_edits, cx| {
18702 assert_eq!(highlighted_edits.text, "That is a test.");
18703 assert_eq!(highlighted_edits.highlights.len(), 1);
18704 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18705 assert_eq!(
18706 highlighted_edits.highlights[0].1.background_color,
18707 Some(cx.theme().status().created_background)
18708 );
18709 },
18710 )
18711 .await;
18712
18713 // Multiple edits
18714 assert_highlighted_edits(
18715 "Hello, world!",
18716 vec![
18717 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18718 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18719 ],
18720 false,
18721 cx,
18722 |highlighted_edits, cx| {
18723 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18724 assert_eq!(highlighted_edits.highlights.len(), 2);
18725 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18726 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18727 assert_eq!(
18728 highlighted_edits.highlights[0].1.background_color,
18729 Some(cx.theme().status().created_background)
18730 );
18731 assert_eq!(
18732 highlighted_edits.highlights[1].1.background_color,
18733 Some(cx.theme().status().created_background)
18734 );
18735 },
18736 )
18737 .await;
18738
18739 // Multiple lines with edits
18740 assert_highlighted_edits(
18741 "First line\nSecond line\nThird line\nFourth line",
18742 vec![
18743 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18744 (
18745 Point::new(2, 0)..Point::new(2, 10),
18746 "New third line".to_string(),
18747 ),
18748 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18749 ],
18750 false,
18751 cx,
18752 |highlighted_edits, cx| {
18753 assert_eq!(
18754 highlighted_edits.text,
18755 "Second modified\nNew third line\nFourth updated line"
18756 );
18757 assert_eq!(highlighted_edits.highlights.len(), 3);
18758 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18759 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18760 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18761 for highlight in &highlighted_edits.highlights {
18762 assert_eq!(
18763 highlight.1.background_color,
18764 Some(cx.theme().status().created_background)
18765 );
18766 }
18767 },
18768 )
18769 .await;
18770}
18771
18772#[gpui::test]
18773async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18774 init_test(cx, |_| {});
18775
18776 // Deletion
18777 assert_highlighted_edits(
18778 "Hello, world!",
18779 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18780 true,
18781 cx,
18782 |highlighted_edits, cx| {
18783 assert_eq!(highlighted_edits.text, "Hello, world!");
18784 assert_eq!(highlighted_edits.highlights.len(), 1);
18785 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18786 assert_eq!(
18787 highlighted_edits.highlights[0].1.background_color,
18788 Some(cx.theme().status().deleted_background)
18789 );
18790 },
18791 )
18792 .await;
18793
18794 // Insertion
18795 assert_highlighted_edits(
18796 "Hello, world!",
18797 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18798 true,
18799 cx,
18800 |highlighted_edits, cx| {
18801 assert_eq!(highlighted_edits.highlights.len(), 1);
18802 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18803 assert_eq!(
18804 highlighted_edits.highlights[0].1.background_color,
18805 Some(cx.theme().status().created_background)
18806 );
18807 },
18808 )
18809 .await;
18810}
18811
18812async fn assert_highlighted_edits(
18813 text: &str,
18814 edits: Vec<(Range<Point>, String)>,
18815 include_deletions: bool,
18816 cx: &mut TestAppContext,
18817 assertion_fn: impl Fn(HighlightedText, &App),
18818) {
18819 let window = cx.add_window(|window, cx| {
18820 let buffer = MultiBuffer::build_simple(text, cx);
18821 Editor::new(EditorMode::full(), buffer, None, window, cx)
18822 });
18823 let cx = &mut VisualTestContext::from_window(*window, cx);
18824
18825 let (buffer, snapshot) = window
18826 .update(cx, |editor, _window, cx| {
18827 (
18828 editor.buffer().clone(),
18829 editor.buffer().read(cx).snapshot(cx),
18830 )
18831 })
18832 .unwrap();
18833
18834 let edits = edits
18835 .into_iter()
18836 .map(|(range, edit)| {
18837 (
18838 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18839 edit,
18840 )
18841 })
18842 .collect::<Vec<_>>();
18843
18844 let text_anchor_edits = edits
18845 .clone()
18846 .into_iter()
18847 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18848 .collect::<Vec<_>>();
18849
18850 let edit_preview = window
18851 .update(cx, |_, _window, cx| {
18852 buffer
18853 .read(cx)
18854 .as_singleton()
18855 .unwrap()
18856 .read(cx)
18857 .preview_edits(text_anchor_edits.into(), cx)
18858 })
18859 .unwrap()
18860 .await;
18861
18862 cx.update(|_window, cx| {
18863 let highlighted_edits = inline_completion_edit_text(
18864 &snapshot.as_singleton().unwrap().2,
18865 &edits,
18866 &edit_preview,
18867 include_deletions,
18868 cx,
18869 );
18870 assertion_fn(highlighted_edits, cx)
18871 });
18872}
18873
18874#[track_caller]
18875fn assert_breakpoint(
18876 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18877 path: &Arc<Path>,
18878 expected: Vec<(u32, Breakpoint)>,
18879) {
18880 if expected.len() == 0usize {
18881 assert!(!breakpoints.contains_key(path), "{}", path.display());
18882 } else {
18883 let mut breakpoint = breakpoints
18884 .get(path)
18885 .unwrap()
18886 .into_iter()
18887 .map(|breakpoint| {
18888 (
18889 breakpoint.row,
18890 Breakpoint {
18891 message: breakpoint.message.clone(),
18892 state: breakpoint.state,
18893 condition: breakpoint.condition.clone(),
18894 hit_condition: breakpoint.hit_condition.clone(),
18895 },
18896 )
18897 })
18898 .collect::<Vec<_>>();
18899
18900 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18901
18902 assert_eq!(expected, breakpoint);
18903 }
18904}
18905
18906fn add_log_breakpoint_at_cursor(
18907 editor: &mut Editor,
18908 log_message: &str,
18909 window: &mut Window,
18910 cx: &mut Context<Editor>,
18911) {
18912 let (anchor, bp) = editor
18913 .breakpoints_at_cursors(window, cx)
18914 .first()
18915 .and_then(|(anchor, bp)| {
18916 if let Some(bp) = bp {
18917 Some((*anchor, bp.clone()))
18918 } else {
18919 None
18920 }
18921 })
18922 .unwrap_or_else(|| {
18923 let cursor_position: Point = editor.selections.newest(cx).head();
18924
18925 let breakpoint_position = editor
18926 .snapshot(window, cx)
18927 .display_snapshot
18928 .buffer_snapshot
18929 .anchor_before(Point::new(cursor_position.row, 0));
18930
18931 (breakpoint_position, Breakpoint::new_log(&log_message))
18932 });
18933
18934 editor.edit_breakpoint_at_anchor(
18935 anchor,
18936 bp,
18937 BreakpointEditAction::EditLogMessage(log_message.into()),
18938 cx,
18939 );
18940}
18941
18942#[gpui::test]
18943async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18944 init_test(cx, |_| {});
18945
18946 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18947 let fs = FakeFs::new(cx.executor());
18948 fs.insert_tree(
18949 path!("/a"),
18950 json!({
18951 "main.rs": sample_text,
18952 }),
18953 )
18954 .await;
18955 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18956 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18957 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18958
18959 let fs = FakeFs::new(cx.executor());
18960 fs.insert_tree(
18961 path!("/a"),
18962 json!({
18963 "main.rs": sample_text,
18964 }),
18965 )
18966 .await;
18967 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18968 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18969 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18970 let worktree_id = workspace
18971 .update(cx, |workspace, _window, cx| {
18972 workspace.project().update(cx, |project, cx| {
18973 project.worktrees(cx).next().unwrap().read(cx).id()
18974 })
18975 })
18976 .unwrap();
18977
18978 let buffer = project
18979 .update(cx, |project, cx| {
18980 project.open_buffer((worktree_id, "main.rs"), cx)
18981 })
18982 .await
18983 .unwrap();
18984
18985 let (editor, cx) = cx.add_window_view(|window, cx| {
18986 Editor::new(
18987 EditorMode::full(),
18988 MultiBuffer::build_from_buffer(buffer, cx),
18989 Some(project.clone()),
18990 window,
18991 cx,
18992 )
18993 });
18994
18995 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18996 let abs_path = project.read_with(cx, |project, cx| {
18997 project
18998 .absolute_path(&project_path, cx)
18999 .map(|path_buf| Arc::from(path_buf.to_owned()))
19000 .unwrap()
19001 });
19002
19003 // assert we can add breakpoint on the first line
19004 editor.update_in(cx, |editor, window, cx| {
19005 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19006 editor.move_to_end(&MoveToEnd, window, cx);
19007 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19008 });
19009
19010 let breakpoints = editor.update(cx, |editor, cx| {
19011 editor
19012 .breakpoint_store()
19013 .as_ref()
19014 .unwrap()
19015 .read(cx)
19016 .all_source_breakpoints(cx)
19017 .clone()
19018 });
19019
19020 assert_eq!(1, breakpoints.len());
19021 assert_breakpoint(
19022 &breakpoints,
19023 &abs_path,
19024 vec![
19025 (0, Breakpoint::new_standard()),
19026 (3, Breakpoint::new_standard()),
19027 ],
19028 );
19029
19030 editor.update_in(cx, |editor, window, cx| {
19031 editor.move_to_beginning(&MoveToBeginning, window, cx);
19032 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19033 });
19034
19035 let breakpoints = editor.update(cx, |editor, cx| {
19036 editor
19037 .breakpoint_store()
19038 .as_ref()
19039 .unwrap()
19040 .read(cx)
19041 .all_source_breakpoints(cx)
19042 .clone()
19043 });
19044
19045 assert_eq!(1, breakpoints.len());
19046 assert_breakpoint(
19047 &breakpoints,
19048 &abs_path,
19049 vec![(3, Breakpoint::new_standard())],
19050 );
19051
19052 editor.update_in(cx, |editor, window, cx| {
19053 editor.move_to_end(&MoveToEnd, window, cx);
19054 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19055 });
19056
19057 let breakpoints = editor.update(cx, |editor, cx| {
19058 editor
19059 .breakpoint_store()
19060 .as_ref()
19061 .unwrap()
19062 .read(cx)
19063 .all_source_breakpoints(cx)
19064 .clone()
19065 });
19066
19067 assert_eq!(0, breakpoints.len());
19068 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19069}
19070
19071#[gpui::test]
19072async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19073 init_test(cx, |_| {});
19074
19075 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19076
19077 let fs = FakeFs::new(cx.executor());
19078 fs.insert_tree(
19079 path!("/a"),
19080 json!({
19081 "main.rs": sample_text,
19082 }),
19083 )
19084 .await;
19085 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19086 let (workspace, cx) =
19087 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19088
19089 let worktree_id = workspace.update(cx, |workspace, cx| {
19090 workspace.project().update(cx, |project, cx| {
19091 project.worktrees(cx).next().unwrap().read(cx).id()
19092 })
19093 });
19094
19095 let buffer = project
19096 .update(cx, |project, cx| {
19097 project.open_buffer((worktree_id, "main.rs"), cx)
19098 })
19099 .await
19100 .unwrap();
19101
19102 let (editor, cx) = cx.add_window_view(|window, cx| {
19103 Editor::new(
19104 EditorMode::full(),
19105 MultiBuffer::build_from_buffer(buffer, cx),
19106 Some(project.clone()),
19107 window,
19108 cx,
19109 )
19110 });
19111
19112 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19113 let abs_path = project.read_with(cx, |project, cx| {
19114 project
19115 .absolute_path(&project_path, cx)
19116 .map(|path_buf| Arc::from(path_buf.to_owned()))
19117 .unwrap()
19118 });
19119
19120 editor.update_in(cx, |editor, window, cx| {
19121 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19122 });
19123
19124 let breakpoints = editor.update(cx, |editor, cx| {
19125 editor
19126 .breakpoint_store()
19127 .as_ref()
19128 .unwrap()
19129 .read(cx)
19130 .all_source_breakpoints(cx)
19131 .clone()
19132 });
19133
19134 assert_breakpoint(
19135 &breakpoints,
19136 &abs_path,
19137 vec![(0, Breakpoint::new_log("hello world"))],
19138 );
19139
19140 // Removing a log message from a log breakpoint should remove it
19141 editor.update_in(cx, |editor, window, cx| {
19142 add_log_breakpoint_at_cursor(editor, "", window, cx);
19143 });
19144
19145 let breakpoints = editor.update(cx, |editor, cx| {
19146 editor
19147 .breakpoint_store()
19148 .as_ref()
19149 .unwrap()
19150 .read(cx)
19151 .all_source_breakpoints(cx)
19152 .clone()
19153 });
19154
19155 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19156
19157 editor.update_in(cx, |editor, window, cx| {
19158 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19159 editor.move_to_end(&MoveToEnd, window, cx);
19160 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19161 // Not adding a log message to a standard breakpoint shouldn't remove it
19162 add_log_breakpoint_at_cursor(editor, "", window, cx);
19163 });
19164
19165 let breakpoints = editor.update(cx, |editor, cx| {
19166 editor
19167 .breakpoint_store()
19168 .as_ref()
19169 .unwrap()
19170 .read(cx)
19171 .all_source_breakpoints(cx)
19172 .clone()
19173 });
19174
19175 assert_breakpoint(
19176 &breakpoints,
19177 &abs_path,
19178 vec![
19179 (0, Breakpoint::new_standard()),
19180 (3, Breakpoint::new_standard()),
19181 ],
19182 );
19183
19184 editor.update_in(cx, |editor, window, cx| {
19185 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19186 });
19187
19188 let breakpoints = editor.update(cx, |editor, cx| {
19189 editor
19190 .breakpoint_store()
19191 .as_ref()
19192 .unwrap()
19193 .read(cx)
19194 .all_source_breakpoints(cx)
19195 .clone()
19196 });
19197
19198 assert_breakpoint(
19199 &breakpoints,
19200 &abs_path,
19201 vec![
19202 (0, Breakpoint::new_standard()),
19203 (3, Breakpoint::new_log("hello world")),
19204 ],
19205 );
19206
19207 editor.update_in(cx, |editor, window, cx| {
19208 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19209 });
19210
19211 let breakpoints = editor.update(cx, |editor, cx| {
19212 editor
19213 .breakpoint_store()
19214 .as_ref()
19215 .unwrap()
19216 .read(cx)
19217 .all_source_breakpoints(cx)
19218 .clone()
19219 });
19220
19221 assert_breakpoint(
19222 &breakpoints,
19223 &abs_path,
19224 vec![
19225 (0, Breakpoint::new_standard()),
19226 (3, Breakpoint::new_log("hello Earth!!")),
19227 ],
19228 );
19229}
19230
19231/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19232/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19233/// or when breakpoints were placed out of order. This tests for a regression too
19234#[gpui::test]
19235async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19236 init_test(cx, |_| {});
19237
19238 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19239 let fs = FakeFs::new(cx.executor());
19240 fs.insert_tree(
19241 path!("/a"),
19242 json!({
19243 "main.rs": sample_text,
19244 }),
19245 )
19246 .await;
19247 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19248 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19249 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19250
19251 let fs = FakeFs::new(cx.executor());
19252 fs.insert_tree(
19253 path!("/a"),
19254 json!({
19255 "main.rs": sample_text,
19256 }),
19257 )
19258 .await;
19259 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19260 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19261 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19262 let worktree_id = workspace
19263 .update(cx, |workspace, _window, cx| {
19264 workspace.project().update(cx, |project, cx| {
19265 project.worktrees(cx).next().unwrap().read(cx).id()
19266 })
19267 })
19268 .unwrap();
19269
19270 let buffer = project
19271 .update(cx, |project, cx| {
19272 project.open_buffer((worktree_id, "main.rs"), cx)
19273 })
19274 .await
19275 .unwrap();
19276
19277 let (editor, cx) = cx.add_window_view(|window, cx| {
19278 Editor::new(
19279 EditorMode::full(),
19280 MultiBuffer::build_from_buffer(buffer, cx),
19281 Some(project.clone()),
19282 window,
19283 cx,
19284 )
19285 });
19286
19287 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19288 let abs_path = project.read_with(cx, |project, cx| {
19289 project
19290 .absolute_path(&project_path, cx)
19291 .map(|path_buf| Arc::from(path_buf.to_owned()))
19292 .unwrap()
19293 });
19294
19295 // assert we can add breakpoint on the first line
19296 editor.update_in(cx, |editor, window, cx| {
19297 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19298 editor.move_to_end(&MoveToEnd, window, cx);
19299 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19300 editor.move_up(&MoveUp, window, cx);
19301 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19302 });
19303
19304 let breakpoints = editor.update(cx, |editor, cx| {
19305 editor
19306 .breakpoint_store()
19307 .as_ref()
19308 .unwrap()
19309 .read(cx)
19310 .all_source_breakpoints(cx)
19311 .clone()
19312 });
19313
19314 assert_eq!(1, breakpoints.len());
19315 assert_breakpoint(
19316 &breakpoints,
19317 &abs_path,
19318 vec![
19319 (0, Breakpoint::new_standard()),
19320 (2, Breakpoint::new_standard()),
19321 (3, Breakpoint::new_standard()),
19322 ],
19323 );
19324
19325 editor.update_in(cx, |editor, window, cx| {
19326 editor.move_to_beginning(&MoveToBeginning, window, cx);
19327 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19328 editor.move_to_end(&MoveToEnd, window, cx);
19329 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19330 // Disabling a breakpoint that doesn't exist should do nothing
19331 editor.move_up(&MoveUp, window, cx);
19332 editor.move_up(&MoveUp, window, cx);
19333 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19334 });
19335
19336 let breakpoints = editor.update(cx, |editor, cx| {
19337 editor
19338 .breakpoint_store()
19339 .as_ref()
19340 .unwrap()
19341 .read(cx)
19342 .all_source_breakpoints(cx)
19343 .clone()
19344 });
19345
19346 let disable_breakpoint = {
19347 let mut bp = Breakpoint::new_standard();
19348 bp.state = BreakpointState::Disabled;
19349 bp
19350 };
19351
19352 assert_eq!(1, breakpoints.len());
19353 assert_breakpoint(
19354 &breakpoints,
19355 &abs_path,
19356 vec![
19357 (0, disable_breakpoint.clone()),
19358 (2, Breakpoint::new_standard()),
19359 (3, disable_breakpoint.clone()),
19360 ],
19361 );
19362
19363 editor.update_in(cx, |editor, window, cx| {
19364 editor.move_to_beginning(&MoveToBeginning, window, cx);
19365 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19366 editor.move_to_end(&MoveToEnd, window, cx);
19367 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19368 editor.move_up(&MoveUp, window, cx);
19369 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19370 });
19371
19372 let breakpoints = editor.update(cx, |editor, cx| {
19373 editor
19374 .breakpoint_store()
19375 .as_ref()
19376 .unwrap()
19377 .read(cx)
19378 .all_source_breakpoints(cx)
19379 .clone()
19380 });
19381
19382 assert_eq!(1, breakpoints.len());
19383 assert_breakpoint(
19384 &breakpoints,
19385 &abs_path,
19386 vec![
19387 (0, Breakpoint::new_standard()),
19388 (2, disable_breakpoint),
19389 (3, Breakpoint::new_standard()),
19390 ],
19391 );
19392}
19393
19394#[gpui::test]
19395async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19396 init_test(cx, |_| {});
19397 let capabilities = lsp::ServerCapabilities {
19398 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19399 prepare_provider: Some(true),
19400 work_done_progress_options: Default::default(),
19401 })),
19402 ..Default::default()
19403 };
19404 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19405
19406 cx.set_state(indoc! {"
19407 struct Fˇoo {}
19408 "});
19409
19410 cx.update_editor(|editor, _, cx| {
19411 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19412 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19413 editor.highlight_background::<DocumentHighlightRead>(
19414 &[highlight_range],
19415 |c| c.editor_document_highlight_read_background,
19416 cx,
19417 );
19418 });
19419
19420 let mut prepare_rename_handler = cx
19421 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19422 move |_, _, _| async move {
19423 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19424 start: lsp::Position {
19425 line: 0,
19426 character: 7,
19427 },
19428 end: lsp::Position {
19429 line: 0,
19430 character: 10,
19431 },
19432 })))
19433 },
19434 );
19435 let prepare_rename_task = cx
19436 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19437 .expect("Prepare rename was not started");
19438 prepare_rename_handler.next().await.unwrap();
19439 prepare_rename_task.await.expect("Prepare rename failed");
19440
19441 let mut rename_handler =
19442 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19443 let edit = lsp::TextEdit {
19444 range: lsp::Range {
19445 start: lsp::Position {
19446 line: 0,
19447 character: 7,
19448 },
19449 end: lsp::Position {
19450 line: 0,
19451 character: 10,
19452 },
19453 },
19454 new_text: "FooRenamed".to_string(),
19455 };
19456 Ok(Some(lsp::WorkspaceEdit::new(
19457 // Specify the same edit twice
19458 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19459 )))
19460 });
19461 let rename_task = cx
19462 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19463 .expect("Confirm rename was not started");
19464 rename_handler.next().await.unwrap();
19465 rename_task.await.expect("Confirm rename failed");
19466 cx.run_until_parked();
19467
19468 // Despite two edits, only one is actually applied as those are identical
19469 cx.assert_editor_state(indoc! {"
19470 struct FooRenamedˇ {}
19471 "});
19472}
19473
19474#[gpui::test]
19475async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19476 init_test(cx, |_| {});
19477 // These capabilities indicate that the server does not support prepare rename.
19478 let capabilities = lsp::ServerCapabilities {
19479 rename_provider: Some(lsp::OneOf::Left(true)),
19480 ..Default::default()
19481 };
19482 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19483
19484 cx.set_state(indoc! {"
19485 struct Fˇoo {}
19486 "});
19487
19488 cx.update_editor(|editor, _window, cx| {
19489 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19490 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19491 editor.highlight_background::<DocumentHighlightRead>(
19492 &[highlight_range],
19493 |c| c.editor_document_highlight_read_background,
19494 cx,
19495 );
19496 });
19497
19498 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19499 .expect("Prepare rename was not started")
19500 .await
19501 .expect("Prepare rename failed");
19502
19503 let mut rename_handler =
19504 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19505 let edit = lsp::TextEdit {
19506 range: lsp::Range {
19507 start: lsp::Position {
19508 line: 0,
19509 character: 7,
19510 },
19511 end: lsp::Position {
19512 line: 0,
19513 character: 10,
19514 },
19515 },
19516 new_text: "FooRenamed".to_string(),
19517 };
19518 Ok(Some(lsp::WorkspaceEdit::new(
19519 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19520 )))
19521 });
19522 let rename_task = cx
19523 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19524 .expect("Confirm rename was not started");
19525 rename_handler.next().await.unwrap();
19526 rename_task.await.expect("Confirm rename failed");
19527 cx.run_until_parked();
19528
19529 // Correct range is renamed, as `surrounding_word` is used to find it.
19530 cx.assert_editor_state(indoc! {"
19531 struct FooRenamedˇ {}
19532 "});
19533}
19534
19535#[gpui::test]
19536async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19537 init_test(cx, |_| {});
19538 let mut cx = EditorTestContext::new(cx).await;
19539
19540 let language = Arc::new(
19541 Language::new(
19542 LanguageConfig::default(),
19543 Some(tree_sitter_html::LANGUAGE.into()),
19544 )
19545 .with_brackets_query(
19546 r#"
19547 ("<" @open "/>" @close)
19548 ("</" @open ">" @close)
19549 ("<" @open ">" @close)
19550 ("\"" @open "\"" @close)
19551 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19552 "#,
19553 )
19554 .unwrap(),
19555 );
19556 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19557
19558 cx.set_state(indoc! {"
19559 <span>ˇ</span>
19560 "});
19561 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19562 cx.assert_editor_state(indoc! {"
19563 <span>
19564 ˇ
19565 </span>
19566 "});
19567
19568 cx.set_state(indoc! {"
19569 <span><span></span>ˇ</span>
19570 "});
19571 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19572 cx.assert_editor_state(indoc! {"
19573 <span><span></span>
19574 ˇ</span>
19575 "});
19576
19577 cx.set_state(indoc! {"
19578 <span>ˇ
19579 </span>
19580 "});
19581 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19582 cx.assert_editor_state(indoc! {"
19583 <span>
19584 ˇ
19585 </span>
19586 "});
19587}
19588
19589#[gpui::test(iterations = 10)]
19590async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19591 init_test(cx, |_| {});
19592
19593 let fs = FakeFs::new(cx.executor());
19594 fs.insert_tree(
19595 path!("/dir"),
19596 json!({
19597 "a.ts": "a",
19598 }),
19599 )
19600 .await;
19601
19602 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19603 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19604 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19605
19606 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19607 language_registry.add(Arc::new(Language::new(
19608 LanguageConfig {
19609 name: "TypeScript".into(),
19610 matcher: LanguageMatcher {
19611 path_suffixes: vec!["ts".to_string()],
19612 ..Default::default()
19613 },
19614 ..Default::default()
19615 },
19616 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19617 )));
19618 let mut fake_language_servers = language_registry.register_fake_lsp(
19619 "TypeScript",
19620 FakeLspAdapter {
19621 capabilities: lsp::ServerCapabilities {
19622 code_lens_provider: Some(lsp::CodeLensOptions {
19623 resolve_provider: Some(true),
19624 }),
19625 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19626 commands: vec!["_the/command".to_string()],
19627 ..lsp::ExecuteCommandOptions::default()
19628 }),
19629 ..lsp::ServerCapabilities::default()
19630 },
19631 ..FakeLspAdapter::default()
19632 },
19633 );
19634
19635 let (buffer, _handle) = project
19636 .update(cx, |p, cx| {
19637 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19638 })
19639 .await
19640 .unwrap();
19641 cx.executor().run_until_parked();
19642
19643 let fake_server = fake_language_servers.next().await.unwrap();
19644
19645 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19646 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19647 drop(buffer_snapshot);
19648 let actions = cx
19649 .update_window(*workspace, |_, window, cx| {
19650 project.code_actions(&buffer, anchor..anchor, window, cx)
19651 })
19652 .unwrap();
19653
19654 fake_server
19655 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19656 Ok(Some(vec![
19657 lsp::CodeLens {
19658 range: lsp::Range::default(),
19659 command: Some(lsp::Command {
19660 title: "Code lens command".to_owned(),
19661 command: "_the/command".to_owned(),
19662 arguments: None,
19663 }),
19664 data: None,
19665 },
19666 lsp::CodeLens {
19667 range: lsp::Range::default(),
19668 command: Some(lsp::Command {
19669 title: "Command not in capabilities".to_owned(),
19670 command: "not in capabilities".to_owned(),
19671 arguments: None,
19672 }),
19673 data: None,
19674 },
19675 lsp::CodeLens {
19676 range: lsp::Range {
19677 start: lsp::Position {
19678 line: 1,
19679 character: 1,
19680 },
19681 end: lsp::Position {
19682 line: 1,
19683 character: 1,
19684 },
19685 },
19686 command: Some(lsp::Command {
19687 title: "Command not in range".to_owned(),
19688 command: "_the/command".to_owned(),
19689 arguments: None,
19690 }),
19691 data: None,
19692 },
19693 ]))
19694 })
19695 .next()
19696 .await;
19697
19698 let actions = actions.await.unwrap();
19699 assert_eq!(
19700 actions.len(),
19701 1,
19702 "Should have only one valid action for the 0..0 range"
19703 );
19704 let action = actions[0].clone();
19705 let apply = project.update(cx, |project, cx| {
19706 project.apply_code_action(buffer.clone(), action, true, cx)
19707 });
19708
19709 // Resolving the code action does not populate its edits. In absence of
19710 // edits, we must execute the given command.
19711 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19712 |mut lens, _| async move {
19713 let lens_command = lens.command.as_mut().expect("should have a command");
19714 assert_eq!(lens_command.title, "Code lens command");
19715 lens_command.arguments = Some(vec![json!("the-argument")]);
19716 Ok(lens)
19717 },
19718 );
19719
19720 // While executing the command, the language server sends the editor
19721 // a `workspaceEdit` request.
19722 fake_server
19723 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19724 let fake = fake_server.clone();
19725 move |params, _| {
19726 assert_eq!(params.command, "_the/command");
19727 let fake = fake.clone();
19728 async move {
19729 fake.server
19730 .request::<lsp::request::ApplyWorkspaceEdit>(
19731 lsp::ApplyWorkspaceEditParams {
19732 label: None,
19733 edit: lsp::WorkspaceEdit {
19734 changes: Some(
19735 [(
19736 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19737 vec![lsp::TextEdit {
19738 range: lsp::Range::new(
19739 lsp::Position::new(0, 0),
19740 lsp::Position::new(0, 0),
19741 ),
19742 new_text: "X".into(),
19743 }],
19744 )]
19745 .into_iter()
19746 .collect(),
19747 ),
19748 ..Default::default()
19749 },
19750 },
19751 )
19752 .await
19753 .into_response()
19754 .unwrap();
19755 Ok(Some(json!(null)))
19756 }
19757 }
19758 })
19759 .next()
19760 .await;
19761
19762 // Applying the code lens command returns a project transaction containing the edits
19763 // sent by the language server in its `workspaceEdit` request.
19764 let transaction = apply.await.unwrap();
19765 assert!(transaction.0.contains_key(&buffer));
19766 buffer.update(cx, |buffer, cx| {
19767 assert_eq!(buffer.text(), "Xa");
19768 buffer.undo(cx);
19769 assert_eq!(buffer.text(), "a");
19770 });
19771}
19772
19773#[gpui::test]
19774async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19775 init_test(cx, |_| {});
19776
19777 let fs = FakeFs::new(cx.executor());
19778 let main_text = r#"fn main() {
19779println!("1");
19780println!("2");
19781println!("3");
19782println!("4");
19783println!("5");
19784}"#;
19785 let lib_text = "mod foo {}";
19786 fs.insert_tree(
19787 path!("/a"),
19788 json!({
19789 "lib.rs": lib_text,
19790 "main.rs": main_text,
19791 }),
19792 )
19793 .await;
19794
19795 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19796 let (workspace, cx) =
19797 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19798 let worktree_id = workspace.update(cx, |workspace, cx| {
19799 workspace.project().update(cx, |project, cx| {
19800 project.worktrees(cx).next().unwrap().read(cx).id()
19801 })
19802 });
19803
19804 let expected_ranges = vec![
19805 Point::new(0, 0)..Point::new(0, 0),
19806 Point::new(1, 0)..Point::new(1, 1),
19807 Point::new(2, 0)..Point::new(2, 2),
19808 Point::new(3, 0)..Point::new(3, 3),
19809 ];
19810
19811 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19812 let editor_1 = workspace
19813 .update_in(cx, |workspace, window, cx| {
19814 workspace.open_path(
19815 (worktree_id, "main.rs"),
19816 Some(pane_1.downgrade()),
19817 true,
19818 window,
19819 cx,
19820 )
19821 })
19822 .unwrap()
19823 .await
19824 .downcast::<Editor>()
19825 .unwrap();
19826 pane_1.update(cx, |pane, cx| {
19827 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19828 open_editor.update(cx, |editor, cx| {
19829 assert_eq!(
19830 editor.display_text(cx),
19831 main_text,
19832 "Original main.rs text on initial open",
19833 );
19834 assert_eq!(
19835 editor
19836 .selections
19837 .all::<Point>(cx)
19838 .into_iter()
19839 .map(|s| s.range())
19840 .collect::<Vec<_>>(),
19841 vec![Point::zero()..Point::zero()],
19842 "Default selections on initial open",
19843 );
19844 })
19845 });
19846 editor_1.update_in(cx, |editor, window, cx| {
19847 editor.change_selections(None, window, cx, |s| {
19848 s.select_ranges(expected_ranges.clone());
19849 });
19850 });
19851
19852 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19853 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19854 });
19855 let editor_2 = workspace
19856 .update_in(cx, |workspace, window, cx| {
19857 workspace.open_path(
19858 (worktree_id, "main.rs"),
19859 Some(pane_2.downgrade()),
19860 true,
19861 window,
19862 cx,
19863 )
19864 })
19865 .unwrap()
19866 .await
19867 .downcast::<Editor>()
19868 .unwrap();
19869 pane_2.update(cx, |pane, cx| {
19870 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19871 open_editor.update(cx, |editor, cx| {
19872 assert_eq!(
19873 editor.display_text(cx),
19874 main_text,
19875 "Original main.rs text on initial open in another panel",
19876 );
19877 assert_eq!(
19878 editor
19879 .selections
19880 .all::<Point>(cx)
19881 .into_iter()
19882 .map(|s| s.range())
19883 .collect::<Vec<_>>(),
19884 vec![Point::zero()..Point::zero()],
19885 "Default selections on initial open in another panel",
19886 );
19887 })
19888 });
19889
19890 editor_2.update_in(cx, |editor, window, cx| {
19891 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19892 });
19893
19894 let _other_editor_1 = workspace
19895 .update_in(cx, |workspace, window, cx| {
19896 workspace.open_path(
19897 (worktree_id, "lib.rs"),
19898 Some(pane_1.downgrade()),
19899 true,
19900 window,
19901 cx,
19902 )
19903 })
19904 .unwrap()
19905 .await
19906 .downcast::<Editor>()
19907 .unwrap();
19908 pane_1
19909 .update_in(cx, |pane, window, cx| {
19910 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19911 .unwrap()
19912 })
19913 .await
19914 .unwrap();
19915 drop(editor_1);
19916 pane_1.update(cx, |pane, cx| {
19917 pane.active_item()
19918 .unwrap()
19919 .downcast::<Editor>()
19920 .unwrap()
19921 .update(cx, |editor, cx| {
19922 assert_eq!(
19923 editor.display_text(cx),
19924 lib_text,
19925 "Other file should be open and active",
19926 );
19927 });
19928 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19929 });
19930
19931 let _other_editor_2 = workspace
19932 .update_in(cx, |workspace, window, cx| {
19933 workspace.open_path(
19934 (worktree_id, "lib.rs"),
19935 Some(pane_2.downgrade()),
19936 true,
19937 window,
19938 cx,
19939 )
19940 })
19941 .unwrap()
19942 .await
19943 .downcast::<Editor>()
19944 .unwrap();
19945 pane_2
19946 .update_in(cx, |pane, window, cx| {
19947 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19948 .unwrap()
19949 })
19950 .await
19951 .unwrap();
19952 drop(editor_2);
19953 pane_2.update(cx, |pane, cx| {
19954 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19955 open_editor.update(cx, |editor, cx| {
19956 assert_eq!(
19957 editor.display_text(cx),
19958 lib_text,
19959 "Other file should be open and active in another panel too",
19960 );
19961 });
19962 assert_eq!(
19963 pane.items().count(),
19964 1,
19965 "No other editors should be open in another pane",
19966 );
19967 });
19968
19969 let _editor_1_reopened = workspace
19970 .update_in(cx, |workspace, window, cx| {
19971 workspace.open_path(
19972 (worktree_id, "main.rs"),
19973 Some(pane_1.downgrade()),
19974 true,
19975 window,
19976 cx,
19977 )
19978 })
19979 .unwrap()
19980 .await
19981 .downcast::<Editor>()
19982 .unwrap();
19983 let _editor_2_reopened = workspace
19984 .update_in(cx, |workspace, window, cx| {
19985 workspace.open_path(
19986 (worktree_id, "main.rs"),
19987 Some(pane_2.downgrade()),
19988 true,
19989 window,
19990 cx,
19991 )
19992 })
19993 .unwrap()
19994 .await
19995 .downcast::<Editor>()
19996 .unwrap();
19997 pane_1.update(cx, |pane, cx| {
19998 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19999 open_editor.update(cx, |editor, cx| {
20000 assert_eq!(
20001 editor.display_text(cx),
20002 main_text,
20003 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20004 );
20005 assert_eq!(
20006 editor
20007 .selections
20008 .all::<Point>(cx)
20009 .into_iter()
20010 .map(|s| s.range())
20011 .collect::<Vec<_>>(),
20012 expected_ranges,
20013 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20014 );
20015 })
20016 });
20017 pane_2.update(cx, |pane, cx| {
20018 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20019 open_editor.update(cx, |editor, cx| {
20020 assert_eq!(
20021 editor.display_text(cx),
20022 r#"fn main() {
20023⋯rintln!("1");
20024⋯intln!("2");
20025⋯ntln!("3");
20026println!("4");
20027println!("5");
20028}"#,
20029 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20030 );
20031 assert_eq!(
20032 editor
20033 .selections
20034 .all::<Point>(cx)
20035 .into_iter()
20036 .map(|s| s.range())
20037 .collect::<Vec<_>>(),
20038 vec![Point::zero()..Point::zero()],
20039 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20040 );
20041 })
20042 });
20043}
20044
20045#[gpui::test]
20046async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20047 init_test(cx, |_| {});
20048
20049 let fs = FakeFs::new(cx.executor());
20050 let main_text = r#"fn main() {
20051println!("1");
20052println!("2");
20053println!("3");
20054println!("4");
20055println!("5");
20056}"#;
20057 let lib_text = "mod foo {}";
20058 fs.insert_tree(
20059 path!("/a"),
20060 json!({
20061 "lib.rs": lib_text,
20062 "main.rs": main_text,
20063 }),
20064 )
20065 .await;
20066
20067 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20068 let (workspace, cx) =
20069 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20070 let worktree_id = workspace.update(cx, |workspace, cx| {
20071 workspace.project().update(cx, |project, cx| {
20072 project.worktrees(cx).next().unwrap().read(cx).id()
20073 })
20074 });
20075
20076 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20077 let editor = workspace
20078 .update_in(cx, |workspace, window, cx| {
20079 workspace.open_path(
20080 (worktree_id, "main.rs"),
20081 Some(pane.downgrade()),
20082 true,
20083 window,
20084 cx,
20085 )
20086 })
20087 .unwrap()
20088 .await
20089 .downcast::<Editor>()
20090 .unwrap();
20091 pane.update(cx, |pane, cx| {
20092 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20093 open_editor.update(cx, |editor, cx| {
20094 assert_eq!(
20095 editor.display_text(cx),
20096 main_text,
20097 "Original main.rs text on initial open",
20098 );
20099 })
20100 });
20101 editor.update_in(cx, |editor, window, cx| {
20102 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20103 });
20104
20105 cx.update_global(|store: &mut SettingsStore, cx| {
20106 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20107 s.restore_on_file_reopen = Some(false);
20108 });
20109 });
20110 editor.update_in(cx, |editor, window, cx| {
20111 editor.fold_ranges(
20112 vec![
20113 Point::new(1, 0)..Point::new(1, 1),
20114 Point::new(2, 0)..Point::new(2, 2),
20115 Point::new(3, 0)..Point::new(3, 3),
20116 ],
20117 false,
20118 window,
20119 cx,
20120 );
20121 });
20122 pane.update_in(cx, |pane, window, cx| {
20123 pane.close_all_items(&CloseAllItems::default(), window, cx)
20124 .unwrap()
20125 })
20126 .await
20127 .unwrap();
20128 pane.update(cx, |pane, _| {
20129 assert!(pane.active_item().is_none());
20130 });
20131 cx.update_global(|store: &mut SettingsStore, cx| {
20132 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20133 s.restore_on_file_reopen = Some(true);
20134 });
20135 });
20136
20137 let _editor_reopened = workspace
20138 .update_in(cx, |workspace, window, cx| {
20139 workspace.open_path(
20140 (worktree_id, "main.rs"),
20141 Some(pane.downgrade()),
20142 true,
20143 window,
20144 cx,
20145 )
20146 })
20147 .unwrap()
20148 .await
20149 .downcast::<Editor>()
20150 .unwrap();
20151 pane.update(cx, |pane, cx| {
20152 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20153 open_editor.update(cx, |editor, cx| {
20154 assert_eq!(
20155 editor.display_text(cx),
20156 main_text,
20157 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20158 );
20159 })
20160 });
20161}
20162
20163#[gpui::test]
20164async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20165 struct EmptyModalView {
20166 focus_handle: gpui::FocusHandle,
20167 }
20168 impl EventEmitter<DismissEvent> for EmptyModalView {}
20169 impl Render for EmptyModalView {
20170 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20171 div()
20172 }
20173 }
20174 impl Focusable for EmptyModalView {
20175 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20176 self.focus_handle.clone()
20177 }
20178 }
20179 impl workspace::ModalView for EmptyModalView {}
20180 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20181 EmptyModalView {
20182 focus_handle: cx.focus_handle(),
20183 }
20184 }
20185
20186 init_test(cx, |_| {});
20187
20188 let fs = FakeFs::new(cx.executor());
20189 let project = Project::test(fs, [], cx).await;
20190 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20191 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20192 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20193 let editor = cx.new_window_entity(|window, cx| {
20194 Editor::new(
20195 EditorMode::full(),
20196 buffer,
20197 Some(project.clone()),
20198 window,
20199 cx,
20200 )
20201 });
20202 workspace
20203 .update(cx, |workspace, window, cx| {
20204 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20205 })
20206 .unwrap();
20207 editor.update_in(cx, |editor, window, cx| {
20208 editor.open_context_menu(&OpenContextMenu, window, cx);
20209 assert!(editor.mouse_context_menu.is_some());
20210 });
20211 workspace
20212 .update(cx, |workspace, window, cx| {
20213 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20214 })
20215 .unwrap();
20216 cx.read(|cx| {
20217 assert!(editor.read(cx).mouse_context_menu.is_none());
20218 });
20219}
20220
20221#[gpui::test]
20222async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20223 init_test(cx, |_| {});
20224
20225 let fs = FakeFs::new(cx.executor());
20226 fs.insert_file(path!("/file.html"), Default::default())
20227 .await;
20228
20229 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20230
20231 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20232 let html_language = Arc::new(Language::new(
20233 LanguageConfig {
20234 name: "HTML".into(),
20235 matcher: LanguageMatcher {
20236 path_suffixes: vec!["html".to_string()],
20237 ..LanguageMatcher::default()
20238 },
20239 brackets: BracketPairConfig {
20240 pairs: vec![BracketPair {
20241 start: "<".into(),
20242 end: ">".into(),
20243 close: true,
20244 ..Default::default()
20245 }],
20246 ..Default::default()
20247 },
20248 ..Default::default()
20249 },
20250 Some(tree_sitter_html::LANGUAGE.into()),
20251 ));
20252 language_registry.add(html_language);
20253 let mut fake_servers = language_registry.register_fake_lsp(
20254 "HTML",
20255 FakeLspAdapter {
20256 capabilities: lsp::ServerCapabilities {
20257 completion_provider: Some(lsp::CompletionOptions {
20258 resolve_provider: Some(true),
20259 ..Default::default()
20260 }),
20261 ..Default::default()
20262 },
20263 ..Default::default()
20264 },
20265 );
20266
20267 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20268 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20269
20270 let worktree_id = workspace
20271 .update(cx, |workspace, _window, cx| {
20272 workspace.project().update(cx, |project, cx| {
20273 project.worktrees(cx).next().unwrap().read(cx).id()
20274 })
20275 })
20276 .unwrap();
20277 project
20278 .update(cx, |project, cx| {
20279 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20280 })
20281 .await
20282 .unwrap();
20283 let editor = workspace
20284 .update(cx, |workspace, window, cx| {
20285 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20286 })
20287 .unwrap()
20288 .await
20289 .unwrap()
20290 .downcast::<Editor>()
20291 .unwrap();
20292
20293 let fake_server = fake_servers.next().await.unwrap();
20294 editor.update_in(cx, |editor, window, cx| {
20295 editor.set_text("<ad></ad>", window, cx);
20296 editor.change_selections(None, window, cx, |selections| {
20297 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20298 });
20299 let Some((buffer, _)) = editor
20300 .buffer
20301 .read(cx)
20302 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20303 else {
20304 panic!("Failed to get buffer for selection position");
20305 };
20306 let buffer = buffer.read(cx);
20307 let buffer_id = buffer.remote_id();
20308 let opening_range =
20309 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20310 let closing_range =
20311 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20312 let mut linked_ranges = HashMap::default();
20313 linked_ranges.insert(
20314 buffer_id,
20315 vec![(opening_range.clone(), vec![closing_range.clone()])],
20316 );
20317 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20318 });
20319 let mut completion_handle =
20320 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20321 Ok(Some(lsp::CompletionResponse::Array(vec![
20322 lsp::CompletionItem {
20323 label: "head".to_string(),
20324 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20325 lsp::InsertReplaceEdit {
20326 new_text: "head".to_string(),
20327 insert: lsp::Range::new(
20328 lsp::Position::new(0, 1),
20329 lsp::Position::new(0, 3),
20330 ),
20331 replace: lsp::Range::new(
20332 lsp::Position::new(0, 1),
20333 lsp::Position::new(0, 3),
20334 ),
20335 },
20336 )),
20337 ..Default::default()
20338 },
20339 ])))
20340 });
20341 editor.update_in(cx, |editor, window, cx| {
20342 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20343 });
20344 cx.run_until_parked();
20345 completion_handle.next().await.unwrap();
20346 editor.update(cx, |editor, _| {
20347 assert!(
20348 editor.context_menu_visible(),
20349 "Completion menu should be visible"
20350 );
20351 });
20352 editor.update_in(cx, |editor, window, cx| {
20353 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20354 });
20355 cx.executor().run_until_parked();
20356 editor.update(cx, |editor, cx| {
20357 assert_eq!(editor.text(cx), "<head></head>");
20358 });
20359}
20360
20361#[gpui::test]
20362async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20363 init_test(cx, |_| {});
20364
20365 let fs = FakeFs::new(cx.executor());
20366 fs.insert_tree(
20367 path!("/root"),
20368 json!({
20369 "a": {
20370 "main.rs": "fn main() {}",
20371 },
20372 "foo": {
20373 "bar": {
20374 "external_file.rs": "pub mod external {}",
20375 }
20376 }
20377 }),
20378 )
20379 .await;
20380
20381 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20382 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20383 language_registry.add(rust_lang());
20384 let _fake_servers = language_registry.register_fake_lsp(
20385 "Rust",
20386 FakeLspAdapter {
20387 ..FakeLspAdapter::default()
20388 },
20389 );
20390 let (workspace, cx) =
20391 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20392 let worktree_id = workspace.update(cx, |workspace, cx| {
20393 workspace.project().update(cx, |project, cx| {
20394 project.worktrees(cx).next().unwrap().read(cx).id()
20395 })
20396 });
20397
20398 let assert_language_servers_count =
20399 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20400 project.update(cx, |project, cx| {
20401 let current = project
20402 .lsp_store()
20403 .read(cx)
20404 .as_local()
20405 .unwrap()
20406 .language_servers
20407 .len();
20408 assert_eq!(expected, current, "{context}");
20409 });
20410 };
20411
20412 assert_language_servers_count(
20413 0,
20414 "No servers should be running before any file is open",
20415 cx,
20416 );
20417 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20418 let main_editor = workspace
20419 .update_in(cx, |workspace, window, cx| {
20420 workspace.open_path(
20421 (worktree_id, "main.rs"),
20422 Some(pane.downgrade()),
20423 true,
20424 window,
20425 cx,
20426 )
20427 })
20428 .unwrap()
20429 .await
20430 .downcast::<Editor>()
20431 .unwrap();
20432 pane.update(cx, |pane, cx| {
20433 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20434 open_editor.update(cx, |editor, cx| {
20435 assert_eq!(
20436 editor.display_text(cx),
20437 "fn main() {}",
20438 "Original main.rs text on initial open",
20439 );
20440 });
20441 assert_eq!(open_editor, main_editor);
20442 });
20443 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20444
20445 let external_editor = workspace
20446 .update_in(cx, |workspace, window, cx| {
20447 workspace.open_abs_path(
20448 PathBuf::from("/root/foo/bar/external_file.rs"),
20449 OpenOptions::default(),
20450 window,
20451 cx,
20452 )
20453 })
20454 .await
20455 .expect("opening external file")
20456 .downcast::<Editor>()
20457 .expect("downcasted external file's open element to editor");
20458 pane.update(cx, |pane, cx| {
20459 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20460 open_editor.update(cx, |editor, cx| {
20461 assert_eq!(
20462 editor.display_text(cx),
20463 "pub mod external {}",
20464 "External file is open now",
20465 );
20466 });
20467 assert_eq!(open_editor, external_editor);
20468 });
20469 assert_language_servers_count(
20470 1,
20471 "Second, external, *.rs file should join the existing server",
20472 cx,
20473 );
20474
20475 pane.update_in(cx, |pane, window, cx| {
20476 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20477 })
20478 .unwrap()
20479 .await
20480 .unwrap();
20481 pane.update_in(cx, |pane, window, cx| {
20482 pane.navigate_backward(window, cx);
20483 });
20484 cx.run_until_parked();
20485 pane.update(cx, |pane, cx| {
20486 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20487 open_editor.update(cx, |editor, cx| {
20488 assert_eq!(
20489 editor.display_text(cx),
20490 "pub mod external {}",
20491 "External file is open now",
20492 );
20493 });
20494 });
20495 assert_language_servers_count(
20496 1,
20497 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20498 cx,
20499 );
20500
20501 cx.update(|_, cx| {
20502 workspace::reload(&workspace::Reload::default(), cx);
20503 });
20504 assert_language_servers_count(
20505 1,
20506 "After reloading the worktree with local and external files opened, only one project should be started",
20507 cx,
20508 );
20509}
20510
20511#[gpui::test]
20512async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20513 init_test(cx, |_| {});
20514
20515 let mut cx = EditorTestContext::new(cx).await;
20516 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20517 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20518
20519 // test cursor move to start of each line on tab
20520 // for `if`, `elif`, `else`, `while`, `with` and `for`
20521 cx.set_state(indoc! {"
20522 def main():
20523 ˇ for item in items:
20524 ˇ while item.active:
20525 ˇ if item.value > 10:
20526 ˇ continue
20527 ˇ elif item.value < 0:
20528 ˇ break
20529 ˇ else:
20530 ˇ with item.context() as ctx:
20531 ˇ yield count
20532 ˇ else:
20533 ˇ log('while else')
20534 ˇ else:
20535 ˇ log('for else')
20536 "});
20537 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20538 cx.assert_editor_state(indoc! {"
20539 def main():
20540 ˇfor item in items:
20541 ˇwhile item.active:
20542 ˇif item.value > 10:
20543 ˇcontinue
20544 ˇelif item.value < 0:
20545 ˇbreak
20546 ˇelse:
20547 ˇwith item.context() as ctx:
20548 ˇyield count
20549 ˇelse:
20550 ˇlog('while else')
20551 ˇelse:
20552 ˇlog('for else')
20553 "});
20554 // test relative indent is preserved when tab
20555 // for `if`, `elif`, `else`, `while`, `with` and `for`
20556 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20557 cx.assert_editor_state(indoc! {"
20558 def main():
20559 ˇfor item in items:
20560 ˇwhile item.active:
20561 ˇif item.value > 10:
20562 ˇcontinue
20563 ˇelif item.value < 0:
20564 ˇbreak
20565 ˇelse:
20566 ˇwith item.context() as ctx:
20567 ˇyield count
20568 ˇelse:
20569 ˇlog('while else')
20570 ˇelse:
20571 ˇlog('for else')
20572 "});
20573
20574 // test cursor move to start of each line on tab
20575 // for `try`, `except`, `else`, `finally`, `match` and `def`
20576 cx.set_state(indoc! {"
20577 def main():
20578 ˇ try:
20579 ˇ fetch()
20580 ˇ except ValueError:
20581 ˇ handle_error()
20582 ˇ else:
20583 ˇ match value:
20584 ˇ case _:
20585 ˇ finally:
20586 ˇ def status():
20587 ˇ return 0
20588 "});
20589 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20590 cx.assert_editor_state(indoc! {"
20591 def main():
20592 ˇtry:
20593 ˇfetch()
20594 ˇexcept ValueError:
20595 ˇhandle_error()
20596 ˇelse:
20597 ˇmatch value:
20598 ˇcase _:
20599 ˇfinally:
20600 ˇdef status():
20601 ˇreturn 0
20602 "});
20603 // test relative indent is preserved when tab
20604 // for `try`, `except`, `else`, `finally`, `match` and `def`
20605 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20606 cx.assert_editor_state(indoc! {"
20607 def main():
20608 ˇtry:
20609 ˇfetch()
20610 ˇexcept ValueError:
20611 ˇhandle_error()
20612 ˇelse:
20613 ˇmatch value:
20614 ˇcase _:
20615 ˇfinally:
20616 ˇdef status():
20617 ˇreturn 0
20618 "});
20619}
20620
20621#[gpui::test]
20622async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20623 init_test(cx, |_| {});
20624
20625 let mut cx = EditorTestContext::new(cx).await;
20626 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20627 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20628
20629 // test `else` auto outdents when typed inside `if` block
20630 cx.set_state(indoc! {"
20631 def main():
20632 if i == 2:
20633 return
20634 ˇ
20635 "});
20636 cx.update_editor(|editor, window, cx| {
20637 editor.handle_input("else:", window, cx);
20638 });
20639 cx.assert_editor_state(indoc! {"
20640 def main():
20641 if i == 2:
20642 return
20643 else:ˇ
20644 "});
20645
20646 // test `except` auto outdents when typed inside `try` block
20647 cx.set_state(indoc! {"
20648 def main():
20649 try:
20650 i = 2
20651 ˇ
20652 "});
20653 cx.update_editor(|editor, window, cx| {
20654 editor.handle_input("except:", window, cx);
20655 });
20656 cx.assert_editor_state(indoc! {"
20657 def main():
20658 try:
20659 i = 2
20660 except:ˇ
20661 "});
20662
20663 // test `else` auto outdents when typed inside `except` block
20664 cx.set_state(indoc! {"
20665 def main():
20666 try:
20667 i = 2
20668 except:
20669 j = 2
20670 ˇ
20671 "});
20672 cx.update_editor(|editor, window, cx| {
20673 editor.handle_input("else:", window, cx);
20674 });
20675 cx.assert_editor_state(indoc! {"
20676 def main():
20677 try:
20678 i = 2
20679 except:
20680 j = 2
20681 else:ˇ
20682 "});
20683
20684 // test `finally` auto outdents when typed inside `else` block
20685 cx.set_state(indoc! {"
20686 def main():
20687 try:
20688 i = 2
20689 except:
20690 j = 2
20691 else:
20692 k = 2
20693 ˇ
20694 "});
20695 cx.update_editor(|editor, window, cx| {
20696 editor.handle_input("finally:", window, cx);
20697 });
20698 cx.assert_editor_state(indoc! {"
20699 def main():
20700 try:
20701 i = 2
20702 except:
20703 j = 2
20704 else:
20705 k = 2
20706 finally:ˇ
20707 "});
20708
20709 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20710 // cx.set_state(indoc! {"
20711 // def main():
20712 // try:
20713 // for i in range(n):
20714 // pass
20715 // ˇ
20716 // "});
20717 // cx.update_editor(|editor, window, cx| {
20718 // editor.handle_input("except:", window, cx);
20719 // });
20720 // cx.assert_editor_state(indoc! {"
20721 // def main():
20722 // try:
20723 // for i in range(n):
20724 // pass
20725 // except:ˇ
20726 // "});
20727
20728 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20729 // cx.set_state(indoc! {"
20730 // def main():
20731 // try:
20732 // i = 2
20733 // except:
20734 // for i in range(n):
20735 // pass
20736 // ˇ
20737 // "});
20738 // cx.update_editor(|editor, window, cx| {
20739 // editor.handle_input("else:", window, cx);
20740 // });
20741 // cx.assert_editor_state(indoc! {"
20742 // def main():
20743 // try:
20744 // i = 2
20745 // except:
20746 // for i in range(n):
20747 // pass
20748 // else:ˇ
20749 // "});
20750
20751 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20752 // cx.set_state(indoc! {"
20753 // def main():
20754 // try:
20755 // i = 2
20756 // except:
20757 // j = 2
20758 // else:
20759 // for i in range(n):
20760 // pass
20761 // ˇ
20762 // "});
20763 // cx.update_editor(|editor, window, cx| {
20764 // editor.handle_input("finally:", window, cx);
20765 // });
20766 // cx.assert_editor_state(indoc! {"
20767 // def main():
20768 // try:
20769 // i = 2
20770 // except:
20771 // j = 2
20772 // else:
20773 // for i in range(n):
20774 // pass
20775 // finally:ˇ
20776 // "});
20777
20778 // test `else` stays at correct indent when typed after `for` block
20779 cx.set_state(indoc! {"
20780 def main():
20781 for i in range(10):
20782 if i == 3:
20783 break
20784 ˇ
20785 "});
20786 cx.update_editor(|editor, window, cx| {
20787 editor.handle_input("else:", window, cx);
20788 });
20789 cx.assert_editor_state(indoc! {"
20790 def main():
20791 for i in range(10):
20792 if i == 3:
20793 break
20794 else:ˇ
20795 "});
20796}
20797
20798#[gpui::test]
20799async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20800 init_test(cx, |_| {});
20801 update_test_language_settings(cx, |settings| {
20802 settings.defaults.extend_comment_on_newline = Some(false);
20803 });
20804 let mut cx = EditorTestContext::new(cx).await;
20805 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20806 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20807
20808 // test correct indent after newline on comment
20809 cx.set_state(indoc! {"
20810 # COMMENT:ˇ
20811 "});
20812 cx.update_editor(|editor, window, cx| {
20813 editor.newline(&Newline, window, cx);
20814 });
20815 cx.assert_editor_state(indoc! {"
20816 # COMMENT:
20817 ˇ
20818 "});
20819
20820 // test correct indent after newline in curly brackets
20821 cx.set_state(indoc! {"
20822 {ˇ}
20823 "});
20824 cx.update_editor(|editor, window, cx| {
20825 editor.newline(&Newline, window, cx);
20826 });
20827 cx.run_until_parked();
20828 cx.assert_editor_state(indoc! {"
20829 {
20830 ˇ
20831 }
20832 "});
20833}
20834
20835fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20836 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20837 point..point
20838}
20839
20840fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20841 let (text, ranges) = marked_text_ranges(marked_text, true);
20842 assert_eq!(editor.text(cx), text);
20843 assert_eq!(
20844 editor.selections.ranges(cx),
20845 ranges,
20846 "Assert selections are {}",
20847 marked_text
20848 );
20849}
20850
20851pub fn handle_signature_help_request(
20852 cx: &mut EditorLspTestContext,
20853 mocked_response: lsp::SignatureHelp,
20854) -> impl Future<Output = ()> + use<> {
20855 let mut request =
20856 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20857 let mocked_response = mocked_response.clone();
20858 async move { Ok(Some(mocked_response)) }
20859 });
20860
20861 async move {
20862 request.next().await;
20863 }
20864}
20865
20866/// Handle completion request passing a marked string specifying where the completion
20867/// should be triggered from using '|' character, what range should be replaced, and what completions
20868/// should be returned using '<' and '>' to delimit the range.
20869///
20870/// Also see `handle_completion_request_with_insert_and_replace`.
20871#[track_caller]
20872pub fn handle_completion_request(
20873 cx: &mut EditorLspTestContext,
20874 marked_string: &str,
20875 completions: Vec<&'static str>,
20876 counter: Arc<AtomicUsize>,
20877) -> impl Future<Output = ()> {
20878 let complete_from_marker: TextRangeMarker = '|'.into();
20879 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20880 let (_, mut marked_ranges) = marked_text_ranges_by(
20881 marked_string,
20882 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20883 );
20884
20885 let complete_from_position =
20886 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20887 let replace_range =
20888 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20889
20890 let mut request =
20891 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20892 let completions = completions.clone();
20893 counter.fetch_add(1, atomic::Ordering::Release);
20894 async move {
20895 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20896 assert_eq!(
20897 params.text_document_position.position,
20898 complete_from_position
20899 );
20900 Ok(Some(lsp::CompletionResponse::Array(
20901 completions
20902 .iter()
20903 .map(|completion_text| lsp::CompletionItem {
20904 label: completion_text.to_string(),
20905 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20906 range: replace_range,
20907 new_text: completion_text.to_string(),
20908 })),
20909 ..Default::default()
20910 })
20911 .collect(),
20912 )))
20913 }
20914 });
20915
20916 async move {
20917 request.next().await;
20918 }
20919}
20920
20921/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20922/// given instead, which also contains an `insert` range.
20923///
20924/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20925/// that is, `replace_range.start..cursor_pos`.
20926pub fn handle_completion_request_with_insert_and_replace(
20927 cx: &mut EditorLspTestContext,
20928 marked_string: &str,
20929 completions: Vec<&'static str>,
20930 counter: Arc<AtomicUsize>,
20931) -> impl Future<Output = ()> {
20932 let complete_from_marker: TextRangeMarker = '|'.into();
20933 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20934 let (_, mut marked_ranges) = marked_text_ranges_by(
20935 marked_string,
20936 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20937 );
20938
20939 let complete_from_position =
20940 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20941 let replace_range =
20942 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20943
20944 let mut request =
20945 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20946 let completions = completions.clone();
20947 counter.fetch_add(1, atomic::Ordering::Release);
20948 async move {
20949 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20950 assert_eq!(
20951 params.text_document_position.position, complete_from_position,
20952 "marker `|` position doesn't match",
20953 );
20954 Ok(Some(lsp::CompletionResponse::Array(
20955 completions
20956 .iter()
20957 .map(|completion_text| lsp::CompletionItem {
20958 label: completion_text.to_string(),
20959 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20960 lsp::InsertReplaceEdit {
20961 insert: lsp::Range {
20962 start: replace_range.start,
20963 end: complete_from_position,
20964 },
20965 replace: replace_range,
20966 new_text: completion_text.to_string(),
20967 },
20968 )),
20969 ..Default::default()
20970 })
20971 .collect(),
20972 )))
20973 }
20974 });
20975
20976 async move {
20977 request.next().await;
20978 }
20979}
20980
20981fn handle_resolve_completion_request(
20982 cx: &mut EditorLspTestContext,
20983 edits: Option<Vec<(&'static str, &'static str)>>,
20984) -> impl Future<Output = ()> {
20985 let edits = edits.map(|edits| {
20986 edits
20987 .iter()
20988 .map(|(marked_string, new_text)| {
20989 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20990 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20991 lsp::TextEdit::new(replace_range, new_text.to_string())
20992 })
20993 .collect::<Vec<_>>()
20994 });
20995
20996 let mut request =
20997 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20998 let edits = edits.clone();
20999 async move {
21000 Ok(lsp::CompletionItem {
21001 additional_text_edits: edits,
21002 ..Default::default()
21003 })
21004 }
21005 });
21006
21007 async move {
21008 request.next().await;
21009 }
21010}
21011
21012pub(crate) fn update_test_language_settings(
21013 cx: &mut TestAppContext,
21014 f: impl Fn(&mut AllLanguageSettingsContent),
21015) {
21016 cx.update(|cx| {
21017 SettingsStore::update_global(cx, |store, cx| {
21018 store.update_user_settings::<AllLanguageSettings>(cx, f);
21019 });
21020 });
21021}
21022
21023pub(crate) fn update_test_project_settings(
21024 cx: &mut TestAppContext,
21025 f: impl Fn(&mut ProjectSettings),
21026) {
21027 cx.update(|cx| {
21028 SettingsStore::update_global(cx, |store, cx| {
21029 store.update_user_settings::<ProjectSettings>(cx, f);
21030 });
21031 });
21032}
21033
21034pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21035 cx.update(|cx| {
21036 assets::Assets.load_test_fonts(cx);
21037 let store = SettingsStore::test(cx);
21038 cx.set_global(store);
21039 theme::init(theme::LoadThemes::JustBase, cx);
21040 release_channel::init(SemanticVersion::default(), cx);
21041 client::init_settings(cx);
21042 language::init(cx);
21043 Project::init_settings(cx);
21044 workspace::init_settings(cx);
21045 crate::init(cx);
21046 });
21047
21048 update_test_language_settings(cx, f);
21049}
21050
21051#[track_caller]
21052fn assert_hunk_revert(
21053 not_reverted_text_with_selections: &str,
21054 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21055 expected_reverted_text_with_selections: &str,
21056 base_text: &str,
21057 cx: &mut EditorLspTestContext,
21058) {
21059 cx.set_state(not_reverted_text_with_selections);
21060 cx.set_head_text(base_text);
21061 cx.executor().run_until_parked();
21062
21063 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21064 let snapshot = editor.snapshot(window, cx);
21065 let reverted_hunk_statuses = snapshot
21066 .buffer_snapshot
21067 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21068 .map(|hunk| hunk.status().kind)
21069 .collect::<Vec<_>>();
21070
21071 editor.git_restore(&Default::default(), window, cx);
21072 reverted_hunk_statuses
21073 });
21074 cx.executor().run_until_parked();
21075 cx.assert_editor_state(expected_reverted_text_with_selections);
21076 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21077}