1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
59 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
60};
61
62#[gpui::test]
63fn test_edit_events(cx: &mut TestAppContext) {
64 init_test(cx, |_| {});
65
66 let buffer = cx.new(|cx| {
67 let mut buffer = language::Buffer::local("123456", cx);
68 buffer.set_group_interval(Duration::from_secs(1));
69 buffer
70 });
71
72 let events = Rc::new(RefCell::new(Vec::new()));
73 let editor1 = cx.add_window({
74 let events = events.clone();
75 |window, cx| {
76 let entity = cx.entity().clone();
77 cx.subscribe_in(
78 &entity,
79 window,
80 move |_, _, event: &EditorEvent, _, _| match event {
81 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
82 EditorEvent::BufferEdited => {
83 events.borrow_mut().push(("editor1", "buffer edited"))
84 }
85 _ => {}
86 },
87 )
88 .detach();
89 Editor::for_buffer(buffer.clone(), None, window, cx)
90 }
91 });
92
93 let editor2 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 cx.subscribe_in(
97 &cx.entity().clone(),
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor2", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
113
114 // Mutating editor 1 will emit an `Edited` event only for that editor.
115 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor1", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Mutating editor 2 will emit an `Edited` event only for that editor.
126 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor2", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Undoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Redoing on editor 1 will emit an `Edited` event only for that editor.
148 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor1", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Undoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // Redoing on editor 2 will emit an `Edited` event only for that editor.
170 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
171 assert_eq!(
172 mem::take(&mut *events.borrow_mut()),
173 [
174 ("editor2", "edited"),
175 ("editor1", "buffer edited"),
176 ("editor2", "buffer edited"),
177 ]
178 );
179
180 // No event is emitted when the mutation is a no-op.
181 _ = editor2.update(cx, |editor, window, cx| {
182 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
183
184 editor.backspace(&Backspace, window, cx);
185 });
186 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
187}
188
189#[gpui::test]
190fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
191 init_test(cx, |_| {});
192
193 let mut now = Instant::now();
194 let group_interval = Duration::from_millis(1);
195 let buffer = cx.new(|cx| {
196 let mut buf = language::Buffer::local("123456", cx);
197 buf.set_group_interval(group_interval);
198 buf
199 });
200 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
201 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
202
203 _ = editor.update(cx, |editor, window, cx| {
204 editor.start_transaction_at(now, window, cx);
205 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
206
207 editor.insert("cd", window, cx);
208 editor.end_transaction_at(now, cx);
209 assert_eq!(editor.text(cx), "12cd56");
210 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
211
212 editor.start_transaction_at(now, window, cx);
213 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
214 editor.insert("e", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cde6");
217 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
218
219 now += group_interval + Duration::from_millis(1);
220 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
221
222 // Simulate an edit in another editor
223 buffer.update(cx, |buffer, cx| {
224 buffer.start_transaction_at(now, cx);
225 buffer.edit([(0..1, "a")], None, cx);
226 buffer.edit([(1..1, "b")], None, cx);
227 buffer.end_transaction_at(now, cx);
228 });
229
230 assert_eq!(editor.text(cx), "ab2cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
232
233 // Last transaction happened past the group interval in a different editor.
234 // Undo it individually and don't restore selections.
235 editor.undo(&Undo, window, cx);
236 assert_eq!(editor.text(cx), "12cde6");
237 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
238
239 // First two transactions happened within the group interval in this editor.
240 // Undo them together and restore selections.
241 editor.undo(&Undo, window, cx);
242 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
243 assert_eq!(editor.text(cx), "123456");
244 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
245
246 // Redo the first two transactions together.
247 editor.redo(&Redo, window, cx);
248 assert_eq!(editor.text(cx), "12cde6");
249 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
250
251 // Redo the last transaction on its own.
252 editor.redo(&Redo, window, cx);
253 assert_eq!(editor.text(cx), "ab2cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
255
256 // Test empty transactions.
257 editor.start_transaction_at(now, window, cx);
258 editor.end_transaction_at(now, cx);
259 editor.undo(&Undo, window, cx);
260 assert_eq!(editor.text(cx), "12cde6");
261 });
262}
263
264#[gpui::test]
265fn test_ime_composition(cx: &mut TestAppContext) {
266 init_test(cx, |_| {});
267
268 let buffer = cx.new(|cx| {
269 let mut buffer = language::Buffer::local("abcde", cx);
270 // Ensure automatic grouping doesn't occur.
271 buffer.set_group_interval(Duration::ZERO);
272 buffer
273 });
274
275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
276 cx.add_window(|window, cx| {
277 let mut editor = build_editor(buffer.clone(), window, cx);
278
279 // Start a new IME composition.
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 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
283 assert_eq!(editor.text(cx), "äbcde");
284 assert_eq!(
285 editor.marked_text_ranges(cx),
286 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
287 );
288
289 // Finalize IME composition.
290 editor.replace_text_in_range(None, "ā", window, cx);
291 assert_eq!(editor.text(cx), "ābcde");
292 assert_eq!(editor.marked_text_ranges(cx), None);
293
294 // IME composition edits are grouped and are undone/redone at once.
295 editor.undo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "abcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298 editor.redo(&Default::default(), window, cx);
299 assert_eq!(editor.text(cx), "ābcde");
300 assert_eq!(editor.marked_text_ranges(cx), None);
301
302 // Start a new IME composition.
303 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
304 assert_eq!(
305 editor.marked_text_ranges(cx),
306 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
307 );
308
309 // Undoing during an IME composition cancels it.
310 editor.undo(&Default::default(), window, cx);
311 assert_eq!(editor.text(cx), "ābcde");
312 assert_eq!(editor.marked_text_ranges(cx), None);
313
314 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
315 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
316 assert_eq!(editor.text(cx), "ābcdè");
317 assert_eq!(
318 editor.marked_text_ranges(cx),
319 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
320 );
321
322 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
323 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
324 assert_eq!(editor.text(cx), "ābcdę");
325 assert_eq!(editor.marked_text_ranges(cx), None);
326
327 // Start a new IME composition with multiple cursors.
328 editor.change_selections(None, window, cx, |s| {
329 s.select_ranges([
330 OffsetUtf16(1)..OffsetUtf16(1),
331 OffsetUtf16(3)..OffsetUtf16(3),
332 OffsetUtf16(5)..OffsetUtf16(5),
333 ])
334 });
335 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
336 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
337 assert_eq!(
338 editor.marked_text_ranges(cx),
339 Some(vec![
340 OffsetUtf16(0)..OffsetUtf16(3),
341 OffsetUtf16(4)..OffsetUtf16(7),
342 OffsetUtf16(8)..OffsetUtf16(11)
343 ])
344 );
345
346 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
347 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
348 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![
352 OffsetUtf16(1)..OffsetUtf16(2),
353 OffsetUtf16(5)..OffsetUtf16(6),
354 OffsetUtf16(9)..OffsetUtf16(10)
355 ])
356 );
357
358 // Finalize IME composition with multiple cursors.
359 editor.replace_text_in_range(Some(9..10), "2", window, cx);
360 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
361 assert_eq!(editor.marked_text_ranges(cx), None);
362
363 editor
364 });
365}
366
367#[gpui::test]
368fn test_selection_with_mouse(cx: &mut TestAppContext) {
369 init_test(cx, |_| {});
370
371 let editor = cx.add_window(|window, cx| {
372 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
373 build_editor(buffer, window, cx)
374 });
375
376 _ = editor.update(cx, |editor, window, cx| {
377 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
378 });
379 assert_eq!(
380 editor
381 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
382 .unwrap(),
383 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
384 );
385
386 _ = editor.update(cx, |editor, window, cx| {
387 editor.update_selection(
388 DisplayPoint::new(DisplayRow(3), 3),
389 0,
390 gpui::Point::<f32>::default(),
391 window,
392 cx,
393 );
394 });
395
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(1), 1),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.end_selection(window, cx);
422 editor.update_selection(
423 DisplayPoint::new(DisplayRow(3), 3),
424 0,
425 gpui::Point::<f32>::default(),
426 window,
427 cx,
428 );
429 });
430
431 assert_eq!(
432 editor
433 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
434 .unwrap(),
435 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
436 );
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
440 editor.update_selection(
441 DisplayPoint::new(DisplayRow(0), 0),
442 0,
443 gpui::Point::<f32>::default(),
444 window,
445 cx,
446 );
447 });
448
449 assert_eq!(
450 editor
451 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
452 .unwrap(),
453 [
454 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
455 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
456 ]
457 );
458
459 _ = editor.update(cx, |editor, window, cx| {
460 editor.end_selection(window, cx);
461 });
462
463 assert_eq!(
464 editor
465 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
466 .unwrap(),
467 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
468 );
469}
470
471#[gpui::test]
472fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
473 init_test(cx, |_| {});
474
475 let editor = cx.add_window(|window, cx| {
476 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
477 build_editor(buffer, window, cx)
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
490 });
491
492 _ = editor.update(cx, |editor, window, cx| {
493 editor.end_selection(window, cx);
494 });
495
496 assert_eq!(
497 editor
498 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
499 .unwrap(),
500 [
501 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
502 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
503 ]
504 );
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
508 });
509
510 _ = editor.update(cx, |editor, window, cx| {
511 editor.end_selection(window, cx);
512 });
513
514 assert_eq!(
515 editor
516 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
517 .unwrap(),
518 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
519 );
520}
521
522#[gpui::test]
523fn test_canceling_pending_selection(cx: &mut TestAppContext) {
524 init_test(cx, |_| {});
525
526 let editor = cx.add_window(|window, cx| {
527 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
528 build_editor(buffer, window, cx)
529 });
530
531 _ = editor.update(cx, |editor, window, cx| {
532 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
533 assert_eq!(
534 editor.selections.display_ranges(cx),
535 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
536 );
537 });
538
539 _ = editor.update(cx, |editor, window, cx| {
540 editor.update_selection(
541 DisplayPoint::new(DisplayRow(3), 3),
542 0,
543 gpui::Point::<f32>::default(),
544 window,
545 cx,
546 );
547 assert_eq!(
548 editor.selections.display_ranges(cx),
549 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
550 );
551 });
552
553 _ = editor.update(cx, |editor, window, cx| {
554 editor.cancel(&Cancel, window, cx);
555 editor.update_selection(
556 DisplayPoint::new(DisplayRow(1), 1),
557 0,
558 gpui::Point::<f32>::default(),
559 window,
560 cx,
561 );
562 assert_eq!(
563 editor.selections.display_ranges(cx),
564 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
565 );
566 });
567}
568
569#[gpui::test]
570fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
571 init_test(cx, |_| {});
572
573 let editor = cx.add_window(|window, cx| {
574 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
575 build_editor(buffer, window, cx)
576 });
577
578 _ = editor.update(cx, |editor, window, cx| {
579 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
580 assert_eq!(
581 editor.selections.display_ranges(cx),
582 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
583 );
584
585 editor.move_down(&Default::default(), window, cx);
586 assert_eq!(
587 editor.selections.display_ranges(cx),
588 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
589 );
590
591 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
592 assert_eq!(
593 editor.selections.display_ranges(cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
595 );
596
597 editor.move_up(&Default::default(), window, cx);
598 assert_eq!(
599 editor.selections.display_ranges(cx),
600 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
601 );
602 });
603}
604
605#[gpui::test]
606fn test_clone(cx: &mut TestAppContext) {
607 init_test(cx, |_| {});
608
609 let (text, selection_ranges) = marked_text_ranges(
610 indoc! {"
611 one
612 two
613 threeˇ
614 four
615 fiveˇ
616 "},
617 true,
618 );
619
620 let editor = cx.add_window(|window, cx| {
621 let buffer = MultiBuffer::build_simple(&text, cx);
622 build_editor(buffer, window, cx)
623 });
624
625 _ = editor.update(cx, |editor, window, cx| {
626 editor.change_selections(None, window, cx, |s| {
627 s.select_ranges(selection_ranges.clone())
628 });
629 editor.fold_creases(
630 vec![
631 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
632 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
633 ],
634 true,
635 window,
636 cx,
637 );
638 });
639
640 let cloned_editor = editor
641 .update(cx, |editor, _, cx| {
642 cx.open_window(Default::default(), |window, cx| {
643 cx.new(|cx| editor.clone(window, cx))
644 })
645 })
646 .unwrap()
647 .unwrap();
648
649 let snapshot = editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652 let cloned_snapshot = cloned_editor
653 .update(cx, |e, window, cx| e.snapshot(window, cx))
654 .unwrap();
655
656 assert_eq!(
657 cloned_editor
658 .update(cx, |e, _, cx| e.display_text(cx))
659 .unwrap(),
660 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
661 );
662 assert_eq!(
663 cloned_snapshot
664 .folds_in_range(0..text.len())
665 .collect::<Vec<_>>(),
666 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
667 );
668 assert_set_eq!(
669 cloned_editor
670 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
671 .unwrap(),
672 editor
673 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
674 .unwrap()
675 );
676 assert_set_eq!(
677 cloned_editor
678 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
679 .unwrap(),
680 editor
681 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
682 .unwrap()
683 );
684}
685
686#[gpui::test]
687async fn test_navigation_history(cx: &mut TestAppContext) {
688 init_test(cx, |_| {});
689
690 use workspace::item::Item;
691
692 let fs = FakeFs::new(cx.executor());
693 let project = Project::test(fs, [], cx).await;
694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
695 let pane = workspace
696 .update(cx, |workspace, _, _| workspace.active_pane().clone())
697 .unwrap();
698
699 _ = workspace.update(cx, |_v, window, cx| {
700 cx.new(|cx| {
701 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
702 let mut editor = build_editor(buffer.clone(), window, cx);
703 let handle = cx.entity();
704 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
705
706 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
707 editor.nav_history.as_mut().unwrap().pop_backward(cx)
708 }
709
710 // Move the cursor a small distance.
711 // Nothing is added to the navigation history.
712 editor.change_selections(None, window, cx, |s| {
713 s.select_display_ranges([
714 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
715 ])
716 });
717 editor.change_selections(None, window, cx, |s| {
718 s.select_display_ranges([
719 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
720 ])
721 });
722 assert!(pop_history(&mut editor, cx).is_none());
723
724 // Move the cursor a large distance.
725 // The history can jump back to the previous position.
726 editor.change_selections(None, window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
729 ])
730 });
731 let nav_entry = pop_history(&mut editor, cx).unwrap();
732 editor.navigate(nav_entry.data.unwrap(), window, cx);
733 assert_eq!(nav_entry.item.id(), cx.entity_id());
734 assert_eq!(
735 editor.selections.display_ranges(cx),
736 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
737 );
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a small distance via the mouse.
741 // Nothing is added to the navigation history.
742 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
743 editor.end_selection(window, cx);
744 assert_eq!(
745 editor.selections.display_ranges(cx),
746 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
747 );
748 assert!(pop_history(&mut editor, cx).is_none());
749
750 // Move the cursor a large distance via the mouse.
751 // The history can jump back to the previous position.
752 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
753 editor.end_selection(window, cx);
754 assert_eq!(
755 editor.selections.display_ranges(cx),
756 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
757 );
758 let nav_entry = pop_history(&mut editor, cx).unwrap();
759 editor.navigate(nav_entry.data.unwrap(), window, cx);
760 assert_eq!(nav_entry.item.id(), cx.entity_id());
761 assert_eq!(
762 editor.selections.display_ranges(cx),
763 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
764 );
765 assert!(pop_history(&mut editor, cx).is_none());
766
767 // Set scroll position to check later
768 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
769 let original_scroll_position = editor.scroll_manager.anchor();
770
771 // Jump to the end of the document and adjust scroll
772 editor.move_to_end(&MoveToEnd, window, cx);
773 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
774 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 let nav_entry = pop_history(&mut editor, cx).unwrap();
777 editor.navigate(nav_entry.data.unwrap(), window, cx);
778 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
779
780 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
781 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
782 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
783 let invalid_point = Point::new(9999, 0);
784 editor.navigate(
785 Box::new(NavigationData {
786 cursor_anchor: invalid_anchor,
787 cursor_position: invalid_point,
788 scroll_anchor: ScrollAnchor {
789 anchor: invalid_anchor,
790 offset: Default::default(),
791 },
792 scroll_top_row: invalid_point.row,
793 }),
794 window,
795 cx,
796 );
797 assert_eq!(
798 editor.selections.display_ranges(cx),
799 &[editor.max_point(cx)..editor.max_point(cx)]
800 );
801 assert_eq!(
802 editor.scroll_position(cx),
803 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
804 );
805
806 editor
807 })
808 });
809}
810
811#[gpui::test]
812fn test_cancel(cx: &mut TestAppContext) {
813 init_test(cx, |_| {});
814
815 let editor = cx.add_window(|window, cx| {
816 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
817 build_editor(buffer, window, cx)
818 });
819
820 _ = editor.update(cx, |editor, window, cx| {
821 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
822 editor.update_selection(
823 DisplayPoint::new(DisplayRow(1), 1),
824 0,
825 gpui::Point::<f32>::default(),
826 window,
827 cx,
828 );
829 editor.end_selection(window, cx);
830
831 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
832 editor.update_selection(
833 DisplayPoint::new(DisplayRow(0), 3),
834 0,
835 gpui::Point::<f32>::default(),
836 window,
837 cx,
838 );
839 editor.end_selection(window, cx);
840 assert_eq!(
841 editor.selections.display_ranges(cx),
842 [
843 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
844 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
845 ]
846 );
847 });
848
849 _ = editor.update(cx, |editor, window, cx| {
850 editor.cancel(&Cancel, window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
854 );
855 });
856
857 _ = editor.update(cx, |editor, window, cx| {
858 editor.cancel(&Cancel, window, cx);
859 assert_eq!(
860 editor.selections.display_ranges(cx),
861 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
862 );
863 });
864}
865
866#[gpui::test]
867fn test_fold_action(cx: &mut TestAppContext) {
868 init_test(cx, |_| {});
869
870 let editor = cx.add_window(|window, cx| {
871 let buffer = MultiBuffer::build_simple(
872 &"
873 impl Foo {
874 // Hello!
875
876 fn a() {
877 1
878 }
879
880 fn b() {
881 2
882 }
883
884 fn c() {
885 3
886 }
887 }
888 "
889 .unindent(),
890 cx,
891 );
892 build_editor(buffer.clone(), window, cx)
893 });
894
895 _ = editor.update(cx, |editor, window, cx| {
896 editor.change_selections(None, window, cx, |s| {
897 s.select_display_ranges([
898 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
899 ]);
900 });
901 editor.fold(&Fold, window, cx);
902 assert_eq!(
903 editor.display_text(cx),
904 "
905 impl Foo {
906 // Hello!
907
908 fn a() {
909 1
910 }
911
912 fn b() {⋯
913 }
914
915 fn c() {⋯
916 }
917 }
918 "
919 .unindent(),
920 );
921
922 editor.fold(&Fold, window, cx);
923 assert_eq!(
924 editor.display_text(cx),
925 "
926 impl Foo {⋯
927 }
928 "
929 .unindent(),
930 );
931
932 editor.unfold_lines(&UnfoldLines, window, cx);
933 assert_eq!(
934 editor.display_text(cx),
935 "
936 impl Foo {
937 // Hello!
938
939 fn a() {
940 1
941 }
942
943 fn b() {⋯
944 }
945
946 fn c() {⋯
947 }
948 }
949 "
950 .unindent(),
951 );
952
953 editor.unfold_lines(&UnfoldLines, window, cx);
954 assert_eq!(
955 editor.display_text(cx),
956 editor.buffer.read(cx).read(cx).text()
957 );
958 });
959}
960
961#[gpui::test]
962fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple(
967 &"
968 class Foo:
969 # Hello!
970
971 def a():
972 print(1)
973
974 def b():
975 print(2)
976
977 def c():
978 print(3)
979 "
980 .unindent(),
981 cx,
982 );
983 build_editor(buffer.clone(), window, cx)
984 });
985
986 _ = editor.update(cx, |editor, window, cx| {
987 editor.change_selections(None, window, cx, |s| {
988 s.select_display_ranges([
989 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
990 ]);
991 });
992 editor.fold(&Fold, window, cx);
993 assert_eq!(
994 editor.display_text(cx),
995 "
996 class Foo:
997 # Hello!
998
999 def a():
1000 print(1)
1001
1002 def b():⋯
1003
1004 def c():⋯
1005 "
1006 .unindent(),
1007 );
1008
1009 editor.fold(&Fold, window, cx);
1010 assert_eq!(
1011 editor.display_text(cx),
1012 "
1013 class Foo:⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.unfold_lines(&UnfoldLines, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:
1023 # Hello!
1024
1025 def a():
1026 print(1)
1027
1028 def b():⋯
1029
1030 def c():⋯
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 editor.buffer.read(cx).read(cx).text()
1039 );
1040 });
1041}
1042
1043#[gpui::test]
1044fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1045 init_test(cx, |_| {});
1046
1047 let editor = cx.add_window(|window, cx| {
1048 let buffer = MultiBuffer::build_simple(
1049 &"
1050 class Foo:
1051 # Hello!
1052
1053 def a():
1054 print(1)
1055
1056 def b():
1057 print(2)
1058
1059
1060 def c():
1061 print(3)
1062
1063
1064 "
1065 .unindent(),
1066 cx,
1067 );
1068 build_editor(buffer.clone(), window, cx)
1069 });
1070
1071 _ = editor.update(cx, |editor, window, cx| {
1072 editor.change_selections(None, window, cx, |s| {
1073 s.select_display_ranges([
1074 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1075 ]);
1076 });
1077 editor.fold(&Fold, window, cx);
1078 assert_eq!(
1079 editor.display_text(cx),
1080 "
1081 class Foo:
1082 # Hello!
1083
1084 def a():
1085 print(1)
1086
1087 def b():⋯
1088
1089
1090 def c():⋯
1091
1092
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.fold(&Fold, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 class Foo:⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.unfold_lines(&UnfoldLines, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:
1113 # Hello!
1114
1115 def a():
1116 print(1)
1117
1118 def b():⋯
1119
1120
1121 def c():⋯
1122
1123
1124 "
1125 .unindent(),
1126 );
1127
1128 editor.unfold_lines(&UnfoldLines, window, cx);
1129 assert_eq!(
1130 editor.display_text(cx),
1131 editor.buffer.read(cx).read(cx).text()
1132 );
1133 });
1134}
1135
1136#[gpui::test]
1137fn test_fold_at_level(cx: &mut TestAppContext) {
1138 init_test(cx, |_| {});
1139
1140 let editor = cx.add_window(|window, cx| {
1141 let buffer = MultiBuffer::build_simple(
1142 &"
1143 class Foo:
1144 # Hello!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152
1153 class Bar:
1154 # World!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 "
1164 .unindent(),
1165 cx,
1166 );
1167 build_editor(buffer.clone(), window, cx)
1168 });
1169
1170 _ = editor.update(cx, |editor, window, cx| {
1171 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1172 assert_eq!(
1173 editor.display_text(cx),
1174 "
1175 class Foo:
1176 # Hello!
1177
1178 def a():⋯
1179
1180 def b():⋯
1181
1182
1183 class Bar:
1184 # World!
1185
1186 def a():⋯
1187
1188 def b():⋯
1189
1190
1191 "
1192 .unindent(),
1193 );
1194
1195 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1196 assert_eq!(
1197 editor.display_text(cx),
1198 "
1199 class Foo:⋯
1200
1201
1202 class Bar:⋯
1203
1204
1205 "
1206 .unindent(),
1207 );
1208
1209 editor.unfold_all(&UnfoldAll, window, cx);
1210 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 "
1214 class Foo:
1215 # Hello!
1216
1217 def a():
1218 print(1)
1219
1220 def b():
1221 print(2)
1222
1223
1224 class Bar:
1225 # World!
1226
1227 def a():
1228 print(1)
1229
1230 def b():
1231 print(2)
1232
1233
1234 "
1235 .unindent(),
1236 );
1237
1238 assert_eq!(
1239 editor.display_text(cx),
1240 editor.buffer.read(cx).read(cx).text()
1241 );
1242 });
1243}
1244
1245#[gpui::test]
1246fn test_move_cursor(cx: &mut TestAppContext) {
1247 init_test(cx, |_| {});
1248
1249 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1250 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1251
1252 buffer.update(cx, |buffer, cx| {
1253 buffer.edit(
1254 vec![
1255 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1256 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1257 ],
1258 None,
1259 cx,
1260 );
1261 });
1262 _ = editor.update(cx, |editor, window, cx| {
1263 assert_eq!(
1264 editor.selections.display_ranges(cx),
1265 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1266 );
1267
1268 editor.move_down(&MoveDown, window, cx);
1269 assert_eq!(
1270 editor.selections.display_ranges(cx),
1271 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1272 );
1273
1274 editor.move_right(&MoveRight, window, cx);
1275 assert_eq!(
1276 editor.selections.display_ranges(cx),
1277 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1278 );
1279
1280 editor.move_left(&MoveLeft, window, cx);
1281 assert_eq!(
1282 editor.selections.display_ranges(cx),
1283 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1284 );
1285
1286 editor.move_up(&MoveUp, window, cx);
1287 assert_eq!(
1288 editor.selections.display_ranges(cx),
1289 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1290 );
1291
1292 editor.move_to_end(&MoveToEnd, window, cx);
1293 assert_eq!(
1294 editor.selections.display_ranges(cx),
1295 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1296 );
1297
1298 editor.move_to_beginning(&MoveToBeginning, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.change_selections(None, window, cx, |s| {
1305 s.select_display_ranges([
1306 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1307 ]);
1308 });
1309 editor.select_to_beginning(&SelectToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.select_to_end(&SelectToEnd, window, cx);
1316 assert_eq!(
1317 editor.selections.display_ranges(cx),
1318 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1319 );
1320 });
1321}
1322
1323#[gpui::test]
1324fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1325 init_test(cx, |_| {});
1326
1327 let editor = cx.add_window(|window, cx| {
1328 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1329 build_editor(buffer.clone(), window, cx)
1330 });
1331
1332 assert_eq!('🟥'.len_utf8(), 4);
1333 assert_eq!('α'.len_utf8(), 2);
1334
1335 _ = editor.update(cx, |editor, window, cx| {
1336 editor.fold_creases(
1337 vec![
1338 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1340 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1341 ],
1342 true,
1343 window,
1344 cx,
1345 );
1346 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1347
1348 editor.move_right(&MoveRight, window, cx);
1349 assert_eq!(
1350 editor.selections.display_ranges(cx),
1351 &[empty_range(0, "🟥".len())]
1352 );
1353 editor.move_right(&MoveRight, window, cx);
1354 assert_eq!(
1355 editor.selections.display_ranges(cx),
1356 &[empty_range(0, "🟥🟧".len())]
1357 );
1358 editor.move_right(&MoveRight, window, cx);
1359 assert_eq!(
1360 editor.selections.display_ranges(cx),
1361 &[empty_range(0, "🟥🟧⋯".len())]
1362 );
1363
1364 editor.move_down(&MoveDown, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(1, "ab⋯e".len())]
1368 );
1369 editor.move_left(&MoveLeft, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(1, "ab⋯".len())]
1373 );
1374 editor.move_left(&MoveLeft, window, cx);
1375 assert_eq!(
1376 editor.selections.display_ranges(cx),
1377 &[empty_range(1, "ab".len())]
1378 );
1379 editor.move_left(&MoveLeft, window, cx);
1380 assert_eq!(
1381 editor.selections.display_ranges(cx),
1382 &[empty_range(1, "a".len())]
1383 );
1384
1385 editor.move_down(&MoveDown, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(2, "α".len())]
1389 );
1390 editor.move_right(&MoveRight, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(2, "αβ".len())]
1394 );
1395 editor.move_right(&MoveRight, window, cx);
1396 assert_eq!(
1397 editor.selections.display_ranges(cx),
1398 &[empty_range(2, "αβ⋯".len())]
1399 );
1400 editor.move_right(&MoveRight, window, cx);
1401 assert_eq!(
1402 editor.selections.display_ranges(cx),
1403 &[empty_range(2, "αβ⋯ε".len())]
1404 );
1405
1406 editor.move_up(&MoveUp, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(1, "ab⋯e".len())]
1410 );
1411 editor.move_down(&MoveDown, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416 editor.move_up(&MoveUp, window, cx);
1417 assert_eq!(
1418 editor.selections.display_ranges(cx),
1419 &[empty_range(1, "ab⋯e".len())]
1420 );
1421
1422 editor.move_up(&MoveUp, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(0, "🟥🟧".len())]
1426 );
1427 editor.move_left(&MoveLeft, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(0, "🟥".len())]
1431 );
1432 editor.move_left(&MoveLeft, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(0, "".len())]
1436 );
1437 });
1438}
1439
1440#[gpui::test]
1441fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1442 init_test(cx, |_| {});
1443
1444 let editor = cx.add_window(|window, cx| {
1445 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1446 build_editor(buffer.clone(), window, cx)
1447 });
1448 _ = editor.update(cx, |editor, window, cx| {
1449 editor.change_selections(None, window, cx, |s| {
1450 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1451 });
1452
1453 // moving above start of document should move selection to start of document,
1454 // but the next move down should still be at the original goal_x
1455 editor.move_up(&MoveUp, window, cx);
1456 assert_eq!(
1457 editor.selections.display_ranges(cx),
1458 &[empty_range(0, "".len())]
1459 );
1460
1461 editor.move_down(&MoveDown, window, cx);
1462 assert_eq!(
1463 editor.selections.display_ranges(cx),
1464 &[empty_range(1, "abcd".len())]
1465 );
1466
1467 editor.move_down(&MoveDown, window, cx);
1468 assert_eq!(
1469 editor.selections.display_ranges(cx),
1470 &[empty_range(2, "αβγ".len())]
1471 );
1472
1473 editor.move_down(&MoveDown, window, cx);
1474 assert_eq!(
1475 editor.selections.display_ranges(cx),
1476 &[empty_range(3, "abcd".len())]
1477 );
1478
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1483 );
1484
1485 // moving past end of document should not change goal_x
1486 editor.move_down(&MoveDown, window, cx);
1487 assert_eq!(
1488 editor.selections.display_ranges(cx),
1489 &[empty_range(5, "".len())]
1490 );
1491
1492 editor.move_down(&MoveDown, window, cx);
1493 assert_eq!(
1494 editor.selections.display_ranges(cx),
1495 &[empty_range(5, "".len())]
1496 );
1497
1498 editor.move_up(&MoveUp, window, cx);
1499 assert_eq!(
1500 editor.selections.display_ranges(cx),
1501 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1502 );
1503
1504 editor.move_up(&MoveUp, window, cx);
1505 assert_eq!(
1506 editor.selections.display_ranges(cx),
1507 &[empty_range(3, "abcd".len())]
1508 );
1509
1510 editor.move_up(&MoveUp, window, cx);
1511 assert_eq!(
1512 editor.selections.display_ranges(cx),
1513 &[empty_range(2, "αβγ".len())]
1514 );
1515 });
1516}
1517
1518#[gpui::test]
1519fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1520 init_test(cx, |_| {});
1521 let move_to_beg = MoveToBeginningOfLine {
1522 stop_at_soft_wraps: true,
1523 stop_at_indent: true,
1524 };
1525
1526 let delete_to_beg = DeleteToBeginningOfLine {
1527 stop_at_indent: false,
1528 };
1529
1530 let move_to_end = MoveToEndOfLine {
1531 stop_at_soft_wraps: true,
1532 };
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538 _ = editor.update(cx, |editor, window, cx| {
1539 editor.change_selections(None, window, cx, |s| {
1540 s.select_display_ranges([
1541 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1542 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1543 ]);
1544 });
1545 });
1546
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1549 assert_eq!(
1550 editor.selections.display_ranges(cx),
1551 &[
1552 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1553 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1554 ]
1555 );
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_end_of_line(&move_to_end, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1586 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1587 ]
1588 );
1589 });
1590
1591 // Moving to the end of line again is a no-op.
1592 _ = editor.update(cx, |editor, window, cx| {
1593 editor.move_to_end_of_line(&move_to_end, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[
1597 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1598 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1599 ]
1600 );
1601 });
1602
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_left(&MoveLeft, window, cx);
1605 editor.select_to_beginning_of_line(
1606 &SelectToBeginningOfLine {
1607 stop_at_soft_wraps: true,
1608 stop_at_indent: true,
1609 },
1610 window,
1611 cx,
1612 );
1613 assert_eq!(
1614 editor.selections.display_ranges(cx),
1615 &[
1616 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1617 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1618 ]
1619 );
1620 });
1621
1622 _ = editor.update(cx, |editor, window, cx| {
1623 editor.select_to_beginning_of_line(
1624 &SelectToBeginningOfLine {
1625 stop_at_soft_wraps: true,
1626 stop_at_indent: true,
1627 },
1628 window,
1629 cx,
1630 );
1631 assert_eq!(
1632 editor.selections.display_ranges(cx),
1633 &[
1634 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1635 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1636 ]
1637 );
1638 });
1639
1640 _ = editor.update(cx, |editor, window, cx| {
1641 editor.select_to_beginning_of_line(
1642 &SelectToBeginningOfLine {
1643 stop_at_soft_wraps: true,
1644 stop_at_indent: true,
1645 },
1646 window,
1647 cx,
1648 );
1649 assert_eq!(
1650 editor.selections.display_ranges(cx),
1651 &[
1652 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1653 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1654 ]
1655 );
1656 });
1657
1658 _ = editor.update(cx, |editor, window, cx| {
1659 editor.select_to_end_of_line(
1660 &SelectToEndOfLine {
1661 stop_at_soft_wraps: true,
1662 },
1663 window,
1664 cx,
1665 );
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[
1669 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1670 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1671 ]
1672 );
1673 });
1674
1675 _ = editor.update(cx, |editor, window, cx| {
1676 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1677 assert_eq!(editor.display_text(cx), "ab\n de");
1678 assert_eq!(
1679 editor.selections.display_ranges(cx),
1680 &[
1681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1682 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1683 ]
1684 );
1685 });
1686
1687 _ = editor.update(cx, |editor, window, cx| {
1688 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1689 assert_eq!(editor.display_text(cx), "\n");
1690 assert_eq!(
1691 editor.selections.display_ranges(cx),
1692 &[
1693 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1694 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1695 ]
1696 );
1697 });
1698}
1699
1700#[gpui::test]
1701fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1702 init_test(cx, |_| {});
1703 let move_to_beg = MoveToBeginningOfLine {
1704 stop_at_soft_wraps: false,
1705 stop_at_indent: false,
1706 };
1707
1708 let move_to_end = MoveToEndOfLine {
1709 stop_at_soft_wraps: false,
1710 };
1711
1712 let editor = cx.add_window(|window, cx| {
1713 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1714 build_editor(buffer, window, cx)
1715 });
1716
1717 _ = editor.update(cx, |editor, window, cx| {
1718 editor.set_wrap_width(Some(140.0.into()), cx);
1719
1720 // We expect the following lines after wrapping
1721 // ```
1722 // thequickbrownfox
1723 // jumpedoverthelazydo
1724 // gs
1725 // ```
1726 // The final `gs` was soft-wrapped onto a new line.
1727 assert_eq!(
1728 "thequickbrownfox\njumpedoverthelaz\nydogs",
1729 editor.display_text(cx),
1730 );
1731
1732 // First, let's assert behavior on the first line, that was not soft-wrapped.
1733 // Start the cursor at the `k` on the first line
1734 editor.change_selections(None, window, cx, |s| {
1735 s.select_display_ranges([
1736 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1737 ]);
1738 });
1739
1740 // Moving to the beginning of the line should put us at the beginning of the line.
1741 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1742 assert_eq!(
1743 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1744 editor.selections.display_ranges(cx)
1745 );
1746
1747 // Moving to the end of the line should put us at the end of the line.
1748 editor.move_to_end_of_line(&move_to_end, window, cx);
1749 assert_eq!(
1750 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1751 editor.selections.display_ranges(cx)
1752 );
1753
1754 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1755 // Start the cursor at the last line (`y` that was wrapped to a new line)
1756 editor.change_selections(None, window, cx, |s| {
1757 s.select_display_ranges([
1758 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1759 ]);
1760 });
1761
1762 // Moving to the beginning of the line should put us at the start of the second line of
1763 // display text, i.e., the `j`.
1764 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1765 assert_eq!(
1766 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1767 editor.selections.display_ranges(cx)
1768 );
1769
1770 // Moving to the beginning of the line again should be a no-op.
1771 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1772 assert_eq!(
1773 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1774 editor.selections.display_ranges(cx)
1775 );
1776
1777 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1778 // next display line.
1779 editor.move_to_end_of_line(&move_to_end, window, cx);
1780 assert_eq!(
1781 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1782 editor.selections.display_ranges(cx)
1783 );
1784
1785 // Moving to the end of the line again should be a no-op.
1786 editor.move_to_end_of_line(&move_to_end, window, cx);
1787 assert_eq!(
1788 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1789 editor.selections.display_ranges(cx)
1790 );
1791 });
1792}
1793
1794#[gpui::test]
1795fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1796 init_test(cx, |_| {});
1797
1798 let move_to_beg = MoveToBeginningOfLine {
1799 stop_at_soft_wraps: true,
1800 stop_at_indent: true,
1801 };
1802
1803 let select_to_beg = SelectToBeginningOfLine {
1804 stop_at_soft_wraps: true,
1805 stop_at_indent: true,
1806 };
1807
1808 let delete_to_beg = DeleteToBeginningOfLine {
1809 stop_at_indent: true,
1810 };
1811
1812 let move_to_end = MoveToEndOfLine {
1813 stop_at_soft_wraps: false,
1814 };
1815
1816 let editor = cx.add_window(|window, cx| {
1817 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1818 build_editor(buffer, window, cx)
1819 });
1820
1821 _ = editor.update(cx, |editor, window, cx| {
1822 editor.change_selections(None, window, cx, |s| {
1823 s.select_display_ranges([
1824 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1825 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1826 ]);
1827 });
1828
1829 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1830 // and the second cursor at the first non-whitespace character in the line.
1831 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1832 assert_eq!(
1833 editor.selections.display_ranges(cx),
1834 &[
1835 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1836 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1837 ]
1838 );
1839
1840 // Moving to the beginning of the line again should be a no-op for the first cursor,
1841 // and should move the second cursor to the beginning of the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1852 // and should move the second cursor back to the first non-whitespace character in the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1859 ]
1860 );
1861
1862 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1863 // and to the first non-whitespace character in the line for the second cursor.
1864 editor.move_to_end_of_line(&move_to_end, window, cx);
1865 editor.move_left(&MoveLeft, window, cx);
1866 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1867 assert_eq!(
1868 editor.selections.display_ranges(cx),
1869 &[
1870 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1871 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1872 ]
1873 );
1874
1875 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1876 // and should select to the beginning of the line for the second cursor.
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1883 ]
1884 );
1885
1886 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1887 // and should delete to the first non-whitespace character in the line for the second cursor.
1888 editor.move_to_end_of_line(&move_to_end, window, cx);
1889 editor.move_left(&MoveLeft, window, cx);
1890 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1891 assert_eq!(editor.text(cx), "c\n f");
1892 });
1893}
1894
1895#[gpui::test]
1896fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1897 init_test(cx, |_| {});
1898
1899 let editor = cx.add_window(|window, cx| {
1900 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1901 build_editor(buffer, window, cx)
1902 });
1903 _ = editor.update(cx, |editor, window, cx| {
1904 editor.change_selections(None, window, cx, |s| {
1905 s.select_display_ranges([
1906 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
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» {baz.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» {baz.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» {baz.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(
2867 Language::new(
2868 LanguageConfig {
2869 documentation: Some(language::DocumentationConfig {
2870 start: "/**".into(),
2871 end: "*/".into(),
2872 prefix: "* ".into(),
2873 tab_size: NonZeroU32::new(1).unwrap(),
2874 }),
2875
2876 ..LanguageConfig::default()
2877 },
2878 Some(tree_sitter_rust::LANGUAGE.into()),
2879 )
2880 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2881 .unwrap(),
2882 );
2883
2884 {
2885 let mut cx = EditorTestContext::new(cx).await;
2886 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2887 cx.set_state(indoc! {"
2888 /**ˇ
2889 "});
2890
2891 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2892 cx.assert_editor_state(indoc! {"
2893 /**
2894 * ˇ
2895 "});
2896 // Ensure that if cursor is before the comment start,
2897 // we do not actually insert a comment prefix.
2898 cx.set_state(indoc! {"
2899 ˇ/**
2900 "});
2901 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2902 cx.assert_editor_state(indoc! {"
2903
2904 ˇ/**
2905 "});
2906 // Ensure that if cursor is between it doesn't add comment prefix.
2907 cx.set_state(indoc! {"
2908 /*ˇ*
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 /*
2913 ˇ*
2914 "});
2915 // Ensure that if suffix exists on same line after cursor it adds new line.
2916 cx.set_state(indoc! {"
2917 /**ˇ*/
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /**
2922 * ˇ
2923 */
2924 "});
2925 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2926 cx.set_state(indoc! {"
2927 /**ˇ */
2928 "});
2929 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2930 cx.assert_editor_state(indoc! {"
2931 /**
2932 * ˇ
2933 */
2934 "});
2935 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2936 cx.set_state(indoc! {"
2937 /** ˇ*/
2938 "});
2939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2940 cx.assert_editor_state(
2941 indoc! {"
2942 /**s
2943 * ˇ
2944 */
2945 "}
2946 .replace("s", " ") // s is used as space placeholder to prevent format on save
2947 .as_str(),
2948 );
2949 // Ensure that delimiter space is preserved when newline on already
2950 // spaced delimiter.
2951 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2952 cx.assert_editor_state(
2953 indoc! {"
2954 /**s
2955 *s
2956 * ˇ
2957 */
2958 "}
2959 .replace("s", " ") // s is used as space placeholder to prevent format on save
2960 .as_str(),
2961 );
2962 // Ensure that delimiter space is preserved when space is not
2963 // on existing delimiter.
2964 cx.set_state(indoc! {"
2965 /**
2966 *ˇ
2967 */
2968 "});
2969 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2970 cx.assert_editor_state(indoc! {"
2971 /**
2972 *
2973 * ˇ
2974 */
2975 "});
2976 // Ensure that if suffix exists on same line after cursor it
2977 // doesn't add extra new line if prefix is not on same line.
2978 cx.set_state(indoc! {"
2979 /**
2980 ˇ*/
2981 "});
2982 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2983 cx.assert_editor_state(indoc! {"
2984 /**
2985
2986 ˇ*/
2987 "});
2988 // Ensure that it detects suffix after existing prefix.
2989 cx.set_state(indoc! {"
2990 /**ˇ/
2991 "});
2992 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 /**
2995 ˇ/
2996 "});
2997 // Ensure that if suffix exists on same line before
2998 // cursor it does not add comment prefix.
2999 cx.set_state(indoc! {"
3000 /** */ˇ
3001 "});
3002 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3003 cx.assert_editor_state(indoc! {"
3004 /** */
3005 ˇ
3006 "});
3007 // Ensure that if suffix exists on same line before
3008 // cursor it does not add comment prefix.
3009 cx.set_state(indoc! {"
3010 /**
3011 *
3012 */ˇ
3013 "});
3014 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3015 cx.assert_editor_state(indoc! {"
3016 /**
3017 *
3018 */
3019 ˇ
3020 "});
3021
3022 // Ensure that inline comment followed by code
3023 // doesn't add comment prefix on newline
3024 cx.set_state(indoc! {"
3025 /** */ textˇ
3026 "});
3027 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3028 cx.assert_editor_state(indoc! {"
3029 /** */ text
3030 ˇ
3031 "});
3032
3033 // Ensure that text after comment end tag
3034 // doesn't add comment prefix on newline
3035 cx.set_state(indoc! {"
3036 /**
3037 *
3038 */ˇtext
3039 "});
3040 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3041 cx.assert_editor_state(indoc! {"
3042 /**
3043 *
3044 */
3045 ˇtext
3046 "});
3047
3048 // Ensure if not comment block it doesn't
3049 // add comment prefix on newline
3050 cx.set_state(indoc! {"
3051 * textˇ
3052 "});
3053 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3054 cx.assert_editor_state(indoc! {"
3055 * text
3056 ˇ
3057 "});
3058 }
3059 // Ensure that comment continuations can be disabled.
3060 update_test_language_settings(cx, |settings| {
3061 settings.defaults.extend_comment_on_newline = Some(false);
3062 });
3063 let mut cx = EditorTestContext::new(cx).await;
3064 cx.set_state(indoc! {"
3065 /**ˇ
3066 "});
3067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 /**
3070 ˇ
3071 "});
3072}
3073
3074#[gpui::test]
3075fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3076 init_test(cx, |_| {});
3077
3078 let editor = cx.add_window(|window, cx| {
3079 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3080 let mut editor = build_editor(buffer.clone(), window, cx);
3081 editor.change_selections(None, window, cx, |s| {
3082 s.select_ranges([3..4, 11..12, 19..20])
3083 });
3084 editor
3085 });
3086
3087 _ = editor.update(cx, |editor, window, cx| {
3088 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3089 editor.buffer.update(cx, |buffer, cx| {
3090 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3091 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3092 });
3093 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3094
3095 editor.insert("Z", window, cx);
3096 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3097
3098 // The selections are moved after the inserted characters
3099 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3100 });
3101}
3102
3103#[gpui::test]
3104async fn test_tab(cx: &mut TestAppContext) {
3105 init_test(cx, |settings| {
3106 settings.defaults.tab_size = NonZeroU32::new(3)
3107 });
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.set_state(indoc! {"
3111 ˇabˇc
3112 ˇ🏀ˇ🏀ˇefg
3113 dˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 ˇab ˇc
3118 ˇ🏀 ˇ🏀 ˇefg
3119 d ˇ
3120 "});
3121
3122 cx.set_state(indoc! {"
3123 a
3124 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3125 "});
3126 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 a
3129 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3130 "});
3131}
3132
3133#[gpui::test]
3134async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3135 init_test(cx, |_| {});
3136
3137 let mut cx = EditorTestContext::new(cx).await;
3138 let language = Arc::new(
3139 Language::new(
3140 LanguageConfig::default(),
3141 Some(tree_sitter_rust::LANGUAGE.into()),
3142 )
3143 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3144 .unwrap(),
3145 );
3146 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3147
3148 // test when all cursors are not at suggested indent
3149 // then simply move to their suggested indent location
3150 cx.set_state(indoc! {"
3151 const a: B = (
3152 c(
3153 ˇ
3154 ˇ )
3155 );
3156 "});
3157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 const a: B = (
3160 c(
3161 ˇ
3162 ˇ)
3163 );
3164 "});
3165
3166 // test cursor already at suggested indent not moving when
3167 // other cursors are yet to reach their suggested indents
3168 cx.set_state(indoc! {"
3169 ˇ
3170 const a: B = (
3171 c(
3172 d(
3173 ˇ
3174 )
3175 ˇ
3176 ˇ )
3177 );
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 ˇ
3182 const a: B = (
3183 c(
3184 d(
3185 ˇ
3186 )
3187 ˇ
3188 ˇ)
3189 );
3190 "});
3191 // test when all cursors are at suggested indent then tab is inserted
3192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3193 cx.assert_editor_state(indoc! {"
3194 ˇ
3195 const a: B = (
3196 c(
3197 d(
3198 ˇ
3199 )
3200 ˇ
3201 ˇ)
3202 );
3203 "});
3204
3205 // test when current indent is less than suggested indent,
3206 // we adjust line to match suggested indent and move cursor to it
3207 //
3208 // when no other cursor is at word boundary, all of them should move
3209 cx.set_state(indoc! {"
3210 const a: B = (
3211 c(
3212 d(
3213 ˇ
3214 ˇ )
3215 ˇ )
3216 );
3217 "});
3218 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3219 cx.assert_editor_state(indoc! {"
3220 const a: B = (
3221 c(
3222 d(
3223 ˇ
3224 ˇ)
3225 ˇ)
3226 );
3227 "});
3228
3229 // test when current indent is less than suggested indent,
3230 // we adjust line to match suggested indent and move cursor to it
3231 //
3232 // when some other cursor is at word boundary, it should not move
3233 cx.set_state(indoc! {"
3234 const a: B = (
3235 c(
3236 d(
3237 ˇ
3238 ˇ )
3239 ˇ)
3240 );
3241 "});
3242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 const a: B = (
3245 c(
3246 d(
3247 ˇ
3248 ˇ)
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is more than suggested indent,
3254 // we just move cursor to current indent instead of suggested indent
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
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 d(
3281 ˇ
3282 ˇ)
3283 ˇ)
3284 );
3285 "});
3286
3287 // test when current indent is more than suggested indent,
3288 // we just move cursor to current indent instead of suggested indent
3289 //
3290 // when some other cursor is at word boundary, it doesn't move
3291 cx.set_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ )
3297 ˇ)
3298 );
3299 "});
3300 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 const a: B = (
3303 c(
3304 d(
3305 ˇ
3306 ˇ)
3307 ˇ)
3308 );
3309 "});
3310
3311 // handle auto-indent when there are multiple cursors on the same line
3312 cx.set_state(indoc! {"
3313 const a: B = (
3314 c(
3315 ˇ ˇ
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 ˇ
3324 ˇ)
3325 );
3326 "});
3327}
3328
3329#[gpui::test]
3330async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3331 init_test(cx, |settings| {
3332 settings.defaults.tab_size = NonZeroU32::new(3)
3333 });
3334
3335 let mut cx = EditorTestContext::new(cx).await;
3336 cx.set_state(indoc! {"
3337 ˇ
3338 \t ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t \t\t \t \t\t \t\t \t \t ˇ
3342 "});
3343
3344 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3345 cx.assert_editor_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352}
3353
3354#[gpui::test]
3355async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3356 init_test(cx, |settings| {
3357 settings.defaults.tab_size = NonZeroU32::new(4)
3358 });
3359
3360 let language = Arc::new(
3361 Language::new(
3362 LanguageConfig::default(),
3363 Some(tree_sitter_rust::LANGUAGE.into()),
3364 )
3365 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3366 .unwrap(),
3367 );
3368
3369 let mut cx = EditorTestContext::new(cx).await;
3370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3371 cx.set_state(indoc! {"
3372 fn a() {
3373 if b {
3374 \t ˇc
3375 }
3376 }
3377 "});
3378
3379 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381 fn a() {
3382 if b {
3383 ˇc
3384 }
3385 }
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_indent_outdent(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4);
3393 });
3394
3395 let mut cx = EditorTestContext::new(cx).await;
3396
3397 cx.set_state(indoc! {"
3398 «oneˇ» «twoˇ»
3399 three
3400 four
3401 "});
3402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404 «oneˇ» «twoˇ»
3405 three
3406 four
3407 "});
3408
3409 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3410 cx.assert_editor_state(indoc! {"
3411 «oneˇ» «twoˇ»
3412 three
3413 four
3414 "});
3415
3416 // select across line ending
3417 cx.set_state(indoc! {"
3418 one two
3419 t«hree
3420 ˇ» four
3421 "});
3422 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 one two
3425 t«hree
3426 ˇ» four
3427 "});
3428
3429 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3430 cx.assert_editor_state(indoc! {"
3431 one two
3432 t«hree
3433 ˇ» four
3434 "});
3435
3436 // Ensure that indenting/outdenting works when the cursor is at column 0.
3437 cx.set_state(indoc! {"
3438 one two
3439 ˇthree
3440 four
3441 "});
3442 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 one two
3445 ˇthree
3446 four
3447 "});
3448
3449 cx.set_state(indoc! {"
3450 one two
3451 ˇ three
3452 four
3453 "});
3454 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3455 cx.assert_editor_state(indoc! {"
3456 one two
3457 ˇthree
3458 four
3459 "});
3460}
3461
3462#[gpui::test]
3463async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3464 init_test(cx, |settings| {
3465 settings.defaults.hard_tabs = Some(true);
3466 });
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 // select two ranges on one line
3471 cx.set_state(indoc! {"
3472 «oneˇ» «twoˇ»
3473 three
3474 four
3475 "});
3476 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478 \t«oneˇ» «twoˇ»
3479 three
3480 four
3481 "});
3482 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 \t\t«oneˇ» «twoˇ»
3485 three
3486 four
3487 "});
3488 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3489 cx.assert_editor_state(indoc! {"
3490 \t«oneˇ» «twoˇ»
3491 three
3492 four
3493 "});
3494 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 «oneˇ» «twoˇ»
3497 three
3498 four
3499 "});
3500
3501 // select across a line ending
3502 cx.set_state(indoc! {"
3503 one two
3504 t«hree
3505 ˇ»four
3506 "});
3507 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 \tt«hree
3511 ˇ»four
3512 "});
3513 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3514 cx.assert_editor_state(indoc! {"
3515 one two
3516 \t\tt«hree
3517 ˇ»four
3518 "});
3519 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3520 cx.assert_editor_state(indoc! {"
3521 one two
3522 \tt«hree
3523 ˇ»four
3524 "});
3525 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3526 cx.assert_editor_state(indoc! {"
3527 one two
3528 t«hree
3529 ˇ»four
3530 "});
3531
3532 // Ensure that indenting/outdenting works when the cursor is at column 0.
3533 cx.set_state(indoc! {"
3534 one two
3535 ˇthree
3536 four
3537 "});
3538 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3539 cx.assert_editor_state(indoc! {"
3540 one two
3541 ˇthree
3542 four
3543 "});
3544 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3545 cx.assert_editor_state(indoc! {"
3546 one two
3547 \tˇthree
3548 four
3549 "});
3550 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3551 cx.assert_editor_state(indoc! {"
3552 one two
3553 ˇthree
3554 four
3555 "});
3556}
3557
3558#[gpui::test]
3559fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3560 init_test(cx, |settings| {
3561 settings.languages.extend([
3562 (
3563 "TOML".into(),
3564 LanguageSettingsContent {
3565 tab_size: NonZeroU32::new(2),
3566 ..Default::default()
3567 },
3568 ),
3569 (
3570 "Rust".into(),
3571 LanguageSettingsContent {
3572 tab_size: NonZeroU32::new(4),
3573 ..Default::default()
3574 },
3575 ),
3576 ]);
3577 });
3578
3579 let toml_language = Arc::new(Language::new(
3580 LanguageConfig {
3581 name: "TOML".into(),
3582 ..Default::default()
3583 },
3584 None,
3585 ));
3586 let rust_language = Arc::new(Language::new(
3587 LanguageConfig {
3588 name: "Rust".into(),
3589 ..Default::default()
3590 },
3591 None,
3592 ));
3593
3594 let toml_buffer =
3595 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3596 let rust_buffer =
3597 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3598 let multibuffer = cx.new(|cx| {
3599 let mut multibuffer = MultiBuffer::new(ReadWrite);
3600 multibuffer.push_excerpts(
3601 toml_buffer.clone(),
3602 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3603 cx,
3604 );
3605 multibuffer.push_excerpts(
3606 rust_buffer.clone(),
3607 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3608 cx,
3609 );
3610 multibuffer
3611 });
3612
3613 cx.add_window(|window, cx| {
3614 let mut editor = build_editor(multibuffer, window, cx);
3615
3616 assert_eq!(
3617 editor.text(cx),
3618 indoc! {"
3619 a = 1
3620 b = 2
3621
3622 const c: usize = 3;
3623 "}
3624 );
3625
3626 select_ranges(
3627 &mut editor,
3628 indoc! {"
3629 «aˇ» = 1
3630 b = 2
3631
3632 «const c:ˇ» usize = 3;
3633 "},
3634 window,
3635 cx,
3636 );
3637
3638 editor.tab(&Tab, window, cx);
3639 assert_text_with_selections(
3640 &mut editor,
3641 indoc! {"
3642 «aˇ» = 1
3643 b = 2
3644
3645 «const c:ˇ» usize = 3;
3646 "},
3647 cx,
3648 );
3649 editor.backtab(&Backtab, window, cx);
3650 assert_text_with_selections(
3651 &mut editor,
3652 indoc! {"
3653 «aˇ» = 1
3654 b = 2
3655
3656 «const c:ˇ» usize = 3;
3657 "},
3658 cx,
3659 );
3660
3661 editor
3662 });
3663}
3664
3665#[gpui::test]
3666async fn test_backspace(cx: &mut TestAppContext) {
3667 init_test(cx, |_| {});
3668
3669 let mut cx = EditorTestContext::new(cx).await;
3670
3671 // Basic backspace
3672 cx.set_state(indoc! {"
3673 onˇe two three
3674 fou«rˇ» five six
3675 seven «ˇeight nine
3676 »ten
3677 "});
3678 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3679 cx.assert_editor_state(indoc! {"
3680 oˇe two three
3681 fouˇ five six
3682 seven ˇten
3683 "});
3684
3685 // Test backspace inside and around indents
3686 cx.set_state(indoc! {"
3687 zero
3688 ˇone
3689 ˇtwo
3690 ˇ ˇ ˇ three
3691 ˇ ˇ four
3692 "});
3693 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3694 cx.assert_editor_state(indoc! {"
3695 zero
3696 ˇone
3697 ˇtwo
3698 ˇ threeˇ four
3699 "});
3700}
3701
3702#[gpui::test]
3703async fn test_delete(cx: &mut TestAppContext) {
3704 init_test(cx, |_| {});
3705
3706 let mut cx = EditorTestContext::new(cx).await;
3707 cx.set_state(indoc! {"
3708 onˇe two three
3709 fou«rˇ» five six
3710 seven «ˇeight nine
3711 »ten
3712 "});
3713 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3714 cx.assert_editor_state(indoc! {"
3715 onˇ two three
3716 fouˇ five six
3717 seven ˇten
3718 "});
3719}
3720
3721#[gpui::test]
3722fn test_delete_line(cx: &mut TestAppContext) {
3723 init_test(cx, |_| {});
3724
3725 let editor = cx.add_window(|window, cx| {
3726 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3727 build_editor(buffer, window, cx)
3728 });
3729 _ = editor.update(cx, |editor, window, cx| {
3730 editor.change_selections(None, window, cx, |s| {
3731 s.select_display_ranges([
3732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3733 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3734 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3735 ])
3736 });
3737 editor.delete_line(&DeleteLine, window, cx);
3738 assert_eq!(editor.display_text(cx), "ghi");
3739 assert_eq!(
3740 editor.selections.display_ranges(cx),
3741 vec![
3742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3744 ]
3745 );
3746 });
3747
3748 let editor = cx.add_window(|window, cx| {
3749 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3750 build_editor(buffer, window, cx)
3751 });
3752 _ = editor.update(cx, |editor, window, cx| {
3753 editor.change_selections(None, window, cx, |s| {
3754 s.select_display_ranges([
3755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3756 ])
3757 });
3758 editor.delete_line(&DeleteLine, window, cx);
3759 assert_eq!(editor.display_text(cx), "ghi\n");
3760 assert_eq!(
3761 editor.selections.display_ranges(cx),
3762 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3763 );
3764 });
3765}
3766
3767#[gpui::test]
3768fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3769 init_test(cx, |_| {});
3770
3771 cx.add_window(|window, cx| {
3772 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3773 let mut editor = build_editor(buffer.clone(), window, cx);
3774 let buffer = buffer.read(cx).as_singleton().unwrap();
3775
3776 assert_eq!(
3777 editor.selections.ranges::<Point>(cx),
3778 &[Point::new(0, 0)..Point::new(0, 0)]
3779 );
3780
3781 // When on single line, replace newline at end by space
3782 editor.join_lines(&JoinLines, window, cx);
3783 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3784 assert_eq!(
3785 editor.selections.ranges::<Point>(cx),
3786 &[Point::new(0, 3)..Point::new(0, 3)]
3787 );
3788
3789 // When multiple lines are selected, remove newlines that are spanned by the selection
3790 editor.change_selections(None, window, cx, |s| {
3791 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3792 });
3793 editor.join_lines(&JoinLines, window, cx);
3794 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3795 assert_eq!(
3796 editor.selections.ranges::<Point>(cx),
3797 &[Point::new(0, 11)..Point::new(0, 11)]
3798 );
3799
3800 // Undo should be transactional
3801 editor.undo(&Undo, window, cx);
3802 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3803 assert_eq!(
3804 editor.selections.ranges::<Point>(cx),
3805 &[Point::new(0, 5)..Point::new(2, 2)]
3806 );
3807
3808 // When joining an empty line don't insert a space
3809 editor.change_selections(None, window, cx, |s| {
3810 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3811 });
3812 editor.join_lines(&JoinLines, window, cx);
3813 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3814 assert_eq!(
3815 editor.selections.ranges::<Point>(cx),
3816 [Point::new(2, 3)..Point::new(2, 3)]
3817 );
3818
3819 // We can remove trailing newlines
3820 editor.join_lines(&JoinLines, window, cx);
3821 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3822 assert_eq!(
3823 editor.selections.ranges::<Point>(cx),
3824 [Point::new(2, 3)..Point::new(2, 3)]
3825 );
3826
3827 // We don't blow up on the last line
3828 editor.join_lines(&JoinLines, window, cx);
3829 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3830 assert_eq!(
3831 editor.selections.ranges::<Point>(cx),
3832 [Point::new(2, 3)..Point::new(2, 3)]
3833 );
3834
3835 // reset to test indentation
3836 editor.buffer.update(cx, |buffer, cx| {
3837 buffer.edit(
3838 [
3839 (Point::new(1, 0)..Point::new(1, 2), " "),
3840 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3841 ],
3842 None,
3843 cx,
3844 )
3845 });
3846
3847 // We remove any leading spaces
3848 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3849 editor.change_selections(None, window, cx, |s| {
3850 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3851 });
3852 editor.join_lines(&JoinLines, window, cx);
3853 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3854
3855 // We don't insert a space for a line containing only spaces
3856 editor.join_lines(&JoinLines, window, cx);
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3858
3859 // We ignore any leading tabs
3860 editor.join_lines(&JoinLines, window, cx);
3861 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3862
3863 editor
3864 });
3865}
3866
3867#[gpui::test]
3868fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3869 init_test(cx, |_| {});
3870
3871 cx.add_window(|window, cx| {
3872 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3873 let mut editor = build_editor(buffer.clone(), window, cx);
3874 let buffer = buffer.read(cx).as_singleton().unwrap();
3875
3876 editor.change_selections(None, window, cx, |s| {
3877 s.select_ranges([
3878 Point::new(0, 2)..Point::new(1, 1),
3879 Point::new(1, 2)..Point::new(1, 2),
3880 Point::new(3, 1)..Point::new(3, 2),
3881 ])
3882 });
3883
3884 editor.join_lines(&JoinLines, window, cx);
3885 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3886
3887 assert_eq!(
3888 editor.selections.ranges::<Point>(cx),
3889 [
3890 Point::new(0, 7)..Point::new(0, 7),
3891 Point::new(1, 3)..Point::new(1, 3)
3892 ]
3893 );
3894 editor
3895 });
3896}
3897
3898#[gpui::test]
3899async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903
3904 let diff_base = r#"
3905 Line 0
3906 Line 1
3907 Line 2
3908 Line 3
3909 "#
3910 .unindent();
3911
3912 cx.set_state(
3913 &r#"
3914 ˇLine 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent(),
3920 );
3921
3922 cx.set_head_text(&diff_base);
3923 executor.run_until_parked();
3924
3925 // Join lines
3926 cx.update_editor(|editor, window, cx| {
3927 editor.join_lines(&JoinLines, window, cx);
3928 });
3929 executor.run_until_parked();
3930
3931 cx.assert_editor_state(
3932 &r#"
3933 Line 0ˇ Line 1
3934 Line 2
3935 Line 3
3936 "#
3937 .unindent(),
3938 );
3939 // Join again
3940 cx.update_editor(|editor, window, cx| {
3941 editor.join_lines(&JoinLines, window, cx);
3942 });
3943 executor.run_until_parked();
3944
3945 cx.assert_editor_state(
3946 &r#"
3947 Line 0 Line 1ˇ Line 2
3948 Line 3
3949 "#
3950 .unindent(),
3951 );
3952}
3953
3954#[gpui::test]
3955async fn test_custom_newlines_cause_no_false_positive_diffs(
3956 executor: BackgroundExecutor,
3957 cx: &mut TestAppContext,
3958) {
3959 init_test(cx, |_| {});
3960 let mut cx = EditorTestContext::new(cx).await;
3961 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3962 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3963 executor.run_until_parked();
3964
3965 cx.update_editor(|editor, window, cx| {
3966 let snapshot = editor.snapshot(window, cx);
3967 assert_eq!(
3968 snapshot
3969 .buffer_snapshot
3970 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3971 .collect::<Vec<_>>(),
3972 Vec::new(),
3973 "Should not have any diffs for files with custom newlines"
3974 );
3975 });
3976}
3977
3978#[gpui::test]
3979async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
3980 init_test(cx, |_| {});
3981
3982 let mut cx = EditorTestContext::new(cx).await;
3983
3984 // Test sort_lines_case_insensitive()
3985 cx.set_state(indoc! {"
3986 «z
3987 y
3988 x
3989 Z
3990 Y
3991 Xˇ»
3992 "});
3993 cx.update_editor(|e, window, cx| {
3994 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3995 });
3996 cx.assert_editor_state(indoc! {"
3997 «x
3998 X
3999 y
4000 Y
4001 z
4002 Zˇ»
4003 "});
4004
4005 // Test reverse_lines()
4006 cx.set_state(indoc! {"
4007 «5
4008 4
4009 3
4010 2
4011 1ˇ»
4012 "});
4013 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 «1
4016 2
4017 3
4018 4
4019 5ˇ»
4020 "});
4021
4022 // Skip testing shuffle_line()
4023
4024 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4025 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4026
4027 // Don't manipulate when cursor is on single line, but expand the selection
4028 cx.set_state(indoc! {"
4029 ddˇdd
4030 ccc
4031 bb
4032 a
4033 "});
4034 cx.update_editor(|e, window, cx| {
4035 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4036 });
4037 cx.assert_editor_state(indoc! {"
4038 «ddddˇ»
4039 ccc
4040 bb
4041 a
4042 "});
4043
4044 // Basic manipulate case
4045 // Start selection moves to column 0
4046 // End of selection shrinks to fit shorter line
4047 cx.set_state(indoc! {"
4048 dd«d
4049 ccc
4050 bb
4051 aaaaaˇ»
4052 "});
4053 cx.update_editor(|e, window, cx| {
4054 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4055 });
4056 cx.assert_editor_state(indoc! {"
4057 «aaaaa
4058 bb
4059 ccc
4060 dddˇ»
4061 "});
4062
4063 // Manipulate case with newlines
4064 cx.set_state(indoc! {"
4065 dd«d
4066 ccc
4067
4068 bb
4069 aaaaa
4070
4071 ˇ»
4072 "});
4073 cx.update_editor(|e, window, cx| {
4074 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4075 });
4076 cx.assert_editor_state(indoc! {"
4077 «
4078
4079 aaaaa
4080 bb
4081 ccc
4082 dddˇ»
4083
4084 "});
4085
4086 // Adding new line
4087 cx.set_state(indoc! {"
4088 aa«a
4089 bbˇ»b
4090 "});
4091 cx.update_editor(|e, window, cx| {
4092 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4093 });
4094 cx.assert_editor_state(indoc! {"
4095 «aaa
4096 bbb
4097 added_lineˇ»
4098 "});
4099
4100 // Removing line
4101 cx.set_state(indoc! {"
4102 aa«a
4103 bbbˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.manipulate_immutable_lines(window, cx, |lines| {
4107 lines.pop();
4108 })
4109 });
4110 cx.assert_editor_state(indoc! {"
4111 «aaaˇ»
4112 "});
4113
4114 // Removing all lines
4115 cx.set_state(indoc! {"
4116 aa«a
4117 bbbˇ»
4118 "});
4119 cx.update_editor(|e, window, cx| {
4120 e.manipulate_immutable_lines(window, cx, |lines| {
4121 lines.drain(..);
4122 })
4123 });
4124 cx.assert_editor_state(indoc! {"
4125 ˇ
4126 "});
4127}
4128
4129#[gpui::test]
4130async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4131 init_test(cx, |_| {});
4132
4133 let mut cx = EditorTestContext::new(cx).await;
4134
4135 // Consider continuous selection as single selection
4136 cx.set_state(indoc! {"
4137 Aaa«aa
4138 cˇ»c«c
4139 bb
4140 aaaˇ»aa
4141 "});
4142 cx.update_editor(|e, window, cx| {
4143 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4144 });
4145 cx.assert_editor_state(indoc! {"
4146 «Aaaaa
4147 ccc
4148 bb
4149 aaaaaˇ»
4150 "});
4151
4152 cx.set_state(indoc! {"
4153 Aaa«aa
4154 cˇ»c«c
4155 bb
4156 aaaˇ»aa
4157 "});
4158 cx.update_editor(|e, window, cx| {
4159 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4160 });
4161 cx.assert_editor_state(indoc! {"
4162 «Aaaaa
4163 ccc
4164 bbˇ»
4165 "});
4166
4167 // Consider non continuous selection as distinct dedup operations
4168 cx.set_state(indoc! {"
4169 «aaaaa
4170 bb
4171 aaaaa
4172 aaaaaˇ»
4173
4174 aaa«aaˇ»
4175 "});
4176 cx.update_editor(|e, window, cx| {
4177 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4178 });
4179 cx.assert_editor_state(indoc! {"
4180 «aaaaa
4181 bbˇ»
4182
4183 «aaaaaˇ»
4184 "});
4185}
4186
4187#[gpui::test]
4188async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4189 init_test(cx, |_| {});
4190
4191 let mut cx = EditorTestContext::new(cx).await;
4192
4193 cx.set_state(indoc! {"
4194 «Aaa
4195 aAa
4196 Aaaˇ»
4197 "});
4198 cx.update_editor(|e, window, cx| {
4199 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4200 });
4201 cx.assert_editor_state(indoc! {"
4202 «Aaa
4203 aAaˇ»
4204 "});
4205
4206 cx.set_state(indoc! {"
4207 «Aaa
4208 aAa
4209 aaAˇ»
4210 "});
4211 cx.update_editor(|e, window, cx| {
4212 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4213 });
4214 cx.assert_editor_state(indoc! {"
4215 «Aaaˇ»
4216 "});
4217}
4218
4219#[gpui::test]
4220async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let mut cx = EditorTestContext::new(cx).await;
4224
4225 // Manipulate with multiple selections on a single line
4226 cx.set_state(indoc! {"
4227 dd«dd
4228 cˇ»c«c
4229 bb
4230 aaaˇ»aa
4231 "});
4232 cx.update_editor(|e, window, cx| {
4233 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4234 });
4235 cx.assert_editor_state(indoc! {"
4236 «aaaaa
4237 bb
4238 ccc
4239 ddddˇ»
4240 "});
4241
4242 // Manipulate with multiple disjoin selections
4243 cx.set_state(indoc! {"
4244 5«
4245 4
4246 3
4247 2
4248 1ˇ»
4249
4250 dd«dd
4251 ccc
4252 bb
4253 aaaˇ»aa
4254 "});
4255 cx.update_editor(|e, window, cx| {
4256 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4257 });
4258 cx.assert_editor_state(indoc! {"
4259 «1
4260 2
4261 3
4262 4
4263 5ˇ»
4264
4265 «aaaaa
4266 bb
4267 ccc
4268 ddddˇ»
4269 "});
4270
4271 // Adding lines on each selection
4272 cx.set_state(indoc! {"
4273 2«
4274 1ˇ»
4275
4276 bb«bb
4277 aaaˇ»aa
4278 "});
4279 cx.update_editor(|e, window, cx| {
4280 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4281 });
4282 cx.assert_editor_state(indoc! {"
4283 «2
4284 1
4285 added lineˇ»
4286
4287 «bbbb
4288 aaaaa
4289 added lineˇ»
4290 "});
4291
4292 // Removing lines on each selection
4293 cx.set_state(indoc! {"
4294 2«
4295 1ˇ»
4296
4297 bb«bb
4298 aaaˇ»aa
4299 "});
4300 cx.update_editor(|e, window, cx| {
4301 e.manipulate_immutable_lines(window, cx, |lines| {
4302 lines.pop();
4303 })
4304 });
4305 cx.assert_editor_state(indoc! {"
4306 «2ˇ»
4307
4308 «bbbbˇ»
4309 "});
4310}
4311
4312#[gpui::test]
4313async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4314 init_test(cx, |settings| {
4315 settings.defaults.tab_size = NonZeroU32::new(3)
4316 });
4317
4318 let mut cx = EditorTestContext::new(cx).await;
4319
4320 // MULTI SELECTION
4321 // Ln.1 "«" tests empty lines
4322 // Ln.9 tests just leading whitespace
4323 cx.set_state(indoc! {"
4324 «
4325 abc // No indentationˇ»
4326 «\tabc // 1 tabˇ»
4327 \t\tabc « ˇ» // 2 tabs
4328 \t ab«c // Tab followed by space
4329 \tabc // Space followed by tab (3 spaces should be the result)
4330 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4331 abˇ»ˇc ˇ ˇ // Already space indented«
4332 \t
4333 \tabc\tdef // Only the leading tab is manipulatedˇ»
4334 "});
4335 cx.update_editor(|e, window, cx| {
4336 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4337 });
4338 cx.assert_editor_state(indoc! {"
4339 «
4340 abc // No indentation
4341 abc // 1 tab
4342 abc // 2 tabs
4343 abc // Tab followed by space
4344 abc // Space followed by tab (3 spaces should be the result)
4345 abc // Mixed indentation (tab conversion depends on the column)
4346 abc // Already space indented
4347
4348 abc\tdef // Only the leading tab is manipulatedˇ»
4349 "});
4350
4351 // Test on just a few lines, the others should remain unchanged
4352 // Only lines (3, 5, 10, 11) should change
4353 cx.set_state(indoc! {"
4354
4355 abc // No indentation
4356 \tabcˇ // 1 tab
4357 \t\tabc // 2 tabs
4358 \t abcˇ // Tab followed by space
4359 \tabc // Space followed by tab (3 spaces should be the result)
4360 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4361 abc // Already space indented
4362 «\t
4363 \tabc\tdef // Only the leading tab is manipulatedˇ»
4364 "});
4365 cx.update_editor(|e, window, cx| {
4366 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4367 });
4368 cx.assert_editor_state(indoc! {"
4369
4370 abc // No indentation
4371 « abc // 1 tabˇ»
4372 \t\tabc // 2 tabs
4373 « abc // Tab followed by spaceˇ»
4374 \tabc // Space followed by tab (3 spaces should be the result)
4375 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4376 abc // Already space indented
4377 «
4378 abc\tdef // Only the leading tab is manipulatedˇ»
4379 "});
4380
4381 // SINGLE SELECTION
4382 // Ln.1 "«" tests empty lines
4383 // Ln.9 tests just leading whitespace
4384 cx.set_state(indoc! {"
4385 «
4386 abc // No indentation
4387 \tabc // 1 tab
4388 \t\tabc // 2 tabs
4389 \t abc // Tab followed by space
4390 \tabc // Space followed by tab (3 spaces should be the result)
4391 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4392 abc // Already space indented
4393 \t
4394 \tabc\tdef // Only the leading tab is manipulatedˇ»
4395 "});
4396 cx.update_editor(|e, window, cx| {
4397 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4398 });
4399 cx.assert_editor_state(indoc! {"
4400 «
4401 abc // No indentation
4402 abc // 1 tab
4403 abc // 2 tabs
4404 abc // Tab followed by space
4405 abc // Space followed by tab (3 spaces should be the result)
4406 abc // Mixed indentation (tab conversion depends on the column)
4407 abc // Already space indented
4408
4409 abc\tdef // Only the leading tab is manipulatedˇ»
4410 "});
4411}
4412
4413#[gpui::test]
4414async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4415 init_test(cx, |settings| {
4416 settings.defaults.tab_size = NonZeroU32::new(3)
4417 });
4418
4419 let mut cx = EditorTestContext::new(cx).await;
4420
4421 // MULTI SELECTION
4422 // Ln.1 "«" tests empty lines
4423 // Ln.11 tests just leading whitespace
4424 cx.set_state(indoc! {"
4425 «
4426 abˇ»ˇc // No indentation
4427 abc ˇ ˇ // 1 space (< 3 so dont convert)
4428 abc « // 2 spaces (< 3 so dont convert)
4429 abc // 3 spaces (convert)
4430 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4431 «\tˇ»\t«\tˇ»abc // Already tab indented
4432 «\t abc // Tab followed by space
4433 \tabc // Space followed by tab (should be consumed due to tab)
4434 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4435 \tˇ» «\t
4436 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4437 "});
4438 cx.update_editor(|e, window, cx| {
4439 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4440 });
4441 cx.assert_editor_state(indoc! {"
4442 «
4443 abc // No indentation
4444 abc // 1 space (< 3 so dont convert)
4445 abc // 2 spaces (< 3 so dont convert)
4446 \tabc // 3 spaces (convert)
4447 \t abc // 5 spaces (1 tab + 2 spaces)
4448 \t\t\tabc // Already tab indented
4449 \t abc // Tab followed by space
4450 \tabc // Space followed by tab (should be consumed due to tab)
4451 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4452 \t\t\t
4453 \tabc \t // Only the leading spaces should be convertedˇ»
4454 "});
4455
4456 // Test on just a few lines, the other should remain unchanged
4457 // Only lines (4, 8, 11, 12) should change
4458 cx.set_state(indoc! {"
4459
4460 abc // No indentation
4461 abc // 1 space (< 3 so dont convert)
4462 abc // 2 spaces (< 3 so dont convert)
4463 « abc // 3 spaces (convert)ˇ»
4464 abc // 5 spaces (1 tab + 2 spaces)
4465 \t\t\tabc // Already tab indented
4466 \t abc // Tab followed by space
4467 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4468 \t\t \tabc // Mixed indentation
4469 \t \t \t \tabc // Mixed indentation
4470 \t \tˇ
4471 « abc \t // Only the leading spaces should be convertedˇ»
4472 "});
4473 cx.update_editor(|e, window, cx| {
4474 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4475 });
4476 cx.assert_editor_state(indoc! {"
4477
4478 abc // No indentation
4479 abc // 1 space (< 3 so dont convert)
4480 abc // 2 spaces (< 3 so dont convert)
4481 «\tabc // 3 spaces (convert)ˇ»
4482 abc // 5 spaces (1 tab + 2 spaces)
4483 \t\t\tabc // Already tab indented
4484 \t abc // Tab followed by space
4485 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4486 \t\t \tabc // Mixed indentation
4487 \t \t \t \tabc // Mixed indentation
4488 «\t\t\t
4489 \tabc \t // Only the leading spaces should be convertedˇ»
4490 "});
4491
4492 // SINGLE SELECTION
4493 // Ln.1 "«" tests empty lines
4494 // Ln.11 tests just leading whitespace
4495 cx.set_state(indoc! {"
4496 «
4497 abc // No indentation
4498 abc // 1 space (< 3 so dont convert)
4499 abc // 2 spaces (< 3 so dont convert)
4500 abc // 3 spaces (convert)
4501 abc // 5 spaces (1 tab + 2 spaces)
4502 \t\t\tabc // Already tab indented
4503 \t abc // Tab followed by space
4504 \tabc // Space followed by tab (should be consumed due to tab)
4505 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4506 \t \t
4507 abc \t // Only the leading spaces should be convertedˇ»
4508 "});
4509 cx.update_editor(|e, window, cx| {
4510 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4511 });
4512 cx.assert_editor_state(indoc! {"
4513 «
4514 abc // No indentation
4515 abc // 1 space (< 3 so dont convert)
4516 abc // 2 spaces (< 3 so dont convert)
4517 \tabc // 3 spaces (convert)
4518 \t abc // 5 spaces (1 tab + 2 spaces)
4519 \t\t\tabc // Already tab indented
4520 \t abc // Tab followed by space
4521 \tabc // Space followed by tab (should be consumed due to tab)
4522 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4523 \t\t\t
4524 \tabc \t // Only the leading spaces should be convertedˇ»
4525 "});
4526}
4527
4528#[gpui::test]
4529async fn test_toggle_case(cx: &mut TestAppContext) {
4530 init_test(cx, |_| {});
4531
4532 let mut cx = EditorTestContext::new(cx).await;
4533
4534 // If all lower case -> upper case
4535 cx.set_state(indoc! {"
4536 «hello worldˇ»
4537 "});
4538 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4539 cx.assert_editor_state(indoc! {"
4540 «HELLO WORLDˇ»
4541 "});
4542
4543 // If all upper case -> lower case
4544 cx.set_state(indoc! {"
4545 «HELLO WORLDˇ»
4546 "});
4547 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4548 cx.assert_editor_state(indoc! {"
4549 «hello worldˇ»
4550 "});
4551
4552 // If any upper case characters are identified -> lower case
4553 // This matches JetBrains IDEs
4554 cx.set_state(indoc! {"
4555 «hEllo worldˇ»
4556 "});
4557 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4558 cx.assert_editor_state(indoc! {"
4559 «hello worldˇ»
4560 "});
4561}
4562
4563#[gpui::test]
4564async fn test_manipulate_text(cx: &mut TestAppContext) {
4565 init_test(cx, |_| {});
4566
4567 let mut cx = EditorTestContext::new(cx).await;
4568
4569 // Test convert_to_upper_case()
4570 cx.set_state(indoc! {"
4571 «hello worldˇ»
4572 "});
4573 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4574 cx.assert_editor_state(indoc! {"
4575 «HELLO WORLDˇ»
4576 "});
4577
4578 // Test convert_to_lower_case()
4579 cx.set_state(indoc! {"
4580 «HELLO WORLDˇ»
4581 "});
4582 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4583 cx.assert_editor_state(indoc! {"
4584 «hello worldˇ»
4585 "});
4586
4587 // Test multiple line, single selection case
4588 cx.set_state(indoc! {"
4589 «The quick brown
4590 fox jumps over
4591 the lazy dogˇ»
4592 "});
4593 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4594 cx.assert_editor_state(indoc! {"
4595 «The Quick Brown
4596 Fox Jumps Over
4597 The Lazy Dogˇ»
4598 "});
4599
4600 // Test multiple line, single selection case
4601 cx.set_state(indoc! {"
4602 «The quick brown
4603 fox jumps over
4604 the lazy dogˇ»
4605 "});
4606 cx.update_editor(|e, window, cx| {
4607 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4608 });
4609 cx.assert_editor_state(indoc! {"
4610 «TheQuickBrown
4611 FoxJumpsOver
4612 TheLazyDogˇ»
4613 "});
4614
4615 // From here on out, test more complex cases of manipulate_text()
4616
4617 // Test no selection case - should affect words cursors are in
4618 // Cursor at beginning, middle, and end of word
4619 cx.set_state(indoc! {"
4620 ˇhello big beauˇtiful worldˇ
4621 "});
4622 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4623 cx.assert_editor_state(indoc! {"
4624 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4625 "});
4626
4627 // Test multiple selections on a single line and across multiple lines
4628 cx.set_state(indoc! {"
4629 «Theˇ» quick «brown
4630 foxˇ» jumps «overˇ»
4631 the «lazyˇ» dog
4632 "});
4633 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4634 cx.assert_editor_state(indoc! {"
4635 «THEˇ» quick «BROWN
4636 FOXˇ» jumps «OVERˇ»
4637 the «LAZYˇ» dog
4638 "});
4639
4640 // Test case where text length grows
4641 cx.set_state(indoc! {"
4642 «tschüߡ»
4643 "});
4644 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4645 cx.assert_editor_state(indoc! {"
4646 «TSCHÜSSˇ»
4647 "});
4648
4649 // Test to make sure we don't crash when text shrinks
4650 cx.set_state(indoc! {"
4651 aaa_bbbˇ
4652 "});
4653 cx.update_editor(|e, window, cx| {
4654 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4655 });
4656 cx.assert_editor_state(indoc! {"
4657 «aaaBbbˇ»
4658 "});
4659
4660 // Test to make sure we all aware of the fact that each word can grow and shrink
4661 // Final selections should be aware of this fact
4662 cx.set_state(indoc! {"
4663 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4664 "});
4665 cx.update_editor(|e, window, cx| {
4666 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4667 });
4668 cx.assert_editor_state(indoc! {"
4669 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4670 "});
4671
4672 cx.set_state(indoc! {"
4673 «hElLo, WoRld!ˇ»
4674 "});
4675 cx.update_editor(|e, window, cx| {
4676 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4677 });
4678 cx.assert_editor_state(indoc! {"
4679 «HeLlO, wOrLD!ˇ»
4680 "});
4681}
4682
4683#[gpui::test]
4684fn test_duplicate_line(cx: &mut TestAppContext) {
4685 init_test(cx, |_| {});
4686
4687 let editor = cx.add_window(|window, cx| {
4688 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4689 build_editor(buffer, window, cx)
4690 });
4691 _ = editor.update(cx, |editor, window, cx| {
4692 editor.change_selections(None, window, cx, |s| {
4693 s.select_display_ranges([
4694 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4695 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4696 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4697 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4698 ])
4699 });
4700 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4701 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4702 assert_eq!(
4703 editor.selections.display_ranges(cx),
4704 vec![
4705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4706 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4707 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4708 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4709 ]
4710 );
4711 });
4712
4713 let editor = cx.add_window(|window, cx| {
4714 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4715 build_editor(buffer, window, cx)
4716 });
4717 _ = editor.update(cx, |editor, window, cx| {
4718 editor.change_selections(None, window, cx, |s| {
4719 s.select_display_ranges([
4720 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4721 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4722 ])
4723 });
4724 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4725 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4726 assert_eq!(
4727 editor.selections.display_ranges(cx),
4728 vec![
4729 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4730 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4731 ]
4732 );
4733 });
4734
4735 // With `move_upwards` the selections stay in place, except for
4736 // the lines inserted above them
4737 let editor = cx.add_window(|window, cx| {
4738 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4739 build_editor(buffer, window, cx)
4740 });
4741 _ = editor.update(cx, |editor, window, cx| {
4742 editor.change_selections(None, window, cx, |s| {
4743 s.select_display_ranges([
4744 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4745 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4746 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4747 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4748 ])
4749 });
4750 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4751 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4752 assert_eq!(
4753 editor.selections.display_ranges(cx),
4754 vec![
4755 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4756 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4757 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4758 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4759 ]
4760 );
4761 });
4762
4763 let editor = cx.add_window(|window, cx| {
4764 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4765 build_editor(buffer, window, cx)
4766 });
4767 _ = editor.update(cx, |editor, window, cx| {
4768 editor.change_selections(None, window, cx, |s| {
4769 s.select_display_ranges([
4770 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4771 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4772 ])
4773 });
4774 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4775 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4776 assert_eq!(
4777 editor.selections.display_ranges(cx),
4778 vec![
4779 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4780 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4781 ]
4782 );
4783 });
4784
4785 let editor = cx.add_window(|window, cx| {
4786 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4787 build_editor(buffer, window, cx)
4788 });
4789 _ = editor.update(cx, |editor, window, cx| {
4790 editor.change_selections(None, window, cx, |s| {
4791 s.select_display_ranges([
4792 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4793 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4794 ])
4795 });
4796 editor.duplicate_selection(&DuplicateSelection, window, cx);
4797 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4798 assert_eq!(
4799 editor.selections.display_ranges(cx),
4800 vec![
4801 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4802 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4803 ]
4804 );
4805 });
4806}
4807
4808#[gpui::test]
4809fn test_move_line_up_down(cx: &mut TestAppContext) {
4810 init_test(cx, |_| {});
4811
4812 let editor = cx.add_window(|window, cx| {
4813 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4814 build_editor(buffer, window, cx)
4815 });
4816 _ = editor.update(cx, |editor, window, cx| {
4817 editor.fold_creases(
4818 vec![
4819 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4820 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4821 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4822 ],
4823 true,
4824 window,
4825 cx,
4826 );
4827 editor.change_selections(None, window, cx, |s| {
4828 s.select_display_ranges([
4829 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4830 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4831 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4832 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4833 ])
4834 });
4835 assert_eq!(
4836 editor.display_text(cx),
4837 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4838 );
4839
4840 editor.move_line_up(&MoveLineUp, window, cx);
4841 assert_eq!(
4842 editor.display_text(cx),
4843 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4844 );
4845 assert_eq!(
4846 editor.selections.display_ranges(cx),
4847 vec![
4848 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4849 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4850 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4851 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4852 ]
4853 );
4854 });
4855
4856 _ = editor.update(cx, |editor, window, cx| {
4857 editor.move_line_down(&MoveLineDown, window, cx);
4858 assert_eq!(
4859 editor.display_text(cx),
4860 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4861 );
4862 assert_eq!(
4863 editor.selections.display_ranges(cx),
4864 vec![
4865 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4866 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4867 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4868 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4869 ]
4870 );
4871 });
4872
4873 _ = editor.update(cx, |editor, window, cx| {
4874 editor.move_line_down(&MoveLineDown, window, cx);
4875 assert_eq!(
4876 editor.display_text(cx),
4877 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4878 );
4879 assert_eq!(
4880 editor.selections.display_ranges(cx),
4881 vec![
4882 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4883 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4884 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4885 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4886 ]
4887 );
4888 });
4889
4890 _ = editor.update(cx, |editor, window, cx| {
4891 editor.move_line_up(&MoveLineUp, window, cx);
4892 assert_eq!(
4893 editor.display_text(cx),
4894 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4895 );
4896 assert_eq!(
4897 editor.selections.display_ranges(cx),
4898 vec![
4899 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4900 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4901 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4902 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4903 ]
4904 );
4905 });
4906}
4907
4908#[gpui::test]
4909fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4910 init_test(cx, |_| {});
4911
4912 let editor = cx.add_window(|window, cx| {
4913 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4914 build_editor(buffer, window, cx)
4915 });
4916 _ = editor.update(cx, |editor, window, cx| {
4917 let snapshot = editor.buffer.read(cx).snapshot(cx);
4918 editor.insert_blocks(
4919 [BlockProperties {
4920 style: BlockStyle::Fixed,
4921 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4922 height: Some(1),
4923 render: Arc::new(|_| div().into_any()),
4924 priority: 0,
4925 render_in_minimap: true,
4926 }],
4927 Some(Autoscroll::fit()),
4928 cx,
4929 );
4930 editor.change_selections(None, window, cx, |s| {
4931 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4932 });
4933 editor.move_line_down(&MoveLineDown, window, cx);
4934 });
4935}
4936
4937#[gpui::test]
4938async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4939 init_test(cx, |_| {});
4940
4941 let mut cx = EditorTestContext::new(cx).await;
4942 cx.set_state(
4943 &"
4944 ˇzero
4945 one
4946 two
4947 three
4948 four
4949 five
4950 "
4951 .unindent(),
4952 );
4953
4954 // Create a four-line block that replaces three lines of text.
4955 cx.update_editor(|editor, window, cx| {
4956 let snapshot = editor.snapshot(window, cx);
4957 let snapshot = &snapshot.buffer_snapshot;
4958 let placement = BlockPlacement::Replace(
4959 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4960 );
4961 editor.insert_blocks(
4962 [BlockProperties {
4963 placement,
4964 height: Some(4),
4965 style: BlockStyle::Sticky,
4966 render: Arc::new(|_| gpui::div().into_any_element()),
4967 priority: 0,
4968 render_in_minimap: true,
4969 }],
4970 None,
4971 cx,
4972 );
4973 });
4974
4975 // Move down so that the cursor touches the block.
4976 cx.update_editor(|editor, window, cx| {
4977 editor.move_down(&Default::default(), window, cx);
4978 });
4979 cx.assert_editor_state(
4980 &"
4981 zero
4982 «one
4983 two
4984 threeˇ»
4985 four
4986 five
4987 "
4988 .unindent(),
4989 );
4990
4991 // Move down past the block.
4992 cx.update_editor(|editor, window, cx| {
4993 editor.move_down(&Default::default(), window, cx);
4994 });
4995 cx.assert_editor_state(
4996 &"
4997 zero
4998 one
4999 two
5000 three
5001 ˇfour
5002 five
5003 "
5004 .unindent(),
5005 );
5006}
5007
5008#[gpui::test]
5009fn test_transpose(cx: &mut TestAppContext) {
5010 init_test(cx, |_| {});
5011
5012 _ = cx.add_window(|window, cx| {
5013 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5014 editor.set_style(EditorStyle::default(), window, cx);
5015 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
5016 editor.transpose(&Default::default(), window, cx);
5017 assert_eq!(editor.text(cx), "bac");
5018 assert_eq!(editor.selections.ranges(cx), [2..2]);
5019
5020 editor.transpose(&Default::default(), window, cx);
5021 assert_eq!(editor.text(cx), "bca");
5022 assert_eq!(editor.selections.ranges(cx), [3..3]);
5023
5024 editor.transpose(&Default::default(), window, cx);
5025 assert_eq!(editor.text(cx), "bac");
5026 assert_eq!(editor.selections.ranges(cx), [3..3]);
5027
5028 editor
5029 });
5030
5031 _ = cx.add_window(|window, cx| {
5032 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5033 editor.set_style(EditorStyle::default(), window, cx);
5034 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
5035 editor.transpose(&Default::default(), window, cx);
5036 assert_eq!(editor.text(cx), "acb\nde");
5037 assert_eq!(editor.selections.ranges(cx), [3..3]);
5038
5039 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
5040 editor.transpose(&Default::default(), window, cx);
5041 assert_eq!(editor.text(cx), "acbd\ne");
5042 assert_eq!(editor.selections.ranges(cx), [5..5]);
5043
5044 editor.transpose(&Default::default(), window, cx);
5045 assert_eq!(editor.text(cx), "acbde\n");
5046 assert_eq!(editor.selections.ranges(cx), [6..6]);
5047
5048 editor.transpose(&Default::default(), window, cx);
5049 assert_eq!(editor.text(cx), "acbd\ne");
5050 assert_eq!(editor.selections.ranges(cx), [6..6]);
5051
5052 editor
5053 });
5054
5055 _ = cx.add_window(|window, cx| {
5056 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5057 editor.set_style(EditorStyle::default(), window, cx);
5058 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
5059 editor.transpose(&Default::default(), window, cx);
5060 assert_eq!(editor.text(cx), "bacd\ne");
5061 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5062
5063 editor.transpose(&Default::default(), window, cx);
5064 assert_eq!(editor.text(cx), "bcade\n");
5065 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5066
5067 editor.transpose(&Default::default(), window, cx);
5068 assert_eq!(editor.text(cx), "bcda\ne");
5069 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5070
5071 editor.transpose(&Default::default(), window, cx);
5072 assert_eq!(editor.text(cx), "bcade\n");
5073 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5074
5075 editor.transpose(&Default::default(), window, cx);
5076 assert_eq!(editor.text(cx), "bcaed\n");
5077 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5078
5079 editor
5080 });
5081
5082 _ = cx.add_window(|window, cx| {
5083 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5084 editor.set_style(EditorStyle::default(), window, cx);
5085 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
5086 editor.transpose(&Default::default(), window, cx);
5087 assert_eq!(editor.text(cx), "🏀🍐✋");
5088 assert_eq!(editor.selections.ranges(cx), [8..8]);
5089
5090 editor.transpose(&Default::default(), window, cx);
5091 assert_eq!(editor.text(cx), "🏀✋🍐");
5092 assert_eq!(editor.selections.ranges(cx), [11..11]);
5093
5094 editor.transpose(&Default::default(), window, cx);
5095 assert_eq!(editor.text(cx), "🏀🍐✋");
5096 assert_eq!(editor.selections.ranges(cx), [11..11]);
5097
5098 editor
5099 });
5100}
5101
5102#[gpui::test]
5103async fn test_rewrap(cx: &mut TestAppContext) {
5104 init_test(cx, |settings| {
5105 settings.languages.extend([
5106 (
5107 "Markdown".into(),
5108 LanguageSettingsContent {
5109 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5110 ..Default::default()
5111 },
5112 ),
5113 (
5114 "Plain Text".into(),
5115 LanguageSettingsContent {
5116 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5117 ..Default::default()
5118 },
5119 ),
5120 ])
5121 });
5122
5123 let mut cx = EditorTestContext::new(cx).await;
5124
5125 let language_with_c_comments = Arc::new(Language::new(
5126 LanguageConfig {
5127 line_comments: vec!["// ".into()],
5128 ..LanguageConfig::default()
5129 },
5130 None,
5131 ));
5132 let language_with_pound_comments = Arc::new(Language::new(
5133 LanguageConfig {
5134 line_comments: vec!["# ".into()],
5135 ..LanguageConfig::default()
5136 },
5137 None,
5138 ));
5139 let markdown_language = Arc::new(Language::new(
5140 LanguageConfig {
5141 name: "Markdown".into(),
5142 ..LanguageConfig::default()
5143 },
5144 None,
5145 ));
5146 let language_with_doc_comments = Arc::new(Language::new(
5147 LanguageConfig {
5148 line_comments: vec!["// ".into(), "/// ".into()],
5149 ..LanguageConfig::default()
5150 },
5151 Some(tree_sitter_rust::LANGUAGE.into()),
5152 ));
5153
5154 let plaintext_language = Arc::new(Language::new(
5155 LanguageConfig {
5156 name: "Plain Text".into(),
5157 ..LanguageConfig::default()
5158 },
5159 None,
5160 ));
5161
5162 assert_rewrap(
5163 indoc! {"
5164 // ˇ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.
5165 "},
5166 indoc! {"
5167 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5168 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5169 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
5170 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5171 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
5172 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5173 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5174 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5175 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5176 // porttitor id. Aliquam id accumsan eros.
5177 "},
5178 language_with_c_comments.clone(),
5179 &mut cx,
5180 );
5181
5182 // Test that rewrapping works inside of a selection
5183 assert_rewrap(
5184 indoc! {"
5185 «// 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.ˇ»
5186 "},
5187 indoc! {"
5188 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5189 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5190 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
5191 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5192 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
5193 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5194 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5195 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5196 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5197 // porttitor id. Aliquam id accumsan eros.ˇ»
5198 "},
5199 language_with_c_comments.clone(),
5200 &mut cx,
5201 );
5202
5203 // Test that cursors that expand to the same region are collapsed.
5204 assert_rewrap(
5205 indoc! {"
5206 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5207 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5208 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5209 // ˇ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.
5210 "},
5211 indoc! {"
5212 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5213 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5214 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
5215 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5216 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5217 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5218 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5219 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5220 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5221 // porttitor id. Aliquam id accumsan eros.
5222 "},
5223 language_with_c_comments.clone(),
5224 &mut cx,
5225 );
5226
5227 // Test that non-contiguous selections are treated separately.
5228 assert_rewrap(
5229 indoc! {"
5230 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5231 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5232 //
5233 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5234 // ˇ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.
5235 "},
5236 indoc! {"
5237 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5238 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5239 // auctor, eu lacinia sapien scelerisque.
5240 //
5241 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5242 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5243 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5244 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5245 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5246 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5247 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5248 "},
5249 language_with_c_comments.clone(),
5250 &mut cx,
5251 );
5252
5253 // Test that different comment prefixes are supported.
5254 assert_rewrap(
5255 indoc! {"
5256 # ˇ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.
5257 "},
5258 indoc! {"
5259 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5260 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5261 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5262 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5263 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5264 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5265 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5266 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5267 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5268 # accumsan eros.
5269 "},
5270 language_with_pound_comments.clone(),
5271 &mut cx,
5272 );
5273
5274 // Test that rewrapping is ignored outside of comments in most languages.
5275 assert_rewrap(
5276 indoc! {"
5277 /// Adds two numbers.
5278 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5279 fn add(a: u32, b: u32) -> u32 {
5280 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ˇ
5281 }
5282 "},
5283 indoc! {"
5284 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5285 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5286 fn add(a: u32, b: u32) -> u32 {
5287 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ˇ
5288 }
5289 "},
5290 language_with_doc_comments.clone(),
5291 &mut cx,
5292 );
5293
5294 // Test that rewrapping works in Markdown and Plain Text languages.
5295 assert_rewrap(
5296 indoc! {"
5297 # Hello
5298
5299 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.
5300 "},
5301 indoc! {"
5302 # Hello
5303
5304 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5305 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5306 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5307 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5308 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5309 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5310 Integer sit amet scelerisque nisi.
5311 "},
5312 markdown_language,
5313 &mut cx,
5314 );
5315
5316 assert_rewrap(
5317 indoc! {"
5318 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.
5319 "},
5320 indoc! {"
5321 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5322 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5323 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5324 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5325 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5326 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5327 Integer sit amet scelerisque nisi.
5328 "},
5329 plaintext_language.clone(),
5330 &mut cx,
5331 );
5332
5333 // Test rewrapping unaligned comments in a selection.
5334 assert_rewrap(
5335 indoc! {"
5336 fn foo() {
5337 if true {
5338 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5339 // Praesent semper egestas tellus id dignissim.ˇ»
5340 do_something();
5341 } else {
5342 //
5343 }
5344 }
5345 "},
5346 indoc! {"
5347 fn foo() {
5348 if true {
5349 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5350 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5351 // egestas tellus id dignissim.ˇ»
5352 do_something();
5353 } else {
5354 //
5355 }
5356 }
5357 "},
5358 language_with_doc_comments.clone(),
5359 &mut cx,
5360 );
5361
5362 assert_rewrap(
5363 indoc! {"
5364 fn foo() {
5365 if true {
5366 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5367 // Praesent semper egestas tellus id dignissim.»
5368 do_something();
5369 } else {
5370 //
5371 }
5372
5373 }
5374 "},
5375 indoc! {"
5376 fn foo() {
5377 if true {
5378 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5379 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5380 // egestas tellus id dignissim.»
5381 do_something();
5382 } else {
5383 //
5384 }
5385
5386 }
5387 "},
5388 language_with_doc_comments.clone(),
5389 &mut cx,
5390 );
5391
5392 assert_rewrap(
5393 indoc! {"
5394 «ˇone one one one one one one one one one one one one one one one one one one one one one one one one
5395
5396 two»
5397
5398 three
5399
5400 «ˇ\t
5401
5402 four four four four four four four four four four four four four four four four four four four four»
5403
5404 «ˇfive five five five five five five five five five five five five five five five five five five five
5405 \t»
5406 six six six six six six six six six six six six six six six six six six six six six six six six six
5407 "},
5408 indoc! {"
5409 «ˇone one one one one one one one one one one one one one one one one one one one
5410 one one one one one
5411
5412 two»
5413
5414 three
5415
5416 «ˇ\t
5417
5418 four four four four four four four four four four four four four four four four
5419 four four four four»
5420
5421 «ˇfive five five five five five five five five five five five five five five five
5422 five five five five
5423 \t»
5424 six six six six six six six six six six six six six six six six six six six six six six six six six
5425 "},
5426 plaintext_language.clone(),
5427 &mut cx,
5428 );
5429
5430 assert_rewrap(
5431 indoc! {"
5432 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5433 //ˇ
5434 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5435 //ˇ short short short
5436 int main(void) {
5437 return 17;
5438 }
5439 "},
5440 indoc! {"
5441 //ˇ long long long long long long long long long long long long long long long
5442 // long long long long long long long long long long long long long
5443 //ˇ
5444 //ˇ long long long long long long long long long long long long long long long
5445 //ˇ long long long long long long long long long long long long long short short
5446 // short
5447 int main(void) {
5448 return 17;
5449 }
5450 "},
5451 language_with_c_comments,
5452 &mut cx,
5453 );
5454
5455 #[track_caller]
5456 fn assert_rewrap(
5457 unwrapped_text: &str,
5458 wrapped_text: &str,
5459 language: Arc<Language>,
5460 cx: &mut EditorTestContext,
5461 ) {
5462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5463 cx.set_state(unwrapped_text);
5464 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5465 cx.assert_editor_state(wrapped_text);
5466 }
5467}
5468
5469#[gpui::test]
5470async fn test_hard_wrap(cx: &mut TestAppContext) {
5471 init_test(cx, |_| {});
5472 let mut cx = EditorTestContext::new(cx).await;
5473
5474 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5475 cx.update_editor(|editor, _, cx| {
5476 editor.set_hard_wrap(Some(14), cx);
5477 });
5478
5479 cx.set_state(indoc!(
5480 "
5481 one two three ˇ
5482 "
5483 ));
5484 cx.simulate_input("four");
5485 cx.run_until_parked();
5486
5487 cx.assert_editor_state(indoc!(
5488 "
5489 one two three
5490 fourˇ
5491 "
5492 ));
5493
5494 cx.update_editor(|editor, window, cx| {
5495 editor.newline(&Default::default(), window, cx);
5496 });
5497 cx.run_until_parked();
5498 cx.assert_editor_state(indoc!(
5499 "
5500 one two three
5501 four
5502 ˇ
5503 "
5504 ));
5505
5506 cx.simulate_input("five");
5507 cx.run_until_parked();
5508 cx.assert_editor_state(indoc!(
5509 "
5510 one two three
5511 four
5512 fiveˇ
5513 "
5514 ));
5515
5516 cx.update_editor(|editor, window, cx| {
5517 editor.newline(&Default::default(), window, cx);
5518 });
5519 cx.run_until_parked();
5520 cx.simulate_input("# ");
5521 cx.run_until_parked();
5522 cx.assert_editor_state(indoc!(
5523 "
5524 one two three
5525 four
5526 five
5527 # ˇ
5528 "
5529 ));
5530
5531 cx.update_editor(|editor, window, cx| {
5532 editor.newline(&Default::default(), window, cx);
5533 });
5534 cx.run_until_parked();
5535 cx.assert_editor_state(indoc!(
5536 "
5537 one two three
5538 four
5539 five
5540 #\x20
5541 #ˇ
5542 "
5543 ));
5544
5545 cx.simulate_input(" 6");
5546 cx.run_until_parked();
5547 cx.assert_editor_state(indoc!(
5548 "
5549 one two three
5550 four
5551 five
5552 #
5553 # 6ˇ
5554 "
5555 ));
5556}
5557
5558#[gpui::test]
5559async fn test_clipboard(cx: &mut TestAppContext) {
5560 init_test(cx, |_| {});
5561
5562 let mut cx = EditorTestContext::new(cx).await;
5563
5564 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5565 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5566 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5567
5568 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5569 cx.set_state("two ˇfour ˇsix ˇ");
5570 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5571 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5572
5573 // Paste again but with only two cursors. Since the number of cursors doesn't
5574 // match the number of slices in the clipboard, the entire clipboard text
5575 // is pasted at each cursor.
5576 cx.set_state("ˇtwo one✅ four three six five ˇ");
5577 cx.update_editor(|e, window, cx| {
5578 e.handle_input("( ", window, cx);
5579 e.paste(&Paste, window, cx);
5580 e.handle_input(") ", window, cx);
5581 });
5582 cx.assert_editor_state(
5583 &([
5584 "( one✅ ",
5585 "three ",
5586 "five ) ˇtwo one✅ four three six five ( one✅ ",
5587 "three ",
5588 "five ) ˇ",
5589 ]
5590 .join("\n")),
5591 );
5592
5593 // Cut with three selections, one of which is full-line.
5594 cx.set_state(indoc! {"
5595 1«2ˇ»3
5596 4ˇ567
5597 «8ˇ»9"});
5598 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5599 cx.assert_editor_state(indoc! {"
5600 1ˇ3
5601 ˇ9"});
5602
5603 // Paste with three selections, noticing how the copied selection that was full-line
5604 // gets inserted before the second cursor.
5605 cx.set_state(indoc! {"
5606 1ˇ3
5607 9ˇ
5608 «oˇ»ne"});
5609 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5610 cx.assert_editor_state(indoc! {"
5611 12ˇ3
5612 4567
5613 9ˇ
5614 8ˇne"});
5615
5616 // Copy with a single cursor only, which writes the whole line into the clipboard.
5617 cx.set_state(indoc! {"
5618 The quick brown
5619 fox juˇmps over
5620 the lazy dog"});
5621 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5622 assert_eq!(
5623 cx.read_from_clipboard()
5624 .and_then(|item| item.text().as_deref().map(str::to_string)),
5625 Some("fox jumps over\n".to_string())
5626 );
5627
5628 // Paste with three selections, noticing how the copied full-line selection is inserted
5629 // before the empty selections but replaces the selection that is non-empty.
5630 cx.set_state(indoc! {"
5631 Tˇhe quick brown
5632 «foˇ»x jumps over
5633 tˇhe lazy dog"});
5634 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5635 cx.assert_editor_state(indoc! {"
5636 fox jumps over
5637 Tˇhe quick brown
5638 fox jumps over
5639 ˇx jumps over
5640 fox jumps over
5641 tˇhe lazy dog"});
5642}
5643
5644#[gpui::test]
5645async fn test_copy_trim(cx: &mut TestAppContext) {
5646 init_test(cx, |_| {});
5647
5648 let mut cx = EditorTestContext::new(cx).await;
5649 cx.set_state(
5650 r#" «for selection in selections.iter() {
5651 let mut start = selection.start;
5652 let mut end = selection.end;
5653 let is_entire_line = selection.is_empty();
5654 if is_entire_line {
5655 start = Point::new(start.row, 0);ˇ»
5656 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5657 }
5658 "#,
5659 );
5660 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5661 assert_eq!(
5662 cx.read_from_clipboard()
5663 .and_then(|item| item.text().as_deref().map(str::to_string)),
5664 Some(
5665 "for selection in selections.iter() {
5666 let mut start = selection.start;
5667 let mut end = selection.end;
5668 let is_entire_line = selection.is_empty();
5669 if is_entire_line {
5670 start = Point::new(start.row, 0);"
5671 .to_string()
5672 ),
5673 "Regular copying preserves all indentation selected",
5674 );
5675 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5676 assert_eq!(
5677 cx.read_from_clipboard()
5678 .and_then(|item| item.text().as_deref().map(str::to_string)),
5679 Some(
5680 "for selection in selections.iter() {
5681let mut start = selection.start;
5682let mut end = selection.end;
5683let is_entire_line = selection.is_empty();
5684if is_entire_line {
5685 start = Point::new(start.row, 0);"
5686 .to_string()
5687 ),
5688 "Copying with stripping should strip all leading whitespaces"
5689 );
5690
5691 cx.set_state(
5692 r#" « for selection in selections.iter() {
5693 let mut start = selection.start;
5694 let mut end = selection.end;
5695 let is_entire_line = selection.is_empty();
5696 if is_entire_line {
5697 start = Point::new(start.row, 0);ˇ»
5698 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5699 }
5700 "#,
5701 );
5702 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5703 assert_eq!(
5704 cx.read_from_clipboard()
5705 .and_then(|item| item.text().as_deref().map(str::to_string)),
5706 Some(
5707 " for selection in selections.iter() {
5708 let mut start = selection.start;
5709 let mut end = selection.end;
5710 let is_entire_line = selection.is_empty();
5711 if is_entire_line {
5712 start = Point::new(start.row, 0);"
5713 .to_string()
5714 ),
5715 "Regular copying preserves all indentation selected",
5716 );
5717 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5718 assert_eq!(
5719 cx.read_from_clipboard()
5720 .and_then(|item| item.text().as_deref().map(str::to_string)),
5721 Some(
5722 "for selection in selections.iter() {
5723let mut start = selection.start;
5724let mut end = selection.end;
5725let is_entire_line = selection.is_empty();
5726if is_entire_line {
5727 start = Point::new(start.row, 0);"
5728 .to_string()
5729 ),
5730 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5731 );
5732
5733 cx.set_state(
5734 r#" «ˇ for selection in selections.iter() {
5735 let mut start = selection.start;
5736 let mut end = selection.end;
5737 let is_entire_line = selection.is_empty();
5738 if is_entire_line {
5739 start = Point::new(start.row, 0);»
5740 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5741 }
5742 "#,
5743 );
5744 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5745 assert_eq!(
5746 cx.read_from_clipboard()
5747 .and_then(|item| item.text().as_deref().map(str::to_string)),
5748 Some(
5749 " for selection in selections.iter() {
5750 let mut start = selection.start;
5751 let mut end = selection.end;
5752 let is_entire_line = selection.is_empty();
5753 if is_entire_line {
5754 start = Point::new(start.row, 0);"
5755 .to_string()
5756 ),
5757 "Regular copying for reverse selection works the same",
5758 );
5759 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5760 assert_eq!(
5761 cx.read_from_clipboard()
5762 .and_then(|item| item.text().as_deref().map(str::to_string)),
5763 Some(
5764 "for selection in selections.iter() {
5765let mut start = selection.start;
5766let mut end = selection.end;
5767let is_entire_line = selection.is_empty();
5768if is_entire_line {
5769 start = Point::new(start.row, 0);"
5770 .to_string()
5771 ),
5772 "Copying with stripping for reverse selection works the same"
5773 );
5774
5775 cx.set_state(
5776 r#" for selection «in selections.iter() {
5777 let mut start = selection.start;
5778 let mut end = selection.end;
5779 let is_entire_line = selection.is_empty();
5780 if is_entire_line {
5781 start = Point::new(start.row, 0);ˇ»
5782 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5783 }
5784 "#,
5785 );
5786 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5787 assert_eq!(
5788 cx.read_from_clipboard()
5789 .and_then(|item| item.text().as_deref().map(str::to_string)),
5790 Some(
5791 "in selections.iter() {
5792 let mut start = selection.start;
5793 let mut end = selection.end;
5794 let is_entire_line = selection.is_empty();
5795 if is_entire_line {
5796 start = Point::new(start.row, 0);"
5797 .to_string()
5798 ),
5799 "When selecting past the indent, the copying works as usual",
5800 );
5801 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5802 assert_eq!(
5803 cx.read_from_clipboard()
5804 .and_then(|item| item.text().as_deref().map(str::to_string)),
5805 Some(
5806 "in selections.iter() {
5807 let mut start = selection.start;
5808 let mut end = selection.end;
5809 let is_entire_line = selection.is_empty();
5810 if is_entire_line {
5811 start = Point::new(start.row, 0);"
5812 .to_string()
5813 ),
5814 "When selecting past the indent, nothing is trimmed"
5815 );
5816
5817 cx.set_state(
5818 r#" «for selection in selections.iter() {
5819 let mut start = selection.start;
5820
5821 let mut end = selection.end;
5822 let is_entire_line = selection.is_empty();
5823 if is_entire_line {
5824 start = Point::new(start.row, 0);
5825ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5826 }
5827 "#,
5828 );
5829 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5830 assert_eq!(
5831 cx.read_from_clipboard()
5832 .and_then(|item| item.text().as_deref().map(str::to_string)),
5833 Some(
5834 "for selection in selections.iter() {
5835let mut start = selection.start;
5836
5837let mut end = selection.end;
5838let is_entire_line = selection.is_empty();
5839if is_entire_line {
5840 start = Point::new(start.row, 0);
5841"
5842 .to_string()
5843 ),
5844 "Copying with stripping should ignore empty lines"
5845 );
5846}
5847
5848#[gpui::test]
5849async fn test_paste_multiline(cx: &mut TestAppContext) {
5850 init_test(cx, |_| {});
5851
5852 let mut cx = EditorTestContext::new(cx).await;
5853 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5854
5855 // Cut an indented block, without the leading whitespace.
5856 cx.set_state(indoc! {"
5857 const a: B = (
5858 c(),
5859 «d(
5860 e,
5861 f
5862 )ˇ»
5863 );
5864 "});
5865 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5866 cx.assert_editor_state(indoc! {"
5867 const a: B = (
5868 c(),
5869 ˇ
5870 );
5871 "});
5872
5873 // Paste it at the same position.
5874 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5875 cx.assert_editor_state(indoc! {"
5876 const a: B = (
5877 c(),
5878 d(
5879 e,
5880 f
5881 )ˇ
5882 );
5883 "});
5884
5885 // Paste it at a line with a lower indent level.
5886 cx.set_state(indoc! {"
5887 ˇ
5888 const a: B = (
5889 c(),
5890 );
5891 "});
5892 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5893 cx.assert_editor_state(indoc! {"
5894 d(
5895 e,
5896 f
5897 )ˇ
5898 const a: B = (
5899 c(),
5900 );
5901 "});
5902
5903 // Cut an indented block, with the leading whitespace.
5904 cx.set_state(indoc! {"
5905 const a: B = (
5906 c(),
5907 « d(
5908 e,
5909 f
5910 )
5911 ˇ»);
5912 "});
5913 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5914 cx.assert_editor_state(indoc! {"
5915 const a: B = (
5916 c(),
5917 ˇ);
5918 "});
5919
5920 // Paste it at the same position.
5921 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5922 cx.assert_editor_state(indoc! {"
5923 const a: B = (
5924 c(),
5925 d(
5926 e,
5927 f
5928 )
5929 ˇ);
5930 "});
5931
5932 // Paste it at a line with a higher indent level.
5933 cx.set_state(indoc! {"
5934 const a: B = (
5935 c(),
5936 d(
5937 e,
5938 fˇ
5939 )
5940 );
5941 "});
5942 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5943 cx.assert_editor_state(indoc! {"
5944 const a: B = (
5945 c(),
5946 d(
5947 e,
5948 f d(
5949 e,
5950 f
5951 )
5952 ˇ
5953 )
5954 );
5955 "});
5956
5957 // Copy an indented block, starting mid-line
5958 cx.set_state(indoc! {"
5959 const a: B = (
5960 c(),
5961 somethin«g(
5962 e,
5963 f
5964 )ˇ»
5965 );
5966 "});
5967 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5968
5969 // Paste it on a line with a lower indent level
5970 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5971 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5972 cx.assert_editor_state(indoc! {"
5973 const a: B = (
5974 c(),
5975 something(
5976 e,
5977 f
5978 )
5979 );
5980 g(
5981 e,
5982 f
5983 )ˇ"});
5984}
5985
5986#[gpui::test]
5987async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5988 init_test(cx, |_| {});
5989
5990 cx.write_to_clipboard(ClipboardItem::new_string(
5991 " d(\n e\n );\n".into(),
5992 ));
5993
5994 let mut cx = EditorTestContext::new(cx).await;
5995 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5996
5997 cx.set_state(indoc! {"
5998 fn a() {
5999 b();
6000 if c() {
6001 ˇ
6002 }
6003 }
6004 "});
6005
6006 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6007 cx.assert_editor_state(indoc! {"
6008 fn a() {
6009 b();
6010 if c() {
6011 d(
6012 e
6013 );
6014 ˇ
6015 }
6016 }
6017 "});
6018
6019 cx.set_state(indoc! {"
6020 fn a() {
6021 b();
6022 ˇ
6023 }
6024 "});
6025
6026 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6027 cx.assert_editor_state(indoc! {"
6028 fn a() {
6029 b();
6030 d(
6031 e
6032 );
6033 ˇ
6034 }
6035 "});
6036}
6037
6038#[gpui::test]
6039fn test_select_all(cx: &mut TestAppContext) {
6040 init_test(cx, |_| {});
6041
6042 let editor = cx.add_window(|window, cx| {
6043 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6044 build_editor(buffer, window, cx)
6045 });
6046 _ = editor.update(cx, |editor, window, cx| {
6047 editor.select_all(&SelectAll, window, cx);
6048 assert_eq!(
6049 editor.selections.display_ranges(cx),
6050 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6051 );
6052 });
6053}
6054
6055#[gpui::test]
6056fn test_select_line(cx: &mut TestAppContext) {
6057 init_test(cx, |_| {});
6058
6059 let editor = cx.add_window(|window, cx| {
6060 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6061 build_editor(buffer, window, cx)
6062 });
6063 _ = editor.update(cx, |editor, window, cx| {
6064 editor.change_selections(None, window, cx, |s| {
6065 s.select_display_ranges([
6066 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6067 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6068 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6069 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6070 ])
6071 });
6072 editor.select_line(&SelectLine, window, cx);
6073 assert_eq!(
6074 editor.selections.display_ranges(cx),
6075 vec![
6076 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6077 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6078 ]
6079 );
6080 });
6081
6082 _ = editor.update(cx, |editor, window, cx| {
6083 editor.select_line(&SelectLine, window, cx);
6084 assert_eq!(
6085 editor.selections.display_ranges(cx),
6086 vec![
6087 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6088 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6089 ]
6090 );
6091 });
6092
6093 _ = editor.update(cx, |editor, window, cx| {
6094 editor.select_line(&SelectLine, window, cx);
6095 assert_eq!(
6096 editor.selections.display_ranges(cx),
6097 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6098 );
6099 });
6100}
6101
6102#[gpui::test]
6103async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6104 init_test(cx, |_| {});
6105 let mut cx = EditorTestContext::new(cx).await;
6106
6107 #[track_caller]
6108 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6109 cx.set_state(initial_state);
6110 cx.update_editor(|e, window, cx| {
6111 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6112 });
6113 cx.assert_editor_state(expected_state);
6114 }
6115
6116 // Selection starts and ends at the middle of lines, left-to-right
6117 test(
6118 &mut cx,
6119 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6120 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6121 );
6122 // Same thing, right-to-left
6123 test(
6124 &mut cx,
6125 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6126 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6127 );
6128
6129 // Whole buffer, left-to-right, last line *doesn't* end with newline
6130 test(
6131 &mut cx,
6132 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6133 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6134 );
6135 // Same thing, right-to-left
6136 test(
6137 &mut cx,
6138 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6139 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6140 );
6141
6142 // Whole buffer, left-to-right, last line ends with newline
6143 test(
6144 &mut cx,
6145 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6146 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6147 );
6148 // Same thing, right-to-left
6149 test(
6150 &mut cx,
6151 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6152 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6153 );
6154
6155 // Starts at the end of a line, ends at the start of another
6156 test(
6157 &mut cx,
6158 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6159 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6160 );
6161}
6162
6163#[gpui::test]
6164async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6165 init_test(cx, |_| {});
6166
6167 let editor = cx.add_window(|window, cx| {
6168 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6169 build_editor(buffer, window, cx)
6170 });
6171
6172 // setup
6173 _ = editor.update(cx, |editor, window, cx| {
6174 editor.fold_creases(
6175 vec![
6176 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6177 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6178 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6179 ],
6180 true,
6181 window,
6182 cx,
6183 );
6184 assert_eq!(
6185 editor.display_text(cx),
6186 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6187 );
6188 });
6189
6190 _ = editor.update(cx, |editor, window, cx| {
6191 editor.change_selections(None, window, cx, |s| {
6192 s.select_display_ranges([
6193 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6194 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6195 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6196 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6197 ])
6198 });
6199 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6200 assert_eq!(
6201 editor.display_text(cx),
6202 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6203 );
6204 });
6205 EditorTestContext::for_editor(editor, cx)
6206 .await
6207 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6208
6209 _ = editor.update(cx, |editor, window, cx| {
6210 editor.change_selections(None, window, cx, |s| {
6211 s.select_display_ranges([
6212 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6213 ])
6214 });
6215 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6216 assert_eq!(
6217 editor.display_text(cx),
6218 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6219 );
6220 assert_eq!(
6221 editor.selections.display_ranges(cx),
6222 [
6223 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6224 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6225 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6226 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6227 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6228 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6229 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6230 ]
6231 );
6232 });
6233 EditorTestContext::for_editor(editor, cx)
6234 .await
6235 .assert_editor_state(
6236 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6237 );
6238}
6239
6240#[gpui::test]
6241async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6242 init_test(cx, |_| {});
6243
6244 let mut cx = EditorTestContext::new(cx).await;
6245
6246 cx.set_state(indoc!(
6247 r#"abc
6248 defˇghi
6249
6250 jk
6251 nlmo
6252 "#
6253 ));
6254
6255 cx.update_editor(|editor, window, cx| {
6256 editor.add_selection_above(&Default::default(), window, cx);
6257 });
6258
6259 cx.assert_editor_state(indoc!(
6260 r#"abcˇ
6261 defˇghi
6262
6263 jk
6264 nlmo
6265 "#
6266 ));
6267
6268 cx.update_editor(|editor, window, cx| {
6269 editor.add_selection_above(&Default::default(), window, cx);
6270 });
6271
6272 cx.assert_editor_state(indoc!(
6273 r#"abcˇ
6274 defˇghi
6275
6276 jk
6277 nlmo
6278 "#
6279 ));
6280
6281 cx.update_editor(|editor, window, cx| {
6282 editor.add_selection_below(&Default::default(), window, cx);
6283 });
6284
6285 cx.assert_editor_state(indoc!(
6286 r#"abc
6287 defˇghi
6288
6289 jk
6290 nlmo
6291 "#
6292 ));
6293
6294 cx.update_editor(|editor, window, cx| {
6295 editor.undo_selection(&Default::default(), window, cx);
6296 });
6297
6298 cx.assert_editor_state(indoc!(
6299 r#"abcˇ
6300 defˇghi
6301
6302 jk
6303 nlmo
6304 "#
6305 ));
6306
6307 cx.update_editor(|editor, window, cx| {
6308 editor.redo_selection(&Default::default(), window, cx);
6309 });
6310
6311 cx.assert_editor_state(indoc!(
6312 r#"abc
6313 defˇghi
6314
6315 jk
6316 nlmo
6317 "#
6318 ));
6319
6320 cx.update_editor(|editor, window, cx| {
6321 editor.add_selection_below(&Default::default(), window, cx);
6322 });
6323
6324 cx.assert_editor_state(indoc!(
6325 r#"abc
6326 defˇghi
6327 ˇ
6328 jk
6329 nlmo
6330 "#
6331 ));
6332
6333 cx.update_editor(|editor, window, cx| {
6334 editor.add_selection_below(&Default::default(), window, cx);
6335 });
6336
6337 cx.assert_editor_state(indoc!(
6338 r#"abc
6339 defˇghi
6340 ˇ
6341 jkˇ
6342 nlmo
6343 "#
6344 ));
6345
6346 cx.update_editor(|editor, window, cx| {
6347 editor.add_selection_below(&Default::default(), window, cx);
6348 });
6349
6350 cx.assert_editor_state(indoc!(
6351 r#"abc
6352 defˇghi
6353 ˇ
6354 jkˇ
6355 nlmˇo
6356 "#
6357 ));
6358
6359 cx.update_editor(|editor, window, cx| {
6360 editor.add_selection_below(&Default::default(), window, cx);
6361 });
6362
6363 cx.assert_editor_state(indoc!(
6364 r#"abc
6365 defˇghi
6366 ˇ
6367 jkˇ
6368 nlmˇo
6369 ˇ"#
6370 ));
6371
6372 // change selections
6373 cx.set_state(indoc!(
6374 r#"abc
6375 def«ˇg»hi
6376
6377 jk
6378 nlmo
6379 "#
6380 ));
6381
6382 cx.update_editor(|editor, window, cx| {
6383 editor.add_selection_below(&Default::default(), window, cx);
6384 });
6385
6386 cx.assert_editor_state(indoc!(
6387 r#"abc
6388 def«ˇg»hi
6389
6390 jk
6391 nlm«ˇo»
6392 "#
6393 ));
6394
6395 cx.update_editor(|editor, window, cx| {
6396 editor.add_selection_below(&Default::default(), window, cx);
6397 });
6398
6399 cx.assert_editor_state(indoc!(
6400 r#"abc
6401 def«ˇg»hi
6402
6403 jk
6404 nlm«ˇo»
6405 "#
6406 ));
6407
6408 cx.update_editor(|editor, window, cx| {
6409 editor.add_selection_above(&Default::default(), window, cx);
6410 });
6411
6412 cx.assert_editor_state(indoc!(
6413 r#"abc
6414 def«ˇg»hi
6415
6416 jk
6417 nlmo
6418 "#
6419 ));
6420
6421 cx.update_editor(|editor, window, cx| {
6422 editor.add_selection_above(&Default::default(), window, cx);
6423 });
6424
6425 cx.assert_editor_state(indoc!(
6426 r#"abc
6427 def«ˇg»hi
6428
6429 jk
6430 nlmo
6431 "#
6432 ));
6433
6434 // Change selections again
6435 cx.set_state(indoc!(
6436 r#"a«bc
6437 defgˇ»hi
6438
6439 jk
6440 nlmo
6441 "#
6442 ));
6443
6444 cx.update_editor(|editor, window, cx| {
6445 editor.add_selection_below(&Default::default(), window, cx);
6446 });
6447
6448 cx.assert_editor_state(indoc!(
6449 r#"a«bcˇ»
6450 d«efgˇ»hi
6451
6452 j«kˇ»
6453 nlmo
6454 "#
6455 ));
6456
6457 cx.update_editor(|editor, window, cx| {
6458 editor.add_selection_below(&Default::default(), window, cx);
6459 });
6460 cx.assert_editor_state(indoc!(
6461 r#"a«bcˇ»
6462 d«efgˇ»hi
6463
6464 j«kˇ»
6465 n«lmoˇ»
6466 "#
6467 ));
6468 cx.update_editor(|editor, window, cx| {
6469 editor.add_selection_above(&Default::default(), window, cx);
6470 });
6471
6472 cx.assert_editor_state(indoc!(
6473 r#"a«bcˇ»
6474 d«efgˇ»hi
6475
6476 j«kˇ»
6477 nlmo
6478 "#
6479 ));
6480
6481 // Change selections again
6482 cx.set_state(indoc!(
6483 r#"abc
6484 d«ˇefghi
6485
6486 jk
6487 nlm»o
6488 "#
6489 ));
6490
6491 cx.update_editor(|editor, window, cx| {
6492 editor.add_selection_above(&Default::default(), window, cx);
6493 });
6494
6495 cx.assert_editor_state(indoc!(
6496 r#"a«ˇbc»
6497 d«ˇef»ghi
6498
6499 j«ˇk»
6500 n«ˇlm»o
6501 "#
6502 ));
6503
6504 cx.update_editor(|editor, window, cx| {
6505 editor.add_selection_below(&Default::default(), window, cx);
6506 });
6507
6508 cx.assert_editor_state(indoc!(
6509 r#"abc
6510 d«ˇef»ghi
6511
6512 j«ˇk»
6513 n«ˇlm»o
6514 "#
6515 ));
6516}
6517
6518#[gpui::test]
6519async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6520 init_test(cx, |_| {});
6521 let mut cx = EditorTestContext::new(cx).await;
6522
6523 cx.set_state(indoc!(
6524 r#"line onˇe
6525 liˇne two
6526 line three
6527 line four"#
6528 ));
6529
6530 cx.update_editor(|editor, window, cx| {
6531 editor.add_selection_below(&Default::default(), window, cx);
6532 });
6533
6534 // test multiple cursors expand in the same direction
6535 cx.assert_editor_state(indoc!(
6536 r#"line onˇe
6537 liˇne twˇo
6538 liˇne three
6539 line four"#
6540 ));
6541
6542 cx.update_editor(|editor, window, cx| {
6543 editor.add_selection_below(&Default::default(), window, cx);
6544 });
6545
6546 cx.update_editor(|editor, window, cx| {
6547 editor.add_selection_below(&Default::default(), window, cx);
6548 });
6549
6550 // test multiple cursors expand below overflow
6551 cx.assert_editor_state(indoc!(
6552 r#"line onˇe
6553 liˇne twˇo
6554 liˇne thˇree
6555 liˇne foˇur"#
6556 ));
6557
6558 cx.update_editor(|editor, window, cx| {
6559 editor.add_selection_above(&Default::default(), window, cx);
6560 });
6561
6562 // test multiple cursors retrieves back correctly
6563 cx.assert_editor_state(indoc!(
6564 r#"line onˇe
6565 liˇne twˇo
6566 liˇne thˇree
6567 line four"#
6568 ));
6569
6570 cx.update_editor(|editor, window, cx| {
6571 editor.add_selection_above(&Default::default(), window, cx);
6572 });
6573
6574 cx.update_editor(|editor, window, cx| {
6575 editor.add_selection_above(&Default::default(), window, cx);
6576 });
6577
6578 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6579 cx.assert_editor_state(indoc!(
6580 r#"liˇne onˇe
6581 liˇne two
6582 line three
6583 line four"#
6584 ));
6585
6586 cx.update_editor(|editor, window, cx| {
6587 editor.undo_selection(&Default::default(), window, cx);
6588 });
6589
6590 // test undo
6591 cx.assert_editor_state(indoc!(
6592 r#"line onˇe
6593 liˇne twˇo
6594 line three
6595 line four"#
6596 ));
6597
6598 cx.update_editor(|editor, window, cx| {
6599 editor.redo_selection(&Default::default(), window, cx);
6600 });
6601
6602 // test redo
6603 cx.assert_editor_state(indoc!(
6604 r#"liˇne onˇe
6605 liˇne two
6606 line three
6607 line four"#
6608 ));
6609
6610 cx.set_state(indoc!(
6611 r#"abcd
6612 ef«ghˇ»
6613 ijkl
6614 «mˇ»nop"#
6615 ));
6616
6617 cx.update_editor(|editor, window, cx| {
6618 editor.add_selection_above(&Default::default(), window, cx);
6619 });
6620
6621 // test multiple selections expand in the same direction
6622 cx.assert_editor_state(indoc!(
6623 r#"ab«cdˇ»
6624 ef«ghˇ»
6625 «iˇ»jkl
6626 «mˇ»nop"#
6627 ));
6628
6629 cx.update_editor(|editor, window, cx| {
6630 editor.add_selection_above(&Default::default(), window, cx);
6631 });
6632
6633 // test multiple selection upward overflow
6634 cx.assert_editor_state(indoc!(
6635 r#"ab«cdˇ»
6636 «eˇ»f«ghˇ»
6637 «iˇ»jkl
6638 «mˇ»nop"#
6639 ));
6640
6641 cx.update_editor(|editor, window, cx| {
6642 editor.add_selection_below(&Default::default(), window, cx);
6643 });
6644
6645 // test multiple selection retrieves back correctly
6646 cx.assert_editor_state(indoc!(
6647 r#"abcd
6648 ef«ghˇ»
6649 «iˇ»jkl
6650 «mˇ»nop"#
6651 ));
6652
6653 cx.update_editor(|editor, window, cx| {
6654 editor.add_selection_below(&Default::default(), window, cx);
6655 });
6656
6657 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6658 cx.assert_editor_state(indoc!(
6659 r#"abcd
6660 ef«ghˇ»
6661 ij«klˇ»
6662 «mˇ»nop"#
6663 ));
6664
6665 cx.update_editor(|editor, window, cx| {
6666 editor.undo_selection(&Default::default(), window, cx);
6667 });
6668
6669 // test undo
6670 cx.assert_editor_state(indoc!(
6671 r#"abcd
6672 ef«ghˇ»
6673 «iˇ»jkl
6674 «mˇ»nop"#
6675 ));
6676
6677 cx.update_editor(|editor, window, cx| {
6678 editor.redo_selection(&Default::default(), window, cx);
6679 });
6680
6681 // test redo
6682 cx.assert_editor_state(indoc!(
6683 r#"abcd
6684 ef«ghˇ»
6685 ij«klˇ»
6686 «mˇ»nop"#
6687 ));
6688}
6689
6690#[gpui::test]
6691async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6692 init_test(cx, |_| {});
6693 let mut cx = EditorTestContext::new(cx).await;
6694
6695 cx.set_state(indoc!(
6696 r#"line onˇe
6697 liˇne two
6698 line three
6699 line four"#
6700 ));
6701
6702 cx.update_editor(|editor, window, cx| {
6703 editor.add_selection_below(&Default::default(), window, cx);
6704 editor.add_selection_below(&Default::default(), window, cx);
6705 editor.add_selection_below(&Default::default(), window, cx);
6706 });
6707
6708 // initial state with two multi cursor groups
6709 cx.assert_editor_state(indoc!(
6710 r#"line onˇe
6711 liˇne twˇo
6712 liˇne thˇree
6713 liˇne foˇur"#
6714 ));
6715
6716 // add single cursor in middle - simulate opt click
6717 cx.update_editor(|editor, window, cx| {
6718 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6719 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6720 editor.end_selection(window, cx);
6721 });
6722
6723 cx.assert_editor_state(indoc!(
6724 r#"line onˇe
6725 liˇne twˇo
6726 liˇneˇ thˇree
6727 liˇne foˇur"#
6728 ));
6729
6730 cx.update_editor(|editor, window, cx| {
6731 editor.add_selection_above(&Default::default(), window, cx);
6732 });
6733
6734 // test new added selection expands above and existing selection shrinks
6735 cx.assert_editor_state(indoc!(
6736 r#"line onˇe
6737 liˇneˇ twˇo
6738 liˇneˇ thˇree
6739 line four"#
6740 ));
6741
6742 cx.update_editor(|editor, window, cx| {
6743 editor.add_selection_above(&Default::default(), window, cx);
6744 });
6745
6746 // test new added selection expands above and existing selection shrinks
6747 cx.assert_editor_state(indoc!(
6748 r#"lineˇ onˇe
6749 liˇneˇ twˇo
6750 lineˇ three
6751 line four"#
6752 ));
6753
6754 // intial state with two selection groups
6755 cx.set_state(indoc!(
6756 r#"abcd
6757 ef«ghˇ»
6758 ijkl
6759 «mˇ»nop"#
6760 ));
6761
6762 cx.update_editor(|editor, window, cx| {
6763 editor.add_selection_above(&Default::default(), window, cx);
6764 editor.add_selection_above(&Default::default(), window, cx);
6765 });
6766
6767 cx.assert_editor_state(indoc!(
6768 r#"ab«cdˇ»
6769 «eˇ»f«ghˇ»
6770 «iˇ»jkl
6771 «mˇ»nop"#
6772 ));
6773
6774 // add single selection in middle - simulate opt drag
6775 cx.update_editor(|editor, window, cx| {
6776 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6777 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6778 editor.update_selection(
6779 DisplayPoint::new(DisplayRow(2), 4),
6780 0,
6781 gpui::Point::<f32>::default(),
6782 window,
6783 cx,
6784 );
6785 editor.end_selection(window, cx);
6786 });
6787
6788 cx.assert_editor_state(indoc!(
6789 r#"ab«cdˇ»
6790 «eˇ»f«ghˇ»
6791 «iˇ»jk«lˇ»
6792 «mˇ»nop"#
6793 ));
6794
6795 cx.update_editor(|editor, window, cx| {
6796 editor.add_selection_below(&Default::default(), window, cx);
6797 });
6798
6799 // test new added selection expands below, others shrinks from above
6800 cx.assert_editor_state(indoc!(
6801 r#"abcd
6802 ef«ghˇ»
6803 «iˇ»jk«lˇ»
6804 «mˇ»no«pˇ»"#
6805 ));
6806}
6807
6808#[gpui::test]
6809async fn test_select_next(cx: &mut TestAppContext) {
6810 init_test(cx, |_| {});
6811
6812 let mut cx = EditorTestContext::new(cx).await;
6813 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6814
6815 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6816 .unwrap();
6817 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6818
6819 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6820 .unwrap();
6821 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6822
6823 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6824 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6825
6826 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6827 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6828
6829 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6830 .unwrap();
6831 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6832
6833 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6834 .unwrap();
6835 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6836
6837 // Test selection direction should be preserved
6838 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6839
6840 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6841 .unwrap();
6842 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6843}
6844
6845#[gpui::test]
6846async fn test_select_all_matches(cx: &mut TestAppContext) {
6847 init_test(cx, |_| {});
6848
6849 let mut cx = EditorTestContext::new(cx).await;
6850
6851 // Test caret-only selections
6852 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6853 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6854 .unwrap();
6855 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6856
6857 // Test left-to-right selections
6858 cx.set_state("abc\n«abcˇ»\nabc");
6859 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6860 .unwrap();
6861 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6862
6863 // Test right-to-left selections
6864 cx.set_state("abc\n«ˇabc»\nabc");
6865 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6866 .unwrap();
6867 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6868
6869 // Test selecting whitespace with caret selection
6870 cx.set_state("abc\nˇ abc\nabc");
6871 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6872 .unwrap();
6873 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6874
6875 // Test selecting whitespace with left-to-right selection
6876 cx.set_state("abc\n«ˇ »abc\nabc");
6877 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6878 .unwrap();
6879 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6880
6881 // Test no matches with right-to-left selection
6882 cx.set_state("abc\n« ˇ»abc\nabc");
6883 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6884 .unwrap();
6885 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6886
6887 // Test with a single word and clip_at_line_ends=true (#29823)
6888 cx.set_state("aˇbc");
6889 cx.update_editor(|e, window, cx| {
6890 e.set_clip_at_line_ends(true, cx);
6891 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
6892 e.set_clip_at_line_ends(false, cx);
6893 });
6894 cx.assert_editor_state("«abcˇ»");
6895}
6896
6897#[gpui::test]
6898async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6899 init_test(cx, |_| {});
6900
6901 let mut cx = EditorTestContext::new(cx).await;
6902
6903 let large_body_1 = "\nd".repeat(200);
6904 let large_body_2 = "\ne".repeat(200);
6905
6906 cx.set_state(&format!(
6907 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6908 ));
6909 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6910 let scroll_position = editor.scroll_position(cx);
6911 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6912 scroll_position
6913 });
6914
6915 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6916 .unwrap();
6917 cx.assert_editor_state(&format!(
6918 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6919 ));
6920 let scroll_position_after_selection =
6921 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6922 assert_eq!(
6923 initial_scroll_position, scroll_position_after_selection,
6924 "Scroll position should not change after selecting all matches"
6925 );
6926}
6927
6928#[gpui::test]
6929async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6930 init_test(cx, |_| {});
6931
6932 let mut cx = EditorLspTestContext::new_rust(
6933 lsp::ServerCapabilities {
6934 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6935 ..Default::default()
6936 },
6937 cx,
6938 )
6939 .await;
6940
6941 cx.set_state(indoc! {"
6942 line 1
6943 line 2
6944 linˇe 3
6945 line 4
6946 line 5
6947 "});
6948
6949 // Make an edit
6950 cx.update_editor(|editor, window, cx| {
6951 editor.handle_input("X", window, cx);
6952 });
6953
6954 // Move cursor to a different position
6955 cx.update_editor(|editor, window, cx| {
6956 editor.change_selections(None, window, cx, |s| {
6957 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6958 });
6959 });
6960
6961 cx.assert_editor_state(indoc! {"
6962 line 1
6963 line 2
6964 linXe 3
6965 line 4
6966 liˇne 5
6967 "});
6968
6969 cx.lsp
6970 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6971 Ok(Some(vec![lsp::TextEdit::new(
6972 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6973 "PREFIX ".to_string(),
6974 )]))
6975 });
6976
6977 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6978 .unwrap()
6979 .await
6980 .unwrap();
6981
6982 cx.assert_editor_state(indoc! {"
6983 PREFIX line 1
6984 line 2
6985 linXe 3
6986 line 4
6987 liˇne 5
6988 "});
6989
6990 // Undo formatting
6991 cx.update_editor(|editor, window, cx| {
6992 editor.undo(&Default::default(), window, cx);
6993 });
6994
6995 // Verify cursor moved back to position after edit
6996 cx.assert_editor_state(indoc! {"
6997 line 1
6998 line 2
6999 linXˇe 3
7000 line 4
7001 line 5
7002 "});
7003}
7004
7005#[gpui::test]
7006async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7007 init_test(cx, |_| {});
7008
7009 let mut cx = EditorTestContext::new(cx).await;
7010
7011 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7012 cx.update_editor(|editor, window, cx| {
7013 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7014 });
7015
7016 cx.set_state(indoc! {"
7017 line 1
7018 line 2
7019 linˇe 3
7020 line 4
7021 line 5
7022 line 6
7023 line 7
7024 line 8
7025 line 9
7026 line 10
7027 "});
7028
7029 let snapshot = cx.buffer_snapshot();
7030 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7031
7032 cx.update(|_, cx| {
7033 provider.update(cx, |provider, _| {
7034 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7035 id: None,
7036 edits: vec![(edit_position..edit_position, "X".into())],
7037 edit_preview: None,
7038 }))
7039 })
7040 });
7041
7042 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7043 cx.update_editor(|editor, window, cx| {
7044 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7045 });
7046
7047 cx.assert_editor_state(indoc! {"
7048 line 1
7049 line 2
7050 lineXˇ 3
7051 line 4
7052 line 5
7053 line 6
7054 line 7
7055 line 8
7056 line 9
7057 line 10
7058 "});
7059
7060 cx.update_editor(|editor, window, cx| {
7061 editor.change_selections(None, window, cx, |s| {
7062 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7063 });
7064 });
7065
7066 cx.assert_editor_state(indoc! {"
7067 line 1
7068 line 2
7069 lineX 3
7070 line 4
7071 line 5
7072 line 6
7073 line 7
7074 line 8
7075 line 9
7076 liˇne 10
7077 "});
7078
7079 cx.update_editor(|editor, window, cx| {
7080 editor.undo(&Default::default(), window, cx);
7081 });
7082
7083 cx.assert_editor_state(indoc! {"
7084 line 1
7085 line 2
7086 lineˇ 3
7087 line 4
7088 line 5
7089 line 6
7090 line 7
7091 line 8
7092 line 9
7093 line 10
7094 "});
7095}
7096
7097#[gpui::test]
7098async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7099 init_test(cx, |_| {});
7100
7101 let mut cx = EditorTestContext::new(cx).await;
7102 cx.set_state(
7103 r#"let foo = 2;
7104lˇet foo = 2;
7105let fooˇ = 2;
7106let foo = 2;
7107let foo = ˇ2;"#,
7108 );
7109
7110 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7111 .unwrap();
7112 cx.assert_editor_state(
7113 r#"let foo = 2;
7114«letˇ» foo = 2;
7115let «fooˇ» = 2;
7116let foo = 2;
7117let foo = «2ˇ»;"#,
7118 );
7119
7120 // noop for multiple selections with different contents
7121 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7122 .unwrap();
7123 cx.assert_editor_state(
7124 r#"let foo = 2;
7125«letˇ» foo = 2;
7126let «fooˇ» = 2;
7127let foo = 2;
7128let foo = «2ˇ»;"#,
7129 );
7130
7131 // Test last selection direction should be preserved
7132 cx.set_state(
7133 r#"let foo = 2;
7134let foo = 2;
7135let «fooˇ» = 2;
7136let «ˇfoo» = 2;
7137let foo = 2;"#,
7138 );
7139
7140 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7141 .unwrap();
7142 cx.assert_editor_state(
7143 r#"let foo = 2;
7144let foo = 2;
7145let «fooˇ» = 2;
7146let «ˇfoo» = 2;
7147let «ˇfoo» = 2;"#,
7148 );
7149}
7150
7151#[gpui::test]
7152async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7153 init_test(cx, |_| {});
7154
7155 let mut cx =
7156 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7157
7158 cx.assert_editor_state(indoc! {"
7159 ˇbbb
7160 ccc
7161
7162 bbb
7163 ccc
7164 "});
7165 cx.dispatch_action(SelectPrevious::default());
7166 cx.assert_editor_state(indoc! {"
7167 «bbbˇ»
7168 ccc
7169
7170 bbb
7171 ccc
7172 "});
7173 cx.dispatch_action(SelectPrevious::default());
7174 cx.assert_editor_state(indoc! {"
7175 «bbbˇ»
7176 ccc
7177
7178 «bbbˇ»
7179 ccc
7180 "});
7181}
7182
7183#[gpui::test]
7184async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7185 init_test(cx, |_| {});
7186
7187 let mut cx = EditorTestContext::new(cx).await;
7188 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7189
7190 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7191 .unwrap();
7192 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7193
7194 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7195 .unwrap();
7196 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7197
7198 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7199 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7200
7201 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7202 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7203
7204 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7205 .unwrap();
7206 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7207
7208 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7209 .unwrap();
7210 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7211}
7212
7213#[gpui::test]
7214async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7215 init_test(cx, |_| {});
7216
7217 let mut cx = EditorTestContext::new(cx).await;
7218 cx.set_state("aˇ");
7219
7220 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7221 .unwrap();
7222 cx.assert_editor_state("«aˇ»");
7223 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7224 .unwrap();
7225 cx.assert_editor_state("«aˇ»");
7226}
7227
7228#[gpui::test]
7229async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7230 init_test(cx, |_| {});
7231
7232 let mut cx = EditorTestContext::new(cx).await;
7233 cx.set_state(
7234 r#"let foo = 2;
7235lˇet foo = 2;
7236let fooˇ = 2;
7237let foo = 2;
7238let foo = ˇ2;"#,
7239 );
7240
7241 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7242 .unwrap();
7243 cx.assert_editor_state(
7244 r#"let foo = 2;
7245«letˇ» foo = 2;
7246let «fooˇ» = 2;
7247let foo = 2;
7248let foo = «2ˇ»;"#,
7249 );
7250
7251 // noop for multiple selections with different contents
7252 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7253 .unwrap();
7254 cx.assert_editor_state(
7255 r#"let foo = 2;
7256«letˇ» foo = 2;
7257let «fooˇ» = 2;
7258let foo = 2;
7259let foo = «2ˇ»;"#,
7260 );
7261}
7262
7263#[gpui::test]
7264async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7265 init_test(cx, |_| {});
7266
7267 let mut cx = EditorTestContext::new(cx).await;
7268 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7269
7270 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7271 .unwrap();
7272 // selection direction is preserved
7273 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7274
7275 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7276 .unwrap();
7277 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7278
7279 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7280 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7281
7282 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7283 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7284
7285 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7286 .unwrap();
7287 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7288
7289 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7290 .unwrap();
7291 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7292}
7293
7294#[gpui::test]
7295async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7296 init_test(cx, |_| {});
7297
7298 let language = Arc::new(Language::new(
7299 LanguageConfig::default(),
7300 Some(tree_sitter_rust::LANGUAGE.into()),
7301 ));
7302
7303 let text = r#"
7304 use mod1::mod2::{mod3, mod4};
7305
7306 fn fn_1(param1: bool, param2: &str) {
7307 let var1 = "text";
7308 }
7309 "#
7310 .unindent();
7311
7312 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7313 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7314 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7315
7316 editor
7317 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7318 .await;
7319
7320 editor.update_in(cx, |editor, window, cx| {
7321 editor.change_selections(None, window, cx, |s| {
7322 s.select_display_ranges([
7323 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7324 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7325 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7326 ]);
7327 });
7328 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7329 });
7330 editor.update(cx, |editor, cx| {
7331 assert_text_with_selections(
7332 editor,
7333 indoc! {r#"
7334 use mod1::mod2::{mod3, «mod4ˇ»};
7335
7336 fn fn_1«ˇ(param1: bool, param2: &str)» {
7337 let var1 = "«ˇtext»";
7338 }
7339 "#},
7340 cx,
7341 );
7342 });
7343
7344 editor.update_in(cx, |editor, window, cx| {
7345 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7346 });
7347 editor.update(cx, |editor, cx| {
7348 assert_text_with_selections(
7349 editor,
7350 indoc! {r#"
7351 use mod1::mod2::«{mod3, mod4}ˇ»;
7352
7353 «ˇfn fn_1(param1: bool, param2: &str) {
7354 let var1 = "text";
7355 }»
7356 "#},
7357 cx,
7358 );
7359 });
7360
7361 editor.update_in(cx, |editor, window, cx| {
7362 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7363 });
7364 assert_eq!(
7365 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7366 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7367 );
7368
7369 // Trying to expand the selected syntax node one more time has no effect.
7370 editor.update_in(cx, |editor, window, cx| {
7371 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7372 });
7373 assert_eq!(
7374 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7375 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7376 );
7377
7378 editor.update_in(cx, |editor, window, cx| {
7379 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7380 });
7381 editor.update(cx, |editor, cx| {
7382 assert_text_with_selections(
7383 editor,
7384 indoc! {r#"
7385 use mod1::mod2::«{mod3, mod4}ˇ»;
7386
7387 «ˇfn fn_1(param1: bool, param2: &str) {
7388 let var1 = "text";
7389 }»
7390 "#},
7391 cx,
7392 );
7393 });
7394
7395 editor.update_in(cx, |editor, window, cx| {
7396 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7397 });
7398 editor.update(cx, |editor, cx| {
7399 assert_text_with_selections(
7400 editor,
7401 indoc! {r#"
7402 use mod1::mod2::{mod3, «mod4ˇ»};
7403
7404 fn fn_1«ˇ(param1: bool, param2: &str)» {
7405 let var1 = "«ˇtext»";
7406 }
7407 "#},
7408 cx,
7409 );
7410 });
7411
7412 editor.update_in(cx, |editor, window, cx| {
7413 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7414 });
7415 editor.update(cx, |editor, cx| {
7416 assert_text_with_selections(
7417 editor,
7418 indoc! {r#"
7419 use mod1::mod2::{mod3, mo«ˇ»d4};
7420
7421 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7422 let var1 = "te«ˇ»xt";
7423 }
7424 "#},
7425 cx,
7426 );
7427 });
7428
7429 // Trying to shrink the selected syntax node one more time has no effect.
7430 editor.update_in(cx, |editor, window, cx| {
7431 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7432 });
7433 editor.update_in(cx, |editor, _, cx| {
7434 assert_text_with_selections(
7435 editor,
7436 indoc! {r#"
7437 use mod1::mod2::{mod3, mo«ˇ»d4};
7438
7439 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7440 let var1 = "te«ˇ»xt";
7441 }
7442 "#},
7443 cx,
7444 );
7445 });
7446
7447 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7448 // a fold.
7449 editor.update_in(cx, |editor, window, cx| {
7450 editor.fold_creases(
7451 vec![
7452 Crease::simple(
7453 Point::new(0, 21)..Point::new(0, 24),
7454 FoldPlaceholder::test(),
7455 ),
7456 Crease::simple(
7457 Point::new(3, 20)..Point::new(3, 22),
7458 FoldPlaceholder::test(),
7459 ),
7460 ],
7461 true,
7462 window,
7463 cx,
7464 );
7465 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7466 });
7467 editor.update(cx, |editor, cx| {
7468 assert_text_with_selections(
7469 editor,
7470 indoc! {r#"
7471 use mod1::mod2::«{mod3, mod4}ˇ»;
7472
7473 fn fn_1«ˇ(param1: bool, param2: &str)» {
7474 let var1 = "«ˇtext»";
7475 }
7476 "#},
7477 cx,
7478 );
7479 });
7480}
7481
7482#[gpui::test]
7483async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7484 init_test(cx, |_| {});
7485
7486 let language = Arc::new(Language::new(
7487 LanguageConfig::default(),
7488 Some(tree_sitter_rust::LANGUAGE.into()),
7489 ));
7490
7491 let text = "let a = 2;";
7492
7493 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7494 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7495 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7496
7497 editor
7498 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7499 .await;
7500
7501 // Test case 1: Cursor at end of word
7502 editor.update_in(cx, |editor, window, cx| {
7503 editor.change_selections(None, window, cx, |s| {
7504 s.select_display_ranges([
7505 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7506 ]);
7507 });
7508 });
7509 editor.update(cx, |editor, cx| {
7510 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7511 });
7512 editor.update_in(cx, |editor, window, cx| {
7513 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7514 });
7515 editor.update(cx, |editor, cx| {
7516 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7517 });
7518 editor.update_in(cx, |editor, window, cx| {
7519 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7520 });
7521 editor.update(cx, |editor, cx| {
7522 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7523 });
7524
7525 // Test case 2: Cursor at end of statement
7526 editor.update_in(cx, |editor, window, cx| {
7527 editor.change_selections(None, window, cx, |s| {
7528 s.select_display_ranges([
7529 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7530 ]);
7531 });
7532 });
7533 editor.update(cx, |editor, cx| {
7534 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7535 });
7536 editor.update_in(cx, |editor, window, cx| {
7537 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7538 });
7539 editor.update(cx, |editor, cx| {
7540 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7541 });
7542}
7543
7544#[gpui::test]
7545async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7546 init_test(cx, |_| {});
7547
7548 let language = Arc::new(Language::new(
7549 LanguageConfig::default(),
7550 Some(tree_sitter_rust::LANGUAGE.into()),
7551 ));
7552
7553 let text = r#"
7554 use mod1::mod2::{mod3, mod4};
7555
7556 fn fn_1(param1: bool, param2: &str) {
7557 let var1 = "hello world";
7558 }
7559 "#
7560 .unindent();
7561
7562 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7563 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7564 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7565
7566 editor
7567 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7568 .await;
7569
7570 // Test 1: Cursor on a letter of a string word
7571 editor.update_in(cx, |editor, window, cx| {
7572 editor.change_selections(None, window, cx, |s| {
7573 s.select_display_ranges([
7574 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7575 ]);
7576 });
7577 });
7578 editor.update_in(cx, |editor, window, cx| {
7579 assert_text_with_selections(
7580 editor,
7581 indoc! {r#"
7582 use mod1::mod2::{mod3, mod4};
7583
7584 fn fn_1(param1: bool, param2: &str) {
7585 let var1 = "hˇello world";
7586 }
7587 "#},
7588 cx,
7589 );
7590 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7591 assert_text_with_selections(
7592 editor,
7593 indoc! {r#"
7594 use mod1::mod2::{mod3, mod4};
7595
7596 fn fn_1(param1: bool, param2: &str) {
7597 let var1 = "«ˇhello» world";
7598 }
7599 "#},
7600 cx,
7601 );
7602 });
7603
7604 // Test 2: Partial selection within a word
7605 editor.update_in(cx, |editor, window, cx| {
7606 editor.change_selections(None, window, cx, |s| {
7607 s.select_display_ranges([
7608 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7609 ]);
7610 });
7611 });
7612 editor.update_in(cx, |editor, window, cx| {
7613 assert_text_with_selections(
7614 editor,
7615 indoc! {r#"
7616 use mod1::mod2::{mod3, mod4};
7617
7618 fn fn_1(param1: bool, param2: &str) {
7619 let var1 = "h«elˇ»lo world";
7620 }
7621 "#},
7622 cx,
7623 );
7624 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7625 assert_text_with_selections(
7626 editor,
7627 indoc! {r#"
7628 use mod1::mod2::{mod3, mod4};
7629
7630 fn fn_1(param1: bool, param2: &str) {
7631 let var1 = "«ˇhello» world";
7632 }
7633 "#},
7634 cx,
7635 );
7636 });
7637
7638 // Test 3: Complete word already selected
7639 editor.update_in(cx, |editor, window, cx| {
7640 editor.change_selections(None, window, cx, |s| {
7641 s.select_display_ranges([
7642 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7643 ]);
7644 });
7645 });
7646 editor.update_in(cx, |editor, window, cx| {
7647 assert_text_with_selections(
7648 editor,
7649 indoc! {r#"
7650 use mod1::mod2::{mod3, mod4};
7651
7652 fn fn_1(param1: bool, param2: &str) {
7653 let var1 = "«helloˇ» world";
7654 }
7655 "#},
7656 cx,
7657 );
7658 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7659 assert_text_with_selections(
7660 editor,
7661 indoc! {r#"
7662 use mod1::mod2::{mod3, mod4};
7663
7664 fn fn_1(param1: bool, param2: &str) {
7665 let var1 = "«hello worldˇ»";
7666 }
7667 "#},
7668 cx,
7669 );
7670 });
7671
7672 // Test 4: Selection spanning across words
7673 editor.update_in(cx, |editor, window, cx| {
7674 editor.change_selections(None, window, cx, |s| {
7675 s.select_display_ranges([
7676 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7677 ]);
7678 });
7679 });
7680 editor.update_in(cx, |editor, window, cx| {
7681 assert_text_with_selections(
7682 editor,
7683 indoc! {r#"
7684 use mod1::mod2::{mod3, mod4};
7685
7686 fn fn_1(param1: bool, param2: &str) {
7687 let var1 = "hel«lo woˇ»rld";
7688 }
7689 "#},
7690 cx,
7691 );
7692 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7693 assert_text_with_selections(
7694 editor,
7695 indoc! {r#"
7696 use mod1::mod2::{mod3, mod4};
7697
7698 fn fn_1(param1: bool, param2: &str) {
7699 let var1 = "«ˇhello world»";
7700 }
7701 "#},
7702 cx,
7703 );
7704 });
7705
7706 // Test 5: Expansion beyond string
7707 editor.update_in(cx, |editor, window, cx| {
7708 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7709 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7710 assert_text_with_selections(
7711 editor,
7712 indoc! {r#"
7713 use mod1::mod2::{mod3, mod4};
7714
7715 fn fn_1(param1: bool, param2: &str) {
7716 «ˇlet var1 = "hello world";»
7717 }
7718 "#},
7719 cx,
7720 );
7721 });
7722}
7723
7724#[gpui::test]
7725async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7726 init_test(cx, |_| {});
7727
7728 let base_text = r#"
7729 impl A {
7730 // this is an uncommitted comment
7731
7732 fn b() {
7733 c();
7734 }
7735
7736 // this is another uncommitted comment
7737
7738 fn d() {
7739 // e
7740 // f
7741 }
7742 }
7743
7744 fn g() {
7745 // h
7746 }
7747 "#
7748 .unindent();
7749
7750 let text = r#"
7751 ˇimpl A {
7752
7753 fn b() {
7754 c();
7755 }
7756
7757 fn d() {
7758 // e
7759 // f
7760 }
7761 }
7762
7763 fn g() {
7764 // h
7765 }
7766 "#
7767 .unindent();
7768
7769 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7770 cx.set_state(&text);
7771 cx.set_head_text(&base_text);
7772 cx.update_editor(|editor, window, cx| {
7773 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7774 });
7775
7776 cx.assert_state_with_diff(
7777 "
7778 ˇimpl A {
7779 - // this is an uncommitted comment
7780
7781 fn b() {
7782 c();
7783 }
7784
7785 - // this is another uncommitted comment
7786 -
7787 fn d() {
7788 // e
7789 // f
7790 }
7791 }
7792
7793 fn g() {
7794 // h
7795 }
7796 "
7797 .unindent(),
7798 );
7799
7800 let expected_display_text = "
7801 impl A {
7802 // this is an uncommitted comment
7803
7804 fn b() {
7805 ⋯
7806 }
7807
7808 // this is another uncommitted comment
7809
7810 fn d() {
7811 ⋯
7812 }
7813 }
7814
7815 fn g() {
7816 ⋯
7817 }
7818 "
7819 .unindent();
7820
7821 cx.update_editor(|editor, window, cx| {
7822 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7823 assert_eq!(editor.display_text(cx), expected_display_text);
7824 });
7825}
7826
7827#[gpui::test]
7828async fn test_autoindent(cx: &mut TestAppContext) {
7829 init_test(cx, |_| {});
7830
7831 let language = Arc::new(
7832 Language::new(
7833 LanguageConfig {
7834 brackets: BracketPairConfig {
7835 pairs: vec![
7836 BracketPair {
7837 start: "{".to_string(),
7838 end: "}".to_string(),
7839 close: false,
7840 surround: false,
7841 newline: true,
7842 },
7843 BracketPair {
7844 start: "(".to_string(),
7845 end: ")".to_string(),
7846 close: false,
7847 surround: false,
7848 newline: true,
7849 },
7850 ],
7851 ..Default::default()
7852 },
7853 ..Default::default()
7854 },
7855 Some(tree_sitter_rust::LANGUAGE.into()),
7856 )
7857 .with_indents_query(
7858 r#"
7859 (_ "(" ")" @end) @indent
7860 (_ "{" "}" @end) @indent
7861 "#,
7862 )
7863 .unwrap(),
7864 );
7865
7866 let text = "fn a() {}";
7867
7868 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7869 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7870 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7871 editor
7872 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7873 .await;
7874
7875 editor.update_in(cx, |editor, window, cx| {
7876 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7877 editor.newline(&Newline, window, cx);
7878 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7879 assert_eq!(
7880 editor.selections.ranges(cx),
7881 &[
7882 Point::new(1, 4)..Point::new(1, 4),
7883 Point::new(3, 4)..Point::new(3, 4),
7884 Point::new(5, 0)..Point::new(5, 0)
7885 ]
7886 );
7887 });
7888}
7889
7890#[gpui::test]
7891async fn test_autoindent_selections(cx: &mut TestAppContext) {
7892 init_test(cx, |_| {});
7893
7894 {
7895 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7896 cx.set_state(indoc! {"
7897 impl A {
7898
7899 fn b() {}
7900
7901 «fn c() {
7902
7903 }ˇ»
7904 }
7905 "});
7906
7907 cx.update_editor(|editor, window, cx| {
7908 editor.autoindent(&Default::default(), window, cx);
7909 });
7910
7911 cx.assert_editor_state(indoc! {"
7912 impl A {
7913
7914 fn b() {}
7915
7916 «fn c() {
7917
7918 }ˇ»
7919 }
7920 "});
7921 }
7922
7923 {
7924 let mut cx = EditorTestContext::new_multibuffer(
7925 cx,
7926 [indoc! { "
7927 impl A {
7928 «
7929 // a
7930 fn b(){}
7931 »
7932 «
7933 }
7934 fn c(){}
7935 »
7936 "}],
7937 );
7938
7939 let buffer = cx.update_editor(|editor, _, cx| {
7940 let buffer = editor.buffer().update(cx, |buffer, _| {
7941 buffer.all_buffers().iter().next().unwrap().clone()
7942 });
7943 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7944 buffer
7945 });
7946
7947 cx.run_until_parked();
7948 cx.update_editor(|editor, window, cx| {
7949 editor.select_all(&Default::default(), window, cx);
7950 editor.autoindent(&Default::default(), window, cx)
7951 });
7952 cx.run_until_parked();
7953
7954 cx.update(|_, cx| {
7955 assert_eq!(
7956 buffer.read(cx).text(),
7957 indoc! { "
7958 impl A {
7959
7960 // a
7961 fn b(){}
7962
7963
7964 }
7965 fn c(){}
7966
7967 " }
7968 )
7969 });
7970 }
7971}
7972
7973#[gpui::test]
7974async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7975 init_test(cx, |_| {});
7976
7977 let mut cx = EditorTestContext::new(cx).await;
7978
7979 let language = Arc::new(Language::new(
7980 LanguageConfig {
7981 brackets: BracketPairConfig {
7982 pairs: vec![
7983 BracketPair {
7984 start: "{".to_string(),
7985 end: "}".to_string(),
7986 close: true,
7987 surround: true,
7988 newline: true,
7989 },
7990 BracketPair {
7991 start: "(".to_string(),
7992 end: ")".to_string(),
7993 close: true,
7994 surround: true,
7995 newline: true,
7996 },
7997 BracketPair {
7998 start: "/*".to_string(),
7999 end: " */".to_string(),
8000 close: true,
8001 surround: true,
8002 newline: true,
8003 },
8004 BracketPair {
8005 start: "[".to_string(),
8006 end: "]".to_string(),
8007 close: false,
8008 surround: false,
8009 newline: true,
8010 },
8011 BracketPair {
8012 start: "\"".to_string(),
8013 end: "\"".to_string(),
8014 close: true,
8015 surround: true,
8016 newline: false,
8017 },
8018 BracketPair {
8019 start: "<".to_string(),
8020 end: ">".to_string(),
8021 close: false,
8022 surround: true,
8023 newline: true,
8024 },
8025 ],
8026 ..Default::default()
8027 },
8028 autoclose_before: "})]".to_string(),
8029 ..Default::default()
8030 },
8031 Some(tree_sitter_rust::LANGUAGE.into()),
8032 ));
8033
8034 cx.language_registry().add(language.clone());
8035 cx.update_buffer(|buffer, cx| {
8036 buffer.set_language(Some(language), cx);
8037 });
8038
8039 cx.set_state(
8040 &r#"
8041 🏀ˇ
8042 εˇ
8043 ❤️ˇ
8044 "#
8045 .unindent(),
8046 );
8047
8048 // autoclose multiple nested brackets at multiple cursors
8049 cx.update_editor(|editor, window, cx| {
8050 editor.handle_input("{", window, cx);
8051 editor.handle_input("{", window, cx);
8052 editor.handle_input("{", window, cx);
8053 });
8054 cx.assert_editor_state(
8055 &"
8056 🏀{{{ˇ}}}
8057 ε{{{ˇ}}}
8058 ❤️{{{ˇ}}}
8059 "
8060 .unindent(),
8061 );
8062
8063 // insert a different closing bracket
8064 cx.update_editor(|editor, window, cx| {
8065 editor.handle_input(")", window, cx);
8066 });
8067 cx.assert_editor_state(
8068 &"
8069 🏀{{{)ˇ}}}
8070 ε{{{)ˇ}}}
8071 ❤️{{{)ˇ}}}
8072 "
8073 .unindent(),
8074 );
8075
8076 // skip over the auto-closed brackets when typing a closing bracket
8077 cx.update_editor(|editor, window, cx| {
8078 editor.move_right(&MoveRight, window, cx);
8079 editor.handle_input("}", window, cx);
8080 editor.handle_input("}", window, cx);
8081 editor.handle_input("}", window, cx);
8082 });
8083 cx.assert_editor_state(
8084 &"
8085 🏀{{{)}}}}ˇ
8086 ε{{{)}}}}ˇ
8087 ❤️{{{)}}}}ˇ
8088 "
8089 .unindent(),
8090 );
8091
8092 // autoclose multi-character pairs
8093 cx.set_state(
8094 &"
8095 ˇ
8096 ˇ
8097 "
8098 .unindent(),
8099 );
8100 cx.update_editor(|editor, window, cx| {
8101 editor.handle_input("/", window, cx);
8102 editor.handle_input("*", window, cx);
8103 });
8104 cx.assert_editor_state(
8105 &"
8106 /*ˇ */
8107 /*ˇ */
8108 "
8109 .unindent(),
8110 );
8111
8112 // one cursor autocloses a multi-character pair, one cursor
8113 // does not autoclose.
8114 cx.set_state(
8115 &"
8116 /ˇ
8117 ˇ
8118 "
8119 .unindent(),
8120 );
8121 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8122 cx.assert_editor_state(
8123 &"
8124 /*ˇ */
8125 *ˇ
8126 "
8127 .unindent(),
8128 );
8129
8130 // Don't autoclose if the next character isn't whitespace and isn't
8131 // listed in the language's "autoclose_before" section.
8132 cx.set_state("ˇa b");
8133 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8134 cx.assert_editor_state("{ˇa b");
8135
8136 // Don't autoclose if `close` is false for the bracket pair
8137 cx.set_state("ˇ");
8138 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8139 cx.assert_editor_state("[ˇ");
8140
8141 // Surround with brackets if text is selected
8142 cx.set_state("«aˇ» b");
8143 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8144 cx.assert_editor_state("{«aˇ»} b");
8145
8146 // Autoclose when not immediately after a word character
8147 cx.set_state("a ˇ");
8148 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8149 cx.assert_editor_state("a \"ˇ\"");
8150
8151 // Autoclose pair where the start and end characters are the same
8152 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8153 cx.assert_editor_state("a \"\"ˇ");
8154
8155 // Don't autoclose when immediately after a word character
8156 cx.set_state("aˇ");
8157 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8158 cx.assert_editor_state("a\"ˇ");
8159
8160 // Do autoclose when after a non-word character
8161 cx.set_state("{ˇ");
8162 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8163 cx.assert_editor_state("{\"ˇ\"");
8164
8165 // Non identical pairs autoclose regardless of preceding character
8166 cx.set_state("aˇ");
8167 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8168 cx.assert_editor_state("a{ˇ}");
8169
8170 // Don't autoclose pair if autoclose is disabled
8171 cx.set_state("ˇ");
8172 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8173 cx.assert_editor_state("<ˇ");
8174
8175 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8176 cx.set_state("«aˇ» b");
8177 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8178 cx.assert_editor_state("<«aˇ»> b");
8179}
8180
8181#[gpui::test]
8182async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8183 init_test(cx, |settings| {
8184 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8185 });
8186
8187 let mut cx = EditorTestContext::new(cx).await;
8188
8189 let language = Arc::new(Language::new(
8190 LanguageConfig {
8191 brackets: BracketPairConfig {
8192 pairs: vec![
8193 BracketPair {
8194 start: "{".to_string(),
8195 end: "}".to_string(),
8196 close: true,
8197 surround: true,
8198 newline: true,
8199 },
8200 BracketPair {
8201 start: "(".to_string(),
8202 end: ")".to_string(),
8203 close: true,
8204 surround: true,
8205 newline: true,
8206 },
8207 BracketPair {
8208 start: "[".to_string(),
8209 end: "]".to_string(),
8210 close: false,
8211 surround: false,
8212 newline: true,
8213 },
8214 ],
8215 ..Default::default()
8216 },
8217 autoclose_before: "})]".to_string(),
8218 ..Default::default()
8219 },
8220 Some(tree_sitter_rust::LANGUAGE.into()),
8221 ));
8222
8223 cx.language_registry().add(language.clone());
8224 cx.update_buffer(|buffer, cx| {
8225 buffer.set_language(Some(language), cx);
8226 });
8227
8228 cx.set_state(
8229 &"
8230 ˇ
8231 ˇ
8232 ˇ
8233 "
8234 .unindent(),
8235 );
8236
8237 // ensure only matching closing brackets are skipped over
8238 cx.update_editor(|editor, window, cx| {
8239 editor.handle_input("}", window, cx);
8240 editor.move_left(&MoveLeft, window, cx);
8241 editor.handle_input(")", window, cx);
8242 editor.move_left(&MoveLeft, window, cx);
8243 });
8244 cx.assert_editor_state(
8245 &"
8246 ˇ)}
8247 ˇ)}
8248 ˇ)}
8249 "
8250 .unindent(),
8251 );
8252
8253 // skip-over closing brackets at multiple cursors
8254 cx.update_editor(|editor, window, cx| {
8255 editor.handle_input(")", window, cx);
8256 editor.handle_input("}", window, cx);
8257 });
8258 cx.assert_editor_state(
8259 &"
8260 )}ˇ
8261 )}ˇ
8262 )}ˇ
8263 "
8264 .unindent(),
8265 );
8266
8267 // ignore non-close brackets
8268 cx.update_editor(|editor, window, cx| {
8269 editor.handle_input("]", window, cx);
8270 editor.move_left(&MoveLeft, window, cx);
8271 editor.handle_input("]", window, cx);
8272 });
8273 cx.assert_editor_state(
8274 &"
8275 )}]ˇ]
8276 )}]ˇ]
8277 )}]ˇ]
8278 "
8279 .unindent(),
8280 );
8281}
8282
8283#[gpui::test]
8284async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8285 init_test(cx, |_| {});
8286
8287 let mut cx = EditorTestContext::new(cx).await;
8288
8289 let html_language = Arc::new(
8290 Language::new(
8291 LanguageConfig {
8292 name: "HTML".into(),
8293 brackets: BracketPairConfig {
8294 pairs: vec![
8295 BracketPair {
8296 start: "<".into(),
8297 end: ">".into(),
8298 close: true,
8299 ..Default::default()
8300 },
8301 BracketPair {
8302 start: "{".into(),
8303 end: "}".into(),
8304 close: true,
8305 ..Default::default()
8306 },
8307 BracketPair {
8308 start: "(".into(),
8309 end: ")".into(),
8310 close: true,
8311 ..Default::default()
8312 },
8313 ],
8314 ..Default::default()
8315 },
8316 autoclose_before: "})]>".into(),
8317 ..Default::default()
8318 },
8319 Some(tree_sitter_html::LANGUAGE.into()),
8320 )
8321 .with_injection_query(
8322 r#"
8323 (script_element
8324 (raw_text) @injection.content
8325 (#set! injection.language "javascript"))
8326 "#,
8327 )
8328 .unwrap(),
8329 );
8330
8331 let javascript_language = Arc::new(Language::new(
8332 LanguageConfig {
8333 name: "JavaScript".into(),
8334 brackets: BracketPairConfig {
8335 pairs: vec![
8336 BracketPair {
8337 start: "/*".into(),
8338 end: " */".into(),
8339 close: true,
8340 ..Default::default()
8341 },
8342 BracketPair {
8343 start: "{".into(),
8344 end: "}".into(),
8345 close: true,
8346 ..Default::default()
8347 },
8348 BracketPair {
8349 start: "(".into(),
8350 end: ")".into(),
8351 close: true,
8352 ..Default::default()
8353 },
8354 ],
8355 ..Default::default()
8356 },
8357 autoclose_before: "})]>".into(),
8358 ..Default::default()
8359 },
8360 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8361 ));
8362
8363 cx.language_registry().add(html_language.clone());
8364 cx.language_registry().add(javascript_language.clone());
8365
8366 cx.update_buffer(|buffer, cx| {
8367 buffer.set_language(Some(html_language), cx);
8368 });
8369
8370 cx.set_state(
8371 &r#"
8372 <body>ˇ
8373 <script>
8374 var x = 1;ˇ
8375 </script>
8376 </body>ˇ
8377 "#
8378 .unindent(),
8379 );
8380
8381 // Precondition: different languages are active at different locations.
8382 cx.update_editor(|editor, window, cx| {
8383 let snapshot = editor.snapshot(window, cx);
8384 let cursors = editor.selections.ranges::<usize>(cx);
8385 let languages = cursors
8386 .iter()
8387 .map(|c| snapshot.language_at(c.start).unwrap().name())
8388 .collect::<Vec<_>>();
8389 assert_eq!(
8390 languages,
8391 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8392 );
8393 });
8394
8395 // Angle brackets autoclose in HTML, but not JavaScript.
8396 cx.update_editor(|editor, window, cx| {
8397 editor.handle_input("<", window, cx);
8398 editor.handle_input("a", window, cx);
8399 });
8400 cx.assert_editor_state(
8401 &r#"
8402 <body><aˇ>
8403 <script>
8404 var x = 1;<aˇ
8405 </script>
8406 </body><aˇ>
8407 "#
8408 .unindent(),
8409 );
8410
8411 // Curly braces and parens autoclose in both HTML and JavaScript.
8412 cx.update_editor(|editor, window, cx| {
8413 editor.handle_input(" b=", window, cx);
8414 editor.handle_input("{", window, cx);
8415 editor.handle_input("c", window, cx);
8416 editor.handle_input("(", window, cx);
8417 });
8418 cx.assert_editor_state(
8419 &r#"
8420 <body><a b={c(ˇ)}>
8421 <script>
8422 var x = 1;<a b={c(ˇ)}
8423 </script>
8424 </body><a b={c(ˇ)}>
8425 "#
8426 .unindent(),
8427 );
8428
8429 // Brackets that were already autoclosed are skipped.
8430 cx.update_editor(|editor, window, cx| {
8431 editor.handle_input(")", window, cx);
8432 editor.handle_input("d", window, cx);
8433 editor.handle_input("}", window, cx);
8434 });
8435 cx.assert_editor_state(
8436 &r#"
8437 <body><a b={c()d}ˇ>
8438 <script>
8439 var x = 1;<a b={c()d}ˇ
8440 </script>
8441 </body><a b={c()d}ˇ>
8442 "#
8443 .unindent(),
8444 );
8445 cx.update_editor(|editor, window, cx| {
8446 editor.handle_input(">", window, cx);
8447 });
8448 cx.assert_editor_state(
8449 &r#"
8450 <body><a b={c()d}>ˇ
8451 <script>
8452 var x = 1;<a b={c()d}>ˇ
8453 </script>
8454 </body><a b={c()d}>ˇ
8455 "#
8456 .unindent(),
8457 );
8458
8459 // Reset
8460 cx.set_state(
8461 &r#"
8462 <body>ˇ
8463 <script>
8464 var x = 1;ˇ
8465 </script>
8466 </body>ˇ
8467 "#
8468 .unindent(),
8469 );
8470
8471 cx.update_editor(|editor, window, cx| {
8472 editor.handle_input("<", window, cx);
8473 });
8474 cx.assert_editor_state(
8475 &r#"
8476 <body><ˇ>
8477 <script>
8478 var x = 1;<ˇ
8479 </script>
8480 </body><ˇ>
8481 "#
8482 .unindent(),
8483 );
8484
8485 // When backspacing, the closing angle brackets are removed.
8486 cx.update_editor(|editor, window, cx| {
8487 editor.backspace(&Backspace, window, cx);
8488 });
8489 cx.assert_editor_state(
8490 &r#"
8491 <body>ˇ
8492 <script>
8493 var x = 1;ˇ
8494 </script>
8495 </body>ˇ
8496 "#
8497 .unindent(),
8498 );
8499
8500 // Block comments autoclose in JavaScript, but not HTML.
8501 cx.update_editor(|editor, window, cx| {
8502 editor.handle_input("/", window, cx);
8503 editor.handle_input("*", window, cx);
8504 });
8505 cx.assert_editor_state(
8506 &r#"
8507 <body>/*ˇ
8508 <script>
8509 var x = 1;/*ˇ */
8510 </script>
8511 </body>/*ˇ
8512 "#
8513 .unindent(),
8514 );
8515}
8516
8517#[gpui::test]
8518async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8519 init_test(cx, |_| {});
8520
8521 let mut cx = EditorTestContext::new(cx).await;
8522
8523 let rust_language = Arc::new(
8524 Language::new(
8525 LanguageConfig {
8526 name: "Rust".into(),
8527 brackets: serde_json::from_value(json!([
8528 { "start": "{", "end": "}", "close": true, "newline": true },
8529 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8530 ]))
8531 .unwrap(),
8532 autoclose_before: "})]>".into(),
8533 ..Default::default()
8534 },
8535 Some(tree_sitter_rust::LANGUAGE.into()),
8536 )
8537 .with_override_query("(string_literal) @string")
8538 .unwrap(),
8539 );
8540
8541 cx.language_registry().add(rust_language.clone());
8542 cx.update_buffer(|buffer, cx| {
8543 buffer.set_language(Some(rust_language), cx);
8544 });
8545
8546 cx.set_state(
8547 &r#"
8548 let x = ˇ
8549 "#
8550 .unindent(),
8551 );
8552
8553 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8554 cx.update_editor(|editor, window, cx| {
8555 editor.handle_input("\"", window, cx);
8556 });
8557 cx.assert_editor_state(
8558 &r#"
8559 let x = "ˇ"
8560 "#
8561 .unindent(),
8562 );
8563
8564 // Inserting another quotation mark. The cursor moves across the existing
8565 // automatically-inserted quotation mark.
8566 cx.update_editor(|editor, window, cx| {
8567 editor.handle_input("\"", window, cx);
8568 });
8569 cx.assert_editor_state(
8570 &r#"
8571 let x = ""ˇ
8572 "#
8573 .unindent(),
8574 );
8575
8576 // Reset
8577 cx.set_state(
8578 &r#"
8579 let x = ˇ
8580 "#
8581 .unindent(),
8582 );
8583
8584 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8585 cx.update_editor(|editor, window, cx| {
8586 editor.handle_input("\"", window, cx);
8587 editor.handle_input(" ", window, cx);
8588 editor.move_left(&Default::default(), window, cx);
8589 editor.handle_input("\\", window, cx);
8590 editor.handle_input("\"", window, cx);
8591 });
8592 cx.assert_editor_state(
8593 &r#"
8594 let x = "\"ˇ "
8595 "#
8596 .unindent(),
8597 );
8598
8599 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8600 // mark. Nothing is inserted.
8601 cx.update_editor(|editor, window, cx| {
8602 editor.move_right(&Default::default(), window, cx);
8603 editor.handle_input("\"", window, cx);
8604 });
8605 cx.assert_editor_state(
8606 &r#"
8607 let x = "\" "ˇ
8608 "#
8609 .unindent(),
8610 );
8611}
8612
8613#[gpui::test]
8614async fn test_surround_with_pair(cx: &mut TestAppContext) {
8615 init_test(cx, |_| {});
8616
8617 let language = Arc::new(Language::new(
8618 LanguageConfig {
8619 brackets: BracketPairConfig {
8620 pairs: vec![
8621 BracketPair {
8622 start: "{".to_string(),
8623 end: "}".to_string(),
8624 close: true,
8625 surround: true,
8626 newline: true,
8627 },
8628 BracketPair {
8629 start: "/* ".to_string(),
8630 end: "*/".to_string(),
8631 close: true,
8632 surround: true,
8633 ..Default::default()
8634 },
8635 ],
8636 ..Default::default()
8637 },
8638 ..Default::default()
8639 },
8640 Some(tree_sitter_rust::LANGUAGE.into()),
8641 ));
8642
8643 let text = r#"
8644 a
8645 b
8646 c
8647 "#
8648 .unindent();
8649
8650 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8651 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8652 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8653 editor
8654 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8655 .await;
8656
8657 editor.update_in(cx, |editor, window, cx| {
8658 editor.change_selections(None, window, cx, |s| {
8659 s.select_display_ranges([
8660 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8661 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8662 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8663 ])
8664 });
8665
8666 editor.handle_input("{", window, cx);
8667 editor.handle_input("{", window, cx);
8668 editor.handle_input("{", window, cx);
8669 assert_eq!(
8670 editor.text(cx),
8671 "
8672 {{{a}}}
8673 {{{b}}}
8674 {{{c}}}
8675 "
8676 .unindent()
8677 );
8678 assert_eq!(
8679 editor.selections.display_ranges(cx),
8680 [
8681 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8682 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8683 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8684 ]
8685 );
8686
8687 editor.undo(&Undo, window, cx);
8688 editor.undo(&Undo, window, cx);
8689 editor.undo(&Undo, window, cx);
8690 assert_eq!(
8691 editor.text(cx),
8692 "
8693 a
8694 b
8695 c
8696 "
8697 .unindent()
8698 );
8699 assert_eq!(
8700 editor.selections.display_ranges(cx),
8701 [
8702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8704 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8705 ]
8706 );
8707
8708 // Ensure inserting the first character of a multi-byte bracket pair
8709 // doesn't surround the selections with the bracket.
8710 editor.handle_input("/", window, cx);
8711 assert_eq!(
8712 editor.text(cx),
8713 "
8714 /
8715 /
8716 /
8717 "
8718 .unindent()
8719 );
8720 assert_eq!(
8721 editor.selections.display_ranges(cx),
8722 [
8723 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8724 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8725 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8726 ]
8727 );
8728
8729 editor.undo(&Undo, window, cx);
8730 assert_eq!(
8731 editor.text(cx),
8732 "
8733 a
8734 b
8735 c
8736 "
8737 .unindent()
8738 );
8739 assert_eq!(
8740 editor.selections.display_ranges(cx),
8741 [
8742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8743 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8744 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8745 ]
8746 );
8747
8748 // Ensure inserting the last character of a multi-byte bracket pair
8749 // doesn't surround the selections with the bracket.
8750 editor.handle_input("*", window, cx);
8751 assert_eq!(
8752 editor.text(cx),
8753 "
8754 *
8755 *
8756 *
8757 "
8758 .unindent()
8759 );
8760 assert_eq!(
8761 editor.selections.display_ranges(cx),
8762 [
8763 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8764 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8765 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8766 ]
8767 );
8768 });
8769}
8770
8771#[gpui::test]
8772async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8773 init_test(cx, |_| {});
8774
8775 let language = Arc::new(Language::new(
8776 LanguageConfig {
8777 brackets: BracketPairConfig {
8778 pairs: vec![BracketPair {
8779 start: "{".to_string(),
8780 end: "}".to_string(),
8781 close: true,
8782 surround: true,
8783 newline: true,
8784 }],
8785 ..Default::default()
8786 },
8787 autoclose_before: "}".to_string(),
8788 ..Default::default()
8789 },
8790 Some(tree_sitter_rust::LANGUAGE.into()),
8791 ));
8792
8793 let text = r#"
8794 a
8795 b
8796 c
8797 "#
8798 .unindent();
8799
8800 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8801 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8802 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8803 editor
8804 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8805 .await;
8806
8807 editor.update_in(cx, |editor, window, cx| {
8808 editor.change_selections(None, window, cx, |s| {
8809 s.select_ranges([
8810 Point::new(0, 1)..Point::new(0, 1),
8811 Point::new(1, 1)..Point::new(1, 1),
8812 Point::new(2, 1)..Point::new(2, 1),
8813 ])
8814 });
8815
8816 editor.handle_input("{", window, cx);
8817 editor.handle_input("{", window, cx);
8818 editor.handle_input("_", window, cx);
8819 assert_eq!(
8820 editor.text(cx),
8821 "
8822 a{{_}}
8823 b{{_}}
8824 c{{_}}
8825 "
8826 .unindent()
8827 );
8828 assert_eq!(
8829 editor.selections.ranges::<Point>(cx),
8830 [
8831 Point::new(0, 4)..Point::new(0, 4),
8832 Point::new(1, 4)..Point::new(1, 4),
8833 Point::new(2, 4)..Point::new(2, 4)
8834 ]
8835 );
8836
8837 editor.backspace(&Default::default(), window, cx);
8838 editor.backspace(&Default::default(), window, cx);
8839 assert_eq!(
8840 editor.text(cx),
8841 "
8842 a{}
8843 b{}
8844 c{}
8845 "
8846 .unindent()
8847 );
8848 assert_eq!(
8849 editor.selections.ranges::<Point>(cx),
8850 [
8851 Point::new(0, 2)..Point::new(0, 2),
8852 Point::new(1, 2)..Point::new(1, 2),
8853 Point::new(2, 2)..Point::new(2, 2)
8854 ]
8855 );
8856
8857 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8858 assert_eq!(
8859 editor.text(cx),
8860 "
8861 a
8862 b
8863 c
8864 "
8865 .unindent()
8866 );
8867 assert_eq!(
8868 editor.selections.ranges::<Point>(cx),
8869 [
8870 Point::new(0, 1)..Point::new(0, 1),
8871 Point::new(1, 1)..Point::new(1, 1),
8872 Point::new(2, 1)..Point::new(2, 1)
8873 ]
8874 );
8875 });
8876}
8877
8878#[gpui::test]
8879async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8880 init_test(cx, |settings| {
8881 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8882 });
8883
8884 let mut cx = EditorTestContext::new(cx).await;
8885
8886 let language = Arc::new(Language::new(
8887 LanguageConfig {
8888 brackets: BracketPairConfig {
8889 pairs: vec![
8890 BracketPair {
8891 start: "{".to_string(),
8892 end: "}".to_string(),
8893 close: true,
8894 surround: true,
8895 newline: true,
8896 },
8897 BracketPair {
8898 start: "(".to_string(),
8899 end: ")".to_string(),
8900 close: true,
8901 surround: true,
8902 newline: true,
8903 },
8904 BracketPair {
8905 start: "[".to_string(),
8906 end: "]".to_string(),
8907 close: false,
8908 surround: true,
8909 newline: true,
8910 },
8911 ],
8912 ..Default::default()
8913 },
8914 autoclose_before: "})]".to_string(),
8915 ..Default::default()
8916 },
8917 Some(tree_sitter_rust::LANGUAGE.into()),
8918 ));
8919
8920 cx.language_registry().add(language.clone());
8921 cx.update_buffer(|buffer, cx| {
8922 buffer.set_language(Some(language), cx);
8923 });
8924
8925 cx.set_state(
8926 &"
8927 {(ˇ)}
8928 [[ˇ]]
8929 {(ˇ)}
8930 "
8931 .unindent(),
8932 );
8933
8934 cx.update_editor(|editor, window, cx| {
8935 editor.backspace(&Default::default(), window, cx);
8936 editor.backspace(&Default::default(), window, cx);
8937 });
8938
8939 cx.assert_editor_state(
8940 &"
8941 ˇ
8942 ˇ]]
8943 ˇ
8944 "
8945 .unindent(),
8946 );
8947
8948 cx.update_editor(|editor, window, cx| {
8949 editor.handle_input("{", window, cx);
8950 editor.handle_input("{", window, cx);
8951 editor.move_right(&MoveRight, window, cx);
8952 editor.move_right(&MoveRight, window, cx);
8953 editor.move_left(&MoveLeft, window, cx);
8954 editor.move_left(&MoveLeft, window, cx);
8955 editor.backspace(&Default::default(), window, cx);
8956 });
8957
8958 cx.assert_editor_state(
8959 &"
8960 {ˇ}
8961 {ˇ}]]
8962 {ˇ}
8963 "
8964 .unindent(),
8965 );
8966
8967 cx.update_editor(|editor, window, cx| {
8968 editor.backspace(&Default::default(), window, cx);
8969 });
8970
8971 cx.assert_editor_state(
8972 &"
8973 ˇ
8974 ˇ]]
8975 ˇ
8976 "
8977 .unindent(),
8978 );
8979}
8980
8981#[gpui::test]
8982async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8983 init_test(cx, |_| {});
8984
8985 let language = Arc::new(Language::new(
8986 LanguageConfig::default(),
8987 Some(tree_sitter_rust::LANGUAGE.into()),
8988 ));
8989
8990 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8991 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8992 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8993 editor
8994 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8995 .await;
8996
8997 editor.update_in(cx, |editor, window, cx| {
8998 editor.set_auto_replace_emoji_shortcode(true);
8999
9000 editor.handle_input("Hello ", window, cx);
9001 editor.handle_input(":wave", window, cx);
9002 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9003
9004 editor.handle_input(":", window, cx);
9005 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9006
9007 editor.handle_input(" :smile", window, cx);
9008 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9009
9010 editor.handle_input(":", window, cx);
9011 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9012
9013 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9014 editor.handle_input(":wave", window, cx);
9015 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9016
9017 editor.handle_input(":", window, cx);
9018 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9019
9020 editor.handle_input(":1", window, cx);
9021 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9022
9023 editor.handle_input(":", window, cx);
9024 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9025
9026 // Ensure shortcode does not get replaced when it is part of a word
9027 editor.handle_input(" Test:wave", window, cx);
9028 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9029
9030 editor.handle_input(":", window, cx);
9031 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9032
9033 editor.set_auto_replace_emoji_shortcode(false);
9034
9035 // Ensure shortcode does not get replaced when auto replace is off
9036 editor.handle_input(" :wave", window, cx);
9037 assert_eq!(
9038 editor.text(cx),
9039 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9040 );
9041
9042 editor.handle_input(":", window, cx);
9043 assert_eq!(
9044 editor.text(cx),
9045 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9046 );
9047 });
9048}
9049
9050#[gpui::test]
9051async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9052 init_test(cx, |_| {});
9053
9054 let (text, insertion_ranges) = marked_text_ranges(
9055 indoc! {"
9056 ˇ
9057 "},
9058 false,
9059 );
9060
9061 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9062 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9063
9064 _ = editor.update_in(cx, |editor, window, cx| {
9065 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9066
9067 editor
9068 .insert_snippet(&insertion_ranges, snippet, window, cx)
9069 .unwrap();
9070
9071 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9072 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9073 assert_eq!(editor.text(cx), expected_text);
9074 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9075 }
9076
9077 assert(
9078 editor,
9079 cx,
9080 indoc! {"
9081 type «» =•
9082 "},
9083 );
9084
9085 assert!(editor.context_menu_visible(), "There should be a matches");
9086 });
9087}
9088
9089#[gpui::test]
9090async fn test_snippets(cx: &mut TestAppContext) {
9091 init_test(cx, |_| {});
9092
9093 let mut cx = EditorTestContext::new(cx).await;
9094
9095 cx.set_state(indoc! {"
9096 a.ˇ b
9097 a.ˇ b
9098 a.ˇ b
9099 "});
9100
9101 cx.update_editor(|editor, window, cx| {
9102 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9103 let insertion_ranges = editor
9104 .selections
9105 .all(cx)
9106 .iter()
9107 .map(|s| s.range().clone())
9108 .collect::<Vec<_>>();
9109 editor
9110 .insert_snippet(&insertion_ranges, snippet, window, cx)
9111 .unwrap();
9112 });
9113
9114 cx.assert_editor_state(indoc! {"
9115 a.f(«oneˇ», two, «threeˇ») b
9116 a.f(«oneˇ», two, «threeˇ») b
9117 a.f(«oneˇ», two, «threeˇ») b
9118 "});
9119
9120 // Can't move earlier than the first tab stop
9121 cx.update_editor(|editor, window, cx| {
9122 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9123 });
9124 cx.assert_editor_state(indoc! {"
9125 a.f(«oneˇ», two, «threeˇ») b
9126 a.f(«oneˇ», two, «threeˇ») b
9127 a.f(«oneˇ», two, «threeˇ») b
9128 "});
9129
9130 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9131 cx.assert_editor_state(indoc! {"
9132 a.f(one, «twoˇ», three) b
9133 a.f(one, «twoˇ», three) b
9134 a.f(one, «twoˇ», three) b
9135 "});
9136
9137 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9138 cx.assert_editor_state(indoc! {"
9139 a.f(«oneˇ», two, «threeˇ») b
9140 a.f(«oneˇ», two, «threeˇ») b
9141 a.f(«oneˇ», two, «threeˇ») b
9142 "});
9143
9144 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9145 cx.assert_editor_state(indoc! {"
9146 a.f(one, «twoˇ», three) b
9147 a.f(one, «twoˇ», three) b
9148 a.f(one, «twoˇ», three) b
9149 "});
9150 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9151 cx.assert_editor_state(indoc! {"
9152 a.f(one, two, three)ˇ b
9153 a.f(one, two, three)ˇ b
9154 a.f(one, two, three)ˇ b
9155 "});
9156
9157 // As soon as the last tab stop is reached, snippet state is gone
9158 cx.update_editor(|editor, window, cx| {
9159 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9160 });
9161 cx.assert_editor_state(indoc! {"
9162 a.f(one, two, three)ˇ b
9163 a.f(one, two, three)ˇ b
9164 a.f(one, two, three)ˇ b
9165 "});
9166}
9167
9168#[gpui::test]
9169async fn test_snippet_indentation(cx: &mut TestAppContext) {
9170 init_test(cx, |_| {});
9171
9172 let mut cx = EditorTestContext::new(cx).await;
9173
9174 cx.update_editor(|editor, window, cx| {
9175 let snippet = Snippet::parse(indoc! {"
9176 /*
9177 * Multiline comment with leading indentation
9178 *
9179 * $1
9180 */
9181 $0"})
9182 .unwrap();
9183 let insertion_ranges = editor
9184 .selections
9185 .all(cx)
9186 .iter()
9187 .map(|s| s.range().clone())
9188 .collect::<Vec<_>>();
9189 editor
9190 .insert_snippet(&insertion_ranges, snippet, window, cx)
9191 .unwrap();
9192 });
9193
9194 cx.assert_editor_state(indoc! {"
9195 /*
9196 * Multiline comment with leading indentation
9197 *
9198 * ˇ
9199 */
9200 "});
9201
9202 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9203 cx.assert_editor_state(indoc! {"
9204 /*
9205 * Multiline comment with leading indentation
9206 *
9207 *•
9208 */
9209 ˇ"});
9210}
9211
9212#[gpui::test]
9213async fn test_document_format_during_save(cx: &mut TestAppContext) {
9214 init_test(cx, |_| {});
9215
9216 let fs = FakeFs::new(cx.executor());
9217 fs.insert_file(path!("/file.rs"), Default::default()).await;
9218
9219 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9220
9221 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9222 language_registry.add(rust_lang());
9223 let mut fake_servers = language_registry.register_fake_lsp(
9224 "Rust",
9225 FakeLspAdapter {
9226 capabilities: lsp::ServerCapabilities {
9227 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9228 ..Default::default()
9229 },
9230 ..Default::default()
9231 },
9232 );
9233
9234 let buffer = project
9235 .update(cx, |project, cx| {
9236 project.open_local_buffer(path!("/file.rs"), cx)
9237 })
9238 .await
9239 .unwrap();
9240
9241 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9242 let (editor, cx) = cx.add_window_view(|window, cx| {
9243 build_editor_with_project(project.clone(), buffer, window, cx)
9244 });
9245 editor.update_in(cx, |editor, window, cx| {
9246 editor.set_text("one\ntwo\nthree\n", window, cx)
9247 });
9248 assert!(cx.read(|cx| editor.is_dirty(cx)));
9249
9250 cx.executor().start_waiting();
9251 let fake_server = fake_servers.next().await.unwrap();
9252
9253 {
9254 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9255 move |params, _| async move {
9256 assert_eq!(
9257 params.text_document.uri,
9258 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9259 );
9260 assert_eq!(params.options.tab_size, 4);
9261 Ok(Some(vec![lsp::TextEdit::new(
9262 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9263 ", ".to_string(),
9264 )]))
9265 },
9266 );
9267 let save = editor
9268 .update_in(cx, |editor, window, cx| {
9269 editor.save(
9270 SaveOptions {
9271 format: true,
9272 autosave: false,
9273 },
9274 project.clone(),
9275 window,
9276 cx,
9277 )
9278 })
9279 .unwrap();
9280 cx.executor().start_waiting();
9281 save.await;
9282
9283 assert_eq!(
9284 editor.update(cx, |editor, cx| editor.text(cx)),
9285 "one, two\nthree\n"
9286 );
9287 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9288 }
9289
9290 {
9291 editor.update_in(cx, |editor, window, cx| {
9292 editor.set_text("one\ntwo\nthree\n", window, cx)
9293 });
9294 assert!(cx.read(|cx| editor.is_dirty(cx)));
9295
9296 // Ensure we can still save even if formatting hangs.
9297 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9298 move |params, _| async move {
9299 assert_eq!(
9300 params.text_document.uri,
9301 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9302 );
9303 futures::future::pending::<()>().await;
9304 unreachable!()
9305 },
9306 );
9307 let save = editor
9308 .update_in(cx, |editor, window, cx| {
9309 editor.save(
9310 SaveOptions {
9311 format: true,
9312 autosave: false,
9313 },
9314 project.clone(),
9315 window,
9316 cx,
9317 )
9318 })
9319 .unwrap();
9320 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9321 cx.executor().start_waiting();
9322 save.await;
9323 assert_eq!(
9324 editor.update(cx, |editor, cx| editor.text(cx)),
9325 "one\ntwo\nthree\n"
9326 );
9327 }
9328
9329 // Set rust language override and assert overridden tabsize is sent to language server
9330 update_test_language_settings(cx, |settings| {
9331 settings.languages.insert(
9332 "Rust".into(),
9333 LanguageSettingsContent {
9334 tab_size: NonZeroU32::new(8),
9335 ..Default::default()
9336 },
9337 );
9338 });
9339
9340 {
9341 editor.update_in(cx, |editor, window, cx| {
9342 editor.set_text("somehting_new\n", window, cx)
9343 });
9344 assert!(cx.read(|cx| editor.is_dirty(cx)));
9345 let _formatting_request_signal = fake_server
9346 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9347 assert_eq!(
9348 params.text_document.uri,
9349 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9350 );
9351 assert_eq!(params.options.tab_size, 8);
9352 Ok(Some(vec![]))
9353 });
9354 let save = editor
9355 .update_in(cx, |editor, window, cx| {
9356 editor.save(
9357 SaveOptions {
9358 format: true,
9359 autosave: false,
9360 },
9361 project.clone(),
9362 window,
9363 cx,
9364 )
9365 })
9366 .unwrap();
9367 cx.executor().start_waiting();
9368 save.await;
9369 }
9370}
9371
9372#[gpui::test]
9373async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9374 init_test(cx, |_| {});
9375
9376 let cols = 4;
9377 let rows = 10;
9378 let sample_text_1 = sample_text(rows, cols, 'a');
9379 assert_eq!(
9380 sample_text_1,
9381 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9382 );
9383 let sample_text_2 = sample_text(rows, cols, 'l');
9384 assert_eq!(
9385 sample_text_2,
9386 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9387 );
9388 let sample_text_3 = sample_text(rows, cols, 'v');
9389 assert_eq!(
9390 sample_text_3,
9391 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9392 );
9393
9394 let fs = FakeFs::new(cx.executor());
9395 fs.insert_tree(
9396 path!("/a"),
9397 json!({
9398 "main.rs": sample_text_1,
9399 "other.rs": sample_text_2,
9400 "lib.rs": sample_text_3,
9401 }),
9402 )
9403 .await;
9404
9405 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9406 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9407 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9408
9409 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9410 language_registry.add(rust_lang());
9411 let mut fake_servers = language_registry.register_fake_lsp(
9412 "Rust",
9413 FakeLspAdapter {
9414 capabilities: lsp::ServerCapabilities {
9415 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9416 ..Default::default()
9417 },
9418 ..Default::default()
9419 },
9420 );
9421
9422 let worktree = project.update(cx, |project, cx| {
9423 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9424 assert_eq!(worktrees.len(), 1);
9425 worktrees.pop().unwrap()
9426 });
9427 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9428
9429 let buffer_1 = project
9430 .update(cx, |project, cx| {
9431 project.open_buffer((worktree_id, "main.rs"), cx)
9432 })
9433 .await
9434 .unwrap();
9435 let buffer_2 = project
9436 .update(cx, |project, cx| {
9437 project.open_buffer((worktree_id, "other.rs"), cx)
9438 })
9439 .await
9440 .unwrap();
9441 let buffer_3 = project
9442 .update(cx, |project, cx| {
9443 project.open_buffer((worktree_id, "lib.rs"), cx)
9444 })
9445 .await
9446 .unwrap();
9447
9448 let multi_buffer = cx.new(|cx| {
9449 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9450 multi_buffer.push_excerpts(
9451 buffer_1.clone(),
9452 [
9453 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9454 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9455 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9456 ],
9457 cx,
9458 );
9459 multi_buffer.push_excerpts(
9460 buffer_2.clone(),
9461 [
9462 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9463 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9464 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9465 ],
9466 cx,
9467 );
9468 multi_buffer.push_excerpts(
9469 buffer_3.clone(),
9470 [
9471 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9472 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9473 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9474 ],
9475 cx,
9476 );
9477 multi_buffer
9478 });
9479 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9480 Editor::new(
9481 EditorMode::full(),
9482 multi_buffer,
9483 Some(project.clone()),
9484 window,
9485 cx,
9486 )
9487 });
9488
9489 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9490 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9491 s.select_ranges(Some(1..2))
9492 });
9493 editor.insert("|one|two|three|", window, cx);
9494 });
9495 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9496 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9497 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9498 s.select_ranges(Some(60..70))
9499 });
9500 editor.insert("|four|five|six|", window, cx);
9501 });
9502 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9503
9504 // First two buffers should be edited, but not the third one.
9505 assert_eq!(
9506 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9507 "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}",
9508 );
9509 buffer_1.update(cx, |buffer, _| {
9510 assert!(buffer.is_dirty());
9511 assert_eq!(
9512 buffer.text(),
9513 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9514 )
9515 });
9516 buffer_2.update(cx, |buffer, _| {
9517 assert!(buffer.is_dirty());
9518 assert_eq!(
9519 buffer.text(),
9520 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9521 )
9522 });
9523 buffer_3.update(cx, |buffer, _| {
9524 assert!(!buffer.is_dirty());
9525 assert_eq!(buffer.text(), sample_text_3,)
9526 });
9527 cx.executor().run_until_parked();
9528
9529 cx.executor().start_waiting();
9530 let save = multi_buffer_editor
9531 .update_in(cx, |editor, window, cx| {
9532 editor.save(
9533 SaveOptions {
9534 format: true,
9535 autosave: false,
9536 },
9537 project.clone(),
9538 window,
9539 cx,
9540 )
9541 })
9542 .unwrap();
9543
9544 let fake_server = fake_servers.next().await.unwrap();
9545 fake_server
9546 .server
9547 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9548 Ok(Some(vec![lsp::TextEdit::new(
9549 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9550 format!("[{} formatted]", params.text_document.uri),
9551 )]))
9552 })
9553 .detach();
9554 save.await;
9555
9556 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9557 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9558 assert_eq!(
9559 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9560 uri!(
9561 "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}"
9562 ),
9563 );
9564 buffer_1.update(cx, |buffer, _| {
9565 assert!(!buffer.is_dirty());
9566 assert_eq!(
9567 buffer.text(),
9568 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9569 )
9570 });
9571 buffer_2.update(cx, |buffer, _| {
9572 assert!(!buffer.is_dirty());
9573 assert_eq!(
9574 buffer.text(),
9575 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9576 )
9577 });
9578 buffer_3.update(cx, |buffer, _| {
9579 assert!(!buffer.is_dirty());
9580 assert_eq!(buffer.text(), sample_text_3,)
9581 });
9582}
9583
9584#[gpui::test]
9585async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9586 init_test(cx, |_| {});
9587
9588 let fs = FakeFs::new(cx.executor());
9589 fs.insert_tree(
9590 path!("/dir"),
9591 json!({
9592 "file1.rs": "fn main() { println!(\"hello\"); }",
9593 "file2.rs": "fn test() { println!(\"test\"); }",
9594 "file3.rs": "fn other() { println!(\"other\"); }\n",
9595 }),
9596 )
9597 .await;
9598
9599 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9600 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9601 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9602
9603 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9604 language_registry.add(rust_lang());
9605
9606 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9607 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9608
9609 // Open three buffers
9610 let buffer_1 = project
9611 .update(cx, |project, cx| {
9612 project.open_buffer((worktree_id, "file1.rs"), cx)
9613 })
9614 .await
9615 .unwrap();
9616 let buffer_2 = project
9617 .update(cx, |project, cx| {
9618 project.open_buffer((worktree_id, "file2.rs"), cx)
9619 })
9620 .await
9621 .unwrap();
9622 let buffer_3 = project
9623 .update(cx, |project, cx| {
9624 project.open_buffer((worktree_id, "file3.rs"), cx)
9625 })
9626 .await
9627 .unwrap();
9628
9629 // Create a multi-buffer with all three buffers
9630 let multi_buffer = cx.new(|cx| {
9631 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9632 multi_buffer.push_excerpts(
9633 buffer_1.clone(),
9634 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9635 cx,
9636 );
9637 multi_buffer.push_excerpts(
9638 buffer_2.clone(),
9639 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9640 cx,
9641 );
9642 multi_buffer.push_excerpts(
9643 buffer_3.clone(),
9644 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9645 cx,
9646 );
9647 multi_buffer
9648 });
9649
9650 let editor = cx.new_window_entity(|window, cx| {
9651 Editor::new(
9652 EditorMode::full(),
9653 multi_buffer,
9654 Some(project.clone()),
9655 window,
9656 cx,
9657 )
9658 });
9659
9660 // Edit only the first buffer
9661 editor.update_in(cx, |editor, window, cx| {
9662 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9663 s.select_ranges(Some(10..10))
9664 });
9665 editor.insert("// edited", window, cx);
9666 });
9667
9668 // Verify that only buffer 1 is dirty
9669 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9670 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9671 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9672
9673 // Get write counts after file creation (files were created with initial content)
9674 // We expect each file to have been written once during creation
9675 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9676 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9677 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9678
9679 // Perform autosave
9680 let save_task = editor.update_in(cx, |editor, window, cx| {
9681 editor.save(
9682 SaveOptions {
9683 format: true,
9684 autosave: true,
9685 },
9686 project.clone(),
9687 window,
9688 cx,
9689 )
9690 });
9691 save_task.await.unwrap();
9692
9693 // Only the dirty buffer should have been saved
9694 assert_eq!(
9695 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9696 1,
9697 "Buffer 1 was dirty, so it should have been written once during autosave"
9698 );
9699 assert_eq!(
9700 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9701 0,
9702 "Buffer 2 was clean, so it should not have been written during autosave"
9703 );
9704 assert_eq!(
9705 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9706 0,
9707 "Buffer 3 was clean, so it should not have been written during autosave"
9708 );
9709
9710 // Verify buffer states after autosave
9711 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9712 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9713 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9714
9715 // Now perform a manual save (format = true)
9716 let save_task = editor.update_in(cx, |editor, window, cx| {
9717 editor.save(
9718 SaveOptions {
9719 format: true,
9720 autosave: false,
9721 },
9722 project.clone(),
9723 window,
9724 cx,
9725 )
9726 });
9727 save_task.await.unwrap();
9728
9729 // During manual save, clean buffers don't get written to disk
9730 // They just get did_save called for language server notifications
9731 assert_eq!(
9732 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9733 1,
9734 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9735 );
9736 assert_eq!(
9737 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9738 0,
9739 "Buffer 2 should not have been written at all"
9740 );
9741 assert_eq!(
9742 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9743 0,
9744 "Buffer 3 should not have been written at all"
9745 );
9746}
9747
9748#[gpui::test]
9749async fn test_range_format_during_save(cx: &mut TestAppContext) {
9750 init_test(cx, |_| {});
9751
9752 let fs = FakeFs::new(cx.executor());
9753 fs.insert_file(path!("/file.rs"), Default::default()).await;
9754
9755 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9756
9757 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9758 language_registry.add(rust_lang());
9759 let mut fake_servers = language_registry.register_fake_lsp(
9760 "Rust",
9761 FakeLspAdapter {
9762 capabilities: lsp::ServerCapabilities {
9763 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9764 ..Default::default()
9765 },
9766 ..Default::default()
9767 },
9768 );
9769
9770 let buffer = project
9771 .update(cx, |project, cx| {
9772 project.open_local_buffer(path!("/file.rs"), cx)
9773 })
9774 .await
9775 .unwrap();
9776
9777 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9778 let (editor, cx) = cx.add_window_view(|window, cx| {
9779 build_editor_with_project(project.clone(), buffer, window, cx)
9780 });
9781 editor.update_in(cx, |editor, window, cx| {
9782 editor.set_text("one\ntwo\nthree\n", window, cx)
9783 });
9784 assert!(cx.read(|cx| editor.is_dirty(cx)));
9785
9786 cx.executor().start_waiting();
9787 let fake_server = fake_servers.next().await.unwrap();
9788
9789 let save = editor
9790 .update_in(cx, |editor, window, cx| {
9791 editor.save(
9792 SaveOptions {
9793 format: true,
9794 autosave: false,
9795 },
9796 project.clone(),
9797 window,
9798 cx,
9799 )
9800 })
9801 .unwrap();
9802 fake_server
9803 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9804 assert_eq!(
9805 params.text_document.uri,
9806 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9807 );
9808 assert_eq!(params.options.tab_size, 4);
9809 Ok(Some(vec![lsp::TextEdit::new(
9810 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9811 ", ".to_string(),
9812 )]))
9813 })
9814 .next()
9815 .await;
9816 cx.executor().start_waiting();
9817 save.await;
9818 assert_eq!(
9819 editor.update(cx, |editor, cx| editor.text(cx)),
9820 "one, two\nthree\n"
9821 );
9822 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9823
9824 editor.update_in(cx, |editor, window, cx| {
9825 editor.set_text("one\ntwo\nthree\n", window, cx)
9826 });
9827 assert!(cx.read(|cx| editor.is_dirty(cx)));
9828
9829 // Ensure we can still save even if formatting hangs.
9830 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9831 move |params, _| async move {
9832 assert_eq!(
9833 params.text_document.uri,
9834 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9835 );
9836 futures::future::pending::<()>().await;
9837 unreachable!()
9838 },
9839 );
9840 let save = editor
9841 .update_in(cx, |editor, window, cx| {
9842 editor.save(
9843 SaveOptions {
9844 format: true,
9845 autosave: false,
9846 },
9847 project.clone(),
9848 window,
9849 cx,
9850 )
9851 })
9852 .unwrap();
9853 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9854 cx.executor().start_waiting();
9855 save.await;
9856 assert_eq!(
9857 editor.update(cx, |editor, cx| editor.text(cx)),
9858 "one\ntwo\nthree\n"
9859 );
9860 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9861
9862 // For non-dirty buffer, no formatting request should be sent
9863 let save = editor
9864 .update_in(cx, |editor, window, cx| {
9865 editor.save(
9866 SaveOptions {
9867 format: false,
9868 autosave: false,
9869 },
9870 project.clone(),
9871 window,
9872 cx,
9873 )
9874 })
9875 .unwrap();
9876 let _pending_format_request = fake_server
9877 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9878 panic!("Should not be invoked");
9879 })
9880 .next();
9881 cx.executor().start_waiting();
9882 save.await;
9883
9884 // Set Rust language override and assert overridden tabsize is sent to language server
9885 update_test_language_settings(cx, |settings| {
9886 settings.languages.insert(
9887 "Rust".into(),
9888 LanguageSettingsContent {
9889 tab_size: NonZeroU32::new(8),
9890 ..Default::default()
9891 },
9892 );
9893 });
9894
9895 editor.update_in(cx, |editor, window, cx| {
9896 editor.set_text("somehting_new\n", window, cx)
9897 });
9898 assert!(cx.read(|cx| editor.is_dirty(cx)));
9899 let save = editor
9900 .update_in(cx, |editor, window, cx| {
9901 editor.save(
9902 SaveOptions {
9903 format: true,
9904 autosave: false,
9905 },
9906 project.clone(),
9907 window,
9908 cx,
9909 )
9910 })
9911 .unwrap();
9912 fake_server
9913 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9914 assert_eq!(
9915 params.text_document.uri,
9916 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9917 );
9918 assert_eq!(params.options.tab_size, 8);
9919 Ok(Some(Vec::new()))
9920 })
9921 .next()
9922 .await;
9923 save.await;
9924}
9925
9926#[gpui::test]
9927async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9928 init_test(cx, |settings| {
9929 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9930 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9931 ))
9932 });
9933
9934 let fs = FakeFs::new(cx.executor());
9935 fs.insert_file(path!("/file.rs"), Default::default()).await;
9936
9937 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9938
9939 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9940 language_registry.add(Arc::new(Language::new(
9941 LanguageConfig {
9942 name: "Rust".into(),
9943 matcher: LanguageMatcher {
9944 path_suffixes: vec!["rs".to_string()],
9945 ..Default::default()
9946 },
9947 ..LanguageConfig::default()
9948 },
9949 Some(tree_sitter_rust::LANGUAGE.into()),
9950 )));
9951 update_test_language_settings(cx, |settings| {
9952 // Enable Prettier formatting for the same buffer, and ensure
9953 // LSP is called instead of Prettier.
9954 settings.defaults.prettier = Some(PrettierSettings {
9955 allowed: true,
9956 ..PrettierSettings::default()
9957 });
9958 });
9959 let mut fake_servers = language_registry.register_fake_lsp(
9960 "Rust",
9961 FakeLspAdapter {
9962 capabilities: lsp::ServerCapabilities {
9963 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9964 ..Default::default()
9965 },
9966 ..Default::default()
9967 },
9968 );
9969
9970 let buffer = project
9971 .update(cx, |project, cx| {
9972 project.open_local_buffer(path!("/file.rs"), cx)
9973 })
9974 .await
9975 .unwrap();
9976
9977 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9978 let (editor, cx) = cx.add_window_view(|window, cx| {
9979 build_editor_with_project(project.clone(), buffer, window, cx)
9980 });
9981 editor.update_in(cx, |editor, window, cx| {
9982 editor.set_text("one\ntwo\nthree\n", window, cx)
9983 });
9984
9985 cx.executor().start_waiting();
9986 let fake_server = fake_servers.next().await.unwrap();
9987
9988 let format = editor
9989 .update_in(cx, |editor, window, cx| {
9990 editor.perform_format(
9991 project.clone(),
9992 FormatTrigger::Manual,
9993 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9994 window,
9995 cx,
9996 )
9997 })
9998 .unwrap();
9999 fake_server
10000 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10001 assert_eq!(
10002 params.text_document.uri,
10003 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10004 );
10005 assert_eq!(params.options.tab_size, 4);
10006 Ok(Some(vec![lsp::TextEdit::new(
10007 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10008 ", ".to_string(),
10009 )]))
10010 })
10011 .next()
10012 .await;
10013 cx.executor().start_waiting();
10014 format.await;
10015 assert_eq!(
10016 editor.update(cx, |editor, cx| editor.text(cx)),
10017 "one, two\nthree\n"
10018 );
10019
10020 editor.update_in(cx, |editor, window, cx| {
10021 editor.set_text("one\ntwo\nthree\n", window, cx)
10022 });
10023 // Ensure we don't lock if formatting hangs.
10024 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10025 move |params, _| async move {
10026 assert_eq!(
10027 params.text_document.uri,
10028 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10029 );
10030 futures::future::pending::<()>().await;
10031 unreachable!()
10032 },
10033 );
10034 let format = editor
10035 .update_in(cx, |editor, window, cx| {
10036 editor.perform_format(
10037 project,
10038 FormatTrigger::Manual,
10039 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10040 window,
10041 cx,
10042 )
10043 })
10044 .unwrap();
10045 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10046 cx.executor().start_waiting();
10047 format.await;
10048 assert_eq!(
10049 editor.update(cx, |editor, cx| editor.text(cx)),
10050 "one\ntwo\nthree\n"
10051 );
10052}
10053
10054#[gpui::test]
10055async fn test_multiple_formatters(cx: &mut TestAppContext) {
10056 init_test(cx, |settings| {
10057 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10058 settings.defaults.formatter =
10059 Some(language_settings::SelectedFormatter::List(FormatterList(
10060 vec![
10061 Formatter::LanguageServer { name: None },
10062 Formatter::CodeActions(
10063 [
10064 ("code-action-1".into(), true),
10065 ("code-action-2".into(), true),
10066 ]
10067 .into_iter()
10068 .collect(),
10069 ),
10070 ]
10071 .into(),
10072 )))
10073 });
10074
10075 let fs = FakeFs::new(cx.executor());
10076 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10077 .await;
10078
10079 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10080 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10081 language_registry.add(rust_lang());
10082
10083 let mut fake_servers = language_registry.register_fake_lsp(
10084 "Rust",
10085 FakeLspAdapter {
10086 capabilities: lsp::ServerCapabilities {
10087 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10088 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10089 commands: vec!["the-command-for-code-action-1".into()],
10090 ..Default::default()
10091 }),
10092 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10093 ..Default::default()
10094 },
10095 ..Default::default()
10096 },
10097 );
10098
10099 let buffer = project
10100 .update(cx, |project, cx| {
10101 project.open_local_buffer(path!("/file.rs"), cx)
10102 })
10103 .await
10104 .unwrap();
10105
10106 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10107 let (editor, cx) = cx.add_window_view(|window, cx| {
10108 build_editor_with_project(project.clone(), buffer, window, cx)
10109 });
10110
10111 cx.executor().start_waiting();
10112
10113 let fake_server = fake_servers.next().await.unwrap();
10114 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10115 move |_params, _| async move {
10116 Ok(Some(vec![lsp::TextEdit::new(
10117 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10118 "applied-formatting\n".to_string(),
10119 )]))
10120 },
10121 );
10122 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10123 move |params, _| async move {
10124 assert_eq!(
10125 params.context.only,
10126 Some(vec!["code-action-1".into(), "code-action-2".into()])
10127 );
10128 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10129 Ok(Some(vec![
10130 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10131 kind: Some("code-action-1".into()),
10132 edit: Some(lsp::WorkspaceEdit::new(
10133 [(
10134 uri.clone(),
10135 vec![lsp::TextEdit::new(
10136 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10137 "applied-code-action-1-edit\n".to_string(),
10138 )],
10139 )]
10140 .into_iter()
10141 .collect(),
10142 )),
10143 command: Some(lsp::Command {
10144 command: "the-command-for-code-action-1".into(),
10145 ..Default::default()
10146 }),
10147 ..Default::default()
10148 }),
10149 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10150 kind: Some("code-action-2".into()),
10151 edit: Some(lsp::WorkspaceEdit::new(
10152 [(
10153 uri.clone(),
10154 vec![lsp::TextEdit::new(
10155 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10156 "applied-code-action-2-edit\n".to_string(),
10157 )],
10158 )]
10159 .into_iter()
10160 .collect(),
10161 )),
10162 ..Default::default()
10163 }),
10164 ]))
10165 },
10166 );
10167
10168 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10169 move |params, _| async move { Ok(params) }
10170 });
10171
10172 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10173 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10174 let fake = fake_server.clone();
10175 let lock = command_lock.clone();
10176 move |params, _| {
10177 assert_eq!(params.command, "the-command-for-code-action-1");
10178 let fake = fake.clone();
10179 let lock = lock.clone();
10180 async move {
10181 lock.lock().await;
10182 fake.server
10183 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10184 label: None,
10185 edit: lsp::WorkspaceEdit {
10186 changes: Some(
10187 [(
10188 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10189 vec![lsp::TextEdit {
10190 range: lsp::Range::new(
10191 lsp::Position::new(0, 0),
10192 lsp::Position::new(0, 0),
10193 ),
10194 new_text: "applied-code-action-1-command\n".into(),
10195 }],
10196 )]
10197 .into_iter()
10198 .collect(),
10199 ),
10200 ..Default::default()
10201 },
10202 })
10203 .await
10204 .into_response()
10205 .unwrap();
10206 Ok(Some(json!(null)))
10207 }
10208 }
10209 });
10210
10211 cx.executor().start_waiting();
10212 editor
10213 .update_in(cx, |editor, window, cx| {
10214 editor.perform_format(
10215 project.clone(),
10216 FormatTrigger::Manual,
10217 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10218 window,
10219 cx,
10220 )
10221 })
10222 .unwrap()
10223 .await;
10224 editor.update(cx, |editor, cx| {
10225 assert_eq!(
10226 editor.text(cx),
10227 r#"
10228 applied-code-action-2-edit
10229 applied-code-action-1-command
10230 applied-code-action-1-edit
10231 applied-formatting
10232 one
10233 two
10234 three
10235 "#
10236 .unindent()
10237 );
10238 });
10239
10240 editor.update_in(cx, |editor, window, cx| {
10241 editor.undo(&Default::default(), window, cx);
10242 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10243 });
10244
10245 // Perform a manual edit while waiting for an LSP command
10246 // that's being run as part of a formatting code action.
10247 let lock_guard = command_lock.lock().await;
10248 let format = editor
10249 .update_in(cx, |editor, window, cx| {
10250 editor.perform_format(
10251 project.clone(),
10252 FormatTrigger::Manual,
10253 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10254 window,
10255 cx,
10256 )
10257 })
10258 .unwrap();
10259 cx.run_until_parked();
10260 editor.update(cx, |editor, cx| {
10261 assert_eq!(
10262 editor.text(cx),
10263 r#"
10264 applied-code-action-1-edit
10265 applied-formatting
10266 one
10267 two
10268 three
10269 "#
10270 .unindent()
10271 );
10272
10273 editor.buffer.update(cx, |buffer, cx| {
10274 let ix = buffer.len(cx);
10275 buffer.edit([(ix..ix, "edited\n")], None, cx);
10276 });
10277 });
10278
10279 // Allow the LSP command to proceed. Because the buffer was edited,
10280 // the second code action will not be run.
10281 drop(lock_guard);
10282 format.await;
10283 editor.update_in(cx, |editor, window, cx| {
10284 assert_eq!(
10285 editor.text(cx),
10286 r#"
10287 applied-code-action-1-command
10288 applied-code-action-1-edit
10289 applied-formatting
10290 one
10291 two
10292 three
10293 edited
10294 "#
10295 .unindent()
10296 );
10297
10298 // The manual edit is undone first, because it is the last thing the user did
10299 // (even though the command completed afterwards).
10300 editor.undo(&Default::default(), window, cx);
10301 assert_eq!(
10302 editor.text(cx),
10303 r#"
10304 applied-code-action-1-command
10305 applied-code-action-1-edit
10306 applied-formatting
10307 one
10308 two
10309 three
10310 "#
10311 .unindent()
10312 );
10313
10314 // All the formatting (including the command, which completed after the manual edit)
10315 // is undone together.
10316 editor.undo(&Default::default(), window, cx);
10317 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10318 });
10319}
10320
10321#[gpui::test]
10322async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10323 init_test(cx, |settings| {
10324 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
10325 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
10326 ))
10327 });
10328
10329 let fs = FakeFs::new(cx.executor());
10330 fs.insert_file(path!("/file.ts"), Default::default()).await;
10331
10332 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10333
10334 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10335 language_registry.add(Arc::new(Language::new(
10336 LanguageConfig {
10337 name: "TypeScript".into(),
10338 matcher: LanguageMatcher {
10339 path_suffixes: vec!["ts".to_string()],
10340 ..Default::default()
10341 },
10342 ..LanguageConfig::default()
10343 },
10344 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10345 )));
10346 update_test_language_settings(cx, |settings| {
10347 settings.defaults.prettier = Some(PrettierSettings {
10348 allowed: true,
10349 ..PrettierSettings::default()
10350 });
10351 });
10352 let mut fake_servers = language_registry.register_fake_lsp(
10353 "TypeScript",
10354 FakeLspAdapter {
10355 capabilities: lsp::ServerCapabilities {
10356 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10357 ..Default::default()
10358 },
10359 ..Default::default()
10360 },
10361 );
10362
10363 let buffer = project
10364 .update(cx, |project, cx| {
10365 project.open_local_buffer(path!("/file.ts"), cx)
10366 })
10367 .await
10368 .unwrap();
10369
10370 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10371 let (editor, cx) = cx.add_window_view(|window, cx| {
10372 build_editor_with_project(project.clone(), buffer, window, cx)
10373 });
10374 editor.update_in(cx, |editor, window, cx| {
10375 editor.set_text(
10376 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10377 window,
10378 cx,
10379 )
10380 });
10381
10382 cx.executor().start_waiting();
10383 let fake_server = fake_servers.next().await.unwrap();
10384
10385 let format = editor
10386 .update_in(cx, |editor, window, cx| {
10387 editor.perform_code_action_kind(
10388 project.clone(),
10389 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10390 window,
10391 cx,
10392 )
10393 })
10394 .unwrap();
10395 fake_server
10396 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10397 assert_eq!(
10398 params.text_document.uri,
10399 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10400 );
10401 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10402 lsp::CodeAction {
10403 title: "Organize Imports".to_string(),
10404 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10405 edit: Some(lsp::WorkspaceEdit {
10406 changes: Some(
10407 [(
10408 params.text_document.uri.clone(),
10409 vec![lsp::TextEdit::new(
10410 lsp::Range::new(
10411 lsp::Position::new(1, 0),
10412 lsp::Position::new(2, 0),
10413 ),
10414 "".to_string(),
10415 )],
10416 )]
10417 .into_iter()
10418 .collect(),
10419 ),
10420 ..Default::default()
10421 }),
10422 ..Default::default()
10423 },
10424 )]))
10425 })
10426 .next()
10427 .await;
10428 cx.executor().start_waiting();
10429 format.await;
10430 assert_eq!(
10431 editor.update(cx, |editor, cx| editor.text(cx)),
10432 "import { a } from 'module';\n\nconst x = a;\n"
10433 );
10434
10435 editor.update_in(cx, |editor, window, cx| {
10436 editor.set_text(
10437 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10438 window,
10439 cx,
10440 )
10441 });
10442 // Ensure we don't lock if code action hangs.
10443 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10444 move |params, _| async move {
10445 assert_eq!(
10446 params.text_document.uri,
10447 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10448 );
10449 futures::future::pending::<()>().await;
10450 unreachable!()
10451 },
10452 );
10453 let format = editor
10454 .update_in(cx, |editor, window, cx| {
10455 editor.perform_code_action_kind(
10456 project,
10457 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10458 window,
10459 cx,
10460 )
10461 })
10462 .unwrap();
10463 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10464 cx.executor().start_waiting();
10465 format.await;
10466 assert_eq!(
10467 editor.update(cx, |editor, cx| editor.text(cx)),
10468 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10469 );
10470}
10471
10472#[gpui::test]
10473async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10474 init_test(cx, |_| {});
10475
10476 let mut cx = EditorLspTestContext::new_rust(
10477 lsp::ServerCapabilities {
10478 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10479 ..Default::default()
10480 },
10481 cx,
10482 )
10483 .await;
10484
10485 cx.set_state(indoc! {"
10486 one.twoˇ
10487 "});
10488
10489 // The format request takes a long time. When it completes, it inserts
10490 // a newline and an indent before the `.`
10491 cx.lsp
10492 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10493 let executor = cx.background_executor().clone();
10494 async move {
10495 executor.timer(Duration::from_millis(100)).await;
10496 Ok(Some(vec![lsp::TextEdit {
10497 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10498 new_text: "\n ".into(),
10499 }]))
10500 }
10501 });
10502
10503 // Submit a format request.
10504 let format_1 = cx
10505 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10506 .unwrap();
10507 cx.executor().run_until_parked();
10508
10509 // Submit a second format request.
10510 let format_2 = cx
10511 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10512 .unwrap();
10513 cx.executor().run_until_parked();
10514
10515 // Wait for both format requests to complete
10516 cx.executor().advance_clock(Duration::from_millis(200));
10517 cx.executor().start_waiting();
10518 format_1.await.unwrap();
10519 cx.executor().start_waiting();
10520 format_2.await.unwrap();
10521
10522 // The formatting edits only happens once.
10523 cx.assert_editor_state(indoc! {"
10524 one
10525 .twoˇ
10526 "});
10527}
10528
10529#[gpui::test]
10530async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10531 init_test(cx, |settings| {
10532 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10533 });
10534
10535 let mut cx = EditorLspTestContext::new_rust(
10536 lsp::ServerCapabilities {
10537 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10538 ..Default::default()
10539 },
10540 cx,
10541 )
10542 .await;
10543
10544 // Set up a buffer white some trailing whitespace and no trailing newline.
10545 cx.set_state(
10546 &[
10547 "one ", //
10548 "twoˇ", //
10549 "three ", //
10550 "four", //
10551 ]
10552 .join("\n"),
10553 );
10554
10555 // Submit a format request.
10556 let format = cx
10557 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10558 .unwrap();
10559
10560 // Record which buffer changes have been sent to the language server
10561 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10562 cx.lsp
10563 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10564 let buffer_changes = buffer_changes.clone();
10565 move |params, _| {
10566 buffer_changes.lock().extend(
10567 params
10568 .content_changes
10569 .into_iter()
10570 .map(|e| (e.range.unwrap(), e.text)),
10571 );
10572 }
10573 });
10574
10575 // Handle formatting requests to the language server.
10576 cx.lsp
10577 .set_request_handler::<lsp::request::Formatting, _, _>({
10578 let buffer_changes = buffer_changes.clone();
10579 move |_, _| {
10580 // When formatting is requested, trailing whitespace has already been stripped,
10581 // and the trailing newline has already been added.
10582 assert_eq!(
10583 &buffer_changes.lock()[1..],
10584 &[
10585 (
10586 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10587 "".into()
10588 ),
10589 (
10590 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10591 "".into()
10592 ),
10593 (
10594 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10595 "\n".into()
10596 ),
10597 ]
10598 );
10599
10600 // Insert blank lines between each line of the buffer.
10601 async move {
10602 Ok(Some(vec![
10603 lsp::TextEdit {
10604 range: lsp::Range::new(
10605 lsp::Position::new(1, 0),
10606 lsp::Position::new(1, 0),
10607 ),
10608 new_text: "\n".into(),
10609 },
10610 lsp::TextEdit {
10611 range: lsp::Range::new(
10612 lsp::Position::new(2, 0),
10613 lsp::Position::new(2, 0),
10614 ),
10615 new_text: "\n".into(),
10616 },
10617 ]))
10618 }
10619 }
10620 });
10621
10622 // After formatting the buffer, the trailing whitespace is stripped,
10623 // a newline is appended, and the edits provided by the language server
10624 // have been applied.
10625 format.await.unwrap();
10626 cx.assert_editor_state(
10627 &[
10628 "one", //
10629 "", //
10630 "twoˇ", //
10631 "", //
10632 "three", //
10633 "four", //
10634 "", //
10635 ]
10636 .join("\n"),
10637 );
10638
10639 // Undoing the formatting undoes the trailing whitespace removal, the
10640 // trailing newline, and the LSP edits.
10641 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10642 cx.assert_editor_state(
10643 &[
10644 "one ", //
10645 "twoˇ", //
10646 "three ", //
10647 "four", //
10648 ]
10649 .join("\n"),
10650 );
10651}
10652
10653#[gpui::test]
10654async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10655 cx: &mut TestAppContext,
10656) {
10657 init_test(cx, |_| {});
10658
10659 cx.update(|cx| {
10660 cx.update_global::<SettingsStore, _>(|settings, cx| {
10661 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10662 settings.auto_signature_help = Some(true);
10663 });
10664 });
10665 });
10666
10667 let mut cx = EditorLspTestContext::new_rust(
10668 lsp::ServerCapabilities {
10669 signature_help_provider: Some(lsp::SignatureHelpOptions {
10670 ..Default::default()
10671 }),
10672 ..Default::default()
10673 },
10674 cx,
10675 )
10676 .await;
10677
10678 let language = Language::new(
10679 LanguageConfig {
10680 name: "Rust".into(),
10681 brackets: BracketPairConfig {
10682 pairs: vec![
10683 BracketPair {
10684 start: "{".to_string(),
10685 end: "}".to_string(),
10686 close: true,
10687 surround: true,
10688 newline: true,
10689 },
10690 BracketPair {
10691 start: "(".to_string(),
10692 end: ")".to_string(),
10693 close: true,
10694 surround: true,
10695 newline: true,
10696 },
10697 BracketPair {
10698 start: "/*".to_string(),
10699 end: " */".to_string(),
10700 close: true,
10701 surround: true,
10702 newline: true,
10703 },
10704 BracketPair {
10705 start: "[".to_string(),
10706 end: "]".to_string(),
10707 close: false,
10708 surround: false,
10709 newline: true,
10710 },
10711 BracketPair {
10712 start: "\"".to_string(),
10713 end: "\"".to_string(),
10714 close: true,
10715 surround: true,
10716 newline: false,
10717 },
10718 BracketPair {
10719 start: "<".to_string(),
10720 end: ">".to_string(),
10721 close: false,
10722 surround: true,
10723 newline: true,
10724 },
10725 ],
10726 ..Default::default()
10727 },
10728 autoclose_before: "})]".to_string(),
10729 ..Default::default()
10730 },
10731 Some(tree_sitter_rust::LANGUAGE.into()),
10732 );
10733 let language = Arc::new(language);
10734
10735 cx.language_registry().add(language.clone());
10736 cx.update_buffer(|buffer, cx| {
10737 buffer.set_language(Some(language), cx);
10738 });
10739
10740 cx.set_state(
10741 &r#"
10742 fn main() {
10743 sampleˇ
10744 }
10745 "#
10746 .unindent(),
10747 );
10748
10749 cx.update_editor(|editor, window, cx| {
10750 editor.handle_input("(", window, cx);
10751 });
10752 cx.assert_editor_state(
10753 &"
10754 fn main() {
10755 sample(ˇ)
10756 }
10757 "
10758 .unindent(),
10759 );
10760
10761 let mocked_response = lsp::SignatureHelp {
10762 signatures: vec![lsp::SignatureInformation {
10763 label: "fn sample(param1: u8, param2: u8)".to_string(),
10764 documentation: None,
10765 parameters: Some(vec![
10766 lsp::ParameterInformation {
10767 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10768 documentation: None,
10769 },
10770 lsp::ParameterInformation {
10771 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10772 documentation: None,
10773 },
10774 ]),
10775 active_parameter: None,
10776 }],
10777 active_signature: Some(0),
10778 active_parameter: Some(0),
10779 };
10780 handle_signature_help_request(&mut cx, mocked_response).await;
10781
10782 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10783 .await;
10784
10785 cx.editor(|editor, _, _| {
10786 let signature_help_state = editor.signature_help_state.popover().cloned();
10787 assert_eq!(
10788 signature_help_state.unwrap().label,
10789 "param1: u8, param2: u8"
10790 );
10791 });
10792}
10793
10794#[gpui::test]
10795async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10796 init_test(cx, |_| {});
10797
10798 cx.update(|cx| {
10799 cx.update_global::<SettingsStore, _>(|settings, cx| {
10800 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10801 settings.auto_signature_help = Some(false);
10802 settings.show_signature_help_after_edits = Some(false);
10803 });
10804 });
10805 });
10806
10807 let mut cx = EditorLspTestContext::new_rust(
10808 lsp::ServerCapabilities {
10809 signature_help_provider: Some(lsp::SignatureHelpOptions {
10810 ..Default::default()
10811 }),
10812 ..Default::default()
10813 },
10814 cx,
10815 )
10816 .await;
10817
10818 let language = Language::new(
10819 LanguageConfig {
10820 name: "Rust".into(),
10821 brackets: BracketPairConfig {
10822 pairs: vec![
10823 BracketPair {
10824 start: "{".to_string(),
10825 end: "}".to_string(),
10826 close: true,
10827 surround: true,
10828 newline: true,
10829 },
10830 BracketPair {
10831 start: "(".to_string(),
10832 end: ")".to_string(),
10833 close: true,
10834 surround: true,
10835 newline: true,
10836 },
10837 BracketPair {
10838 start: "/*".to_string(),
10839 end: " */".to_string(),
10840 close: true,
10841 surround: true,
10842 newline: true,
10843 },
10844 BracketPair {
10845 start: "[".to_string(),
10846 end: "]".to_string(),
10847 close: false,
10848 surround: false,
10849 newline: true,
10850 },
10851 BracketPair {
10852 start: "\"".to_string(),
10853 end: "\"".to_string(),
10854 close: true,
10855 surround: true,
10856 newline: false,
10857 },
10858 BracketPair {
10859 start: "<".to_string(),
10860 end: ">".to_string(),
10861 close: false,
10862 surround: true,
10863 newline: true,
10864 },
10865 ],
10866 ..Default::default()
10867 },
10868 autoclose_before: "})]".to_string(),
10869 ..Default::default()
10870 },
10871 Some(tree_sitter_rust::LANGUAGE.into()),
10872 );
10873 let language = Arc::new(language);
10874
10875 cx.language_registry().add(language.clone());
10876 cx.update_buffer(|buffer, cx| {
10877 buffer.set_language(Some(language), cx);
10878 });
10879
10880 // Ensure that signature_help is not called when no signature help is enabled.
10881 cx.set_state(
10882 &r#"
10883 fn main() {
10884 sampleˇ
10885 }
10886 "#
10887 .unindent(),
10888 );
10889 cx.update_editor(|editor, window, cx| {
10890 editor.handle_input("(", window, cx);
10891 });
10892 cx.assert_editor_state(
10893 &"
10894 fn main() {
10895 sample(ˇ)
10896 }
10897 "
10898 .unindent(),
10899 );
10900 cx.editor(|editor, _, _| {
10901 assert!(editor.signature_help_state.task().is_none());
10902 });
10903
10904 let mocked_response = lsp::SignatureHelp {
10905 signatures: vec![lsp::SignatureInformation {
10906 label: "fn sample(param1: u8, param2: u8)".to_string(),
10907 documentation: None,
10908 parameters: Some(vec![
10909 lsp::ParameterInformation {
10910 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10911 documentation: None,
10912 },
10913 lsp::ParameterInformation {
10914 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10915 documentation: None,
10916 },
10917 ]),
10918 active_parameter: None,
10919 }],
10920 active_signature: Some(0),
10921 active_parameter: Some(0),
10922 };
10923
10924 // Ensure that signature_help is called when enabled afte edits
10925 cx.update(|_, cx| {
10926 cx.update_global::<SettingsStore, _>(|settings, cx| {
10927 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10928 settings.auto_signature_help = Some(false);
10929 settings.show_signature_help_after_edits = Some(true);
10930 });
10931 });
10932 });
10933 cx.set_state(
10934 &r#"
10935 fn main() {
10936 sampleˇ
10937 }
10938 "#
10939 .unindent(),
10940 );
10941 cx.update_editor(|editor, window, cx| {
10942 editor.handle_input("(", window, cx);
10943 });
10944 cx.assert_editor_state(
10945 &"
10946 fn main() {
10947 sample(ˇ)
10948 }
10949 "
10950 .unindent(),
10951 );
10952 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10953 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10954 .await;
10955 cx.update_editor(|editor, _, _| {
10956 let signature_help_state = editor.signature_help_state.popover().cloned();
10957 assert!(signature_help_state.is_some());
10958 assert_eq!(
10959 signature_help_state.unwrap().label,
10960 "param1: u8, param2: u8"
10961 );
10962 editor.signature_help_state = SignatureHelpState::default();
10963 });
10964
10965 // Ensure that signature_help is called when auto signature help override is enabled
10966 cx.update(|_, cx| {
10967 cx.update_global::<SettingsStore, _>(|settings, cx| {
10968 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10969 settings.auto_signature_help = Some(true);
10970 settings.show_signature_help_after_edits = Some(false);
10971 });
10972 });
10973 });
10974 cx.set_state(
10975 &r#"
10976 fn main() {
10977 sampleˇ
10978 }
10979 "#
10980 .unindent(),
10981 );
10982 cx.update_editor(|editor, window, cx| {
10983 editor.handle_input("(", window, cx);
10984 });
10985 cx.assert_editor_state(
10986 &"
10987 fn main() {
10988 sample(ˇ)
10989 }
10990 "
10991 .unindent(),
10992 );
10993 handle_signature_help_request(&mut cx, mocked_response).await;
10994 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10995 .await;
10996 cx.editor(|editor, _, _| {
10997 let signature_help_state = editor.signature_help_state.popover().cloned();
10998 assert!(signature_help_state.is_some());
10999 assert_eq!(
11000 signature_help_state.unwrap().label,
11001 "param1: u8, param2: u8"
11002 );
11003 });
11004}
11005
11006#[gpui::test]
11007async fn test_signature_help(cx: &mut TestAppContext) {
11008 init_test(cx, |_| {});
11009 cx.update(|cx| {
11010 cx.update_global::<SettingsStore, _>(|settings, cx| {
11011 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11012 settings.auto_signature_help = Some(true);
11013 });
11014 });
11015 });
11016
11017 let mut cx = EditorLspTestContext::new_rust(
11018 lsp::ServerCapabilities {
11019 signature_help_provider: Some(lsp::SignatureHelpOptions {
11020 ..Default::default()
11021 }),
11022 ..Default::default()
11023 },
11024 cx,
11025 )
11026 .await;
11027
11028 // A test that directly calls `show_signature_help`
11029 cx.update_editor(|editor, window, cx| {
11030 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11031 });
11032
11033 let mocked_response = lsp::SignatureHelp {
11034 signatures: vec![lsp::SignatureInformation {
11035 label: "fn sample(param1: u8, param2: u8)".to_string(),
11036 documentation: None,
11037 parameters: Some(vec![
11038 lsp::ParameterInformation {
11039 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11040 documentation: None,
11041 },
11042 lsp::ParameterInformation {
11043 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11044 documentation: None,
11045 },
11046 ]),
11047 active_parameter: None,
11048 }],
11049 active_signature: Some(0),
11050 active_parameter: Some(0),
11051 };
11052 handle_signature_help_request(&mut cx, mocked_response).await;
11053
11054 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11055 .await;
11056
11057 cx.editor(|editor, _, _| {
11058 let signature_help_state = editor.signature_help_state.popover().cloned();
11059 assert!(signature_help_state.is_some());
11060 assert_eq!(
11061 signature_help_state.unwrap().label,
11062 "param1: u8, param2: u8"
11063 );
11064 });
11065
11066 // When exiting outside from inside the brackets, `signature_help` is closed.
11067 cx.set_state(indoc! {"
11068 fn main() {
11069 sample(ˇ);
11070 }
11071
11072 fn sample(param1: u8, param2: u8) {}
11073 "});
11074
11075 cx.update_editor(|editor, window, cx| {
11076 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11077 });
11078
11079 let mocked_response = lsp::SignatureHelp {
11080 signatures: Vec::new(),
11081 active_signature: None,
11082 active_parameter: None,
11083 };
11084 handle_signature_help_request(&mut cx, mocked_response).await;
11085
11086 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11087 .await;
11088
11089 cx.editor(|editor, _, _| {
11090 assert!(!editor.signature_help_state.is_shown());
11091 });
11092
11093 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11094 cx.set_state(indoc! {"
11095 fn main() {
11096 sample(ˇ);
11097 }
11098
11099 fn sample(param1: u8, param2: u8) {}
11100 "});
11101
11102 let mocked_response = lsp::SignatureHelp {
11103 signatures: vec![lsp::SignatureInformation {
11104 label: "fn sample(param1: u8, param2: u8)".to_string(),
11105 documentation: None,
11106 parameters: Some(vec![
11107 lsp::ParameterInformation {
11108 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11109 documentation: None,
11110 },
11111 lsp::ParameterInformation {
11112 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11113 documentation: None,
11114 },
11115 ]),
11116 active_parameter: None,
11117 }],
11118 active_signature: Some(0),
11119 active_parameter: Some(0),
11120 };
11121 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11122 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11123 .await;
11124 cx.editor(|editor, _, _| {
11125 assert!(editor.signature_help_state.is_shown());
11126 });
11127
11128 // Restore the popover with more parameter input
11129 cx.set_state(indoc! {"
11130 fn main() {
11131 sample(param1, param2ˇ);
11132 }
11133
11134 fn sample(param1: u8, param2: u8) {}
11135 "});
11136
11137 let mocked_response = lsp::SignatureHelp {
11138 signatures: vec![lsp::SignatureInformation {
11139 label: "fn sample(param1: u8, param2: u8)".to_string(),
11140 documentation: None,
11141 parameters: Some(vec![
11142 lsp::ParameterInformation {
11143 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11144 documentation: None,
11145 },
11146 lsp::ParameterInformation {
11147 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11148 documentation: None,
11149 },
11150 ]),
11151 active_parameter: None,
11152 }],
11153 active_signature: Some(0),
11154 active_parameter: Some(1),
11155 };
11156 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11157 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11158 .await;
11159
11160 // When selecting a range, the popover is gone.
11161 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11162 cx.update_editor(|editor, window, cx| {
11163 editor.change_selections(None, window, cx, |s| {
11164 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11165 })
11166 });
11167 cx.assert_editor_state(indoc! {"
11168 fn main() {
11169 sample(param1, «ˇparam2»);
11170 }
11171
11172 fn sample(param1: u8, param2: u8) {}
11173 "});
11174 cx.editor(|editor, _, _| {
11175 assert!(!editor.signature_help_state.is_shown());
11176 });
11177
11178 // When unselecting again, the popover is back if within the brackets.
11179 cx.update_editor(|editor, window, cx| {
11180 editor.change_selections(None, window, cx, |s| {
11181 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11182 })
11183 });
11184 cx.assert_editor_state(indoc! {"
11185 fn main() {
11186 sample(param1, ˇparam2);
11187 }
11188
11189 fn sample(param1: u8, param2: u8) {}
11190 "});
11191 handle_signature_help_request(&mut cx, mocked_response).await;
11192 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11193 .await;
11194 cx.editor(|editor, _, _| {
11195 assert!(editor.signature_help_state.is_shown());
11196 });
11197
11198 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11199 cx.update_editor(|editor, window, cx| {
11200 editor.change_selections(None, window, cx, |s| {
11201 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11202 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11203 })
11204 });
11205 cx.assert_editor_state(indoc! {"
11206 fn main() {
11207 sample(param1, ˇparam2);
11208 }
11209
11210 fn sample(param1: u8, param2: u8) {}
11211 "});
11212
11213 let mocked_response = lsp::SignatureHelp {
11214 signatures: vec![lsp::SignatureInformation {
11215 label: "fn sample(param1: u8, param2: u8)".to_string(),
11216 documentation: None,
11217 parameters: Some(vec![
11218 lsp::ParameterInformation {
11219 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11220 documentation: None,
11221 },
11222 lsp::ParameterInformation {
11223 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11224 documentation: None,
11225 },
11226 ]),
11227 active_parameter: None,
11228 }],
11229 active_signature: Some(0),
11230 active_parameter: Some(1),
11231 };
11232 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11233 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11234 .await;
11235 cx.update_editor(|editor, _, cx| {
11236 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11237 });
11238 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11239 .await;
11240 cx.update_editor(|editor, window, cx| {
11241 editor.change_selections(None, window, cx, |s| {
11242 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11243 })
11244 });
11245 cx.assert_editor_state(indoc! {"
11246 fn main() {
11247 sample(param1, «ˇparam2»);
11248 }
11249
11250 fn sample(param1: u8, param2: u8) {}
11251 "});
11252 cx.update_editor(|editor, window, cx| {
11253 editor.change_selections(None, window, cx, |s| {
11254 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11255 })
11256 });
11257 cx.assert_editor_state(indoc! {"
11258 fn main() {
11259 sample(param1, ˇparam2);
11260 }
11261
11262 fn sample(param1: u8, param2: u8) {}
11263 "});
11264 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11265 .await;
11266}
11267
11268#[gpui::test]
11269async fn test_completion_mode(cx: &mut TestAppContext) {
11270 init_test(cx, |_| {});
11271 let mut cx = EditorLspTestContext::new_rust(
11272 lsp::ServerCapabilities {
11273 completion_provider: Some(lsp::CompletionOptions {
11274 resolve_provider: Some(true),
11275 ..Default::default()
11276 }),
11277 ..Default::default()
11278 },
11279 cx,
11280 )
11281 .await;
11282
11283 struct Run {
11284 run_description: &'static str,
11285 initial_state: String,
11286 buffer_marked_text: String,
11287 completion_label: &'static str,
11288 completion_text: &'static str,
11289 expected_with_insert_mode: String,
11290 expected_with_replace_mode: String,
11291 expected_with_replace_subsequence_mode: String,
11292 expected_with_replace_suffix_mode: String,
11293 }
11294
11295 let runs = [
11296 Run {
11297 run_description: "Start of word matches completion text",
11298 initial_state: "before ediˇ after".into(),
11299 buffer_marked_text: "before <edi|> after".into(),
11300 completion_label: "editor",
11301 completion_text: "editor",
11302 expected_with_insert_mode: "before editorˇ after".into(),
11303 expected_with_replace_mode: "before editorˇ after".into(),
11304 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11305 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11306 },
11307 Run {
11308 run_description: "Accept same text at the middle of the word",
11309 initial_state: "before ediˇtor after".into(),
11310 buffer_marked_text: "before <edi|tor> after".into(),
11311 completion_label: "editor",
11312 completion_text: "editor",
11313 expected_with_insert_mode: "before editorˇtor after".into(),
11314 expected_with_replace_mode: "before editorˇ after".into(),
11315 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11316 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11317 },
11318 Run {
11319 run_description: "End of word matches completion text -- cursor at end",
11320 initial_state: "before torˇ after".into(),
11321 buffer_marked_text: "before <tor|> after".into(),
11322 completion_label: "editor",
11323 completion_text: "editor",
11324 expected_with_insert_mode: "before editorˇ after".into(),
11325 expected_with_replace_mode: "before editorˇ after".into(),
11326 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11327 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11328 },
11329 Run {
11330 run_description: "End of word matches completion text -- cursor at start",
11331 initial_state: "before ˇtor after".into(),
11332 buffer_marked_text: "before <|tor> after".into(),
11333 completion_label: "editor",
11334 completion_text: "editor",
11335 expected_with_insert_mode: "before editorˇtor after".into(),
11336 expected_with_replace_mode: "before editorˇ after".into(),
11337 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11338 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11339 },
11340 Run {
11341 run_description: "Prepend text containing whitespace",
11342 initial_state: "pˇfield: bool".into(),
11343 buffer_marked_text: "<p|field>: bool".into(),
11344 completion_label: "pub ",
11345 completion_text: "pub ",
11346 expected_with_insert_mode: "pub ˇfield: bool".into(),
11347 expected_with_replace_mode: "pub ˇ: bool".into(),
11348 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11349 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11350 },
11351 Run {
11352 run_description: "Add element to start of list",
11353 initial_state: "[element_ˇelement_2]".into(),
11354 buffer_marked_text: "[<element_|element_2>]".into(),
11355 completion_label: "element_1",
11356 completion_text: "element_1",
11357 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11358 expected_with_replace_mode: "[element_1ˇ]".into(),
11359 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11360 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11361 },
11362 Run {
11363 run_description: "Add element to start of list -- first and second elements are equal",
11364 initial_state: "[elˇelement]".into(),
11365 buffer_marked_text: "[<el|element>]".into(),
11366 completion_label: "element",
11367 completion_text: "element",
11368 expected_with_insert_mode: "[elementˇelement]".into(),
11369 expected_with_replace_mode: "[elementˇ]".into(),
11370 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11371 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11372 },
11373 Run {
11374 run_description: "Ends with matching suffix",
11375 initial_state: "SubˇError".into(),
11376 buffer_marked_text: "<Sub|Error>".into(),
11377 completion_label: "SubscriptionError",
11378 completion_text: "SubscriptionError",
11379 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11380 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11381 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11382 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11383 },
11384 Run {
11385 run_description: "Suffix is a subsequence -- contiguous",
11386 initial_state: "SubˇErr".into(),
11387 buffer_marked_text: "<Sub|Err>".into(),
11388 completion_label: "SubscriptionError",
11389 completion_text: "SubscriptionError",
11390 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11391 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11392 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11393 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11394 },
11395 Run {
11396 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11397 initial_state: "Suˇscrirr".into(),
11398 buffer_marked_text: "<Su|scrirr>".into(),
11399 completion_label: "SubscriptionError",
11400 completion_text: "SubscriptionError",
11401 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11402 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11403 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11404 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11405 },
11406 Run {
11407 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11408 initial_state: "foo(indˇix)".into(),
11409 buffer_marked_text: "foo(<ind|ix>)".into(),
11410 completion_label: "node_index",
11411 completion_text: "node_index",
11412 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11413 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11414 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11415 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11416 },
11417 Run {
11418 run_description: "Replace range ends before cursor - should extend to cursor",
11419 initial_state: "before editˇo after".into(),
11420 buffer_marked_text: "before <{ed}>it|o after".into(),
11421 completion_label: "editor",
11422 completion_text: "editor",
11423 expected_with_insert_mode: "before editorˇo after".into(),
11424 expected_with_replace_mode: "before editorˇo after".into(),
11425 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11426 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11427 },
11428 Run {
11429 run_description: "Uses label for suffix matching",
11430 initial_state: "before ediˇtor after".into(),
11431 buffer_marked_text: "before <edi|tor> after".into(),
11432 completion_label: "editor",
11433 completion_text: "editor()",
11434 expected_with_insert_mode: "before editor()ˇtor after".into(),
11435 expected_with_replace_mode: "before editor()ˇ after".into(),
11436 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11437 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11438 },
11439 Run {
11440 run_description: "Case insensitive subsequence and suffix matching",
11441 initial_state: "before EDiˇtoR after".into(),
11442 buffer_marked_text: "before <EDi|toR> after".into(),
11443 completion_label: "editor",
11444 completion_text: "editor",
11445 expected_with_insert_mode: "before editorˇtoR after".into(),
11446 expected_with_replace_mode: "before editorˇ after".into(),
11447 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11448 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11449 },
11450 ];
11451
11452 for run in runs {
11453 let run_variations = [
11454 (LspInsertMode::Insert, run.expected_with_insert_mode),
11455 (LspInsertMode::Replace, run.expected_with_replace_mode),
11456 (
11457 LspInsertMode::ReplaceSubsequence,
11458 run.expected_with_replace_subsequence_mode,
11459 ),
11460 (
11461 LspInsertMode::ReplaceSuffix,
11462 run.expected_with_replace_suffix_mode,
11463 ),
11464 ];
11465
11466 for (lsp_insert_mode, expected_text) in run_variations {
11467 eprintln!(
11468 "run = {:?}, mode = {lsp_insert_mode:.?}",
11469 run.run_description,
11470 );
11471
11472 update_test_language_settings(&mut cx, |settings| {
11473 settings.defaults.completions = Some(CompletionSettings {
11474 lsp_insert_mode,
11475 words: WordsCompletionMode::Disabled,
11476 lsp: true,
11477 lsp_fetch_timeout_ms: 0,
11478 });
11479 });
11480
11481 cx.set_state(&run.initial_state);
11482 cx.update_editor(|editor, window, cx| {
11483 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11484 });
11485
11486 let counter = Arc::new(AtomicUsize::new(0));
11487 handle_completion_request_with_insert_and_replace(
11488 &mut cx,
11489 &run.buffer_marked_text,
11490 vec![(run.completion_label, run.completion_text)],
11491 counter.clone(),
11492 )
11493 .await;
11494 cx.condition(|editor, _| editor.context_menu_visible())
11495 .await;
11496 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11497
11498 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11499 editor
11500 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11501 .unwrap()
11502 });
11503 cx.assert_editor_state(&expected_text);
11504 handle_resolve_completion_request(&mut cx, None).await;
11505 apply_additional_edits.await.unwrap();
11506 }
11507 }
11508}
11509
11510#[gpui::test]
11511async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11512 init_test(cx, |_| {});
11513 let mut cx = EditorLspTestContext::new_rust(
11514 lsp::ServerCapabilities {
11515 completion_provider: Some(lsp::CompletionOptions {
11516 resolve_provider: Some(true),
11517 ..Default::default()
11518 }),
11519 ..Default::default()
11520 },
11521 cx,
11522 )
11523 .await;
11524
11525 let initial_state = "SubˇError";
11526 let buffer_marked_text = "<Sub|Error>";
11527 let completion_text = "SubscriptionError";
11528 let expected_with_insert_mode = "SubscriptionErrorˇError";
11529 let expected_with_replace_mode = "SubscriptionErrorˇ";
11530
11531 update_test_language_settings(&mut cx, |settings| {
11532 settings.defaults.completions = Some(CompletionSettings {
11533 words: WordsCompletionMode::Disabled,
11534 // set the opposite here to ensure that the action is overriding the default behavior
11535 lsp_insert_mode: LspInsertMode::Insert,
11536 lsp: true,
11537 lsp_fetch_timeout_ms: 0,
11538 });
11539 });
11540
11541 cx.set_state(initial_state);
11542 cx.update_editor(|editor, window, cx| {
11543 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11544 });
11545
11546 let counter = Arc::new(AtomicUsize::new(0));
11547 handle_completion_request_with_insert_and_replace(
11548 &mut cx,
11549 &buffer_marked_text,
11550 vec![(completion_text, completion_text)],
11551 counter.clone(),
11552 )
11553 .await;
11554 cx.condition(|editor, _| editor.context_menu_visible())
11555 .await;
11556 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11557
11558 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11559 editor
11560 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11561 .unwrap()
11562 });
11563 cx.assert_editor_state(&expected_with_replace_mode);
11564 handle_resolve_completion_request(&mut cx, None).await;
11565 apply_additional_edits.await.unwrap();
11566
11567 update_test_language_settings(&mut cx, |settings| {
11568 settings.defaults.completions = Some(CompletionSettings {
11569 words: WordsCompletionMode::Disabled,
11570 // set the opposite here to ensure that the action is overriding the default behavior
11571 lsp_insert_mode: LspInsertMode::Replace,
11572 lsp: true,
11573 lsp_fetch_timeout_ms: 0,
11574 });
11575 });
11576
11577 cx.set_state(initial_state);
11578 cx.update_editor(|editor, window, cx| {
11579 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11580 });
11581 handle_completion_request_with_insert_and_replace(
11582 &mut cx,
11583 &buffer_marked_text,
11584 vec![(completion_text, completion_text)],
11585 counter.clone(),
11586 )
11587 .await;
11588 cx.condition(|editor, _| editor.context_menu_visible())
11589 .await;
11590 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11591
11592 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11593 editor
11594 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11595 .unwrap()
11596 });
11597 cx.assert_editor_state(&expected_with_insert_mode);
11598 handle_resolve_completion_request(&mut cx, None).await;
11599 apply_additional_edits.await.unwrap();
11600}
11601
11602#[gpui::test]
11603async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11604 init_test(cx, |_| {});
11605 let mut cx = EditorLspTestContext::new_rust(
11606 lsp::ServerCapabilities {
11607 completion_provider: Some(lsp::CompletionOptions {
11608 resolve_provider: Some(true),
11609 ..Default::default()
11610 }),
11611 ..Default::default()
11612 },
11613 cx,
11614 )
11615 .await;
11616
11617 // scenario: surrounding text matches completion text
11618 let completion_text = "to_offset";
11619 let initial_state = indoc! {"
11620 1. buf.to_offˇsuffix
11621 2. buf.to_offˇsuf
11622 3. buf.to_offˇfix
11623 4. buf.to_offˇ
11624 5. into_offˇensive
11625 6. ˇsuffix
11626 7. let ˇ //
11627 8. aaˇzz
11628 9. buf.to_off«zzzzzˇ»suffix
11629 10. buf.«ˇzzzzz»suffix
11630 11. to_off«ˇzzzzz»
11631
11632 buf.to_offˇsuffix // newest cursor
11633 "};
11634 let completion_marked_buffer = indoc! {"
11635 1. buf.to_offsuffix
11636 2. buf.to_offsuf
11637 3. buf.to_offfix
11638 4. buf.to_off
11639 5. into_offensive
11640 6. suffix
11641 7. let //
11642 8. aazz
11643 9. buf.to_offzzzzzsuffix
11644 10. buf.zzzzzsuffix
11645 11. to_offzzzzz
11646
11647 buf.<to_off|suffix> // newest cursor
11648 "};
11649 let expected = indoc! {"
11650 1. buf.to_offsetˇ
11651 2. buf.to_offsetˇsuf
11652 3. buf.to_offsetˇfix
11653 4. buf.to_offsetˇ
11654 5. into_offsetˇensive
11655 6. to_offsetˇsuffix
11656 7. let to_offsetˇ //
11657 8. aato_offsetˇzz
11658 9. buf.to_offsetˇ
11659 10. buf.to_offsetˇsuffix
11660 11. to_offsetˇ
11661
11662 buf.to_offsetˇ // newest cursor
11663 "};
11664 cx.set_state(initial_state);
11665 cx.update_editor(|editor, window, cx| {
11666 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11667 });
11668 handle_completion_request_with_insert_and_replace(
11669 &mut cx,
11670 completion_marked_buffer,
11671 vec![(completion_text, completion_text)],
11672 Arc::new(AtomicUsize::new(0)),
11673 )
11674 .await;
11675 cx.condition(|editor, _| editor.context_menu_visible())
11676 .await;
11677 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11678 editor
11679 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11680 .unwrap()
11681 });
11682 cx.assert_editor_state(expected);
11683 handle_resolve_completion_request(&mut cx, None).await;
11684 apply_additional_edits.await.unwrap();
11685
11686 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11687 let completion_text = "foo_and_bar";
11688 let initial_state = indoc! {"
11689 1. ooanbˇ
11690 2. zooanbˇ
11691 3. ooanbˇz
11692 4. zooanbˇz
11693 5. ooanˇ
11694 6. oanbˇ
11695
11696 ooanbˇ
11697 "};
11698 let completion_marked_buffer = indoc! {"
11699 1. ooanb
11700 2. zooanb
11701 3. ooanbz
11702 4. zooanbz
11703 5. ooan
11704 6. oanb
11705
11706 <ooanb|>
11707 "};
11708 let expected = indoc! {"
11709 1. foo_and_barˇ
11710 2. zfoo_and_barˇ
11711 3. foo_and_barˇz
11712 4. zfoo_and_barˇz
11713 5. ooanfoo_and_barˇ
11714 6. oanbfoo_and_barˇ
11715
11716 foo_and_barˇ
11717 "};
11718 cx.set_state(initial_state);
11719 cx.update_editor(|editor, window, cx| {
11720 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11721 });
11722 handle_completion_request_with_insert_and_replace(
11723 &mut cx,
11724 completion_marked_buffer,
11725 vec![(completion_text, completion_text)],
11726 Arc::new(AtomicUsize::new(0)),
11727 )
11728 .await;
11729 cx.condition(|editor, _| editor.context_menu_visible())
11730 .await;
11731 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11732 editor
11733 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11734 .unwrap()
11735 });
11736 cx.assert_editor_state(expected);
11737 handle_resolve_completion_request(&mut cx, None).await;
11738 apply_additional_edits.await.unwrap();
11739
11740 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11741 // (expects the same as if it was inserted at the end)
11742 let completion_text = "foo_and_bar";
11743 let initial_state = indoc! {"
11744 1. ooˇanb
11745 2. zooˇanb
11746 3. ooˇanbz
11747 4. zooˇanbz
11748
11749 ooˇanb
11750 "};
11751 let completion_marked_buffer = indoc! {"
11752 1. ooanb
11753 2. zooanb
11754 3. ooanbz
11755 4. zooanbz
11756
11757 <oo|anb>
11758 "};
11759 let expected = indoc! {"
11760 1. foo_and_barˇ
11761 2. zfoo_and_barˇ
11762 3. foo_and_barˇz
11763 4. zfoo_and_barˇz
11764
11765 foo_and_barˇ
11766 "};
11767 cx.set_state(initial_state);
11768 cx.update_editor(|editor, window, cx| {
11769 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11770 });
11771 handle_completion_request_with_insert_and_replace(
11772 &mut cx,
11773 completion_marked_buffer,
11774 vec![(completion_text, completion_text)],
11775 Arc::new(AtomicUsize::new(0)),
11776 )
11777 .await;
11778 cx.condition(|editor, _| editor.context_menu_visible())
11779 .await;
11780 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11781 editor
11782 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11783 .unwrap()
11784 });
11785 cx.assert_editor_state(expected);
11786 handle_resolve_completion_request(&mut cx, None).await;
11787 apply_additional_edits.await.unwrap();
11788}
11789
11790// This used to crash
11791#[gpui::test]
11792async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11793 init_test(cx, |_| {});
11794
11795 let buffer_text = indoc! {"
11796 fn main() {
11797 10.satu;
11798
11799 //
11800 // separate cursors so they open in different excerpts (manually reproducible)
11801 //
11802
11803 10.satu20;
11804 }
11805 "};
11806 let multibuffer_text_with_selections = indoc! {"
11807 fn main() {
11808 10.satuˇ;
11809
11810 //
11811
11812 //
11813
11814 10.satuˇ20;
11815 }
11816 "};
11817 let expected_multibuffer = indoc! {"
11818 fn main() {
11819 10.saturating_sub()ˇ;
11820
11821 //
11822
11823 //
11824
11825 10.saturating_sub()ˇ;
11826 }
11827 "};
11828
11829 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11830 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11831
11832 let fs = FakeFs::new(cx.executor());
11833 fs.insert_tree(
11834 path!("/a"),
11835 json!({
11836 "main.rs": buffer_text,
11837 }),
11838 )
11839 .await;
11840
11841 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11842 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11843 language_registry.add(rust_lang());
11844 let mut fake_servers = language_registry.register_fake_lsp(
11845 "Rust",
11846 FakeLspAdapter {
11847 capabilities: lsp::ServerCapabilities {
11848 completion_provider: Some(lsp::CompletionOptions {
11849 resolve_provider: None,
11850 ..lsp::CompletionOptions::default()
11851 }),
11852 ..lsp::ServerCapabilities::default()
11853 },
11854 ..FakeLspAdapter::default()
11855 },
11856 );
11857 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11858 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11859 let buffer = project
11860 .update(cx, |project, cx| {
11861 project.open_local_buffer(path!("/a/main.rs"), cx)
11862 })
11863 .await
11864 .unwrap();
11865
11866 let multi_buffer = cx.new(|cx| {
11867 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11868 multi_buffer.push_excerpts(
11869 buffer.clone(),
11870 [ExcerptRange::new(0..first_excerpt_end)],
11871 cx,
11872 );
11873 multi_buffer.push_excerpts(
11874 buffer.clone(),
11875 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11876 cx,
11877 );
11878 multi_buffer
11879 });
11880
11881 let editor = workspace
11882 .update(cx, |_, window, cx| {
11883 cx.new(|cx| {
11884 Editor::new(
11885 EditorMode::Full {
11886 scale_ui_elements_with_buffer_font_size: false,
11887 show_active_line_background: false,
11888 sized_by_content: false,
11889 },
11890 multi_buffer.clone(),
11891 Some(project.clone()),
11892 window,
11893 cx,
11894 )
11895 })
11896 })
11897 .unwrap();
11898
11899 let pane = workspace
11900 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11901 .unwrap();
11902 pane.update_in(cx, |pane, window, cx| {
11903 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11904 });
11905
11906 let fake_server = fake_servers.next().await.unwrap();
11907
11908 editor.update_in(cx, |editor, window, cx| {
11909 editor.change_selections(None, window, cx, |s| {
11910 s.select_ranges([
11911 Point::new(1, 11)..Point::new(1, 11),
11912 Point::new(7, 11)..Point::new(7, 11),
11913 ])
11914 });
11915
11916 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11917 });
11918
11919 editor.update_in(cx, |editor, window, cx| {
11920 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11921 });
11922
11923 fake_server
11924 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11925 let completion_item = lsp::CompletionItem {
11926 label: "saturating_sub()".into(),
11927 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11928 lsp::InsertReplaceEdit {
11929 new_text: "saturating_sub()".to_owned(),
11930 insert: lsp::Range::new(
11931 lsp::Position::new(7, 7),
11932 lsp::Position::new(7, 11),
11933 ),
11934 replace: lsp::Range::new(
11935 lsp::Position::new(7, 7),
11936 lsp::Position::new(7, 13),
11937 ),
11938 },
11939 )),
11940 ..lsp::CompletionItem::default()
11941 };
11942
11943 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11944 })
11945 .next()
11946 .await
11947 .unwrap();
11948
11949 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11950 .await;
11951
11952 editor
11953 .update_in(cx, |editor, window, cx| {
11954 editor
11955 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11956 .unwrap()
11957 })
11958 .await
11959 .unwrap();
11960
11961 editor.update(cx, |editor, cx| {
11962 assert_text_with_selections(editor, expected_multibuffer, cx);
11963 })
11964}
11965
11966#[gpui::test]
11967async fn test_completion(cx: &mut TestAppContext) {
11968 init_test(cx, |_| {});
11969
11970 let mut cx = EditorLspTestContext::new_rust(
11971 lsp::ServerCapabilities {
11972 completion_provider: Some(lsp::CompletionOptions {
11973 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11974 resolve_provider: Some(true),
11975 ..Default::default()
11976 }),
11977 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11978 ..Default::default()
11979 },
11980 cx,
11981 )
11982 .await;
11983 let counter = Arc::new(AtomicUsize::new(0));
11984
11985 cx.set_state(indoc! {"
11986 oneˇ
11987 two
11988 three
11989 "});
11990 cx.simulate_keystroke(".");
11991 handle_completion_request(
11992 indoc! {"
11993 one.|<>
11994 two
11995 three
11996 "},
11997 vec!["first_completion", "second_completion"],
11998 true,
11999 counter.clone(),
12000 &mut cx,
12001 )
12002 .await;
12003 cx.condition(|editor, _| editor.context_menu_visible())
12004 .await;
12005 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12006
12007 let _handler = handle_signature_help_request(
12008 &mut cx,
12009 lsp::SignatureHelp {
12010 signatures: vec![lsp::SignatureInformation {
12011 label: "test signature".to_string(),
12012 documentation: None,
12013 parameters: Some(vec![lsp::ParameterInformation {
12014 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12015 documentation: None,
12016 }]),
12017 active_parameter: None,
12018 }],
12019 active_signature: None,
12020 active_parameter: None,
12021 },
12022 );
12023 cx.update_editor(|editor, window, cx| {
12024 assert!(
12025 !editor.signature_help_state.is_shown(),
12026 "No signature help was called for"
12027 );
12028 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12029 });
12030 cx.run_until_parked();
12031 cx.update_editor(|editor, _, _| {
12032 assert!(
12033 !editor.signature_help_state.is_shown(),
12034 "No signature help should be shown when completions menu is open"
12035 );
12036 });
12037
12038 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12039 editor.context_menu_next(&Default::default(), window, cx);
12040 editor
12041 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12042 .unwrap()
12043 });
12044 cx.assert_editor_state(indoc! {"
12045 one.second_completionˇ
12046 two
12047 three
12048 "});
12049
12050 handle_resolve_completion_request(
12051 &mut cx,
12052 Some(vec![
12053 (
12054 //This overlaps with the primary completion edit which is
12055 //misbehavior from the LSP spec, test that we filter it out
12056 indoc! {"
12057 one.second_ˇcompletion
12058 two
12059 threeˇ
12060 "},
12061 "overlapping additional edit",
12062 ),
12063 (
12064 indoc! {"
12065 one.second_completion
12066 two
12067 threeˇ
12068 "},
12069 "\nadditional edit",
12070 ),
12071 ]),
12072 )
12073 .await;
12074 apply_additional_edits.await.unwrap();
12075 cx.assert_editor_state(indoc! {"
12076 one.second_completionˇ
12077 two
12078 three
12079 additional edit
12080 "});
12081
12082 cx.set_state(indoc! {"
12083 one.second_completion
12084 twoˇ
12085 threeˇ
12086 additional edit
12087 "});
12088 cx.simulate_keystroke(" ");
12089 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12090 cx.simulate_keystroke("s");
12091 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12092
12093 cx.assert_editor_state(indoc! {"
12094 one.second_completion
12095 two sˇ
12096 three sˇ
12097 additional edit
12098 "});
12099 handle_completion_request(
12100 indoc! {"
12101 one.second_completion
12102 two s
12103 three <s|>
12104 additional edit
12105 "},
12106 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12107 true,
12108 counter.clone(),
12109 &mut cx,
12110 )
12111 .await;
12112 cx.condition(|editor, _| editor.context_menu_visible())
12113 .await;
12114 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12115
12116 cx.simulate_keystroke("i");
12117
12118 handle_completion_request(
12119 indoc! {"
12120 one.second_completion
12121 two si
12122 three <si|>
12123 additional edit
12124 "},
12125 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12126 true,
12127 counter.clone(),
12128 &mut cx,
12129 )
12130 .await;
12131 cx.condition(|editor, _| editor.context_menu_visible())
12132 .await;
12133 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12134
12135 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12136 editor
12137 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12138 .unwrap()
12139 });
12140 cx.assert_editor_state(indoc! {"
12141 one.second_completion
12142 two sixth_completionˇ
12143 three sixth_completionˇ
12144 additional edit
12145 "});
12146
12147 apply_additional_edits.await.unwrap();
12148
12149 update_test_language_settings(&mut cx, |settings| {
12150 settings.defaults.show_completions_on_input = Some(false);
12151 });
12152 cx.set_state("editorˇ");
12153 cx.simulate_keystroke(".");
12154 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12155 cx.simulate_keystrokes("c l o");
12156 cx.assert_editor_state("editor.cloˇ");
12157 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12158 cx.update_editor(|editor, window, cx| {
12159 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12160 });
12161 handle_completion_request(
12162 "editor.<clo|>",
12163 vec!["close", "clobber"],
12164 true,
12165 counter.clone(),
12166 &mut cx,
12167 )
12168 .await;
12169 cx.condition(|editor, _| editor.context_menu_visible())
12170 .await;
12171 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12172
12173 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12174 editor
12175 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12176 .unwrap()
12177 });
12178 cx.assert_editor_state("editor.clobberˇ");
12179 handle_resolve_completion_request(&mut cx, None).await;
12180 apply_additional_edits.await.unwrap();
12181}
12182
12183#[gpui::test]
12184async fn test_completion_reuse(cx: &mut TestAppContext) {
12185 init_test(cx, |_| {});
12186
12187 let mut cx = EditorLspTestContext::new_rust(
12188 lsp::ServerCapabilities {
12189 completion_provider: Some(lsp::CompletionOptions {
12190 trigger_characters: Some(vec![".".to_string()]),
12191 ..Default::default()
12192 }),
12193 ..Default::default()
12194 },
12195 cx,
12196 )
12197 .await;
12198
12199 let counter = Arc::new(AtomicUsize::new(0));
12200 cx.set_state("objˇ");
12201 cx.simulate_keystroke(".");
12202
12203 // Initial completion request returns complete results
12204 let is_incomplete = false;
12205 handle_completion_request(
12206 "obj.|<>",
12207 vec!["a", "ab", "abc"],
12208 is_incomplete,
12209 counter.clone(),
12210 &mut cx,
12211 )
12212 .await;
12213 cx.run_until_parked();
12214 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12215 cx.assert_editor_state("obj.ˇ");
12216 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12217
12218 // Type "a" - filters existing completions
12219 cx.simulate_keystroke("a");
12220 cx.run_until_parked();
12221 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12222 cx.assert_editor_state("obj.aˇ");
12223 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12224
12225 // Type "b" - filters existing completions
12226 cx.simulate_keystroke("b");
12227 cx.run_until_parked();
12228 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12229 cx.assert_editor_state("obj.abˇ");
12230 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12231
12232 // Type "c" - filters existing completions
12233 cx.simulate_keystroke("c");
12234 cx.run_until_parked();
12235 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12236 cx.assert_editor_state("obj.abcˇ");
12237 check_displayed_completions(vec!["abc"], &mut cx);
12238
12239 // Backspace to delete "c" - filters existing completions
12240 cx.update_editor(|editor, window, cx| {
12241 editor.backspace(&Backspace, window, cx);
12242 });
12243 cx.run_until_parked();
12244 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12245 cx.assert_editor_state("obj.abˇ");
12246 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12247
12248 // Moving cursor to the left dismisses menu.
12249 cx.update_editor(|editor, window, cx| {
12250 editor.move_left(&MoveLeft, window, cx);
12251 });
12252 cx.run_until_parked();
12253 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12254 cx.assert_editor_state("obj.aˇb");
12255 cx.update_editor(|editor, _, _| {
12256 assert_eq!(editor.context_menu_visible(), false);
12257 });
12258
12259 // Type "b" - new request
12260 cx.simulate_keystroke("b");
12261 let is_incomplete = false;
12262 handle_completion_request(
12263 "obj.<ab|>a",
12264 vec!["ab", "abc"],
12265 is_incomplete,
12266 counter.clone(),
12267 &mut cx,
12268 )
12269 .await;
12270 cx.run_until_parked();
12271 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12272 cx.assert_editor_state("obj.abˇb");
12273 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12274
12275 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12276 cx.update_editor(|editor, window, cx| {
12277 editor.backspace(&Backspace, window, cx);
12278 });
12279 let is_incomplete = false;
12280 handle_completion_request(
12281 "obj.<a|>b",
12282 vec!["a", "ab", "abc"],
12283 is_incomplete,
12284 counter.clone(),
12285 &mut cx,
12286 )
12287 .await;
12288 cx.run_until_parked();
12289 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12290 cx.assert_editor_state("obj.aˇb");
12291 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12292
12293 // Backspace to delete "a" - dismisses menu.
12294 cx.update_editor(|editor, window, cx| {
12295 editor.backspace(&Backspace, window, cx);
12296 });
12297 cx.run_until_parked();
12298 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12299 cx.assert_editor_state("obj.ˇb");
12300 cx.update_editor(|editor, _, _| {
12301 assert_eq!(editor.context_menu_visible(), false);
12302 });
12303}
12304
12305#[gpui::test]
12306async fn test_word_completion(cx: &mut TestAppContext) {
12307 let lsp_fetch_timeout_ms = 10;
12308 init_test(cx, |language_settings| {
12309 language_settings.defaults.completions = Some(CompletionSettings {
12310 words: WordsCompletionMode::Fallback,
12311 lsp: true,
12312 lsp_fetch_timeout_ms: 10,
12313 lsp_insert_mode: LspInsertMode::Insert,
12314 });
12315 });
12316
12317 let mut cx = EditorLspTestContext::new_rust(
12318 lsp::ServerCapabilities {
12319 completion_provider: Some(lsp::CompletionOptions {
12320 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12321 ..lsp::CompletionOptions::default()
12322 }),
12323 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12324 ..lsp::ServerCapabilities::default()
12325 },
12326 cx,
12327 )
12328 .await;
12329
12330 let throttle_completions = Arc::new(AtomicBool::new(false));
12331
12332 let lsp_throttle_completions = throttle_completions.clone();
12333 let _completion_requests_handler =
12334 cx.lsp
12335 .server
12336 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12337 let lsp_throttle_completions = lsp_throttle_completions.clone();
12338 let cx = cx.clone();
12339 async move {
12340 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12341 cx.background_executor()
12342 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12343 .await;
12344 }
12345 Ok(Some(lsp::CompletionResponse::Array(vec![
12346 lsp::CompletionItem {
12347 label: "first".into(),
12348 ..lsp::CompletionItem::default()
12349 },
12350 lsp::CompletionItem {
12351 label: "last".into(),
12352 ..lsp::CompletionItem::default()
12353 },
12354 ])))
12355 }
12356 });
12357
12358 cx.set_state(indoc! {"
12359 oneˇ
12360 two
12361 three
12362 "});
12363 cx.simulate_keystroke(".");
12364 cx.executor().run_until_parked();
12365 cx.condition(|editor, _| editor.context_menu_visible())
12366 .await;
12367 cx.update_editor(|editor, window, cx| {
12368 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12369 {
12370 assert_eq!(
12371 completion_menu_entries(&menu),
12372 &["first", "last"],
12373 "When LSP server is fast to reply, no fallback word completions are used"
12374 );
12375 } else {
12376 panic!("expected completion menu to be open");
12377 }
12378 editor.cancel(&Cancel, window, cx);
12379 });
12380 cx.executor().run_until_parked();
12381 cx.condition(|editor, _| !editor.context_menu_visible())
12382 .await;
12383
12384 throttle_completions.store(true, atomic::Ordering::Release);
12385 cx.simulate_keystroke(".");
12386 cx.executor()
12387 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12388 cx.executor().run_until_parked();
12389 cx.condition(|editor, _| editor.context_menu_visible())
12390 .await;
12391 cx.update_editor(|editor, _, _| {
12392 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12393 {
12394 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12395 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12396 } else {
12397 panic!("expected completion menu to be open");
12398 }
12399 });
12400}
12401
12402#[gpui::test]
12403async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12404 init_test(cx, |language_settings| {
12405 language_settings.defaults.completions = Some(CompletionSettings {
12406 words: WordsCompletionMode::Enabled,
12407 lsp: true,
12408 lsp_fetch_timeout_ms: 0,
12409 lsp_insert_mode: LspInsertMode::Insert,
12410 });
12411 });
12412
12413 let mut cx = EditorLspTestContext::new_rust(
12414 lsp::ServerCapabilities {
12415 completion_provider: Some(lsp::CompletionOptions {
12416 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12417 ..lsp::CompletionOptions::default()
12418 }),
12419 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12420 ..lsp::ServerCapabilities::default()
12421 },
12422 cx,
12423 )
12424 .await;
12425
12426 let _completion_requests_handler =
12427 cx.lsp
12428 .server
12429 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12430 Ok(Some(lsp::CompletionResponse::Array(vec![
12431 lsp::CompletionItem {
12432 label: "first".into(),
12433 ..lsp::CompletionItem::default()
12434 },
12435 lsp::CompletionItem {
12436 label: "last".into(),
12437 ..lsp::CompletionItem::default()
12438 },
12439 ])))
12440 });
12441
12442 cx.set_state(indoc! {"ˇ
12443 first
12444 last
12445 second
12446 "});
12447 cx.simulate_keystroke(".");
12448 cx.executor().run_until_parked();
12449 cx.condition(|editor, _| editor.context_menu_visible())
12450 .await;
12451 cx.update_editor(|editor, _, _| {
12452 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12453 {
12454 assert_eq!(
12455 completion_menu_entries(&menu),
12456 &["first", "last", "second"],
12457 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12458 );
12459 } else {
12460 panic!("expected completion menu to be open");
12461 }
12462 });
12463}
12464
12465#[gpui::test]
12466async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12467 init_test(cx, |language_settings| {
12468 language_settings.defaults.completions = Some(CompletionSettings {
12469 words: WordsCompletionMode::Disabled,
12470 lsp: true,
12471 lsp_fetch_timeout_ms: 0,
12472 lsp_insert_mode: LspInsertMode::Insert,
12473 });
12474 });
12475
12476 let mut cx = EditorLspTestContext::new_rust(
12477 lsp::ServerCapabilities {
12478 completion_provider: Some(lsp::CompletionOptions {
12479 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12480 ..lsp::CompletionOptions::default()
12481 }),
12482 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12483 ..lsp::ServerCapabilities::default()
12484 },
12485 cx,
12486 )
12487 .await;
12488
12489 let _completion_requests_handler =
12490 cx.lsp
12491 .server
12492 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12493 panic!("LSP completions should not be queried when dealing with word completions")
12494 });
12495
12496 cx.set_state(indoc! {"ˇ
12497 first
12498 last
12499 second
12500 "});
12501 cx.update_editor(|editor, window, cx| {
12502 editor.show_word_completions(&ShowWordCompletions, window, cx);
12503 });
12504 cx.executor().run_until_parked();
12505 cx.condition(|editor, _| editor.context_menu_visible())
12506 .await;
12507 cx.update_editor(|editor, _, _| {
12508 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12509 {
12510 assert_eq!(
12511 completion_menu_entries(&menu),
12512 &["first", "last", "second"],
12513 "`ShowWordCompletions` action should show word completions"
12514 );
12515 } else {
12516 panic!("expected completion menu to be open");
12517 }
12518 });
12519
12520 cx.simulate_keystroke("l");
12521 cx.executor().run_until_parked();
12522 cx.condition(|editor, _| editor.context_menu_visible())
12523 .await;
12524 cx.update_editor(|editor, _, _| {
12525 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12526 {
12527 assert_eq!(
12528 completion_menu_entries(&menu),
12529 &["last"],
12530 "After showing word completions, further editing should filter them and not query the LSP"
12531 );
12532 } else {
12533 panic!("expected completion menu to be open");
12534 }
12535 });
12536}
12537
12538#[gpui::test]
12539async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12540 init_test(cx, |language_settings| {
12541 language_settings.defaults.completions = Some(CompletionSettings {
12542 words: WordsCompletionMode::Fallback,
12543 lsp: false,
12544 lsp_fetch_timeout_ms: 0,
12545 lsp_insert_mode: LspInsertMode::Insert,
12546 });
12547 });
12548
12549 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12550
12551 cx.set_state(indoc! {"ˇ
12552 0_usize
12553 let
12554 33
12555 4.5f32
12556 "});
12557 cx.update_editor(|editor, window, cx| {
12558 editor.show_completions(&ShowCompletions::default(), window, cx);
12559 });
12560 cx.executor().run_until_parked();
12561 cx.condition(|editor, _| editor.context_menu_visible())
12562 .await;
12563 cx.update_editor(|editor, window, cx| {
12564 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12565 {
12566 assert_eq!(
12567 completion_menu_entries(&menu),
12568 &["let"],
12569 "With no digits in the completion query, no digits should be in the word completions"
12570 );
12571 } else {
12572 panic!("expected completion menu to be open");
12573 }
12574 editor.cancel(&Cancel, window, cx);
12575 });
12576
12577 cx.set_state(indoc! {"3ˇ
12578 0_usize
12579 let
12580 3
12581 33.35f32
12582 "});
12583 cx.update_editor(|editor, window, cx| {
12584 editor.show_completions(&ShowCompletions::default(), window, cx);
12585 });
12586 cx.executor().run_until_parked();
12587 cx.condition(|editor, _| editor.context_menu_visible())
12588 .await;
12589 cx.update_editor(|editor, _, _| {
12590 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12591 {
12592 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12593 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12594 } else {
12595 panic!("expected completion menu to be open");
12596 }
12597 });
12598}
12599
12600fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12601 let position = || lsp::Position {
12602 line: params.text_document_position.position.line,
12603 character: params.text_document_position.position.character,
12604 };
12605 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12606 range: lsp::Range {
12607 start: position(),
12608 end: position(),
12609 },
12610 new_text: text.to_string(),
12611 }))
12612}
12613
12614#[gpui::test]
12615async fn test_multiline_completion(cx: &mut TestAppContext) {
12616 init_test(cx, |_| {});
12617
12618 let fs = FakeFs::new(cx.executor());
12619 fs.insert_tree(
12620 path!("/a"),
12621 json!({
12622 "main.ts": "a",
12623 }),
12624 )
12625 .await;
12626
12627 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12628 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12629 let typescript_language = Arc::new(Language::new(
12630 LanguageConfig {
12631 name: "TypeScript".into(),
12632 matcher: LanguageMatcher {
12633 path_suffixes: vec!["ts".to_string()],
12634 ..LanguageMatcher::default()
12635 },
12636 line_comments: vec!["// ".into()],
12637 ..LanguageConfig::default()
12638 },
12639 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12640 ));
12641 language_registry.add(typescript_language.clone());
12642 let mut fake_servers = language_registry.register_fake_lsp(
12643 "TypeScript",
12644 FakeLspAdapter {
12645 capabilities: lsp::ServerCapabilities {
12646 completion_provider: Some(lsp::CompletionOptions {
12647 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12648 ..lsp::CompletionOptions::default()
12649 }),
12650 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12651 ..lsp::ServerCapabilities::default()
12652 },
12653 // Emulate vtsls label generation
12654 label_for_completion: Some(Box::new(|item, _| {
12655 let text = if let Some(description) = item
12656 .label_details
12657 .as_ref()
12658 .and_then(|label_details| label_details.description.as_ref())
12659 {
12660 format!("{} {}", item.label, description)
12661 } else if let Some(detail) = &item.detail {
12662 format!("{} {}", item.label, detail)
12663 } else {
12664 item.label.clone()
12665 };
12666 let len = text.len();
12667 Some(language::CodeLabel {
12668 text,
12669 runs: Vec::new(),
12670 filter_range: 0..len,
12671 })
12672 })),
12673 ..FakeLspAdapter::default()
12674 },
12675 );
12676 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12677 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12678 let worktree_id = workspace
12679 .update(cx, |workspace, _window, cx| {
12680 workspace.project().update(cx, |project, cx| {
12681 project.worktrees(cx).next().unwrap().read(cx).id()
12682 })
12683 })
12684 .unwrap();
12685 let _buffer = project
12686 .update(cx, |project, cx| {
12687 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12688 })
12689 .await
12690 .unwrap();
12691 let editor = workspace
12692 .update(cx, |workspace, window, cx| {
12693 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12694 })
12695 .unwrap()
12696 .await
12697 .unwrap()
12698 .downcast::<Editor>()
12699 .unwrap();
12700 let fake_server = fake_servers.next().await.unwrap();
12701
12702 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12703 let multiline_label_2 = "a\nb\nc\n";
12704 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12705 let multiline_description = "d\ne\nf\n";
12706 let multiline_detail_2 = "g\nh\ni\n";
12707
12708 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12709 move |params, _| async move {
12710 Ok(Some(lsp::CompletionResponse::Array(vec![
12711 lsp::CompletionItem {
12712 label: multiline_label.to_string(),
12713 text_edit: gen_text_edit(¶ms, "new_text_1"),
12714 ..lsp::CompletionItem::default()
12715 },
12716 lsp::CompletionItem {
12717 label: "single line label 1".to_string(),
12718 detail: Some(multiline_detail.to_string()),
12719 text_edit: gen_text_edit(¶ms, "new_text_2"),
12720 ..lsp::CompletionItem::default()
12721 },
12722 lsp::CompletionItem {
12723 label: "single line label 2".to_string(),
12724 label_details: Some(lsp::CompletionItemLabelDetails {
12725 description: Some(multiline_description.to_string()),
12726 detail: None,
12727 }),
12728 text_edit: gen_text_edit(¶ms, "new_text_2"),
12729 ..lsp::CompletionItem::default()
12730 },
12731 lsp::CompletionItem {
12732 label: multiline_label_2.to_string(),
12733 detail: Some(multiline_detail_2.to_string()),
12734 text_edit: gen_text_edit(¶ms, "new_text_3"),
12735 ..lsp::CompletionItem::default()
12736 },
12737 lsp::CompletionItem {
12738 label: "Label with many spaces and \t but without newlines".to_string(),
12739 detail: Some(
12740 "Details with many spaces and \t but without newlines".to_string(),
12741 ),
12742 text_edit: gen_text_edit(¶ms, "new_text_4"),
12743 ..lsp::CompletionItem::default()
12744 },
12745 ])))
12746 },
12747 );
12748
12749 editor.update_in(cx, |editor, window, cx| {
12750 cx.focus_self(window);
12751 editor.move_to_end(&MoveToEnd, window, cx);
12752 editor.handle_input(".", window, cx);
12753 });
12754 cx.run_until_parked();
12755 completion_handle.next().await.unwrap();
12756
12757 editor.update(cx, |editor, _| {
12758 assert!(editor.context_menu_visible());
12759 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12760 {
12761 let completion_labels = menu
12762 .completions
12763 .borrow()
12764 .iter()
12765 .map(|c| c.label.text.clone())
12766 .collect::<Vec<_>>();
12767 assert_eq!(
12768 completion_labels,
12769 &[
12770 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12771 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12772 "single line label 2 d e f ",
12773 "a b c g h i ",
12774 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12775 ],
12776 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12777 );
12778
12779 for completion in menu
12780 .completions
12781 .borrow()
12782 .iter() {
12783 assert_eq!(
12784 completion.label.filter_range,
12785 0..completion.label.text.len(),
12786 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12787 );
12788 }
12789 } else {
12790 panic!("expected completion menu to be open");
12791 }
12792 });
12793}
12794
12795#[gpui::test]
12796async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12797 init_test(cx, |_| {});
12798 let mut cx = EditorLspTestContext::new_rust(
12799 lsp::ServerCapabilities {
12800 completion_provider: Some(lsp::CompletionOptions {
12801 trigger_characters: Some(vec![".".to_string()]),
12802 ..Default::default()
12803 }),
12804 ..Default::default()
12805 },
12806 cx,
12807 )
12808 .await;
12809 cx.lsp
12810 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12811 Ok(Some(lsp::CompletionResponse::Array(vec![
12812 lsp::CompletionItem {
12813 label: "first".into(),
12814 ..Default::default()
12815 },
12816 lsp::CompletionItem {
12817 label: "last".into(),
12818 ..Default::default()
12819 },
12820 ])))
12821 });
12822 cx.set_state("variableˇ");
12823 cx.simulate_keystroke(".");
12824 cx.executor().run_until_parked();
12825
12826 cx.update_editor(|editor, _, _| {
12827 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12828 {
12829 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12830 } else {
12831 panic!("expected completion menu to be open");
12832 }
12833 });
12834
12835 cx.update_editor(|editor, window, cx| {
12836 editor.move_page_down(&MovePageDown::default(), window, cx);
12837 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12838 {
12839 assert!(
12840 menu.selected_item == 1,
12841 "expected PageDown to select the last item from the context menu"
12842 );
12843 } else {
12844 panic!("expected completion menu to stay open after PageDown");
12845 }
12846 });
12847
12848 cx.update_editor(|editor, window, cx| {
12849 editor.move_page_up(&MovePageUp::default(), window, cx);
12850 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12851 {
12852 assert!(
12853 menu.selected_item == 0,
12854 "expected PageUp to select the first item from the context menu"
12855 );
12856 } else {
12857 panic!("expected completion menu to stay open after PageUp");
12858 }
12859 });
12860}
12861
12862#[gpui::test]
12863async fn test_as_is_completions(cx: &mut TestAppContext) {
12864 init_test(cx, |_| {});
12865 let mut cx = EditorLspTestContext::new_rust(
12866 lsp::ServerCapabilities {
12867 completion_provider: Some(lsp::CompletionOptions {
12868 ..Default::default()
12869 }),
12870 ..Default::default()
12871 },
12872 cx,
12873 )
12874 .await;
12875 cx.lsp
12876 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12877 Ok(Some(lsp::CompletionResponse::Array(vec![
12878 lsp::CompletionItem {
12879 label: "unsafe".into(),
12880 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12881 range: lsp::Range {
12882 start: lsp::Position {
12883 line: 1,
12884 character: 2,
12885 },
12886 end: lsp::Position {
12887 line: 1,
12888 character: 3,
12889 },
12890 },
12891 new_text: "unsafe".to_string(),
12892 })),
12893 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12894 ..Default::default()
12895 },
12896 ])))
12897 });
12898 cx.set_state("fn a() {}\n nˇ");
12899 cx.executor().run_until_parked();
12900 cx.update_editor(|editor, window, cx| {
12901 editor.show_completions(
12902 &ShowCompletions {
12903 trigger: Some("\n".into()),
12904 },
12905 window,
12906 cx,
12907 );
12908 });
12909 cx.executor().run_until_parked();
12910
12911 cx.update_editor(|editor, window, cx| {
12912 editor.confirm_completion(&Default::default(), window, cx)
12913 });
12914 cx.executor().run_until_parked();
12915 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12916}
12917
12918#[gpui::test]
12919async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12920 init_test(cx, |_| {});
12921
12922 let mut cx = EditorLspTestContext::new_rust(
12923 lsp::ServerCapabilities {
12924 completion_provider: Some(lsp::CompletionOptions {
12925 trigger_characters: Some(vec![".".to_string()]),
12926 resolve_provider: Some(true),
12927 ..Default::default()
12928 }),
12929 ..Default::default()
12930 },
12931 cx,
12932 )
12933 .await;
12934
12935 cx.set_state("fn main() { let a = 2ˇ; }");
12936 cx.simulate_keystroke(".");
12937 let completion_item = lsp::CompletionItem {
12938 label: "Some".into(),
12939 kind: Some(lsp::CompletionItemKind::SNIPPET),
12940 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12941 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12942 kind: lsp::MarkupKind::Markdown,
12943 value: "```rust\nSome(2)\n```".to_string(),
12944 })),
12945 deprecated: Some(false),
12946 sort_text: Some("Some".to_string()),
12947 filter_text: Some("Some".to_string()),
12948 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12949 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12950 range: lsp::Range {
12951 start: lsp::Position {
12952 line: 0,
12953 character: 22,
12954 },
12955 end: lsp::Position {
12956 line: 0,
12957 character: 22,
12958 },
12959 },
12960 new_text: "Some(2)".to_string(),
12961 })),
12962 additional_text_edits: Some(vec![lsp::TextEdit {
12963 range: lsp::Range {
12964 start: lsp::Position {
12965 line: 0,
12966 character: 20,
12967 },
12968 end: lsp::Position {
12969 line: 0,
12970 character: 22,
12971 },
12972 },
12973 new_text: "".to_string(),
12974 }]),
12975 ..Default::default()
12976 };
12977
12978 let closure_completion_item = completion_item.clone();
12979 let counter = Arc::new(AtomicUsize::new(0));
12980 let counter_clone = counter.clone();
12981 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12982 let task_completion_item = closure_completion_item.clone();
12983 counter_clone.fetch_add(1, atomic::Ordering::Release);
12984 async move {
12985 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
12986 is_incomplete: true,
12987 item_defaults: None,
12988 items: vec![task_completion_item],
12989 })))
12990 }
12991 });
12992
12993 cx.condition(|editor, _| editor.context_menu_visible())
12994 .await;
12995 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12996 assert!(request.next().await.is_some());
12997 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12998
12999 cx.simulate_keystrokes("S o m");
13000 cx.condition(|editor, _| editor.context_menu_visible())
13001 .await;
13002 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13003 assert!(request.next().await.is_some());
13004 assert!(request.next().await.is_some());
13005 assert!(request.next().await.is_some());
13006 request.close();
13007 assert!(request.next().await.is_none());
13008 assert_eq!(
13009 counter.load(atomic::Ordering::Acquire),
13010 4,
13011 "With the completions menu open, only one LSP request should happen per input"
13012 );
13013}
13014
13015#[gpui::test]
13016async fn test_toggle_comment(cx: &mut TestAppContext) {
13017 init_test(cx, |_| {});
13018 let mut cx = EditorTestContext::new(cx).await;
13019 let language = Arc::new(Language::new(
13020 LanguageConfig {
13021 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13022 ..Default::default()
13023 },
13024 Some(tree_sitter_rust::LANGUAGE.into()),
13025 ));
13026 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13027
13028 // If multiple selections intersect a line, the line is only toggled once.
13029 cx.set_state(indoc! {"
13030 fn a() {
13031 «//b();
13032 ˇ»// «c();
13033 //ˇ» d();
13034 }
13035 "});
13036
13037 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13038
13039 cx.assert_editor_state(indoc! {"
13040 fn a() {
13041 «b();
13042 c();
13043 ˇ» d();
13044 }
13045 "});
13046
13047 // The comment prefix is inserted at the same column for every line in a
13048 // selection.
13049 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13050
13051 cx.assert_editor_state(indoc! {"
13052 fn a() {
13053 // «b();
13054 // c();
13055 ˇ»// d();
13056 }
13057 "});
13058
13059 // If a selection ends at the beginning of a line, that line is not toggled.
13060 cx.set_selections_state(indoc! {"
13061 fn a() {
13062 // b();
13063 «// c();
13064 ˇ» // d();
13065 }
13066 "});
13067
13068 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13069
13070 cx.assert_editor_state(indoc! {"
13071 fn a() {
13072 // b();
13073 «c();
13074 ˇ» // d();
13075 }
13076 "});
13077
13078 // If a selection span a single line and is empty, the line is toggled.
13079 cx.set_state(indoc! {"
13080 fn a() {
13081 a();
13082 b();
13083 ˇ
13084 }
13085 "});
13086
13087 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13088
13089 cx.assert_editor_state(indoc! {"
13090 fn a() {
13091 a();
13092 b();
13093 //•ˇ
13094 }
13095 "});
13096
13097 // If a selection span multiple lines, empty lines are not toggled.
13098 cx.set_state(indoc! {"
13099 fn a() {
13100 «a();
13101
13102 c();ˇ»
13103 }
13104 "});
13105
13106 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13107
13108 cx.assert_editor_state(indoc! {"
13109 fn a() {
13110 // «a();
13111
13112 // c();ˇ»
13113 }
13114 "});
13115
13116 // If a selection includes multiple comment prefixes, all lines are uncommented.
13117 cx.set_state(indoc! {"
13118 fn a() {
13119 «// a();
13120 /// b();
13121 //! c();ˇ»
13122 }
13123 "});
13124
13125 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13126
13127 cx.assert_editor_state(indoc! {"
13128 fn a() {
13129 «a();
13130 b();
13131 c();ˇ»
13132 }
13133 "});
13134}
13135
13136#[gpui::test]
13137async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13138 init_test(cx, |_| {});
13139 let mut cx = EditorTestContext::new(cx).await;
13140 let language = Arc::new(Language::new(
13141 LanguageConfig {
13142 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13143 ..Default::default()
13144 },
13145 Some(tree_sitter_rust::LANGUAGE.into()),
13146 ));
13147 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13148
13149 let toggle_comments = &ToggleComments {
13150 advance_downwards: false,
13151 ignore_indent: true,
13152 };
13153
13154 // If multiple selections intersect a line, the line is only toggled once.
13155 cx.set_state(indoc! {"
13156 fn a() {
13157 // «b();
13158 // c();
13159 // ˇ» d();
13160 }
13161 "});
13162
13163 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13164
13165 cx.assert_editor_state(indoc! {"
13166 fn a() {
13167 «b();
13168 c();
13169 ˇ» d();
13170 }
13171 "});
13172
13173 // The comment prefix is inserted at the beginning of each line
13174 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13175
13176 cx.assert_editor_state(indoc! {"
13177 fn a() {
13178 // «b();
13179 // c();
13180 // ˇ» d();
13181 }
13182 "});
13183
13184 // If a selection ends at the beginning of a line, that line is not toggled.
13185 cx.set_selections_state(indoc! {"
13186 fn a() {
13187 // b();
13188 // «c();
13189 ˇ»// d();
13190 }
13191 "});
13192
13193 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13194
13195 cx.assert_editor_state(indoc! {"
13196 fn a() {
13197 // b();
13198 «c();
13199 ˇ»// d();
13200 }
13201 "});
13202
13203 // If a selection span a single line and is empty, the line is toggled.
13204 cx.set_state(indoc! {"
13205 fn a() {
13206 a();
13207 b();
13208 ˇ
13209 }
13210 "});
13211
13212 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13213
13214 cx.assert_editor_state(indoc! {"
13215 fn a() {
13216 a();
13217 b();
13218 //ˇ
13219 }
13220 "});
13221
13222 // If a selection span multiple lines, empty lines are not toggled.
13223 cx.set_state(indoc! {"
13224 fn a() {
13225 «a();
13226
13227 c();ˇ»
13228 }
13229 "});
13230
13231 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13232
13233 cx.assert_editor_state(indoc! {"
13234 fn a() {
13235 // «a();
13236
13237 // c();ˇ»
13238 }
13239 "});
13240
13241 // If a selection includes multiple comment prefixes, all lines are uncommented.
13242 cx.set_state(indoc! {"
13243 fn a() {
13244 // «a();
13245 /// b();
13246 //! c();ˇ»
13247 }
13248 "});
13249
13250 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13251
13252 cx.assert_editor_state(indoc! {"
13253 fn a() {
13254 «a();
13255 b();
13256 c();ˇ»
13257 }
13258 "});
13259}
13260
13261#[gpui::test]
13262async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13263 init_test(cx, |_| {});
13264
13265 let language = Arc::new(Language::new(
13266 LanguageConfig {
13267 line_comments: vec!["// ".into()],
13268 ..Default::default()
13269 },
13270 Some(tree_sitter_rust::LANGUAGE.into()),
13271 ));
13272
13273 let mut cx = EditorTestContext::new(cx).await;
13274
13275 cx.language_registry().add(language.clone());
13276 cx.update_buffer(|buffer, cx| {
13277 buffer.set_language(Some(language), cx);
13278 });
13279
13280 let toggle_comments = &ToggleComments {
13281 advance_downwards: true,
13282 ignore_indent: false,
13283 };
13284
13285 // Single cursor on one line -> advance
13286 // Cursor moves horizontally 3 characters as well on non-blank line
13287 cx.set_state(indoc!(
13288 "fn a() {
13289 ˇdog();
13290 cat();
13291 }"
13292 ));
13293 cx.update_editor(|editor, window, cx| {
13294 editor.toggle_comments(toggle_comments, window, cx);
13295 });
13296 cx.assert_editor_state(indoc!(
13297 "fn a() {
13298 // dog();
13299 catˇ();
13300 }"
13301 ));
13302
13303 // Single selection on one line -> don't advance
13304 cx.set_state(indoc!(
13305 "fn a() {
13306 «dog()ˇ»;
13307 cat();
13308 }"
13309 ));
13310 cx.update_editor(|editor, window, cx| {
13311 editor.toggle_comments(toggle_comments, window, cx);
13312 });
13313 cx.assert_editor_state(indoc!(
13314 "fn a() {
13315 // «dog()ˇ»;
13316 cat();
13317 }"
13318 ));
13319
13320 // Multiple cursors on one line -> advance
13321 cx.set_state(indoc!(
13322 "fn a() {
13323 ˇdˇog();
13324 cat();
13325 }"
13326 ));
13327 cx.update_editor(|editor, window, cx| {
13328 editor.toggle_comments(toggle_comments, window, cx);
13329 });
13330 cx.assert_editor_state(indoc!(
13331 "fn a() {
13332 // dog();
13333 catˇ(ˇ);
13334 }"
13335 ));
13336
13337 // Multiple cursors on one line, with selection -> don't advance
13338 cx.set_state(indoc!(
13339 "fn a() {
13340 ˇdˇog«()ˇ»;
13341 cat();
13342 }"
13343 ));
13344 cx.update_editor(|editor, window, cx| {
13345 editor.toggle_comments(toggle_comments, window, cx);
13346 });
13347 cx.assert_editor_state(indoc!(
13348 "fn a() {
13349 // ˇdˇog«()ˇ»;
13350 cat();
13351 }"
13352 ));
13353
13354 // Single cursor on one line -> advance
13355 // Cursor moves to column 0 on blank line
13356 cx.set_state(indoc!(
13357 "fn a() {
13358 ˇdog();
13359
13360 cat();
13361 }"
13362 ));
13363 cx.update_editor(|editor, window, cx| {
13364 editor.toggle_comments(toggle_comments, window, cx);
13365 });
13366 cx.assert_editor_state(indoc!(
13367 "fn a() {
13368 // dog();
13369 ˇ
13370 cat();
13371 }"
13372 ));
13373
13374 // Single cursor on one line -> advance
13375 // Cursor starts and ends at column 0
13376 cx.set_state(indoc!(
13377 "fn a() {
13378 ˇ dog();
13379 cat();
13380 }"
13381 ));
13382 cx.update_editor(|editor, window, cx| {
13383 editor.toggle_comments(toggle_comments, window, cx);
13384 });
13385 cx.assert_editor_state(indoc!(
13386 "fn a() {
13387 // dog();
13388 ˇ cat();
13389 }"
13390 ));
13391}
13392
13393#[gpui::test]
13394async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13395 init_test(cx, |_| {});
13396
13397 let mut cx = EditorTestContext::new(cx).await;
13398
13399 let html_language = Arc::new(
13400 Language::new(
13401 LanguageConfig {
13402 name: "HTML".into(),
13403 block_comment: Some(("<!-- ".into(), " -->".into())),
13404 ..Default::default()
13405 },
13406 Some(tree_sitter_html::LANGUAGE.into()),
13407 )
13408 .with_injection_query(
13409 r#"
13410 (script_element
13411 (raw_text) @injection.content
13412 (#set! injection.language "javascript"))
13413 "#,
13414 )
13415 .unwrap(),
13416 );
13417
13418 let javascript_language = Arc::new(Language::new(
13419 LanguageConfig {
13420 name: "JavaScript".into(),
13421 line_comments: vec!["// ".into()],
13422 ..Default::default()
13423 },
13424 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13425 ));
13426
13427 cx.language_registry().add(html_language.clone());
13428 cx.language_registry().add(javascript_language.clone());
13429 cx.update_buffer(|buffer, cx| {
13430 buffer.set_language(Some(html_language), cx);
13431 });
13432
13433 // Toggle comments for empty selections
13434 cx.set_state(
13435 &r#"
13436 <p>A</p>ˇ
13437 <p>B</p>ˇ
13438 <p>C</p>ˇ
13439 "#
13440 .unindent(),
13441 );
13442 cx.update_editor(|editor, window, cx| {
13443 editor.toggle_comments(&ToggleComments::default(), window, cx)
13444 });
13445 cx.assert_editor_state(
13446 &r#"
13447 <!-- <p>A</p>ˇ -->
13448 <!-- <p>B</p>ˇ -->
13449 <!-- <p>C</p>ˇ -->
13450 "#
13451 .unindent(),
13452 );
13453 cx.update_editor(|editor, window, cx| {
13454 editor.toggle_comments(&ToggleComments::default(), window, cx)
13455 });
13456 cx.assert_editor_state(
13457 &r#"
13458 <p>A</p>ˇ
13459 <p>B</p>ˇ
13460 <p>C</p>ˇ
13461 "#
13462 .unindent(),
13463 );
13464
13465 // Toggle comments for mixture of empty and non-empty selections, where
13466 // multiple selections occupy a given line.
13467 cx.set_state(
13468 &r#"
13469 <p>A«</p>
13470 <p>ˇ»B</p>ˇ
13471 <p>C«</p>
13472 <p>ˇ»D</p>ˇ
13473 "#
13474 .unindent(),
13475 );
13476
13477 cx.update_editor(|editor, window, cx| {
13478 editor.toggle_comments(&ToggleComments::default(), window, cx)
13479 });
13480 cx.assert_editor_state(
13481 &r#"
13482 <!-- <p>A«</p>
13483 <p>ˇ»B</p>ˇ -->
13484 <!-- <p>C«</p>
13485 <p>ˇ»D</p>ˇ -->
13486 "#
13487 .unindent(),
13488 );
13489 cx.update_editor(|editor, window, cx| {
13490 editor.toggle_comments(&ToggleComments::default(), window, cx)
13491 });
13492 cx.assert_editor_state(
13493 &r#"
13494 <p>A«</p>
13495 <p>ˇ»B</p>ˇ
13496 <p>C«</p>
13497 <p>ˇ»D</p>ˇ
13498 "#
13499 .unindent(),
13500 );
13501
13502 // Toggle comments when different languages are active for different
13503 // selections.
13504 cx.set_state(
13505 &r#"
13506 ˇ<script>
13507 ˇvar x = new Y();
13508 ˇ</script>
13509 "#
13510 .unindent(),
13511 );
13512 cx.executor().run_until_parked();
13513 cx.update_editor(|editor, window, cx| {
13514 editor.toggle_comments(&ToggleComments::default(), window, cx)
13515 });
13516 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13517 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13518 cx.assert_editor_state(
13519 &r#"
13520 <!-- ˇ<script> -->
13521 // ˇvar x = new Y();
13522 <!-- ˇ</script> -->
13523 "#
13524 .unindent(),
13525 );
13526}
13527
13528#[gpui::test]
13529fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13530 init_test(cx, |_| {});
13531
13532 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13533 let multibuffer = cx.new(|cx| {
13534 let mut multibuffer = MultiBuffer::new(ReadWrite);
13535 multibuffer.push_excerpts(
13536 buffer.clone(),
13537 [
13538 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13539 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13540 ],
13541 cx,
13542 );
13543 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13544 multibuffer
13545 });
13546
13547 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13548 editor.update_in(cx, |editor, window, cx| {
13549 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13550 editor.change_selections(None, window, cx, |s| {
13551 s.select_ranges([
13552 Point::new(0, 0)..Point::new(0, 0),
13553 Point::new(1, 0)..Point::new(1, 0),
13554 ])
13555 });
13556
13557 editor.handle_input("X", window, cx);
13558 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13559 assert_eq!(
13560 editor.selections.ranges(cx),
13561 [
13562 Point::new(0, 1)..Point::new(0, 1),
13563 Point::new(1, 1)..Point::new(1, 1),
13564 ]
13565 );
13566
13567 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13568 editor.change_selections(None, window, cx, |s| {
13569 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13570 });
13571 editor.backspace(&Default::default(), window, cx);
13572 assert_eq!(editor.text(cx), "Xa\nbbb");
13573 assert_eq!(
13574 editor.selections.ranges(cx),
13575 [Point::new(1, 0)..Point::new(1, 0)]
13576 );
13577
13578 editor.change_selections(None, window, cx, |s| {
13579 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13580 });
13581 editor.backspace(&Default::default(), window, cx);
13582 assert_eq!(editor.text(cx), "X\nbb");
13583 assert_eq!(
13584 editor.selections.ranges(cx),
13585 [Point::new(0, 1)..Point::new(0, 1)]
13586 );
13587 });
13588}
13589
13590#[gpui::test]
13591fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13592 init_test(cx, |_| {});
13593
13594 let markers = vec![('[', ']').into(), ('(', ')').into()];
13595 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13596 indoc! {"
13597 [aaaa
13598 (bbbb]
13599 cccc)",
13600 },
13601 markers.clone(),
13602 );
13603 let excerpt_ranges = markers.into_iter().map(|marker| {
13604 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13605 ExcerptRange::new(context.clone())
13606 });
13607 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13608 let multibuffer = cx.new(|cx| {
13609 let mut multibuffer = MultiBuffer::new(ReadWrite);
13610 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13611 multibuffer
13612 });
13613
13614 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13615 editor.update_in(cx, |editor, window, cx| {
13616 let (expected_text, selection_ranges) = marked_text_ranges(
13617 indoc! {"
13618 aaaa
13619 bˇbbb
13620 bˇbbˇb
13621 cccc"
13622 },
13623 true,
13624 );
13625 assert_eq!(editor.text(cx), expected_text);
13626 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
13627
13628 editor.handle_input("X", window, cx);
13629
13630 let (expected_text, expected_selections) = marked_text_ranges(
13631 indoc! {"
13632 aaaa
13633 bXˇbbXb
13634 bXˇbbXˇb
13635 cccc"
13636 },
13637 false,
13638 );
13639 assert_eq!(editor.text(cx), expected_text);
13640 assert_eq!(editor.selections.ranges(cx), expected_selections);
13641
13642 editor.newline(&Newline, window, cx);
13643 let (expected_text, expected_selections) = marked_text_ranges(
13644 indoc! {"
13645 aaaa
13646 bX
13647 ˇbbX
13648 b
13649 bX
13650 ˇbbX
13651 ˇb
13652 cccc"
13653 },
13654 false,
13655 );
13656 assert_eq!(editor.text(cx), expected_text);
13657 assert_eq!(editor.selections.ranges(cx), expected_selections);
13658 });
13659}
13660
13661#[gpui::test]
13662fn test_refresh_selections(cx: &mut TestAppContext) {
13663 init_test(cx, |_| {});
13664
13665 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13666 let mut excerpt1_id = None;
13667 let multibuffer = cx.new(|cx| {
13668 let mut multibuffer = MultiBuffer::new(ReadWrite);
13669 excerpt1_id = multibuffer
13670 .push_excerpts(
13671 buffer.clone(),
13672 [
13673 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13674 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13675 ],
13676 cx,
13677 )
13678 .into_iter()
13679 .next();
13680 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13681 multibuffer
13682 });
13683
13684 let editor = cx.add_window(|window, cx| {
13685 let mut editor = build_editor(multibuffer.clone(), window, cx);
13686 let snapshot = editor.snapshot(window, cx);
13687 editor.change_selections(None, window, cx, |s| {
13688 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13689 });
13690 editor.begin_selection(
13691 Point::new(2, 1).to_display_point(&snapshot),
13692 true,
13693 1,
13694 window,
13695 cx,
13696 );
13697 assert_eq!(
13698 editor.selections.ranges(cx),
13699 [
13700 Point::new(1, 3)..Point::new(1, 3),
13701 Point::new(2, 1)..Point::new(2, 1),
13702 ]
13703 );
13704 editor
13705 });
13706
13707 // Refreshing selections is a no-op when excerpts haven't changed.
13708 _ = editor.update(cx, |editor, window, cx| {
13709 editor.change_selections(None, window, cx, |s| s.refresh());
13710 assert_eq!(
13711 editor.selections.ranges(cx),
13712 [
13713 Point::new(1, 3)..Point::new(1, 3),
13714 Point::new(2, 1)..Point::new(2, 1),
13715 ]
13716 );
13717 });
13718
13719 multibuffer.update(cx, |multibuffer, cx| {
13720 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13721 });
13722 _ = editor.update(cx, |editor, window, cx| {
13723 // Removing an excerpt causes the first selection to become degenerate.
13724 assert_eq!(
13725 editor.selections.ranges(cx),
13726 [
13727 Point::new(0, 0)..Point::new(0, 0),
13728 Point::new(0, 1)..Point::new(0, 1)
13729 ]
13730 );
13731
13732 // Refreshing selections will relocate the first selection to the original buffer
13733 // location.
13734 editor.change_selections(None, window, cx, |s| s.refresh());
13735 assert_eq!(
13736 editor.selections.ranges(cx),
13737 [
13738 Point::new(0, 1)..Point::new(0, 1),
13739 Point::new(0, 3)..Point::new(0, 3)
13740 ]
13741 );
13742 assert!(editor.selections.pending_anchor().is_some());
13743 });
13744}
13745
13746#[gpui::test]
13747fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13748 init_test(cx, |_| {});
13749
13750 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13751 let mut excerpt1_id = None;
13752 let multibuffer = cx.new(|cx| {
13753 let mut multibuffer = MultiBuffer::new(ReadWrite);
13754 excerpt1_id = multibuffer
13755 .push_excerpts(
13756 buffer.clone(),
13757 [
13758 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13759 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13760 ],
13761 cx,
13762 )
13763 .into_iter()
13764 .next();
13765 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13766 multibuffer
13767 });
13768
13769 let editor = cx.add_window(|window, cx| {
13770 let mut editor = build_editor(multibuffer.clone(), window, cx);
13771 let snapshot = editor.snapshot(window, cx);
13772 editor.begin_selection(
13773 Point::new(1, 3).to_display_point(&snapshot),
13774 false,
13775 1,
13776 window,
13777 cx,
13778 );
13779 assert_eq!(
13780 editor.selections.ranges(cx),
13781 [Point::new(1, 3)..Point::new(1, 3)]
13782 );
13783 editor
13784 });
13785
13786 multibuffer.update(cx, |multibuffer, cx| {
13787 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13788 });
13789 _ = editor.update(cx, |editor, window, cx| {
13790 assert_eq!(
13791 editor.selections.ranges(cx),
13792 [Point::new(0, 0)..Point::new(0, 0)]
13793 );
13794
13795 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13796 editor.change_selections(None, window, cx, |s| s.refresh());
13797 assert_eq!(
13798 editor.selections.ranges(cx),
13799 [Point::new(0, 3)..Point::new(0, 3)]
13800 );
13801 assert!(editor.selections.pending_anchor().is_some());
13802 });
13803}
13804
13805#[gpui::test]
13806async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13807 init_test(cx, |_| {});
13808
13809 let language = Arc::new(
13810 Language::new(
13811 LanguageConfig {
13812 brackets: BracketPairConfig {
13813 pairs: vec![
13814 BracketPair {
13815 start: "{".to_string(),
13816 end: "}".to_string(),
13817 close: true,
13818 surround: true,
13819 newline: true,
13820 },
13821 BracketPair {
13822 start: "/* ".to_string(),
13823 end: " */".to_string(),
13824 close: true,
13825 surround: true,
13826 newline: true,
13827 },
13828 ],
13829 ..Default::default()
13830 },
13831 ..Default::default()
13832 },
13833 Some(tree_sitter_rust::LANGUAGE.into()),
13834 )
13835 .with_indents_query("")
13836 .unwrap(),
13837 );
13838
13839 let text = concat!(
13840 "{ }\n", //
13841 " x\n", //
13842 " /* */\n", //
13843 "x\n", //
13844 "{{} }\n", //
13845 );
13846
13847 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13848 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13849 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13850 editor
13851 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13852 .await;
13853
13854 editor.update_in(cx, |editor, window, cx| {
13855 editor.change_selections(None, window, cx, |s| {
13856 s.select_display_ranges([
13857 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13858 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13859 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13860 ])
13861 });
13862 editor.newline(&Newline, window, cx);
13863
13864 assert_eq!(
13865 editor.buffer().read(cx).read(cx).text(),
13866 concat!(
13867 "{ \n", // Suppress rustfmt
13868 "\n", //
13869 "}\n", //
13870 " x\n", //
13871 " /* \n", //
13872 " \n", //
13873 " */\n", //
13874 "x\n", //
13875 "{{} \n", //
13876 "}\n", //
13877 )
13878 );
13879 });
13880}
13881
13882#[gpui::test]
13883fn test_highlighted_ranges(cx: &mut TestAppContext) {
13884 init_test(cx, |_| {});
13885
13886 let editor = cx.add_window(|window, cx| {
13887 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13888 build_editor(buffer.clone(), window, cx)
13889 });
13890
13891 _ = editor.update(cx, |editor, window, cx| {
13892 struct Type1;
13893 struct Type2;
13894
13895 let buffer = editor.buffer.read(cx).snapshot(cx);
13896
13897 let anchor_range =
13898 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13899
13900 editor.highlight_background::<Type1>(
13901 &[
13902 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13903 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13904 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13905 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13906 ],
13907 |_| Hsla::red(),
13908 cx,
13909 );
13910 editor.highlight_background::<Type2>(
13911 &[
13912 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13913 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13914 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13915 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13916 ],
13917 |_| Hsla::green(),
13918 cx,
13919 );
13920
13921 let snapshot = editor.snapshot(window, cx);
13922 let mut highlighted_ranges = editor.background_highlights_in_range(
13923 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13924 &snapshot,
13925 cx.theme(),
13926 );
13927 // Enforce a consistent ordering based on color without relying on the ordering of the
13928 // highlight's `TypeId` which is non-executor.
13929 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13930 assert_eq!(
13931 highlighted_ranges,
13932 &[
13933 (
13934 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13935 Hsla::red(),
13936 ),
13937 (
13938 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13939 Hsla::red(),
13940 ),
13941 (
13942 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13943 Hsla::green(),
13944 ),
13945 (
13946 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13947 Hsla::green(),
13948 ),
13949 ]
13950 );
13951 assert_eq!(
13952 editor.background_highlights_in_range(
13953 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13954 &snapshot,
13955 cx.theme(),
13956 ),
13957 &[(
13958 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13959 Hsla::red(),
13960 )]
13961 );
13962 });
13963}
13964
13965#[gpui::test]
13966async fn test_following(cx: &mut TestAppContext) {
13967 init_test(cx, |_| {});
13968
13969 let fs = FakeFs::new(cx.executor());
13970 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13971
13972 let buffer = project.update(cx, |project, cx| {
13973 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13974 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13975 });
13976 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13977 let follower = cx.update(|cx| {
13978 cx.open_window(
13979 WindowOptions {
13980 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13981 gpui::Point::new(px(0.), px(0.)),
13982 gpui::Point::new(px(10.), px(80.)),
13983 ))),
13984 ..Default::default()
13985 },
13986 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13987 )
13988 .unwrap()
13989 });
13990
13991 let is_still_following = Rc::new(RefCell::new(true));
13992 let follower_edit_event_count = Rc::new(RefCell::new(0));
13993 let pending_update = Rc::new(RefCell::new(None));
13994 let leader_entity = leader.root(cx).unwrap();
13995 let follower_entity = follower.root(cx).unwrap();
13996 _ = follower.update(cx, {
13997 let update = pending_update.clone();
13998 let is_still_following = is_still_following.clone();
13999 let follower_edit_event_count = follower_edit_event_count.clone();
14000 |_, window, cx| {
14001 cx.subscribe_in(
14002 &leader_entity,
14003 window,
14004 move |_, leader, event, window, cx| {
14005 leader.read(cx).add_event_to_update_proto(
14006 event,
14007 &mut update.borrow_mut(),
14008 window,
14009 cx,
14010 );
14011 },
14012 )
14013 .detach();
14014
14015 cx.subscribe_in(
14016 &follower_entity,
14017 window,
14018 move |_, _, event: &EditorEvent, _window, _cx| {
14019 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14020 *is_still_following.borrow_mut() = false;
14021 }
14022
14023 if let EditorEvent::BufferEdited = event {
14024 *follower_edit_event_count.borrow_mut() += 1;
14025 }
14026 },
14027 )
14028 .detach();
14029 }
14030 });
14031
14032 // Update the selections only
14033 _ = leader.update(cx, |leader, window, cx| {
14034 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
14035 });
14036 follower
14037 .update(cx, |follower, window, cx| {
14038 follower.apply_update_proto(
14039 &project,
14040 pending_update.borrow_mut().take().unwrap(),
14041 window,
14042 cx,
14043 )
14044 })
14045 .unwrap()
14046 .await
14047 .unwrap();
14048 _ = follower.update(cx, |follower, _, cx| {
14049 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14050 });
14051 assert!(*is_still_following.borrow());
14052 assert_eq!(*follower_edit_event_count.borrow(), 0);
14053
14054 // Update the scroll position only
14055 _ = leader.update(cx, |leader, window, cx| {
14056 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14057 });
14058 follower
14059 .update(cx, |follower, window, cx| {
14060 follower.apply_update_proto(
14061 &project,
14062 pending_update.borrow_mut().take().unwrap(),
14063 window,
14064 cx,
14065 )
14066 })
14067 .unwrap()
14068 .await
14069 .unwrap();
14070 assert_eq!(
14071 follower
14072 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14073 .unwrap(),
14074 gpui::Point::new(1.5, 3.5)
14075 );
14076 assert!(*is_still_following.borrow());
14077 assert_eq!(*follower_edit_event_count.borrow(), 0);
14078
14079 // Update the selections and scroll position. The follower's scroll position is updated
14080 // via autoscroll, not via the leader's exact scroll position.
14081 _ = leader.update(cx, |leader, window, cx| {
14082 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
14083 leader.request_autoscroll(Autoscroll::newest(), cx);
14084 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14085 });
14086 follower
14087 .update(cx, |follower, window, cx| {
14088 follower.apply_update_proto(
14089 &project,
14090 pending_update.borrow_mut().take().unwrap(),
14091 window,
14092 cx,
14093 )
14094 })
14095 .unwrap()
14096 .await
14097 .unwrap();
14098 _ = follower.update(cx, |follower, _, cx| {
14099 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14100 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14101 });
14102 assert!(*is_still_following.borrow());
14103
14104 // Creating a pending selection that precedes another selection
14105 _ = leader.update(cx, |leader, window, cx| {
14106 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
14107 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14108 });
14109 follower
14110 .update(cx, |follower, window, cx| {
14111 follower.apply_update_proto(
14112 &project,
14113 pending_update.borrow_mut().take().unwrap(),
14114 window,
14115 cx,
14116 )
14117 })
14118 .unwrap()
14119 .await
14120 .unwrap();
14121 _ = follower.update(cx, |follower, _, cx| {
14122 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14123 });
14124 assert!(*is_still_following.borrow());
14125
14126 // Extend the pending selection so that it surrounds another selection
14127 _ = leader.update(cx, |leader, window, cx| {
14128 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14129 });
14130 follower
14131 .update(cx, |follower, window, cx| {
14132 follower.apply_update_proto(
14133 &project,
14134 pending_update.borrow_mut().take().unwrap(),
14135 window,
14136 cx,
14137 )
14138 })
14139 .unwrap()
14140 .await
14141 .unwrap();
14142 _ = follower.update(cx, |follower, _, cx| {
14143 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14144 });
14145
14146 // Scrolling locally breaks the follow
14147 _ = follower.update(cx, |follower, window, cx| {
14148 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14149 follower.set_scroll_anchor(
14150 ScrollAnchor {
14151 anchor: top_anchor,
14152 offset: gpui::Point::new(0.0, 0.5),
14153 },
14154 window,
14155 cx,
14156 );
14157 });
14158 assert!(!(*is_still_following.borrow()));
14159}
14160
14161#[gpui::test]
14162async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14163 init_test(cx, |_| {});
14164
14165 let fs = FakeFs::new(cx.executor());
14166 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14167 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14168 let pane = workspace
14169 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14170 .unwrap();
14171
14172 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14173
14174 let leader = pane.update_in(cx, |_, window, cx| {
14175 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14176 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14177 });
14178
14179 // Start following the editor when it has no excerpts.
14180 let mut state_message =
14181 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14182 let workspace_entity = workspace.root(cx).unwrap();
14183 let follower_1 = cx
14184 .update_window(*workspace.deref(), |_, window, cx| {
14185 Editor::from_state_proto(
14186 workspace_entity,
14187 ViewId {
14188 creator: CollaboratorId::PeerId(PeerId::default()),
14189 id: 0,
14190 },
14191 &mut state_message,
14192 window,
14193 cx,
14194 )
14195 })
14196 .unwrap()
14197 .unwrap()
14198 .await
14199 .unwrap();
14200
14201 let update_message = Rc::new(RefCell::new(None));
14202 follower_1.update_in(cx, {
14203 let update = update_message.clone();
14204 |_, window, cx| {
14205 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14206 leader.read(cx).add_event_to_update_proto(
14207 event,
14208 &mut update.borrow_mut(),
14209 window,
14210 cx,
14211 );
14212 })
14213 .detach();
14214 }
14215 });
14216
14217 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14218 (
14219 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14220 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14221 )
14222 });
14223
14224 // Insert some excerpts.
14225 leader.update(cx, |leader, cx| {
14226 leader.buffer.update(cx, |multibuffer, cx| {
14227 multibuffer.set_excerpts_for_path(
14228 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14229 buffer_1.clone(),
14230 vec![
14231 Point::row_range(0..3),
14232 Point::row_range(1..6),
14233 Point::row_range(12..15),
14234 ],
14235 0,
14236 cx,
14237 );
14238 multibuffer.set_excerpts_for_path(
14239 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14240 buffer_2.clone(),
14241 vec![Point::row_range(0..6), Point::row_range(8..12)],
14242 0,
14243 cx,
14244 );
14245 });
14246 });
14247
14248 // Apply the update of adding the excerpts.
14249 follower_1
14250 .update_in(cx, |follower, window, cx| {
14251 follower.apply_update_proto(
14252 &project,
14253 update_message.borrow().clone().unwrap(),
14254 window,
14255 cx,
14256 )
14257 })
14258 .await
14259 .unwrap();
14260 assert_eq!(
14261 follower_1.update(cx, |editor, cx| editor.text(cx)),
14262 leader.update(cx, |editor, cx| editor.text(cx))
14263 );
14264 update_message.borrow_mut().take();
14265
14266 // Start following separately after it already has excerpts.
14267 let mut state_message =
14268 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14269 let workspace_entity = workspace.root(cx).unwrap();
14270 let follower_2 = cx
14271 .update_window(*workspace.deref(), |_, window, cx| {
14272 Editor::from_state_proto(
14273 workspace_entity,
14274 ViewId {
14275 creator: CollaboratorId::PeerId(PeerId::default()),
14276 id: 0,
14277 },
14278 &mut state_message,
14279 window,
14280 cx,
14281 )
14282 })
14283 .unwrap()
14284 .unwrap()
14285 .await
14286 .unwrap();
14287 assert_eq!(
14288 follower_2.update(cx, |editor, cx| editor.text(cx)),
14289 leader.update(cx, |editor, cx| editor.text(cx))
14290 );
14291
14292 // Remove some excerpts.
14293 leader.update(cx, |leader, cx| {
14294 leader.buffer.update(cx, |multibuffer, cx| {
14295 let excerpt_ids = multibuffer.excerpt_ids();
14296 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14297 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14298 });
14299 });
14300
14301 // Apply the update of removing the excerpts.
14302 follower_1
14303 .update_in(cx, |follower, window, cx| {
14304 follower.apply_update_proto(
14305 &project,
14306 update_message.borrow().clone().unwrap(),
14307 window,
14308 cx,
14309 )
14310 })
14311 .await
14312 .unwrap();
14313 follower_2
14314 .update_in(cx, |follower, window, cx| {
14315 follower.apply_update_proto(
14316 &project,
14317 update_message.borrow().clone().unwrap(),
14318 window,
14319 cx,
14320 )
14321 })
14322 .await
14323 .unwrap();
14324 update_message.borrow_mut().take();
14325 assert_eq!(
14326 follower_1.update(cx, |editor, cx| editor.text(cx)),
14327 leader.update(cx, |editor, cx| editor.text(cx))
14328 );
14329}
14330
14331#[gpui::test]
14332async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14333 init_test(cx, |_| {});
14334
14335 let mut cx = EditorTestContext::new(cx).await;
14336 let lsp_store =
14337 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14338
14339 cx.set_state(indoc! {"
14340 ˇfn func(abc def: i32) -> u32 {
14341 }
14342 "});
14343
14344 cx.update(|_, cx| {
14345 lsp_store.update(cx, |lsp_store, cx| {
14346 lsp_store
14347 .update_diagnostics(
14348 LanguageServerId(0),
14349 lsp::PublishDiagnosticsParams {
14350 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14351 version: None,
14352 diagnostics: vec![
14353 lsp::Diagnostic {
14354 range: lsp::Range::new(
14355 lsp::Position::new(0, 11),
14356 lsp::Position::new(0, 12),
14357 ),
14358 severity: Some(lsp::DiagnosticSeverity::ERROR),
14359 ..Default::default()
14360 },
14361 lsp::Diagnostic {
14362 range: lsp::Range::new(
14363 lsp::Position::new(0, 12),
14364 lsp::Position::new(0, 15),
14365 ),
14366 severity: Some(lsp::DiagnosticSeverity::ERROR),
14367 ..Default::default()
14368 },
14369 lsp::Diagnostic {
14370 range: lsp::Range::new(
14371 lsp::Position::new(0, 25),
14372 lsp::Position::new(0, 28),
14373 ),
14374 severity: Some(lsp::DiagnosticSeverity::ERROR),
14375 ..Default::default()
14376 },
14377 ],
14378 },
14379 None,
14380 DiagnosticSourceKind::Pushed,
14381 &[],
14382 cx,
14383 )
14384 .unwrap()
14385 });
14386 });
14387
14388 executor.run_until_parked();
14389
14390 cx.update_editor(|editor, window, cx| {
14391 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14392 });
14393
14394 cx.assert_editor_state(indoc! {"
14395 fn func(abc def: i32) -> ˇu32 {
14396 }
14397 "});
14398
14399 cx.update_editor(|editor, window, cx| {
14400 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14401 });
14402
14403 cx.assert_editor_state(indoc! {"
14404 fn func(abc ˇdef: i32) -> u32 {
14405 }
14406 "});
14407
14408 cx.update_editor(|editor, window, cx| {
14409 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14410 });
14411
14412 cx.assert_editor_state(indoc! {"
14413 fn func(abcˇ def: i32) -> u32 {
14414 }
14415 "});
14416
14417 cx.update_editor(|editor, window, cx| {
14418 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14419 });
14420
14421 cx.assert_editor_state(indoc! {"
14422 fn func(abc def: i32) -> ˇu32 {
14423 }
14424 "});
14425}
14426
14427#[gpui::test]
14428async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14429 init_test(cx, |_| {});
14430
14431 let mut cx = EditorTestContext::new(cx).await;
14432
14433 let diff_base = r#"
14434 use some::mod;
14435
14436 const A: u32 = 42;
14437
14438 fn main() {
14439 println!("hello");
14440
14441 println!("world");
14442 }
14443 "#
14444 .unindent();
14445
14446 // Edits are modified, removed, modified, added
14447 cx.set_state(
14448 &r#"
14449 use some::modified;
14450
14451 ˇ
14452 fn main() {
14453 println!("hello there");
14454
14455 println!("around the");
14456 println!("world");
14457 }
14458 "#
14459 .unindent(),
14460 );
14461
14462 cx.set_head_text(&diff_base);
14463 executor.run_until_parked();
14464
14465 cx.update_editor(|editor, window, cx| {
14466 //Wrap around the bottom of the buffer
14467 for _ in 0..3 {
14468 editor.go_to_next_hunk(&GoToHunk, window, cx);
14469 }
14470 });
14471
14472 cx.assert_editor_state(
14473 &r#"
14474 ˇuse some::modified;
14475
14476
14477 fn main() {
14478 println!("hello there");
14479
14480 println!("around the");
14481 println!("world");
14482 }
14483 "#
14484 .unindent(),
14485 );
14486
14487 cx.update_editor(|editor, window, cx| {
14488 //Wrap around the top of the buffer
14489 for _ in 0..2 {
14490 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14491 }
14492 });
14493
14494 cx.assert_editor_state(
14495 &r#"
14496 use some::modified;
14497
14498
14499 fn main() {
14500 ˇ println!("hello there");
14501
14502 println!("around the");
14503 println!("world");
14504 }
14505 "#
14506 .unindent(),
14507 );
14508
14509 cx.update_editor(|editor, window, cx| {
14510 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14511 });
14512
14513 cx.assert_editor_state(
14514 &r#"
14515 use some::modified;
14516
14517 ˇ
14518 fn main() {
14519 println!("hello there");
14520
14521 println!("around the");
14522 println!("world");
14523 }
14524 "#
14525 .unindent(),
14526 );
14527
14528 cx.update_editor(|editor, window, cx| {
14529 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14530 });
14531
14532 cx.assert_editor_state(
14533 &r#"
14534 ˇuse some::modified;
14535
14536
14537 fn main() {
14538 println!("hello there");
14539
14540 println!("around the");
14541 println!("world");
14542 }
14543 "#
14544 .unindent(),
14545 );
14546
14547 cx.update_editor(|editor, window, cx| {
14548 for _ in 0..2 {
14549 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14550 }
14551 });
14552
14553 cx.assert_editor_state(
14554 &r#"
14555 use some::modified;
14556
14557
14558 fn main() {
14559 ˇ println!("hello there");
14560
14561 println!("around the");
14562 println!("world");
14563 }
14564 "#
14565 .unindent(),
14566 );
14567
14568 cx.update_editor(|editor, window, cx| {
14569 editor.fold(&Fold, window, cx);
14570 });
14571
14572 cx.update_editor(|editor, window, cx| {
14573 editor.go_to_next_hunk(&GoToHunk, window, cx);
14574 });
14575
14576 cx.assert_editor_state(
14577 &r#"
14578 ˇuse some::modified;
14579
14580
14581 fn main() {
14582 println!("hello there");
14583
14584 println!("around the");
14585 println!("world");
14586 }
14587 "#
14588 .unindent(),
14589 );
14590}
14591
14592#[test]
14593fn test_split_words() {
14594 fn split(text: &str) -> Vec<&str> {
14595 split_words(text).collect()
14596 }
14597
14598 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14599 assert_eq!(split("hello_world"), &["hello_", "world"]);
14600 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14601 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14602 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14603 assert_eq!(split("helloworld"), &["helloworld"]);
14604
14605 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14606}
14607
14608#[gpui::test]
14609async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14610 init_test(cx, |_| {});
14611
14612 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14613 let mut assert = |before, after| {
14614 let _state_context = cx.set_state(before);
14615 cx.run_until_parked();
14616 cx.update_editor(|editor, window, cx| {
14617 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14618 });
14619 cx.run_until_parked();
14620 cx.assert_editor_state(after);
14621 };
14622
14623 // Outside bracket jumps to outside of matching bracket
14624 assert("console.logˇ(var);", "console.log(var)ˇ;");
14625 assert("console.log(var)ˇ;", "console.logˇ(var);");
14626
14627 // Inside bracket jumps to inside of matching bracket
14628 assert("console.log(ˇvar);", "console.log(varˇ);");
14629 assert("console.log(varˇ);", "console.log(ˇvar);");
14630
14631 // When outside a bracket and inside, favor jumping to the inside bracket
14632 assert(
14633 "console.log('foo', [1, 2, 3]ˇ);",
14634 "console.log(ˇ'foo', [1, 2, 3]);",
14635 );
14636 assert(
14637 "console.log(ˇ'foo', [1, 2, 3]);",
14638 "console.log('foo', [1, 2, 3]ˇ);",
14639 );
14640
14641 // Bias forward if two options are equally likely
14642 assert(
14643 "let result = curried_fun()ˇ();",
14644 "let result = curried_fun()()ˇ;",
14645 );
14646
14647 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14648 assert(
14649 indoc! {"
14650 function test() {
14651 console.log('test')ˇ
14652 }"},
14653 indoc! {"
14654 function test() {
14655 console.logˇ('test')
14656 }"},
14657 );
14658}
14659
14660#[gpui::test]
14661async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14662 init_test(cx, |_| {});
14663
14664 let fs = FakeFs::new(cx.executor());
14665 fs.insert_tree(
14666 path!("/a"),
14667 json!({
14668 "main.rs": "fn main() { let a = 5; }",
14669 "other.rs": "// Test file",
14670 }),
14671 )
14672 .await;
14673 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14674
14675 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14676 language_registry.add(Arc::new(Language::new(
14677 LanguageConfig {
14678 name: "Rust".into(),
14679 matcher: LanguageMatcher {
14680 path_suffixes: vec!["rs".to_string()],
14681 ..Default::default()
14682 },
14683 brackets: BracketPairConfig {
14684 pairs: vec![BracketPair {
14685 start: "{".to_string(),
14686 end: "}".to_string(),
14687 close: true,
14688 surround: true,
14689 newline: true,
14690 }],
14691 disabled_scopes_by_bracket_ix: Vec::new(),
14692 },
14693 ..Default::default()
14694 },
14695 Some(tree_sitter_rust::LANGUAGE.into()),
14696 )));
14697 let mut fake_servers = language_registry.register_fake_lsp(
14698 "Rust",
14699 FakeLspAdapter {
14700 capabilities: lsp::ServerCapabilities {
14701 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14702 first_trigger_character: "{".to_string(),
14703 more_trigger_character: None,
14704 }),
14705 ..Default::default()
14706 },
14707 ..Default::default()
14708 },
14709 );
14710
14711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14712
14713 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14714
14715 let worktree_id = workspace
14716 .update(cx, |workspace, _, cx| {
14717 workspace.project().update(cx, |project, cx| {
14718 project.worktrees(cx).next().unwrap().read(cx).id()
14719 })
14720 })
14721 .unwrap();
14722
14723 let buffer = project
14724 .update(cx, |project, cx| {
14725 project.open_local_buffer(path!("/a/main.rs"), cx)
14726 })
14727 .await
14728 .unwrap();
14729 let editor_handle = workspace
14730 .update(cx, |workspace, window, cx| {
14731 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14732 })
14733 .unwrap()
14734 .await
14735 .unwrap()
14736 .downcast::<Editor>()
14737 .unwrap();
14738
14739 cx.executor().start_waiting();
14740 let fake_server = fake_servers.next().await.unwrap();
14741
14742 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14743 |params, _| async move {
14744 assert_eq!(
14745 params.text_document_position.text_document.uri,
14746 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14747 );
14748 assert_eq!(
14749 params.text_document_position.position,
14750 lsp::Position::new(0, 21),
14751 );
14752
14753 Ok(Some(vec![lsp::TextEdit {
14754 new_text: "]".to_string(),
14755 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14756 }]))
14757 },
14758 );
14759
14760 editor_handle.update_in(cx, |editor, window, cx| {
14761 window.focus(&editor.focus_handle(cx));
14762 editor.change_selections(None, window, cx, |s| {
14763 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14764 });
14765 editor.handle_input("{", window, cx);
14766 });
14767
14768 cx.executor().run_until_parked();
14769
14770 buffer.update(cx, |buffer, _| {
14771 assert_eq!(
14772 buffer.text(),
14773 "fn main() { let a = {5}; }",
14774 "No extra braces from on type formatting should appear in the buffer"
14775 )
14776 });
14777}
14778
14779#[gpui::test(iterations = 20, seeds(31))]
14780async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
14781 init_test(cx, |_| {});
14782
14783 let mut cx = EditorLspTestContext::new_rust(
14784 lsp::ServerCapabilities {
14785 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14786 first_trigger_character: ".".to_string(),
14787 more_trigger_character: None,
14788 }),
14789 ..Default::default()
14790 },
14791 cx,
14792 )
14793 .await;
14794
14795 cx.update_buffer(|buffer, _| {
14796 // This causes autoindent to be async.
14797 buffer.set_sync_parse_timeout(Duration::ZERO)
14798 });
14799
14800 cx.set_state("fn c() {\n d()ˇ\n}\n");
14801 cx.simulate_keystroke("\n");
14802 cx.run_until_parked();
14803
14804 let buffer_cloned =
14805 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
14806 let mut request =
14807 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
14808 let buffer_cloned = buffer_cloned.clone();
14809 async move {
14810 buffer_cloned.update(&mut cx, |buffer, _| {
14811 assert_eq!(
14812 buffer.text(),
14813 "fn c() {\n d()\n .\n}\n",
14814 "OnTypeFormatting should triggered after autoindent applied"
14815 )
14816 })?;
14817
14818 Ok(Some(vec![]))
14819 }
14820 });
14821
14822 cx.simulate_keystroke(".");
14823 cx.run_until_parked();
14824
14825 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
14826 assert!(request.next().await.is_some());
14827 request.close();
14828 assert!(request.next().await.is_none());
14829}
14830
14831#[gpui::test]
14832async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14833 init_test(cx, |_| {});
14834
14835 let fs = FakeFs::new(cx.executor());
14836 fs.insert_tree(
14837 path!("/a"),
14838 json!({
14839 "main.rs": "fn main() { let a = 5; }",
14840 "other.rs": "// Test file",
14841 }),
14842 )
14843 .await;
14844
14845 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14846
14847 let server_restarts = Arc::new(AtomicUsize::new(0));
14848 let closure_restarts = Arc::clone(&server_restarts);
14849 let language_server_name = "test language server";
14850 let language_name: LanguageName = "Rust".into();
14851
14852 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14853 language_registry.add(Arc::new(Language::new(
14854 LanguageConfig {
14855 name: language_name.clone(),
14856 matcher: LanguageMatcher {
14857 path_suffixes: vec!["rs".to_string()],
14858 ..Default::default()
14859 },
14860 ..Default::default()
14861 },
14862 Some(tree_sitter_rust::LANGUAGE.into()),
14863 )));
14864 let mut fake_servers = language_registry.register_fake_lsp(
14865 "Rust",
14866 FakeLspAdapter {
14867 name: language_server_name,
14868 initialization_options: Some(json!({
14869 "testOptionValue": true
14870 })),
14871 initializer: Some(Box::new(move |fake_server| {
14872 let task_restarts = Arc::clone(&closure_restarts);
14873 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14874 task_restarts.fetch_add(1, atomic::Ordering::Release);
14875 futures::future::ready(Ok(()))
14876 });
14877 })),
14878 ..Default::default()
14879 },
14880 );
14881
14882 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14883 let _buffer = project
14884 .update(cx, |project, cx| {
14885 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14886 })
14887 .await
14888 .unwrap();
14889 let _fake_server = fake_servers.next().await.unwrap();
14890 update_test_language_settings(cx, |language_settings| {
14891 language_settings.languages.insert(
14892 language_name.clone(),
14893 LanguageSettingsContent {
14894 tab_size: NonZeroU32::new(8),
14895 ..Default::default()
14896 },
14897 );
14898 });
14899 cx.executor().run_until_parked();
14900 assert_eq!(
14901 server_restarts.load(atomic::Ordering::Acquire),
14902 0,
14903 "Should not restart LSP server on an unrelated change"
14904 );
14905
14906 update_test_project_settings(cx, |project_settings| {
14907 project_settings.lsp.insert(
14908 "Some other server name".into(),
14909 LspSettings {
14910 binary: None,
14911 settings: None,
14912 initialization_options: Some(json!({
14913 "some other init value": false
14914 })),
14915 enable_lsp_tasks: false,
14916 },
14917 );
14918 });
14919 cx.executor().run_until_parked();
14920 assert_eq!(
14921 server_restarts.load(atomic::Ordering::Acquire),
14922 0,
14923 "Should not restart LSP server on an unrelated LSP settings change"
14924 );
14925
14926 update_test_project_settings(cx, |project_settings| {
14927 project_settings.lsp.insert(
14928 language_server_name.into(),
14929 LspSettings {
14930 binary: None,
14931 settings: None,
14932 initialization_options: Some(json!({
14933 "anotherInitValue": false
14934 })),
14935 enable_lsp_tasks: false,
14936 },
14937 );
14938 });
14939 cx.executor().run_until_parked();
14940 assert_eq!(
14941 server_restarts.load(atomic::Ordering::Acquire),
14942 1,
14943 "Should restart LSP server on a related LSP settings change"
14944 );
14945
14946 update_test_project_settings(cx, |project_settings| {
14947 project_settings.lsp.insert(
14948 language_server_name.into(),
14949 LspSettings {
14950 binary: None,
14951 settings: None,
14952 initialization_options: Some(json!({
14953 "anotherInitValue": false
14954 })),
14955 enable_lsp_tasks: false,
14956 },
14957 );
14958 });
14959 cx.executor().run_until_parked();
14960 assert_eq!(
14961 server_restarts.load(atomic::Ordering::Acquire),
14962 1,
14963 "Should not restart LSP server on a related LSP settings change that is the same"
14964 );
14965
14966 update_test_project_settings(cx, |project_settings| {
14967 project_settings.lsp.insert(
14968 language_server_name.into(),
14969 LspSettings {
14970 binary: None,
14971 settings: None,
14972 initialization_options: None,
14973 enable_lsp_tasks: false,
14974 },
14975 );
14976 });
14977 cx.executor().run_until_parked();
14978 assert_eq!(
14979 server_restarts.load(atomic::Ordering::Acquire),
14980 2,
14981 "Should restart LSP server on another related LSP settings change"
14982 );
14983}
14984
14985#[gpui::test]
14986async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14987 init_test(cx, |_| {});
14988
14989 let mut cx = EditorLspTestContext::new_rust(
14990 lsp::ServerCapabilities {
14991 completion_provider: Some(lsp::CompletionOptions {
14992 trigger_characters: Some(vec![".".to_string()]),
14993 resolve_provider: Some(true),
14994 ..Default::default()
14995 }),
14996 ..Default::default()
14997 },
14998 cx,
14999 )
15000 .await;
15001
15002 cx.set_state("fn main() { let a = 2ˇ; }");
15003 cx.simulate_keystroke(".");
15004 let completion_item = lsp::CompletionItem {
15005 label: "some".into(),
15006 kind: Some(lsp::CompletionItemKind::SNIPPET),
15007 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15008 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15009 kind: lsp::MarkupKind::Markdown,
15010 value: "```rust\nSome(2)\n```".to_string(),
15011 })),
15012 deprecated: Some(false),
15013 sort_text: Some("fffffff2".to_string()),
15014 filter_text: Some("some".to_string()),
15015 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15016 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15017 range: lsp::Range {
15018 start: lsp::Position {
15019 line: 0,
15020 character: 22,
15021 },
15022 end: lsp::Position {
15023 line: 0,
15024 character: 22,
15025 },
15026 },
15027 new_text: "Some(2)".to_string(),
15028 })),
15029 additional_text_edits: Some(vec![lsp::TextEdit {
15030 range: lsp::Range {
15031 start: lsp::Position {
15032 line: 0,
15033 character: 20,
15034 },
15035 end: lsp::Position {
15036 line: 0,
15037 character: 22,
15038 },
15039 },
15040 new_text: "".to_string(),
15041 }]),
15042 ..Default::default()
15043 };
15044
15045 let closure_completion_item = completion_item.clone();
15046 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15047 let task_completion_item = closure_completion_item.clone();
15048 async move {
15049 Ok(Some(lsp::CompletionResponse::Array(vec![
15050 task_completion_item,
15051 ])))
15052 }
15053 });
15054
15055 request.next().await;
15056
15057 cx.condition(|editor, _| editor.context_menu_visible())
15058 .await;
15059 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15060 editor
15061 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15062 .unwrap()
15063 });
15064 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15065
15066 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15067 let task_completion_item = completion_item.clone();
15068 async move { Ok(task_completion_item) }
15069 })
15070 .next()
15071 .await
15072 .unwrap();
15073 apply_additional_edits.await.unwrap();
15074 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15075}
15076
15077#[gpui::test]
15078async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15079 init_test(cx, |_| {});
15080
15081 let mut cx = EditorLspTestContext::new_rust(
15082 lsp::ServerCapabilities {
15083 completion_provider: Some(lsp::CompletionOptions {
15084 trigger_characters: Some(vec![".".to_string()]),
15085 resolve_provider: Some(true),
15086 ..Default::default()
15087 }),
15088 ..Default::default()
15089 },
15090 cx,
15091 )
15092 .await;
15093
15094 cx.set_state("fn main() { let a = 2ˇ; }");
15095 cx.simulate_keystroke(".");
15096
15097 let item1 = lsp::CompletionItem {
15098 label: "method id()".to_string(),
15099 filter_text: Some("id".to_string()),
15100 detail: None,
15101 documentation: None,
15102 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15103 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15104 new_text: ".id".to_string(),
15105 })),
15106 ..lsp::CompletionItem::default()
15107 };
15108
15109 let item2 = lsp::CompletionItem {
15110 label: "other".to_string(),
15111 filter_text: Some("other".to_string()),
15112 detail: None,
15113 documentation: None,
15114 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15115 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15116 new_text: ".other".to_string(),
15117 })),
15118 ..lsp::CompletionItem::default()
15119 };
15120
15121 let item1 = item1.clone();
15122 cx.set_request_handler::<lsp::request::Completion, _, _>({
15123 let item1 = item1.clone();
15124 move |_, _, _| {
15125 let item1 = item1.clone();
15126 let item2 = item2.clone();
15127 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15128 }
15129 })
15130 .next()
15131 .await;
15132
15133 cx.condition(|editor, _| editor.context_menu_visible())
15134 .await;
15135 cx.update_editor(|editor, _, _| {
15136 let context_menu = editor.context_menu.borrow_mut();
15137 let context_menu = context_menu
15138 .as_ref()
15139 .expect("Should have the context menu deployed");
15140 match context_menu {
15141 CodeContextMenu::Completions(completions_menu) => {
15142 let completions = completions_menu.completions.borrow_mut();
15143 assert_eq!(
15144 completions
15145 .iter()
15146 .map(|completion| &completion.label.text)
15147 .collect::<Vec<_>>(),
15148 vec!["method id()", "other"]
15149 )
15150 }
15151 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15152 }
15153 });
15154
15155 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15156 let item1 = item1.clone();
15157 move |_, item_to_resolve, _| {
15158 let item1 = item1.clone();
15159 async move {
15160 if item1 == item_to_resolve {
15161 Ok(lsp::CompletionItem {
15162 label: "method id()".to_string(),
15163 filter_text: Some("id".to_string()),
15164 detail: Some("Now resolved!".to_string()),
15165 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15166 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15167 range: lsp::Range::new(
15168 lsp::Position::new(0, 22),
15169 lsp::Position::new(0, 22),
15170 ),
15171 new_text: ".id".to_string(),
15172 })),
15173 ..lsp::CompletionItem::default()
15174 })
15175 } else {
15176 Ok(item_to_resolve)
15177 }
15178 }
15179 }
15180 })
15181 .next()
15182 .await
15183 .unwrap();
15184 cx.run_until_parked();
15185
15186 cx.update_editor(|editor, window, cx| {
15187 editor.context_menu_next(&Default::default(), window, cx);
15188 });
15189
15190 cx.update_editor(|editor, _, _| {
15191 let context_menu = editor.context_menu.borrow_mut();
15192 let context_menu = context_menu
15193 .as_ref()
15194 .expect("Should have the context menu deployed");
15195 match context_menu {
15196 CodeContextMenu::Completions(completions_menu) => {
15197 let completions = completions_menu.completions.borrow_mut();
15198 assert_eq!(
15199 completions
15200 .iter()
15201 .map(|completion| &completion.label.text)
15202 .collect::<Vec<_>>(),
15203 vec!["method id() Now resolved!", "other"],
15204 "Should update first completion label, but not second as the filter text did not match."
15205 );
15206 }
15207 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15208 }
15209 });
15210}
15211
15212#[gpui::test]
15213async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15214 init_test(cx, |_| {});
15215 let mut cx = EditorLspTestContext::new_rust(
15216 lsp::ServerCapabilities {
15217 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15218 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15219 completion_provider: Some(lsp::CompletionOptions {
15220 resolve_provider: Some(true),
15221 ..Default::default()
15222 }),
15223 ..Default::default()
15224 },
15225 cx,
15226 )
15227 .await;
15228 cx.set_state(indoc! {"
15229 struct TestStruct {
15230 field: i32
15231 }
15232
15233 fn mainˇ() {
15234 let unused_var = 42;
15235 let test_struct = TestStruct { field: 42 };
15236 }
15237 "});
15238 let symbol_range = cx.lsp_range(indoc! {"
15239 struct TestStruct {
15240 field: i32
15241 }
15242
15243 «fn main»() {
15244 let unused_var = 42;
15245 let test_struct = TestStruct { field: 42 };
15246 }
15247 "});
15248 let mut hover_requests =
15249 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15250 Ok(Some(lsp::Hover {
15251 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15252 kind: lsp::MarkupKind::Markdown,
15253 value: "Function documentation".to_string(),
15254 }),
15255 range: Some(symbol_range),
15256 }))
15257 });
15258
15259 // Case 1: Test that code action menu hide hover popover
15260 cx.dispatch_action(Hover);
15261 hover_requests.next().await;
15262 cx.condition(|editor, _| editor.hover_state.visible()).await;
15263 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15264 move |_, _, _| async move {
15265 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15266 lsp::CodeAction {
15267 title: "Remove unused variable".to_string(),
15268 kind: Some(CodeActionKind::QUICKFIX),
15269 edit: Some(lsp::WorkspaceEdit {
15270 changes: Some(
15271 [(
15272 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15273 vec![lsp::TextEdit {
15274 range: lsp::Range::new(
15275 lsp::Position::new(5, 4),
15276 lsp::Position::new(5, 27),
15277 ),
15278 new_text: "".to_string(),
15279 }],
15280 )]
15281 .into_iter()
15282 .collect(),
15283 ),
15284 ..Default::default()
15285 }),
15286 ..Default::default()
15287 },
15288 )]))
15289 },
15290 );
15291 cx.update_editor(|editor, window, cx| {
15292 editor.toggle_code_actions(
15293 &ToggleCodeActions {
15294 deployed_from: None,
15295 quick_launch: false,
15296 },
15297 window,
15298 cx,
15299 );
15300 });
15301 code_action_requests.next().await;
15302 cx.run_until_parked();
15303 cx.condition(|editor, _| editor.context_menu_visible())
15304 .await;
15305 cx.update_editor(|editor, _, _| {
15306 assert!(
15307 !editor.hover_state.visible(),
15308 "Hover popover should be hidden when code action menu is shown"
15309 );
15310 // Hide code actions
15311 editor.context_menu.take();
15312 });
15313
15314 // Case 2: Test that code completions hide hover popover
15315 cx.dispatch_action(Hover);
15316 hover_requests.next().await;
15317 cx.condition(|editor, _| editor.hover_state.visible()).await;
15318 let counter = Arc::new(AtomicUsize::new(0));
15319 let mut completion_requests =
15320 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15321 let counter = counter.clone();
15322 async move {
15323 counter.fetch_add(1, atomic::Ordering::Release);
15324 Ok(Some(lsp::CompletionResponse::Array(vec![
15325 lsp::CompletionItem {
15326 label: "main".into(),
15327 kind: Some(lsp::CompletionItemKind::FUNCTION),
15328 detail: Some("() -> ()".to_string()),
15329 ..Default::default()
15330 },
15331 lsp::CompletionItem {
15332 label: "TestStruct".into(),
15333 kind: Some(lsp::CompletionItemKind::STRUCT),
15334 detail: Some("struct TestStruct".to_string()),
15335 ..Default::default()
15336 },
15337 ])))
15338 }
15339 });
15340 cx.update_editor(|editor, window, cx| {
15341 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15342 });
15343 completion_requests.next().await;
15344 cx.condition(|editor, _| editor.context_menu_visible())
15345 .await;
15346 cx.update_editor(|editor, _, _| {
15347 assert!(
15348 !editor.hover_state.visible(),
15349 "Hover popover should be hidden when completion menu is shown"
15350 );
15351 });
15352}
15353
15354#[gpui::test]
15355async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15356 init_test(cx, |_| {});
15357
15358 let mut cx = EditorLspTestContext::new_rust(
15359 lsp::ServerCapabilities {
15360 completion_provider: Some(lsp::CompletionOptions {
15361 trigger_characters: Some(vec![".".to_string()]),
15362 resolve_provider: Some(true),
15363 ..Default::default()
15364 }),
15365 ..Default::default()
15366 },
15367 cx,
15368 )
15369 .await;
15370
15371 cx.set_state("fn main() { let a = 2ˇ; }");
15372 cx.simulate_keystroke(".");
15373
15374 let unresolved_item_1 = lsp::CompletionItem {
15375 label: "id".to_string(),
15376 filter_text: Some("id".to_string()),
15377 detail: None,
15378 documentation: None,
15379 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15380 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15381 new_text: ".id".to_string(),
15382 })),
15383 ..lsp::CompletionItem::default()
15384 };
15385 let resolved_item_1 = lsp::CompletionItem {
15386 additional_text_edits: Some(vec![lsp::TextEdit {
15387 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15388 new_text: "!!".to_string(),
15389 }]),
15390 ..unresolved_item_1.clone()
15391 };
15392 let unresolved_item_2 = lsp::CompletionItem {
15393 label: "other".to_string(),
15394 filter_text: Some("other".to_string()),
15395 detail: None,
15396 documentation: None,
15397 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15398 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15399 new_text: ".other".to_string(),
15400 })),
15401 ..lsp::CompletionItem::default()
15402 };
15403 let resolved_item_2 = lsp::CompletionItem {
15404 additional_text_edits: Some(vec![lsp::TextEdit {
15405 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15406 new_text: "??".to_string(),
15407 }]),
15408 ..unresolved_item_2.clone()
15409 };
15410
15411 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15412 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15413 cx.lsp
15414 .server
15415 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15416 let unresolved_item_1 = unresolved_item_1.clone();
15417 let resolved_item_1 = resolved_item_1.clone();
15418 let unresolved_item_2 = unresolved_item_2.clone();
15419 let resolved_item_2 = resolved_item_2.clone();
15420 let resolve_requests_1 = resolve_requests_1.clone();
15421 let resolve_requests_2 = resolve_requests_2.clone();
15422 move |unresolved_request, _| {
15423 let unresolved_item_1 = unresolved_item_1.clone();
15424 let resolved_item_1 = resolved_item_1.clone();
15425 let unresolved_item_2 = unresolved_item_2.clone();
15426 let resolved_item_2 = resolved_item_2.clone();
15427 let resolve_requests_1 = resolve_requests_1.clone();
15428 let resolve_requests_2 = resolve_requests_2.clone();
15429 async move {
15430 if unresolved_request == unresolved_item_1 {
15431 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15432 Ok(resolved_item_1.clone())
15433 } else if unresolved_request == unresolved_item_2 {
15434 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15435 Ok(resolved_item_2.clone())
15436 } else {
15437 panic!("Unexpected completion item {unresolved_request:?}")
15438 }
15439 }
15440 }
15441 })
15442 .detach();
15443
15444 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15445 let unresolved_item_1 = unresolved_item_1.clone();
15446 let unresolved_item_2 = unresolved_item_2.clone();
15447 async move {
15448 Ok(Some(lsp::CompletionResponse::Array(vec![
15449 unresolved_item_1,
15450 unresolved_item_2,
15451 ])))
15452 }
15453 })
15454 .next()
15455 .await;
15456
15457 cx.condition(|editor, _| editor.context_menu_visible())
15458 .await;
15459 cx.update_editor(|editor, _, _| {
15460 let context_menu = editor.context_menu.borrow_mut();
15461 let context_menu = context_menu
15462 .as_ref()
15463 .expect("Should have the context menu deployed");
15464 match context_menu {
15465 CodeContextMenu::Completions(completions_menu) => {
15466 let completions = completions_menu.completions.borrow_mut();
15467 assert_eq!(
15468 completions
15469 .iter()
15470 .map(|completion| &completion.label.text)
15471 .collect::<Vec<_>>(),
15472 vec!["id", "other"]
15473 )
15474 }
15475 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15476 }
15477 });
15478 cx.run_until_parked();
15479
15480 cx.update_editor(|editor, window, cx| {
15481 editor.context_menu_next(&ContextMenuNext, window, cx);
15482 });
15483 cx.run_until_parked();
15484 cx.update_editor(|editor, window, cx| {
15485 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15486 });
15487 cx.run_until_parked();
15488 cx.update_editor(|editor, window, cx| {
15489 editor.context_menu_next(&ContextMenuNext, window, cx);
15490 });
15491 cx.run_until_parked();
15492 cx.update_editor(|editor, window, cx| {
15493 editor
15494 .compose_completion(&ComposeCompletion::default(), window, cx)
15495 .expect("No task returned")
15496 })
15497 .await
15498 .expect("Completion failed");
15499 cx.run_until_parked();
15500
15501 cx.update_editor(|editor, _, cx| {
15502 assert_eq!(
15503 resolve_requests_1.load(atomic::Ordering::Acquire),
15504 1,
15505 "Should always resolve once despite multiple selections"
15506 );
15507 assert_eq!(
15508 resolve_requests_2.load(atomic::Ordering::Acquire),
15509 1,
15510 "Should always resolve once after multiple selections and applying the completion"
15511 );
15512 assert_eq!(
15513 editor.text(cx),
15514 "fn main() { let a = ??.other; }",
15515 "Should use resolved data when applying the completion"
15516 );
15517 });
15518}
15519
15520#[gpui::test]
15521async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15522 init_test(cx, |_| {});
15523
15524 let item_0 = lsp::CompletionItem {
15525 label: "abs".into(),
15526 insert_text: Some("abs".into()),
15527 data: Some(json!({ "very": "special"})),
15528 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15529 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15530 lsp::InsertReplaceEdit {
15531 new_text: "abs".to_string(),
15532 insert: lsp::Range::default(),
15533 replace: lsp::Range::default(),
15534 },
15535 )),
15536 ..lsp::CompletionItem::default()
15537 };
15538 let items = iter::once(item_0.clone())
15539 .chain((11..51).map(|i| lsp::CompletionItem {
15540 label: format!("item_{}", i),
15541 insert_text: Some(format!("item_{}", i)),
15542 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15543 ..lsp::CompletionItem::default()
15544 }))
15545 .collect::<Vec<_>>();
15546
15547 let default_commit_characters = vec!["?".to_string()];
15548 let default_data = json!({ "default": "data"});
15549 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15550 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15551 let default_edit_range = lsp::Range {
15552 start: lsp::Position {
15553 line: 0,
15554 character: 5,
15555 },
15556 end: lsp::Position {
15557 line: 0,
15558 character: 5,
15559 },
15560 };
15561
15562 let mut cx = EditorLspTestContext::new_rust(
15563 lsp::ServerCapabilities {
15564 completion_provider: Some(lsp::CompletionOptions {
15565 trigger_characters: Some(vec![".".to_string()]),
15566 resolve_provider: Some(true),
15567 ..Default::default()
15568 }),
15569 ..Default::default()
15570 },
15571 cx,
15572 )
15573 .await;
15574
15575 cx.set_state("fn main() { let a = 2ˇ; }");
15576 cx.simulate_keystroke(".");
15577
15578 let completion_data = default_data.clone();
15579 let completion_characters = default_commit_characters.clone();
15580 let completion_items = items.clone();
15581 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15582 let default_data = completion_data.clone();
15583 let default_commit_characters = completion_characters.clone();
15584 let items = completion_items.clone();
15585 async move {
15586 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15587 items,
15588 item_defaults: Some(lsp::CompletionListItemDefaults {
15589 data: Some(default_data.clone()),
15590 commit_characters: Some(default_commit_characters.clone()),
15591 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15592 default_edit_range,
15593 )),
15594 insert_text_format: Some(default_insert_text_format),
15595 insert_text_mode: Some(default_insert_text_mode),
15596 }),
15597 ..lsp::CompletionList::default()
15598 })))
15599 }
15600 })
15601 .next()
15602 .await;
15603
15604 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15605 cx.lsp
15606 .server
15607 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15608 let closure_resolved_items = resolved_items.clone();
15609 move |item_to_resolve, _| {
15610 let closure_resolved_items = closure_resolved_items.clone();
15611 async move {
15612 closure_resolved_items.lock().push(item_to_resolve.clone());
15613 Ok(item_to_resolve)
15614 }
15615 }
15616 })
15617 .detach();
15618
15619 cx.condition(|editor, _| editor.context_menu_visible())
15620 .await;
15621 cx.run_until_parked();
15622 cx.update_editor(|editor, _, _| {
15623 let menu = editor.context_menu.borrow_mut();
15624 match menu.as_ref().expect("should have the completions menu") {
15625 CodeContextMenu::Completions(completions_menu) => {
15626 assert_eq!(
15627 completions_menu
15628 .entries
15629 .borrow()
15630 .iter()
15631 .map(|mat| mat.string.clone())
15632 .collect::<Vec<String>>(),
15633 items
15634 .iter()
15635 .map(|completion| completion.label.clone())
15636 .collect::<Vec<String>>()
15637 );
15638 }
15639 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15640 }
15641 });
15642 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15643 // with 4 from the end.
15644 assert_eq!(
15645 *resolved_items.lock(),
15646 [&items[0..16], &items[items.len() - 4..items.len()]]
15647 .concat()
15648 .iter()
15649 .cloned()
15650 .map(|mut item| {
15651 if item.data.is_none() {
15652 item.data = Some(default_data.clone());
15653 }
15654 item
15655 })
15656 .collect::<Vec<lsp::CompletionItem>>(),
15657 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15658 );
15659 resolved_items.lock().clear();
15660
15661 cx.update_editor(|editor, window, cx| {
15662 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15663 });
15664 cx.run_until_parked();
15665 // Completions that have already been resolved are skipped.
15666 assert_eq!(
15667 *resolved_items.lock(),
15668 items[items.len() - 17..items.len() - 4]
15669 .iter()
15670 .cloned()
15671 .map(|mut item| {
15672 if item.data.is_none() {
15673 item.data = Some(default_data.clone());
15674 }
15675 item
15676 })
15677 .collect::<Vec<lsp::CompletionItem>>()
15678 );
15679 resolved_items.lock().clear();
15680}
15681
15682#[gpui::test]
15683async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15684 init_test(cx, |_| {});
15685
15686 let mut cx = EditorLspTestContext::new(
15687 Language::new(
15688 LanguageConfig {
15689 matcher: LanguageMatcher {
15690 path_suffixes: vec!["jsx".into()],
15691 ..Default::default()
15692 },
15693 overrides: [(
15694 "element".into(),
15695 LanguageConfigOverride {
15696 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15697 ..Default::default()
15698 },
15699 )]
15700 .into_iter()
15701 .collect(),
15702 ..Default::default()
15703 },
15704 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15705 )
15706 .with_override_query("(jsx_self_closing_element) @element")
15707 .unwrap(),
15708 lsp::ServerCapabilities {
15709 completion_provider: Some(lsp::CompletionOptions {
15710 trigger_characters: Some(vec![":".to_string()]),
15711 ..Default::default()
15712 }),
15713 ..Default::default()
15714 },
15715 cx,
15716 )
15717 .await;
15718
15719 cx.lsp
15720 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15721 Ok(Some(lsp::CompletionResponse::Array(vec![
15722 lsp::CompletionItem {
15723 label: "bg-blue".into(),
15724 ..Default::default()
15725 },
15726 lsp::CompletionItem {
15727 label: "bg-red".into(),
15728 ..Default::default()
15729 },
15730 lsp::CompletionItem {
15731 label: "bg-yellow".into(),
15732 ..Default::default()
15733 },
15734 ])))
15735 });
15736
15737 cx.set_state(r#"<p class="bgˇ" />"#);
15738
15739 // Trigger completion when typing a dash, because the dash is an extra
15740 // word character in the 'element' scope, which contains the cursor.
15741 cx.simulate_keystroke("-");
15742 cx.executor().run_until_parked();
15743 cx.update_editor(|editor, _, _| {
15744 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15745 {
15746 assert_eq!(
15747 completion_menu_entries(&menu),
15748 &["bg-blue", "bg-red", "bg-yellow"]
15749 );
15750 } else {
15751 panic!("expected completion menu to be open");
15752 }
15753 });
15754
15755 cx.simulate_keystroke("l");
15756 cx.executor().run_until_parked();
15757 cx.update_editor(|editor, _, _| {
15758 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15759 {
15760 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15761 } else {
15762 panic!("expected completion menu to be open");
15763 }
15764 });
15765
15766 // When filtering completions, consider the character after the '-' to
15767 // be the start of a subword.
15768 cx.set_state(r#"<p class="yelˇ" />"#);
15769 cx.simulate_keystroke("l");
15770 cx.executor().run_until_parked();
15771 cx.update_editor(|editor, _, _| {
15772 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15773 {
15774 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15775 } else {
15776 panic!("expected completion menu to be open");
15777 }
15778 });
15779}
15780
15781fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15782 let entries = menu.entries.borrow();
15783 entries.iter().map(|mat| mat.string.clone()).collect()
15784}
15785
15786#[gpui::test]
15787async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15788 init_test(cx, |settings| {
15789 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15790 FormatterList(vec![Formatter::Prettier].into()),
15791 ))
15792 });
15793
15794 let fs = FakeFs::new(cx.executor());
15795 fs.insert_file(path!("/file.ts"), Default::default()).await;
15796
15797 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15798 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15799
15800 language_registry.add(Arc::new(Language::new(
15801 LanguageConfig {
15802 name: "TypeScript".into(),
15803 matcher: LanguageMatcher {
15804 path_suffixes: vec!["ts".to_string()],
15805 ..Default::default()
15806 },
15807 ..Default::default()
15808 },
15809 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15810 )));
15811 update_test_language_settings(cx, |settings| {
15812 settings.defaults.prettier = Some(PrettierSettings {
15813 allowed: true,
15814 ..PrettierSettings::default()
15815 });
15816 });
15817
15818 let test_plugin = "test_plugin";
15819 let _ = language_registry.register_fake_lsp(
15820 "TypeScript",
15821 FakeLspAdapter {
15822 prettier_plugins: vec![test_plugin],
15823 ..Default::default()
15824 },
15825 );
15826
15827 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15828 let buffer = project
15829 .update(cx, |project, cx| {
15830 project.open_local_buffer(path!("/file.ts"), cx)
15831 })
15832 .await
15833 .unwrap();
15834
15835 let buffer_text = "one\ntwo\nthree\n";
15836 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15837 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15838 editor.update_in(cx, |editor, window, cx| {
15839 editor.set_text(buffer_text, window, cx)
15840 });
15841
15842 editor
15843 .update_in(cx, |editor, window, cx| {
15844 editor.perform_format(
15845 project.clone(),
15846 FormatTrigger::Manual,
15847 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15848 window,
15849 cx,
15850 )
15851 })
15852 .unwrap()
15853 .await;
15854 assert_eq!(
15855 editor.update(cx, |editor, cx| editor.text(cx)),
15856 buffer_text.to_string() + prettier_format_suffix,
15857 "Test prettier formatting was not applied to the original buffer text",
15858 );
15859
15860 update_test_language_settings(cx, |settings| {
15861 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15862 });
15863 let format = editor.update_in(cx, |editor, window, cx| {
15864 editor.perform_format(
15865 project.clone(),
15866 FormatTrigger::Manual,
15867 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15868 window,
15869 cx,
15870 )
15871 });
15872 format.await.unwrap();
15873 assert_eq!(
15874 editor.update(cx, |editor, cx| editor.text(cx)),
15875 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15876 "Autoformatting (via test prettier) was not applied to the original buffer text",
15877 );
15878}
15879
15880#[gpui::test]
15881async fn test_addition_reverts(cx: &mut TestAppContext) {
15882 init_test(cx, |_| {});
15883 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15884 let base_text = indoc! {r#"
15885 struct Row;
15886 struct Row1;
15887 struct Row2;
15888
15889 struct Row4;
15890 struct Row5;
15891 struct Row6;
15892
15893 struct Row8;
15894 struct Row9;
15895 struct Row10;"#};
15896
15897 // When addition hunks are not adjacent to carets, no hunk revert is performed
15898 assert_hunk_revert(
15899 indoc! {r#"struct Row;
15900 struct Row1;
15901 struct Row1.1;
15902 struct Row1.2;
15903 struct Row2;ˇ
15904
15905 struct Row4;
15906 struct Row5;
15907 struct Row6;
15908
15909 struct Row8;
15910 ˇstruct Row9;
15911 struct Row9.1;
15912 struct Row9.2;
15913 struct Row9.3;
15914 struct Row10;"#},
15915 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15916 indoc! {r#"struct Row;
15917 struct Row1;
15918 struct Row1.1;
15919 struct Row1.2;
15920 struct Row2;ˇ
15921
15922 struct Row4;
15923 struct Row5;
15924 struct Row6;
15925
15926 struct Row8;
15927 ˇstruct Row9;
15928 struct Row9.1;
15929 struct Row9.2;
15930 struct Row9.3;
15931 struct Row10;"#},
15932 base_text,
15933 &mut cx,
15934 );
15935 // Same for selections
15936 assert_hunk_revert(
15937 indoc! {r#"struct Row;
15938 struct Row1;
15939 struct Row2;
15940 struct Row2.1;
15941 struct Row2.2;
15942 «ˇ
15943 struct Row4;
15944 struct» Row5;
15945 «struct Row6;
15946 ˇ»
15947 struct Row9.1;
15948 struct Row9.2;
15949 struct Row9.3;
15950 struct Row8;
15951 struct Row9;
15952 struct Row10;"#},
15953 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15954 indoc! {r#"struct Row;
15955 struct Row1;
15956 struct Row2;
15957 struct Row2.1;
15958 struct Row2.2;
15959 «ˇ
15960 struct Row4;
15961 struct» Row5;
15962 «struct Row6;
15963 ˇ»
15964 struct Row9.1;
15965 struct Row9.2;
15966 struct Row9.3;
15967 struct Row8;
15968 struct Row9;
15969 struct Row10;"#},
15970 base_text,
15971 &mut cx,
15972 );
15973
15974 // When carets and selections intersect the addition hunks, those are reverted.
15975 // Adjacent carets got merged.
15976 assert_hunk_revert(
15977 indoc! {r#"struct Row;
15978 ˇ// something on the top
15979 struct Row1;
15980 struct Row2;
15981 struct Roˇw3.1;
15982 struct Row2.2;
15983 struct Row2.3;ˇ
15984
15985 struct Row4;
15986 struct ˇRow5.1;
15987 struct Row5.2;
15988 struct «Rowˇ»5.3;
15989 struct Row5;
15990 struct Row6;
15991 ˇ
15992 struct Row9.1;
15993 struct «Rowˇ»9.2;
15994 struct «ˇRow»9.3;
15995 struct Row8;
15996 struct Row9;
15997 «ˇ// something on bottom»
15998 struct Row10;"#},
15999 vec![
16000 DiffHunkStatusKind::Added,
16001 DiffHunkStatusKind::Added,
16002 DiffHunkStatusKind::Added,
16003 DiffHunkStatusKind::Added,
16004 DiffHunkStatusKind::Added,
16005 ],
16006 indoc! {r#"struct Row;
16007 ˇstruct Row1;
16008 struct Row2;
16009 ˇ
16010 struct Row4;
16011 ˇstruct Row5;
16012 struct Row6;
16013 ˇ
16014 ˇstruct Row8;
16015 struct Row9;
16016 ˇstruct Row10;"#},
16017 base_text,
16018 &mut cx,
16019 );
16020}
16021
16022#[gpui::test]
16023async fn test_modification_reverts(cx: &mut TestAppContext) {
16024 init_test(cx, |_| {});
16025 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16026 let base_text = indoc! {r#"
16027 struct Row;
16028 struct Row1;
16029 struct Row2;
16030
16031 struct Row4;
16032 struct Row5;
16033 struct Row6;
16034
16035 struct Row8;
16036 struct Row9;
16037 struct Row10;"#};
16038
16039 // Modification hunks behave the same as the addition ones.
16040 assert_hunk_revert(
16041 indoc! {r#"struct Row;
16042 struct Row1;
16043 struct Row33;
16044 ˇ
16045 struct Row4;
16046 struct Row5;
16047 struct Row6;
16048 ˇ
16049 struct Row99;
16050 struct Row9;
16051 struct Row10;"#},
16052 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16053 indoc! {r#"struct Row;
16054 struct Row1;
16055 struct Row33;
16056 ˇ
16057 struct Row4;
16058 struct Row5;
16059 struct Row6;
16060 ˇ
16061 struct Row99;
16062 struct Row9;
16063 struct Row10;"#},
16064 base_text,
16065 &mut cx,
16066 );
16067 assert_hunk_revert(
16068 indoc! {r#"struct Row;
16069 struct Row1;
16070 struct Row33;
16071 «ˇ
16072 struct Row4;
16073 struct» Row5;
16074 «struct Row6;
16075 ˇ»
16076 struct Row99;
16077 struct Row9;
16078 struct Row10;"#},
16079 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16080 indoc! {r#"struct Row;
16081 struct Row1;
16082 struct Row33;
16083 «ˇ
16084 struct Row4;
16085 struct» Row5;
16086 «struct Row6;
16087 ˇ»
16088 struct Row99;
16089 struct Row9;
16090 struct Row10;"#},
16091 base_text,
16092 &mut cx,
16093 );
16094
16095 assert_hunk_revert(
16096 indoc! {r#"ˇstruct Row1.1;
16097 struct Row1;
16098 «ˇstr»uct Row22;
16099
16100 struct ˇRow44;
16101 struct Row5;
16102 struct «Rˇ»ow66;ˇ
16103
16104 «struˇ»ct Row88;
16105 struct Row9;
16106 struct Row1011;ˇ"#},
16107 vec![
16108 DiffHunkStatusKind::Modified,
16109 DiffHunkStatusKind::Modified,
16110 DiffHunkStatusKind::Modified,
16111 DiffHunkStatusKind::Modified,
16112 DiffHunkStatusKind::Modified,
16113 DiffHunkStatusKind::Modified,
16114 ],
16115 indoc! {r#"struct Row;
16116 ˇstruct Row1;
16117 struct Row2;
16118 ˇ
16119 struct Row4;
16120 ˇstruct Row5;
16121 struct Row6;
16122 ˇ
16123 struct Row8;
16124 ˇstruct Row9;
16125 struct Row10;ˇ"#},
16126 base_text,
16127 &mut cx,
16128 );
16129}
16130
16131#[gpui::test]
16132async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16133 init_test(cx, |_| {});
16134 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16135 let base_text = indoc! {r#"
16136 one
16137
16138 two
16139 three
16140 "#};
16141
16142 cx.set_head_text(base_text);
16143 cx.set_state("\nˇ\n");
16144 cx.executor().run_until_parked();
16145 cx.update_editor(|editor, _window, cx| {
16146 editor.expand_selected_diff_hunks(cx);
16147 });
16148 cx.executor().run_until_parked();
16149 cx.update_editor(|editor, window, cx| {
16150 editor.backspace(&Default::default(), window, cx);
16151 });
16152 cx.run_until_parked();
16153 cx.assert_state_with_diff(
16154 indoc! {r#"
16155
16156 - two
16157 - threeˇ
16158 +
16159 "#}
16160 .to_string(),
16161 );
16162}
16163
16164#[gpui::test]
16165async fn test_deletion_reverts(cx: &mut TestAppContext) {
16166 init_test(cx, |_| {});
16167 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16168 let base_text = indoc! {r#"struct Row;
16169struct Row1;
16170struct Row2;
16171
16172struct Row4;
16173struct Row5;
16174struct Row6;
16175
16176struct Row8;
16177struct Row9;
16178struct Row10;"#};
16179
16180 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16181 assert_hunk_revert(
16182 indoc! {r#"struct Row;
16183 struct Row2;
16184
16185 ˇstruct Row4;
16186 struct Row5;
16187 struct Row6;
16188 ˇ
16189 struct Row8;
16190 struct Row10;"#},
16191 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16192 indoc! {r#"struct Row;
16193 struct Row2;
16194
16195 ˇstruct Row4;
16196 struct Row5;
16197 struct Row6;
16198 ˇ
16199 struct Row8;
16200 struct Row10;"#},
16201 base_text,
16202 &mut cx,
16203 );
16204 assert_hunk_revert(
16205 indoc! {r#"struct Row;
16206 struct Row2;
16207
16208 «ˇstruct Row4;
16209 struct» Row5;
16210 «struct Row6;
16211 ˇ»
16212 struct Row8;
16213 struct Row10;"#},
16214 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16215 indoc! {r#"struct Row;
16216 struct Row2;
16217
16218 «ˇstruct Row4;
16219 struct» Row5;
16220 «struct Row6;
16221 ˇ»
16222 struct Row8;
16223 struct Row10;"#},
16224 base_text,
16225 &mut cx,
16226 );
16227
16228 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16229 assert_hunk_revert(
16230 indoc! {r#"struct Row;
16231 ˇstruct Row2;
16232
16233 struct Row4;
16234 struct Row5;
16235 struct Row6;
16236
16237 struct Row8;ˇ
16238 struct Row10;"#},
16239 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16240 indoc! {r#"struct Row;
16241 struct Row1;
16242 ˇstruct Row2;
16243
16244 struct Row4;
16245 struct Row5;
16246 struct Row6;
16247
16248 struct Row8;ˇ
16249 struct Row9;
16250 struct Row10;"#},
16251 base_text,
16252 &mut cx,
16253 );
16254 assert_hunk_revert(
16255 indoc! {r#"struct Row;
16256 struct Row2«ˇ;
16257 struct Row4;
16258 struct» Row5;
16259 «struct Row6;
16260
16261 struct Row8;ˇ»
16262 struct Row10;"#},
16263 vec![
16264 DiffHunkStatusKind::Deleted,
16265 DiffHunkStatusKind::Deleted,
16266 DiffHunkStatusKind::Deleted,
16267 ],
16268 indoc! {r#"struct Row;
16269 struct Row1;
16270 struct Row2«ˇ;
16271
16272 struct Row4;
16273 struct» Row5;
16274 «struct Row6;
16275
16276 struct Row8;ˇ»
16277 struct Row9;
16278 struct Row10;"#},
16279 base_text,
16280 &mut cx,
16281 );
16282}
16283
16284#[gpui::test]
16285async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16286 init_test(cx, |_| {});
16287
16288 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16289 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16290 let base_text_3 =
16291 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16292
16293 let text_1 = edit_first_char_of_every_line(base_text_1);
16294 let text_2 = edit_first_char_of_every_line(base_text_2);
16295 let text_3 = edit_first_char_of_every_line(base_text_3);
16296
16297 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16298 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16299 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16300
16301 let multibuffer = cx.new(|cx| {
16302 let mut multibuffer = MultiBuffer::new(ReadWrite);
16303 multibuffer.push_excerpts(
16304 buffer_1.clone(),
16305 [
16306 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16307 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16308 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16309 ],
16310 cx,
16311 );
16312 multibuffer.push_excerpts(
16313 buffer_2.clone(),
16314 [
16315 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16316 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16317 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16318 ],
16319 cx,
16320 );
16321 multibuffer.push_excerpts(
16322 buffer_3.clone(),
16323 [
16324 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16325 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16326 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16327 ],
16328 cx,
16329 );
16330 multibuffer
16331 });
16332
16333 let fs = FakeFs::new(cx.executor());
16334 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16335 let (editor, cx) = cx
16336 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16337 editor.update_in(cx, |editor, _window, cx| {
16338 for (buffer, diff_base) in [
16339 (buffer_1.clone(), base_text_1),
16340 (buffer_2.clone(), base_text_2),
16341 (buffer_3.clone(), base_text_3),
16342 ] {
16343 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16344 editor
16345 .buffer
16346 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16347 }
16348 });
16349 cx.executor().run_until_parked();
16350
16351 editor.update_in(cx, |editor, window, cx| {
16352 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}");
16353 editor.select_all(&SelectAll, window, cx);
16354 editor.git_restore(&Default::default(), window, cx);
16355 });
16356 cx.executor().run_until_parked();
16357
16358 // When all ranges are selected, all buffer hunks are reverted.
16359 editor.update(cx, |editor, cx| {
16360 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");
16361 });
16362 buffer_1.update(cx, |buffer, _| {
16363 assert_eq!(buffer.text(), base_text_1);
16364 });
16365 buffer_2.update(cx, |buffer, _| {
16366 assert_eq!(buffer.text(), base_text_2);
16367 });
16368 buffer_3.update(cx, |buffer, _| {
16369 assert_eq!(buffer.text(), base_text_3);
16370 });
16371
16372 editor.update_in(cx, |editor, window, cx| {
16373 editor.undo(&Default::default(), window, cx);
16374 });
16375
16376 editor.update_in(cx, |editor, window, cx| {
16377 editor.change_selections(None, window, cx, |s| {
16378 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16379 });
16380 editor.git_restore(&Default::default(), window, cx);
16381 });
16382
16383 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16384 // but not affect buffer_2 and its related excerpts.
16385 editor.update(cx, |editor, cx| {
16386 assert_eq!(
16387 editor.text(cx),
16388 "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}"
16389 );
16390 });
16391 buffer_1.update(cx, |buffer, _| {
16392 assert_eq!(buffer.text(), base_text_1);
16393 });
16394 buffer_2.update(cx, |buffer, _| {
16395 assert_eq!(
16396 buffer.text(),
16397 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16398 );
16399 });
16400 buffer_3.update(cx, |buffer, _| {
16401 assert_eq!(
16402 buffer.text(),
16403 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16404 );
16405 });
16406
16407 fn edit_first_char_of_every_line(text: &str) -> String {
16408 text.split('\n')
16409 .map(|line| format!("X{}", &line[1..]))
16410 .collect::<Vec<_>>()
16411 .join("\n")
16412 }
16413}
16414
16415#[gpui::test]
16416async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16417 init_test(cx, |_| {});
16418
16419 let cols = 4;
16420 let rows = 10;
16421 let sample_text_1 = sample_text(rows, cols, 'a');
16422 assert_eq!(
16423 sample_text_1,
16424 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16425 );
16426 let sample_text_2 = sample_text(rows, cols, 'l');
16427 assert_eq!(
16428 sample_text_2,
16429 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16430 );
16431 let sample_text_3 = sample_text(rows, cols, 'v');
16432 assert_eq!(
16433 sample_text_3,
16434 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16435 );
16436
16437 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16438 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16439 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16440
16441 let multi_buffer = cx.new(|cx| {
16442 let mut multibuffer = MultiBuffer::new(ReadWrite);
16443 multibuffer.push_excerpts(
16444 buffer_1.clone(),
16445 [
16446 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16447 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16448 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16449 ],
16450 cx,
16451 );
16452 multibuffer.push_excerpts(
16453 buffer_2.clone(),
16454 [
16455 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16456 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16457 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16458 ],
16459 cx,
16460 );
16461 multibuffer.push_excerpts(
16462 buffer_3.clone(),
16463 [
16464 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16465 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16466 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16467 ],
16468 cx,
16469 );
16470 multibuffer
16471 });
16472
16473 let fs = FakeFs::new(cx.executor());
16474 fs.insert_tree(
16475 "/a",
16476 json!({
16477 "main.rs": sample_text_1,
16478 "other.rs": sample_text_2,
16479 "lib.rs": sample_text_3,
16480 }),
16481 )
16482 .await;
16483 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16484 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16485 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16486 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16487 Editor::new(
16488 EditorMode::full(),
16489 multi_buffer,
16490 Some(project.clone()),
16491 window,
16492 cx,
16493 )
16494 });
16495 let multibuffer_item_id = workspace
16496 .update(cx, |workspace, window, cx| {
16497 assert!(
16498 workspace.active_item(cx).is_none(),
16499 "active item should be None before the first item is added"
16500 );
16501 workspace.add_item_to_active_pane(
16502 Box::new(multi_buffer_editor.clone()),
16503 None,
16504 true,
16505 window,
16506 cx,
16507 );
16508 let active_item = workspace
16509 .active_item(cx)
16510 .expect("should have an active item after adding the multi buffer");
16511 assert!(
16512 !active_item.is_singleton(cx),
16513 "A multi buffer was expected to active after adding"
16514 );
16515 active_item.item_id()
16516 })
16517 .unwrap();
16518 cx.executor().run_until_parked();
16519
16520 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16521 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16522 s.select_ranges(Some(1..2))
16523 });
16524 editor.open_excerpts(&OpenExcerpts, window, cx);
16525 });
16526 cx.executor().run_until_parked();
16527 let first_item_id = workspace
16528 .update(cx, |workspace, window, cx| {
16529 let active_item = workspace
16530 .active_item(cx)
16531 .expect("should have an active item after navigating into the 1st buffer");
16532 let first_item_id = active_item.item_id();
16533 assert_ne!(
16534 first_item_id, multibuffer_item_id,
16535 "Should navigate into the 1st buffer and activate it"
16536 );
16537 assert!(
16538 active_item.is_singleton(cx),
16539 "New active item should be a singleton buffer"
16540 );
16541 assert_eq!(
16542 active_item
16543 .act_as::<Editor>(cx)
16544 .expect("should have navigated into an editor for the 1st buffer")
16545 .read(cx)
16546 .text(cx),
16547 sample_text_1
16548 );
16549
16550 workspace
16551 .go_back(workspace.active_pane().downgrade(), window, cx)
16552 .detach_and_log_err(cx);
16553
16554 first_item_id
16555 })
16556 .unwrap();
16557 cx.executor().run_until_parked();
16558 workspace
16559 .update(cx, |workspace, _, cx| {
16560 let active_item = workspace
16561 .active_item(cx)
16562 .expect("should have an active item after navigating back");
16563 assert_eq!(
16564 active_item.item_id(),
16565 multibuffer_item_id,
16566 "Should navigate back to the multi buffer"
16567 );
16568 assert!(!active_item.is_singleton(cx));
16569 })
16570 .unwrap();
16571
16572 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16573 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16574 s.select_ranges(Some(39..40))
16575 });
16576 editor.open_excerpts(&OpenExcerpts, window, cx);
16577 });
16578 cx.executor().run_until_parked();
16579 let second_item_id = workspace
16580 .update(cx, |workspace, window, cx| {
16581 let active_item = workspace
16582 .active_item(cx)
16583 .expect("should have an active item after navigating into the 2nd buffer");
16584 let second_item_id = active_item.item_id();
16585 assert_ne!(
16586 second_item_id, multibuffer_item_id,
16587 "Should navigate away from the multibuffer"
16588 );
16589 assert_ne!(
16590 second_item_id, first_item_id,
16591 "Should navigate into the 2nd buffer and activate it"
16592 );
16593 assert!(
16594 active_item.is_singleton(cx),
16595 "New active item should be a singleton buffer"
16596 );
16597 assert_eq!(
16598 active_item
16599 .act_as::<Editor>(cx)
16600 .expect("should have navigated into an editor")
16601 .read(cx)
16602 .text(cx),
16603 sample_text_2
16604 );
16605
16606 workspace
16607 .go_back(workspace.active_pane().downgrade(), window, cx)
16608 .detach_and_log_err(cx);
16609
16610 second_item_id
16611 })
16612 .unwrap();
16613 cx.executor().run_until_parked();
16614 workspace
16615 .update(cx, |workspace, _, cx| {
16616 let active_item = workspace
16617 .active_item(cx)
16618 .expect("should have an active item after navigating back from the 2nd buffer");
16619 assert_eq!(
16620 active_item.item_id(),
16621 multibuffer_item_id,
16622 "Should navigate back from the 2nd buffer to the multi buffer"
16623 );
16624 assert!(!active_item.is_singleton(cx));
16625 })
16626 .unwrap();
16627
16628 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16629 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
16630 s.select_ranges(Some(70..70))
16631 });
16632 editor.open_excerpts(&OpenExcerpts, window, cx);
16633 });
16634 cx.executor().run_until_parked();
16635 workspace
16636 .update(cx, |workspace, window, cx| {
16637 let active_item = workspace
16638 .active_item(cx)
16639 .expect("should have an active item after navigating into the 3rd buffer");
16640 let third_item_id = active_item.item_id();
16641 assert_ne!(
16642 third_item_id, multibuffer_item_id,
16643 "Should navigate into the 3rd buffer and activate it"
16644 );
16645 assert_ne!(third_item_id, first_item_id);
16646 assert_ne!(third_item_id, second_item_id);
16647 assert!(
16648 active_item.is_singleton(cx),
16649 "New active item should be a singleton buffer"
16650 );
16651 assert_eq!(
16652 active_item
16653 .act_as::<Editor>(cx)
16654 .expect("should have navigated into an editor")
16655 .read(cx)
16656 .text(cx),
16657 sample_text_3
16658 );
16659
16660 workspace
16661 .go_back(workspace.active_pane().downgrade(), window, cx)
16662 .detach_and_log_err(cx);
16663 })
16664 .unwrap();
16665 cx.executor().run_until_parked();
16666 workspace
16667 .update(cx, |workspace, _, cx| {
16668 let active_item = workspace
16669 .active_item(cx)
16670 .expect("should have an active item after navigating back from the 3rd buffer");
16671 assert_eq!(
16672 active_item.item_id(),
16673 multibuffer_item_id,
16674 "Should navigate back from the 3rd buffer to the multi buffer"
16675 );
16676 assert!(!active_item.is_singleton(cx));
16677 })
16678 .unwrap();
16679}
16680
16681#[gpui::test]
16682async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16683 init_test(cx, |_| {});
16684
16685 let mut cx = EditorTestContext::new(cx).await;
16686
16687 let diff_base = r#"
16688 use some::mod;
16689
16690 const A: u32 = 42;
16691
16692 fn main() {
16693 println!("hello");
16694
16695 println!("world");
16696 }
16697 "#
16698 .unindent();
16699
16700 cx.set_state(
16701 &r#"
16702 use some::modified;
16703
16704 ˇ
16705 fn main() {
16706 println!("hello there");
16707
16708 println!("around the");
16709 println!("world");
16710 }
16711 "#
16712 .unindent(),
16713 );
16714
16715 cx.set_head_text(&diff_base);
16716 executor.run_until_parked();
16717
16718 cx.update_editor(|editor, window, cx| {
16719 editor.go_to_next_hunk(&GoToHunk, window, cx);
16720 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16721 });
16722 executor.run_until_parked();
16723 cx.assert_state_with_diff(
16724 r#"
16725 use some::modified;
16726
16727
16728 fn main() {
16729 - println!("hello");
16730 + ˇ println!("hello there");
16731
16732 println!("around the");
16733 println!("world");
16734 }
16735 "#
16736 .unindent(),
16737 );
16738
16739 cx.update_editor(|editor, window, cx| {
16740 for _ in 0..2 {
16741 editor.go_to_next_hunk(&GoToHunk, window, cx);
16742 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16743 }
16744 });
16745 executor.run_until_parked();
16746 cx.assert_state_with_diff(
16747 r#"
16748 - use some::mod;
16749 + ˇuse some::modified;
16750
16751
16752 fn main() {
16753 - println!("hello");
16754 + println!("hello there");
16755
16756 + println!("around the");
16757 println!("world");
16758 }
16759 "#
16760 .unindent(),
16761 );
16762
16763 cx.update_editor(|editor, window, cx| {
16764 editor.go_to_next_hunk(&GoToHunk, window, cx);
16765 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16766 });
16767 executor.run_until_parked();
16768 cx.assert_state_with_diff(
16769 r#"
16770 - use some::mod;
16771 + use some::modified;
16772
16773 - const A: u32 = 42;
16774 ˇ
16775 fn main() {
16776 - println!("hello");
16777 + println!("hello there");
16778
16779 + println!("around the");
16780 println!("world");
16781 }
16782 "#
16783 .unindent(),
16784 );
16785
16786 cx.update_editor(|editor, window, cx| {
16787 editor.cancel(&Cancel, window, cx);
16788 });
16789
16790 cx.assert_state_with_diff(
16791 r#"
16792 use some::modified;
16793
16794 ˇ
16795 fn main() {
16796 println!("hello there");
16797
16798 println!("around the");
16799 println!("world");
16800 }
16801 "#
16802 .unindent(),
16803 );
16804}
16805
16806#[gpui::test]
16807async fn test_diff_base_change_with_expanded_diff_hunks(
16808 executor: BackgroundExecutor,
16809 cx: &mut TestAppContext,
16810) {
16811 init_test(cx, |_| {});
16812
16813 let mut cx = EditorTestContext::new(cx).await;
16814
16815 let diff_base = r#"
16816 use some::mod1;
16817 use some::mod2;
16818
16819 const A: u32 = 42;
16820 const B: u32 = 42;
16821 const C: u32 = 42;
16822
16823 fn main() {
16824 println!("hello");
16825
16826 println!("world");
16827 }
16828 "#
16829 .unindent();
16830
16831 cx.set_state(
16832 &r#"
16833 use some::mod2;
16834
16835 const A: u32 = 42;
16836 const C: u32 = 42;
16837
16838 fn main(ˇ) {
16839 //println!("hello");
16840
16841 println!("world");
16842 //
16843 //
16844 }
16845 "#
16846 .unindent(),
16847 );
16848
16849 cx.set_head_text(&diff_base);
16850 executor.run_until_parked();
16851
16852 cx.update_editor(|editor, window, cx| {
16853 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16854 });
16855 executor.run_until_parked();
16856 cx.assert_state_with_diff(
16857 r#"
16858 - use some::mod1;
16859 use some::mod2;
16860
16861 const A: u32 = 42;
16862 - const B: u32 = 42;
16863 const C: u32 = 42;
16864
16865 fn main(ˇ) {
16866 - println!("hello");
16867 + //println!("hello");
16868
16869 println!("world");
16870 + //
16871 + //
16872 }
16873 "#
16874 .unindent(),
16875 );
16876
16877 cx.set_head_text("new diff base!");
16878 executor.run_until_parked();
16879 cx.assert_state_with_diff(
16880 r#"
16881 - new diff base!
16882 + use some::mod2;
16883 +
16884 + const A: u32 = 42;
16885 + const C: u32 = 42;
16886 +
16887 + fn main(ˇ) {
16888 + //println!("hello");
16889 +
16890 + println!("world");
16891 + //
16892 + //
16893 + }
16894 "#
16895 .unindent(),
16896 );
16897}
16898
16899#[gpui::test]
16900async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16901 init_test(cx, |_| {});
16902
16903 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16904 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16905 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16906 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16907 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16908 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16909
16910 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16911 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16912 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16913
16914 let multi_buffer = cx.new(|cx| {
16915 let mut multibuffer = MultiBuffer::new(ReadWrite);
16916 multibuffer.push_excerpts(
16917 buffer_1.clone(),
16918 [
16919 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16920 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16921 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16922 ],
16923 cx,
16924 );
16925 multibuffer.push_excerpts(
16926 buffer_2.clone(),
16927 [
16928 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16929 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16930 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16931 ],
16932 cx,
16933 );
16934 multibuffer.push_excerpts(
16935 buffer_3.clone(),
16936 [
16937 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16938 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16939 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16940 ],
16941 cx,
16942 );
16943 multibuffer
16944 });
16945
16946 let editor =
16947 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16948 editor
16949 .update(cx, |editor, _window, cx| {
16950 for (buffer, diff_base) in [
16951 (buffer_1.clone(), file_1_old),
16952 (buffer_2.clone(), file_2_old),
16953 (buffer_3.clone(), file_3_old),
16954 ] {
16955 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16956 editor
16957 .buffer
16958 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16959 }
16960 })
16961 .unwrap();
16962
16963 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16964 cx.run_until_parked();
16965
16966 cx.assert_editor_state(
16967 &"
16968 ˇaaa
16969 ccc
16970 ddd
16971
16972 ggg
16973 hhh
16974
16975
16976 lll
16977 mmm
16978 NNN
16979
16980 qqq
16981 rrr
16982
16983 uuu
16984 111
16985 222
16986 333
16987
16988 666
16989 777
16990
16991 000
16992 !!!"
16993 .unindent(),
16994 );
16995
16996 cx.update_editor(|editor, window, cx| {
16997 editor.select_all(&SelectAll, window, cx);
16998 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16999 });
17000 cx.executor().run_until_parked();
17001
17002 cx.assert_state_with_diff(
17003 "
17004 «aaa
17005 - bbb
17006 ccc
17007 ddd
17008
17009 ggg
17010 hhh
17011
17012
17013 lll
17014 mmm
17015 - nnn
17016 + NNN
17017
17018 qqq
17019 rrr
17020
17021 uuu
17022 111
17023 222
17024 333
17025
17026 + 666
17027 777
17028
17029 000
17030 !!!ˇ»"
17031 .unindent(),
17032 );
17033}
17034
17035#[gpui::test]
17036async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17037 init_test(cx, |_| {});
17038
17039 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17040 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17041
17042 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17043 let multi_buffer = cx.new(|cx| {
17044 let mut multibuffer = MultiBuffer::new(ReadWrite);
17045 multibuffer.push_excerpts(
17046 buffer.clone(),
17047 [
17048 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17049 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17050 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17051 ],
17052 cx,
17053 );
17054 multibuffer
17055 });
17056
17057 let editor =
17058 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17059 editor
17060 .update(cx, |editor, _window, cx| {
17061 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17062 editor
17063 .buffer
17064 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17065 })
17066 .unwrap();
17067
17068 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17069 cx.run_until_parked();
17070
17071 cx.update_editor(|editor, window, cx| {
17072 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17073 });
17074 cx.executor().run_until_parked();
17075
17076 // When the start of a hunk coincides with the start of its excerpt,
17077 // the hunk is expanded. When the start of a a hunk is earlier than
17078 // the start of its excerpt, the hunk is not expanded.
17079 cx.assert_state_with_diff(
17080 "
17081 ˇaaa
17082 - bbb
17083 + BBB
17084
17085 - ddd
17086 - eee
17087 + DDD
17088 + EEE
17089 fff
17090
17091 iii
17092 "
17093 .unindent(),
17094 );
17095}
17096
17097#[gpui::test]
17098async fn test_edits_around_expanded_insertion_hunks(
17099 executor: BackgroundExecutor,
17100 cx: &mut TestAppContext,
17101) {
17102 init_test(cx, |_| {});
17103
17104 let mut cx = EditorTestContext::new(cx).await;
17105
17106 let diff_base = r#"
17107 use some::mod1;
17108 use some::mod2;
17109
17110 const A: u32 = 42;
17111
17112 fn main() {
17113 println!("hello");
17114
17115 println!("world");
17116 }
17117 "#
17118 .unindent();
17119 executor.run_until_parked();
17120 cx.set_state(
17121 &r#"
17122 use some::mod1;
17123 use some::mod2;
17124
17125 const A: u32 = 42;
17126 const B: u32 = 42;
17127 const C: u32 = 42;
17128 ˇ
17129
17130 fn main() {
17131 println!("hello");
17132
17133 println!("world");
17134 }
17135 "#
17136 .unindent(),
17137 );
17138
17139 cx.set_head_text(&diff_base);
17140 executor.run_until_parked();
17141
17142 cx.update_editor(|editor, window, cx| {
17143 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17144 });
17145 executor.run_until_parked();
17146
17147 cx.assert_state_with_diff(
17148 r#"
17149 use some::mod1;
17150 use some::mod2;
17151
17152 const A: u32 = 42;
17153 + const B: u32 = 42;
17154 + const C: u32 = 42;
17155 + ˇ
17156
17157 fn main() {
17158 println!("hello");
17159
17160 println!("world");
17161 }
17162 "#
17163 .unindent(),
17164 );
17165
17166 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17167 executor.run_until_parked();
17168
17169 cx.assert_state_with_diff(
17170 r#"
17171 use some::mod1;
17172 use some::mod2;
17173
17174 const A: u32 = 42;
17175 + const B: u32 = 42;
17176 + const C: u32 = 42;
17177 + const D: u32 = 42;
17178 + ˇ
17179
17180 fn main() {
17181 println!("hello");
17182
17183 println!("world");
17184 }
17185 "#
17186 .unindent(),
17187 );
17188
17189 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17190 executor.run_until_parked();
17191
17192 cx.assert_state_with_diff(
17193 r#"
17194 use some::mod1;
17195 use some::mod2;
17196
17197 const A: u32 = 42;
17198 + const B: u32 = 42;
17199 + const C: u32 = 42;
17200 + const D: u32 = 42;
17201 + const E: u32 = 42;
17202 + ˇ
17203
17204 fn main() {
17205 println!("hello");
17206
17207 println!("world");
17208 }
17209 "#
17210 .unindent(),
17211 );
17212
17213 cx.update_editor(|editor, window, cx| {
17214 editor.delete_line(&DeleteLine, window, cx);
17215 });
17216 executor.run_until_parked();
17217
17218 cx.assert_state_with_diff(
17219 r#"
17220 use some::mod1;
17221 use some::mod2;
17222
17223 const A: u32 = 42;
17224 + const B: u32 = 42;
17225 + const C: u32 = 42;
17226 + const D: u32 = 42;
17227 + const E: u32 = 42;
17228 ˇ
17229 fn main() {
17230 println!("hello");
17231
17232 println!("world");
17233 }
17234 "#
17235 .unindent(),
17236 );
17237
17238 cx.update_editor(|editor, window, cx| {
17239 editor.move_up(&MoveUp, window, cx);
17240 editor.delete_line(&DeleteLine, window, cx);
17241 editor.move_up(&MoveUp, window, cx);
17242 editor.delete_line(&DeleteLine, window, cx);
17243 editor.move_up(&MoveUp, window, cx);
17244 editor.delete_line(&DeleteLine, window, cx);
17245 });
17246 executor.run_until_parked();
17247 cx.assert_state_with_diff(
17248 r#"
17249 use some::mod1;
17250 use some::mod2;
17251
17252 const A: u32 = 42;
17253 + const B: u32 = 42;
17254 ˇ
17255 fn main() {
17256 println!("hello");
17257
17258 println!("world");
17259 }
17260 "#
17261 .unindent(),
17262 );
17263
17264 cx.update_editor(|editor, window, cx| {
17265 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17266 editor.delete_line(&DeleteLine, window, cx);
17267 });
17268 executor.run_until_parked();
17269 cx.assert_state_with_diff(
17270 r#"
17271 ˇ
17272 fn main() {
17273 println!("hello");
17274
17275 println!("world");
17276 }
17277 "#
17278 .unindent(),
17279 );
17280}
17281
17282#[gpui::test]
17283async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17284 init_test(cx, |_| {});
17285
17286 let mut cx = EditorTestContext::new(cx).await;
17287 cx.set_head_text(indoc! { "
17288 one
17289 two
17290 three
17291 four
17292 five
17293 "
17294 });
17295 cx.set_state(indoc! { "
17296 one
17297 ˇthree
17298 five
17299 "});
17300 cx.run_until_parked();
17301 cx.update_editor(|editor, window, cx| {
17302 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17303 });
17304 cx.assert_state_with_diff(
17305 indoc! { "
17306 one
17307 - two
17308 ˇthree
17309 - four
17310 five
17311 "}
17312 .to_string(),
17313 );
17314 cx.update_editor(|editor, window, cx| {
17315 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17316 });
17317
17318 cx.assert_state_with_diff(
17319 indoc! { "
17320 one
17321 ˇthree
17322 five
17323 "}
17324 .to_string(),
17325 );
17326
17327 cx.set_state(indoc! { "
17328 one
17329 ˇTWO
17330 three
17331 four
17332 five
17333 "});
17334 cx.run_until_parked();
17335 cx.update_editor(|editor, window, cx| {
17336 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17337 });
17338
17339 cx.assert_state_with_diff(
17340 indoc! { "
17341 one
17342 - two
17343 + ˇTWO
17344 three
17345 four
17346 five
17347 "}
17348 .to_string(),
17349 );
17350 cx.update_editor(|editor, window, cx| {
17351 editor.move_up(&Default::default(), window, cx);
17352 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17353 });
17354 cx.assert_state_with_diff(
17355 indoc! { "
17356 one
17357 ˇTWO
17358 three
17359 four
17360 five
17361 "}
17362 .to_string(),
17363 );
17364}
17365
17366#[gpui::test]
17367async fn test_edits_around_expanded_deletion_hunks(
17368 executor: BackgroundExecutor,
17369 cx: &mut TestAppContext,
17370) {
17371 init_test(cx, |_| {});
17372
17373 let mut cx = EditorTestContext::new(cx).await;
17374
17375 let diff_base = r#"
17376 use some::mod1;
17377 use some::mod2;
17378
17379 const A: u32 = 42;
17380 const B: u32 = 42;
17381 const C: u32 = 42;
17382
17383
17384 fn main() {
17385 println!("hello");
17386
17387 println!("world");
17388 }
17389 "#
17390 .unindent();
17391 executor.run_until_parked();
17392 cx.set_state(
17393 &r#"
17394 use some::mod1;
17395 use some::mod2;
17396
17397 ˇconst B: u32 = 42;
17398 const C: u32 = 42;
17399
17400
17401 fn main() {
17402 println!("hello");
17403
17404 println!("world");
17405 }
17406 "#
17407 .unindent(),
17408 );
17409
17410 cx.set_head_text(&diff_base);
17411 executor.run_until_parked();
17412
17413 cx.update_editor(|editor, window, cx| {
17414 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17415 });
17416 executor.run_until_parked();
17417
17418 cx.assert_state_with_diff(
17419 r#"
17420 use some::mod1;
17421 use some::mod2;
17422
17423 - const A: u32 = 42;
17424 ˇconst B: u32 = 42;
17425 const C: u32 = 42;
17426
17427
17428 fn main() {
17429 println!("hello");
17430
17431 println!("world");
17432 }
17433 "#
17434 .unindent(),
17435 );
17436
17437 cx.update_editor(|editor, window, cx| {
17438 editor.delete_line(&DeleteLine, window, cx);
17439 });
17440 executor.run_until_parked();
17441 cx.assert_state_with_diff(
17442 r#"
17443 use some::mod1;
17444 use some::mod2;
17445
17446 - const A: u32 = 42;
17447 - const B: u32 = 42;
17448 ˇconst C: u32 = 42;
17449
17450
17451 fn main() {
17452 println!("hello");
17453
17454 println!("world");
17455 }
17456 "#
17457 .unindent(),
17458 );
17459
17460 cx.update_editor(|editor, window, cx| {
17461 editor.delete_line(&DeleteLine, window, cx);
17462 });
17463 executor.run_until_parked();
17464 cx.assert_state_with_diff(
17465 r#"
17466 use some::mod1;
17467 use some::mod2;
17468
17469 - const A: u32 = 42;
17470 - const B: u32 = 42;
17471 - const C: u32 = 42;
17472 ˇ
17473
17474 fn main() {
17475 println!("hello");
17476
17477 println!("world");
17478 }
17479 "#
17480 .unindent(),
17481 );
17482
17483 cx.update_editor(|editor, window, cx| {
17484 editor.handle_input("replacement", window, cx);
17485 });
17486 executor.run_until_parked();
17487 cx.assert_state_with_diff(
17488 r#"
17489 use some::mod1;
17490 use some::mod2;
17491
17492 - const A: u32 = 42;
17493 - const B: u32 = 42;
17494 - const C: u32 = 42;
17495 -
17496 + replacementˇ
17497
17498 fn main() {
17499 println!("hello");
17500
17501 println!("world");
17502 }
17503 "#
17504 .unindent(),
17505 );
17506}
17507
17508#[gpui::test]
17509async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17510 init_test(cx, |_| {});
17511
17512 let mut cx = EditorTestContext::new(cx).await;
17513
17514 let base_text = r#"
17515 one
17516 two
17517 three
17518 four
17519 five
17520 "#
17521 .unindent();
17522 executor.run_until_parked();
17523 cx.set_state(
17524 &r#"
17525 one
17526 two
17527 fˇour
17528 five
17529 "#
17530 .unindent(),
17531 );
17532
17533 cx.set_head_text(&base_text);
17534 executor.run_until_parked();
17535
17536 cx.update_editor(|editor, window, cx| {
17537 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17538 });
17539 executor.run_until_parked();
17540
17541 cx.assert_state_with_diff(
17542 r#"
17543 one
17544 two
17545 - three
17546 fˇour
17547 five
17548 "#
17549 .unindent(),
17550 );
17551
17552 cx.update_editor(|editor, window, cx| {
17553 editor.backspace(&Backspace, window, cx);
17554 editor.backspace(&Backspace, window, cx);
17555 });
17556 executor.run_until_parked();
17557 cx.assert_state_with_diff(
17558 r#"
17559 one
17560 two
17561 - threeˇ
17562 - four
17563 + our
17564 five
17565 "#
17566 .unindent(),
17567 );
17568}
17569
17570#[gpui::test]
17571async fn test_edit_after_expanded_modification_hunk(
17572 executor: BackgroundExecutor,
17573 cx: &mut TestAppContext,
17574) {
17575 init_test(cx, |_| {});
17576
17577 let mut cx = EditorTestContext::new(cx).await;
17578
17579 let diff_base = r#"
17580 use some::mod1;
17581 use some::mod2;
17582
17583 const A: u32 = 42;
17584 const B: u32 = 42;
17585 const C: u32 = 42;
17586 const D: u32 = 42;
17587
17588
17589 fn main() {
17590 println!("hello");
17591
17592 println!("world");
17593 }"#
17594 .unindent();
17595
17596 cx.set_state(
17597 &r#"
17598 use some::mod1;
17599 use some::mod2;
17600
17601 const A: u32 = 42;
17602 const B: u32 = 42;
17603 const C: u32 = 43ˇ
17604 const D: u32 = 42;
17605
17606
17607 fn main() {
17608 println!("hello");
17609
17610 println!("world");
17611 }"#
17612 .unindent(),
17613 );
17614
17615 cx.set_head_text(&diff_base);
17616 executor.run_until_parked();
17617 cx.update_editor(|editor, window, cx| {
17618 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17619 });
17620 executor.run_until_parked();
17621
17622 cx.assert_state_with_diff(
17623 r#"
17624 use some::mod1;
17625 use some::mod2;
17626
17627 const A: u32 = 42;
17628 const B: u32 = 42;
17629 - const C: u32 = 42;
17630 + const C: u32 = 43ˇ
17631 const D: u32 = 42;
17632
17633
17634 fn main() {
17635 println!("hello");
17636
17637 println!("world");
17638 }"#
17639 .unindent(),
17640 );
17641
17642 cx.update_editor(|editor, window, cx| {
17643 editor.handle_input("\nnew_line\n", window, cx);
17644 });
17645 executor.run_until_parked();
17646
17647 cx.assert_state_with_diff(
17648 r#"
17649 use some::mod1;
17650 use some::mod2;
17651
17652 const A: u32 = 42;
17653 const B: u32 = 42;
17654 - const C: u32 = 42;
17655 + const C: u32 = 43
17656 + new_line
17657 + ˇ
17658 const D: u32 = 42;
17659
17660
17661 fn main() {
17662 println!("hello");
17663
17664 println!("world");
17665 }"#
17666 .unindent(),
17667 );
17668}
17669
17670#[gpui::test]
17671async fn test_stage_and_unstage_added_file_hunk(
17672 executor: BackgroundExecutor,
17673 cx: &mut TestAppContext,
17674) {
17675 init_test(cx, |_| {});
17676
17677 let mut cx = EditorTestContext::new(cx).await;
17678 cx.update_editor(|editor, _, cx| {
17679 editor.set_expand_all_diff_hunks(cx);
17680 });
17681
17682 let working_copy = r#"
17683 ˇfn main() {
17684 println!("hello, world!");
17685 }
17686 "#
17687 .unindent();
17688
17689 cx.set_state(&working_copy);
17690 executor.run_until_parked();
17691
17692 cx.assert_state_with_diff(
17693 r#"
17694 + ˇfn main() {
17695 + println!("hello, world!");
17696 + }
17697 "#
17698 .unindent(),
17699 );
17700 cx.assert_index_text(None);
17701
17702 cx.update_editor(|editor, window, cx| {
17703 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17704 });
17705 executor.run_until_parked();
17706 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17707 cx.assert_state_with_diff(
17708 r#"
17709 + ˇfn main() {
17710 + println!("hello, world!");
17711 + }
17712 "#
17713 .unindent(),
17714 );
17715
17716 cx.update_editor(|editor, window, cx| {
17717 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17718 });
17719 executor.run_until_parked();
17720 cx.assert_index_text(None);
17721}
17722
17723async fn setup_indent_guides_editor(
17724 text: &str,
17725 cx: &mut TestAppContext,
17726) -> (BufferId, EditorTestContext) {
17727 init_test(cx, |_| {});
17728
17729 let mut cx = EditorTestContext::new(cx).await;
17730
17731 let buffer_id = cx.update_editor(|editor, window, cx| {
17732 editor.set_text(text, window, cx);
17733 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17734
17735 buffer_ids[0]
17736 });
17737
17738 (buffer_id, cx)
17739}
17740
17741fn assert_indent_guides(
17742 range: Range<u32>,
17743 expected: Vec<IndentGuide>,
17744 active_indices: Option<Vec<usize>>,
17745 cx: &mut EditorTestContext,
17746) {
17747 let indent_guides = cx.update_editor(|editor, window, cx| {
17748 let snapshot = editor.snapshot(window, cx).display_snapshot;
17749 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17750 editor,
17751 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17752 true,
17753 &snapshot,
17754 cx,
17755 );
17756
17757 indent_guides.sort_by(|a, b| {
17758 a.depth.cmp(&b.depth).then(
17759 a.start_row
17760 .cmp(&b.start_row)
17761 .then(a.end_row.cmp(&b.end_row)),
17762 )
17763 });
17764 indent_guides
17765 });
17766
17767 if let Some(expected) = active_indices {
17768 let active_indices = cx.update_editor(|editor, window, cx| {
17769 let snapshot = editor.snapshot(window, cx).display_snapshot;
17770 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17771 });
17772
17773 assert_eq!(
17774 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17775 expected,
17776 "Active indent guide indices do not match"
17777 );
17778 }
17779
17780 assert_eq!(indent_guides, expected, "Indent guides do not match");
17781}
17782
17783fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17784 IndentGuide {
17785 buffer_id,
17786 start_row: MultiBufferRow(start_row),
17787 end_row: MultiBufferRow(end_row),
17788 depth,
17789 tab_size: 4,
17790 settings: IndentGuideSettings {
17791 enabled: true,
17792 line_width: 1,
17793 active_line_width: 1,
17794 ..Default::default()
17795 },
17796 }
17797}
17798
17799#[gpui::test]
17800async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17801 let (buffer_id, mut cx) = setup_indent_guides_editor(
17802 &"
17803 fn main() {
17804 let a = 1;
17805 }"
17806 .unindent(),
17807 cx,
17808 )
17809 .await;
17810
17811 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17812}
17813
17814#[gpui::test]
17815async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17816 let (buffer_id, mut cx) = setup_indent_guides_editor(
17817 &"
17818 fn main() {
17819 let a = 1;
17820 let b = 2;
17821 }"
17822 .unindent(),
17823 cx,
17824 )
17825 .await;
17826
17827 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17828}
17829
17830#[gpui::test]
17831async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17832 let (buffer_id, mut cx) = setup_indent_guides_editor(
17833 &"
17834 fn main() {
17835 let a = 1;
17836 if a == 3 {
17837 let b = 2;
17838 } else {
17839 let c = 3;
17840 }
17841 }"
17842 .unindent(),
17843 cx,
17844 )
17845 .await;
17846
17847 assert_indent_guides(
17848 0..8,
17849 vec![
17850 indent_guide(buffer_id, 1, 6, 0),
17851 indent_guide(buffer_id, 3, 3, 1),
17852 indent_guide(buffer_id, 5, 5, 1),
17853 ],
17854 None,
17855 &mut cx,
17856 );
17857}
17858
17859#[gpui::test]
17860async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17861 let (buffer_id, mut cx) = setup_indent_guides_editor(
17862 &"
17863 fn main() {
17864 let a = 1;
17865 let b = 2;
17866 let c = 3;
17867 }"
17868 .unindent(),
17869 cx,
17870 )
17871 .await;
17872
17873 assert_indent_guides(
17874 0..5,
17875 vec![
17876 indent_guide(buffer_id, 1, 3, 0),
17877 indent_guide(buffer_id, 2, 2, 1),
17878 ],
17879 None,
17880 &mut cx,
17881 );
17882}
17883
17884#[gpui::test]
17885async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17886 let (buffer_id, mut cx) = setup_indent_guides_editor(
17887 &"
17888 fn main() {
17889 let a = 1;
17890
17891 let c = 3;
17892 }"
17893 .unindent(),
17894 cx,
17895 )
17896 .await;
17897
17898 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17899}
17900
17901#[gpui::test]
17902async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17903 let (buffer_id, mut cx) = setup_indent_guides_editor(
17904 &"
17905 fn main() {
17906 let a = 1;
17907
17908 let c = 3;
17909
17910 if a == 3 {
17911 let b = 2;
17912 } else {
17913 let c = 3;
17914 }
17915 }"
17916 .unindent(),
17917 cx,
17918 )
17919 .await;
17920
17921 assert_indent_guides(
17922 0..11,
17923 vec![
17924 indent_guide(buffer_id, 1, 9, 0),
17925 indent_guide(buffer_id, 6, 6, 1),
17926 indent_guide(buffer_id, 8, 8, 1),
17927 ],
17928 None,
17929 &mut cx,
17930 );
17931}
17932
17933#[gpui::test]
17934async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
17935 let (buffer_id, mut cx) = setup_indent_guides_editor(
17936 &"
17937 fn main() {
17938 let a = 1;
17939
17940 let c = 3;
17941
17942 if a == 3 {
17943 let b = 2;
17944 } else {
17945 let c = 3;
17946 }
17947 }"
17948 .unindent(),
17949 cx,
17950 )
17951 .await;
17952
17953 assert_indent_guides(
17954 1..11,
17955 vec![
17956 indent_guide(buffer_id, 1, 9, 0),
17957 indent_guide(buffer_id, 6, 6, 1),
17958 indent_guide(buffer_id, 8, 8, 1),
17959 ],
17960 None,
17961 &mut cx,
17962 );
17963}
17964
17965#[gpui::test]
17966async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
17967 let (buffer_id, mut cx) = setup_indent_guides_editor(
17968 &"
17969 fn main() {
17970 let a = 1;
17971
17972 let c = 3;
17973
17974 if a == 3 {
17975 let b = 2;
17976 } else {
17977 let c = 3;
17978 }
17979 }"
17980 .unindent(),
17981 cx,
17982 )
17983 .await;
17984
17985 assert_indent_guides(
17986 1..10,
17987 vec![
17988 indent_guide(buffer_id, 1, 9, 0),
17989 indent_guide(buffer_id, 6, 6, 1),
17990 indent_guide(buffer_id, 8, 8, 1),
17991 ],
17992 None,
17993 &mut cx,
17994 );
17995}
17996
17997#[gpui::test]
17998async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17999 let (buffer_id, mut cx) = setup_indent_guides_editor(
18000 &"
18001 fn main() {
18002 if a {
18003 b(
18004 c,
18005 d,
18006 )
18007 } else {
18008 e(
18009 f
18010 )
18011 }
18012 }"
18013 .unindent(),
18014 cx,
18015 )
18016 .await;
18017
18018 assert_indent_guides(
18019 0..11,
18020 vec![
18021 indent_guide(buffer_id, 1, 10, 0),
18022 indent_guide(buffer_id, 2, 5, 1),
18023 indent_guide(buffer_id, 7, 9, 1),
18024 indent_guide(buffer_id, 3, 4, 2),
18025 indent_guide(buffer_id, 8, 8, 2),
18026 ],
18027 None,
18028 &mut cx,
18029 );
18030
18031 cx.update_editor(|editor, window, cx| {
18032 editor.fold_at(MultiBufferRow(2), window, cx);
18033 assert_eq!(
18034 editor.display_text(cx),
18035 "
18036 fn main() {
18037 if a {
18038 b(⋯
18039 )
18040 } else {
18041 e(
18042 f
18043 )
18044 }
18045 }"
18046 .unindent()
18047 );
18048 });
18049
18050 assert_indent_guides(
18051 0..11,
18052 vec![
18053 indent_guide(buffer_id, 1, 10, 0),
18054 indent_guide(buffer_id, 2, 5, 1),
18055 indent_guide(buffer_id, 7, 9, 1),
18056 indent_guide(buffer_id, 8, 8, 2),
18057 ],
18058 None,
18059 &mut cx,
18060 );
18061}
18062
18063#[gpui::test]
18064async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18065 let (buffer_id, mut cx) = setup_indent_guides_editor(
18066 &"
18067 block1
18068 block2
18069 block3
18070 block4
18071 block2
18072 block1
18073 block1"
18074 .unindent(),
18075 cx,
18076 )
18077 .await;
18078
18079 assert_indent_guides(
18080 1..10,
18081 vec![
18082 indent_guide(buffer_id, 1, 4, 0),
18083 indent_guide(buffer_id, 2, 3, 1),
18084 indent_guide(buffer_id, 3, 3, 2),
18085 ],
18086 None,
18087 &mut cx,
18088 );
18089}
18090
18091#[gpui::test]
18092async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18093 let (buffer_id, mut cx) = setup_indent_guides_editor(
18094 &"
18095 block1
18096 block2
18097 block3
18098
18099 block1
18100 block1"
18101 .unindent(),
18102 cx,
18103 )
18104 .await;
18105
18106 assert_indent_guides(
18107 0..6,
18108 vec![
18109 indent_guide(buffer_id, 1, 2, 0),
18110 indent_guide(buffer_id, 2, 2, 1),
18111 ],
18112 None,
18113 &mut cx,
18114 );
18115}
18116
18117#[gpui::test]
18118async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18119 let (buffer_id, mut cx) = setup_indent_guides_editor(
18120 &"
18121 function component() {
18122 \treturn (
18123 \t\t\t
18124 \t\t<div>
18125 \t\t\t<abc></abc>
18126 \t\t</div>
18127 \t)
18128 }"
18129 .unindent(),
18130 cx,
18131 )
18132 .await;
18133
18134 assert_indent_guides(
18135 0..8,
18136 vec![
18137 indent_guide(buffer_id, 1, 6, 0),
18138 indent_guide(buffer_id, 2, 5, 1),
18139 indent_guide(buffer_id, 4, 4, 2),
18140 ],
18141 None,
18142 &mut cx,
18143 );
18144}
18145
18146#[gpui::test]
18147async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18148 let (buffer_id, mut cx) = setup_indent_guides_editor(
18149 &"
18150 function component() {
18151 \treturn (
18152 \t
18153 \t\t<div>
18154 \t\t\t<abc></abc>
18155 \t\t</div>
18156 \t)
18157 }"
18158 .unindent(),
18159 cx,
18160 )
18161 .await;
18162
18163 assert_indent_guides(
18164 0..8,
18165 vec![
18166 indent_guide(buffer_id, 1, 6, 0),
18167 indent_guide(buffer_id, 2, 5, 1),
18168 indent_guide(buffer_id, 4, 4, 2),
18169 ],
18170 None,
18171 &mut cx,
18172 );
18173}
18174
18175#[gpui::test]
18176async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18177 let (buffer_id, mut cx) = setup_indent_guides_editor(
18178 &"
18179 block1
18180
18181
18182
18183 block2
18184 "
18185 .unindent(),
18186 cx,
18187 )
18188 .await;
18189
18190 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18191}
18192
18193#[gpui::test]
18194async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18195 let (buffer_id, mut cx) = setup_indent_guides_editor(
18196 &"
18197 def a:
18198 \tb = 3
18199 \tif True:
18200 \t\tc = 4
18201 \t\td = 5
18202 \tprint(b)
18203 "
18204 .unindent(),
18205 cx,
18206 )
18207 .await;
18208
18209 assert_indent_guides(
18210 0..6,
18211 vec![
18212 indent_guide(buffer_id, 1, 5, 0),
18213 indent_guide(buffer_id, 3, 4, 1),
18214 ],
18215 None,
18216 &mut cx,
18217 );
18218}
18219
18220#[gpui::test]
18221async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18222 let (buffer_id, mut cx) = setup_indent_guides_editor(
18223 &"
18224 fn main() {
18225 let a = 1;
18226 }"
18227 .unindent(),
18228 cx,
18229 )
18230 .await;
18231
18232 cx.update_editor(|editor, window, cx| {
18233 editor.change_selections(None, window, cx, |s| {
18234 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18235 });
18236 });
18237
18238 assert_indent_guides(
18239 0..3,
18240 vec![indent_guide(buffer_id, 1, 1, 0)],
18241 Some(vec![0]),
18242 &mut cx,
18243 );
18244}
18245
18246#[gpui::test]
18247async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18248 let (buffer_id, mut cx) = setup_indent_guides_editor(
18249 &"
18250 fn main() {
18251 if 1 == 2 {
18252 let a = 1;
18253 }
18254 }"
18255 .unindent(),
18256 cx,
18257 )
18258 .await;
18259
18260 cx.update_editor(|editor, window, cx| {
18261 editor.change_selections(None, window, cx, |s| {
18262 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18263 });
18264 });
18265
18266 assert_indent_guides(
18267 0..4,
18268 vec![
18269 indent_guide(buffer_id, 1, 3, 0),
18270 indent_guide(buffer_id, 2, 2, 1),
18271 ],
18272 Some(vec![1]),
18273 &mut cx,
18274 );
18275
18276 cx.update_editor(|editor, window, cx| {
18277 editor.change_selections(None, window, cx, |s| {
18278 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18279 });
18280 });
18281
18282 assert_indent_guides(
18283 0..4,
18284 vec![
18285 indent_guide(buffer_id, 1, 3, 0),
18286 indent_guide(buffer_id, 2, 2, 1),
18287 ],
18288 Some(vec![1]),
18289 &mut cx,
18290 );
18291
18292 cx.update_editor(|editor, window, cx| {
18293 editor.change_selections(None, window, cx, |s| {
18294 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18295 });
18296 });
18297
18298 assert_indent_guides(
18299 0..4,
18300 vec![
18301 indent_guide(buffer_id, 1, 3, 0),
18302 indent_guide(buffer_id, 2, 2, 1),
18303 ],
18304 Some(vec![0]),
18305 &mut cx,
18306 );
18307}
18308
18309#[gpui::test]
18310async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18311 let (buffer_id, mut cx) = setup_indent_guides_editor(
18312 &"
18313 fn main() {
18314 let a = 1;
18315
18316 let b = 2;
18317 }"
18318 .unindent(),
18319 cx,
18320 )
18321 .await;
18322
18323 cx.update_editor(|editor, window, cx| {
18324 editor.change_selections(None, window, cx, |s| {
18325 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18326 });
18327 });
18328
18329 assert_indent_guides(
18330 0..5,
18331 vec![indent_guide(buffer_id, 1, 3, 0)],
18332 Some(vec![0]),
18333 &mut cx,
18334 );
18335}
18336
18337#[gpui::test]
18338async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18339 let (buffer_id, mut cx) = setup_indent_guides_editor(
18340 &"
18341 def m:
18342 a = 1
18343 pass"
18344 .unindent(),
18345 cx,
18346 )
18347 .await;
18348
18349 cx.update_editor(|editor, window, cx| {
18350 editor.change_selections(None, window, cx, |s| {
18351 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18352 });
18353 });
18354
18355 assert_indent_guides(
18356 0..3,
18357 vec![indent_guide(buffer_id, 1, 2, 0)],
18358 Some(vec![0]),
18359 &mut cx,
18360 );
18361}
18362
18363#[gpui::test]
18364async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18365 init_test(cx, |_| {});
18366 let mut cx = EditorTestContext::new(cx).await;
18367 let text = indoc! {
18368 "
18369 impl A {
18370 fn b() {
18371 0;
18372 3;
18373 5;
18374 6;
18375 7;
18376 }
18377 }
18378 "
18379 };
18380 let base_text = indoc! {
18381 "
18382 impl A {
18383 fn b() {
18384 0;
18385 1;
18386 2;
18387 3;
18388 4;
18389 }
18390 fn c() {
18391 5;
18392 6;
18393 7;
18394 }
18395 }
18396 "
18397 };
18398
18399 cx.update_editor(|editor, window, cx| {
18400 editor.set_text(text, window, cx);
18401
18402 editor.buffer().update(cx, |multibuffer, cx| {
18403 let buffer = multibuffer.as_singleton().unwrap();
18404 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18405
18406 multibuffer.set_all_diff_hunks_expanded(cx);
18407 multibuffer.add_diff(diff, cx);
18408
18409 buffer.read(cx).remote_id()
18410 })
18411 });
18412 cx.run_until_parked();
18413
18414 cx.assert_state_with_diff(
18415 indoc! { "
18416 impl A {
18417 fn b() {
18418 0;
18419 - 1;
18420 - 2;
18421 3;
18422 - 4;
18423 - }
18424 - fn c() {
18425 5;
18426 6;
18427 7;
18428 }
18429 }
18430 ˇ"
18431 }
18432 .to_string(),
18433 );
18434
18435 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18436 editor
18437 .snapshot(window, cx)
18438 .buffer_snapshot
18439 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18440 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18441 .collect::<Vec<_>>()
18442 });
18443 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18444 assert_eq!(
18445 actual_guides,
18446 vec![
18447 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18448 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18449 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18450 ]
18451 );
18452}
18453
18454#[gpui::test]
18455async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18456 init_test(cx, |_| {});
18457 let mut cx = EditorTestContext::new(cx).await;
18458
18459 let diff_base = r#"
18460 a
18461 b
18462 c
18463 "#
18464 .unindent();
18465
18466 cx.set_state(
18467 &r#"
18468 ˇA
18469 b
18470 C
18471 "#
18472 .unindent(),
18473 );
18474 cx.set_head_text(&diff_base);
18475 cx.update_editor(|editor, window, cx| {
18476 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18477 });
18478 executor.run_until_parked();
18479
18480 let both_hunks_expanded = r#"
18481 - a
18482 + ˇA
18483 b
18484 - c
18485 + C
18486 "#
18487 .unindent();
18488
18489 cx.assert_state_with_diff(both_hunks_expanded.clone());
18490
18491 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18492 let snapshot = editor.snapshot(window, cx);
18493 let hunks = editor
18494 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18495 .collect::<Vec<_>>();
18496 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18497 let buffer_id = hunks[0].buffer_id;
18498 hunks
18499 .into_iter()
18500 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18501 .collect::<Vec<_>>()
18502 });
18503 assert_eq!(hunk_ranges.len(), 2);
18504
18505 cx.update_editor(|editor, _, cx| {
18506 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18507 });
18508 executor.run_until_parked();
18509
18510 let second_hunk_expanded = r#"
18511 ˇA
18512 b
18513 - c
18514 + C
18515 "#
18516 .unindent();
18517
18518 cx.assert_state_with_diff(second_hunk_expanded);
18519
18520 cx.update_editor(|editor, _, cx| {
18521 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18522 });
18523 executor.run_until_parked();
18524
18525 cx.assert_state_with_diff(both_hunks_expanded.clone());
18526
18527 cx.update_editor(|editor, _, cx| {
18528 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18529 });
18530 executor.run_until_parked();
18531
18532 let first_hunk_expanded = r#"
18533 - a
18534 + ˇA
18535 b
18536 C
18537 "#
18538 .unindent();
18539
18540 cx.assert_state_with_diff(first_hunk_expanded);
18541
18542 cx.update_editor(|editor, _, cx| {
18543 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18544 });
18545 executor.run_until_parked();
18546
18547 cx.assert_state_with_diff(both_hunks_expanded);
18548
18549 cx.set_state(
18550 &r#"
18551 ˇA
18552 b
18553 "#
18554 .unindent(),
18555 );
18556 cx.run_until_parked();
18557
18558 // TODO this cursor position seems bad
18559 cx.assert_state_with_diff(
18560 r#"
18561 - ˇa
18562 + A
18563 b
18564 "#
18565 .unindent(),
18566 );
18567
18568 cx.update_editor(|editor, window, cx| {
18569 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18570 });
18571
18572 cx.assert_state_with_diff(
18573 r#"
18574 - ˇa
18575 + A
18576 b
18577 - c
18578 "#
18579 .unindent(),
18580 );
18581
18582 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18583 let snapshot = editor.snapshot(window, cx);
18584 let hunks = editor
18585 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18586 .collect::<Vec<_>>();
18587 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18588 let buffer_id = hunks[0].buffer_id;
18589 hunks
18590 .into_iter()
18591 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18592 .collect::<Vec<_>>()
18593 });
18594 assert_eq!(hunk_ranges.len(), 2);
18595
18596 cx.update_editor(|editor, _, cx| {
18597 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18598 });
18599 executor.run_until_parked();
18600
18601 cx.assert_state_with_diff(
18602 r#"
18603 - ˇa
18604 + A
18605 b
18606 "#
18607 .unindent(),
18608 );
18609}
18610
18611#[gpui::test]
18612async fn test_toggle_deletion_hunk_at_start_of_file(
18613 executor: BackgroundExecutor,
18614 cx: &mut TestAppContext,
18615) {
18616 init_test(cx, |_| {});
18617 let mut cx = EditorTestContext::new(cx).await;
18618
18619 let diff_base = r#"
18620 a
18621 b
18622 c
18623 "#
18624 .unindent();
18625
18626 cx.set_state(
18627 &r#"
18628 ˇb
18629 c
18630 "#
18631 .unindent(),
18632 );
18633 cx.set_head_text(&diff_base);
18634 cx.update_editor(|editor, window, cx| {
18635 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18636 });
18637 executor.run_until_parked();
18638
18639 let hunk_expanded = r#"
18640 - a
18641 ˇb
18642 c
18643 "#
18644 .unindent();
18645
18646 cx.assert_state_with_diff(hunk_expanded.clone());
18647
18648 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18649 let snapshot = editor.snapshot(window, cx);
18650 let hunks = editor
18651 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18652 .collect::<Vec<_>>();
18653 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18654 let buffer_id = hunks[0].buffer_id;
18655 hunks
18656 .into_iter()
18657 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18658 .collect::<Vec<_>>()
18659 });
18660 assert_eq!(hunk_ranges.len(), 1);
18661
18662 cx.update_editor(|editor, _, cx| {
18663 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18664 });
18665 executor.run_until_parked();
18666
18667 let hunk_collapsed = r#"
18668 ˇb
18669 c
18670 "#
18671 .unindent();
18672
18673 cx.assert_state_with_diff(hunk_collapsed);
18674
18675 cx.update_editor(|editor, _, cx| {
18676 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18677 });
18678 executor.run_until_parked();
18679
18680 cx.assert_state_with_diff(hunk_expanded.clone());
18681}
18682
18683#[gpui::test]
18684async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18685 init_test(cx, |_| {});
18686
18687 let fs = FakeFs::new(cx.executor());
18688 fs.insert_tree(
18689 path!("/test"),
18690 json!({
18691 ".git": {},
18692 "file-1": "ONE\n",
18693 "file-2": "TWO\n",
18694 "file-3": "THREE\n",
18695 }),
18696 )
18697 .await;
18698
18699 fs.set_head_for_repo(
18700 path!("/test/.git").as_ref(),
18701 &[
18702 ("file-1".into(), "one\n".into()),
18703 ("file-2".into(), "two\n".into()),
18704 ("file-3".into(), "three\n".into()),
18705 ],
18706 "deadbeef",
18707 );
18708
18709 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18710 let mut buffers = vec![];
18711 for i in 1..=3 {
18712 let buffer = project
18713 .update(cx, |project, cx| {
18714 let path = format!(path!("/test/file-{}"), i);
18715 project.open_local_buffer(path, cx)
18716 })
18717 .await
18718 .unwrap();
18719 buffers.push(buffer);
18720 }
18721
18722 let multibuffer = cx.new(|cx| {
18723 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18724 multibuffer.set_all_diff_hunks_expanded(cx);
18725 for buffer in &buffers {
18726 let snapshot = buffer.read(cx).snapshot();
18727 multibuffer.set_excerpts_for_path(
18728 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18729 buffer.clone(),
18730 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18731 DEFAULT_MULTIBUFFER_CONTEXT,
18732 cx,
18733 );
18734 }
18735 multibuffer
18736 });
18737
18738 let editor = cx.add_window(|window, cx| {
18739 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18740 });
18741 cx.run_until_parked();
18742
18743 let snapshot = editor
18744 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18745 .unwrap();
18746 let hunks = snapshot
18747 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18748 .map(|hunk| match hunk {
18749 DisplayDiffHunk::Unfolded {
18750 display_row_range, ..
18751 } => display_row_range,
18752 DisplayDiffHunk::Folded { .. } => unreachable!(),
18753 })
18754 .collect::<Vec<_>>();
18755 assert_eq!(
18756 hunks,
18757 [
18758 DisplayRow(2)..DisplayRow(4),
18759 DisplayRow(7)..DisplayRow(9),
18760 DisplayRow(12)..DisplayRow(14),
18761 ]
18762 );
18763}
18764
18765#[gpui::test]
18766async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18767 init_test(cx, |_| {});
18768
18769 let mut cx = EditorTestContext::new(cx).await;
18770 cx.set_head_text(indoc! { "
18771 one
18772 two
18773 three
18774 four
18775 five
18776 "
18777 });
18778 cx.set_index_text(indoc! { "
18779 one
18780 two
18781 three
18782 four
18783 five
18784 "
18785 });
18786 cx.set_state(indoc! {"
18787 one
18788 TWO
18789 ˇTHREE
18790 FOUR
18791 five
18792 "});
18793 cx.run_until_parked();
18794 cx.update_editor(|editor, window, cx| {
18795 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18796 });
18797 cx.run_until_parked();
18798 cx.assert_index_text(Some(indoc! {"
18799 one
18800 TWO
18801 THREE
18802 FOUR
18803 five
18804 "}));
18805 cx.set_state(indoc! { "
18806 one
18807 TWO
18808 ˇTHREE-HUNDRED
18809 FOUR
18810 five
18811 "});
18812 cx.run_until_parked();
18813 cx.update_editor(|editor, window, cx| {
18814 let snapshot = editor.snapshot(window, cx);
18815 let hunks = editor
18816 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18817 .collect::<Vec<_>>();
18818 assert_eq!(hunks.len(), 1);
18819 assert_eq!(
18820 hunks[0].status(),
18821 DiffHunkStatus {
18822 kind: DiffHunkStatusKind::Modified,
18823 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18824 }
18825 );
18826
18827 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18828 });
18829 cx.run_until_parked();
18830 cx.assert_index_text(Some(indoc! {"
18831 one
18832 TWO
18833 THREE-HUNDRED
18834 FOUR
18835 five
18836 "}));
18837}
18838
18839#[gpui::test]
18840fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18841 init_test(cx, |_| {});
18842
18843 let editor = cx.add_window(|window, cx| {
18844 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18845 build_editor(buffer, window, cx)
18846 });
18847
18848 let render_args = Arc::new(Mutex::new(None));
18849 let snapshot = editor
18850 .update(cx, |editor, window, cx| {
18851 let snapshot = editor.buffer().read(cx).snapshot(cx);
18852 let range =
18853 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18854
18855 struct RenderArgs {
18856 row: MultiBufferRow,
18857 folded: bool,
18858 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18859 }
18860
18861 let crease = Crease::inline(
18862 range,
18863 FoldPlaceholder::test(),
18864 {
18865 let toggle_callback = render_args.clone();
18866 move |row, folded, callback, _window, _cx| {
18867 *toggle_callback.lock() = Some(RenderArgs {
18868 row,
18869 folded,
18870 callback,
18871 });
18872 div()
18873 }
18874 },
18875 |_row, _folded, _window, _cx| div(),
18876 );
18877
18878 editor.insert_creases(Some(crease), cx);
18879 let snapshot = editor.snapshot(window, cx);
18880 let _div = snapshot.render_crease_toggle(
18881 MultiBufferRow(1),
18882 false,
18883 cx.entity().clone(),
18884 window,
18885 cx,
18886 );
18887 snapshot
18888 })
18889 .unwrap();
18890
18891 let render_args = render_args.lock().take().unwrap();
18892 assert_eq!(render_args.row, MultiBufferRow(1));
18893 assert!(!render_args.folded);
18894 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18895
18896 cx.update_window(*editor, |_, window, cx| {
18897 (render_args.callback)(true, window, cx)
18898 })
18899 .unwrap();
18900 let snapshot = editor
18901 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18902 .unwrap();
18903 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18904
18905 cx.update_window(*editor, |_, window, cx| {
18906 (render_args.callback)(false, window, cx)
18907 })
18908 .unwrap();
18909 let snapshot = editor
18910 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18911 .unwrap();
18912 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18913}
18914
18915#[gpui::test]
18916async fn test_input_text(cx: &mut TestAppContext) {
18917 init_test(cx, |_| {});
18918 let mut cx = EditorTestContext::new(cx).await;
18919
18920 cx.set_state(
18921 &r#"ˇone
18922 two
18923
18924 three
18925 fourˇ
18926 five
18927
18928 siˇx"#
18929 .unindent(),
18930 );
18931
18932 cx.dispatch_action(HandleInput(String::new()));
18933 cx.assert_editor_state(
18934 &r#"ˇone
18935 two
18936
18937 three
18938 fourˇ
18939 five
18940
18941 siˇx"#
18942 .unindent(),
18943 );
18944
18945 cx.dispatch_action(HandleInput("AAAA".to_string()));
18946 cx.assert_editor_state(
18947 &r#"AAAAˇone
18948 two
18949
18950 three
18951 fourAAAAˇ
18952 five
18953
18954 siAAAAˇx"#
18955 .unindent(),
18956 );
18957}
18958
18959#[gpui::test]
18960async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
18961 init_test(cx, |_| {});
18962
18963 let mut cx = EditorTestContext::new(cx).await;
18964 cx.set_state(
18965 r#"let foo = 1;
18966let foo = 2;
18967let foo = 3;
18968let fooˇ = 4;
18969let foo = 5;
18970let foo = 6;
18971let foo = 7;
18972let foo = 8;
18973let foo = 9;
18974let foo = 10;
18975let foo = 11;
18976let foo = 12;
18977let foo = 13;
18978let foo = 14;
18979let foo = 15;"#,
18980 );
18981
18982 cx.update_editor(|e, window, cx| {
18983 assert_eq!(
18984 e.next_scroll_position,
18985 NextScrollCursorCenterTopBottom::Center,
18986 "Default next scroll direction is center",
18987 );
18988
18989 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18990 assert_eq!(
18991 e.next_scroll_position,
18992 NextScrollCursorCenterTopBottom::Top,
18993 "After center, next scroll direction should be top",
18994 );
18995
18996 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18997 assert_eq!(
18998 e.next_scroll_position,
18999 NextScrollCursorCenterTopBottom::Bottom,
19000 "After top, next scroll direction should be bottom",
19001 );
19002
19003 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19004 assert_eq!(
19005 e.next_scroll_position,
19006 NextScrollCursorCenterTopBottom::Center,
19007 "After bottom, scrolling should start over",
19008 );
19009
19010 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19011 assert_eq!(
19012 e.next_scroll_position,
19013 NextScrollCursorCenterTopBottom::Top,
19014 "Scrolling continues if retriggered fast enough"
19015 );
19016 });
19017
19018 cx.executor()
19019 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19020 cx.executor().run_until_parked();
19021 cx.update_editor(|e, _, _| {
19022 assert_eq!(
19023 e.next_scroll_position,
19024 NextScrollCursorCenterTopBottom::Center,
19025 "If scrolling is not triggered fast enough, it should reset"
19026 );
19027 });
19028}
19029
19030#[gpui::test]
19031async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19032 init_test(cx, |_| {});
19033 let mut cx = EditorLspTestContext::new_rust(
19034 lsp::ServerCapabilities {
19035 definition_provider: Some(lsp::OneOf::Left(true)),
19036 references_provider: Some(lsp::OneOf::Left(true)),
19037 ..lsp::ServerCapabilities::default()
19038 },
19039 cx,
19040 )
19041 .await;
19042
19043 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19044 let go_to_definition = cx
19045 .lsp
19046 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19047 move |params, _| async move {
19048 if empty_go_to_definition {
19049 Ok(None)
19050 } else {
19051 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19052 uri: params.text_document_position_params.text_document.uri,
19053 range: lsp::Range::new(
19054 lsp::Position::new(4, 3),
19055 lsp::Position::new(4, 6),
19056 ),
19057 })))
19058 }
19059 },
19060 );
19061 let references = cx
19062 .lsp
19063 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19064 Ok(Some(vec![lsp::Location {
19065 uri: params.text_document_position.text_document.uri,
19066 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19067 }]))
19068 });
19069 (go_to_definition, references)
19070 };
19071
19072 cx.set_state(
19073 &r#"fn one() {
19074 let mut a = ˇtwo();
19075 }
19076
19077 fn two() {}"#
19078 .unindent(),
19079 );
19080 set_up_lsp_handlers(false, &mut cx);
19081 let navigated = cx
19082 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19083 .await
19084 .expect("Failed to navigate to definition");
19085 assert_eq!(
19086 navigated,
19087 Navigated::Yes,
19088 "Should have navigated to definition from the GetDefinition response"
19089 );
19090 cx.assert_editor_state(
19091 &r#"fn one() {
19092 let mut a = two();
19093 }
19094
19095 fn «twoˇ»() {}"#
19096 .unindent(),
19097 );
19098
19099 let editors = cx.update_workspace(|workspace, _, cx| {
19100 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19101 });
19102 cx.update_editor(|_, _, test_editor_cx| {
19103 assert_eq!(
19104 editors.len(),
19105 1,
19106 "Initially, only one, test, editor should be open in the workspace"
19107 );
19108 assert_eq!(
19109 test_editor_cx.entity(),
19110 editors.last().expect("Asserted len is 1").clone()
19111 );
19112 });
19113
19114 set_up_lsp_handlers(true, &mut cx);
19115 let navigated = cx
19116 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19117 .await
19118 .expect("Failed to navigate to lookup references");
19119 assert_eq!(
19120 navigated,
19121 Navigated::Yes,
19122 "Should have navigated to references as a fallback after empty GoToDefinition response"
19123 );
19124 // We should not change the selections in the existing file,
19125 // if opening another milti buffer with the references
19126 cx.assert_editor_state(
19127 &r#"fn one() {
19128 let mut a = two();
19129 }
19130
19131 fn «twoˇ»() {}"#
19132 .unindent(),
19133 );
19134 let editors = cx.update_workspace(|workspace, _, cx| {
19135 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19136 });
19137 cx.update_editor(|_, _, test_editor_cx| {
19138 assert_eq!(
19139 editors.len(),
19140 2,
19141 "After falling back to references search, we open a new editor with the results"
19142 );
19143 let references_fallback_text = editors
19144 .into_iter()
19145 .find(|new_editor| *new_editor != test_editor_cx.entity())
19146 .expect("Should have one non-test editor now")
19147 .read(test_editor_cx)
19148 .text(test_editor_cx);
19149 assert_eq!(
19150 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19151 "Should use the range from the references response and not the GoToDefinition one"
19152 );
19153 });
19154}
19155
19156#[gpui::test]
19157async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19158 init_test(cx, |_| {});
19159 cx.update(|cx| {
19160 let mut editor_settings = EditorSettings::get_global(cx).clone();
19161 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19162 EditorSettings::override_global(editor_settings, cx);
19163 });
19164 let mut cx = EditorLspTestContext::new_rust(
19165 lsp::ServerCapabilities {
19166 definition_provider: Some(lsp::OneOf::Left(true)),
19167 references_provider: Some(lsp::OneOf::Left(true)),
19168 ..lsp::ServerCapabilities::default()
19169 },
19170 cx,
19171 )
19172 .await;
19173 let original_state = r#"fn one() {
19174 let mut a = ˇtwo();
19175 }
19176
19177 fn two() {}"#
19178 .unindent();
19179 cx.set_state(&original_state);
19180
19181 let mut go_to_definition = cx
19182 .lsp
19183 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19184 move |_, _| async move { Ok(None) },
19185 );
19186 let _references = cx
19187 .lsp
19188 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19189 panic!("Should not call for references with no go to definition fallback")
19190 });
19191
19192 let navigated = cx
19193 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19194 .await
19195 .expect("Failed to navigate to lookup references");
19196 go_to_definition
19197 .next()
19198 .await
19199 .expect("Should have called the go_to_definition handler");
19200
19201 assert_eq!(
19202 navigated,
19203 Navigated::No,
19204 "Should have navigated to references as a fallback after empty GoToDefinition response"
19205 );
19206 cx.assert_editor_state(&original_state);
19207 let editors = cx.update_workspace(|workspace, _, cx| {
19208 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19209 });
19210 cx.update_editor(|_, _, _| {
19211 assert_eq!(
19212 editors.len(),
19213 1,
19214 "After unsuccessful fallback, no other editor should have been opened"
19215 );
19216 });
19217}
19218
19219#[gpui::test]
19220async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19221 init_test(cx, |_| {});
19222
19223 let language = Arc::new(Language::new(
19224 LanguageConfig::default(),
19225 Some(tree_sitter_rust::LANGUAGE.into()),
19226 ));
19227
19228 let text = r#"
19229 #[cfg(test)]
19230 mod tests() {
19231 #[test]
19232 fn runnable_1() {
19233 let a = 1;
19234 }
19235
19236 #[test]
19237 fn runnable_2() {
19238 let a = 1;
19239 let b = 2;
19240 }
19241 }
19242 "#
19243 .unindent();
19244
19245 let fs = FakeFs::new(cx.executor());
19246 fs.insert_file("/file.rs", Default::default()).await;
19247
19248 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19249 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19250 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19251 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19252 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19253
19254 let editor = cx.new_window_entity(|window, cx| {
19255 Editor::new(
19256 EditorMode::full(),
19257 multi_buffer,
19258 Some(project.clone()),
19259 window,
19260 cx,
19261 )
19262 });
19263
19264 editor.update_in(cx, |editor, window, cx| {
19265 let snapshot = editor.buffer().read(cx).snapshot(cx);
19266 editor.tasks.insert(
19267 (buffer.read(cx).remote_id(), 3),
19268 RunnableTasks {
19269 templates: vec![],
19270 offset: snapshot.anchor_before(43),
19271 column: 0,
19272 extra_variables: HashMap::default(),
19273 context_range: BufferOffset(43)..BufferOffset(85),
19274 },
19275 );
19276 editor.tasks.insert(
19277 (buffer.read(cx).remote_id(), 8),
19278 RunnableTasks {
19279 templates: vec![],
19280 offset: snapshot.anchor_before(86),
19281 column: 0,
19282 extra_variables: HashMap::default(),
19283 context_range: BufferOffset(86)..BufferOffset(191),
19284 },
19285 );
19286
19287 // Test finding task when cursor is inside function body
19288 editor.change_selections(None, window, cx, |s| {
19289 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19290 });
19291 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19292 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19293
19294 // Test finding task when cursor is on function name
19295 editor.change_selections(None, window, cx, |s| {
19296 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19297 });
19298 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19299 assert_eq!(row, 8, "Should find task when cursor is on function name");
19300 });
19301}
19302
19303#[gpui::test]
19304async fn test_folding_buffers(cx: &mut TestAppContext) {
19305 init_test(cx, |_| {});
19306
19307 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19308 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19309 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19310
19311 let fs = FakeFs::new(cx.executor());
19312 fs.insert_tree(
19313 path!("/a"),
19314 json!({
19315 "first.rs": sample_text_1,
19316 "second.rs": sample_text_2,
19317 "third.rs": sample_text_3,
19318 }),
19319 )
19320 .await;
19321 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19322 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19323 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19324 let worktree = project.update(cx, |project, cx| {
19325 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19326 assert_eq!(worktrees.len(), 1);
19327 worktrees.pop().unwrap()
19328 });
19329 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19330
19331 let buffer_1 = project
19332 .update(cx, |project, cx| {
19333 project.open_buffer((worktree_id, "first.rs"), cx)
19334 })
19335 .await
19336 .unwrap();
19337 let buffer_2 = project
19338 .update(cx, |project, cx| {
19339 project.open_buffer((worktree_id, "second.rs"), cx)
19340 })
19341 .await
19342 .unwrap();
19343 let buffer_3 = project
19344 .update(cx, |project, cx| {
19345 project.open_buffer((worktree_id, "third.rs"), cx)
19346 })
19347 .await
19348 .unwrap();
19349
19350 let multi_buffer = cx.new(|cx| {
19351 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19352 multi_buffer.push_excerpts(
19353 buffer_1.clone(),
19354 [
19355 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19356 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19357 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19358 ],
19359 cx,
19360 );
19361 multi_buffer.push_excerpts(
19362 buffer_2.clone(),
19363 [
19364 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19365 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19366 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19367 ],
19368 cx,
19369 );
19370 multi_buffer.push_excerpts(
19371 buffer_3.clone(),
19372 [
19373 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19374 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19375 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19376 ],
19377 cx,
19378 );
19379 multi_buffer
19380 });
19381 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19382 Editor::new(
19383 EditorMode::full(),
19384 multi_buffer.clone(),
19385 Some(project.clone()),
19386 window,
19387 cx,
19388 )
19389 });
19390
19391 assert_eq!(
19392 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19393 "\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",
19394 );
19395
19396 multi_buffer_editor.update(cx, |editor, cx| {
19397 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19398 });
19399 assert_eq!(
19400 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19401 "\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",
19402 "After folding the first buffer, its text should not be displayed"
19403 );
19404
19405 multi_buffer_editor.update(cx, |editor, cx| {
19406 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19407 });
19408 assert_eq!(
19409 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19410 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19411 "After folding the second buffer, its text should not be displayed"
19412 );
19413
19414 multi_buffer_editor.update(cx, |editor, cx| {
19415 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19416 });
19417 assert_eq!(
19418 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19419 "\n\n\n\n\n",
19420 "After folding the third buffer, its text should not be displayed"
19421 );
19422
19423 // Emulate selection inside the fold logic, that should work
19424 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19425 editor
19426 .snapshot(window, cx)
19427 .next_line_boundary(Point::new(0, 4));
19428 });
19429
19430 multi_buffer_editor.update(cx, |editor, cx| {
19431 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19432 });
19433 assert_eq!(
19434 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19435 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19436 "After unfolding the second buffer, its text should be displayed"
19437 );
19438
19439 // Typing inside of buffer 1 causes that buffer to be unfolded.
19440 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19441 assert_eq!(
19442 multi_buffer
19443 .read(cx)
19444 .snapshot(cx)
19445 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19446 .collect::<String>(),
19447 "bbbb"
19448 );
19449 editor.change_selections(None, window, cx, |selections| {
19450 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19451 });
19452 editor.handle_input("B", window, cx);
19453 });
19454
19455 assert_eq!(
19456 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19457 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19458 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19459 );
19460
19461 multi_buffer_editor.update(cx, |editor, cx| {
19462 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19463 });
19464 assert_eq!(
19465 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19466 "\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",
19467 "After unfolding the all buffers, all original text should be displayed"
19468 );
19469}
19470
19471#[gpui::test]
19472async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19473 init_test(cx, |_| {});
19474
19475 let sample_text_1 = "1111\n2222\n3333".to_string();
19476 let sample_text_2 = "4444\n5555\n6666".to_string();
19477 let sample_text_3 = "7777\n8888\n9999".to_string();
19478
19479 let fs = FakeFs::new(cx.executor());
19480 fs.insert_tree(
19481 path!("/a"),
19482 json!({
19483 "first.rs": sample_text_1,
19484 "second.rs": sample_text_2,
19485 "third.rs": sample_text_3,
19486 }),
19487 )
19488 .await;
19489 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19490 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19491 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19492 let worktree = project.update(cx, |project, cx| {
19493 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19494 assert_eq!(worktrees.len(), 1);
19495 worktrees.pop().unwrap()
19496 });
19497 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19498
19499 let buffer_1 = project
19500 .update(cx, |project, cx| {
19501 project.open_buffer((worktree_id, "first.rs"), cx)
19502 })
19503 .await
19504 .unwrap();
19505 let buffer_2 = project
19506 .update(cx, |project, cx| {
19507 project.open_buffer((worktree_id, "second.rs"), cx)
19508 })
19509 .await
19510 .unwrap();
19511 let buffer_3 = project
19512 .update(cx, |project, cx| {
19513 project.open_buffer((worktree_id, "third.rs"), cx)
19514 })
19515 .await
19516 .unwrap();
19517
19518 let multi_buffer = cx.new(|cx| {
19519 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19520 multi_buffer.push_excerpts(
19521 buffer_1.clone(),
19522 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19523 cx,
19524 );
19525 multi_buffer.push_excerpts(
19526 buffer_2.clone(),
19527 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19528 cx,
19529 );
19530 multi_buffer.push_excerpts(
19531 buffer_3.clone(),
19532 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19533 cx,
19534 );
19535 multi_buffer
19536 });
19537
19538 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19539 Editor::new(
19540 EditorMode::full(),
19541 multi_buffer,
19542 Some(project.clone()),
19543 window,
19544 cx,
19545 )
19546 });
19547
19548 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19549 assert_eq!(
19550 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19551 full_text,
19552 );
19553
19554 multi_buffer_editor.update(cx, |editor, cx| {
19555 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19556 });
19557 assert_eq!(
19558 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19559 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19560 "After folding the first buffer, its text should not be displayed"
19561 );
19562
19563 multi_buffer_editor.update(cx, |editor, cx| {
19564 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19565 });
19566
19567 assert_eq!(
19568 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19569 "\n\n\n\n\n\n7777\n8888\n9999",
19570 "After folding the second buffer, its text should not be displayed"
19571 );
19572
19573 multi_buffer_editor.update(cx, |editor, cx| {
19574 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19575 });
19576 assert_eq!(
19577 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19578 "\n\n\n\n\n",
19579 "After folding the third buffer, its text should not be displayed"
19580 );
19581
19582 multi_buffer_editor.update(cx, |editor, cx| {
19583 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19584 });
19585 assert_eq!(
19586 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19587 "\n\n\n\n4444\n5555\n6666\n\n",
19588 "After unfolding the second buffer, its text should be displayed"
19589 );
19590
19591 multi_buffer_editor.update(cx, |editor, cx| {
19592 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19593 });
19594 assert_eq!(
19595 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19596 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19597 "After unfolding the first buffer, its text should be displayed"
19598 );
19599
19600 multi_buffer_editor.update(cx, |editor, cx| {
19601 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19602 });
19603 assert_eq!(
19604 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19605 full_text,
19606 "After unfolding all buffers, all original text should be displayed"
19607 );
19608}
19609
19610#[gpui::test]
19611async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19612 init_test(cx, |_| {});
19613
19614 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19615
19616 let fs = FakeFs::new(cx.executor());
19617 fs.insert_tree(
19618 path!("/a"),
19619 json!({
19620 "main.rs": sample_text,
19621 }),
19622 )
19623 .await;
19624 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19625 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19626 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19627 let worktree = project.update(cx, |project, cx| {
19628 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19629 assert_eq!(worktrees.len(), 1);
19630 worktrees.pop().unwrap()
19631 });
19632 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19633
19634 let buffer_1 = project
19635 .update(cx, |project, cx| {
19636 project.open_buffer((worktree_id, "main.rs"), cx)
19637 })
19638 .await
19639 .unwrap();
19640
19641 let multi_buffer = cx.new(|cx| {
19642 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19643 multi_buffer.push_excerpts(
19644 buffer_1.clone(),
19645 [ExcerptRange::new(
19646 Point::new(0, 0)
19647 ..Point::new(
19648 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19649 0,
19650 ),
19651 )],
19652 cx,
19653 );
19654 multi_buffer
19655 });
19656 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19657 Editor::new(
19658 EditorMode::full(),
19659 multi_buffer,
19660 Some(project.clone()),
19661 window,
19662 cx,
19663 )
19664 });
19665
19666 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19667 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19668 enum TestHighlight {}
19669 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19670 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19671 editor.highlight_text::<TestHighlight>(
19672 vec![highlight_range.clone()],
19673 HighlightStyle::color(Hsla::green()),
19674 cx,
19675 );
19676 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
19677 });
19678
19679 let full_text = format!("\n\n{sample_text}");
19680 assert_eq!(
19681 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19682 full_text,
19683 );
19684}
19685
19686#[gpui::test]
19687async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19688 init_test(cx, |_| {});
19689 cx.update(|cx| {
19690 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19691 "keymaps/default-linux.json",
19692 cx,
19693 )
19694 .unwrap();
19695 cx.bind_keys(default_key_bindings);
19696 });
19697
19698 let (editor, cx) = cx.add_window_view(|window, cx| {
19699 let multi_buffer = MultiBuffer::build_multi(
19700 [
19701 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19702 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19703 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19704 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19705 ],
19706 cx,
19707 );
19708 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19709
19710 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19711 // fold all but the second buffer, so that we test navigating between two
19712 // adjacent folded buffers, as well as folded buffers at the start and
19713 // end the multibuffer
19714 editor.fold_buffer(buffer_ids[0], cx);
19715 editor.fold_buffer(buffer_ids[2], cx);
19716 editor.fold_buffer(buffer_ids[3], cx);
19717
19718 editor
19719 });
19720 cx.simulate_resize(size(px(1000.), px(1000.)));
19721
19722 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19723 cx.assert_excerpts_with_selections(indoc! {"
19724 [EXCERPT]
19725 ˇ[FOLDED]
19726 [EXCERPT]
19727 a1
19728 b1
19729 [EXCERPT]
19730 [FOLDED]
19731 [EXCERPT]
19732 [FOLDED]
19733 "
19734 });
19735 cx.simulate_keystroke("down");
19736 cx.assert_excerpts_with_selections(indoc! {"
19737 [EXCERPT]
19738 [FOLDED]
19739 [EXCERPT]
19740 ˇa1
19741 b1
19742 [EXCERPT]
19743 [FOLDED]
19744 [EXCERPT]
19745 [FOLDED]
19746 "
19747 });
19748 cx.simulate_keystroke("down");
19749 cx.assert_excerpts_with_selections(indoc! {"
19750 [EXCERPT]
19751 [FOLDED]
19752 [EXCERPT]
19753 a1
19754 ˇb1
19755 [EXCERPT]
19756 [FOLDED]
19757 [EXCERPT]
19758 [FOLDED]
19759 "
19760 });
19761 cx.simulate_keystroke("down");
19762 cx.assert_excerpts_with_selections(indoc! {"
19763 [EXCERPT]
19764 [FOLDED]
19765 [EXCERPT]
19766 a1
19767 b1
19768 ˇ[EXCERPT]
19769 [FOLDED]
19770 [EXCERPT]
19771 [FOLDED]
19772 "
19773 });
19774 cx.simulate_keystroke("down");
19775 cx.assert_excerpts_with_selections(indoc! {"
19776 [EXCERPT]
19777 [FOLDED]
19778 [EXCERPT]
19779 a1
19780 b1
19781 [EXCERPT]
19782 ˇ[FOLDED]
19783 [EXCERPT]
19784 [FOLDED]
19785 "
19786 });
19787 for _ in 0..5 {
19788 cx.simulate_keystroke("down");
19789 cx.assert_excerpts_with_selections(indoc! {"
19790 [EXCERPT]
19791 [FOLDED]
19792 [EXCERPT]
19793 a1
19794 b1
19795 [EXCERPT]
19796 [FOLDED]
19797 [EXCERPT]
19798 ˇ[FOLDED]
19799 "
19800 });
19801 }
19802
19803 cx.simulate_keystroke("up");
19804 cx.assert_excerpts_with_selections(indoc! {"
19805 [EXCERPT]
19806 [FOLDED]
19807 [EXCERPT]
19808 a1
19809 b1
19810 [EXCERPT]
19811 ˇ[FOLDED]
19812 [EXCERPT]
19813 [FOLDED]
19814 "
19815 });
19816 cx.simulate_keystroke("up");
19817 cx.assert_excerpts_with_selections(indoc! {"
19818 [EXCERPT]
19819 [FOLDED]
19820 [EXCERPT]
19821 a1
19822 b1
19823 ˇ[EXCERPT]
19824 [FOLDED]
19825 [EXCERPT]
19826 [FOLDED]
19827 "
19828 });
19829 cx.simulate_keystroke("up");
19830 cx.assert_excerpts_with_selections(indoc! {"
19831 [EXCERPT]
19832 [FOLDED]
19833 [EXCERPT]
19834 a1
19835 ˇb1
19836 [EXCERPT]
19837 [FOLDED]
19838 [EXCERPT]
19839 [FOLDED]
19840 "
19841 });
19842 cx.simulate_keystroke("up");
19843 cx.assert_excerpts_with_selections(indoc! {"
19844 [EXCERPT]
19845 [FOLDED]
19846 [EXCERPT]
19847 ˇa1
19848 b1
19849 [EXCERPT]
19850 [FOLDED]
19851 [EXCERPT]
19852 [FOLDED]
19853 "
19854 });
19855 for _ in 0..5 {
19856 cx.simulate_keystroke("up");
19857 cx.assert_excerpts_with_selections(indoc! {"
19858 [EXCERPT]
19859 ˇ[FOLDED]
19860 [EXCERPT]
19861 a1
19862 b1
19863 [EXCERPT]
19864 [FOLDED]
19865 [EXCERPT]
19866 [FOLDED]
19867 "
19868 });
19869 }
19870}
19871
19872#[gpui::test]
19873async fn test_inline_completion_text(cx: &mut TestAppContext) {
19874 init_test(cx, |_| {});
19875
19876 // Simple insertion
19877 assert_highlighted_edits(
19878 "Hello, world!",
19879 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19880 true,
19881 cx,
19882 |highlighted_edits, cx| {
19883 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19884 assert_eq!(highlighted_edits.highlights.len(), 1);
19885 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19886 assert_eq!(
19887 highlighted_edits.highlights[0].1.background_color,
19888 Some(cx.theme().status().created_background)
19889 );
19890 },
19891 )
19892 .await;
19893
19894 // Replacement
19895 assert_highlighted_edits(
19896 "This is a test.",
19897 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19898 false,
19899 cx,
19900 |highlighted_edits, cx| {
19901 assert_eq!(highlighted_edits.text, "That is a test.");
19902 assert_eq!(highlighted_edits.highlights.len(), 1);
19903 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19904 assert_eq!(
19905 highlighted_edits.highlights[0].1.background_color,
19906 Some(cx.theme().status().created_background)
19907 );
19908 },
19909 )
19910 .await;
19911
19912 // Multiple edits
19913 assert_highlighted_edits(
19914 "Hello, world!",
19915 vec![
19916 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19917 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19918 ],
19919 false,
19920 cx,
19921 |highlighted_edits, cx| {
19922 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19923 assert_eq!(highlighted_edits.highlights.len(), 2);
19924 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19925 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
19926 assert_eq!(
19927 highlighted_edits.highlights[0].1.background_color,
19928 Some(cx.theme().status().created_background)
19929 );
19930 assert_eq!(
19931 highlighted_edits.highlights[1].1.background_color,
19932 Some(cx.theme().status().created_background)
19933 );
19934 },
19935 )
19936 .await;
19937
19938 // Multiple lines with edits
19939 assert_highlighted_edits(
19940 "First line\nSecond line\nThird line\nFourth line",
19941 vec![
19942 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
19943 (
19944 Point::new(2, 0)..Point::new(2, 10),
19945 "New third line".to_string(),
19946 ),
19947 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
19948 ],
19949 false,
19950 cx,
19951 |highlighted_edits, cx| {
19952 assert_eq!(
19953 highlighted_edits.text,
19954 "Second modified\nNew third line\nFourth updated line"
19955 );
19956 assert_eq!(highlighted_edits.highlights.len(), 3);
19957 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
19958 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
19959 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
19960 for highlight in &highlighted_edits.highlights {
19961 assert_eq!(
19962 highlight.1.background_color,
19963 Some(cx.theme().status().created_background)
19964 );
19965 }
19966 },
19967 )
19968 .await;
19969}
19970
19971#[gpui::test]
19972async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
19973 init_test(cx, |_| {});
19974
19975 // Deletion
19976 assert_highlighted_edits(
19977 "Hello, world!",
19978 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
19979 true,
19980 cx,
19981 |highlighted_edits, cx| {
19982 assert_eq!(highlighted_edits.text, "Hello, world!");
19983 assert_eq!(highlighted_edits.highlights.len(), 1);
19984 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
19985 assert_eq!(
19986 highlighted_edits.highlights[0].1.background_color,
19987 Some(cx.theme().status().deleted_background)
19988 );
19989 },
19990 )
19991 .await;
19992
19993 // Insertion
19994 assert_highlighted_edits(
19995 "Hello, world!",
19996 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19997 true,
19998 cx,
19999 |highlighted_edits, cx| {
20000 assert_eq!(highlighted_edits.highlights.len(), 1);
20001 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20002 assert_eq!(
20003 highlighted_edits.highlights[0].1.background_color,
20004 Some(cx.theme().status().created_background)
20005 );
20006 },
20007 )
20008 .await;
20009}
20010
20011async fn assert_highlighted_edits(
20012 text: &str,
20013 edits: Vec<(Range<Point>, String)>,
20014 include_deletions: bool,
20015 cx: &mut TestAppContext,
20016 assertion_fn: impl Fn(HighlightedText, &App),
20017) {
20018 let window = cx.add_window(|window, cx| {
20019 let buffer = MultiBuffer::build_simple(text, cx);
20020 Editor::new(EditorMode::full(), buffer, None, window, cx)
20021 });
20022 let cx = &mut VisualTestContext::from_window(*window, cx);
20023
20024 let (buffer, snapshot) = window
20025 .update(cx, |editor, _window, cx| {
20026 (
20027 editor.buffer().clone(),
20028 editor.buffer().read(cx).snapshot(cx),
20029 )
20030 })
20031 .unwrap();
20032
20033 let edits = edits
20034 .into_iter()
20035 .map(|(range, edit)| {
20036 (
20037 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20038 edit,
20039 )
20040 })
20041 .collect::<Vec<_>>();
20042
20043 let text_anchor_edits = edits
20044 .clone()
20045 .into_iter()
20046 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20047 .collect::<Vec<_>>();
20048
20049 let edit_preview = window
20050 .update(cx, |_, _window, cx| {
20051 buffer
20052 .read(cx)
20053 .as_singleton()
20054 .unwrap()
20055 .read(cx)
20056 .preview_edits(text_anchor_edits.into(), cx)
20057 })
20058 .unwrap()
20059 .await;
20060
20061 cx.update(|_window, cx| {
20062 let highlighted_edits = inline_completion_edit_text(
20063 &snapshot.as_singleton().unwrap().2,
20064 &edits,
20065 &edit_preview,
20066 include_deletions,
20067 cx,
20068 );
20069 assertion_fn(highlighted_edits, cx)
20070 });
20071}
20072
20073#[track_caller]
20074fn assert_breakpoint(
20075 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20076 path: &Arc<Path>,
20077 expected: Vec<(u32, Breakpoint)>,
20078) {
20079 if expected.len() == 0usize {
20080 assert!(!breakpoints.contains_key(path), "{}", path.display());
20081 } else {
20082 let mut breakpoint = breakpoints
20083 .get(path)
20084 .unwrap()
20085 .into_iter()
20086 .map(|breakpoint| {
20087 (
20088 breakpoint.row,
20089 Breakpoint {
20090 message: breakpoint.message.clone(),
20091 state: breakpoint.state,
20092 condition: breakpoint.condition.clone(),
20093 hit_condition: breakpoint.hit_condition.clone(),
20094 },
20095 )
20096 })
20097 .collect::<Vec<_>>();
20098
20099 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20100
20101 assert_eq!(expected, breakpoint);
20102 }
20103}
20104
20105fn add_log_breakpoint_at_cursor(
20106 editor: &mut Editor,
20107 log_message: &str,
20108 window: &mut Window,
20109 cx: &mut Context<Editor>,
20110) {
20111 let (anchor, bp) = editor
20112 .breakpoints_at_cursors(window, cx)
20113 .first()
20114 .and_then(|(anchor, bp)| {
20115 if let Some(bp) = bp {
20116 Some((*anchor, bp.clone()))
20117 } else {
20118 None
20119 }
20120 })
20121 .unwrap_or_else(|| {
20122 let cursor_position: Point = editor.selections.newest(cx).head();
20123
20124 let breakpoint_position = editor
20125 .snapshot(window, cx)
20126 .display_snapshot
20127 .buffer_snapshot
20128 .anchor_before(Point::new(cursor_position.row, 0));
20129
20130 (breakpoint_position, Breakpoint::new_log(&log_message))
20131 });
20132
20133 editor.edit_breakpoint_at_anchor(
20134 anchor,
20135 bp,
20136 BreakpointEditAction::EditLogMessage(log_message.into()),
20137 cx,
20138 );
20139}
20140
20141#[gpui::test]
20142async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20143 init_test(cx, |_| {});
20144
20145 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20146 let fs = FakeFs::new(cx.executor());
20147 fs.insert_tree(
20148 path!("/a"),
20149 json!({
20150 "main.rs": sample_text,
20151 }),
20152 )
20153 .await;
20154 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20155 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20156 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20157
20158 let fs = FakeFs::new(cx.executor());
20159 fs.insert_tree(
20160 path!("/a"),
20161 json!({
20162 "main.rs": sample_text,
20163 }),
20164 )
20165 .await;
20166 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20167 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20168 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20169 let worktree_id = workspace
20170 .update(cx, |workspace, _window, cx| {
20171 workspace.project().update(cx, |project, cx| {
20172 project.worktrees(cx).next().unwrap().read(cx).id()
20173 })
20174 })
20175 .unwrap();
20176
20177 let buffer = project
20178 .update(cx, |project, cx| {
20179 project.open_buffer((worktree_id, "main.rs"), cx)
20180 })
20181 .await
20182 .unwrap();
20183
20184 let (editor, cx) = cx.add_window_view(|window, cx| {
20185 Editor::new(
20186 EditorMode::full(),
20187 MultiBuffer::build_from_buffer(buffer, cx),
20188 Some(project.clone()),
20189 window,
20190 cx,
20191 )
20192 });
20193
20194 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20195 let abs_path = project.read_with(cx, |project, cx| {
20196 project
20197 .absolute_path(&project_path, cx)
20198 .map(|path_buf| Arc::from(path_buf.to_owned()))
20199 .unwrap()
20200 });
20201
20202 // assert we can add breakpoint on the first line
20203 editor.update_in(cx, |editor, window, cx| {
20204 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20205 editor.move_to_end(&MoveToEnd, window, cx);
20206 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20207 });
20208
20209 let breakpoints = editor.update(cx, |editor, cx| {
20210 editor
20211 .breakpoint_store()
20212 .as_ref()
20213 .unwrap()
20214 .read(cx)
20215 .all_source_breakpoints(cx)
20216 .clone()
20217 });
20218
20219 assert_eq!(1, breakpoints.len());
20220 assert_breakpoint(
20221 &breakpoints,
20222 &abs_path,
20223 vec![
20224 (0, Breakpoint::new_standard()),
20225 (3, Breakpoint::new_standard()),
20226 ],
20227 );
20228
20229 editor.update_in(cx, |editor, window, cx| {
20230 editor.move_to_beginning(&MoveToBeginning, window, cx);
20231 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20232 });
20233
20234 let breakpoints = editor.update(cx, |editor, cx| {
20235 editor
20236 .breakpoint_store()
20237 .as_ref()
20238 .unwrap()
20239 .read(cx)
20240 .all_source_breakpoints(cx)
20241 .clone()
20242 });
20243
20244 assert_eq!(1, breakpoints.len());
20245 assert_breakpoint(
20246 &breakpoints,
20247 &abs_path,
20248 vec![(3, Breakpoint::new_standard())],
20249 );
20250
20251 editor.update_in(cx, |editor, window, cx| {
20252 editor.move_to_end(&MoveToEnd, window, cx);
20253 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20254 });
20255
20256 let breakpoints = editor.update(cx, |editor, cx| {
20257 editor
20258 .breakpoint_store()
20259 .as_ref()
20260 .unwrap()
20261 .read(cx)
20262 .all_source_breakpoints(cx)
20263 .clone()
20264 });
20265
20266 assert_eq!(0, breakpoints.len());
20267 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20268}
20269
20270#[gpui::test]
20271async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20272 init_test(cx, |_| {});
20273
20274 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20275
20276 let fs = FakeFs::new(cx.executor());
20277 fs.insert_tree(
20278 path!("/a"),
20279 json!({
20280 "main.rs": sample_text,
20281 }),
20282 )
20283 .await;
20284 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20285 let (workspace, cx) =
20286 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20287
20288 let worktree_id = workspace.update(cx, |workspace, cx| {
20289 workspace.project().update(cx, |project, cx| {
20290 project.worktrees(cx).next().unwrap().read(cx).id()
20291 })
20292 });
20293
20294 let buffer = project
20295 .update(cx, |project, cx| {
20296 project.open_buffer((worktree_id, "main.rs"), cx)
20297 })
20298 .await
20299 .unwrap();
20300
20301 let (editor, cx) = cx.add_window_view(|window, cx| {
20302 Editor::new(
20303 EditorMode::full(),
20304 MultiBuffer::build_from_buffer(buffer, cx),
20305 Some(project.clone()),
20306 window,
20307 cx,
20308 )
20309 });
20310
20311 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20312 let abs_path = project.read_with(cx, |project, cx| {
20313 project
20314 .absolute_path(&project_path, cx)
20315 .map(|path_buf| Arc::from(path_buf.to_owned()))
20316 .unwrap()
20317 });
20318
20319 editor.update_in(cx, |editor, window, cx| {
20320 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20321 });
20322
20323 let breakpoints = editor.update(cx, |editor, cx| {
20324 editor
20325 .breakpoint_store()
20326 .as_ref()
20327 .unwrap()
20328 .read(cx)
20329 .all_source_breakpoints(cx)
20330 .clone()
20331 });
20332
20333 assert_breakpoint(
20334 &breakpoints,
20335 &abs_path,
20336 vec![(0, Breakpoint::new_log("hello world"))],
20337 );
20338
20339 // Removing a log message from a log breakpoint should remove it
20340 editor.update_in(cx, |editor, window, cx| {
20341 add_log_breakpoint_at_cursor(editor, "", window, cx);
20342 });
20343
20344 let breakpoints = editor.update(cx, |editor, cx| {
20345 editor
20346 .breakpoint_store()
20347 .as_ref()
20348 .unwrap()
20349 .read(cx)
20350 .all_source_breakpoints(cx)
20351 .clone()
20352 });
20353
20354 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20355
20356 editor.update_in(cx, |editor, window, cx| {
20357 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20358 editor.move_to_end(&MoveToEnd, window, cx);
20359 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20360 // Not adding a log message to a standard breakpoint shouldn't remove it
20361 add_log_breakpoint_at_cursor(editor, "", window, cx);
20362 });
20363
20364 let breakpoints = editor.update(cx, |editor, cx| {
20365 editor
20366 .breakpoint_store()
20367 .as_ref()
20368 .unwrap()
20369 .read(cx)
20370 .all_source_breakpoints(cx)
20371 .clone()
20372 });
20373
20374 assert_breakpoint(
20375 &breakpoints,
20376 &abs_path,
20377 vec![
20378 (0, Breakpoint::new_standard()),
20379 (3, Breakpoint::new_standard()),
20380 ],
20381 );
20382
20383 editor.update_in(cx, |editor, window, cx| {
20384 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20385 });
20386
20387 let breakpoints = editor.update(cx, |editor, cx| {
20388 editor
20389 .breakpoint_store()
20390 .as_ref()
20391 .unwrap()
20392 .read(cx)
20393 .all_source_breakpoints(cx)
20394 .clone()
20395 });
20396
20397 assert_breakpoint(
20398 &breakpoints,
20399 &abs_path,
20400 vec![
20401 (0, Breakpoint::new_standard()),
20402 (3, Breakpoint::new_log("hello world")),
20403 ],
20404 );
20405
20406 editor.update_in(cx, |editor, window, cx| {
20407 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20408 });
20409
20410 let breakpoints = editor.update(cx, |editor, cx| {
20411 editor
20412 .breakpoint_store()
20413 .as_ref()
20414 .unwrap()
20415 .read(cx)
20416 .all_source_breakpoints(cx)
20417 .clone()
20418 });
20419
20420 assert_breakpoint(
20421 &breakpoints,
20422 &abs_path,
20423 vec![
20424 (0, Breakpoint::new_standard()),
20425 (3, Breakpoint::new_log("hello Earth!!")),
20426 ],
20427 );
20428}
20429
20430/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20431/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20432/// or when breakpoints were placed out of order. This tests for a regression too
20433#[gpui::test]
20434async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20435 init_test(cx, |_| {});
20436
20437 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20438 let fs = FakeFs::new(cx.executor());
20439 fs.insert_tree(
20440 path!("/a"),
20441 json!({
20442 "main.rs": sample_text,
20443 }),
20444 )
20445 .await;
20446 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20447 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20448 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20449
20450 let fs = FakeFs::new(cx.executor());
20451 fs.insert_tree(
20452 path!("/a"),
20453 json!({
20454 "main.rs": sample_text,
20455 }),
20456 )
20457 .await;
20458 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20459 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20460 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20461 let worktree_id = workspace
20462 .update(cx, |workspace, _window, cx| {
20463 workspace.project().update(cx, |project, cx| {
20464 project.worktrees(cx).next().unwrap().read(cx).id()
20465 })
20466 })
20467 .unwrap();
20468
20469 let buffer = project
20470 .update(cx, |project, cx| {
20471 project.open_buffer((worktree_id, "main.rs"), cx)
20472 })
20473 .await
20474 .unwrap();
20475
20476 let (editor, cx) = cx.add_window_view(|window, cx| {
20477 Editor::new(
20478 EditorMode::full(),
20479 MultiBuffer::build_from_buffer(buffer, cx),
20480 Some(project.clone()),
20481 window,
20482 cx,
20483 )
20484 });
20485
20486 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20487 let abs_path = project.read_with(cx, |project, cx| {
20488 project
20489 .absolute_path(&project_path, cx)
20490 .map(|path_buf| Arc::from(path_buf.to_owned()))
20491 .unwrap()
20492 });
20493
20494 // assert we can add breakpoint on the first line
20495 editor.update_in(cx, |editor, window, cx| {
20496 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20497 editor.move_to_end(&MoveToEnd, window, cx);
20498 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20499 editor.move_up(&MoveUp, window, cx);
20500 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20501 });
20502
20503 let breakpoints = editor.update(cx, |editor, cx| {
20504 editor
20505 .breakpoint_store()
20506 .as_ref()
20507 .unwrap()
20508 .read(cx)
20509 .all_source_breakpoints(cx)
20510 .clone()
20511 });
20512
20513 assert_eq!(1, breakpoints.len());
20514 assert_breakpoint(
20515 &breakpoints,
20516 &abs_path,
20517 vec![
20518 (0, Breakpoint::new_standard()),
20519 (2, Breakpoint::new_standard()),
20520 (3, Breakpoint::new_standard()),
20521 ],
20522 );
20523
20524 editor.update_in(cx, |editor, window, cx| {
20525 editor.move_to_beginning(&MoveToBeginning, window, cx);
20526 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20527 editor.move_to_end(&MoveToEnd, window, cx);
20528 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20529 // Disabling a breakpoint that doesn't exist should do nothing
20530 editor.move_up(&MoveUp, window, cx);
20531 editor.move_up(&MoveUp, window, cx);
20532 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20533 });
20534
20535 let breakpoints = editor.update(cx, |editor, cx| {
20536 editor
20537 .breakpoint_store()
20538 .as_ref()
20539 .unwrap()
20540 .read(cx)
20541 .all_source_breakpoints(cx)
20542 .clone()
20543 });
20544
20545 let disable_breakpoint = {
20546 let mut bp = Breakpoint::new_standard();
20547 bp.state = BreakpointState::Disabled;
20548 bp
20549 };
20550
20551 assert_eq!(1, breakpoints.len());
20552 assert_breakpoint(
20553 &breakpoints,
20554 &abs_path,
20555 vec![
20556 (0, disable_breakpoint.clone()),
20557 (2, Breakpoint::new_standard()),
20558 (3, disable_breakpoint.clone()),
20559 ],
20560 );
20561
20562 editor.update_in(cx, |editor, window, cx| {
20563 editor.move_to_beginning(&MoveToBeginning, window, cx);
20564 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20565 editor.move_to_end(&MoveToEnd, window, cx);
20566 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20567 editor.move_up(&MoveUp, window, cx);
20568 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20569 });
20570
20571 let breakpoints = editor.update(cx, |editor, cx| {
20572 editor
20573 .breakpoint_store()
20574 .as_ref()
20575 .unwrap()
20576 .read(cx)
20577 .all_source_breakpoints(cx)
20578 .clone()
20579 });
20580
20581 assert_eq!(1, breakpoints.len());
20582 assert_breakpoint(
20583 &breakpoints,
20584 &abs_path,
20585 vec![
20586 (0, Breakpoint::new_standard()),
20587 (2, disable_breakpoint),
20588 (3, Breakpoint::new_standard()),
20589 ],
20590 );
20591}
20592
20593#[gpui::test]
20594async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20595 init_test(cx, |_| {});
20596 let capabilities = lsp::ServerCapabilities {
20597 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20598 prepare_provider: Some(true),
20599 work_done_progress_options: Default::default(),
20600 })),
20601 ..Default::default()
20602 };
20603 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20604
20605 cx.set_state(indoc! {"
20606 struct Fˇoo {}
20607 "});
20608
20609 cx.update_editor(|editor, _, cx| {
20610 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20611 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20612 editor.highlight_background::<DocumentHighlightRead>(
20613 &[highlight_range],
20614 |theme| theme.colors().editor_document_highlight_read_background,
20615 cx,
20616 );
20617 });
20618
20619 let mut prepare_rename_handler = cx
20620 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20621 move |_, _, _| async move {
20622 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20623 start: lsp::Position {
20624 line: 0,
20625 character: 7,
20626 },
20627 end: lsp::Position {
20628 line: 0,
20629 character: 10,
20630 },
20631 })))
20632 },
20633 );
20634 let prepare_rename_task = cx
20635 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20636 .expect("Prepare rename was not started");
20637 prepare_rename_handler.next().await.unwrap();
20638 prepare_rename_task.await.expect("Prepare rename failed");
20639
20640 let mut rename_handler =
20641 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20642 let edit = lsp::TextEdit {
20643 range: lsp::Range {
20644 start: lsp::Position {
20645 line: 0,
20646 character: 7,
20647 },
20648 end: lsp::Position {
20649 line: 0,
20650 character: 10,
20651 },
20652 },
20653 new_text: "FooRenamed".to_string(),
20654 };
20655 Ok(Some(lsp::WorkspaceEdit::new(
20656 // Specify the same edit twice
20657 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20658 )))
20659 });
20660 let rename_task = cx
20661 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20662 .expect("Confirm rename was not started");
20663 rename_handler.next().await.unwrap();
20664 rename_task.await.expect("Confirm rename failed");
20665 cx.run_until_parked();
20666
20667 // Despite two edits, only one is actually applied as those are identical
20668 cx.assert_editor_state(indoc! {"
20669 struct FooRenamedˇ {}
20670 "});
20671}
20672
20673#[gpui::test]
20674async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20675 init_test(cx, |_| {});
20676 // These capabilities indicate that the server does not support prepare rename.
20677 let capabilities = lsp::ServerCapabilities {
20678 rename_provider: Some(lsp::OneOf::Left(true)),
20679 ..Default::default()
20680 };
20681 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20682
20683 cx.set_state(indoc! {"
20684 struct Fˇoo {}
20685 "});
20686
20687 cx.update_editor(|editor, _window, cx| {
20688 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20689 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20690 editor.highlight_background::<DocumentHighlightRead>(
20691 &[highlight_range],
20692 |theme| theme.colors().editor_document_highlight_read_background,
20693 cx,
20694 );
20695 });
20696
20697 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20698 .expect("Prepare rename was not started")
20699 .await
20700 .expect("Prepare rename failed");
20701
20702 let mut rename_handler =
20703 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20704 let edit = lsp::TextEdit {
20705 range: lsp::Range {
20706 start: lsp::Position {
20707 line: 0,
20708 character: 7,
20709 },
20710 end: lsp::Position {
20711 line: 0,
20712 character: 10,
20713 },
20714 },
20715 new_text: "FooRenamed".to_string(),
20716 };
20717 Ok(Some(lsp::WorkspaceEdit::new(
20718 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20719 )))
20720 });
20721 let rename_task = cx
20722 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20723 .expect("Confirm rename was not started");
20724 rename_handler.next().await.unwrap();
20725 rename_task.await.expect("Confirm rename failed");
20726 cx.run_until_parked();
20727
20728 // Correct range is renamed, as `surrounding_word` is used to find it.
20729 cx.assert_editor_state(indoc! {"
20730 struct FooRenamedˇ {}
20731 "});
20732}
20733
20734#[gpui::test]
20735async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20736 init_test(cx, |_| {});
20737 let mut cx = EditorTestContext::new(cx).await;
20738
20739 let language = Arc::new(
20740 Language::new(
20741 LanguageConfig::default(),
20742 Some(tree_sitter_html::LANGUAGE.into()),
20743 )
20744 .with_brackets_query(
20745 r#"
20746 ("<" @open "/>" @close)
20747 ("</" @open ">" @close)
20748 ("<" @open ">" @close)
20749 ("\"" @open "\"" @close)
20750 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20751 "#,
20752 )
20753 .unwrap(),
20754 );
20755 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20756
20757 cx.set_state(indoc! {"
20758 <span>ˇ</span>
20759 "});
20760 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20761 cx.assert_editor_state(indoc! {"
20762 <span>
20763 ˇ
20764 </span>
20765 "});
20766
20767 cx.set_state(indoc! {"
20768 <span><span></span>ˇ</span>
20769 "});
20770 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20771 cx.assert_editor_state(indoc! {"
20772 <span><span></span>
20773 ˇ</span>
20774 "});
20775
20776 cx.set_state(indoc! {"
20777 <span>ˇ
20778 </span>
20779 "});
20780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20781 cx.assert_editor_state(indoc! {"
20782 <span>
20783 ˇ
20784 </span>
20785 "});
20786}
20787
20788#[gpui::test(iterations = 10)]
20789async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20790 init_test(cx, |_| {});
20791
20792 let fs = FakeFs::new(cx.executor());
20793 fs.insert_tree(
20794 path!("/dir"),
20795 json!({
20796 "a.ts": "a",
20797 }),
20798 )
20799 .await;
20800
20801 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20802 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20803 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20804
20805 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20806 language_registry.add(Arc::new(Language::new(
20807 LanguageConfig {
20808 name: "TypeScript".into(),
20809 matcher: LanguageMatcher {
20810 path_suffixes: vec!["ts".to_string()],
20811 ..Default::default()
20812 },
20813 ..Default::default()
20814 },
20815 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20816 )));
20817 let mut fake_language_servers = language_registry.register_fake_lsp(
20818 "TypeScript",
20819 FakeLspAdapter {
20820 capabilities: lsp::ServerCapabilities {
20821 code_lens_provider: Some(lsp::CodeLensOptions {
20822 resolve_provider: Some(true),
20823 }),
20824 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20825 commands: vec!["_the/command".to_string()],
20826 ..lsp::ExecuteCommandOptions::default()
20827 }),
20828 ..lsp::ServerCapabilities::default()
20829 },
20830 ..FakeLspAdapter::default()
20831 },
20832 );
20833
20834 let (buffer, _handle) = project
20835 .update(cx, |p, cx| {
20836 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20837 })
20838 .await
20839 .unwrap();
20840 cx.executor().run_until_parked();
20841
20842 let fake_server = fake_language_servers.next().await.unwrap();
20843
20844 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20845 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20846 drop(buffer_snapshot);
20847 let actions = cx
20848 .update_window(*workspace, |_, window, cx| {
20849 project.code_actions(&buffer, anchor..anchor, window, cx)
20850 })
20851 .unwrap();
20852
20853 fake_server
20854 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20855 Ok(Some(vec![
20856 lsp::CodeLens {
20857 range: lsp::Range::default(),
20858 command: Some(lsp::Command {
20859 title: "Code lens command".to_owned(),
20860 command: "_the/command".to_owned(),
20861 arguments: None,
20862 }),
20863 data: None,
20864 },
20865 lsp::CodeLens {
20866 range: lsp::Range::default(),
20867 command: Some(lsp::Command {
20868 title: "Command not in capabilities".to_owned(),
20869 command: "not in capabilities".to_owned(),
20870 arguments: None,
20871 }),
20872 data: None,
20873 },
20874 lsp::CodeLens {
20875 range: lsp::Range {
20876 start: lsp::Position {
20877 line: 1,
20878 character: 1,
20879 },
20880 end: lsp::Position {
20881 line: 1,
20882 character: 1,
20883 },
20884 },
20885 command: Some(lsp::Command {
20886 title: "Command not in range".to_owned(),
20887 command: "_the/command".to_owned(),
20888 arguments: None,
20889 }),
20890 data: None,
20891 },
20892 ]))
20893 })
20894 .next()
20895 .await;
20896
20897 let actions = actions.await.unwrap();
20898 assert_eq!(
20899 actions.len(),
20900 1,
20901 "Should have only one valid action for the 0..0 range"
20902 );
20903 let action = actions[0].clone();
20904 let apply = project.update(cx, |project, cx| {
20905 project.apply_code_action(buffer.clone(), action, true, cx)
20906 });
20907
20908 // Resolving the code action does not populate its edits. In absence of
20909 // edits, we must execute the given command.
20910 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20911 |mut lens, _| async move {
20912 let lens_command = lens.command.as_mut().expect("should have a command");
20913 assert_eq!(lens_command.title, "Code lens command");
20914 lens_command.arguments = Some(vec![json!("the-argument")]);
20915 Ok(lens)
20916 },
20917 );
20918
20919 // While executing the command, the language server sends the editor
20920 // a `workspaceEdit` request.
20921 fake_server
20922 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20923 let fake = fake_server.clone();
20924 move |params, _| {
20925 assert_eq!(params.command, "_the/command");
20926 let fake = fake.clone();
20927 async move {
20928 fake.server
20929 .request::<lsp::request::ApplyWorkspaceEdit>(
20930 lsp::ApplyWorkspaceEditParams {
20931 label: None,
20932 edit: lsp::WorkspaceEdit {
20933 changes: Some(
20934 [(
20935 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
20936 vec![lsp::TextEdit {
20937 range: lsp::Range::new(
20938 lsp::Position::new(0, 0),
20939 lsp::Position::new(0, 0),
20940 ),
20941 new_text: "X".into(),
20942 }],
20943 )]
20944 .into_iter()
20945 .collect(),
20946 ),
20947 ..Default::default()
20948 },
20949 },
20950 )
20951 .await
20952 .into_response()
20953 .unwrap();
20954 Ok(Some(json!(null)))
20955 }
20956 }
20957 })
20958 .next()
20959 .await;
20960
20961 // Applying the code lens command returns a project transaction containing the edits
20962 // sent by the language server in its `workspaceEdit` request.
20963 let transaction = apply.await.unwrap();
20964 assert!(transaction.0.contains_key(&buffer));
20965 buffer.update(cx, |buffer, cx| {
20966 assert_eq!(buffer.text(), "Xa");
20967 buffer.undo(cx);
20968 assert_eq!(buffer.text(), "a");
20969 });
20970}
20971
20972#[gpui::test]
20973async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
20974 init_test(cx, |_| {});
20975
20976 let fs = FakeFs::new(cx.executor());
20977 let main_text = r#"fn main() {
20978println!("1");
20979println!("2");
20980println!("3");
20981println!("4");
20982println!("5");
20983}"#;
20984 let lib_text = "mod foo {}";
20985 fs.insert_tree(
20986 path!("/a"),
20987 json!({
20988 "lib.rs": lib_text,
20989 "main.rs": main_text,
20990 }),
20991 )
20992 .await;
20993
20994 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20995 let (workspace, cx) =
20996 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20997 let worktree_id = workspace.update(cx, |workspace, cx| {
20998 workspace.project().update(cx, |project, cx| {
20999 project.worktrees(cx).next().unwrap().read(cx).id()
21000 })
21001 });
21002
21003 let expected_ranges = vec![
21004 Point::new(0, 0)..Point::new(0, 0),
21005 Point::new(1, 0)..Point::new(1, 1),
21006 Point::new(2, 0)..Point::new(2, 2),
21007 Point::new(3, 0)..Point::new(3, 3),
21008 ];
21009
21010 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21011 let editor_1 = workspace
21012 .update_in(cx, |workspace, window, cx| {
21013 workspace.open_path(
21014 (worktree_id, "main.rs"),
21015 Some(pane_1.downgrade()),
21016 true,
21017 window,
21018 cx,
21019 )
21020 })
21021 .unwrap()
21022 .await
21023 .downcast::<Editor>()
21024 .unwrap();
21025 pane_1.update(cx, |pane, cx| {
21026 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21027 open_editor.update(cx, |editor, cx| {
21028 assert_eq!(
21029 editor.display_text(cx),
21030 main_text,
21031 "Original main.rs text on initial open",
21032 );
21033 assert_eq!(
21034 editor
21035 .selections
21036 .all::<Point>(cx)
21037 .into_iter()
21038 .map(|s| s.range())
21039 .collect::<Vec<_>>(),
21040 vec![Point::zero()..Point::zero()],
21041 "Default selections on initial open",
21042 );
21043 })
21044 });
21045 editor_1.update_in(cx, |editor, window, cx| {
21046 editor.change_selections(None, window, cx, |s| {
21047 s.select_ranges(expected_ranges.clone());
21048 });
21049 });
21050
21051 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21052 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21053 });
21054 let editor_2 = workspace
21055 .update_in(cx, |workspace, window, cx| {
21056 workspace.open_path(
21057 (worktree_id, "main.rs"),
21058 Some(pane_2.downgrade()),
21059 true,
21060 window,
21061 cx,
21062 )
21063 })
21064 .unwrap()
21065 .await
21066 .downcast::<Editor>()
21067 .unwrap();
21068 pane_2.update(cx, |pane, cx| {
21069 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21070 open_editor.update(cx, |editor, cx| {
21071 assert_eq!(
21072 editor.display_text(cx),
21073 main_text,
21074 "Original main.rs text on initial open in another panel",
21075 );
21076 assert_eq!(
21077 editor
21078 .selections
21079 .all::<Point>(cx)
21080 .into_iter()
21081 .map(|s| s.range())
21082 .collect::<Vec<_>>(),
21083 vec![Point::zero()..Point::zero()],
21084 "Default selections on initial open in another panel",
21085 );
21086 })
21087 });
21088
21089 editor_2.update_in(cx, |editor, window, cx| {
21090 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21091 });
21092
21093 let _other_editor_1 = workspace
21094 .update_in(cx, |workspace, window, cx| {
21095 workspace.open_path(
21096 (worktree_id, "lib.rs"),
21097 Some(pane_1.downgrade()),
21098 true,
21099 window,
21100 cx,
21101 )
21102 })
21103 .unwrap()
21104 .await
21105 .downcast::<Editor>()
21106 .unwrap();
21107 pane_1
21108 .update_in(cx, |pane, window, cx| {
21109 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21110 })
21111 .await
21112 .unwrap();
21113 drop(editor_1);
21114 pane_1.update(cx, |pane, cx| {
21115 pane.active_item()
21116 .unwrap()
21117 .downcast::<Editor>()
21118 .unwrap()
21119 .update(cx, |editor, cx| {
21120 assert_eq!(
21121 editor.display_text(cx),
21122 lib_text,
21123 "Other file should be open and active",
21124 );
21125 });
21126 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21127 });
21128
21129 let _other_editor_2 = workspace
21130 .update_in(cx, |workspace, window, cx| {
21131 workspace.open_path(
21132 (worktree_id, "lib.rs"),
21133 Some(pane_2.downgrade()),
21134 true,
21135 window,
21136 cx,
21137 )
21138 })
21139 .unwrap()
21140 .await
21141 .downcast::<Editor>()
21142 .unwrap();
21143 pane_2
21144 .update_in(cx, |pane, window, cx| {
21145 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21146 })
21147 .await
21148 .unwrap();
21149 drop(editor_2);
21150 pane_2.update(cx, |pane, cx| {
21151 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21152 open_editor.update(cx, |editor, cx| {
21153 assert_eq!(
21154 editor.display_text(cx),
21155 lib_text,
21156 "Other file should be open and active in another panel too",
21157 );
21158 });
21159 assert_eq!(
21160 pane.items().count(),
21161 1,
21162 "No other editors should be open in another pane",
21163 );
21164 });
21165
21166 let _editor_1_reopened = workspace
21167 .update_in(cx, |workspace, window, cx| {
21168 workspace.open_path(
21169 (worktree_id, "main.rs"),
21170 Some(pane_1.downgrade()),
21171 true,
21172 window,
21173 cx,
21174 )
21175 })
21176 .unwrap()
21177 .await
21178 .downcast::<Editor>()
21179 .unwrap();
21180 let _editor_2_reopened = workspace
21181 .update_in(cx, |workspace, window, cx| {
21182 workspace.open_path(
21183 (worktree_id, "main.rs"),
21184 Some(pane_2.downgrade()),
21185 true,
21186 window,
21187 cx,
21188 )
21189 })
21190 .unwrap()
21191 .await
21192 .downcast::<Editor>()
21193 .unwrap();
21194 pane_1.update(cx, |pane, cx| {
21195 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21196 open_editor.update(cx, |editor, cx| {
21197 assert_eq!(
21198 editor.display_text(cx),
21199 main_text,
21200 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21201 );
21202 assert_eq!(
21203 editor
21204 .selections
21205 .all::<Point>(cx)
21206 .into_iter()
21207 .map(|s| s.range())
21208 .collect::<Vec<_>>(),
21209 expected_ranges,
21210 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21211 );
21212 })
21213 });
21214 pane_2.update(cx, |pane, cx| {
21215 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21216 open_editor.update(cx, |editor, cx| {
21217 assert_eq!(
21218 editor.display_text(cx),
21219 r#"fn main() {
21220⋯rintln!("1");
21221⋯intln!("2");
21222⋯ntln!("3");
21223println!("4");
21224println!("5");
21225}"#,
21226 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21227 );
21228 assert_eq!(
21229 editor
21230 .selections
21231 .all::<Point>(cx)
21232 .into_iter()
21233 .map(|s| s.range())
21234 .collect::<Vec<_>>(),
21235 vec![Point::zero()..Point::zero()],
21236 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21237 );
21238 })
21239 });
21240}
21241
21242#[gpui::test]
21243async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21244 init_test(cx, |_| {});
21245
21246 let fs = FakeFs::new(cx.executor());
21247 let main_text = r#"fn main() {
21248println!("1");
21249println!("2");
21250println!("3");
21251println!("4");
21252println!("5");
21253}"#;
21254 let lib_text = "mod foo {}";
21255 fs.insert_tree(
21256 path!("/a"),
21257 json!({
21258 "lib.rs": lib_text,
21259 "main.rs": main_text,
21260 }),
21261 )
21262 .await;
21263
21264 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21265 let (workspace, cx) =
21266 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21267 let worktree_id = workspace.update(cx, |workspace, cx| {
21268 workspace.project().update(cx, |project, cx| {
21269 project.worktrees(cx).next().unwrap().read(cx).id()
21270 })
21271 });
21272
21273 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21274 let editor = workspace
21275 .update_in(cx, |workspace, window, cx| {
21276 workspace.open_path(
21277 (worktree_id, "main.rs"),
21278 Some(pane.downgrade()),
21279 true,
21280 window,
21281 cx,
21282 )
21283 })
21284 .unwrap()
21285 .await
21286 .downcast::<Editor>()
21287 .unwrap();
21288 pane.update(cx, |pane, cx| {
21289 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21290 open_editor.update(cx, |editor, cx| {
21291 assert_eq!(
21292 editor.display_text(cx),
21293 main_text,
21294 "Original main.rs text on initial open",
21295 );
21296 })
21297 });
21298 editor.update_in(cx, |editor, window, cx| {
21299 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21300 });
21301
21302 cx.update_global(|store: &mut SettingsStore, cx| {
21303 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21304 s.restore_on_file_reopen = Some(false);
21305 });
21306 });
21307 editor.update_in(cx, |editor, window, cx| {
21308 editor.fold_ranges(
21309 vec![
21310 Point::new(1, 0)..Point::new(1, 1),
21311 Point::new(2, 0)..Point::new(2, 2),
21312 Point::new(3, 0)..Point::new(3, 3),
21313 ],
21314 false,
21315 window,
21316 cx,
21317 );
21318 });
21319 pane.update_in(cx, |pane, window, cx| {
21320 pane.close_all_items(&CloseAllItems::default(), window, cx)
21321 })
21322 .await
21323 .unwrap();
21324 pane.update(cx, |pane, _| {
21325 assert!(pane.active_item().is_none());
21326 });
21327 cx.update_global(|store: &mut SettingsStore, cx| {
21328 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21329 s.restore_on_file_reopen = Some(true);
21330 });
21331 });
21332
21333 let _editor_reopened = workspace
21334 .update_in(cx, |workspace, window, cx| {
21335 workspace.open_path(
21336 (worktree_id, "main.rs"),
21337 Some(pane.downgrade()),
21338 true,
21339 window,
21340 cx,
21341 )
21342 })
21343 .unwrap()
21344 .await
21345 .downcast::<Editor>()
21346 .unwrap();
21347 pane.update(cx, |pane, cx| {
21348 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21349 open_editor.update(cx, |editor, cx| {
21350 assert_eq!(
21351 editor.display_text(cx),
21352 main_text,
21353 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21354 );
21355 })
21356 });
21357}
21358
21359#[gpui::test]
21360async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21361 struct EmptyModalView {
21362 focus_handle: gpui::FocusHandle,
21363 }
21364 impl EventEmitter<DismissEvent> for EmptyModalView {}
21365 impl Render for EmptyModalView {
21366 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21367 div()
21368 }
21369 }
21370 impl Focusable for EmptyModalView {
21371 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21372 self.focus_handle.clone()
21373 }
21374 }
21375 impl workspace::ModalView for EmptyModalView {}
21376 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21377 EmptyModalView {
21378 focus_handle: cx.focus_handle(),
21379 }
21380 }
21381
21382 init_test(cx, |_| {});
21383
21384 let fs = FakeFs::new(cx.executor());
21385 let project = Project::test(fs, [], cx).await;
21386 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21387 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21388 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21389 let editor = cx.new_window_entity(|window, cx| {
21390 Editor::new(
21391 EditorMode::full(),
21392 buffer,
21393 Some(project.clone()),
21394 window,
21395 cx,
21396 )
21397 });
21398 workspace
21399 .update(cx, |workspace, window, cx| {
21400 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21401 })
21402 .unwrap();
21403 editor.update_in(cx, |editor, window, cx| {
21404 editor.open_context_menu(&OpenContextMenu, window, cx);
21405 assert!(editor.mouse_context_menu.is_some());
21406 });
21407 workspace
21408 .update(cx, |workspace, window, cx| {
21409 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21410 })
21411 .unwrap();
21412 cx.read(|cx| {
21413 assert!(editor.read(cx).mouse_context_menu.is_none());
21414 });
21415}
21416
21417#[gpui::test]
21418async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21419 init_test(cx, |_| {});
21420
21421 let fs = FakeFs::new(cx.executor());
21422 fs.insert_file(path!("/file.html"), Default::default())
21423 .await;
21424
21425 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21426
21427 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21428 let html_language = Arc::new(Language::new(
21429 LanguageConfig {
21430 name: "HTML".into(),
21431 matcher: LanguageMatcher {
21432 path_suffixes: vec!["html".to_string()],
21433 ..LanguageMatcher::default()
21434 },
21435 brackets: BracketPairConfig {
21436 pairs: vec![BracketPair {
21437 start: "<".into(),
21438 end: ">".into(),
21439 close: true,
21440 ..Default::default()
21441 }],
21442 ..Default::default()
21443 },
21444 ..Default::default()
21445 },
21446 Some(tree_sitter_html::LANGUAGE.into()),
21447 ));
21448 language_registry.add(html_language);
21449 let mut fake_servers = language_registry.register_fake_lsp(
21450 "HTML",
21451 FakeLspAdapter {
21452 capabilities: lsp::ServerCapabilities {
21453 completion_provider: Some(lsp::CompletionOptions {
21454 resolve_provider: Some(true),
21455 ..Default::default()
21456 }),
21457 ..Default::default()
21458 },
21459 ..Default::default()
21460 },
21461 );
21462
21463 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21464 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21465
21466 let worktree_id = workspace
21467 .update(cx, |workspace, _window, cx| {
21468 workspace.project().update(cx, |project, cx| {
21469 project.worktrees(cx).next().unwrap().read(cx).id()
21470 })
21471 })
21472 .unwrap();
21473 project
21474 .update(cx, |project, cx| {
21475 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21476 })
21477 .await
21478 .unwrap();
21479 let editor = workspace
21480 .update(cx, |workspace, window, cx| {
21481 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21482 })
21483 .unwrap()
21484 .await
21485 .unwrap()
21486 .downcast::<Editor>()
21487 .unwrap();
21488
21489 let fake_server = fake_servers.next().await.unwrap();
21490 editor.update_in(cx, |editor, window, cx| {
21491 editor.set_text("<ad></ad>", window, cx);
21492 editor.change_selections(None, window, cx, |selections| {
21493 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21494 });
21495 let Some((buffer, _)) = editor
21496 .buffer
21497 .read(cx)
21498 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21499 else {
21500 panic!("Failed to get buffer for selection position");
21501 };
21502 let buffer = buffer.read(cx);
21503 let buffer_id = buffer.remote_id();
21504 let opening_range =
21505 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21506 let closing_range =
21507 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21508 let mut linked_ranges = HashMap::default();
21509 linked_ranges.insert(
21510 buffer_id,
21511 vec![(opening_range.clone(), vec![closing_range.clone()])],
21512 );
21513 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21514 });
21515 let mut completion_handle =
21516 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21517 Ok(Some(lsp::CompletionResponse::Array(vec![
21518 lsp::CompletionItem {
21519 label: "head".to_string(),
21520 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21521 lsp::InsertReplaceEdit {
21522 new_text: "head".to_string(),
21523 insert: lsp::Range::new(
21524 lsp::Position::new(0, 1),
21525 lsp::Position::new(0, 3),
21526 ),
21527 replace: lsp::Range::new(
21528 lsp::Position::new(0, 1),
21529 lsp::Position::new(0, 3),
21530 ),
21531 },
21532 )),
21533 ..Default::default()
21534 },
21535 ])))
21536 });
21537 editor.update_in(cx, |editor, window, cx| {
21538 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21539 });
21540 cx.run_until_parked();
21541 completion_handle.next().await.unwrap();
21542 editor.update(cx, |editor, _| {
21543 assert!(
21544 editor.context_menu_visible(),
21545 "Completion menu should be visible"
21546 );
21547 });
21548 editor.update_in(cx, |editor, window, cx| {
21549 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21550 });
21551 cx.executor().run_until_parked();
21552 editor.update(cx, |editor, cx| {
21553 assert_eq!(editor.text(cx), "<head></head>");
21554 });
21555}
21556
21557#[gpui::test]
21558async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21559 init_test(cx, |_| {});
21560
21561 let fs = FakeFs::new(cx.executor());
21562 fs.insert_tree(
21563 path!("/root"),
21564 json!({
21565 "a": {
21566 "main.rs": "fn main() {}",
21567 },
21568 "foo": {
21569 "bar": {
21570 "external_file.rs": "pub mod external {}",
21571 }
21572 }
21573 }),
21574 )
21575 .await;
21576
21577 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21578 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21579 language_registry.add(rust_lang());
21580 let _fake_servers = language_registry.register_fake_lsp(
21581 "Rust",
21582 FakeLspAdapter {
21583 ..FakeLspAdapter::default()
21584 },
21585 );
21586 let (workspace, cx) =
21587 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21588 let worktree_id = workspace.update(cx, |workspace, cx| {
21589 workspace.project().update(cx, |project, cx| {
21590 project.worktrees(cx).next().unwrap().read(cx).id()
21591 })
21592 });
21593
21594 let assert_language_servers_count =
21595 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21596 project.update(cx, |project, cx| {
21597 let current = project
21598 .lsp_store()
21599 .read(cx)
21600 .as_local()
21601 .unwrap()
21602 .language_servers
21603 .len();
21604 assert_eq!(expected, current, "{context}");
21605 });
21606 };
21607
21608 assert_language_servers_count(
21609 0,
21610 "No servers should be running before any file is open",
21611 cx,
21612 );
21613 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21614 let main_editor = workspace
21615 .update_in(cx, |workspace, window, cx| {
21616 workspace.open_path(
21617 (worktree_id, "main.rs"),
21618 Some(pane.downgrade()),
21619 true,
21620 window,
21621 cx,
21622 )
21623 })
21624 .unwrap()
21625 .await
21626 .downcast::<Editor>()
21627 .unwrap();
21628 pane.update(cx, |pane, cx| {
21629 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21630 open_editor.update(cx, |editor, cx| {
21631 assert_eq!(
21632 editor.display_text(cx),
21633 "fn main() {}",
21634 "Original main.rs text on initial open",
21635 );
21636 });
21637 assert_eq!(open_editor, main_editor);
21638 });
21639 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21640
21641 let external_editor = workspace
21642 .update_in(cx, |workspace, window, cx| {
21643 workspace.open_abs_path(
21644 PathBuf::from("/root/foo/bar/external_file.rs"),
21645 OpenOptions::default(),
21646 window,
21647 cx,
21648 )
21649 })
21650 .await
21651 .expect("opening external file")
21652 .downcast::<Editor>()
21653 .expect("downcasted external file's open element to editor");
21654 pane.update(cx, |pane, cx| {
21655 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21656 open_editor.update(cx, |editor, cx| {
21657 assert_eq!(
21658 editor.display_text(cx),
21659 "pub mod external {}",
21660 "External file is open now",
21661 );
21662 });
21663 assert_eq!(open_editor, external_editor);
21664 });
21665 assert_language_servers_count(
21666 1,
21667 "Second, external, *.rs file should join the existing server",
21668 cx,
21669 );
21670
21671 pane.update_in(cx, |pane, window, cx| {
21672 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21673 })
21674 .await
21675 .unwrap();
21676 pane.update_in(cx, |pane, window, cx| {
21677 pane.navigate_backward(window, cx);
21678 });
21679 cx.run_until_parked();
21680 pane.update(cx, |pane, cx| {
21681 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21682 open_editor.update(cx, |editor, cx| {
21683 assert_eq!(
21684 editor.display_text(cx),
21685 "pub mod external {}",
21686 "External file is open now",
21687 );
21688 });
21689 });
21690 assert_language_servers_count(
21691 1,
21692 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21693 cx,
21694 );
21695
21696 cx.update(|_, cx| {
21697 workspace::reload(&workspace::Reload::default(), cx);
21698 });
21699 assert_language_servers_count(
21700 1,
21701 "After reloading the worktree with local and external files opened, only one project should be started",
21702 cx,
21703 );
21704}
21705
21706#[gpui::test]
21707async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21708 init_test(cx, |_| {});
21709
21710 let mut cx = EditorTestContext::new(cx).await;
21711 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21712 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21713
21714 // test cursor move to start of each line on tab
21715 // for `if`, `elif`, `else`, `while`, `with` and `for`
21716 cx.set_state(indoc! {"
21717 def main():
21718 ˇ for item in items:
21719 ˇ while item.active:
21720 ˇ if item.value > 10:
21721 ˇ continue
21722 ˇ elif item.value < 0:
21723 ˇ break
21724 ˇ else:
21725 ˇ with item.context() as ctx:
21726 ˇ yield count
21727 ˇ else:
21728 ˇ log('while else')
21729 ˇ else:
21730 ˇ log('for else')
21731 "});
21732 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21733 cx.assert_editor_state(indoc! {"
21734 def main():
21735 ˇfor item in items:
21736 ˇwhile item.active:
21737 ˇif item.value > 10:
21738 ˇcontinue
21739 ˇelif item.value < 0:
21740 ˇbreak
21741 ˇelse:
21742 ˇwith item.context() as ctx:
21743 ˇyield count
21744 ˇelse:
21745 ˇlog('while else')
21746 ˇelse:
21747 ˇlog('for else')
21748 "});
21749 // test relative indent is preserved when tab
21750 // for `if`, `elif`, `else`, `while`, `with` and `for`
21751 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21752 cx.assert_editor_state(indoc! {"
21753 def main():
21754 ˇfor item in items:
21755 ˇwhile item.active:
21756 ˇif item.value > 10:
21757 ˇcontinue
21758 ˇelif item.value < 0:
21759 ˇbreak
21760 ˇelse:
21761 ˇwith item.context() as ctx:
21762 ˇyield count
21763 ˇelse:
21764 ˇlog('while else')
21765 ˇelse:
21766 ˇlog('for else')
21767 "});
21768
21769 // test cursor move to start of each line on tab
21770 // for `try`, `except`, `else`, `finally`, `match` and `def`
21771 cx.set_state(indoc! {"
21772 def main():
21773 ˇ try:
21774 ˇ fetch()
21775 ˇ except ValueError:
21776 ˇ handle_error()
21777 ˇ else:
21778 ˇ match value:
21779 ˇ case _:
21780 ˇ finally:
21781 ˇ def status():
21782 ˇ return 0
21783 "});
21784 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21785 cx.assert_editor_state(indoc! {"
21786 def main():
21787 ˇtry:
21788 ˇfetch()
21789 ˇexcept ValueError:
21790 ˇhandle_error()
21791 ˇelse:
21792 ˇmatch value:
21793 ˇcase _:
21794 ˇfinally:
21795 ˇdef status():
21796 ˇreturn 0
21797 "});
21798 // test relative indent is preserved when tab
21799 // for `try`, `except`, `else`, `finally`, `match` and `def`
21800 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21801 cx.assert_editor_state(indoc! {"
21802 def main():
21803 ˇtry:
21804 ˇfetch()
21805 ˇexcept ValueError:
21806 ˇhandle_error()
21807 ˇelse:
21808 ˇmatch value:
21809 ˇcase _:
21810 ˇfinally:
21811 ˇdef status():
21812 ˇreturn 0
21813 "});
21814}
21815
21816#[gpui::test]
21817async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21818 init_test(cx, |_| {});
21819
21820 let mut cx = EditorTestContext::new(cx).await;
21821 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21822 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21823
21824 // test `else` auto outdents when typed inside `if` block
21825 cx.set_state(indoc! {"
21826 def main():
21827 if i == 2:
21828 return
21829 ˇ
21830 "});
21831 cx.update_editor(|editor, window, cx| {
21832 editor.handle_input("else:", window, cx);
21833 });
21834 cx.assert_editor_state(indoc! {"
21835 def main():
21836 if i == 2:
21837 return
21838 else:ˇ
21839 "});
21840
21841 // test `except` auto outdents when typed inside `try` block
21842 cx.set_state(indoc! {"
21843 def main():
21844 try:
21845 i = 2
21846 ˇ
21847 "});
21848 cx.update_editor(|editor, window, cx| {
21849 editor.handle_input("except:", window, cx);
21850 });
21851 cx.assert_editor_state(indoc! {"
21852 def main():
21853 try:
21854 i = 2
21855 except:ˇ
21856 "});
21857
21858 // test `else` auto outdents when typed inside `except` block
21859 cx.set_state(indoc! {"
21860 def main():
21861 try:
21862 i = 2
21863 except:
21864 j = 2
21865 ˇ
21866 "});
21867 cx.update_editor(|editor, window, cx| {
21868 editor.handle_input("else:", window, cx);
21869 });
21870 cx.assert_editor_state(indoc! {"
21871 def main():
21872 try:
21873 i = 2
21874 except:
21875 j = 2
21876 else:ˇ
21877 "});
21878
21879 // test `finally` auto outdents when typed inside `else` block
21880 cx.set_state(indoc! {"
21881 def main():
21882 try:
21883 i = 2
21884 except:
21885 j = 2
21886 else:
21887 k = 2
21888 ˇ
21889 "});
21890 cx.update_editor(|editor, window, cx| {
21891 editor.handle_input("finally:", window, cx);
21892 });
21893 cx.assert_editor_state(indoc! {"
21894 def main():
21895 try:
21896 i = 2
21897 except:
21898 j = 2
21899 else:
21900 k = 2
21901 finally:ˇ
21902 "});
21903
21904 // test `else` does not outdents when typed inside `except` block right after for block
21905 cx.set_state(indoc! {"
21906 def main():
21907 try:
21908 i = 2
21909 except:
21910 for i in range(n):
21911 pass
21912 ˇ
21913 "});
21914 cx.update_editor(|editor, window, cx| {
21915 editor.handle_input("else:", window, cx);
21916 });
21917 cx.assert_editor_state(indoc! {"
21918 def main():
21919 try:
21920 i = 2
21921 except:
21922 for i in range(n):
21923 pass
21924 else:ˇ
21925 "});
21926
21927 // test `finally` auto outdents when typed inside `else` block right after for block
21928 cx.set_state(indoc! {"
21929 def main():
21930 try:
21931 i = 2
21932 except:
21933 j = 2
21934 else:
21935 for i in range(n):
21936 pass
21937 ˇ
21938 "});
21939 cx.update_editor(|editor, window, cx| {
21940 editor.handle_input("finally:", window, cx);
21941 });
21942 cx.assert_editor_state(indoc! {"
21943 def main():
21944 try:
21945 i = 2
21946 except:
21947 j = 2
21948 else:
21949 for i in range(n):
21950 pass
21951 finally:ˇ
21952 "});
21953
21954 // test `except` outdents to inner "try" block
21955 cx.set_state(indoc! {"
21956 def main():
21957 try:
21958 i = 2
21959 if i == 2:
21960 try:
21961 i = 3
21962 ˇ
21963 "});
21964 cx.update_editor(|editor, window, cx| {
21965 editor.handle_input("except:", window, cx);
21966 });
21967 cx.assert_editor_state(indoc! {"
21968 def main():
21969 try:
21970 i = 2
21971 if i == 2:
21972 try:
21973 i = 3
21974 except:ˇ
21975 "});
21976
21977 // test `except` outdents to outer "try" block
21978 cx.set_state(indoc! {"
21979 def main():
21980 try:
21981 i = 2
21982 if i == 2:
21983 try:
21984 i = 3
21985 ˇ
21986 "});
21987 cx.update_editor(|editor, window, cx| {
21988 editor.handle_input("except:", window, cx);
21989 });
21990 cx.assert_editor_state(indoc! {"
21991 def main():
21992 try:
21993 i = 2
21994 if i == 2:
21995 try:
21996 i = 3
21997 except:ˇ
21998 "});
21999
22000 // test `else` stays at correct indent when typed after `for` block
22001 cx.set_state(indoc! {"
22002 def main():
22003 for i in range(10):
22004 if i == 3:
22005 break
22006 ˇ
22007 "});
22008 cx.update_editor(|editor, window, cx| {
22009 editor.handle_input("else:", window, cx);
22010 });
22011 cx.assert_editor_state(indoc! {"
22012 def main():
22013 for i in range(10):
22014 if i == 3:
22015 break
22016 else:ˇ
22017 "});
22018
22019 // test does not outdent on typing after line with square brackets
22020 cx.set_state(indoc! {"
22021 def f() -> list[str]:
22022 ˇ
22023 "});
22024 cx.update_editor(|editor, window, cx| {
22025 editor.handle_input("a", window, cx);
22026 });
22027 cx.assert_editor_state(indoc! {"
22028 def f() -> list[str]:
22029 aˇ
22030 "});
22031}
22032
22033#[gpui::test]
22034async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22035 init_test(cx, |_| {});
22036 update_test_language_settings(cx, |settings| {
22037 settings.defaults.extend_comment_on_newline = Some(false);
22038 });
22039 let mut cx = EditorTestContext::new(cx).await;
22040 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22041 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22042
22043 // test correct indent after newline on comment
22044 cx.set_state(indoc! {"
22045 # COMMENT:ˇ
22046 "});
22047 cx.update_editor(|editor, window, cx| {
22048 editor.newline(&Newline, window, cx);
22049 });
22050 cx.assert_editor_state(indoc! {"
22051 # COMMENT:
22052 ˇ
22053 "});
22054
22055 // test correct indent after newline in brackets
22056 cx.set_state(indoc! {"
22057 {ˇ}
22058 "});
22059 cx.update_editor(|editor, window, cx| {
22060 editor.newline(&Newline, window, cx);
22061 });
22062 cx.run_until_parked();
22063 cx.assert_editor_state(indoc! {"
22064 {
22065 ˇ
22066 }
22067 "});
22068
22069 cx.set_state(indoc! {"
22070 (ˇ)
22071 "});
22072 cx.update_editor(|editor, window, cx| {
22073 editor.newline(&Newline, window, cx);
22074 });
22075 cx.run_until_parked();
22076 cx.assert_editor_state(indoc! {"
22077 (
22078 ˇ
22079 )
22080 "});
22081
22082 // do not indent after empty lists or dictionaries
22083 cx.set_state(indoc! {"
22084 a = []ˇ
22085 "});
22086 cx.update_editor(|editor, window, cx| {
22087 editor.newline(&Newline, window, cx);
22088 });
22089 cx.run_until_parked();
22090 cx.assert_editor_state(indoc! {"
22091 a = []
22092 ˇ
22093 "});
22094}
22095
22096fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22097 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22098 point..point
22099}
22100
22101#[track_caller]
22102fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22103 let (text, ranges) = marked_text_ranges(marked_text, true);
22104 assert_eq!(editor.text(cx), text);
22105 assert_eq!(
22106 editor.selections.ranges(cx),
22107 ranges,
22108 "Assert selections are {}",
22109 marked_text
22110 );
22111}
22112
22113pub fn handle_signature_help_request(
22114 cx: &mut EditorLspTestContext,
22115 mocked_response: lsp::SignatureHelp,
22116) -> impl Future<Output = ()> + use<> {
22117 let mut request =
22118 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22119 let mocked_response = mocked_response.clone();
22120 async move { Ok(Some(mocked_response)) }
22121 });
22122
22123 async move {
22124 request.next().await;
22125 }
22126}
22127
22128#[track_caller]
22129pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22130 cx.update_editor(|editor, _, _| {
22131 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22132 let entries = menu.entries.borrow();
22133 let entries = entries
22134 .iter()
22135 .map(|entry| entry.string.as_str())
22136 .collect::<Vec<_>>();
22137 assert_eq!(entries, expected);
22138 } else {
22139 panic!("Expected completions menu");
22140 }
22141 });
22142}
22143
22144/// Handle completion request passing a marked string specifying where the completion
22145/// should be triggered from using '|' character, what range should be replaced, and what completions
22146/// should be returned using '<' and '>' to delimit the range.
22147///
22148/// Also see `handle_completion_request_with_insert_and_replace`.
22149#[track_caller]
22150pub fn handle_completion_request(
22151 marked_string: &str,
22152 completions: Vec<&'static str>,
22153 is_incomplete: bool,
22154 counter: Arc<AtomicUsize>,
22155 cx: &mut EditorLspTestContext,
22156) -> impl Future<Output = ()> {
22157 let complete_from_marker: TextRangeMarker = '|'.into();
22158 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22159 let (_, mut marked_ranges) = marked_text_ranges_by(
22160 marked_string,
22161 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22162 );
22163
22164 let complete_from_position =
22165 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22166 let replace_range =
22167 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22168
22169 let mut request =
22170 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22171 let completions = completions.clone();
22172 counter.fetch_add(1, atomic::Ordering::Release);
22173 async move {
22174 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22175 assert_eq!(
22176 params.text_document_position.position,
22177 complete_from_position
22178 );
22179 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22180 is_incomplete: is_incomplete,
22181 item_defaults: None,
22182 items: completions
22183 .iter()
22184 .map(|completion_text| lsp::CompletionItem {
22185 label: completion_text.to_string(),
22186 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22187 range: replace_range,
22188 new_text: completion_text.to_string(),
22189 })),
22190 ..Default::default()
22191 })
22192 .collect(),
22193 })))
22194 }
22195 });
22196
22197 async move {
22198 request.next().await;
22199 }
22200}
22201
22202/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22203/// given instead, which also contains an `insert` range.
22204///
22205/// This function uses markers to define ranges:
22206/// - `|` marks the cursor position
22207/// - `<>` marks the replace range
22208/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22209pub fn handle_completion_request_with_insert_and_replace(
22210 cx: &mut EditorLspTestContext,
22211 marked_string: &str,
22212 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22213 counter: Arc<AtomicUsize>,
22214) -> impl Future<Output = ()> {
22215 let complete_from_marker: TextRangeMarker = '|'.into();
22216 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22217 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22218
22219 let (_, mut marked_ranges) = marked_text_ranges_by(
22220 marked_string,
22221 vec![
22222 complete_from_marker.clone(),
22223 replace_range_marker.clone(),
22224 insert_range_marker.clone(),
22225 ],
22226 );
22227
22228 let complete_from_position =
22229 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22230 let replace_range =
22231 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22232
22233 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22234 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22235 _ => lsp::Range {
22236 start: replace_range.start,
22237 end: complete_from_position,
22238 },
22239 };
22240
22241 let mut request =
22242 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22243 let completions = completions.clone();
22244 counter.fetch_add(1, atomic::Ordering::Release);
22245 async move {
22246 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22247 assert_eq!(
22248 params.text_document_position.position, complete_from_position,
22249 "marker `|` position doesn't match",
22250 );
22251 Ok(Some(lsp::CompletionResponse::Array(
22252 completions
22253 .iter()
22254 .map(|(label, new_text)| lsp::CompletionItem {
22255 label: label.to_string(),
22256 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22257 lsp::InsertReplaceEdit {
22258 insert: insert_range,
22259 replace: replace_range,
22260 new_text: new_text.to_string(),
22261 },
22262 )),
22263 ..Default::default()
22264 })
22265 .collect(),
22266 )))
22267 }
22268 });
22269
22270 async move {
22271 request.next().await;
22272 }
22273}
22274
22275fn handle_resolve_completion_request(
22276 cx: &mut EditorLspTestContext,
22277 edits: Option<Vec<(&'static str, &'static str)>>,
22278) -> impl Future<Output = ()> {
22279 let edits = edits.map(|edits| {
22280 edits
22281 .iter()
22282 .map(|(marked_string, new_text)| {
22283 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22284 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22285 lsp::TextEdit::new(replace_range, new_text.to_string())
22286 })
22287 .collect::<Vec<_>>()
22288 });
22289
22290 let mut request =
22291 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22292 let edits = edits.clone();
22293 async move {
22294 Ok(lsp::CompletionItem {
22295 additional_text_edits: edits,
22296 ..Default::default()
22297 })
22298 }
22299 });
22300
22301 async move {
22302 request.next().await;
22303 }
22304}
22305
22306pub(crate) fn update_test_language_settings(
22307 cx: &mut TestAppContext,
22308 f: impl Fn(&mut AllLanguageSettingsContent),
22309) {
22310 cx.update(|cx| {
22311 SettingsStore::update_global(cx, |store, cx| {
22312 store.update_user_settings::<AllLanguageSettings>(cx, f);
22313 });
22314 });
22315}
22316
22317pub(crate) fn update_test_project_settings(
22318 cx: &mut TestAppContext,
22319 f: impl Fn(&mut ProjectSettings),
22320) {
22321 cx.update(|cx| {
22322 SettingsStore::update_global(cx, |store, cx| {
22323 store.update_user_settings::<ProjectSettings>(cx, f);
22324 });
22325 });
22326}
22327
22328pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22329 cx.update(|cx| {
22330 assets::Assets.load_test_fonts(cx);
22331 let store = SettingsStore::test(cx);
22332 cx.set_global(store);
22333 theme::init(theme::LoadThemes::JustBase, cx);
22334 release_channel::init(SemanticVersion::default(), cx);
22335 client::init_settings(cx);
22336 language::init(cx);
22337 Project::init_settings(cx);
22338 workspace::init_settings(cx);
22339 crate::init(cx);
22340 });
22341
22342 update_test_language_settings(cx, f);
22343}
22344
22345#[track_caller]
22346fn assert_hunk_revert(
22347 not_reverted_text_with_selections: &str,
22348 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22349 expected_reverted_text_with_selections: &str,
22350 base_text: &str,
22351 cx: &mut EditorLspTestContext,
22352) {
22353 cx.set_state(not_reverted_text_with_selections);
22354 cx.set_head_text(base_text);
22355 cx.executor().run_until_parked();
22356
22357 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22358 let snapshot = editor.snapshot(window, cx);
22359 let reverted_hunk_statuses = snapshot
22360 .buffer_snapshot
22361 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22362 .map(|hunk| hunk.status().kind)
22363 .collect::<Vec<_>>();
22364
22365 editor.git_restore(&Default::default(), window, cx);
22366 reverted_hunk_statuses
22367 });
22368 cx.executor().run_until_parked();
22369 cx.assert_editor_state(expected_reverted_text_with_selections);
22370 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22371}
22372
22373#[gpui::test(iterations = 10)]
22374async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22375 init_test(cx, |_| {});
22376
22377 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22378 let counter = diagnostic_requests.clone();
22379
22380 let fs = FakeFs::new(cx.executor());
22381 fs.insert_tree(
22382 path!("/a"),
22383 json!({
22384 "first.rs": "fn main() { let a = 5; }",
22385 "second.rs": "// Test file",
22386 }),
22387 )
22388 .await;
22389
22390 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22391 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22392 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22393
22394 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22395 language_registry.add(rust_lang());
22396 let mut fake_servers = language_registry.register_fake_lsp(
22397 "Rust",
22398 FakeLspAdapter {
22399 capabilities: lsp::ServerCapabilities {
22400 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22401 lsp::DiagnosticOptions {
22402 identifier: None,
22403 inter_file_dependencies: true,
22404 workspace_diagnostics: true,
22405 work_done_progress_options: Default::default(),
22406 },
22407 )),
22408 ..Default::default()
22409 },
22410 ..Default::default()
22411 },
22412 );
22413
22414 let editor = workspace
22415 .update(cx, |workspace, window, cx| {
22416 workspace.open_abs_path(
22417 PathBuf::from(path!("/a/first.rs")),
22418 OpenOptions::default(),
22419 window,
22420 cx,
22421 )
22422 })
22423 .unwrap()
22424 .await
22425 .unwrap()
22426 .downcast::<Editor>()
22427 .unwrap();
22428 let fake_server = fake_servers.next().await.unwrap();
22429 let server_id = fake_server.server.server_id();
22430 let mut first_request = fake_server
22431 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22432 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22433 let result_id = Some(new_result_id.to_string());
22434 assert_eq!(
22435 params.text_document.uri,
22436 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22437 );
22438 async move {
22439 Ok(lsp::DocumentDiagnosticReportResult::Report(
22440 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22441 related_documents: None,
22442 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22443 items: Vec::new(),
22444 result_id,
22445 },
22446 }),
22447 ))
22448 }
22449 });
22450
22451 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22452 project.update(cx, |project, cx| {
22453 let buffer_id = editor
22454 .read(cx)
22455 .buffer()
22456 .read(cx)
22457 .as_singleton()
22458 .expect("created a singleton buffer")
22459 .read(cx)
22460 .remote_id();
22461 let buffer_result_id = project
22462 .lsp_store()
22463 .read(cx)
22464 .result_id(server_id, buffer_id, cx);
22465 assert_eq!(expected, buffer_result_id);
22466 });
22467 };
22468
22469 ensure_result_id(None, cx);
22470 cx.executor().advance_clock(Duration::from_millis(60));
22471 cx.executor().run_until_parked();
22472 assert_eq!(
22473 diagnostic_requests.load(atomic::Ordering::Acquire),
22474 1,
22475 "Opening file should trigger diagnostic request"
22476 );
22477 first_request
22478 .next()
22479 .await
22480 .expect("should have sent the first diagnostics pull request");
22481 ensure_result_id(Some("1".to_string()), cx);
22482
22483 // Editing should trigger diagnostics
22484 editor.update_in(cx, |editor, window, cx| {
22485 editor.handle_input("2", window, cx)
22486 });
22487 cx.executor().advance_clock(Duration::from_millis(60));
22488 cx.executor().run_until_parked();
22489 assert_eq!(
22490 diagnostic_requests.load(atomic::Ordering::Acquire),
22491 2,
22492 "Editing should trigger diagnostic request"
22493 );
22494 ensure_result_id(Some("2".to_string()), cx);
22495
22496 // Moving cursor should not trigger diagnostic request
22497 editor.update_in(cx, |editor, window, cx| {
22498 editor.change_selections(None, window, cx, |s| {
22499 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22500 });
22501 });
22502 cx.executor().advance_clock(Duration::from_millis(60));
22503 cx.executor().run_until_parked();
22504 assert_eq!(
22505 diagnostic_requests.load(atomic::Ordering::Acquire),
22506 2,
22507 "Cursor movement should not trigger diagnostic request"
22508 );
22509 ensure_result_id(Some("2".to_string()), cx);
22510 // Multiple rapid edits should be debounced
22511 for _ in 0..5 {
22512 editor.update_in(cx, |editor, window, cx| {
22513 editor.handle_input("x", window, cx)
22514 });
22515 }
22516 cx.executor().advance_clock(Duration::from_millis(60));
22517 cx.executor().run_until_parked();
22518
22519 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22520 assert!(
22521 final_requests <= 4,
22522 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22523 );
22524 ensure_result_id(Some(final_requests.to_string()), cx);
22525}
22526
22527#[gpui::test]
22528async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22529 // Regression test for issue #11671
22530 // Previously, adding a cursor after moving multiple cursors would reset
22531 // the cursor count instead of adding to the existing cursors.
22532 init_test(cx, |_| {});
22533 let mut cx = EditorTestContext::new(cx).await;
22534
22535 // Create a simple buffer with cursor at start
22536 cx.set_state(indoc! {"
22537 ˇaaaa
22538 bbbb
22539 cccc
22540 dddd
22541 eeee
22542 ffff
22543 gggg
22544 hhhh"});
22545
22546 // Add 2 cursors below (so we have 3 total)
22547 cx.update_editor(|editor, window, cx| {
22548 editor.add_selection_below(&Default::default(), window, cx);
22549 editor.add_selection_below(&Default::default(), window, cx);
22550 });
22551
22552 // Verify we have 3 cursors
22553 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22554 assert_eq!(
22555 initial_count, 3,
22556 "Should have 3 cursors after adding 2 below"
22557 );
22558
22559 // Move down one line
22560 cx.update_editor(|editor, window, cx| {
22561 editor.move_down(&MoveDown, window, cx);
22562 });
22563
22564 // Add another cursor below
22565 cx.update_editor(|editor, window, cx| {
22566 editor.add_selection_below(&Default::default(), window, cx);
22567 });
22568
22569 // Should now have 4 cursors (3 original + 1 new)
22570 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22571 assert_eq!(
22572 final_count, 4,
22573 "Should have 4 cursors after moving and adding another"
22574 );
22575}
22576
22577#[gpui::test]
22578async fn test_mtime_and_document_colors(cx: &mut TestAppContext) {
22579 let expected_color = Rgba {
22580 r: 0.33,
22581 g: 0.33,
22582 b: 0.33,
22583 a: 0.33,
22584 };
22585
22586 init_test(cx, |_| {});
22587
22588 let fs = FakeFs::new(cx.executor());
22589 fs.insert_tree(
22590 path!("/a"),
22591 json!({
22592 "first.rs": "fn main() { let a = 5; }",
22593 }),
22594 )
22595 .await;
22596
22597 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22598 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22599 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22600
22601 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22602 language_registry.add(rust_lang());
22603 let mut fake_servers = language_registry.register_fake_lsp(
22604 "Rust",
22605 FakeLspAdapter {
22606 capabilities: lsp::ServerCapabilities {
22607 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22608 ..lsp::ServerCapabilities::default()
22609 },
22610 ..FakeLspAdapter::default()
22611 },
22612 );
22613
22614 let editor = workspace
22615 .update(cx, |workspace, window, cx| {
22616 workspace.open_abs_path(
22617 PathBuf::from(path!("/a/first.rs")),
22618 OpenOptions::default(),
22619 window,
22620 cx,
22621 )
22622 })
22623 .unwrap()
22624 .await
22625 .unwrap()
22626 .downcast::<Editor>()
22627 .unwrap();
22628 let fake_language_server = fake_servers.next().await.unwrap();
22629 let requests_made = Arc::new(AtomicUsize::new(0));
22630 let closure_requests_made = Arc::clone(&requests_made);
22631 let mut color_request_handle = fake_language_server
22632 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22633 let requests_made = Arc::clone(&closure_requests_made);
22634 async move {
22635 assert_eq!(
22636 params.text_document.uri,
22637 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22638 );
22639 requests_made.fetch_add(1, atomic::Ordering::Release);
22640 Ok(vec![lsp::ColorInformation {
22641 range: lsp::Range {
22642 start: lsp::Position {
22643 line: 0,
22644 character: 0,
22645 },
22646 end: lsp::Position {
22647 line: 0,
22648 character: 1,
22649 },
22650 },
22651 color: lsp::Color {
22652 red: 0.33,
22653 green: 0.33,
22654 blue: 0.33,
22655 alpha: 0.33,
22656 },
22657 }])
22658 }
22659 });
22660 color_request_handle.next().await.unwrap();
22661 cx.run_until_parked();
22662 color_request_handle.next().await.unwrap();
22663 cx.run_until_parked();
22664 assert_eq!(
22665 2,
22666 requests_made.load(atomic::Ordering::Acquire),
22667 "Should query for colors once per editor open and once after the language server startup"
22668 );
22669
22670 cx.executor().advance_clock(Duration::from_millis(500));
22671 let save = editor.update_in(cx, |editor, window, cx| {
22672 assert_eq!(
22673 vec![expected_color],
22674 extract_color_inlays(editor, cx),
22675 "Should have an initial inlay"
22676 );
22677
22678 editor.move_to_end(&MoveToEnd, window, cx);
22679 editor.handle_input("dirty", window, cx);
22680 editor.save(
22681 SaveOptions {
22682 format: true,
22683 autosave: true,
22684 },
22685 project.clone(),
22686 window,
22687 cx,
22688 )
22689 });
22690 save.await.unwrap();
22691
22692 color_request_handle.next().await.unwrap();
22693 cx.run_until_parked();
22694 color_request_handle.next().await.unwrap();
22695 cx.run_until_parked();
22696 assert_eq!(
22697 4,
22698 requests_made.load(atomic::Ordering::Acquire),
22699 "Should query for colors once per save and once per formatting after save"
22700 );
22701
22702 drop(editor);
22703 let close = workspace
22704 .update(cx, |workspace, window, cx| {
22705 workspace.active_pane().update(cx, |pane, cx| {
22706 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22707 })
22708 })
22709 .unwrap();
22710 close.await.unwrap();
22711 assert_eq!(
22712 4,
22713 requests_made.load(atomic::Ordering::Acquire),
22714 "After saving and closing the editor, no extra requests should be made"
22715 );
22716
22717 workspace
22718 .update(cx, |workspace, window, cx| {
22719 workspace.active_pane().update(cx, |pane, cx| {
22720 pane.navigate_backward(window, cx);
22721 })
22722 })
22723 .unwrap();
22724 color_request_handle.next().await.unwrap();
22725 cx.run_until_parked();
22726 assert_eq!(
22727 5,
22728 requests_made.load(atomic::Ordering::Acquire),
22729 "After navigating back to an editor and reopening it, another color request should be made"
22730 );
22731 let editor = workspace
22732 .update(cx, |workspace, _, cx| {
22733 workspace
22734 .active_item(cx)
22735 .expect("Should have reopened the editor again after navigating back")
22736 .downcast::<Editor>()
22737 .expect("Should be an editor")
22738 })
22739 .unwrap();
22740 editor.update(cx, |editor, cx| {
22741 assert_eq!(
22742 vec![expected_color],
22743 extract_color_inlays(editor, cx),
22744 "Should have an initial inlay"
22745 );
22746 });
22747}
22748
22749#[track_caller]
22750fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
22751 editor
22752 .all_inlays(cx)
22753 .into_iter()
22754 .filter_map(|inlay| inlay.get_color())
22755 .map(Rgba::from)
22756 .collect()
22757}