1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
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, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, 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, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 invalid_buffer_view::InvalidBufferView,
61 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
62 register_project_item,
63};
64
65#[gpui::test]
66fn test_edit_events(cx: &mut TestAppContext) {
67 init_test(cx, |_| {});
68
69 let buffer = cx.new(|cx| {
70 let mut buffer = language::Buffer::local("123456", cx);
71 buffer.set_group_interval(Duration::from_secs(1));
72 buffer
73 });
74
75 let events = Rc::new(RefCell::new(Vec::new()));
76 let editor1 = cx.add_window({
77 let events = events.clone();
78 |window, cx| {
79 let entity = cx.entity();
80 cx.subscribe_in(
81 &entity,
82 window,
83 move |_, _, event: &EditorEvent, _, _| match event {
84 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
85 EditorEvent::BufferEdited => {
86 events.borrow_mut().push(("editor1", "buffer edited"))
87 }
88 _ => {}
89 },
90 )
91 .detach();
92 Editor::for_buffer(buffer.clone(), None, window, cx)
93 }
94 });
95
96 let editor2 = cx.add_window({
97 let events = events.clone();
98 |window, cx| {
99 cx.subscribe_in(
100 &cx.entity(),
101 window,
102 move |_, _, event: &EditorEvent, _, _| match event {
103 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
104 EditorEvent::BufferEdited => {
105 events.borrow_mut().push(("editor2", "buffer edited"))
106 }
107 _ => {}
108 },
109 )
110 .detach();
111 Editor::for_buffer(buffer.clone(), None, window, cx)
112 }
113 });
114
115 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
116
117 // Mutating editor 1 will emit an `Edited` event only for that editor.
118 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
119 assert_eq!(
120 mem::take(&mut *events.borrow_mut()),
121 [
122 ("editor1", "edited"),
123 ("editor1", "buffer edited"),
124 ("editor2", "buffer edited"),
125 ]
126 );
127
128 // Mutating editor 2 will emit an `Edited` event only for that editor.
129 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
130 assert_eq!(
131 mem::take(&mut *events.borrow_mut()),
132 [
133 ("editor2", "edited"),
134 ("editor1", "buffer edited"),
135 ("editor2", "buffer edited"),
136 ]
137 );
138
139 // Undoing on editor 1 will emit an `Edited` event only for that editor.
140 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
141 assert_eq!(
142 mem::take(&mut *events.borrow_mut()),
143 [
144 ("editor1", "edited"),
145 ("editor1", "buffer edited"),
146 ("editor2", "buffer edited"),
147 ]
148 );
149
150 // Redoing on editor 1 will emit an `Edited` event only for that editor.
151 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
152 assert_eq!(
153 mem::take(&mut *events.borrow_mut()),
154 [
155 ("editor1", "edited"),
156 ("editor1", "buffer edited"),
157 ("editor2", "buffer edited"),
158 ]
159 );
160
161 // Undoing on editor 2 will emit an `Edited` event only for that editor.
162 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
163 assert_eq!(
164 mem::take(&mut *events.borrow_mut()),
165 [
166 ("editor2", "edited"),
167 ("editor1", "buffer edited"),
168 ("editor2", "buffer edited"),
169 ]
170 );
171
172 // Redoing on editor 2 will emit an `Edited` event only for that editor.
173 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
174 assert_eq!(
175 mem::take(&mut *events.borrow_mut()),
176 [
177 ("editor2", "edited"),
178 ("editor1", "buffer edited"),
179 ("editor2", "buffer edited"),
180 ]
181 );
182
183 // No event is emitted when the mutation is a no-op.
184 _ = editor2.update(cx, |editor, window, cx| {
185 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
186 s.select_ranges([0..0])
187 });
188
189 editor.backspace(&Backspace, window, cx);
190 });
191 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
192}
193
194#[gpui::test]
195fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
196 init_test(cx, |_| {});
197
198 let mut now = Instant::now();
199 let group_interval = Duration::from_millis(1);
200 let buffer = cx.new(|cx| {
201 let mut buf = language::Buffer::local("123456", cx);
202 buf.set_group_interval(group_interval);
203 buf
204 });
205 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
206 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
207
208 _ = editor.update(cx, |editor, window, cx| {
209 editor.start_transaction_at(now, window, cx);
210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
211 s.select_ranges([2..4])
212 });
213
214 editor.insert("cd", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cd56");
217 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
218
219 editor.start_transaction_at(now, window, cx);
220 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
221 s.select_ranges([4..5])
222 });
223 editor.insert("e", window, cx);
224 editor.end_transaction_at(now, cx);
225 assert_eq!(editor.text(cx), "12cde6");
226 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
227
228 now += group_interval + Duration::from_millis(1);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([2..2])
231 });
232
233 // Simulate an edit in another editor
234 buffer.update(cx, |buffer, cx| {
235 buffer.start_transaction_at(now, cx);
236 buffer.edit([(0..1, "a")], None, cx);
237 buffer.edit([(1..1, "b")], None, cx);
238 buffer.end_transaction_at(now, cx);
239 });
240
241 assert_eq!(editor.text(cx), "ab2cde6");
242 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
243
244 // Last transaction happened past the group interval in a different editor.
245 // Undo it individually and don't restore selections.
246 editor.undo(&Undo, window, cx);
247 assert_eq!(editor.text(cx), "12cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
249
250 // First two transactions happened within the group interval in this editor.
251 // Undo them together and restore selections.
252 editor.undo(&Undo, window, cx);
253 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
254 assert_eq!(editor.text(cx), "123456");
255 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
256
257 // Redo the first two transactions together.
258 editor.redo(&Redo, window, cx);
259 assert_eq!(editor.text(cx), "12cde6");
260 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
261
262 // Redo the last transaction on its own.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "ab2cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
266
267 // Test empty transactions.
268 editor.start_transaction_at(now, window, cx);
269 editor.end_transaction_at(now, cx);
270 editor.undo(&Undo, window, cx);
271 assert_eq!(editor.text(cx), "12cde6");
272 });
273}
274
275#[gpui::test]
276fn test_ime_composition(cx: &mut TestAppContext) {
277 init_test(cx, |_| {});
278
279 let buffer = cx.new(|cx| {
280 let mut buffer = language::Buffer::local("abcde", cx);
281 // Ensure automatic grouping doesn't occur.
282 buffer.set_group_interval(Duration::ZERO);
283 buffer
284 });
285
286 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
287 cx.add_window(|window, cx| {
288 let mut editor = build_editor(buffer.clone(), window, cx);
289
290 // Start a new IME composition.
291 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
292 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
293 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
294 assert_eq!(editor.text(cx), "äbcde");
295 assert_eq!(
296 editor.marked_text_ranges(cx),
297 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
298 );
299
300 // Finalize IME composition.
301 editor.replace_text_in_range(None, "ā", window, cx);
302 assert_eq!(editor.text(cx), "ābcde");
303 assert_eq!(editor.marked_text_ranges(cx), None);
304
305 // IME composition edits are grouped and are undone/redone at once.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "abcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309 editor.redo(&Default::default(), window, cx);
310 assert_eq!(editor.text(cx), "ābcde");
311 assert_eq!(editor.marked_text_ranges(cx), None);
312
313 // Start a new IME composition.
314 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
315 assert_eq!(
316 editor.marked_text_ranges(cx),
317 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
318 );
319
320 // Undoing during an IME composition cancels it.
321 editor.undo(&Default::default(), window, cx);
322 assert_eq!(editor.text(cx), "ābcde");
323 assert_eq!(editor.marked_text_ranges(cx), None);
324
325 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
326 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
327 assert_eq!(editor.text(cx), "ābcdè");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
331 );
332
333 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
334 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
335 assert_eq!(editor.text(cx), "ābcdę");
336 assert_eq!(editor.marked_text_ranges(cx), None);
337
338 // Start a new IME composition with multiple cursors.
339 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
340 s.select_ranges([
341 OffsetUtf16(1)..OffsetUtf16(1),
342 OffsetUtf16(3)..OffsetUtf16(3),
343 OffsetUtf16(5)..OffsetUtf16(5),
344 ])
345 });
346 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
347 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
348 assert_eq!(
349 editor.marked_text_ranges(cx),
350 Some(vec![
351 OffsetUtf16(0)..OffsetUtf16(3),
352 OffsetUtf16(4)..OffsetUtf16(7),
353 OffsetUtf16(8)..OffsetUtf16(11)
354 ])
355 );
356
357 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
358 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
359 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
360 assert_eq!(
361 editor.marked_text_ranges(cx),
362 Some(vec![
363 OffsetUtf16(1)..OffsetUtf16(2),
364 OffsetUtf16(5)..OffsetUtf16(6),
365 OffsetUtf16(9)..OffsetUtf16(10)
366 ])
367 );
368
369 // Finalize IME composition with multiple cursors.
370 editor.replace_text_in_range(Some(9..10), "2", window, cx);
371 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
372 assert_eq!(editor.marked_text_ranges(cx), None);
373
374 editor
375 });
376}
377
378#[gpui::test]
379fn test_selection_with_mouse(cx: &mut TestAppContext) {
380 init_test(cx, |_| {});
381
382 let editor = cx.add_window(|window, cx| {
383 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
384 build_editor(buffer, window, cx)
385 });
386
387 _ = editor.update(cx, |editor, window, cx| {
388 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
389 });
390 assert_eq!(
391 editor
392 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
393 .unwrap(),
394 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
395 );
396
397 _ = editor.update(cx, |editor, window, cx| {
398 editor.update_selection(
399 DisplayPoint::new(DisplayRow(3), 3),
400 0,
401 gpui::Point::<f32>::default(),
402 window,
403 cx,
404 );
405 });
406
407 assert_eq!(
408 editor
409 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
410 .unwrap(),
411 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
412 );
413
414 _ = editor.update(cx, |editor, window, cx| {
415 editor.update_selection(
416 DisplayPoint::new(DisplayRow(1), 1),
417 0,
418 gpui::Point::<f32>::default(),
419 window,
420 cx,
421 );
422 });
423
424 assert_eq!(
425 editor
426 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
427 .unwrap(),
428 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
429 );
430
431 _ = editor.update(cx, |editor, window, cx| {
432 editor.end_selection(window, cx);
433 editor.update_selection(
434 DisplayPoint::new(DisplayRow(3), 3),
435 0,
436 gpui::Point::<f32>::default(),
437 window,
438 cx,
439 );
440 });
441
442 assert_eq!(
443 editor
444 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
445 .unwrap(),
446 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
447 );
448
449 _ = editor.update(cx, |editor, window, cx| {
450 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
451 editor.update_selection(
452 DisplayPoint::new(DisplayRow(0), 0),
453 0,
454 gpui::Point::<f32>::default(),
455 window,
456 cx,
457 );
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
463 .unwrap(),
464 [
465 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
466 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
467 ]
468 );
469
470 _ = editor.update(cx, |editor, window, cx| {
471 editor.end_selection(window, cx);
472 });
473
474 assert_eq!(
475 editor
476 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
477 .unwrap(),
478 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
479 );
480}
481
482#[gpui::test]
483fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
484 init_test(cx, |_| {});
485
486 let editor = cx.add_window(|window, cx| {
487 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
488 build_editor(buffer, window, cx)
489 });
490
491 _ = editor.update(cx, |editor, window, cx| {
492 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
493 });
494
495 _ = editor.update(cx, |editor, window, cx| {
496 editor.end_selection(window, cx);
497 });
498
499 _ = editor.update(cx, |editor, window, cx| {
500 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
501 });
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.end_selection(window, cx);
505 });
506
507 assert_eq!(
508 editor
509 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
510 .unwrap(),
511 [
512 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
513 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
514 ]
515 );
516
517 _ = editor.update(cx, |editor, window, cx| {
518 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
519 });
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.end_selection(window, cx);
523 });
524
525 assert_eq!(
526 editor
527 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
528 .unwrap(),
529 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
530 );
531}
532
533#[gpui::test]
534fn test_canceling_pending_selection(cx: &mut TestAppContext) {
535 init_test(cx, |_| {});
536
537 let editor = cx.add_window(|window, cx| {
538 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
539 build_editor(buffer, window, cx)
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
544 assert_eq!(
545 editor.selections.display_ranges(cx),
546 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
547 );
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(3), 3),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563
564 _ = editor.update(cx, |editor, window, cx| {
565 editor.cancel(&Cancel, window, cx);
566 editor.update_selection(
567 DisplayPoint::new(DisplayRow(1), 1),
568 0,
569 gpui::Point::<f32>::default(),
570 window,
571 cx,
572 );
573 assert_eq!(
574 editor.selections.display_ranges(cx),
575 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
576 );
577 });
578}
579
580#[gpui::test]
581fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
582 init_test(cx, |_| {});
583
584 let editor = cx.add_window(|window, cx| {
585 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
586 build_editor(buffer, window, cx)
587 });
588
589 _ = editor.update(cx, |editor, window, cx| {
590 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
591 assert_eq!(
592 editor.selections.display_ranges(cx),
593 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
594 );
595
596 editor.move_down(&Default::default(), window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
600 );
601
602 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
606 );
607
608 editor.move_up(&Default::default(), window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
612 );
613 });
614}
615
616#[gpui::test]
617fn test_clone(cx: &mut TestAppContext) {
618 init_test(cx, |_| {});
619
620 let (text, selection_ranges) = marked_text_ranges(
621 indoc! {"
622 one
623 two
624 threeˇ
625 four
626 fiveˇ
627 "},
628 true,
629 );
630
631 let editor = cx.add_window(|window, cx| {
632 let buffer = MultiBuffer::build_simple(&text, cx);
633 build_editor(buffer, window, cx)
634 });
635
636 _ = editor.update(cx, |editor, window, cx| {
637 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
638 s.select_ranges(selection_ranges.clone())
639 });
640 editor.fold_creases(
641 vec![
642 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
643 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
644 ],
645 true,
646 window,
647 cx,
648 );
649 });
650
651 let cloned_editor = editor
652 .update(cx, |editor, _, cx| {
653 cx.open_window(Default::default(), |window, cx| {
654 cx.new(|cx| editor.clone(window, cx))
655 })
656 })
657 .unwrap()
658 .unwrap();
659
660 let snapshot = editor
661 .update(cx, |e, window, cx| e.snapshot(window, cx))
662 .unwrap();
663 let cloned_snapshot = cloned_editor
664 .update(cx, |e, window, cx| e.snapshot(window, cx))
665 .unwrap();
666
667 assert_eq!(
668 cloned_editor
669 .update(cx, |e, _, cx| e.display_text(cx))
670 .unwrap(),
671 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
672 );
673 assert_eq!(
674 cloned_snapshot
675 .folds_in_range(0..text.len())
676 .collect::<Vec<_>>(),
677 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
678 );
679 assert_set_eq!(
680 cloned_editor
681 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
682 .unwrap(),
683 editor
684 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
685 .unwrap()
686 );
687 assert_set_eq!(
688 cloned_editor
689 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
690 .unwrap(),
691 editor
692 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
693 .unwrap()
694 );
695}
696
697#[gpui::test]
698async fn test_navigation_history(cx: &mut TestAppContext) {
699 init_test(cx, |_| {});
700
701 use workspace::item::Item;
702
703 let fs = FakeFs::new(cx.executor());
704 let project = Project::test(fs, [], cx).await;
705 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
706 let pane = workspace
707 .update(cx, |workspace, _, _| workspace.active_pane().clone())
708 .unwrap();
709
710 _ = workspace.update(cx, |_v, window, cx| {
711 cx.new(|cx| {
712 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
713 let mut editor = build_editor(buffer, window, cx);
714 let handle = cx.entity();
715 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
716
717 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
718 editor.nav_history.as_mut().unwrap().pop_backward(cx)
719 }
720
721 // Move the cursor a small distance.
722 // Nothing is added to the navigation history.
723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
724 s.select_display_ranges([
725 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
726 ])
727 });
728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
729 s.select_display_ranges([
730 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
731 ])
732 });
733 assert!(pop_history(&mut editor, cx).is_none());
734
735 // Move the cursor a large distance.
736 // The history can jump back to the previous position.
737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
738 s.select_display_ranges([
739 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
740 ])
741 });
742 let nav_entry = pop_history(&mut editor, cx).unwrap();
743 editor.navigate(nav_entry.data.unwrap(), window, cx);
744 assert_eq!(nav_entry.item.id(), cx.entity_id());
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
748 );
749 assert!(pop_history(&mut editor, cx).is_none());
750
751 // Move the cursor a small distance via the mouse.
752 // Nothing is added to the navigation history.
753 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
754 editor.end_selection(window, cx);
755 assert_eq!(
756 editor.selections.display_ranges(cx),
757 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
758 );
759 assert!(pop_history(&mut editor, cx).is_none());
760
761 // Move the cursor a large distance via the mouse.
762 // The history can jump back to the previous position.
763 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
764 editor.end_selection(window, cx);
765 assert_eq!(
766 editor.selections.display_ranges(cx),
767 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
768 );
769 let nav_entry = pop_history(&mut editor, cx).unwrap();
770 editor.navigate(nav_entry.data.unwrap(), window, cx);
771 assert_eq!(nav_entry.item.id(), cx.entity_id());
772 assert_eq!(
773 editor.selections.display_ranges(cx),
774 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
775 );
776 assert!(pop_history(&mut editor, cx).is_none());
777
778 // Set scroll position to check later
779 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
780 let original_scroll_position = editor.scroll_manager.anchor();
781
782 // Jump to the end of the document and adjust scroll
783 editor.move_to_end(&MoveToEnd, window, cx);
784 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
785 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
786
787 let nav_entry = pop_history(&mut editor, cx).unwrap();
788 editor.navigate(nav_entry.data.unwrap(), window, cx);
789 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
790
791 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
792 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
793 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
794 let invalid_point = Point::new(9999, 0);
795 editor.navigate(
796 Box::new(NavigationData {
797 cursor_anchor: invalid_anchor,
798 cursor_position: invalid_point,
799 scroll_anchor: ScrollAnchor {
800 anchor: invalid_anchor,
801 offset: Default::default(),
802 },
803 scroll_top_row: invalid_point.row,
804 }),
805 window,
806 cx,
807 );
808 assert_eq!(
809 editor.selections.display_ranges(cx),
810 &[editor.max_point(cx)..editor.max_point(cx)]
811 );
812 assert_eq!(
813 editor.scroll_position(cx),
814 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
815 );
816
817 editor
818 })
819 });
820}
821
822#[gpui::test]
823fn test_cancel(cx: &mut TestAppContext) {
824 init_test(cx, |_| {});
825
826 let editor = cx.add_window(|window, cx| {
827 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
828 build_editor(buffer, window, cx)
829 });
830
831 _ = editor.update(cx, |editor, window, cx| {
832 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
833 editor.update_selection(
834 DisplayPoint::new(DisplayRow(1), 1),
835 0,
836 gpui::Point::<f32>::default(),
837 window,
838 cx,
839 );
840 editor.end_selection(window, cx);
841
842 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
843 editor.update_selection(
844 DisplayPoint::new(DisplayRow(0), 3),
845 0,
846 gpui::Point::<f32>::default(),
847 window,
848 cx,
849 );
850 editor.end_selection(window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [
854 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
855 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
856 ]
857 );
858 });
859
860 _ = editor.update(cx, |editor, window, cx| {
861 editor.cancel(&Cancel, window, cx);
862 assert_eq!(
863 editor.selections.display_ranges(cx),
864 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
865 );
866 });
867
868 _ = editor.update(cx, |editor, window, cx| {
869 editor.cancel(&Cancel, window, cx);
870 assert_eq!(
871 editor.selections.display_ranges(cx),
872 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
873 );
874 });
875}
876
877#[gpui::test]
878fn test_fold_action(cx: &mut TestAppContext) {
879 init_test(cx, |_| {});
880
881 let editor = cx.add_window(|window, cx| {
882 let buffer = MultiBuffer::build_simple(
883 &"
884 impl Foo {
885 // Hello!
886
887 fn a() {
888 1
889 }
890
891 fn b() {
892 2
893 }
894
895 fn c() {
896 3
897 }
898 }
899 "
900 .unindent(),
901 cx,
902 );
903 build_editor(buffer, window, cx)
904 });
905
906 _ = editor.update(cx, |editor, window, cx| {
907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
908 s.select_display_ranges([
909 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
910 ]);
911 });
912 editor.fold(&Fold, window, cx);
913 assert_eq!(
914 editor.display_text(cx),
915 "
916 impl Foo {
917 // Hello!
918
919 fn a() {
920 1
921 }
922
923 fn b() {⋯
924 }
925
926 fn c() {⋯
927 }
928 }
929 "
930 .unindent(),
931 );
932
933 editor.fold(&Fold, window, cx);
934 assert_eq!(
935 editor.display_text(cx),
936 "
937 impl Foo {⋯
938 }
939 "
940 .unindent(),
941 );
942
943 editor.unfold_lines(&UnfoldLines, window, cx);
944 assert_eq!(
945 editor.display_text(cx),
946 "
947 impl Foo {
948 // Hello!
949
950 fn a() {
951 1
952 }
953
954 fn b() {⋯
955 }
956
957 fn c() {⋯
958 }
959 }
960 "
961 .unindent(),
962 );
963
964 editor.unfold_lines(&UnfoldLines, window, cx);
965 assert_eq!(
966 editor.display_text(cx),
967 editor.buffer.read(cx).read(cx).text()
968 );
969 });
970}
971
972#[gpui::test]
973fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
974 init_test(cx, |_| {});
975
976 let editor = cx.add_window(|window, cx| {
977 let buffer = MultiBuffer::build_simple(
978 &"
979 class Foo:
980 # Hello!
981
982 def a():
983 print(1)
984
985 def b():
986 print(2)
987
988 def c():
989 print(3)
990 "
991 .unindent(),
992 cx,
993 );
994 build_editor(buffer, window, cx)
995 });
996
997 _ = editor.update(cx, |editor, window, cx| {
998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
999 s.select_display_ranges([
1000 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1001 ]);
1002 });
1003 editor.fold(&Fold, window, cx);
1004 assert_eq!(
1005 editor.display_text(cx),
1006 "
1007 class Foo:
1008 # Hello!
1009
1010 def a():
1011 print(1)
1012
1013 def b():⋯
1014
1015 def c():⋯
1016 "
1017 .unindent(),
1018 );
1019
1020 editor.fold(&Fold, window, cx);
1021 assert_eq!(
1022 editor.display_text(cx),
1023 "
1024 class Foo:⋯
1025 "
1026 .unindent(),
1027 );
1028
1029 editor.unfold_lines(&UnfoldLines, window, cx);
1030 assert_eq!(
1031 editor.display_text(cx),
1032 "
1033 class Foo:
1034 # Hello!
1035
1036 def a():
1037 print(1)
1038
1039 def b():⋯
1040
1041 def c():⋯
1042 "
1043 .unindent(),
1044 );
1045
1046 editor.unfold_lines(&UnfoldLines, window, cx);
1047 assert_eq!(
1048 editor.display_text(cx),
1049 editor.buffer.read(cx).read(cx).text()
1050 );
1051 });
1052}
1053
1054#[gpui::test]
1055fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1056 init_test(cx, |_| {});
1057
1058 let editor = cx.add_window(|window, cx| {
1059 let buffer = MultiBuffer::build_simple(
1060 &"
1061 class Foo:
1062 # Hello!
1063
1064 def a():
1065 print(1)
1066
1067 def b():
1068 print(2)
1069
1070
1071 def c():
1072 print(3)
1073
1074
1075 "
1076 .unindent(),
1077 cx,
1078 );
1079 build_editor(buffer, window, cx)
1080 });
1081
1082 _ = editor.update(cx, |editor, window, cx| {
1083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1084 s.select_display_ranges([
1085 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1086 ]);
1087 });
1088 editor.fold(&Fold, window, cx);
1089 assert_eq!(
1090 editor.display_text(cx),
1091 "
1092 class Foo:
1093 # Hello!
1094
1095 def a():
1096 print(1)
1097
1098 def b():⋯
1099
1100
1101 def c():⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.fold(&Fold, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:⋯
1113
1114
1115 "
1116 .unindent(),
1117 );
1118
1119 editor.unfold_lines(&UnfoldLines, window, cx);
1120 assert_eq!(
1121 editor.display_text(cx),
1122 "
1123 class Foo:
1124 # Hello!
1125
1126 def a():
1127 print(1)
1128
1129 def b():⋯
1130
1131
1132 def c():⋯
1133
1134
1135 "
1136 .unindent(),
1137 );
1138
1139 editor.unfold_lines(&UnfoldLines, window, cx);
1140 assert_eq!(
1141 editor.display_text(cx),
1142 editor.buffer.read(cx).read(cx).text()
1143 );
1144 });
1145}
1146
1147#[gpui::test]
1148fn test_fold_at_level(cx: &mut TestAppContext) {
1149 init_test(cx, |_| {});
1150
1151 let editor = cx.add_window(|window, cx| {
1152 let buffer = MultiBuffer::build_simple(
1153 &"
1154 class Foo:
1155 # Hello!
1156
1157 def a():
1158 print(1)
1159
1160 def b():
1161 print(2)
1162
1163
1164 class Bar:
1165 # World!
1166
1167 def a():
1168 print(1)
1169
1170 def b():
1171 print(2)
1172
1173
1174 "
1175 .unindent(),
1176 cx,
1177 );
1178 build_editor(buffer, window, cx)
1179 });
1180
1181 _ = editor.update(cx, |editor, window, cx| {
1182 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1183 assert_eq!(
1184 editor.display_text(cx),
1185 "
1186 class Foo:
1187 # Hello!
1188
1189 def a():⋯
1190
1191 def b():⋯
1192
1193
1194 class Bar:
1195 # World!
1196
1197 def a():⋯
1198
1199 def b():⋯
1200
1201
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:⋯
1211
1212
1213 class Bar:⋯
1214
1215
1216 "
1217 .unindent(),
1218 );
1219
1220 editor.unfold_all(&UnfoldAll, window, cx);
1221 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1222 assert_eq!(
1223 editor.display_text(cx),
1224 "
1225 class Foo:
1226 # Hello!
1227
1228 def a():
1229 print(1)
1230
1231 def b():
1232 print(2)
1233
1234
1235 class Bar:
1236 # World!
1237
1238 def a():
1239 print(1)
1240
1241 def b():
1242 print(2)
1243
1244
1245 "
1246 .unindent(),
1247 );
1248
1249 assert_eq!(
1250 editor.display_text(cx),
1251 editor.buffer.read(cx).read(cx).text()
1252 );
1253 });
1254}
1255
1256#[gpui::test]
1257fn test_move_cursor(cx: &mut TestAppContext) {
1258 init_test(cx, |_| {});
1259
1260 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1261 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1262
1263 buffer.update(cx, |buffer, cx| {
1264 buffer.edit(
1265 vec![
1266 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1267 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1268 ],
1269 None,
1270 cx,
1271 );
1272 });
1273 _ = editor.update(cx, |editor, window, cx| {
1274 assert_eq!(
1275 editor.selections.display_ranges(cx),
1276 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1277 );
1278
1279 editor.move_down(&MoveDown, window, cx);
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1283 );
1284
1285 editor.move_right(&MoveRight, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1289 );
1290
1291 editor.move_left(&MoveLeft, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1295 );
1296
1297 editor.move_up(&MoveUp, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1301 );
1302
1303 editor.move_to_end(&MoveToEnd, window, cx);
1304 assert_eq!(
1305 editor.selections.display_ranges(cx),
1306 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1307 );
1308
1309 editor.move_to_beginning(&MoveToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1316 s.select_display_ranges([
1317 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1318 ]);
1319 });
1320 editor.select_to_beginning(&SelectToBeginning, window, cx);
1321 assert_eq!(
1322 editor.selections.display_ranges(cx),
1323 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1324 );
1325
1326 editor.select_to_end(&SelectToEnd, window, cx);
1327 assert_eq!(
1328 editor.selections.display_ranges(cx),
1329 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1330 );
1331 });
1332}
1333
1334#[gpui::test]
1335fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1336 init_test(cx, |_| {});
1337
1338 let editor = cx.add_window(|window, cx| {
1339 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1340 build_editor(buffer, window, cx)
1341 });
1342
1343 assert_eq!('🟥'.len_utf8(), 4);
1344 assert_eq!('α'.len_utf8(), 2);
1345
1346 _ = editor.update(cx, |editor, window, cx| {
1347 editor.fold_creases(
1348 vec![
1349 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1350 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1351 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1352 ],
1353 true,
1354 window,
1355 cx,
1356 );
1357 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1358
1359 editor.move_right(&MoveRight, window, cx);
1360 assert_eq!(
1361 editor.selections.display_ranges(cx),
1362 &[empty_range(0, "🟥".len())]
1363 );
1364 editor.move_right(&MoveRight, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(0, "🟥🟧".len())]
1368 );
1369 editor.move_right(&MoveRight, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(0, "🟥🟧⋯".len())]
1373 );
1374
1375 editor.move_down(&MoveDown, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(1, "ab⋯e".len())]
1379 );
1380 editor.move_left(&MoveLeft, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(1, "ab⋯".len())]
1384 );
1385 editor.move_left(&MoveLeft, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(1, "ab".len())]
1389 );
1390 editor.move_left(&MoveLeft, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(1, "a".len())]
1394 );
1395
1396 editor.move_down(&MoveDown, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(2, "α".len())]
1400 );
1401 editor.move_right(&MoveRight, window, cx);
1402 assert_eq!(
1403 editor.selections.display_ranges(cx),
1404 &[empty_range(2, "αβ".len())]
1405 );
1406 editor.move_right(&MoveRight, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(2, "αβ⋯".len())]
1410 );
1411 editor.move_right(&MoveRight, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416
1417 editor.move_up(&MoveUp, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(1, "ab⋯e".len())]
1421 );
1422 editor.move_down(&MoveDown, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(2, "αβ⋯ε".len())]
1426 );
1427 editor.move_up(&MoveUp, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(1, "ab⋯e".len())]
1431 );
1432
1433 editor.move_up(&MoveUp, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(0, "🟥🟧".len())]
1437 );
1438 editor.move_left(&MoveLeft, window, cx);
1439 assert_eq!(
1440 editor.selections.display_ranges(cx),
1441 &[empty_range(0, "🟥".len())]
1442 );
1443 editor.move_left(&MoveLeft, window, cx);
1444 assert_eq!(
1445 editor.selections.display_ranges(cx),
1446 &[empty_range(0, "".len())]
1447 );
1448 });
1449}
1450
1451#[gpui::test]
1452fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1453 init_test(cx, |_| {});
1454
1455 let editor = cx.add_window(|window, cx| {
1456 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1457 build_editor(buffer, window, cx)
1458 });
1459 _ = editor.update(cx, |editor, window, cx| {
1460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1461 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1462 });
1463
1464 // moving above start of document should move selection to start of document,
1465 // but the next move down should still be at the original goal_x
1466 editor.move_up(&MoveUp, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(0, "".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(1, "abcd".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(2, "αβγ".len())]
1482 );
1483
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(3, "abcd".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1494 );
1495
1496 // moving past end of document should not change goal_x
1497 editor.move_down(&MoveDown, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(5, "".len())]
1501 );
1502
1503 editor.move_down(&MoveDown, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(5, "".len())]
1507 );
1508
1509 editor.move_up(&MoveUp, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1513 );
1514
1515 editor.move_up(&MoveUp, window, cx);
1516 assert_eq!(
1517 editor.selections.display_ranges(cx),
1518 &[empty_range(3, "abcd".len())]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 editor.selections.display_ranges(cx),
1524 &[empty_range(2, "αβγ".len())]
1525 );
1526 });
1527}
1528
1529#[gpui::test]
1530fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1531 init_test(cx, |_| {});
1532 let move_to_beg = MoveToBeginningOfLine {
1533 stop_at_soft_wraps: true,
1534 stop_at_indent: true,
1535 };
1536
1537 let delete_to_beg = DeleteToBeginningOfLine {
1538 stop_at_indent: false,
1539 };
1540
1541 let move_to_end = MoveToEndOfLine {
1542 stop_at_soft_wraps: true,
1543 };
1544
1545 let editor = cx.add_window(|window, cx| {
1546 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1547 build_editor(buffer, window, cx)
1548 });
1549 _ = editor.update(cx, |editor, window, cx| {
1550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1551 s.select_display_ranges([
1552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1553 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
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), 2)..DisplayPoint::new(DisplayRow(1), 2),
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), 0)..DisplayPoint::new(DisplayRow(1), 0),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1586 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1587 ]
1588 );
1589 });
1590
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_to_end_of_line(&move_to_end, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[
1596 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1597 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1598 ]
1599 );
1600 });
1601
1602 // Moving to the end of line again is a no-op.
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_to_end_of_line(&move_to_end, window, cx);
1605 assert_eq!(
1606 editor.selections.display_ranges(cx),
1607 &[
1608 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1609 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1610 ]
1611 );
1612 });
1613
1614 _ = editor.update(cx, |editor, window, cx| {
1615 editor.move_left(&MoveLeft, window, cx);
1616 editor.select_to_beginning_of_line(
1617 &SelectToBeginningOfLine {
1618 stop_at_soft_wraps: true,
1619 stop_at_indent: true,
1620 },
1621 window,
1622 cx,
1623 );
1624 assert_eq!(
1625 editor.selections.display_ranges(cx),
1626 &[
1627 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1628 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1629 ]
1630 );
1631 });
1632
1633 _ = editor.update(cx, |editor, window, cx| {
1634 editor.select_to_beginning_of_line(
1635 &SelectToBeginningOfLine {
1636 stop_at_soft_wraps: true,
1637 stop_at_indent: true,
1638 },
1639 window,
1640 cx,
1641 );
1642 assert_eq!(
1643 editor.selections.display_ranges(cx),
1644 &[
1645 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1646 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1647 ]
1648 );
1649 });
1650
1651 _ = editor.update(cx, |editor, window, cx| {
1652 editor.select_to_beginning_of_line(
1653 &SelectToBeginningOfLine {
1654 stop_at_soft_wraps: true,
1655 stop_at_indent: true,
1656 },
1657 window,
1658 cx,
1659 );
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[
1663 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1664 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1665 ]
1666 );
1667 });
1668
1669 _ = editor.update(cx, |editor, window, cx| {
1670 editor.select_to_end_of_line(
1671 &SelectToEndOfLine {
1672 stop_at_soft_wraps: true,
1673 },
1674 window,
1675 cx,
1676 );
1677 assert_eq!(
1678 editor.selections.display_ranges(cx),
1679 &[
1680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1681 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1682 ]
1683 );
1684 });
1685
1686 _ = editor.update(cx, |editor, window, cx| {
1687 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1688 assert_eq!(editor.display_text(cx), "ab\n de");
1689 assert_eq!(
1690 editor.selections.display_ranges(cx),
1691 &[
1692 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1693 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1694 ]
1695 );
1696 });
1697
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1700 assert_eq!(editor.display_text(cx), "\n");
1701 assert_eq!(
1702 editor.selections.display_ranges(cx),
1703 &[
1704 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1706 ]
1707 );
1708 });
1709}
1710
1711#[gpui::test]
1712fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1713 init_test(cx, |_| {});
1714 let move_to_beg = MoveToBeginningOfLine {
1715 stop_at_soft_wraps: false,
1716 stop_at_indent: false,
1717 };
1718
1719 let move_to_end = MoveToEndOfLine {
1720 stop_at_soft_wraps: false,
1721 };
1722
1723 let editor = cx.add_window(|window, cx| {
1724 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1725 build_editor(buffer, window, cx)
1726 });
1727
1728 _ = editor.update(cx, |editor, window, cx| {
1729 editor.set_wrap_width(Some(140.0.into()), cx);
1730
1731 // We expect the following lines after wrapping
1732 // ```
1733 // thequickbrownfox
1734 // jumpedoverthelazydo
1735 // gs
1736 // ```
1737 // The final `gs` was soft-wrapped onto a new line.
1738 assert_eq!(
1739 "thequickbrownfox\njumpedoverthelaz\nydogs",
1740 editor.display_text(cx),
1741 );
1742
1743 // First, let's assert behavior on the first line, that was not soft-wrapped.
1744 // Start the cursor at the `k` on the first line
1745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1746 s.select_display_ranges([
1747 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1748 ]);
1749 });
1750
1751 // Moving to the beginning of the line should put us at the beginning of the line.
1752 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1753 assert_eq!(
1754 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1755 editor.selections.display_ranges(cx)
1756 );
1757
1758 // Moving to the end of the line should put us at the end of the line.
1759 editor.move_to_end_of_line(&move_to_end, window, cx);
1760 assert_eq!(
1761 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1762 editor.selections.display_ranges(cx)
1763 );
1764
1765 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1766 // Start the cursor at the last line (`y` that was wrapped to a new line)
1767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1768 s.select_display_ranges([
1769 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1770 ]);
1771 });
1772
1773 // Moving to the beginning of the line should put us at the start of the second line of
1774 // display text, i.e., the `j`.
1775 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1776 assert_eq!(
1777 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1778 editor.selections.display_ranges(cx)
1779 );
1780
1781 // Moving to the beginning of the line again should be a no-op.
1782 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1783 assert_eq!(
1784 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1785 editor.selections.display_ranges(cx)
1786 );
1787
1788 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1789 // next display line.
1790 editor.move_to_end_of_line(&move_to_end, window, cx);
1791 assert_eq!(
1792 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1793 editor.selections.display_ranges(cx)
1794 );
1795
1796 // Moving to the end of the line again should be a no-op.
1797 editor.move_to_end_of_line(&move_to_end, window, cx);
1798 assert_eq!(
1799 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1800 editor.selections.display_ranges(cx)
1801 );
1802 });
1803}
1804
1805#[gpui::test]
1806fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1807 init_test(cx, |_| {});
1808
1809 let move_to_beg = MoveToBeginningOfLine {
1810 stop_at_soft_wraps: true,
1811 stop_at_indent: true,
1812 };
1813
1814 let select_to_beg = SelectToBeginningOfLine {
1815 stop_at_soft_wraps: true,
1816 stop_at_indent: true,
1817 };
1818
1819 let delete_to_beg = DeleteToBeginningOfLine {
1820 stop_at_indent: true,
1821 };
1822
1823 let move_to_end = MoveToEndOfLine {
1824 stop_at_soft_wraps: false,
1825 };
1826
1827 let editor = cx.add_window(|window, cx| {
1828 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1829 build_editor(buffer, window, cx)
1830 });
1831
1832 _ = editor.update(cx, |editor, window, cx| {
1833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1834 s.select_display_ranges([
1835 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1836 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1837 ]);
1838 });
1839
1840 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1841 // and the second cursor at the first non-whitespace character in 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), 2)..DisplayPoint::new(DisplayRow(1), 2),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should be a no-op for the first cursor,
1852 // and should move the second cursor to the beginning of 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), 0)..DisplayPoint::new(DisplayRow(1), 0),
1859 ]
1860 );
1861
1862 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1863 // and should move the second cursor back to the first non-whitespace character in the line.
1864 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1874 // and to the first non-whitespace character in the line for the second cursor.
1875 editor.move_to_end_of_line(&move_to_end, window, cx);
1876 editor.move_left(&MoveLeft, window, cx);
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), 2),
1883 ]
1884 );
1885
1886 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1887 // and should select to the beginning of the line for the second cursor.
1888 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1889 assert_eq!(
1890 editor.selections.display_ranges(cx),
1891 &[
1892 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1893 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1894 ]
1895 );
1896
1897 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1898 // and should delete to the first non-whitespace character in the line for the second cursor.
1899 editor.move_to_end_of_line(&move_to_end, window, cx);
1900 editor.move_left(&MoveLeft, window, cx);
1901 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1902 assert_eq!(editor.text(cx), "c\n f");
1903 });
1904}
1905
1906#[gpui::test]
1907fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1908 init_test(cx, |_| {});
1909
1910 let move_to_beg = MoveToBeginningOfLine {
1911 stop_at_soft_wraps: true,
1912 stop_at_indent: true,
1913 };
1914
1915 let editor = cx.add_window(|window, cx| {
1916 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1917 build_editor(buffer, window, cx)
1918 });
1919
1920 _ = editor.update(cx, |editor, window, cx| {
1921 // test cursor between line_start and indent_start
1922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1923 s.select_display_ranges([
1924 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1925 ]);
1926 });
1927
1928 // cursor should move to line_start
1929 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1930 assert_eq!(
1931 editor.selections.display_ranges(cx),
1932 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1933 );
1934
1935 // cursor should move to indent_start
1936 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1937 assert_eq!(
1938 editor.selections.display_ranges(cx),
1939 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1940 );
1941
1942 // cursor should move to back to line_start
1943 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1944 assert_eq!(
1945 editor.selections.display_ranges(cx),
1946 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1947 );
1948 });
1949}
1950
1951#[gpui::test]
1952fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1953 init_test(cx, |_| {});
1954
1955 let editor = cx.add_window(|window, cx| {
1956 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1957 build_editor(buffer, window, cx)
1958 });
1959 _ = editor.update(cx, |editor, window, cx| {
1960 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1961 s.select_display_ranges([
1962 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1963 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1964 ])
1965 });
1966 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1967 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1968
1969 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1970 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1971
1972 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1973 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1974
1975 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1976 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1977
1978 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1979 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1989
1990 editor.move_right(&MoveRight, window, cx);
1991 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1992 assert_selection_ranges(
1993 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1994 editor,
1995 cx,
1996 );
1997
1998 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1999 assert_selection_ranges(
2000 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2001 editor,
2002 cx,
2003 );
2004
2005 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2006 assert_selection_ranges(
2007 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2008 editor,
2009 cx,
2010 );
2011 });
2012}
2013
2014#[gpui::test]
2015fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2016 init_test(cx, |_| {});
2017
2018 let editor = cx.add_window(|window, cx| {
2019 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2020 build_editor(buffer, window, cx)
2021 });
2022
2023 _ = editor.update(cx, |editor, window, cx| {
2024 editor.set_wrap_width(Some(140.0.into()), cx);
2025 assert_eq!(
2026 editor.display_text(cx),
2027 "use one::{\n two::three::\n four::five\n};"
2028 );
2029
2030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2031 s.select_display_ranges([
2032 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2033 ]);
2034 });
2035
2036 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2037 assert_eq!(
2038 editor.selections.display_ranges(cx),
2039 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2040 );
2041
2042 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2043 assert_eq!(
2044 editor.selections.display_ranges(cx),
2045 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2046 );
2047
2048 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2049 assert_eq!(
2050 editor.selections.display_ranges(cx),
2051 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2052 );
2053
2054 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2055 assert_eq!(
2056 editor.selections.display_ranges(cx),
2057 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2058 );
2059
2060 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2061 assert_eq!(
2062 editor.selections.display_ranges(cx),
2063 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2064 );
2065
2066 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2067 assert_eq!(
2068 editor.selections.display_ranges(cx),
2069 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2070 );
2071 });
2072}
2073
2074#[gpui::test]
2075async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2076 init_test(cx, |_| {});
2077 let mut cx = EditorTestContext::new(cx).await;
2078
2079 let line_height = cx.editor(|editor, window, _| {
2080 editor
2081 .style()
2082 .unwrap()
2083 .text
2084 .line_height_in_pixels(window.rem_size())
2085 });
2086 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2087
2088 cx.set_state(
2089 &r#"ˇone
2090 two
2091
2092 three
2093 fourˇ
2094 five
2095
2096 six"#
2097 .unindent(),
2098 );
2099
2100 cx.update_editor(|editor, window, cx| {
2101 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2102 });
2103 cx.assert_editor_state(
2104 &r#"one
2105 two
2106 ˇ
2107 three
2108 four
2109 five
2110 ˇ
2111 six"#
2112 .unindent(),
2113 );
2114
2115 cx.update_editor(|editor, window, cx| {
2116 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2117 });
2118 cx.assert_editor_state(
2119 &r#"one
2120 two
2121
2122 three
2123 four
2124 five
2125 ˇ
2126 sixˇ"#
2127 .unindent(),
2128 );
2129
2130 cx.update_editor(|editor, window, cx| {
2131 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2132 });
2133 cx.assert_editor_state(
2134 &r#"one
2135 two
2136
2137 three
2138 four
2139 five
2140
2141 sixˇ"#
2142 .unindent(),
2143 );
2144
2145 cx.update_editor(|editor, window, cx| {
2146 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2147 });
2148 cx.assert_editor_state(
2149 &r#"one
2150 two
2151
2152 three
2153 four
2154 five
2155 ˇ
2156 six"#
2157 .unindent(),
2158 );
2159
2160 cx.update_editor(|editor, window, cx| {
2161 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2162 });
2163 cx.assert_editor_state(
2164 &r#"one
2165 two
2166 ˇ
2167 three
2168 four
2169 five
2170
2171 six"#
2172 .unindent(),
2173 );
2174
2175 cx.update_editor(|editor, window, cx| {
2176 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2177 });
2178 cx.assert_editor_state(
2179 &r#"ˇone
2180 two
2181
2182 three
2183 four
2184 five
2185
2186 six"#
2187 .unindent(),
2188 );
2189}
2190
2191#[gpui::test]
2192async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2193 init_test(cx, |_| {});
2194 let mut cx = EditorTestContext::new(cx).await;
2195 let line_height = cx.editor(|editor, window, _| {
2196 editor
2197 .style()
2198 .unwrap()
2199 .text
2200 .line_height_in_pixels(window.rem_size())
2201 });
2202 let window = cx.window;
2203 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2204
2205 cx.set_state(
2206 r#"ˇone
2207 two
2208 three
2209 four
2210 five
2211 six
2212 seven
2213 eight
2214 nine
2215 ten
2216 "#,
2217 );
2218
2219 cx.update_editor(|editor, window, cx| {
2220 assert_eq!(
2221 editor.snapshot(window, cx).scroll_position(),
2222 gpui::Point::new(0., 0.)
2223 );
2224 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 3.)
2228 );
2229 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2230 assert_eq!(
2231 editor.snapshot(window, cx).scroll_position(),
2232 gpui::Point::new(0., 6.)
2233 );
2234 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2235 assert_eq!(
2236 editor.snapshot(window, cx).scroll_position(),
2237 gpui::Point::new(0., 3.)
2238 );
2239
2240 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2241 assert_eq!(
2242 editor.snapshot(window, cx).scroll_position(),
2243 gpui::Point::new(0., 1.)
2244 );
2245 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.)
2249 );
2250 });
2251}
2252
2253#[gpui::test]
2254async fn test_autoscroll(cx: &mut TestAppContext) {
2255 init_test(cx, |_| {});
2256 let mut cx = EditorTestContext::new(cx).await;
2257
2258 let line_height = cx.update_editor(|editor, window, cx| {
2259 editor.set_vertical_scroll_margin(2, cx);
2260 editor
2261 .style()
2262 .unwrap()
2263 .text
2264 .line_height_in_pixels(window.rem_size())
2265 });
2266 let window = cx.window;
2267 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2268
2269 cx.set_state(
2270 r#"ˇone
2271 two
2272 three
2273 four
2274 five
2275 six
2276 seven
2277 eight
2278 nine
2279 ten
2280 "#,
2281 );
2282 cx.update_editor(|editor, window, cx| {
2283 assert_eq!(
2284 editor.snapshot(window, cx).scroll_position(),
2285 gpui::Point::new(0., 0.0)
2286 );
2287 });
2288
2289 // Add a cursor below the visible area. Since both cursors cannot fit
2290 // on screen, the editor autoscrolls to reveal the newest cursor, and
2291 // allows the vertical scroll margin below that cursor.
2292 cx.update_editor(|editor, window, cx| {
2293 editor.change_selections(Default::default(), window, cx, |selections| {
2294 selections.select_ranges([
2295 Point::new(0, 0)..Point::new(0, 0),
2296 Point::new(6, 0)..Point::new(6, 0),
2297 ]);
2298 })
2299 });
2300 cx.update_editor(|editor, window, cx| {
2301 assert_eq!(
2302 editor.snapshot(window, cx).scroll_position(),
2303 gpui::Point::new(0., 3.0)
2304 );
2305 });
2306
2307 // Move down. The editor cursor scrolls down to track the newest cursor.
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_down(&Default::default(), window, cx);
2310 });
2311 cx.update_editor(|editor, window, cx| {
2312 assert_eq!(
2313 editor.snapshot(window, cx).scroll_position(),
2314 gpui::Point::new(0., 4.0)
2315 );
2316 });
2317
2318 // Add a cursor above the visible area. Since both cursors fit on screen,
2319 // the editor scrolls to show both.
2320 cx.update_editor(|editor, window, cx| {
2321 editor.change_selections(Default::default(), window, cx, |selections| {
2322 selections.select_ranges([
2323 Point::new(1, 0)..Point::new(1, 0),
2324 Point::new(6, 0)..Point::new(6, 0),
2325 ]);
2326 })
2327 });
2328 cx.update_editor(|editor, window, cx| {
2329 assert_eq!(
2330 editor.snapshot(window, cx).scroll_position(),
2331 gpui::Point::new(0., 1.0)
2332 );
2333 });
2334}
2335
2336#[gpui::test]
2337async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2338 init_test(cx, |_| {});
2339 let mut cx = EditorTestContext::new(cx).await;
2340
2341 let line_height = cx.editor(|editor, window, _cx| {
2342 editor
2343 .style()
2344 .unwrap()
2345 .text
2346 .line_height_in_pixels(window.rem_size())
2347 });
2348 let window = cx.window;
2349 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2350 cx.set_state(
2351 &r#"
2352 ˇone
2353 two
2354 threeˇ
2355 four
2356 five
2357 six
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| {
2367 editor.move_page_down(&MovePageDown::default(), window, cx)
2368 });
2369 cx.assert_editor_state(
2370 &r#"
2371 one
2372 two
2373 three
2374 ˇfour
2375 five
2376 sixˇ
2377 seven
2378 eight
2379 nine
2380 ten
2381 "#
2382 .unindent(),
2383 );
2384
2385 cx.update_editor(|editor, 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 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2405 cx.assert_editor_state(
2406 &r#"
2407 one
2408 two
2409 three
2410 ˇfour
2411 five
2412 sixˇ
2413 seven
2414 eight
2415 nine
2416 ten
2417 "#
2418 .unindent(),
2419 );
2420
2421 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2422 cx.assert_editor_state(
2423 &r#"
2424 ˇone
2425 two
2426 threeˇ
2427 four
2428 five
2429 six
2430 seven
2431 eight
2432 nine
2433 ten
2434 "#
2435 .unindent(),
2436 );
2437
2438 // Test select collapsing
2439 cx.update_editor(|editor, window, cx| {
2440 editor.move_page_down(&MovePageDown::default(), window, cx);
2441 editor.move_page_down(&MovePageDown::default(), window, cx);
2442 editor.move_page_down(&MovePageDown::default(), window, cx);
2443 });
2444 cx.assert_editor_state(
2445 &r#"
2446 one
2447 two
2448 three
2449 four
2450 five
2451 six
2452 seven
2453 eight
2454 nine
2455 ˇten
2456 ˇ"#
2457 .unindent(),
2458 );
2459}
2460
2461#[gpui::test]
2462async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2463 init_test(cx, |_| {});
2464 let mut cx = EditorTestContext::new(cx).await;
2465 cx.set_state("one «two threeˇ» four");
2466 cx.update_editor(|editor, window, cx| {
2467 editor.delete_to_beginning_of_line(
2468 &DeleteToBeginningOfLine {
2469 stop_at_indent: false,
2470 },
2471 window,
2472 cx,
2473 );
2474 assert_eq!(editor.text(cx), " four");
2475 });
2476}
2477
2478#[gpui::test]
2479fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481
2482 let editor = cx.add_window(|window, cx| {
2483 let buffer = MultiBuffer::build_simple("one two three four", cx);
2484 build_editor(buffer, window, cx)
2485 });
2486
2487 _ = editor.update(cx, |editor, window, cx| {
2488 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2489 s.select_display_ranges([
2490 // an empty selection - the preceding word fragment is deleted
2491 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2492 // characters selected - they are deleted
2493 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2494 ])
2495 });
2496 editor.delete_to_previous_word_start(
2497 &DeleteToPreviousWordStart {
2498 ignore_newlines: false,
2499 },
2500 window,
2501 cx,
2502 );
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2504 });
2505
2506 _ = editor.update(cx, |editor, window, cx| {
2507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2508 s.select_display_ranges([
2509 // an empty selection - the following word fragment is deleted
2510 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2511 // characters selected - they are deleted
2512 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2513 ])
2514 });
2515 editor.delete_to_next_word_end(
2516 &DeleteToNextWordEnd {
2517 ignore_newlines: false,
2518 },
2519 window,
2520 cx,
2521 );
2522 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2523 });
2524}
2525
2526#[gpui::test]
2527fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2528 init_test(cx, |_| {});
2529
2530 let editor = cx.add_window(|window, cx| {
2531 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2532 build_editor(buffer, window, cx)
2533 });
2534 let del_to_prev_word_start = DeleteToPreviousWordStart {
2535 ignore_newlines: false,
2536 };
2537 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2538 ignore_newlines: true,
2539 };
2540
2541 _ = editor.update(cx, |editor, window, cx| {
2542 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2543 s.select_display_ranges([
2544 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2545 ])
2546 });
2547 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2549 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2550 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2551 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2553 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2555 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2557 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2558 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2559 });
2560}
2561
2562#[gpui::test]
2563fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2564 init_test(cx, |_| {});
2565
2566 let editor = cx.add_window(|window, cx| {
2567 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2568 build_editor(buffer, window, cx)
2569 });
2570 let del_to_next_word_end = DeleteToNextWordEnd {
2571 ignore_newlines: false,
2572 };
2573 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2574 ignore_newlines: true,
2575 };
2576
2577 _ = editor.update(cx, |editor, window, cx| {
2578 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2579 s.select_display_ranges([
2580 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2581 ])
2582 });
2583 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2584 assert_eq!(
2585 editor.buffer.read(cx).read(cx).text(),
2586 "one\n two\nthree\n four"
2587 );
2588 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2589 assert_eq!(
2590 editor.buffer.read(cx).read(cx).text(),
2591 "\n two\nthree\n four"
2592 );
2593 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2594 assert_eq!(
2595 editor.buffer.read(cx).read(cx).text(),
2596 "two\nthree\n four"
2597 );
2598 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2599 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2600 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2601 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2602 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2603 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2604 });
2605}
2606
2607#[gpui::test]
2608fn test_newline(cx: &mut TestAppContext) {
2609 init_test(cx, |_| {});
2610
2611 let editor = cx.add_window(|window, cx| {
2612 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2613 build_editor(buffer, window, cx)
2614 });
2615
2616 _ = editor.update(cx, |editor, window, cx| {
2617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2618 s.select_display_ranges([
2619 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2620 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2621 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2622 ])
2623 });
2624
2625 editor.newline(&Newline, window, cx);
2626 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2627 });
2628}
2629
2630#[gpui::test]
2631fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2632 init_test(cx, |_| {});
2633
2634 let editor = cx.add_window(|window, cx| {
2635 let buffer = MultiBuffer::build_simple(
2636 "
2637 a
2638 b(
2639 X
2640 )
2641 c(
2642 X
2643 )
2644 "
2645 .unindent()
2646 .as_str(),
2647 cx,
2648 );
2649 let mut editor = build_editor(buffer, window, cx);
2650 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2651 s.select_ranges([
2652 Point::new(2, 4)..Point::new(2, 5),
2653 Point::new(5, 4)..Point::new(5, 5),
2654 ])
2655 });
2656 editor
2657 });
2658
2659 _ = editor.update(cx, |editor, window, cx| {
2660 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2661 editor.buffer.update(cx, |buffer, cx| {
2662 buffer.edit(
2663 [
2664 (Point::new(1, 2)..Point::new(3, 0), ""),
2665 (Point::new(4, 2)..Point::new(6, 0), ""),
2666 ],
2667 None,
2668 cx,
2669 );
2670 assert_eq!(
2671 buffer.read(cx).text(),
2672 "
2673 a
2674 b()
2675 c()
2676 "
2677 .unindent()
2678 );
2679 });
2680 assert_eq!(
2681 editor.selections.ranges(cx),
2682 &[
2683 Point::new(1, 2)..Point::new(1, 2),
2684 Point::new(2, 2)..Point::new(2, 2),
2685 ],
2686 );
2687
2688 editor.newline(&Newline, window, cx);
2689 assert_eq!(
2690 editor.text(cx),
2691 "
2692 a
2693 b(
2694 )
2695 c(
2696 )
2697 "
2698 .unindent()
2699 );
2700
2701 // The selections are moved after the inserted newlines
2702 assert_eq!(
2703 editor.selections.ranges(cx),
2704 &[
2705 Point::new(2, 0)..Point::new(2, 0),
2706 Point::new(4, 0)..Point::new(4, 0),
2707 ],
2708 );
2709 });
2710}
2711
2712#[gpui::test]
2713async fn test_newline_above(cx: &mut TestAppContext) {
2714 init_test(cx, |settings| {
2715 settings.defaults.tab_size = NonZeroU32::new(4)
2716 });
2717
2718 let language = Arc::new(
2719 Language::new(
2720 LanguageConfig::default(),
2721 Some(tree_sitter_rust::LANGUAGE.into()),
2722 )
2723 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2724 .unwrap(),
2725 );
2726
2727 let mut cx = EditorTestContext::new(cx).await;
2728 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2729 cx.set_state(indoc! {"
2730 const a: ˇA = (
2731 (ˇ
2732 «const_functionˇ»(ˇ),
2733 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2734 )ˇ
2735 ˇ);ˇ
2736 "});
2737
2738 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2739 cx.assert_editor_state(indoc! {"
2740 ˇ
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 ˇ
2746 const_function(),
2747 ˇ
2748 ˇ
2749 ˇ
2750 ˇ
2751 something_else,
2752 ˇ
2753 )
2754 ˇ
2755 ˇ
2756 );
2757 "});
2758}
2759
2760#[gpui::test]
2761async fn test_newline_below(cx: &mut TestAppContext) {
2762 init_test(cx, |settings| {
2763 settings.defaults.tab_size = NonZeroU32::new(4)
2764 });
2765
2766 let language = Arc::new(
2767 Language::new(
2768 LanguageConfig::default(),
2769 Some(tree_sitter_rust::LANGUAGE.into()),
2770 )
2771 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2772 .unwrap(),
2773 );
2774
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 const a: ˇA = (
2779 (ˇ
2780 «const_functionˇ»(ˇ),
2781 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2782 )ˇ
2783 ˇ);ˇ
2784 "});
2785
2786 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2787 cx.assert_editor_state(indoc! {"
2788 const a: A = (
2789 ˇ
2790 (
2791 ˇ
2792 const_function(),
2793 ˇ
2794 ˇ
2795 something_else,
2796 ˇ
2797 ˇ
2798 ˇ
2799 ˇ
2800 )
2801 ˇ
2802 );
2803 ˇ
2804 ˇ
2805 "});
2806}
2807
2808#[gpui::test]
2809async fn test_newline_comments(cx: &mut TestAppContext) {
2810 init_test(cx, |settings| {
2811 settings.defaults.tab_size = NonZeroU32::new(4)
2812 });
2813
2814 let language = Arc::new(Language::new(
2815 LanguageConfig {
2816 line_comments: vec!["// ".into()],
2817 ..LanguageConfig::default()
2818 },
2819 None,
2820 ));
2821 {
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2824 cx.set_state(indoc! {"
2825 // Fooˇ
2826 "});
2827
2828 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2829 cx.assert_editor_state(indoc! {"
2830 // Foo
2831 // ˇ
2832 "});
2833 // Ensure that we add comment prefix when existing line contains space
2834 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2835 cx.assert_editor_state(
2836 indoc! {"
2837 // Foo
2838 //s
2839 // ˇ
2840 "}
2841 .replace("s", " ") // s is used as space placeholder to prevent format on save
2842 .as_str(),
2843 );
2844 // Ensure that we add comment prefix when existing line does not contain space
2845 cx.set_state(indoc! {"
2846 // Foo
2847 //ˇ
2848 "});
2849 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2850 cx.assert_editor_state(indoc! {"
2851 // Foo
2852 //
2853 // ˇ
2854 "});
2855 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2856 cx.set_state(indoc! {"
2857 ˇ// Foo
2858 "});
2859 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2860 cx.assert_editor_state(indoc! {"
2861
2862 ˇ// Foo
2863 "});
2864 }
2865 // Ensure that comment continuations can be disabled.
2866 update_test_language_settings(cx, |settings| {
2867 settings.defaults.extend_comment_on_newline = Some(false);
2868 });
2869 let mut cx = EditorTestContext::new(cx).await;
2870 cx.set_state(indoc! {"
2871 // Fooˇ
2872 "});
2873 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2874 cx.assert_editor_state(indoc! {"
2875 // Foo
2876 ˇ
2877 "});
2878}
2879
2880#[gpui::test]
2881async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2882 init_test(cx, |settings| {
2883 settings.defaults.tab_size = NonZeroU32::new(4)
2884 });
2885
2886 let language = Arc::new(Language::new(
2887 LanguageConfig {
2888 line_comments: vec!["// ".into(), "/// ".into()],
2889 ..LanguageConfig::default()
2890 },
2891 None,
2892 ));
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 //ˇ
2898 "});
2899 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2900 cx.assert_editor_state(indoc! {"
2901 //
2902 // ˇ
2903 "});
2904
2905 cx.set_state(indoc! {"
2906 ///ˇ
2907 "});
2908 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2909 cx.assert_editor_state(indoc! {"
2910 ///
2911 /// ˇ
2912 "});
2913 }
2914}
2915
2916#[gpui::test]
2917async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2918 init_test(cx, |settings| {
2919 settings.defaults.tab_size = NonZeroU32::new(4)
2920 });
2921
2922 let language = Arc::new(
2923 Language::new(
2924 LanguageConfig {
2925 documentation_comment: Some(language::BlockCommentConfig {
2926 start: "/**".into(),
2927 end: "*/".into(),
2928 prefix: "* ".into(),
2929 tab_size: 1,
2930 }),
2931
2932 ..LanguageConfig::default()
2933 },
2934 Some(tree_sitter_rust::LANGUAGE.into()),
2935 )
2936 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2937 .unwrap(),
2938 );
2939
2940 {
2941 let mut cx = EditorTestContext::new(cx).await;
2942 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2943 cx.set_state(indoc! {"
2944 /**ˇ
2945 "});
2946
2947 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2948 cx.assert_editor_state(indoc! {"
2949 /**
2950 * ˇ
2951 "});
2952 // Ensure that if cursor is before the comment start,
2953 // we do not actually insert a comment prefix.
2954 cx.set_state(indoc! {"
2955 ˇ/**
2956 "});
2957 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2958 cx.assert_editor_state(indoc! {"
2959
2960 ˇ/**
2961 "});
2962 // Ensure that if cursor is between it doesn't add comment prefix.
2963 cx.set_state(indoc! {"
2964 /*ˇ*
2965 "});
2966 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2967 cx.assert_editor_state(indoc! {"
2968 /*
2969 ˇ*
2970 "});
2971 // Ensure that if suffix exists on same line after cursor it adds new line.
2972 cx.set_state(indoc! {"
2973 /**ˇ*/
2974 "});
2975 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2976 cx.assert_editor_state(indoc! {"
2977 /**
2978 * ˇ
2979 */
2980 "});
2981 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2982 cx.set_state(indoc! {"
2983 /**ˇ */
2984 "});
2985 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2986 cx.assert_editor_state(indoc! {"
2987 /**
2988 * ˇ
2989 */
2990 "});
2991 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2992 cx.set_state(indoc! {"
2993 /** ˇ*/
2994 "});
2995 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2996 cx.assert_editor_state(
2997 indoc! {"
2998 /**s
2999 * ˇ
3000 */
3001 "}
3002 .replace("s", " ") // s is used as space placeholder to prevent format on save
3003 .as_str(),
3004 );
3005 // Ensure that delimiter space is preserved when newline on already
3006 // spaced delimiter.
3007 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3008 cx.assert_editor_state(
3009 indoc! {"
3010 /**s
3011 *s
3012 * ˇ
3013 */
3014 "}
3015 .replace("s", " ") // s is used as space placeholder to prevent format on save
3016 .as_str(),
3017 );
3018 // Ensure that delimiter space is preserved when space is not
3019 // on existing delimiter.
3020 cx.set_state(indoc! {"
3021 /**
3022 *ˇ
3023 */
3024 "});
3025 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3026 cx.assert_editor_state(indoc! {"
3027 /**
3028 *
3029 * ˇ
3030 */
3031 "});
3032 // Ensure that if suffix exists on same line after cursor it
3033 // doesn't add extra new line if prefix is not on same line.
3034 cx.set_state(indoc! {"
3035 /**
3036 ˇ*/
3037 "});
3038 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3039 cx.assert_editor_state(indoc! {"
3040 /**
3041
3042 ˇ*/
3043 "});
3044 // Ensure that it detects suffix after existing prefix.
3045 cx.set_state(indoc! {"
3046 /**ˇ/
3047 "});
3048 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 /**
3051 ˇ/
3052 "});
3053 // Ensure that if suffix exists on same line before
3054 // cursor it does not add comment prefix.
3055 cx.set_state(indoc! {"
3056 /** */ˇ
3057 "});
3058 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3059 cx.assert_editor_state(indoc! {"
3060 /** */
3061 ˇ
3062 "});
3063 // Ensure that if suffix exists on same line before
3064 // cursor it does not add comment prefix.
3065 cx.set_state(indoc! {"
3066 /**
3067 *
3068 */ˇ
3069 "});
3070 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3071 cx.assert_editor_state(indoc! {"
3072 /**
3073 *
3074 */
3075 ˇ
3076 "});
3077
3078 // Ensure that inline comment followed by code
3079 // doesn't add comment prefix on newline
3080 cx.set_state(indoc! {"
3081 /** */ textˇ
3082 "});
3083 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3084 cx.assert_editor_state(indoc! {"
3085 /** */ text
3086 ˇ
3087 "});
3088
3089 // Ensure that text after comment end tag
3090 // doesn't add comment prefix on newline
3091 cx.set_state(indoc! {"
3092 /**
3093 *
3094 */ˇtext
3095 "});
3096 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3097 cx.assert_editor_state(indoc! {"
3098 /**
3099 *
3100 */
3101 ˇtext
3102 "});
3103
3104 // Ensure if not comment block it doesn't
3105 // add comment prefix on newline
3106 cx.set_state(indoc! {"
3107 * textˇ
3108 "});
3109 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 * text
3112 ˇ
3113 "});
3114 }
3115 // Ensure that comment continuations can be disabled.
3116 update_test_language_settings(cx, |settings| {
3117 settings.defaults.extend_comment_on_newline = Some(false);
3118 });
3119 let mut cx = EditorTestContext::new(cx).await;
3120 cx.set_state(indoc! {"
3121 /**ˇ
3122 "});
3123 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3124 cx.assert_editor_state(indoc! {"
3125 /**
3126 ˇ
3127 "});
3128}
3129
3130#[gpui::test]
3131async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3132 init_test(cx, |settings| {
3133 settings.defaults.tab_size = NonZeroU32::new(4)
3134 });
3135
3136 let lua_language = Arc::new(Language::new(
3137 LanguageConfig {
3138 line_comments: vec!["--".into()],
3139 block_comment: Some(language::BlockCommentConfig {
3140 start: "--[[".into(),
3141 prefix: "".into(),
3142 end: "]]".into(),
3143 tab_size: 0,
3144 }),
3145 ..LanguageConfig::default()
3146 },
3147 None,
3148 ));
3149
3150 let mut cx = EditorTestContext::new(cx).await;
3151 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3152
3153 // Line with line comment should extend
3154 cx.set_state(indoc! {"
3155 --ˇ
3156 "});
3157 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 --
3160 --ˇ
3161 "});
3162
3163 // Line with block comment that matches line comment should not extend
3164 cx.set_state(indoc! {"
3165 --[[ˇ
3166 "});
3167 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3168 cx.assert_editor_state(indoc! {"
3169 --[[
3170 ˇ
3171 "});
3172}
3173
3174#[gpui::test]
3175fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3176 init_test(cx, |_| {});
3177
3178 let editor = cx.add_window(|window, cx| {
3179 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3180 let mut editor = build_editor(buffer, window, cx);
3181 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3182 s.select_ranges([3..4, 11..12, 19..20])
3183 });
3184 editor
3185 });
3186
3187 _ = editor.update(cx, |editor, window, cx| {
3188 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3189 editor.buffer.update(cx, |buffer, cx| {
3190 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3191 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3192 });
3193 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3194
3195 editor.insert("Z", window, cx);
3196 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3197
3198 // The selections are moved after the inserted characters
3199 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3200 });
3201}
3202
3203#[gpui::test]
3204async fn test_tab(cx: &mut TestAppContext) {
3205 init_test(cx, |settings| {
3206 settings.defaults.tab_size = NonZeroU32::new(3)
3207 });
3208
3209 let mut cx = EditorTestContext::new(cx).await;
3210 cx.set_state(indoc! {"
3211 ˇabˇc
3212 ˇ🏀ˇ🏀ˇefg
3213 dˇ
3214 "});
3215 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3216 cx.assert_editor_state(indoc! {"
3217 ˇab ˇc
3218 ˇ🏀 ˇ🏀 ˇefg
3219 d ˇ
3220 "});
3221
3222 cx.set_state(indoc! {"
3223 a
3224 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3225 "});
3226 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3227 cx.assert_editor_state(indoc! {"
3228 a
3229 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3230 "});
3231}
3232
3233#[gpui::test]
3234async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3235 init_test(cx, |_| {});
3236
3237 let mut cx = EditorTestContext::new(cx).await;
3238 let language = Arc::new(
3239 Language::new(
3240 LanguageConfig::default(),
3241 Some(tree_sitter_rust::LANGUAGE.into()),
3242 )
3243 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3244 .unwrap(),
3245 );
3246 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3247
3248 // test when all cursors are not at suggested indent
3249 // then simply move to their suggested indent location
3250 cx.set_state(indoc! {"
3251 const a: B = (
3252 c(
3253 ˇ
3254 ˇ )
3255 );
3256 "});
3257 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3258 cx.assert_editor_state(indoc! {"
3259 const a: B = (
3260 c(
3261 ˇ
3262 ˇ)
3263 );
3264 "});
3265
3266 // test cursor already at suggested indent not moving when
3267 // other cursors are yet to reach their suggested indents
3268 cx.set_state(indoc! {"
3269 ˇ
3270 const a: B = (
3271 c(
3272 d(
3273 ˇ
3274 )
3275 ˇ
3276 ˇ )
3277 );
3278 "});
3279 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3280 cx.assert_editor_state(indoc! {"
3281 ˇ
3282 const a: B = (
3283 c(
3284 d(
3285 ˇ
3286 )
3287 ˇ
3288 ˇ)
3289 );
3290 "});
3291 // test when all cursors are at suggested indent then tab is inserted
3292 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3293 cx.assert_editor_state(indoc! {"
3294 ˇ
3295 const a: B = (
3296 c(
3297 d(
3298 ˇ
3299 )
3300 ˇ
3301 ˇ)
3302 );
3303 "});
3304
3305 // test when current indent is less than suggested indent,
3306 // we adjust line to match suggested indent and move cursor to it
3307 //
3308 // when no other cursor is at word boundary, all of them should move
3309 cx.set_state(indoc! {"
3310 const a: B = (
3311 c(
3312 d(
3313 ˇ
3314 ˇ )
3315 ˇ )
3316 );
3317 "});
3318 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3319 cx.assert_editor_state(indoc! {"
3320 const a: B = (
3321 c(
3322 d(
3323 ˇ
3324 ˇ)
3325 ˇ)
3326 );
3327 "});
3328
3329 // test when current indent is less than suggested indent,
3330 // we adjust line to match suggested indent and move cursor to it
3331 //
3332 // when some other cursor is at word boundary, it should not move
3333 cx.set_state(indoc! {"
3334 const a: B = (
3335 c(
3336 d(
3337 ˇ
3338 ˇ )
3339 ˇ)
3340 );
3341 "});
3342 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3343 cx.assert_editor_state(indoc! {"
3344 const a: B = (
3345 c(
3346 d(
3347 ˇ
3348 ˇ)
3349 ˇ)
3350 );
3351 "});
3352
3353 // test when current indent is more than suggested indent,
3354 // we just move cursor to current indent instead of suggested indent
3355 //
3356 // when no other cursor is at word boundary, all of them should move
3357 cx.set_state(indoc! {"
3358 const a: B = (
3359 c(
3360 d(
3361 ˇ
3362 ˇ )
3363 ˇ )
3364 );
3365 "});
3366 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3367 cx.assert_editor_state(indoc! {"
3368 const a: B = (
3369 c(
3370 d(
3371 ˇ
3372 ˇ)
3373 ˇ)
3374 );
3375 "});
3376 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3377 cx.assert_editor_state(indoc! {"
3378 const a: B = (
3379 c(
3380 d(
3381 ˇ
3382 ˇ)
3383 ˇ)
3384 );
3385 "});
3386
3387 // test when current indent is more than suggested indent,
3388 // we just move cursor to current indent instead of suggested indent
3389 //
3390 // when some other cursor is at word boundary, it doesn't move
3391 cx.set_state(indoc! {"
3392 const a: B = (
3393 c(
3394 d(
3395 ˇ
3396 ˇ )
3397 ˇ)
3398 );
3399 "});
3400 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3401 cx.assert_editor_state(indoc! {"
3402 const a: B = (
3403 c(
3404 d(
3405 ˇ
3406 ˇ)
3407 ˇ)
3408 );
3409 "});
3410
3411 // handle auto-indent when there are multiple cursors on the same line
3412 cx.set_state(indoc! {"
3413 const a: B = (
3414 c(
3415 ˇ ˇ
3416 ˇ )
3417 );
3418 "});
3419 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3420 cx.assert_editor_state(indoc! {"
3421 const a: B = (
3422 c(
3423 ˇ
3424 ˇ)
3425 );
3426 "});
3427}
3428
3429#[gpui::test]
3430async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3431 init_test(cx, |settings| {
3432 settings.defaults.tab_size = NonZeroU32::new(3)
3433 });
3434
3435 let mut cx = EditorTestContext::new(cx).await;
3436 cx.set_state(indoc! {"
3437 ˇ
3438 \t ˇ
3439 \t ˇ
3440 \t ˇ
3441 \t \t\t \t \t\t \t\t \t \t ˇ
3442 "});
3443
3444 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3445 cx.assert_editor_state(indoc! {"
3446 ˇ
3447 \t ˇ
3448 \t ˇ
3449 \t ˇ
3450 \t \t\t \t \t\t \t\t \t \t ˇ
3451 "});
3452}
3453
3454#[gpui::test]
3455async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3456 init_test(cx, |settings| {
3457 settings.defaults.tab_size = NonZeroU32::new(4)
3458 });
3459
3460 let language = Arc::new(
3461 Language::new(
3462 LanguageConfig::default(),
3463 Some(tree_sitter_rust::LANGUAGE.into()),
3464 )
3465 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3466 .unwrap(),
3467 );
3468
3469 let mut cx = EditorTestContext::new(cx).await;
3470 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3471 cx.set_state(indoc! {"
3472 fn a() {
3473 if b {
3474 \t ˇc
3475 }
3476 }
3477 "});
3478
3479 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3480 cx.assert_editor_state(indoc! {"
3481 fn a() {
3482 if b {
3483 ˇc
3484 }
3485 }
3486 "});
3487}
3488
3489#[gpui::test]
3490async fn test_indent_outdent(cx: &mut TestAppContext) {
3491 init_test(cx, |settings| {
3492 settings.defaults.tab_size = NonZeroU32::new(4);
3493 });
3494
3495 let mut cx = EditorTestContext::new(cx).await;
3496
3497 cx.set_state(indoc! {"
3498 «oneˇ» «twoˇ»
3499 three
3500 four
3501 "});
3502 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 «oneˇ» «twoˇ»
3505 three
3506 four
3507 "});
3508
3509 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3510 cx.assert_editor_state(indoc! {"
3511 «oneˇ» «twoˇ»
3512 three
3513 four
3514 "});
3515
3516 // select across line ending
3517 cx.set_state(indoc! {"
3518 one two
3519 t«hree
3520 ˇ» four
3521 "});
3522 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 one two
3525 t«hree
3526 ˇ» four
3527 "});
3528
3529 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3530 cx.assert_editor_state(indoc! {"
3531 one two
3532 t«hree
3533 ˇ» four
3534 "});
3535
3536 // Ensure that indenting/outdenting works when the cursor is at column 0.
3537 cx.set_state(indoc! {"
3538 one two
3539 ˇthree
3540 four
3541 "});
3542 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3543 cx.assert_editor_state(indoc! {"
3544 one two
3545 ˇthree
3546 four
3547 "});
3548
3549 cx.set_state(indoc! {"
3550 one two
3551 ˇ three
3552 four
3553 "});
3554 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3555 cx.assert_editor_state(indoc! {"
3556 one two
3557 ˇthree
3558 four
3559 "});
3560}
3561
3562#[gpui::test]
3563async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3564 // This is a regression test for issue #33761
3565 init_test(cx, |_| {});
3566
3567 let mut cx = EditorTestContext::new(cx).await;
3568 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3569 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3570
3571 cx.set_state(
3572 r#"ˇ# ingress:
3573ˇ# api:
3574ˇ# enabled: false
3575ˇ# pathType: Prefix
3576ˇ# console:
3577ˇ# enabled: false
3578ˇ# pathType: Prefix
3579"#,
3580 );
3581
3582 // Press tab to indent all lines
3583 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3584
3585 cx.assert_editor_state(
3586 r#" ˇ# ingress:
3587 ˇ# api:
3588 ˇ# enabled: false
3589 ˇ# pathType: Prefix
3590 ˇ# console:
3591 ˇ# enabled: false
3592 ˇ# pathType: Prefix
3593"#,
3594 );
3595}
3596
3597#[gpui::test]
3598async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3599 // This is a test to make sure our fix for issue #33761 didn't break anything
3600 init_test(cx, |_| {});
3601
3602 let mut cx = EditorTestContext::new(cx).await;
3603 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3604 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3605
3606 cx.set_state(
3607 r#"ˇingress:
3608ˇ api:
3609ˇ enabled: false
3610ˇ pathType: Prefix
3611"#,
3612 );
3613
3614 // Press tab to indent all lines
3615 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3616
3617 cx.assert_editor_state(
3618 r#"ˇingress:
3619 ˇapi:
3620 ˇenabled: false
3621 ˇpathType: Prefix
3622"#,
3623 );
3624}
3625
3626#[gpui::test]
3627async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3628 init_test(cx, |settings| {
3629 settings.defaults.hard_tabs = Some(true);
3630 });
3631
3632 let mut cx = EditorTestContext::new(cx).await;
3633
3634 // select two ranges on one line
3635 cx.set_state(indoc! {"
3636 «oneˇ» «twoˇ»
3637 three
3638 four
3639 "});
3640 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3641 cx.assert_editor_state(indoc! {"
3642 \t«oneˇ» «twoˇ»
3643 three
3644 four
3645 "});
3646 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3647 cx.assert_editor_state(indoc! {"
3648 \t\t«oneˇ» «twoˇ»
3649 three
3650 four
3651 "});
3652 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3653 cx.assert_editor_state(indoc! {"
3654 \t«oneˇ» «twoˇ»
3655 three
3656 four
3657 "});
3658 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3659 cx.assert_editor_state(indoc! {"
3660 «oneˇ» «twoˇ»
3661 three
3662 four
3663 "});
3664
3665 // select across a line ending
3666 cx.set_state(indoc! {"
3667 one two
3668 t«hree
3669 ˇ»four
3670 "});
3671 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3672 cx.assert_editor_state(indoc! {"
3673 one two
3674 \tt«hree
3675 ˇ»four
3676 "});
3677 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3678 cx.assert_editor_state(indoc! {"
3679 one two
3680 \t\tt«hree
3681 ˇ»four
3682 "});
3683 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3684 cx.assert_editor_state(indoc! {"
3685 one two
3686 \tt«hree
3687 ˇ»four
3688 "});
3689 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3690 cx.assert_editor_state(indoc! {"
3691 one two
3692 t«hree
3693 ˇ»four
3694 "});
3695
3696 // Ensure that indenting/outdenting works when the cursor is at column 0.
3697 cx.set_state(indoc! {"
3698 one two
3699 ˇthree
3700 four
3701 "});
3702 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 one two
3705 ˇthree
3706 four
3707 "});
3708 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3709 cx.assert_editor_state(indoc! {"
3710 one two
3711 \tˇthree
3712 four
3713 "});
3714 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 one two
3717 ˇthree
3718 four
3719 "});
3720}
3721
3722#[gpui::test]
3723fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3724 init_test(cx, |settings| {
3725 settings.languages.0.extend([
3726 (
3727 "TOML".into(),
3728 LanguageSettingsContent {
3729 tab_size: NonZeroU32::new(2),
3730 ..Default::default()
3731 },
3732 ),
3733 (
3734 "Rust".into(),
3735 LanguageSettingsContent {
3736 tab_size: NonZeroU32::new(4),
3737 ..Default::default()
3738 },
3739 ),
3740 ]);
3741 });
3742
3743 let toml_language = Arc::new(Language::new(
3744 LanguageConfig {
3745 name: "TOML".into(),
3746 ..Default::default()
3747 },
3748 None,
3749 ));
3750 let rust_language = Arc::new(Language::new(
3751 LanguageConfig {
3752 name: "Rust".into(),
3753 ..Default::default()
3754 },
3755 None,
3756 ));
3757
3758 let toml_buffer =
3759 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3760 let rust_buffer =
3761 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3762 let multibuffer = cx.new(|cx| {
3763 let mut multibuffer = MultiBuffer::new(ReadWrite);
3764 multibuffer.push_excerpts(
3765 toml_buffer.clone(),
3766 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3767 cx,
3768 );
3769 multibuffer.push_excerpts(
3770 rust_buffer.clone(),
3771 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3772 cx,
3773 );
3774 multibuffer
3775 });
3776
3777 cx.add_window(|window, cx| {
3778 let mut editor = build_editor(multibuffer, window, cx);
3779
3780 assert_eq!(
3781 editor.text(cx),
3782 indoc! {"
3783 a = 1
3784 b = 2
3785
3786 const c: usize = 3;
3787 "}
3788 );
3789
3790 select_ranges(
3791 &mut editor,
3792 indoc! {"
3793 «aˇ» = 1
3794 b = 2
3795
3796 «const c:ˇ» usize = 3;
3797 "},
3798 window,
3799 cx,
3800 );
3801
3802 editor.tab(&Tab, window, cx);
3803 assert_text_with_selections(
3804 &mut editor,
3805 indoc! {"
3806 «aˇ» = 1
3807 b = 2
3808
3809 «const c:ˇ» usize = 3;
3810 "},
3811 cx,
3812 );
3813 editor.backtab(&Backtab, window, cx);
3814 assert_text_with_selections(
3815 &mut editor,
3816 indoc! {"
3817 «aˇ» = 1
3818 b = 2
3819
3820 «const c:ˇ» usize = 3;
3821 "},
3822 cx,
3823 );
3824
3825 editor
3826 });
3827}
3828
3829#[gpui::test]
3830async fn test_backspace(cx: &mut TestAppContext) {
3831 init_test(cx, |_| {});
3832
3833 let mut cx = EditorTestContext::new(cx).await;
3834
3835 // Basic backspace
3836 cx.set_state(indoc! {"
3837 onˇe two three
3838 fou«rˇ» five six
3839 seven «ˇeight nine
3840 »ten
3841 "});
3842 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3843 cx.assert_editor_state(indoc! {"
3844 oˇe two three
3845 fouˇ five six
3846 seven ˇten
3847 "});
3848
3849 // Test backspace inside and around indents
3850 cx.set_state(indoc! {"
3851 zero
3852 ˇone
3853 ˇtwo
3854 ˇ ˇ ˇ three
3855 ˇ ˇ four
3856 "});
3857 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3858 cx.assert_editor_state(indoc! {"
3859 zero
3860 ˇone
3861 ˇtwo
3862 ˇ threeˇ four
3863 "});
3864}
3865
3866#[gpui::test]
3867async fn test_delete(cx: &mut TestAppContext) {
3868 init_test(cx, |_| {});
3869
3870 let mut cx = EditorTestContext::new(cx).await;
3871 cx.set_state(indoc! {"
3872 onˇe two three
3873 fou«rˇ» five six
3874 seven «ˇeight nine
3875 »ten
3876 "});
3877 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3878 cx.assert_editor_state(indoc! {"
3879 onˇ two three
3880 fouˇ five six
3881 seven ˇten
3882 "});
3883}
3884
3885#[gpui::test]
3886fn test_delete_line(cx: &mut TestAppContext) {
3887 init_test(cx, |_| {});
3888
3889 let editor = cx.add_window(|window, cx| {
3890 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3891 build_editor(buffer, window, cx)
3892 });
3893 _ = editor.update(cx, |editor, window, cx| {
3894 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3895 s.select_display_ranges([
3896 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3897 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3898 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3899 ])
3900 });
3901 editor.delete_line(&DeleteLine, window, cx);
3902 assert_eq!(editor.display_text(cx), "ghi");
3903 assert_eq!(
3904 editor.selections.display_ranges(cx),
3905 vec![
3906 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3907 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3908 ]
3909 );
3910 });
3911
3912 let editor = cx.add_window(|window, cx| {
3913 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3914 build_editor(buffer, window, cx)
3915 });
3916 _ = editor.update(cx, |editor, window, cx| {
3917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3918 s.select_display_ranges([
3919 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3920 ])
3921 });
3922 editor.delete_line(&DeleteLine, window, cx);
3923 assert_eq!(editor.display_text(cx), "ghi\n");
3924 assert_eq!(
3925 editor.selections.display_ranges(cx),
3926 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3927 );
3928 });
3929}
3930
3931#[gpui::test]
3932fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3933 init_test(cx, |_| {});
3934
3935 cx.add_window(|window, cx| {
3936 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3937 let mut editor = build_editor(buffer.clone(), window, cx);
3938 let buffer = buffer.read(cx).as_singleton().unwrap();
3939
3940 assert_eq!(
3941 editor.selections.ranges::<Point>(cx),
3942 &[Point::new(0, 0)..Point::new(0, 0)]
3943 );
3944
3945 // When on single line, replace newline at end by space
3946 editor.join_lines(&JoinLines, window, cx);
3947 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3948 assert_eq!(
3949 editor.selections.ranges::<Point>(cx),
3950 &[Point::new(0, 3)..Point::new(0, 3)]
3951 );
3952
3953 // When multiple lines are selected, remove newlines that are spanned by the selection
3954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3955 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3956 });
3957 editor.join_lines(&JoinLines, window, cx);
3958 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3959 assert_eq!(
3960 editor.selections.ranges::<Point>(cx),
3961 &[Point::new(0, 11)..Point::new(0, 11)]
3962 );
3963
3964 // Undo should be transactional
3965 editor.undo(&Undo, window, cx);
3966 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3967 assert_eq!(
3968 editor.selections.ranges::<Point>(cx),
3969 &[Point::new(0, 5)..Point::new(2, 2)]
3970 );
3971
3972 // When joining an empty line don't insert a space
3973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3974 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3975 });
3976 editor.join_lines(&JoinLines, window, cx);
3977 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3978 assert_eq!(
3979 editor.selections.ranges::<Point>(cx),
3980 [Point::new(2, 3)..Point::new(2, 3)]
3981 );
3982
3983 // We can remove trailing newlines
3984 editor.join_lines(&JoinLines, window, cx);
3985 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3986 assert_eq!(
3987 editor.selections.ranges::<Point>(cx),
3988 [Point::new(2, 3)..Point::new(2, 3)]
3989 );
3990
3991 // We don't blow up on the last line
3992 editor.join_lines(&JoinLines, window, cx);
3993 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3994 assert_eq!(
3995 editor.selections.ranges::<Point>(cx),
3996 [Point::new(2, 3)..Point::new(2, 3)]
3997 );
3998
3999 // reset to test indentation
4000 editor.buffer.update(cx, |buffer, cx| {
4001 buffer.edit(
4002 [
4003 (Point::new(1, 0)..Point::new(1, 2), " "),
4004 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4005 ],
4006 None,
4007 cx,
4008 )
4009 });
4010
4011 // We remove any leading spaces
4012 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4014 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4015 });
4016 editor.join_lines(&JoinLines, window, cx);
4017 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4018
4019 // We don't insert a space for a line containing only spaces
4020 editor.join_lines(&JoinLines, window, cx);
4021 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4022
4023 // We ignore any leading tabs
4024 editor.join_lines(&JoinLines, window, cx);
4025 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4026
4027 editor
4028 });
4029}
4030
4031#[gpui::test]
4032fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4033 init_test(cx, |_| {});
4034
4035 cx.add_window(|window, cx| {
4036 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4037 let mut editor = build_editor(buffer.clone(), window, cx);
4038 let buffer = buffer.read(cx).as_singleton().unwrap();
4039
4040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4041 s.select_ranges([
4042 Point::new(0, 2)..Point::new(1, 1),
4043 Point::new(1, 2)..Point::new(1, 2),
4044 Point::new(3, 1)..Point::new(3, 2),
4045 ])
4046 });
4047
4048 editor.join_lines(&JoinLines, window, cx);
4049 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4050
4051 assert_eq!(
4052 editor.selections.ranges::<Point>(cx),
4053 [
4054 Point::new(0, 7)..Point::new(0, 7),
4055 Point::new(1, 3)..Point::new(1, 3)
4056 ]
4057 );
4058 editor
4059 });
4060}
4061
4062#[gpui::test]
4063async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4064 init_test(cx, |_| {});
4065
4066 let mut cx = EditorTestContext::new(cx).await;
4067
4068 let diff_base = r#"
4069 Line 0
4070 Line 1
4071 Line 2
4072 Line 3
4073 "#
4074 .unindent();
4075
4076 cx.set_state(
4077 &r#"
4078 ˇLine 0
4079 Line 1
4080 Line 2
4081 Line 3
4082 "#
4083 .unindent(),
4084 );
4085
4086 cx.set_head_text(&diff_base);
4087 executor.run_until_parked();
4088
4089 // Join lines
4090 cx.update_editor(|editor, window, cx| {
4091 editor.join_lines(&JoinLines, window, cx);
4092 });
4093 executor.run_until_parked();
4094
4095 cx.assert_editor_state(
4096 &r#"
4097 Line 0ˇ Line 1
4098 Line 2
4099 Line 3
4100 "#
4101 .unindent(),
4102 );
4103 // Join again
4104 cx.update_editor(|editor, window, cx| {
4105 editor.join_lines(&JoinLines, window, cx);
4106 });
4107 executor.run_until_parked();
4108
4109 cx.assert_editor_state(
4110 &r#"
4111 Line 0 Line 1ˇ Line 2
4112 Line 3
4113 "#
4114 .unindent(),
4115 );
4116}
4117
4118#[gpui::test]
4119async fn test_custom_newlines_cause_no_false_positive_diffs(
4120 executor: BackgroundExecutor,
4121 cx: &mut TestAppContext,
4122) {
4123 init_test(cx, |_| {});
4124 let mut cx = EditorTestContext::new(cx).await;
4125 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4126 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4127 executor.run_until_parked();
4128
4129 cx.update_editor(|editor, window, cx| {
4130 let snapshot = editor.snapshot(window, cx);
4131 assert_eq!(
4132 snapshot
4133 .buffer_snapshot
4134 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4135 .collect::<Vec<_>>(),
4136 Vec::new(),
4137 "Should not have any diffs for files with custom newlines"
4138 );
4139 });
4140}
4141
4142#[gpui::test]
4143async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4144 init_test(cx, |_| {});
4145
4146 let mut cx = EditorTestContext::new(cx).await;
4147
4148 // Test sort_lines_case_insensitive()
4149 cx.set_state(indoc! {"
4150 «z
4151 y
4152 x
4153 Z
4154 Y
4155 Xˇ»
4156 "});
4157 cx.update_editor(|e, window, cx| {
4158 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4159 });
4160 cx.assert_editor_state(indoc! {"
4161 «x
4162 X
4163 y
4164 Y
4165 z
4166 Zˇ»
4167 "});
4168
4169 // Test sort_lines_by_length()
4170 //
4171 // Demonstrates:
4172 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4173 // - sort is stable
4174 cx.set_state(indoc! {"
4175 «123
4176 æ
4177 12
4178 ∞
4179 1
4180 æˇ»
4181 "});
4182 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4183 cx.assert_editor_state(indoc! {"
4184 «æ
4185 ∞
4186 1
4187 æ
4188 12
4189 123ˇ»
4190 "});
4191
4192 // Test reverse_lines()
4193 cx.set_state(indoc! {"
4194 «5
4195 4
4196 3
4197 2
4198 1ˇ»
4199 "});
4200 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4201 cx.assert_editor_state(indoc! {"
4202 «1
4203 2
4204 3
4205 4
4206 5ˇ»
4207 "});
4208
4209 // Skip testing shuffle_line()
4210
4211 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4212 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4213
4214 // Don't manipulate when cursor is on single line, but expand the selection
4215 cx.set_state(indoc! {"
4216 ddˇdd
4217 ccc
4218 bb
4219 a
4220 "});
4221 cx.update_editor(|e, window, cx| {
4222 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4223 });
4224 cx.assert_editor_state(indoc! {"
4225 «ddddˇ»
4226 ccc
4227 bb
4228 a
4229 "});
4230
4231 // Basic manipulate case
4232 // Start selection moves to column 0
4233 // End of selection shrinks to fit shorter line
4234 cx.set_state(indoc! {"
4235 dd«d
4236 ccc
4237 bb
4238 aaaaaˇ»
4239 "});
4240 cx.update_editor(|e, window, cx| {
4241 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4242 });
4243 cx.assert_editor_state(indoc! {"
4244 «aaaaa
4245 bb
4246 ccc
4247 dddˇ»
4248 "});
4249
4250 // Manipulate case with newlines
4251 cx.set_state(indoc! {"
4252 dd«d
4253 ccc
4254
4255 bb
4256 aaaaa
4257
4258 ˇ»
4259 "});
4260 cx.update_editor(|e, window, cx| {
4261 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4262 });
4263 cx.assert_editor_state(indoc! {"
4264 «
4265
4266 aaaaa
4267 bb
4268 ccc
4269 dddˇ»
4270
4271 "});
4272
4273 // Adding new line
4274 cx.set_state(indoc! {"
4275 aa«a
4276 bbˇ»b
4277 "});
4278 cx.update_editor(|e, window, cx| {
4279 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4280 });
4281 cx.assert_editor_state(indoc! {"
4282 «aaa
4283 bbb
4284 added_lineˇ»
4285 "});
4286
4287 // Removing line
4288 cx.set_state(indoc! {"
4289 aa«a
4290 bbbˇ»
4291 "});
4292 cx.update_editor(|e, window, cx| {
4293 e.manipulate_immutable_lines(window, cx, |lines| {
4294 lines.pop();
4295 })
4296 });
4297 cx.assert_editor_state(indoc! {"
4298 «aaaˇ»
4299 "});
4300
4301 // Removing all lines
4302 cx.set_state(indoc! {"
4303 aa«a
4304 bbbˇ»
4305 "});
4306 cx.update_editor(|e, window, cx| {
4307 e.manipulate_immutable_lines(window, cx, |lines| {
4308 lines.drain(..);
4309 })
4310 });
4311 cx.assert_editor_state(indoc! {"
4312 ˇ
4313 "});
4314}
4315
4316#[gpui::test]
4317async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4318 init_test(cx, |_| {});
4319
4320 let mut cx = EditorTestContext::new(cx).await;
4321
4322 // Consider continuous selection as single selection
4323 cx.set_state(indoc! {"
4324 Aaa«aa
4325 cˇ»c«c
4326 bb
4327 aaaˇ»aa
4328 "});
4329 cx.update_editor(|e, window, cx| {
4330 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4331 });
4332 cx.assert_editor_state(indoc! {"
4333 «Aaaaa
4334 ccc
4335 bb
4336 aaaaaˇ»
4337 "});
4338
4339 cx.set_state(indoc! {"
4340 Aaa«aa
4341 cˇ»c«c
4342 bb
4343 aaaˇ»aa
4344 "});
4345 cx.update_editor(|e, window, cx| {
4346 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4347 });
4348 cx.assert_editor_state(indoc! {"
4349 «Aaaaa
4350 ccc
4351 bbˇ»
4352 "});
4353
4354 // Consider non continuous selection as distinct dedup operations
4355 cx.set_state(indoc! {"
4356 «aaaaa
4357 bb
4358 aaaaa
4359 aaaaaˇ»
4360
4361 aaa«aaˇ»
4362 "});
4363 cx.update_editor(|e, window, cx| {
4364 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4365 });
4366 cx.assert_editor_state(indoc! {"
4367 «aaaaa
4368 bbˇ»
4369
4370 «aaaaaˇ»
4371 "});
4372}
4373
4374#[gpui::test]
4375async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4376 init_test(cx, |_| {});
4377
4378 let mut cx = EditorTestContext::new(cx).await;
4379
4380 cx.set_state(indoc! {"
4381 «Aaa
4382 aAa
4383 Aaaˇ»
4384 "});
4385 cx.update_editor(|e, window, cx| {
4386 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4387 });
4388 cx.assert_editor_state(indoc! {"
4389 «Aaa
4390 aAaˇ»
4391 "});
4392
4393 cx.set_state(indoc! {"
4394 «Aaa
4395 aAa
4396 aaAˇ»
4397 "});
4398 cx.update_editor(|e, window, cx| {
4399 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4400 });
4401 cx.assert_editor_state(indoc! {"
4402 «Aaaˇ»
4403 "});
4404}
4405
4406#[gpui::test]
4407async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4408 init_test(cx, |_| {});
4409
4410 let mut cx = EditorTestContext::new(cx).await;
4411
4412 // Manipulate with multiple selections on a single line
4413 cx.set_state(indoc! {"
4414 dd«dd
4415 cˇ»c«c
4416 bb
4417 aaaˇ»aa
4418 "});
4419 cx.update_editor(|e, window, cx| {
4420 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4421 });
4422 cx.assert_editor_state(indoc! {"
4423 «aaaaa
4424 bb
4425 ccc
4426 ddddˇ»
4427 "});
4428
4429 // Manipulate with multiple disjoin selections
4430 cx.set_state(indoc! {"
4431 5«
4432 4
4433 3
4434 2
4435 1ˇ»
4436
4437 dd«dd
4438 ccc
4439 bb
4440 aaaˇ»aa
4441 "});
4442 cx.update_editor(|e, window, cx| {
4443 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4444 });
4445 cx.assert_editor_state(indoc! {"
4446 «1
4447 2
4448 3
4449 4
4450 5ˇ»
4451
4452 «aaaaa
4453 bb
4454 ccc
4455 ddddˇ»
4456 "});
4457
4458 // Adding lines on each selection
4459 cx.set_state(indoc! {"
4460 2«
4461 1ˇ»
4462
4463 bb«bb
4464 aaaˇ»aa
4465 "});
4466 cx.update_editor(|e, window, cx| {
4467 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4468 });
4469 cx.assert_editor_state(indoc! {"
4470 «2
4471 1
4472 added lineˇ»
4473
4474 «bbbb
4475 aaaaa
4476 added lineˇ»
4477 "});
4478
4479 // Removing lines on each selection
4480 cx.set_state(indoc! {"
4481 2«
4482 1ˇ»
4483
4484 bb«bb
4485 aaaˇ»aa
4486 "});
4487 cx.update_editor(|e, window, cx| {
4488 e.manipulate_immutable_lines(window, cx, |lines| {
4489 lines.pop();
4490 })
4491 });
4492 cx.assert_editor_state(indoc! {"
4493 «2ˇ»
4494
4495 «bbbbˇ»
4496 "});
4497}
4498
4499#[gpui::test]
4500async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4501 init_test(cx, |settings| {
4502 settings.defaults.tab_size = NonZeroU32::new(3)
4503 });
4504
4505 let mut cx = EditorTestContext::new(cx).await;
4506
4507 // MULTI SELECTION
4508 // Ln.1 "«" tests empty lines
4509 // Ln.9 tests just leading whitespace
4510 cx.set_state(indoc! {"
4511 «
4512 abc // No indentationˇ»
4513 «\tabc // 1 tabˇ»
4514 \t\tabc « ˇ» // 2 tabs
4515 \t ab«c // Tab followed by space
4516 \tabc // Space followed by tab (3 spaces should be the result)
4517 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4518 abˇ»ˇc ˇ ˇ // Already space indented«
4519 \t
4520 \tabc\tdef // Only the leading tab is manipulatedˇ»
4521 "});
4522 cx.update_editor(|e, window, cx| {
4523 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4524 });
4525 cx.assert_editor_state(
4526 indoc! {"
4527 «
4528 abc // No indentation
4529 abc // 1 tab
4530 abc // 2 tabs
4531 abc // Tab followed by space
4532 abc // Space followed by tab (3 spaces should be the result)
4533 abc // Mixed indentation (tab conversion depends on the column)
4534 abc // Already space indented
4535 ·
4536 abc\tdef // Only the leading tab is manipulatedˇ»
4537 "}
4538 .replace("·", "")
4539 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4540 );
4541
4542 // Test on just a few lines, the others should remain unchanged
4543 // Only lines (3, 5, 10, 11) should change
4544 cx.set_state(
4545 indoc! {"
4546 ·
4547 abc // No indentation
4548 \tabcˇ // 1 tab
4549 \t\tabc // 2 tabs
4550 \t abcˇ // Tab followed by space
4551 \tabc // Space followed by tab (3 spaces should be the result)
4552 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4553 abc // Already space indented
4554 «\t
4555 \tabc\tdef // Only the leading tab is manipulatedˇ»
4556 "}
4557 .replace("·", "")
4558 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4559 );
4560 cx.update_editor(|e, window, cx| {
4561 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4562 });
4563 cx.assert_editor_state(
4564 indoc! {"
4565 ·
4566 abc // No indentation
4567 « abc // 1 tabˇ»
4568 \t\tabc // 2 tabs
4569 « abc // Tab followed by spaceˇ»
4570 \tabc // Space followed by tab (3 spaces should be the result)
4571 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4572 abc // Already space indented
4573 « ·
4574 abc\tdef // Only the leading tab is manipulatedˇ»
4575 "}
4576 .replace("·", "")
4577 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4578 );
4579
4580 // SINGLE SELECTION
4581 // Ln.1 "«" tests empty lines
4582 // Ln.9 tests just leading whitespace
4583 cx.set_state(indoc! {"
4584 «
4585 abc // No indentation
4586 \tabc // 1 tab
4587 \t\tabc // 2 tabs
4588 \t abc // Tab followed by space
4589 \tabc // Space followed by tab (3 spaces should be the result)
4590 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4591 abc // Already space indented
4592 \t
4593 \tabc\tdef // Only the leading tab is manipulatedˇ»
4594 "});
4595 cx.update_editor(|e, window, cx| {
4596 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4597 });
4598 cx.assert_editor_state(
4599 indoc! {"
4600 «
4601 abc // No indentation
4602 abc // 1 tab
4603 abc // 2 tabs
4604 abc // Tab followed by space
4605 abc // Space followed by tab (3 spaces should be the result)
4606 abc // Mixed indentation (tab conversion depends on the column)
4607 abc // Already space indented
4608 ·
4609 abc\tdef // Only the leading tab is manipulatedˇ»
4610 "}
4611 .replace("·", "")
4612 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4613 );
4614}
4615
4616#[gpui::test]
4617async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4618 init_test(cx, |settings| {
4619 settings.defaults.tab_size = NonZeroU32::new(3)
4620 });
4621
4622 let mut cx = EditorTestContext::new(cx).await;
4623
4624 // MULTI SELECTION
4625 // Ln.1 "«" tests empty lines
4626 // Ln.11 tests just leading whitespace
4627 cx.set_state(indoc! {"
4628 «
4629 abˇ»ˇc // No indentation
4630 abc ˇ ˇ // 1 space (< 3 so dont convert)
4631 abc « // 2 spaces (< 3 so dont convert)
4632 abc // 3 spaces (convert)
4633 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4634 «\tˇ»\t«\tˇ»abc // Already tab indented
4635 «\t abc // Tab followed by space
4636 \tabc // Space followed by tab (should be consumed due to tab)
4637 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4638 \tˇ» «\t
4639 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4640 "});
4641 cx.update_editor(|e, window, cx| {
4642 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4643 });
4644 cx.assert_editor_state(indoc! {"
4645 «
4646 abc // No indentation
4647 abc // 1 space (< 3 so dont convert)
4648 abc // 2 spaces (< 3 so dont convert)
4649 \tabc // 3 spaces (convert)
4650 \t abc // 5 spaces (1 tab + 2 spaces)
4651 \t\t\tabc // Already tab indented
4652 \t abc // Tab followed by space
4653 \tabc // Space followed by tab (should be consumed due to tab)
4654 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4655 \t\t\t
4656 \tabc \t // Only the leading spaces should be convertedˇ»
4657 "});
4658
4659 // Test on just a few lines, the other should remain unchanged
4660 // Only lines (4, 8, 11, 12) should change
4661 cx.set_state(
4662 indoc! {"
4663 ·
4664 abc // No indentation
4665 abc // 1 space (< 3 so dont convert)
4666 abc // 2 spaces (< 3 so dont convert)
4667 « abc // 3 spaces (convert)ˇ»
4668 abc // 5 spaces (1 tab + 2 spaces)
4669 \t\t\tabc // Already tab indented
4670 \t abc // Tab followed by space
4671 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4672 \t\t \tabc // Mixed indentation
4673 \t \t \t \tabc // Mixed indentation
4674 \t \tˇ
4675 « abc \t // Only the leading spaces should be convertedˇ»
4676 "}
4677 .replace("·", "")
4678 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4679 );
4680 cx.update_editor(|e, window, cx| {
4681 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4682 });
4683 cx.assert_editor_state(
4684 indoc! {"
4685 ·
4686 abc // No indentation
4687 abc // 1 space (< 3 so dont convert)
4688 abc // 2 spaces (< 3 so dont convert)
4689 «\tabc // 3 spaces (convert)ˇ»
4690 abc // 5 spaces (1 tab + 2 spaces)
4691 \t\t\tabc // Already tab indented
4692 \t abc // Tab followed by space
4693 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4694 \t\t \tabc // Mixed indentation
4695 \t \t \t \tabc // Mixed indentation
4696 «\t\t\t
4697 \tabc \t // Only the leading spaces should be convertedˇ»
4698 "}
4699 .replace("·", "")
4700 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4701 );
4702
4703 // SINGLE SELECTION
4704 // Ln.1 "«" tests empty lines
4705 // Ln.11 tests just leading whitespace
4706 cx.set_state(indoc! {"
4707 «
4708 abc // No indentation
4709 abc // 1 space (< 3 so dont convert)
4710 abc // 2 spaces (< 3 so dont convert)
4711 abc // 3 spaces (convert)
4712 abc // 5 spaces (1 tab + 2 spaces)
4713 \t\t\tabc // Already tab indented
4714 \t abc // Tab followed by space
4715 \tabc // Space followed by tab (should be consumed due to tab)
4716 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4717 \t \t
4718 abc \t // Only the leading spaces should be convertedˇ»
4719 "});
4720 cx.update_editor(|e, window, cx| {
4721 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4722 });
4723 cx.assert_editor_state(indoc! {"
4724 «
4725 abc // No indentation
4726 abc // 1 space (< 3 so dont convert)
4727 abc // 2 spaces (< 3 so dont convert)
4728 \tabc // 3 spaces (convert)
4729 \t abc // 5 spaces (1 tab + 2 spaces)
4730 \t\t\tabc // Already tab indented
4731 \t abc // Tab followed by space
4732 \tabc // Space followed by tab (should be consumed due to tab)
4733 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4734 \t\t\t
4735 \tabc \t // Only the leading spaces should be convertedˇ»
4736 "});
4737}
4738
4739#[gpui::test]
4740async fn test_toggle_case(cx: &mut TestAppContext) {
4741 init_test(cx, |_| {});
4742
4743 let mut cx = EditorTestContext::new(cx).await;
4744
4745 // If all lower case -> upper case
4746 cx.set_state(indoc! {"
4747 «hello worldˇ»
4748 "});
4749 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4750 cx.assert_editor_state(indoc! {"
4751 «HELLO WORLDˇ»
4752 "});
4753
4754 // If all upper case -> lower case
4755 cx.set_state(indoc! {"
4756 «HELLO WORLDˇ»
4757 "});
4758 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4759 cx.assert_editor_state(indoc! {"
4760 «hello worldˇ»
4761 "});
4762
4763 // If any upper case characters are identified -> lower case
4764 // This matches JetBrains IDEs
4765 cx.set_state(indoc! {"
4766 «hEllo worldˇ»
4767 "});
4768 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4769 cx.assert_editor_state(indoc! {"
4770 «hello worldˇ»
4771 "});
4772}
4773
4774#[gpui::test]
4775async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
4776 init_test(cx, |_| {});
4777
4778 let mut cx = EditorTestContext::new(cx).await;
4779
4780 cx.set_state(indoc! {"
4781 «implement-windows-supportˇ»
4782 "});
4783 cx.update_editor(|e, window, cx| {
4784 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
4785 });
4786 cx.assert_editor_state(indoc! {"
4787 «Implement windows supportˇ»
4788 "});
4789}
4790
4791#[gpui::test]
4792async fn test_manipulate_text(cx: &mut TestAppContext) {
4793 init_test(cx, |_| {});
4794
4795 let mut cx = EditorTestContext::new(cx).await;
4796
4797 // Test convert_to_upper_case()
4798 cx.set_state(indoc! {"
4799 «hello worldˇ»
4800 "});
4801 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4802 cx.assert_editor_state(indoc! {"
4803 «HELLO WORLDˇ»
4804 "});
4805
4806 // Test convert_to_lower_case()
4807 cx.set_state(indoc! {"
4808 «HELLO WORLDˇ»
4809 "});
4810 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4811 cx.assert_editor_state(indoc! {"
4812 «hello worldˇ»
4813 "});
4814
4815 // Test multiple line, single selection case
4816 cx.set_state(indoc! {"
4817 «The quick brown
4818 fox jumps over
4819 the lazy dogˇ»
4820 "});
4821 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4822 cx.assert_editor_state(indoc! {"
4823 «The Quick Brown
4824 Fox Jumps Over
4825 The Lazy Dogˇ»
4826 "});
4827
4828 // Test multiple line, single selection case
4829 cx.set_state(indoc! {"
4830 «The quick brown
4831 fox jumps over
4832 the lazy dogˇ»
4833 "});
4834 cx.update_editor(|e, window, cx| {
4835 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4836 });
4837 cx.assert_editor_state(indoc! {"
4838 «TheQuickBrown
4839 FoxJumpsOver
4840 TheLazyDogˇ»
4841 "});
4842
4843 // From here on out, test more complex cases of manipulate_text()
4844
4845 // Test no selection case - should affect words cursors are in
4846 // Cursor at beginning, middle, and end of word
4847 cx.set_state(indoc! {"
4848 ˇhello big beauˇtiful worldˇ
4849 "});
4850 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4851 cx.assert_editor_state(indoc! {"
4852 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4853 "});
4854
4855 // Test multiple selections on a single line and across multiple lines
4856 cx.set_state(indoc! {"
4857 «Theˇ» quick «brown
4858 foxˇ» jumps «overˇ»
4859 the «lazyˇ» dog
4860 "});
4861 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4862 cx.assert_editor_state(indoc! {"
4863 «THEˇ» quick «BROWN
4864 FOXˇ» jumps «OVERˇ»
4865 the «LAZYˇ» dog
4866 "});
4867
4868 // Test case where text length grows
4869 cx.set_state(indoc! {"
4870 «tschüߡ»
4871 "});
4872 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4873 cx.assert_editor_state(indoc! {"
4874 «TSCHÜSSˇ»
4875 "});
4876
4877 // Test to make sure we don't crash when text shrinks
4878 cx.set_state(indoc! {"
4879 aaa_bbbˇ
4880 "});
4881 cx.update_editor(|e, window, cx| {
4882 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4883 });
4884 cx.assert_editor_state(indoc! {"
4885 «aaaBbbˇ»
4886 "});
4887
4888 // Test to make sure we all aware of the fact that each word can grow and shrink
4889 // Final selections should be aware of this fact
4890 cx.set_state(indoc! {"
4891 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4892 "});
4893 cx.update_editor(|e, window, cx| {
4894 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4895 });
4896 cx.assert_editor_state(indoc! {"
4897 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4898 "});
4899
4900 cx.set_state(indoc! {"
4901 «hElLo, WoRld!ˇ»
4902 "});
4903 cx.update_editor(|e, window, cx| {
4904 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4905 });
4906 cx.assert_editor_state(indoc! {"
4907 «HeLlO, wOrLD!ˇ»
4908 "});
4909}
4910
4911#[gpui::test]
4912fn test_duplicate_line(cx: &mut TestAppContext) {
4913 init_test(cx, |_| {});
4914
4915 let editor = cx.add_window(|window, cx| {
4916 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4917 build_editor(buffer, window, cx)
4918 });
4919 _ = editor.update(cx, |editor, window, cx| {
4920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4921 s.select_display_ranges([
4922 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4923 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4924 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4925 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4926 ])
4927 });
4928 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4929 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4930 assert_eq!(
4931 editor.selections.display_ranges(cx),
4932 vec![
4933 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4934 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4935 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4936 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4937 ]
4938 );
4939 });
4940
4941 let editor = cx.add_window(|window, cx| {
4942 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4943 build_editor(buffer, window, cx)
4944 });
4945 _ = editor.update(cx, |editor, window, cx| {
4946 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4947 s.select_display_ranges([
4948 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4949 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4950 ])
4951 });
4952 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4953 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4954 assert_eq!(
4955 editor.selections.display_ranges(cx),
4956 vec![
4957 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4958 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4959 ]
4960 );
4961 });
4962
4963 // With `move_upwards` the selections stay in place, except for
4964 // the lines inserted above them
4965 let editor = cx.add_window(|window, cx| {
4966 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4967 build_editor(buffer, window, cx)
4968 });
4969 _ = editor.update(cx, |editor, window, cx| {
4970 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4971 s.select_display_ranges([
4972 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4973 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4974 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4975 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4976 ])
4977 });
4978 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4979 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4980 assert_eq!(
4981 editor.selections.display_ranges(cx),
4982 vec![
4983 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4984 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4985 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4986 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4987 ]
4988 );
4989 });
4990
4991 let editor = cx.add_window(|window, cx| {
4992 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4993 build_editor(buffer, window, cx)
4994 });
4995 _ = editor.update(cx, |editor, window, cx| {
4996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4997 s.select_display_ranges([
4998 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4999 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5000 ])
5001 });
5002 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5003 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5004 assert_eq!(
5005 editor.selections.display_ranges(cx),
5006 vec![
5007 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5008 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5009 ]
5010 );
5011 });
5012
5013 let editor = cx.add_window(|window, cx| {
5014 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5015 build_editor(buffer, window, cx)
5016 });
5017 _ = editor.update(cx, |editor, window, cx| {
5018 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5019 s.select_display_ranges([
5020 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5021 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5022 ])
5023 });
5024 editor.duplicate_selection(&DuplicateSelection, window, cx);
5025 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5026 assert_eq!(
5027 editor.selections.display_ranges(cx),
5028 vec![
5029 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5030 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5031 ]
5032 );
5033 });
5034}
5035
5036#[gpui::test]
5037fn test_move_line_up_down(cx: &mut TestAppContext) {
5038 init_test(cx, |_| {});
5039
5040 let editor = cx.add_window(|window, cx| {
5041 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5042 build_editor(buffer, window, cx)
5043 });
5044 _ = editor.update(cx, |editor, window, cx| {
5045 editor.fold_creases(
5046 vec![
5047 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5048 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5049 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5050 ],
5051 true,
5052 window,
5053 cx,
5054 );
5055 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5056 s.select_display_ranges([
5057 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5058 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5059 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5060 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5061 ])
5062 });
5063 assert_eq!(
5064 editor.display_text(cx),
5065 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5066 );
5067
5068 editor.move_line_up(&MoveLineUp, window, cx);
5069 assert_eq!(
5070 editor.display_text(cx),
5071 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5072 );
5073 assert_eq!(
5074 editor.selections.display_ranges(cx),
5075 vec![
5076 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5077 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5078 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5079 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5080 ]
5081 );
5082 });
5083
5084 _ = editor.update(cx, |editor, window, cx| {
5085 editor.move_line_down(&MoveLineDown, window, cx);
5086 assert_eq!(
5087 editor.display_text(cx),
5088 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5089 );
5090 assert_eq!(
5091 editor.selections.display_ranges(cx),
5092 vec![
5093 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5094 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5095 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5096 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5097 ]
5098 );
5099 });
5100
5101 _ = editor.update(cx, |editor, window, cx| {
5102 editor.move_line_down(&MoveLineDown, window, cx);
5103 assert_eq!(
5104 editor.display_text(cx),
5105 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5106 );
5107 assert_eq!(
5108 editor.selections.display_ranges(cx),
5109 vec![
5110 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5111 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5112 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5113 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5114 ]
5115 );
5116 });
5117
5118 _ = editor.update(cx, |editor, window, cx| {
5119 editor.move_line_up(&MoveLineUp, window, cx);
5120 assert_eq!(
5121 editor.display_text(cx),
5122 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5123 );
5124 assert_eq!(
5125 editor.selections.display_ranges(cx),
5126 vec![
5127 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5128 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5129 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5130 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5131 ]
5132 );
5133 });
5134}
5135
5136#[gpui::test]
5137fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5138 init_test(cx, |_| {});
5139 let editor = cx.add_window(|window, cx| {
5140 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5141 build_editor(buffer, window, cx)
5142 });
5143 _ = editor.update(cx, |editor, window, cx| {
5144 editor.fold_creases(
5145 vec![Crease::simple(
5146 Point::new(6, 4)..Point::new(7, 4),
5147 FoldPlaceholder::test(),
5148 )],
5149 true,
5150 window,
5151 cx,
5152 );
5153 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5154 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5155 });
5156 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5157 editor.move_line_up(&MoveLineUp, window, cx);
5158 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5159 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5160 });
5161}
5162
5163#[gpui::test]
5164fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5165 init_test(cx, |_| {});
5166
5167 let editor = cx.add_window(|window, cx| {
5168 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5169 build_editor(buffer, window, cx)
5170 });
5171 _ = editor.update(cx, |editor, window, cx| {
5172 let snapshot = editor.buffer.read(cx).snapshot(cx);
5173 editor.insert_blocks(
5174 [BlockProperties {
5175 style: BlockStyle::Fixed,
5176 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5177 height: Some(1),
5178 render: Arc::new(|_| div().into_any()),
5179 priority: 0,
5180 }],
5181 Some(Autoscroll::fit()),
5182 cx,
5183 );
5184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5185 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5186 });
5187 editor.move_line_down(&MoveLineDown, window, cx);
5188 });
5189}
5190
5191#[gpui::test]
5192async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5193 init_test(cx, |_| {});
5194
5195 let mut cx = EditorTestContext::new(cx).await;
5196 cx.set_state(
5197 &"
5198 ˇzero
5199 one
5200 two
5201 three
5202 four
5203 five
5204 "
5205 .unindent(),
5206 );
5207
5208 // Create a four-line block that replaces three lines of text.
5209 cx.update_editor(|editor, window, cx| {
5210 let snapshot = editor.snapshot(window, cx);
5211 let snapshot = &snapshot.buffer_snapshot;
5212 let placement = BlockPlacement::Replace(
5213 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5214 );
5215 editor.insert_blocks(
5216 [BlockProperties {
5217 placement,
5218 height: Some(4),
5219 style: BlockStyle::Sticky,
5220 render: Arc::new(|_| gpui::div().into_any_element()),
5221 priority: 0,
5222 }],
5223 None,
5224 cx,
5225 );
5226 });
5227
5228 // Move down so that the cursor touches the block.
5229 cx.update_editor(|editor, window, cx| {
5230 editor.move_down(&Default::default(), window, cx);
5231 });
5232 cx.assert_editor_state(
5233 &"
5234 zero
5235 «one
5236 two
5237 threeˇ»
5238 four
5239 five
5240 "
5241 .unindent(),
5242 );
5243
5244 // Move down past the block.
5245 cx.update_editor(|editor, window, cx| {
5246 editor.move_down(&Default::default(), window, cx);
5247 });
5248 cx.assert_editor_state(
5249 &"
5250 zero
5251 one
5252 two
5253 three
5254 ˇfour
5255 five
5256 "
5257 .unindent(),
5258 );
5259}
5260
5261#[gpui::test]
5262fn test_transpose(cx: &mut TestAppContext) {
5263 init_test(cx, |_| {});
5264
5265 _ = cx.add_window(|window, cx| {
5266 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5267 editor.set_style(EditorStyle::default(), window, cx);
5268 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5269 s.select_ranges([1..1])
5270 });
5271 editor.transpose(&Default::default(), window, cx);
5272 assert_eq!(editor.text(cx), "bac");
5273 assert_eq!(editor.selections.ranges(cx), [2..2]);
5274
5275 editor.transpose(&Default::default(), window, cx);
5276 assert_eq!(editor.text(cx), "bca");
5277 assert_eq!(editor.selections.ranges(cx), [3..3]);
5278
5279 editor.transpose(&Default::default(), window, cx);
5280 assert_eq!(editor.text(cx), "bac");
5281 assert_eq!(editor.selections.ranges(cx), [3..3]);
5282
5283 editor
5284 });
5285
5286 _ = cx.add_window(|window, cx| {
5287 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5288 editor.set_style(EditorStyle::default(), window, cx);
5289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5290 s.select_ranges([3..3])
5291 });
5292 editor.transpose(&Default::default(), window, cx);
5293 assert_eq!(editor.text(cx), "acb\nde");
5294 assert_eq!(editor.selections.ranges(cx), [3..3]);
5295
5296 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5297 s.select_ranges([4..4])
5298 });
5299 editor.transpose(&Default::default(), window, cx);
5300 assert_eq!(editor.text(cx), "acbd\ne");
5301 assert_eq!(editor.selections.ranges(cx), [5..5]);
5302
5303 editor.transpose(&Default::default(), window, cx);
5304 assert_eq!(editor.text(cx), "acbde\n");
5305 assert_eq!(editor.selections.ranges(cx), [6..6]);
5306
5307 editor.transpose(&Default::default(), window, cx);
5308 assert_eq!(editor.text(cx), "acbd\ne");
5309 assert_eq!(editor.selections.ranges(cx), [6..6]);
5310
5311 editor
5312 });
5313
5314 _ = cx.add_window(|window, cx| {
5315 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5316 editor.set_style(EditorStyle::default(), window, cx);
5317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5318 s.select_ranges([1..1, 2..2, 4..4])
5319 });
5320 editor.transpose(&Default::default(), window, cx);
5321 assert_eq!(editor.text(cx), "bacd\ne");
5322 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5323
5324 editor.transpose(&Default::default(), window, cx);
5325 assert_eq!(editor.text(cx), "bcade\n");
5326 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5327
5328 editor.transpose(&Default::default(), window, cx);
5329 assert_eq!(editor.text(cx), "bcda\ne");
5330 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5331
5332 editor.transpose(&Default::default(), window, cx);
5333 assert_eq!(editor.text(cx), "bcade\n");
5334 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5335
5336 editor.transpose(&Default::default(), window, cx);
5337 assert_eq!(editor.text(cx), "bcaed\n");
5338 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5339
5340 editor
5341 });
5342
5343 _ = cx.add_window(|window, cx| {
5344 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5345 editor.set_style(EditorStyle::default(), window, cx);
5346 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5347 s.select_ranges([4..4])
5348 });
5349 editor.transpose(&Default::default(), window, cx);
5350 assert_eq!(editor.text(cx), "🏀🍐✋");
5351 assert_eq!(editor.selections.ranges(cx), [8..8]);
5352
5353 editor.transpose(&Default::default(), window, cx);
5354 assert_eq!(editor.text(cx), "🏀✋🍐");
5355 assert_eq!(editor.selections.ranges(cx), [11..11]);
5356
5357 editor.transpose(&Default::default(), window, cx);
5358 assert_eq!(editor.text(cx), "🏀🍐✋");
5359 assert_eq!(editor.selections.ranges(cx), [11..11]);
5360
5361 editor
5362 });
5363}
5364
5365#[gpui::test]
5366async fn test_rewrap(cx: &mut TestAppContext) {
5367 init_test(cx, |settings| {
5368 settings.languages.0.extend([
5369 (
5370 "Markdown".into(),
5371 LanguageSettingsContent {
5372 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5373 preferred_line_length: Some(40),
5374 ..Default::default()
5375 },
5376 ),
5377 (
5378 "Plain Text".into(),
5379 LanguageSettingsContent {
5380 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5381 preferred_line_length: Some(40),
5382 ..Default::default()
5383 },
5384 ),
5385 (
5386 "C++".into(),
5387 LanguageSettingsContent {
5388 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5389 preferred_line_length: Some(40),
5390 ..Default::default()
5391 },
5392 ),
5393 (
5394 "Python".into(),
5395 LanguageSettingsContent {
5396 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5397 preferred_line_length: Some(40),
5398 ..Default::default()
5399 },
5400 ),
5401 (
5402 "Rust".into(),
5403 LanguageSettingsContent {
5404 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5405 preferred_line_length: Some(40),
5406 ..Default::default()
5407 },
5408 ),
5409 ])
5410 });
5411
5412 let mut cx = EditorTestContext::new(cx).await;
5413
5414 let cpp_language = Arc::new(Language::new(
5415 LanguageConfig {
5416 name: "C++".into(),
5417 line_comments: vec!["// ".into()],
5418 ..LanguageConfig::default()
5419 },
5420 None,
5421 ));
5422 let python_language = Arc::new(Language::new(
5423 LanguageConfig {
5424 name: "Python".into(),
5425 line_comments: vec!["# ".into()],
5426 ..LanguageConfig::default()
5427 },
5428 None,
5429 ));
5430 let markdown_language = Arc::new(Language::new(
5431 LanguageConfig {
5432 name: "Markdown".into(),
5433 rewrap_prefixes: vec![
5434 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5435 regex::Regex::new("[-*+]\\s+").unwrap(),
5436 ],
5437 ..LanguageConfig::default()
5438 },
5439 None,
5440 ));
5441 let rust_language = Arc::new(Language::new(
5442 LanguageConfig {
5443 name: "Rust".into(),
5444 line_comments: vec!["// ".into(), "/// ".into()],
5445 ..LanguageConfig::default()
5446 },
5447 Some(tree_sitter_rust::LANGUAGE.into()),
5448 ));
5449
5450 let plaintext_language = Arc::new(Language::new(
5451 LanguageConfig {
5452 name: "Plain Text".into(),
5453 ..LanguageConfig::default()
5454 },
5455 None,
5456 ));
5457
5458 // Test basic rewrapping of a long line with a cursor
5459 assert_rewrap(
5460 indoc! {"
5461 // ˇThis is a long comment that needs to be wrapped.
5462 "},
5463 indoc! {"
5464 // ˇThis is a long comment that needs to
5465 // be wrapped.
5466 "},
5467 cpp_language.clone(),
5468 &mut cx,
5469 );
5470
5471 // Test rewrapping a full selection
5472 assert_rewrap(
5473 indoc! {"
5474 «// This selected long comment needs to be wrapped.ˇ»"
5475 },
5476 indoc! {"
5477 «// This selected long comment needs to
5478 // be wrapped.ˇ»"
5479 },
5480 cpp_language.clone(),
5481 &mut cx,
5482 );
5483
5484 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5485 assert_rewrap(
5486 indoc! {"
5487 // ˇThis is the first line.
5488 // Thisˇ is the second line.
5489 // This is the thirdˇ line, all part of one paragraph.
5490 "},
5491 indoc! {"
5492 // ˇThis is the first line. Thisˇ is the
5493 // second line. This is the thirdˇ line,
5494 // all part of one paragraph.
5495 "},
5496 cpp_language.clone(),
5497 &mut cx,
5498 );
5499
5500 // Test multiple cursors in different paragraphs trigger separate rewraps
5501 assert_rewrap(
5502 indoc! {"
5503 // ˇThis is the first paragraph, first line.
5504 // ˇThis is the first paragraph, second line.
5505
5506 // ˇThis is the second paragraph, first line.
5507 // ˇThis is the second paragraph, second line.
5508 "},
5509 indoc! {"
5510 // ˇThis is the first paragraph, first
5511 // line. ˇThis is the first paragraph,
5512 // second line.
5513
5514 // ˇThis is the second paragraph, first
5515 // line. ˇThis is the second paragraph,
5516 // second line.
5517 "},
5518 cpp_language.clone(),
5519 &mut cx,
5520 );
5521
5522 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5523 assert_rewrap(
5524 indoc! {"
5525 «// A regular long long comment to be wrapped.
5526 /// A documentation long comment to be wrapped.ˇ»
5527 "},
5528 indoc! {"
5529 «// A regular long long comment to be
5530 // wrapped.
5531 /// A documentation long comment to be
5532 /// wrapped.ˇ»
5533 "},
5534 rust_language.clone(),
5535 &mut cx,
5536 );
5537
5538 // Test that change in indentation level trigger seperate rewraps
5539 assert_rewrap(
5540 indoc! {"
5541 fn foo() {
5542 «// This is a long comment at the base indent.
5543 // This is a long comment at the next indent.ˇ»
5544 }
5545 "},
5546 indoc! {"
5547 fn foo() {
5548 «// This is a long comment at the
5549 // base indent.
5550 // This is a long comment at the
5551 // next indent.ˇ»
5552 }
5553 "},
5554 rust_language.clone(),
5555 &mut cx,
5556 );
5557
5558 // Test that different comment prefix characters (e.g., '#') are handled correctly
5559 assert_rewrap(
5560 indoc! {"
5561 # ˇThis is a long comment using a pound sign.
5562 "},
5563 indoc! {"
5564 # ˇThis is a long comment using a pound
5565 # sign.
5566 "},
5567 python_language,
5568 &mut cx,
5569 );
5570
5571 // Test rewrapping only affects comments, not code even when selected
5572 assert_rewrap(
5573 indoc! {"
5574 «/// This doc comment is long and should be wrapped.
5575 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5576 "},
5577 indoc! {"
5578 «/// This doc comment is long and should
5579 /// be wrapped.
5580 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5581 "},
5582 rust_language.clone(),
5583 &mut cx,
5584 );
5585
5586 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5587 assert_rewrap(
5588 indoc! {"
5589 # Header
5590
5591 A long long long line of markdown text to wrap.ˇ
5592 "},
5593 indoc! {"
5594 # Header
5595
5596 A long long long line of markdown text
5597 to wrap.ˇ
5598 "},
5599 markdown_language.clone(),
5600 &mut cx,
5601 );
5602
5603 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5604 assert_rewrap(
5605 indoc! {"
5606 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5607 2. This is a numbered list item that is very long and needs to be wrapped properly.
5608 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5609 "},
5610 indoc! {"
5611 «1. This is a numbered list item that is
5612 very long and needs to be wrapped
5613 properly.
5614 2. This is a numbered list item that is
5615 very long and needs to be wrapped
5616 properly.
5617 - This is an unordered list item that is
5618 also very long and should not merge
5619 with the numbered item.ˇ»
5620 "},
5621 markdown_language.clone(),
5622 &mut cx,
5623 );
5624
5625 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5626 assert_rewrap(
5627 indoc! {"
5628 «1. This is a numbered list item that is
5629 very long and needs to be wrapped
5630 properly.
5631 2. This is a numbered list item that is
5632 very long and needs to be wrapped
5633 properly.
5634 - This is an unordered list item that is
5635 also very long and should not merge with
5636 the numbered item.ˇ»
5637 "},
5638 indoc! {"
5639 «1. This is a numbered list item that is
5640 very long and needs to be wrapped
5641 properly.
5642 2. This is a numbered list item that is
5643 very long and needs to be wrapped
5644 properly.
5645 - This is an unordered list item that is
5646 also very long and should not merge
5647 with the numbered item.ˇ»
5648 "},
5649 markdown_language.clone(),
5650 &mut cx,
5651 );
5652
5653 // Test that rewrapping maintain indents even when they already exists.
5654 assert_rewrap(
5655 indoc! {"
5656 «1. This is a numbered list
5657 item that is very long and needs to be wrapped properly.
5658 2. This is a numbered list
5659 item that is very long and needs to be wrapped properly.
5660 - This is an unordered list item that is also very long and
5661 should not merge with the numbered item.ˇ»
5662 "},
5663 indoc! {"
5664 «1. This is a numbered list item that is
5665 very long and needs to be wrapped
5666 properly.
5667 2. This is a numbered list item that is
5668 very long and needs to be wrapped
5669 properly.
5670 - This is an unordered list item that is
5671 also very long and should not merge
5672 with the numbered item.ˇ»
5673 "},
5674 markdown_language,
5675 &mut cx,
5676 );
5677
5678 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5679 assert_rewrap(
5680 indoc! {"
5681 ˇThis is a very long line of plain text that will be wrapped.
5682 "},
5683 indoc! {"
5684 ˇThis is a very long line of plain text
5685 that will be wrapped.
5686 "},
5687 plaintext_language.clone(),
5688 &mut cx,
5689 );
5690
5691 // Test that non-commented code acts as a paragraph boundary within a selection
5692 assert_rewrap(
5693 indoc! {"
5694 «// This is the first long comment block to be wrapped.
5695 fn my_func(a: u32);
5696 // This is the second long comment block to be wrapped.ˇ»
5697 "},
5698 indoc! {"
5699 «// This is the first long comment block
5700 // to be wrapped.
5701 fn my_func(a: u32);
5702 // This is the second long comment block
5703 // to be wrapped.ˇ»
5704 "},
5705 rust_language,
5706 &mut cx,
5707 );
5708
5709 // Test rewrapping multiple selections, including ones with blank lines or tabs
5710 assert_rewrap(
5711 indoc! {"
5712 «ˇThis is a very long line that will be wrapped.
5713
5714 This is another paragraph in the same selection.»
5715
5716 «\tThis is a very long indented line that will be wrapped.ˇ»
5717 "},
5718 indoc! {"
5719 «ˇThis is a very long line that will be
5720 wrapped.
5721
5722 This is another paragraph in the same
5723 selection.»
5724
5725 «\tThis is a very long indented line
5726 \tthat will be wrapped.ˇ»
5727 "},
5728 plaintext_language,
5729 &mut cx,
5730 );
5731
5732 // Test that an empty comment line acts as a paragraph boundary
5733 assert_rewrap(
5734 indoc! {"
5735 // ˇThis is a long comment that will be wrapped.
5736 //
5737 // And this is another long comment that will also be wrapped.ˇ
5738 "},
5739 indoc! {"
5740 // ˇThis is a long comment that will be
5741 // wrapped.
5742 //
5743 // And this is another long comment that
5744 // will also be wrapped.ˇ
5745 "},
5746 cpp_language,
5747 &mut cx,
5748 );
5749
5750 #[track_caller]
5751 fn assert_rewrap(
5752 unwrapped_text: &str,
5753 wrapped_text: &str,
5754 language: Arc<Language>,
5755 cx: &mut EditorTestContext,
5756 ) {
5757 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5758 cx.set_state(unwrapped_text);
5759 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5760 cx.assert_editor_state(wrapped_text);
5761 }
5762}
5763
5764#[gpui::test]
5765async fn test_hard_wrap(cx: &mut TestAppContext) {
5766 init_test(cx, |_| {});
5767 let mut cx = EditorTestContext::new(cx).await;
5768
5769 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5770 cx.update_editor(|editor, _, cx| {
5771 editor.set_hard_wrap(Some(14), cx);
5772 });
5773
5774 cx.set_state(indoc!(
5775 "
5776 one two three ˇ
5777 "
5778 ));
5779 cx.simulate_input("four");
5780 cx.run_until_parked();
5781
5782 cx.assert_editor_state(indoc!(
5783 "
5784 one two three
5785 fourˇ
5786 "
5787 ));
5788
5789 cx.update_editor(|editor, window, cx| {
5790 editor.newline(&Default::default(), window, cx);
5791 });
5792 cx.run_until_parked();
5793 cx.assert_editor_state(indoc!(
5794 "
5795 one two three
5796 four
5797 ˇ
5798 "
5799 ));
5800
5801 cx.simulate_input("five");
5802 cx.run_until_parked();
5803 cx.assert_editor_state(indoc!(
5804 "
5805 one two three
5806 four
5807 fiveˇ
5808 "
5809 ));
5810
5811 cx.update_editor(|editor, window, cx| {
5812 editor.newline(&Default::default(), window, cx);
5813 });
5814 cx.run_until_parked();
5815 cx.simulate_input("# ");
5816 cx.run_until_parked();
5817 cx.assert_editor_state(indoc!(
5818 "
5819 one two three
5820 four
5821 five
5822 # ˇ
5823 "
5824 ));
5825
5826 cx.update_editor(|editor, window, cx| {
5827 editor.newline(&Default::default(), window, cx);
5828 });
5829 cx.run_until_parked();
5830 cx.assert_editor_state(indoc!(
5831 "
5832 one two three
5833 four
5834 five
5835 #\x20
5836 #ˇ
5837 "
5838 ));
5839
5840 cx.simulate_input(" 6");
5841 cx.run_until_parked();
5842 cx.assert_editor_state(indoc!(
5843 "
5844 one two three
5845 four
5846 five
5847 #
5848 # 6ˇ
5849 "
5850 ));
5851}
5852
5853#[gpui::test]
5854async fn test_clipboard(cx: &mut TestAppContext) {
5855 init_test(cx, |_| {});
5856
5857 let mut cx = EditorTestContext::new(cx).await;
5858
5859 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5860 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5861 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5862
5863 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5864 cx.set_state("two ˇfour ˇsix ˇ");
5865 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5866 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5867
5868 // Paste again but with only two cursors. Since the number of cursors doesn't
5869 // match the number of slices in the clipboard, the entire clipboard text
5870 // is pasted at each cursor.
5871 cx.set_state("ˇtwo one✅ four three six five ˇ");
5872 cx.update_editor(|e, window, cx| {
5873 e.handle_input("( ", window, cx);
5874 e.paste(&Paste, window, cx);
5875 e.handle_input(") ", window, cx);
5876 });
5877 cx.assert_editor_state(
5878 &([
5879 "( one✅ ",
5880 "three ",
5881 "five ) ˇtwo one✅ four three six five ( one✅ ",
5882 "three ",
5883 "five ) ˇ",
5884 ]
5885 .join("\n")),
5886 );
5887
5888 // Cut with three selections, one of which is full-line.
5889 cx.set_state(indoc! {"
5890 1«2ˇ»3
5891 4ˇ567
5892 «8ˇ»9"});
5893 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5894 cx.assert_editor_state(indoc! {"
5895 1ˇ3
5896 ˇ9"});
5897
5898 // Paste with three selections, noticing how the copied selection that was full-line
5899 // gets inserted before the second cursor.
5900 cx.set_state(indoc! {"
5901 1ˇ3
5902 9ˇ
5903 «oˇ»ne"});
5904 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5905 cx.assert_editor_state(indoc! {"
5906 12ˇ3
5907 4567
5908 9ˇ
5909 8ˇne"});
5910
5911 // Copy with a single cursor only, which writes the whole line into the clipboard.
5912 cx.set_state(indoc! {"
5913 The quick brown
5914 fox juˇmps over
5915 the lazy dog"});
5916 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5917 assert_eq!(
5918 cx.read_from_clipboard()
5919 .and_then(|item| item.text().as_deref().map(str::to_string)),
5920 Some("fox jumps over\n".to_string())
5921 );
5922
5923 // Paste with three selections, noticing how the copied full-line selection is inserted
5924 // before the empty selections but replaces the selection that is non-empty.
5925 cx.set_state(indoc! {"
5926 Tˇhe quick brown
5927 «foˇ»x jumps over
5928 tˇhe lazy dog"});
5929 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5930 cx.assert_editor_state(indoc! {"
5931 fox jumps over
5932 Tˇhe quick brown
5933 fox jumps over
5934 ˇx jumps over
5935 fox jumps over
5936 tˇhe lazy dog"});
5937}
5938
5939#[gpui::test]
5940async fn test_copy_trim(cx: &mut TestAppContext) {
5941 init_test(cx, |_| {});
5942
5943 let mut cx = EditorTestContext::new(cx).await;
5944 cx.set_state(
5945 r#" «for selection in selections.iter() {
5946 let mut start = selection.start;
5947 let mut end = selection.end;
5948 let is_entire_line = selection.is_empty();
5949 if is_entire_line {
5950 start = Point::new(start.row, 0);ˇ»
5951 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5952 }
5953 "#,
5954 );
5955 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5956 assert_eq!(
5957 cx.read_from_clipboard()
5958 .and_then(|item| item.text().as_deref().map(str::to_string)),
5959 Some(
5960 "for selection in selections.iter() {
5961 let mut start = selection.start;
5962 let mut end = selection.end;
5963 let is_entire_line = selection.is_empty();
5964 if is_entire_line {
5965 start = Point::new(start.row, 0);"
5966 .to_string()
5967 ),
5968 "Regular copying preserves all indentation selected",
5969 );
5970 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5971 assert_eq!(
5972 cx.read_from_clipboard()
5973 .and_then(|item| item.text().as_deref().map(str::to_string)),
5974 Some(
5975 "for selection in selections.iter() {
5976let mut start = selection.start;
5977let mut end = selection.end;
5978let is_entire_line = selection.is_empty();
5979if is_entire_line {
5980 start = Point::new(start.row, 0);"
5981 .to_string()
5982 ),
5983 "Copying with stripping should strip all leading whitespaces"
5984 );
5985
5986 cx.set_state(
5987 r#" « for selection in selections.iter() {
5988 let mut start = selection.start;
5989 let mut end = selection.end;
5990 let is_entire_line = selection.is_empty();
5991 if is_entire_line {
5992 start = Point::new(start.row, 0);ˇ»
5993 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5994 }
5995 "#,
5996 );
5997 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5998 assert_eq!(
5999 cx.read_from_clipboard()
6000 .and_then(|item| item.text().as_deref().map(str::to_string)),
6001 Some(
6002 " for selection in selections.iter() {
6003 let mut start = selection.start;
6004 let mut end = selection.end;
6005 let is_entire_line = selection.is_empty();
6006 if is_entire_line {
6007 start = Point::new(start.row, 0);"
6008 .to_string()
6009 ),
6010 "Regular copying preserves all indentation selected",
6011 );
6012 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6013 assert_eq!(
6014 cx.read_from_clipboard()
6015 .and_then(|item| item.text().as_deref().map(str::to_string)),
6016 Some(
6017 "for selection in selections.iter() {
6018let mut start = selection.start;
6019let mut end = selection.end;
6020let is_entire_line = selection.is_empty();
6021if is_entire_line {
6022 start = Point::new(start.row, 0);"
6023 .to_string()
6024 ),
6025 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6026 );
6027
6028 cx.set_state(
6029 r#" «ˇ for selection in selections.iter() {
6030 let mut start = selection.start;
6031 let mut end = selection.end;
6032 let is_entire_line = selection.is_empty();
6033 if is_entire_line {
6034 start = Point::new(start.row, 0);»
6035 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6036 }
6037 "#,
6038 );
6039 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6040 assert_eq!(
6041 cx.read_from_clipboard()
6042 .and_then(|item| item.text().as_deref().map(str::to_string)),
6043 Some(
6044 " for selection in selections.iter() {
6045 let mut start = selection.start;
6046 let mut end = selection.end;
6047 let is_entire_line = selection.is_empty();
6048 if is_entire_line {
6049 start = Point::new(start.row, 0);"
6050 .to_string()
6051 ),
6052 "Regular copying for reverse selection works the same",
6053 );
6054 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6055 assert_eq!(
6056 cx.read_from_clipboard()
6057 .and_then(|item| item.text().as_deref().map(str::to_string)),
6058 Some(
6059 "for selection in selections.iter() {
6060let mut start = selection.start;
6061let mut end = selection.end;
6062let is_entire_line = selection.is_empty();
6063if is_entire_line {
6064 start = Point::new(start.row, 0);"
6065 .to_string()
6066 ),
6067 "Copying with stripping for reverse selection works the same"
6068 );
6069
6070 cx.set_state(
6071 r#" for selection «in selections.iter() {
6072 let mut start = selection.start;
6073 let mut end = selection.end;
6074 let is_entire_line = selection.is_empty();
6075 if is_entire_line {
6076 start = Point::new(start.row, 0);ˇ»
6077 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6078 }
6079 "#,
6080 );
6081 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6082 assert_eq!(
6083 cx.read_from_clipboard()
6084 .and_then(|item| item.text().as_deref().map(str::to_string)),
6085 Some(
6086 "in selections.iter() {
6087 let mut start = selection.start;
6088 let mut end = selection.end;
6089 let is_entire_line = selection.is_empty();
6090 if is_entire_line {
6091 start = Point::new(start.row, 0);"
6092 .to_string()
6093 ),
6094 "When selecting past the indent, the copying works as usual",
6095 );
6096 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6097 assert_eq!(
6098 cx.read_from_clipboard()
6099 .and_then(|item| item.text().as_deref().map(str::to_string)),
6100 Some(
6101 "in selections.iter() {
6102 let mut start = selection.start;
6103 let mut end = selection.end;
6104 let is_entire_line = selection.is_empty();
6105 if is_entire_line {
6106 start = Point::new(start.row, 0);"
6107 .to_string()
6108 ),
6109 "When selecting past the indent, nothing is trimmed"
6110 );
6111
6112 cx.set_state(
6113 r#" «for selection in selections.iter() {
6114 let mut start = selection.start;
6115
6116 let mut end = selection.end;
6117 let is_entire_line = selection.is_empty();
6118 if is_entire_line {
6119 start = Point::new(start.row, 0);
6120ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6121 }
6122 "#,
6123 );
6124 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6125 assert_eq!(
6126 cx.read_from_clipboard()
6127 .and_then(|item| item.text().as_deref().map(str::to_string)),
6128 Some(
6129 "for selection in selections.iter() {
6130let mut start = selection.start;
6131
6132let mut end = selection.end;
6133let is_entire_line = selection.is_empty();
6134if is_entire_line {
6135 start = Point::new(start.row, 0);
6136"
6137 .to_string()
6138 ),
6139 "Copying with stripping should ignore empty lines"
6140 );
6141}
6142
6143#[gpui::test]
6144async fn test_paste_multiline(cx: &mut TestAppContext) {
6145 init_test(cx, |_| {});
6146
6147 let mut cx = EditorTestContext::new(cx).await;
6148 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6149
6150 // Cut an indented block, without the leading whitespace.
6151 cx.set_state(indoc! {"
6152 const a: B = (
6153 c(),
6154 «d(
6155 e,
6156 f
6157 )ˇ»
6158 );
6159 "});
6160 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6161 cx.assert_editor_state(indoc! {"
6162 const a: B = (
6163 c(),
6164 ˇ
6165 );
6166 "});
6167
6168 // Paste it at the same position.
6169 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6170 cx.assert_editor_state(indoc! {"
6171 const a: B = (
6172 c(),
6173 d(
6174 e,
6175 f
6176 )ˇ
6177 );
6178 "});
6179
6180 // Paste it at a line with a lower indent level.
6181 cx.set_state(indoc! {"
6182 ˇ
6183 const a: B = (
6184 c(),
6185 );
6186 "});
6187 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6188 cx.assert_editor_state(indoc! {"
6189 d(
6190 e,
6191 f
6192 )ˇ
6193 const a: B = (
6194 c(),
6195 );
6196 "});
6197
6198 // Cut an indented block, with the leading whitespace.
6199 cx.set_state(indoc! {"
6200 const a: B = (
6201 c(),
6202 « d(
6203 e,
6204 f
6205 )
6206 ˇ»);
6207 "});
6208 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6209 cx.assert_editor_state(indoc! {"
6210 const a: B = (
6211 c(),
6212 ˇ);
6213 "});
6214
6215 // Paste it at the same position.
6216 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6217 cx.assert_editor_state(indoc! {"
6218 const a: B = (
6219 c(),
6220 d(
6221 e,
6222 f
6223 )
6224 ˇ);
6225 "});
6226
6227 // Paste it at a line with a higher indent level.
6228 cx.set_state(indoc! {"
6229 const a: B = (
6230 c(),
6231 d(
6232 e,
6233 fˇ
6234 )
6235 );
6236 "});
6237 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6238 cx.assert_editor_state(indoc! {"
6239 const a: B = (
6240 c(),
6241 d(
6242 e,
6243 f d(
6244 e,
6245 f
6246 )
6247 ˇ
6248 )
6249 );
6250 "});
6251
6252 // Copy an indented block, starting mid-line
6253 cx.set_state(indoc! {"
6254 const a: B = (
6255 c(),
6256 somethin«g(
6257 e,
6258 f
6259 )ˇ»
6260 );
6261 "});
6262 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6263
6264 // Paste it on a line with a lower indent level
6265 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6266 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6267 cx.assert_editor_state(indoc! {"
6268 const a: B = (
6269 c(),
6270 something(
6271 e,
6272 f
6273 )
6274 );
6275 g(
6276 e,
6277 f
6278 )ˇ"});
6279}
6280
6281#[gpui::test]
6282async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6283 init_test(cx, |_| {});
6284
6285 cx.write_to_clipboard(ClipboardItem::new_string(
6286 " d(\n e\n );\n".into(),
6287 ));
6288
6289 let mut cx = EditorTestContext::new(cx).await;
6290 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6291
6292 cx.set_state(indoc! {"
6293 fn a() {
6294 b();
6295 if c() {
6296 ˇ
6297 }
6298 }
6299 "});
6300
6301 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6302 cx.assert_editor_state(indoc! {"
6303 fn a() {
6304 b();
6305 if c() {
6306 d(
6307 e
6308 );
6309 ˇ
6310 }
6311 }
6312 "});
6313
6314 cx.set_state(indoc! {"
6315 fn a() {
6316 b();
6317 ˇ
6318 }
6319 "});
6320
6321 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6322 cx.assert_editor_state(indoc! {"
6323 fn a() {
6324 b();
6325 d(
6326 e
6327 );
6328 ˇ
6329 }
6330 "});
6331}
6332
6333#[gpui::test]
6334fn test_select_all(cx: &mut TestAppContext) {
6335 init_test(cx, |_| {});
6336
6337 let editor = cx.add_window(|window, cx| {
6338 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6339 build_editor(buffer, window, cx)
6340 });
6341 _ = editor.update(cx, |editor, window, cx| {
6342 editor.select_all(&SelectAll, window, cx);
6343 assert_eq!(
6344 editor.selections.display_ranges(cx),
6345 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6346 );
6347 });
6348}
6349
6350#[gpui::test]
6351fn test_select_line(cx: &mut TestAppContext) {
6352 init_test(cx, |_| {});
6353
6354 let editor = cx.add_window(|window, cx| {
6355 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6356 build_editor(buffer, window, cx)
6357 });
6358 _ = editor.update(cx, |editor, window, cx| {
6359 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6360 s.select_display_ranges([
6361 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6362 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6363 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6364 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6365 ])
6366 });
6367 editor.select_line(&SelectLine, window, cx);
6368 assert_eq!(
6369 editor.selections.display_ranges(cx),
6370 vec![
6371 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6372 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6373 ]
6374 );
6375 });
6376
6377 _ = editor.update(cx, |editor, window, cx| {
6378 editor.select_line(&SelectLine, window, cx);
6379 assert_eq!(
6380 editor.selections.display_ranges(cx),
6381 vec![
6382 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6383 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6384 ]
6385 );
6386 });
6387
6388 _ = editor.update(cx, |editor, window, cx| {
6389 editor.select_line(&SelectLine, window, cx);
6390 assert_eq!(
6391 editor.selections.display_ranges(cx),
6392 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6393 );
6394 });
6395}
6396
6397#[gpui::test]
6398async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6399 init_test(cx, |_| {});
6400 let mut cx = EditorTestContext::new(cx).await;
6401
6402 #[track_caller]
6403 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6404 cx.set_state(initial_state);
6405 cx.update_editor(|e, window, cx| {
6406 e.split_selection_into_lines(&Default::default(), window, cx)
6407 });
6408 cx.assert_editor_state(expected_state);
6409 }
6410
6411 // Selection starts and ends at the middle of lines, left-to-right
6412 test(
6413 &mut cx,
6414 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6415 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6416 );
6417 // Same thing, right-to-left
6418 test(
6419 &mut cx,
6420 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6421 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6422 );
6423
6424 // Whole buffer, left-to-right, last line *doesn't* end with newline
6425 test(
6426 &mut cx,
6427 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6428 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6429 );
6430 // Same thing, right-to-left
6431 test(
6432 &mut cx,
6433 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6434 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6435 );
6436
6437 // Whole buffer, left-to-right, last line ends with newline
6438 test(
6439 &mut cx,
6440 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6441 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6442 );
6443 // Same thing, right-to-left
6444 test(
6445 &mut cx,
6446 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6447 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6448 );
6449
6450 // Starts at the end of a line, ends at the start of another
6451 test(
6452 &mut cx,
6453 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6454 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6455 );
6456}
6457
6458#[gpui::test]
6459async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6460 init_test(cx, |_| {});
6461
6462 let editor = cx.add_window(|window, cx| {
6463 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6464 build_editor(buffer, window, cx)
6465 });
6466
6467 // setup
6468 _ = editor.update(cx, |editor, window, cx| {
6469 editor.fold_creases(
6470 vec![
6471 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6472 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6473 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6474 ],
6475 true,
6476 window,
6477 cx,
6478 );
6479 assert_eq!(
6480 editor.display_text(cx),
6481 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6482 );
6483 });
6484
6485 _ = editor.update(cx, |editor, window, cx| {
6486 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6487 s.select_display_ranges([
6488 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6489 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6490 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6491 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6492 ])
6493 });
6494 editor.split_selection_into_lines(&Default::default(), window, cx);
6495 assert_eq!(
6496 editor.display_text(cx),
6497 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6498 );
6499 });
6500 EditorTestContext::for_editor(editor, cx)
6501 .await
6502 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6503
6504 _ = editor.update(cx, |editor, window, cx| {
6505 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6506 s.select_display_ranges([
6507 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6508 ])
6509 });
6510 editor.split_selection_into_lines(&Default::default(), window, cx);
6511 assert_eq!(
6512 editor.display_text(cx),
6513 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6514 );
6515 assert_eq!(
6516 editor.selections.display_ranges(cx),
6517 [
6518 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6519 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6520 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6521 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6522 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6523 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6524 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6525 ]
6526 );
6527 });
6528 EditorTestContext::for_editor(editor, cx)
6529 .await
6530 .assert_editor_state(
6531 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6532 );
6533}
6534
6535#[gpui::test]
6536async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6537 init_test(cx, |_| {});
6538
6539 let mut cx = EditorTestContext::new(cx).await;
6540
6541 cx.set_state(indoc!(
6542 r#"abc
6543 defˇghi
6544
6545 jk
6546 nlmo
6547 "#
6548 ));
6549
6550 cx.update_editor(|editor, window, cx| {
6551 editor.add_selection_above(&Default::default(), window, cx);
6552 });
6553
6554 cx.assert_editor_state(indoc!(
6555 r#"abcˇ
6556 defˇghi
6557
6558 jk
6559 nlmo
6560 "#
6561 ));
6562
6563 cx.update_editor(|editor, window, cx| {
6564 editor.add_selection_above(&Default::default(), window, cx);
6565 });
6566
6567 cx.assert_editor_state(indoc!(
6568 r#"abcˇ
6569 defˇghi
6570
6571 jk
6572 nlmo
6573 "#
6574 ));
6575
6576 cx.update_editor(|editor, window, cx| {
6577 editor.add_selection_below(&Default::default(), window, cx);
6578 });
6579
6580 cx.assert_editor_state(indoc!(
6581 r#"abc
6582 defˇghi
6583
6584 jk
6585 nlmo
6586 "#
6587 ));
6588
6589 cx.update_editor(|editor, window, cx| {
6590 editor.undo_selection(&Default::default(), window, cx);
6591 });
6592
6593 cx.assert_editor_state(indoc!(
6594 r#"abcˇ
6595 defˇghi
6596
6597 jk
6598 nlmo
6599 "#
6600 ));
6601
6602 cx.update_editor(|editor, window, cx| {
6603 editor.redo_selection(&Default::default(), window, cx);
6604 });
6605
6606 cx.assert_editor_state(indoc!(
6607 r#"abc
6608 defˇghi
6609
6610 jk
6611 nlmo
6612 "#
6613 ));
6614
6615 cx.update_editor(|editor, window, cx| {
6616 editor.add_selection_below(&Default::default(), window, cx);
6617 });
6618
6619 cx.assert_editor_state(indoc!(
6620 r#"abc
6621 defˇghi
6622 ˇ
6623 jk
6624 nlmo
6625 "#
6626 ));
6627
6628 cx.update_editor(|editor, window, cx| {
6629 editor.add_selection_below(&Default::default(), window, cx);
6630 });
6631
6632 cx.assert_editor_state(indoc!(
6633 r#"abc
6634 defˇghi
6635 ˇ
6636 jkˇ
6637 nlmo
6638 "#
6639 ));
6640
6641 cx.update_editor(|editor, window, cx| {
6642 editor.add_selection_below(&Default::default(), window, cx);
6643 });
6644
6645 cx.assert_editor_state(indoc!(
6646 r#"abc
6647 defˇghi
6648 ˇ
6649 jkˇ
6650 nlmˇo
6651 "#
6652 ));
6653
6654 cx.update_editor(|editor, window, cx| {
6655 editor.add_selection_below(&Default::default(), window, cx);
6656 });
6657
6658 cx.assert_editor_state(indoc!(
6659 r#"abc
6660 defˇghi
6661 ˇ
6662 jkˇ
6663 nlmˇo
6664 ˇ"#
6665 ));
6666
6667 // change selections
6668 cx.set_state(indoc!(
6669 r#"abc
6670 def«ˇg»hi
6671
6672 jk
6673 nlmo
6674 "#
6675 ));
6676
6677 cx.update_editor(|editor, window, cx| {
6678 editor.add_selection_below(&Default::default(), window, cx);
6679 });
6680
6681 cx.assert_editor_state(indoc!(
6682 r#"abc
6683 def«ˇg»hi
6684
6685 jk
6686 nlm«ˇo»
6687 "#
6688 ));
6689
6690 cx.update_editor(|editor, window, cx| {
6691 editor.add_selection_below(&Default::default(), window, cx);
6692 });
6693
6694 cx.assert_editor_state(indoc!(
6695 r#"abc
6696 def«ˇg»hi
6697
6698 jk
6699 nlm«ˇo»
6700 "#
6701 ));
6702
6703 cx.update_editor(|editor, window, cx| {
6704 editor.add_selection_above(&Default::default(), window, cx);
6705 });
6706
6707 cx.assert_editor_state(indoc!(
6708 r#"abc
6709 def«ˇg»hi
6710
6711 jk
6712 nlmo
6713 "#
6714 ));
6715
6716 cx.update_editor(|editor, window, cx| {
6717 editor.add_selection_above(&Default::default(), window, cx);
6718 });
6719
6720 cx.assert_editor_state(indoc!(
6721 r#"abc
6722 def«ˇg»hi
6723
6724 jk
6725 nlmo
6726 "#
6727 ));
6728
6729 // Change selections again
6730 cx.set_state(indoc!(
6731 r#"a«bc
6732 defgˇ»hi
6733
6734 jk
6735 nlmo
6736 "#
6737 ));
6738
6739 cx.update_editor(|editor, window, cx| {
6740 editor.add_selection_below(&Default::default(), window, cx);
6741 });
6742
6743 cx.assert_editor_state(indoc!(
6744 r#"a«bcˇ»
6745 d«efgˇ»hi
6746
6747 j«kˇ»
6748 nlmo
6749 "#
6750 ));
6751
6752 cx.update_editor(|editor, window, cx| {
6753 editor.add_selection_below(&Default::default(), window, cx);
6754 });
6755 cx.assert_editor_state(indoc!(
6756 r#"a«bcˇ»
6757 d«efgˇ»hi
6758
6759 j«kˇ»
6760 n«lmoˇ»
6761 "#
6762 ));
6763 cx.update_editor(|editor, window, cx| {
6764 editor.add_selection_above(&Default::default(), window, cx);
6765 });
6766
6767 cx.assert_editor_state(indoc!(
6768 r#"a«bcˇ»
6769 d«efgˇ»hi
6770
6771 j«kˇ»
6772 nlmo
6773 "#
6774 ));
6775
6776 // Change selections again
6777 cx.set_state(indoc!(
6778 r#"abc
6779 d«ˇefghi
6780
6781 jk
6782 nlm»o
6783 "#
6784 ));
6785
6786 cx.update_editor(|editor, window, cx| {
6787 editor.add_selection_above(&Default::default(), window, cx);
6788 });
6789
6790 cx.assert_editor_state(indoc!(
6791 r#"a«ˇbc»
6792 d«ˇef»ghi
6793
6794 j«ˇk»
6795 n«ˇlm»o
6796 "#
6797 ));
6798
6799 cx.update_editor(|editor, window, cx| {
6800 editor.add_selection_below(&Default::default(), window, cx);
6801 });
6802
6803 cx.assert_editor_state(indoc!(
6804 r#"abc
6805 d«ˇef»ghi
6806
6807 j«ˇk»
6808 n«ˇlm»o
6809 "#
6810 ));
6811}
6812
6813#[gpui::test]
6814async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6815 init_test(cx, |_| {});
6816 let mut cx = EditorTestContext::new(cx).await;
6817
6818 cx.set_state(indoc!(
6819 r#"line onˇe
6820 liˇne two
6821 line three
6822 line four"#
6823 ));
6824
6825 cx.update_editor(|editor, window, cx| {
6826 editor.add_selection_below(&Default::default(), window, cx);
6827 });
6828
6829 // test multiple cursors expand in the same direction
6830 cx.assert_editor_state(indoc!(
6831 r#"line onˇe
6832 liˇne twˇo
6833 liˇne three
6834 line four"#
6835 ));
6836
6837 cx.update_editor(|editor, window, cx| {
6838 editor.add_selection_below(&Default::default(), window, cx);
6839 });
6840
6841 cx.update_editor(|editor, window, cx| {
6842 editor.add_selection_below(&Default::default(), window, cx);
6843 });
6844
6845 // test multiple cursors expand below overflow
6846 cx.assert_editor_state(indoc!(
6847 r#"line onˇe
6848 liˇne twˇo
6849 liˇne thˇree
6850 liˇne foˇur"#
6851 ));
6852
6853 cx.update_editor(|editor, window, cx| {
6854 editor.add_selection_above(&Default::default(), window, cx);
6855 });
6856
6857 // test multiple cursors retrieves back correctly
6858 cx.assert_editor_state(indoc!(
6859 r#"line onˇe
6860 liˇne twˇo
6861 liˇne thˇree
6862 line four"#
6863 ));
6864
6865 cx.update_editor(|editor, window, cx| {
6866 editor.add_selection_above(&Default::default(), window, cx);
6867 });
6868
6869 cx.update_editor(|editor, window, cx| {
6870 editor.add_selection_above(&Default::default(), window, cx);
6871 });
6872
6873 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6874 cx.assert_editor_state(indoc!(
6875 r#"liˇne onˇe
6876 liˇne two
6877 line three
6878 line four"#
6879 ));
6880
6881 cx.update_editor(|editor, window, cx| {
6882 editor.undo_selection(&Default::default(), window, cx);
6883 });
6884
6885 // test undo
6886 cx.assert_editor_state(indoc!(
6887 r#"line onˇe
6888 liˇne twˇo
6889 line three
6890 line four"#
6891 ));
6892
6893 cx.update_editor(|editor, window, cx| {
6894 editor.redo_selection(&Default::default(), window, cx);
6895 });
6896
6897 // test redo
6898 cx.assert_editor_state(indoc!(
6899 r#"liˇne onˇe
6900 liˇne two
6901 line three
6902 line four"#
6903 ));
6904
6905 cx.set_state(indoc!(
6906 r#"abcd
6907 ef«ghˇ»
6908 ijkl
6909 «mˇ»nop"#
6910 ));
6911
6912 cx.update_editor(|editor, window, cx| {
6913 editor.add_selection_above(&Default::default(), window, cx);
6914 });
6915
6916 // test multiple selections expand in the same direction
6917 cx.assert_editor_state(indoc!(
6918 r#"ab«cdˇ»
6919 ef«ghˇ»
6920 «iˇ»jkl
6921 «mˇ»nop"#
6922 ));
6923
6924 cx.update_editor(|editor, window, cx| {
6925 editor.add_selection_above(&Default::default(), window, cx);
6926 });
6927
6928 // test multiple selection upward overflow
6929 cx.assert_editor_state(indoc!(
6930 r#"ab«cdˇ»
6931 «eˇ»f«ghˇ»
6932 «iˇ»jkl
6933 «mˇ»nop"#
6934 ));
6935
6936 cx.update_editor(|editor, window, cx| {
6937 editor.add_selection_below(&Default::default(), window, cx);
6938 });
6939
6940 // test multiple selection retrieves back correctly
6941 cx.assert_editor_state(indoc!(
6942 r#"abcd
6943 ef«ghˇ»
6944 «iˇ»jkl
6945 «mˇ»nop"#
6946 ));
6947
6948 cx.update_editor(|editor, window, cx| {
6949 editor.add_selection_below(&Default::default(), window, cx);
6950 });
6951
6952 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6953 cx.assert_editor_state(indoc!(
6954 r#"abcd
6955 ef«ghˇ»
6956 ij«klˇ»
6957 «mˇ»nop"#
6958 ));
6959
6960 cx.update_editor(|editor, window, cx| {
6961 editor.undo_selection(&Default::default(), window, cx);
6962 });
6963
6964 // test undo
6965 cx.assert_editor_state(indoc!(
6966 r#"abcd
6967 ef«ghˇ»
6968 «iˇ»jkl
6969 «mˇ»nop"#
6970 ));
6971
6972 cx.update_editor(|editor, window, cx| {
6973 editor.redo_selection(&Default::default(), window, cx);
6974 });
6975
6976 // test redo
6977 cx.assert_editor_state(indoc!(
6978 r#"abcd
6979 ef«ghˇ»
6980 ij«klˇ»
6981 «mˇ»nop"#
6982 ));
6983}
6984
6985#[gpui::test]
6986async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6987 init_test(cx, |_| {});
6988 let mut cx = EditorTestContext::new(cx).await;
6989
6990 cx.set_state(indoc!(
6991 r#"line onˇe
6992 liˇne two
6993 line three
6994 line four"#
6995 ));
6996
6997 cx.update_editor(|editor, window, cx| {
6998 editor.add_selection_below(&Default::default(), window, cx);
6999 editor.add_selection_below(&Default::default(), window, cx);
7000 editor.add_selection_below(&Default::default(), window, cx);
7001 });
7002
7003 // initial state with two multi cursor groups
7004 cx.assert_editor_state(indoc!(
7005 r#"line onˇe
7006 liˇne twˇo
7007 liˇne thˇree
7008 liˇne foˇur"#
7009 ));
7010
7011 // add single cursor in middle - simulate opt click
7012 cx.update_editor(|editor, window, cx| {
7013 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7014 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7015 editor.end_selection(window, cx);
7016 });
7017
7018 cx.assert_editor_state(indoc!(
7019 r#"line onˇe
7020 liˇne twˇo
7021 liˇneˇ thˇree
7022 liˇne foˇur"#
7023 ));
7024
7025 cx.update_editor(|editor, window, cx| {
7026 editor.add_selection_above(&Default::default(), window, cx);
7027 });
7028
7029 // test new added selection expands above and existing selection shrinks
7030 cx.assert_editor_state(indoc!(
7031 r#"line onˇe
7032 liˇneˇ twˇo
7033 liˇneˇ thˇree
7034 line four"#
7035 ));
7036
7037 cx.update_editor(|editor, window, cx| {
7038 editor.add_selection_above(&Default::default(), window, cx);
7039 });
7040
7041 // test new added selection expands above and existing selection shrinks
7042 cx.assert_editor_state(indoc!(
7043 r#"lineˇ onˇe
7044 liˇneˇ twˇo
7045 lineˇ three
7046 line four"#
7047 ));
7048
7049 // intial state with two selection groups
7050 cx.set_state(indoc!(
7051 r#"abcd
7052 ef«ghˇ»
7053 ijkl
7054 «mˇ»nop"#
7055 ));
7056
7057 cx.update_editor(|editor, window, cx| {
7058 editor.add_selection_above(&Default::default(), window, cx);
7059 editor.add_selection_above(&Default::default(), window, cx);
7060 });
7061
7062 cx.assert_editor_state(indoc!(
7063 r#"ab«cdˇ»
7064 «eˇ»f«ghˇ»
7065 «iˇ»jkl
7066 «mˇ»nop"#
7067 ));
7068
7069 // add single selection in middle - simulate opt drag
7070 cx.update_editor(|editor, window, cx| {
7071 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7072 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7073 editor.update_selection(
7074 DisplayPoint::new(DisplayRow(2), 4),
7075 0,
7076 gpui::Point::<f32>::default(),
7077 window,
7078 cx,
7079 );
7080 editor.end_selection(window, cx);
7081 });
7082
7083 cx.assert_editor_state(indoc!(
7084 r#"ab«cdˇ»
7085 «eˇ»f«ghˇ»
7086 «iˇ»jk«lˇ»
7087 «mˇ»nop"#
7088 ));
7089
7090 cx.update_editor(|editor, window, cx| {
7091 editor.add_selection_below(&Default::default(), window, cx);
7092 });
7093
7094 // test new added selection expands below, others shrinks from above
7095 cx.assert_editor_state(indoc!(
7096 r#"abcd
7097 ef«ghˇ»
7098 «iˇ»jk«lˇ»
7099 «mˇ»no«pˇ»"#
7100 ));
7101}
7102
7103#[gpui::test]
7104async fn test_select_next(cx: &mut TestAppContext) {
7105 init_test(cx, |_| {});
7106
7107 let mut cx = EditorTestContext::new(cx).await;
7108 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7109
7110 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7111 .unwrap();
7112 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7113
7114 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7115 .unwrap();
7116 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7117
7118 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7119 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7120
7121 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7122 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7123
7124 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7125 .unwrap();
7126 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7127
7128 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7129 .unwrap();
7130 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7131
7132 // Test selection direction should be preserved
7133 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7134
7135 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7136 .unwrap();
7137 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7138}
7139
7140#[gpui::test]
7141async fn test_select_all_matches(cx: &mut TestAppContext) {
7142 init_test(cx, |_| {});
7143
7144 let mut cx = EditorTestContext::new(cx).await;
7145
7146 // Test caret-only selections
7147 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7148 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7149 .unwrap();
7150 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7151
7152 // Test left-to-right selections
7153 cx.set_state("abc\n«abcˇ»\nabc");
7154 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7155 .unwrap();
7156 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7157
7158 // Test right-to-left selections
7159 cx.set_state("abc\n«ˇabc»\nabc");
7160 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7161 .unwrap();
7162 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7163
7164 // Test selecting whitespace with caret selection
7165 cx.set_state("abc\nˇ abc\nabc");
7166 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7167 .unwrap();
7168 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7169
7170 // Test selecting whitespace with left-to-right selection
7171 cx.set_state("abc\n«ˇ »abc\nabc");
7172 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7173 .unwrap();
7174 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7175
7176 // Test no matches with right-to-left selection
7177 cx.set_state("abc\n« ˇ»abc\nabc");
7178 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7179 .unwrap();
7180 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7181
7182 // Test with a single word and clip_at_line_ends=true (#29823)
7183 cx.set_state("aˇbc");
7184 cx.update_editor(|e, window, cx| {
7185 e.set_clip_at_line_ends(true, cx);
7186 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7187 e.set_clip_at_line_ends(false, cx);
7188 });
7189 cx.assert_editor_state("«abcˇ»");
7190}
7191
7192#[gpui::test]
7193async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7194 init_test(cx, |_| {});
7195
7196 let mut cx = EditorTestContext::new(cx).await;
7197
7198 let large_body_1 = "\nd".repeat(200);
7199 let large_body_2 = "\ne".repeat(200);
7200
7201 cx.set_state(&format!(
7202 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7203 ));
7204 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7205 let scroll_position = editor.scroll_position(cx);
7206 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7207 scroll_position
7208 });
7209
7210 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7211 .unwrap();
7212 cx.assert_editor_state(&format!(
7213 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7214 ));
7215 let scroll_position_after_selection =
7216 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7217 assert_eq!(
7218 initial_scroll_position, scroll_position_after_selection,
7219 "Scroll position should not change after selecting all matches"
7220 );
7221}
7222
7223#[gpui::test]
7224async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7225 init_test(cx, |_| {});
7226
7227 let mut cx = EditorLspTestContext::new_rust(
7228 lsp::ServerCapabilities {
7229 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7230 ..Default::default()
7231 },
7232 cx,
7233 )
7234 .await;
7235
7236 cx.set_state(indoc! {"
7237 line 1
7238 line 2
7239 linˇe 3
7240 line 4
7241 line 5
7242 "});
7243
7244 // Make an edit
7245 cx.update_editor(|editor, window, cx| {
7246 editor.handle_input("X", window, cx);
7247 });
7248
7249 // Move cursor to a different position
7250 cx.update_editor(|editor, window, cx| {
7251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7252 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7253 });
7254 });
7255
7256 cx.assert_editor_state(indoc! {"
7257 line 1
7258 line 2
7259 linXe 3
7260 line 4
7261 liˇne 5
7262 "});
7263
7264 cx.lsp
7265 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7266 Ok(Some(vec![lsp::TextEdit::new(
7267 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7268 "PREFIX ".to_string(),
7269 )]))
7270 });
7271
7272 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7273 .unwrap()
7274 .await
7275 .unwrap();
7276
7277 cx.assert_editor_state(indoc! {"
7278 PREFIX line 1
7279 line 2
7280 linXe 3
7281 line 4
7282 liˇne 5
7283 "});
7284
7285 // Undo formatting
7286 cx.update_editor(|editor, window, cx| {
7287 editor.undo(&Default::default(), window, cx);
7288 });
7289
7290 // Verify cursor moved back to position after edit
7291 cx.assert_editor_state(indoc! {"
7292 line 1
7293 line 2
7294 linXˇe 3
7295 line 4
7296 line 5
7297 "});
7298}
7299
7300#[gpui::test]
7301async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7302 init_test(cx, |_| {});
7303
7304 let mut cx = EditorTestContext::new(cx).await;
7305
7306 let provider = cx.new(|_| FakeEditPredictionProvider::default());
7307 cx.update_editor(|editor, window, cx| {
7308 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7309 });
7310
7311 cx.set_state(indoc! {"
7312 line 1
7313 line 2
7314 linˇe 3
7315 line 4
7316 line 5
7317 line 6
7318 line 7
7319 line 8
7320 line 9
7321 line 10
7322 "});
7323
7324 let snapshot = cx.buffer_snapshot();
7325 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7326
7327 cx.update(|_, cx| {
7328 provider.update(cx, |provider, _| {
7329 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
7330 id: None,
7331 edits: vec![(edit_position..edit_position, "X".into())],
7332 edit_preview: None,
7333 }))
7334 })
7335 });
7336
7337 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
7338 cx.update_editor(|editor, window, cx| {
7339 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7340 });
7341
7342 cx.assert_editor_state(indoc! {"
7343 line 1
7344 line 2
7345 lineXˇ 3
7346 line 4
7347 line 5
7348 line 6
7349 line 7
7350 line 8
7351 line 9
7352 line 10
7353 "});
7354
7355 cx.update_editor(|editor, window, cx| {
7356 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7357 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7358 });
7359 });
7360
7361 cx.assert_editor_state(indoc! {"
7362 line 1
7363 line 2
7364 lineX 3
7365 line 4
7366 line 5
7367 line 6
7368 line 7
7369 line 8
7370 line 9
7371 liˇne 10
7372 "});
7373
7374 cx.update_editor(|editor, window, cx| {
7375 editor.undo(&Default::default(), window, cx);
7376 });
7377
7378 cx.assert_editor_state(indoc! {"
7379 line 1
7380 line 2
7381 lineˇ 3
7382 line 4
7383 line 5
7384 line 6
7385 line 7
7386 line 8
7387 line 9
7388 line 10
7389 "});
7390}
7391
7392#[gpui::test]
7393async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7394 init_test(cx, |_| {});
7395
7396 let mut cx = EditorTestContext::new(cx).await;
7397 cx.set_state(
7398 r#"let foo = 2;
7399lˇet foo = 2;
7400let fooˇ = 2;
7401let foo = 2;
7402let foo = ˇ2;"#,
7403 );
7404
7405 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7406 .unwrap();
7407 cx.assert_editor_state(
7408 r#"let foo = 2;
7409«letˇ» foo = 2;
7410let «fooˇ» = 2;
7411let foo = 2;
7412let foo = «2ˇ»;"#,
7413 );
7414
7415 // noop for multiple selections with different contents
7416 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7417 .unwrap();
7418 cx.assert_editor_state(
7419 r#"let foo = 2;
7420«letˇ» foo = 2;
7421let «fooˇ» = 2;
7422let foo = 2;
7423let foo = «2ˇ»;"#,
7424 );
7425
7426 // Test last selection direction should be preserved
7427 cx.set_state(
7428 r#"let foo = 2;
7429let foo = 2;
7430let «fooˇ» = 2;
7431let «ˇfoo» = 2;
7432let foo = 2;"#,
7433 );
7434
7435 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7436 .unwrap();
7437 cx.assert_editor_state(
7438 r#"let foo = 2;
7439let foo = 2;
7440let «fooˇ» = 2;
7441let «ˇfoo» = 2;
7442let «ˇfoo» = 2;"#,
7443 );
7444}
7445
7446#[gpui::test]
7447async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7448 init_test(cx, |_| {});
7449
7450 let mut cx =
7451 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7452
7453 cx.assert_editor_state(indoc! {"
7454 ˇbbb
7455 ccc
7456
7457 bbb
7458 ccc
7459 "});
7460 cx.dispatch_action(SelectPrevious::default());
7461 cx.assert_editor_state(indoc! {"
7462 «bbbˇ»
7463 ccc
7464
7465 bbb
7466 ccc
7467 "});
7468 cx.dispatch_action(SelectPrevious::default());
7469 cx.assert_editor_state(indoc! {"
7470 «bbbˇ»
7471 ccc
7472
7473 «bbbˇ»
7474 ccc
7475 "});
7476}
7477
7478#[gpui::test]
7479async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7480 init_test(cx, |_| {});
7481
7482 let mut cx = EditorTestContext::new(cx).await;
7483 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7484
7485 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7486 .unwrap();
7487 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7488
7489 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7490 .unwrap();
7491 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7492
7493 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7494 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7495
7496 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7497 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7498
7499 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7500 .unwrap();
7501 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7502
7503 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7504 .unwrap();
7505 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7506}
7507
7508#[gpui::test]
7509async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7510 init_test(cx, |_| {});
7511
7512 let mut cx = EditorTestContext::new(cx).await;
7513 cx.set_state("aˇ");
7514
7515 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7516 .unwrap();
7517 cx.assert_editor_state("«aˇ»");
7518 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7519 .unwrap();
7520 cx.assert_editor_state("«aˇ»");
7521}
7522
7523#[gpui::test]
7524async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7525 init_test(cx, |_| {});
7526
7527 let mut cx = EditorTestContext::new(cx).await;
7528 cx.set_state(
7529 r#"let foo = 2;
7530lˇet foo = 2;
7531let fooˇ = 2;
7532let foo = 2;
7533let foo = ˇ2;"#,
7534 );
7535
7536 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7537 .unwrap();
7538 cx.assert_editor_state(
7539 r#"let foo = 2;
7540«letˇ» foo = 2;
7541let «fooˇ» = 2;
7542let foo = 2;
7543let foo = «2ˇ»;"#,
7544 );
7545
7546 // noop for multiple selections with different contents
7547 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7548 .unwrap();
7549 cx.assert_editor_state(
7550 r#"let foo = 2;
7551«letˇ» foo = 2;
7552let «fooˇ» = 2;
7553let foo = 2;
7554let foo = «2ˇ»;"#,
7555 );
7556}
7557
7558#[gpui::test]
7559async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7560 init_test(cx, |_| {});
7561
7562 let mut cx = EditorTestContext::new(cx).await;
7563 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7564
7565 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7566 .unwrap();
7567 // selection direction is preserved
7568 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7569
7570 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7571 .unwrap();
7572 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7573
7574 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7575 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7576
7577 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7578 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7579
7580 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7581 .unwrap();
7582 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7583
7584 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7585 .unwrap();
7586 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7587}
7588
7589#[gpui::test]
7590async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7591 init_test(cx, |_| {});
7592
7593 let language = Arc::new(Language::new(
7594 LanguageConfig::default(),
7595 Some(tree_sitter_rust::LANGUAGE.into()),
7596 ));
7597
7598 let text = r#"
7599 use mod1::mod2::{mod3, mod4};
7600
7601 fn fn_1(param1: bool, param2: &str) {
7602 let var1 = "text";
7603 }
7604 "#
7605 .unindent();
7606
7607 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7608 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7609 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7610
7611 editor
7612 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7613 .await;
7614
7615 editor.update_in(cx, |editor, window, cx| {
7616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7617 s.select_display_ranges([
7618 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7619 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7620 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7621 ]);
7622 });
7623 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7624 });
7625 editor.update(cx, |editor, cx| {
7626 assert_text_with_selections(
7627 editor,
7628 indoc! {r#"
7629 use mod1::mod2::{mod3, «mod4ˇ»};
7630
7631 fn fn_1«ˇ(param1: bool, param2: &str)» {
7632 let var1 = "«ˇtext»";
7633 }
7634 "#},
7635 cx,
7636 );
7637 });
7638
7639 editor.update_in(cx, |editor, window, cx| {
7640 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7641 });
7642 editor.update(cx, |editor, cx| {
7643 assert_text_with_selections(
7644 editor,
7645 indoc! {r#"
7646 use mod1::mod2::«{mod3, mod4}ˇ»;
7647
7648 «ˇfn fn_1(param1: bool, param2: &str) {
7649 let var1 = "text";
7650 }»
7651 "#},
7652 cx,
7653 );
7654 });
7655
7656 editor.update_in(cx, |editor, window, cx| {
7657 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7658 });
7659 assert_eq!(
7660 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7661 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7662 );
7663
7664 // Trying to expand the selected syntax node one more time has no effect.
7665 editor.update_in(cx, |editor, window, cx| {
7666 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7667 });
7668 assert_eq!(
7669 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7670 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7671 );
7672
7673 editor.update_in(cx, |editor, window, cx| {
7674 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7675 });
7676 editor.update(cx, |editor, cx| {
7677 assert_text_with_selections(
7678 editor,
7679 indoc! {r#"
7680 use mod1::mod2::«{mod3, mod4}ˇ»;
7681
7682 «ˇfn fn_1(param1: bool, param2: &str) {
7683 let var1 = "text";
7684 }»
7685 "#},
7686 cx,
7687 );
7688 });
7689
7690 editor.update_in(cx, |editor, window, cx| {
7691 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7692 });
7693 editor.update(cx, |editor, cx| {
7694 assert_text_with_selections(
7695 editor,
7696 indoc! {r#"
7697 use mod1::mod2::{mod3, «mod4ˇ»};
7698
7699 fn fn_1«ˇ(param1: bool, param2: &str)» {
7700 let var1 = "«ˇtext»";
7701 }
7702 "#},
7703 cx,
7704 );
7705 });
7706
7707 editor.update_in(cx, |editor, window, cx| {
7708 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7709 });
7710 editor.update(cx, |editor, cx| {
7711 assert_text_with_selections(
7712 editor,
7713 indoc! {r#"
7714 use mod1::mod2::{mod3, mo«ˇ»d4};
7715
7716 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7717 let var1 = "te«ˇ»xt";
7718 }
7719 "#},
7720 cx,
7721 );
7722 });
7723
7724 // Trying to shrink the selected syntax node one more time has no effect.
7725 editor.update_in(cx, |editor, window, cx| {
7726 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7727 });
7728 editor.update_in(cx, |editor, _, cx| {
7729 assert_text_with_selections(
7730 editor,
7731 indoc! {r#"
7732 use mod1::mod2::{mod3, mo«ˇ»d4};
7733
7734 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7735 let var1 = "te«ˇ»xt";
7736 }
7737 "#},
7738 cx,
7739 );
7740 });
7741
7742 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7743 // a fold.
7744 editor.update_in(cx, |editor, window, cx| {
7745 editor.fold_creases(
7746 vec![
7747 Crease::simple(
7748 Point::new(0, 21)..Point::new(0, 24),
7749 FoldPlaceholder::test(),
7750 ),
7751 Crease::simple(
7752 Point::new(3, 20)..Point::new(3, 22),
7753 FoldPlaceholder::test(),
7754 ),
7755 ],
7756 true,
7757 window,
7758 cx,
7759 );
7760 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7761 });
7762 editor.update(cx, |editor, cx| {
7763 assert_text_with_selections(
7764 editor,
7765 indoc! {r#"
7766 use mod1::mod2::«{mod3, mod4}ˇ»;
7767
7768 fn fn_1«ˇ(param1: bool, param2: &str)» {
7769 let var1 = "«ˇtext»";
7770 }
7771 "#},
7772 cx,
7773 );
7774 });
7775}
7776
7777#[gpui::test]
7778async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7779 init_test(cx, |_| {});
7780
7781 let language = Arc::new(Language::new(
7782 LanguageConfig::default(),
7783 Some(tree_sitter_rust::LANGUAGE.into()),
7784 ));
7785
7786 let text = "let a = 2;";
7787
7788 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7789 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7790 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7791
7792 editor
7793 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7794 .await;
7795
7796 // Test case 1: Cursor at end of word
7797 editor.update_in(cx, |editor, window, cx| {
7798 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7799 s.select_display_ranges([
7800 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7801 ]);
7802 });
7803 });
7804 editor.update(cx, |editor, cx| {
7805 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7806 });
7807 editor.update_in(cx, |editor, window, cx| {
7808 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7809 });
7810 editor.update(cx, |editor, cx| {
7811 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7812 });
7813 editor.update_in(cx, |editor, window, cx| {
7814 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7815 });
7816 editor.update(cx, |editor, cx| {
7817 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7818 });
7819
7820 // Test case 2: Cursor at end of statement
7821 editor.update_in(cx, |editor, window, cx| {
7822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7823 s.select_display_ranges([
7824 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7825 ]);
7826 });
7827 });
7828 editor.update(cx, |editor, cx| {
7829 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7830 });
7831 editor.update_in(cx, |editor, window, cx| {
7832 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7833 });
7834 editor.update(cx, |editor, cx| {
7835 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7836 });
7837}
7838
7839#[gpui::test]
7840async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7841 init_test(cx, |_| {});
7842
7843 let language = Arc::new(Language::new(
7844 LanguageConfig::default(),
7845 Some(tree_sitter_rust::LANGUAGE.into()),
7846 ));
7847
7848 let text = r#"
7849 use mod1::mod2::{mod3, mod4};
7850
7851 fn fn_1(param1: bool, param2: &str) {
7852 let var1 = "hello world";
7853 }
7854 "#
7855 .unindent();
7856
7857 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7858 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7859 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7860
7861 editor
7862 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7863 .await;
7864
7865 // Test 1: Cursor on a letter of a string word
7866 editor.update_in(cx, |editor, window, cx| {
7867 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7868 s.select_display_ranges([
7869 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7870 ]);
7871 });
7872 });
7873 editor.update_in(cx, |editor, window, cx| {
7874 assert_text_with_selections(
7875 editor,
7876 indoc! {r#"
7877 use mod1::mod2::{mod3, mod4};
7878
7879 fn fn_1(param1: bool, param2: &str) {
7880 let var1 = "hˇello world";
7881 }
7882 "#},
7883 cx,
7884 );
7885 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7886 assert_text_with_selections(
7887 editor,
7888 indoc! {r#"
7889 use mod1::mod2::{mod3, mod4};
7890
7891 fn fn_1(param1: bool, param2: &str) {
7892 let var1 = "«ˇhello» world";
7893 }
7894 "#},
7895 cx,
7896 );
7897 });
7898
7899 // Test 2: Partial selection within a word
7900 editor.update_in(cx, |editor, window, cx| {
7901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7902 s.select_display_ranges([
7903 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7904 ]);
7905 });
7906 });
7907 editor.update_in(cx, |editor, window, cx| {
7908 assert_text_with_selections(
7909 editor,
7910 indoc! {r#"
7911 use mod1::mod2::{mod3, mod4};
7912
7913 fn fn_1(param1: bool, param2: &str) {
7914 let var1 = "h«elˇ»lo world";
7915 }
7916 "#},
7917 cx,
7918 );
7919 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7920 assert_text_with_selections(
7921 editor,
7922 indoc! {r#"
7923 use mod1::mod2::{mod3, mod4};
7924
7925 fn fn_1(param1: bool, param2: &str) {
7926 let var1 = "«ˇhello» world";
7927 }
7928 "#},
7929 cx,
7930 );
7931 });
7932
7933 // Test 3: Complete word already selected
7934 editor.update_in(cx, |editor, window, cx| {
7935 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7936 s.select_display_ranges([
7937 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7938 ]);
7939 });
7940 });
7941 editor.update_in(cx, |editor, window, cx| {
7942 assert_text_with_selections(
7943 editor,
7944 indoc! {r#"
7945 use mod1::mod2::{mod3, mod4};
7946
7947 fn fn_1(param1: bool, param2: &str) {
7948 let var1 = "«helloˇ» world";
7949 }
7950 "#},
7951 cx,
7952 );
7953 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7954 assert_text_with_selections(
7955 editor,
7956 indoc! {r#"
7957 use mod1::mod2::{mod3, mod4};
7958
7959 fn fn_1(param1: bool, param2: &str) {
7960 let var1 = "«hello worldˇ»";
7961 }
7962 "#},
7963 cx,
7964 );
7965 });
7966
7967 // Test 4: Selection spanning across words
7968 editor.update_in(cx, |editor, window, cx| {
7969 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7970 s.select_display_ranges([
7971 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7972 ]);
7973 });
7974 });
7975 editor.update_in(cx, |editor, window, cx| {
7976 assert_text_with_selections(
7977 editor,
7978 indoc! {r#"
7979 use mod1::mod2::{mod3, mod4};
7980
7981 fn fn_1(param1: bool, param2: &str) {
7982 let var1 = "hel«lo woˇ»rld";
7983 }
7984 "#},
7985 cx,
7986 );
7987 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7988 assert_text_with_selections(
7989 editor,
7990 indoc! {r#"
7991 use mod1::mod2::{mod3, mod4};
7992
7993 fn fn_1(param1: bool, param2: &str) {
7994 let var1 = "«ˇhello world»";
7995 }
7996 "#},
7997 cx,
7998 );
7999 });
8000
8001 // Test 5: Expansion beyond string
8002 editor.update_in(cx, |editor, window, cx| {
8003 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8004 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8005 assert_text_with_selections(
8006 editor,
8007 indoc! {r#"
8008 use mod1::mod2::{mod3, mod4};
8009
8010 fn fn_1(param1: bool, param2: &str) {
8011 «ˇlet var1 = "hello world";»
8012 }
8013 "#},
8014 cx,
8015 );
8016 });
8017}
8018
8019#[gpui::test]
8020async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
8021 init_test(cx, |_| {});
8022
8023 let mut cx = EditorTestContext::new(cx).await;
8024
8025 let language = Arc::new(Language::new(
8026 LanguageConfig::default(),
8027 Some(tree_sitter_rust::LANGUAGE.into()),
8028 ));
8029
8030 cx.update_buffer(|buffer, cx| {
8031 buffer.set_language(Some(language), cx);
8032 });
8033
8034 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
8035 cx.update_editor(|editor, window, cx| {
8036 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8037 });
8038
8039 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
8040}
8041
8042#[gpui::test]
8043async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8044 init_test(cx, |_| {});
8045
8046 let base_text = r#"
8047 impl A {
8048 // this is an uncommitted comment
8049
8050 fn b() {
8051 c();
8052 }
8053
8054 // this is another uncommitted comment
8055
8056 fn d() {
8057 // e
8058 // f
8059 }
8060 }
8061
8062 fn g() {
8063 // h
8064 }
8065 "#
8066 .unindent();
8067
8068 let text = r#"
8069 ˇimpl A {
8070
8071 fn b() {
8072 c();
8073 }
8074
8075 fn d() {
8076 // e
8077 // f
8078 }
8079 }
8080
8081 fn g() {
8082 // h
8083 }
8084 "#
8085 .unindent();
8086
8087 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8088 cx.set_state(&text);
8089 cx.set_head_text(&base_text);
8090 cx.update_editor(|editor, window, cx| {
8091 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8092 });
8093
8094 cx.assert_state_with_diff(
8095 "
8096 ˇimpl A {
8097 - // this is an uncommitted comment
8098
8099 fn b() {
8100 c();
8101 }
8102
8103 - // this is another uncommitted comment
8104 -
8105 fn d() {
8106 // e
8107 // f
8108 }
8109 }
8110
8111 fn g() {
8112 // h
8113 }
8114 "
8115 .unindent(),
8116 );
8117
8118 let expected_display_text = "
8119 impl A {
8120 // this is an uncommitted comment
8121
8122 fn b() {
8123 ⋯
8124 }
8125
8126 // this is another uncommitted comment
8127
8128 fn d() {
8129 ⋯
8130 }
8131 }
8132
8133 fn g() {
8134 ⋯
8135 }
8136 "
8137 .unindent();
8138
8139 cx.update_editor(|editor, window, cx| {
8140 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8141 assert_eq!(editor.display_text(cx), expected_display_text);
8142 });
8143}
8144
8145#[gpui::test]
8146async fn test_autoindent(cx: &mut TestAppContext) {
8147 init_test(cx, |_| {});
8148
8149 let language = Arc::new(
8150 Language::new(
8151 LanguageConfig {
8152 brackets: BracketPairConfig {
8153 pairs: vec![
8154 BracketPair {
8155 start: "{".to_string(),
8156 end: "}".to_string(),
8157 close: false,
8158 surround: false,
8159 newline: true,
8160 },
8161 BracketPair {
8162 start: "(".to_string(),
8163 end: ")".to_string(),
8164 close: false,
8165 surround: false,
8166 newline: true,
8167 },
8168 ],
8169 ..Default::default()
8170 },
8171 ..Default::default()
8172 },
8173 Some(tree_sitter_rust::LANGUAGE.into()),
8174 )
8175 .with_indents_query(
8176 r#"
8177 (_ "(" ")" @end) @indent
8178 (_ "{" "}" @end) @indent
8179 "#,
8180 )
8181 .unwrap(),
8182 );
8183
8184 let text = "fn a() {}";
8185
8186 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8187 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8188 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8189 editor
8190 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8191 .await;
8192
8193 editor.update_in(cx, |editor, window, cx| {
8194 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8195 s.select_ranges([5..5, 8..8, 9..9])
8196 });
8197 editor.newline(&Newline, window, cx);
8198 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8199 assert_eq!(
8200 editor.selections.ranges(cx),
8201 &[
8202 Point::new(1, 4)..Point::new(1, 4),
8203 Point::new(3, 4)..Point::new(3, 4),
8204 Point::new(5, 0)..Point::new(5, 0)
8205 ]
8206 );
8207 });
8208}
8209
8210#[gpui::test]
8211async fn test_autoindent_disabled(cx: &mut TestAppContext) {
8212 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
8213
8214 let language = Arc::new(
8215 Language::new(
8216 LanguageConfig {
8217 brackets: BracketPairConfig {
8218 pairs: vec![
8219 BracketPair {
8220 start: "{".to_string(),
8221 end: "}".to_string(),
8222 close: false,
8223 surround: false,
8224 newline: true,
8225 },
8226 BracketPair {
8227 start: "(".to_string(),
8228 end: ")".to_string(),
8229 close: false,
8230 surround: false,
8231 newline: true,
8232 },
8233 ],
8234 ..Default::default()
8235 },
8236 ..Default::default()
8237 },
8238 Some(tree_sitter_rust::LANGUAGE.into()),
8239 )
8240 .with_indents_query(
8241 r#"
8242 (_ "(" ")" @end) @indent
8243 (_ "{" "}" @end) @indent
8244 "#,
8245 )
8246 .unwrap(),
8247 );
8248
8249 let text = "fn a() {}";
8250
8251 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8252 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8253 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8254 editor
8255 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8256 .await;
8257
8258 editor.update_in(cx, |editor, window, cx| {
8259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8260 s.select_ranges([5..5, 8..8, 9..9])
8261 });
8262 editor.newline(&Newline, window, cx);
8263 assert_eq!(
8264 editor.text(cx),
8265 indoc!(
8266 "
8267 fn a(
8268
8269 ) {
8270
8271 }
8272 "
8273 )
8274 );
8275 assert_eq!(
8276 editor.selections.ranges(cx),
8277 &[
8278 Point::new(1, 0)..Point::new(1, 0),
8279 Point::new(3, 0)..Point::new(3, 0),
8280 Point::new(5, 0)..Point::new(5, 0)
8281 ]
8282 );
8283 });
8284}
8285
8286#[gpui::test]
8287async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
8288 init_test(cx, |settings| {
8289 settings.defaults.auto_indent = Some(true);
8290 settings.languages.0.insert(
8291 "python".into(),
8292 LanguageSettingsContent {
8293 auto_indent: Some(false),
8294 ..Default::default()
8295 },
8296 );
8297 });
8298
8299 let mut cx = EditorTestContext::new(cx).await;
8300
8301 let injected_language = Arc::new(
8302 Language::new(
8303 LanguageConfig {
8304 brackets: BracketPairConfig {
8305 pairs: vec![
8306 BracketPair {
8307 start: "{".to_string(),
8308 end: "}".to_string(),
8309 close: false,
8310 surround: false,
8311 newline: true,
8312 },
8313 BracketPair {
8314 start: "(".to_string(),
8315 end: ")".to_string(),
8316 close: true,
8317 surround: false,
8318 newline: true,
8319 },
8320 ],
8321 ..Default::default()
8322 },
8323 name: "python".into(),
8324 ..Default::default()
8325 },
8326 Some(tree_sitter_python::LANGUAGE.into()),
8327 )
8328 .with_indents_query(
8329 r#"
8330 (_ "(" ")" @end) @indent
8331 (_ "{" "}" @end) @indent
8332 "#,
8333 )
8334 .unwrap(),
8335 );
8336
8337 let language = Arc::new(
8338 Language::new(
8339 LanguageConfig {
8340 brackets: BracketPairConfig {
8341 pairs: vec![
8342 BracketPair {
8343 start: "{".to_string(),
8344 end: "}".to_string(),
8345 close: false,
8346 surround: false,
8347 newline: true,
8348 },
8349 BracketPair {
8350 start: "(".to_string(),
8351 end: ")".to_string(),
8352 close: true,
8353 surround: false,
8354 newline: true,
8355 },
8356 ],
8357 ..Default::default()
8358 },
8359 name: LanguageName::new("rust"),
8360 ..Default::default()
8361 },
8362 Some(tree_sitter_rust::LANGUAGE.into()),
8363 )
8364 .with_indents_query(
8365 r#"
8366 (_ "(" ")" @end) @indent
8367 (_ "{" "}" @end) @indent
8368 "#,
8369 )
8370 .unwrap()
8371 .with_injection_query(
8372 r#"
8373 (macro_invocation
8374 macro: (identifier) @_macro_name
8375 (token_tree) @injection.content
8376 (#set! injection.language "python"))
8377 "#,
8378 )
8379 .unwrap(),
8380 );
8381
8382 cx.language_registry().add(injected_language);
8383 cx.language_registry().add(language.clone());
8384
8385 cx.update_buffer(|buffer, cx| {
8386 buffer.set_language(Some(language), cx);
8387 });
8388
8389 cx.set_state(r#"struct A {ˇ}"#);
8390
8391 cx.update_editor(|editor, window, cx| {
8392 editor.newline(&Default::default(), window, cx);
8393 });
8394
8395 cx.assert_editor_state(indoc!(
8396 "struct A {
8397 ˇ
8398 }"
8399 ));
8400
8401 cx.set_state(r#"select_biased!(ˇ)"#);
8402
8403 cx.update_editor(|editor, window, cx| {
8404 editor.newline(&Default::default(), window, cx);
8405 editor.handle_input("def ", window, cx);
8406 editor.handle_input("(", window, cx);
8407 editor.newline(&Default::default(), window, cx);
8408 editor.handle_input("a", window, cx);
8409 });
8410
8411 cx.assert_editor_state(indoc!(
8412 "select_biased!(
8413 def (
8414 aˇ
8415 )
8416 )"
8417 ));
8418}
8419
8420#[gpui::test]
8421async fn test_autoindent_selections(cx: &mut TestAppContext) {
8422 init_test(cx, |_| {});
8423
8424 {
8425 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8426 cx.set_state(indoc! {"
8427 impl A {
8428
8429 fn b() {}
8430
8431 «fn c() {
8432
8433 }ˇ»
8434 }
8435 "});
8436
8437 cx.update_editor(|editor, window, cx| {
8438 editor.autoindent(&Default::default(), window, cx);
8439 });
8440
8441 cx.assert_editor_state(indoc! {"
8442 impl A {
8443
8444 fn b() {}
8445
8446 «fn c() {
8447
8448 }ˇ»
8449 }
8450 "});
8451 }
8452
8453 {
8454 let mut cx = EditorTestContext::new_multibuffer(
8455 cx,
8456 [indoc! { "
8457 impl A {
8458 «
8459 // a
8460 fn b(){}
8461 »
8462 «
8463 }
8464 fn c(){}
8465 »
8466 "}],
8467 );
8468
8469 let buffer = cx.update_editor(|editor, _, cx| {
8470 let buffer = editor.buffer().update(cx, |buffer, _| {
8471 buffer.all_buffers().iter().next().unwrap().clone()
8472 });
8473 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8474 buffer
8475 });
8476
8477 cx.run_until_parked();
8478 cx.update_editor(|editor, window, cx| {
8479 editor.select_all(&Default::default(), window, cx);
8480 editor.autoindent(&Default::default(), window, cx)
8481 });
8482 cx.run_until_parked();
8483
8484 cx.update(|_, cx| {
8485 assert_eq!(
8486 buffer.read(cx).text(),
8487 indoc! { "
8488 impl A {
8489
8490 // a
8491 fn b(){}
8492
8493
8494 }
8495 fn c(){}
8496
8497 " }
8498 )
8499 });
8500 }
8501}
8502
8503#[gpui::test]
8504async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8505 init_test(cx, |_| {});
8506
8507 let mut cx = EditorTestContext::new(cx).await;
8508
8509 let language = Arc::new(Language::new(
8510 LanguageConfig {
8511 brackets: BracketPairConfig {
8512 pairs: vec![
8513 BracketPair {
8514 start: "{".to_string(),
8515 end: "}".to_string(),
8516 close: true,
8517 surround: true,
8518 newline: true,
8519 },
8520 BracketPair {
8521 start: "(".to_string(),
8522 end: ")".to_string(),
8523 close: true,
8524 surround: true,
8525 newline: true,
8526 },
8527 BracketPair {
8528 start: "/*".to_string(),
8529 end: " */".to_string(),
8530 close: true,
8531 surround: true,
8532 newline: true,
8533 },
8534 BracketPair {
8535 start: "[".to_string(),
8536 end: "]".to_string(),
8537 close: false,
8538 surround: false,
8539 newline: true,
8540 },
8541 BracketPair {
8542 start: "\"".to_string(),
8543 end: "\"".to_string(),
8544 close: true,
8545 surround: true,
8546 newline: false,
8547 },
8548 BracketPair {
8549 start: "<".to_string(),
8550 end: ">".to_string(),
8551 close: false,
8552 surround: true,
8553 newline: true,
8554 },
8555 ],
8556 ..Default::default()
8557 },
8558 autoclose_before: "})]".to_string(),
8559 ..Default::default()
8560 },
8561 Some(tree_sitter_rust::LANGUAGE.into()),
8562 ));
8563
8564 cx.language_registry().add(language.clone());
8565 cx.update_buffer(|buffer, cx| {
8566 buffer.set_language(Some(language), cx);
8567 });
8568
8569 cx.set_state(
8570 &r#"
8571 🏀ˇ
8572 εˇ
8573 ❤️ˇ
8574 "#
8575 .unindent(),
8576 );
8577
8578 // autoclose multiple nested brackets at multiple cursors
8579 cx.update_editor(|editor, window, cx| {
8580 editor.handle_input("{", window, cx);
8581 editor.handle_input("{", window, cx);
8582 editor.handle_input("{", window, cx);
8583 });
8584 cx.assert_editor_state(
8585 &"
8586 🏀{{{ˇ}}}
8587 ε{{{ˇ}}}
8588 ❤️{{{ˇ}}}
8589 "
8590 .unindent(),
8591 );
8592
8593 // insert a different closing bracket
8594 cx.update_editor(|editor, window, cx| {
8595 editor.handle_input(")", window, cx);
8596 });
8597 cx.assert_editor_state(
8598 &"
8599 🏀{{{)ˇ}}}
8600 ε{{{)ˇ}}}
8601 ❤️{{{)ˇ}}}
8602 "
8603 .unindent(),
8604 );
8605
8606 // skip over the auto-closed brackets when typing a closing bracket
8607 cx.update_editor(|editor, window, cx| {
8608 editor.move_right(&MoveRight, window, cx);
8609 editor.handle_input("}", window, cx);
8610 editor.handle_input("}", window, cx);
8611 editor.handle_input("}", window, cx);
8612 });
8613 cx.assert_editor_state(
8614 &"
8615 🏀{{{)}}}}ˇ
8616 ε{{{)}}}}ˇ
8617 ❤️{{{)}}}}ˇ
8618 "
8619 .unindent(),
8620 );
8621
8622 // autoclose multi-character pairs
8623 cx.set_state(
8624 &"
8625 ˇ
8626 ˇ
8627 "
8628 .unindent(),
8629 );
8630 cx.update_editor(|editor, window, cx| {
8631 editor.handle_input("/", window, cx);
8632 editor.handle_input("*", window, cx);
8633 });
8634 cx.assert_editor_state(
8635 &"
8636 /*ˇ */
8637 /*ˇ */
8638 "
8639 .unindent(),
8640 );
8641
8642 // one cursor autocloses a multi-character pair, one cursor
8643 // does not autoclose.
8644 cx.set_state(
8645 &"
8646 /ˇ
8647 ˇ
8648 "
8649 .unindent(),
8650 );
8651 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8652 cx.assert_editor_state(
8653 &"
8654 /*ˇ */
8655 *ˇ
8656 "
8657 .unindent(),
8658 );
8659
8660 // Don't autoclose if the next character isn't whitespace and isn't
8661 // listed in the language's "autoclose_before" section.
8662 cx.set_state("ˇa b");
8663 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8664 cx.assert_editor_state("{ˇa b");
8665
8666 // Don't autoclose if `close` is false for the bracket pair
8667 cx.set_state("ˇ");
8668 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8669 cx.assert_editor_state("[ˇ");
8670
8671 // Surround with brackets if text is selected
8672 cx.set_state("«aˇ» b");
8673 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8674 cx.assert_editor_state("{«aˇ»} b");
8675
8676 // Autoclose when not immediately after a word character
8677 cx.set_state("a ˇ");
8678 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8679 cx.assert_editor_state("a \"ˇ\"");
8680
8681 // Autoclose pair where the start and end characters are the same
8682 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8683 cx.assert_editor_state("a \"\"ˇ");
8684
8685 // Don't autoclose when immediately after a word character
8686 cx.set_state("aˇ");
8687 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8688 cx.assert_editor_state("a\"ˇ");
8689
8690 // Do autoclose when after a non-word character
8691 cx.set_state("{ˇ");
8692 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8693 cx.assert_editor_state("{\"ˇ\"");
8694
8695 // Non identical pairs autoclose regardless of preceding character
8696 cx.set_state("aˇ");
8697 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8698 cx.assert_editor_state("a{ˇ}");
8699
8700 // Don't autoclose pair if autoclose is disabled
8701 cx.set_state("ˇ");
8702 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8703 cx.assert_editor_state("<ˇ");
8704
8705 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8706 cx.set_state("«aˇ» b");
8707 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8708 cx.assert_editor_state("<«aˇ»> b");
8709}
8710
8711#[gpui::test]
8712async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8713 init_test(cx, |settings| {
8714 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8715 });
8716
8717 let mut cx = EditorTestContext::new(cx).await;
8718
8719 let language = Arc::new(Language::new(
8720 LanguageConfig {
8721 brackets: BracketPairConfig {
8722 pairs: vec![
8723 BracketPair {
8724 start: "{".to_string(),
8725 end: "}".to_string(),
8726 close: true,
8727 surround: true,
8728 newline: true,
8729 },
8730 BracketPair {
8731 start: "(".to_string(),
8732 end: ")".to_string(),
8733 close: true,
8734 surround: true,
8735 newline: true,
8736 },
8737 BracketPair {
8738 start: "[".to_string(),
8739 end: "]".to_string(),
8740 close: false,
8741 surround: false,
8742 newline: true,
8743 },
8744 ],
8745 ..Default::default()
8746 },
8747 autoclose_before: "})]".to_string(),
8748 ..Default::default()
8749 },
8750 Some(tree_sitter_rust::LANGUAGE.into()),
8751 ));
8752
8753 cx.language_registry().add(language.clone());
8754 cx.update_buffer(|buffer, cx| {
8755 buffer.set_language(Some(language), cx);
8756 });
8757
8758 cx.set_state(
8759 &"
8760 ˇ
8761 ˇ
8762 ˇ
8763 "
8764 .unindent(),
8765 );
8766
8767 // ensure only matching closing brackets are skipped over
8768 cx.update_editor(|editor, window, cx| {
8769 editor.handle_input("}", window, cx);
8770 editor.move_left(&MoveLeft, window, cx);
8771 editor.handle_input(")", window, cx);
8772 editor.move_left(&MoveLeft, window, cx);
8773 });
8774 cx.assert_editor_state(
8775 &"
8776 ˇ)}
8777 ˇ)}
8778 ˇ)}
8779 "
8780 .unindent(),
8781 );
8782
8783 // skip-over closing brackets at multiple cursors
8784 cx.update_editor(|editor, window, cx| {
8785 editor.handle_input(")", window, cx);
8786 editor.handle_input("}", window, cx);
8787 });
8788 cx.assert_editor_state(
8789 &"
8790 )}ˇ
8791 )}ˇ
8792 )}ˇ
8793 "
8794 .unindent(),
8795 );
8796
8797 // ignore non-close brackets
8798 cx.update_editor(|editor, window, cx| {
8799 editor.handle_input("]", window, cx);
8800 editor.move_left(&MoveLeft, window, cx);
8801 editor.handle_input("]", window, cx);
8802 });
8803 cx.assert_editor_state(
8804 &"
8805 )}]ˇ]
8806 )}]ˇ]
8807 )}]ˇ]
8808 "
8809 .unindent(),
8810 );
8811}
8812
8813#[gpui::test]
8814async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8815 init_test(cx, |_| {});
8816
8817 let mut cx = EditorTestContext::new(cx).await;
8818
8819 let html_language = Arc::new(
8820 Language::new(
8821 LanguageConfig {
8822 name: "HTML".into(),
8823 brackets: BracketPairConfig {
8824 pairs: vec![
8825 BracketPair {
8826 start: "<".into(),
8827 end: ">".into(),
8828 close: true,
8829 ..Default::default()
8830 },
8831 BracketPair {
8832 start: "{".into(),
8833 end: "}".into(),
8834 close: true,
8835 ..Default::default()
8836 },
8837 BracketPair {
8838 start: "(".into(),
8839 end: ")".into(),
8840 close: true,
8841 ..Default::default()
8842 },
8843 ],
8844 ..Default::default()
8845 },
8846 autoclose_before: "})]>".into(),
8847 ..Default::default()
8848 },
8849 Some(tree_sitter_html::LANGUAGE.into()),
8850 )
8851 .with_injection_query(
8852 r#"
8853 (script_element
8854 (raw_text) @injection.content
8855 (#set! injection.language "javascript"))
8856 "#,
8857 )
8858 .unwrap(),
8859 );
8860
8861 let javascript_language = Arc::new(Language::new(
8862 LanguageConfig {
8863 name: "JavaScript".into(),
8864 brackets: BracketPairConfig {
8865 pairs: vec![
8866 BracketPair {
8867 start: "/*".into(),
8868 end: " */".into(),
8869 close: true,
8870 ..Default::default()
8871 },
8872 BracketPair {
8873 start: "{".into(),
8874 end: "}".into(),
8875 close: true,
8876 ..Default::default()
8877 },
8878 BracketPair {
8879 start: "(".into(),
8880 end: ")".into(),
8881 close: true,
8882 ..Default::default()
8883 },
8884 ],
8885 ..Default::default()
8886 },
8887 autoclose_before: "})]>".into(),
8888 ..Default::default()
8889 },
8890 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8891 ));
8892
8893 cx.language_registry().add(html_language.clone());
8894 cx.language_registry().add(javascript_language);
8895 cx.executor().run_until_parked();
8896
8897 cx.update_buffer(|buffer, cx| {
8898 buffer.set_language(Some(html_language), cx);
8899 });
8900
8901 cx.set_state(
8902 &r#"
8903 <body>ˇ
8904 <script>
8905 var x = 1;ˇ
8906 </script>
8907 </body>ˇ
8908 "#
8909 .unindent(),
8910 );
8911
8912 // Precondition: different languages are active at different locations.
8913 cx.update_editor(|editor, window, cx| {
8914 let snapshot = editor.snapshot(window, cx);
8915 let cursors = editor.selections.ranges::<usize>(cx);
8916 let languages = cursors
8917 .iter()
8918 .map(|c| snapshot.language_at(c.start).unwrap().name())
8919 .collect::<Vec<_>>();
8920 assert_eq!(
8921 languages,
8922 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8923 );
8924 });
8925
8926 // Angle brackets autoclose in HTML, but not JavaScript.
8927 cx.update_editor(|editor, window, cx| {
8928 editor.handle_input("<", window, cx);
8929 editor.handle_input("a", window, cx);
8930 });
8931 cx.assert_editor_state(
8932 &r#"
8933 <body><aˇ>
8934 <script>
8935 var x = 1;<aˇ
8936 </script>
8937 </body><aˇ>
8938 "#
8939 .unindent(),
8940 );
8941
8942 // Curly braces and parens autoclose in both HTML and JavaScript.
8943 cx.update_editor(|editor, window, cx| {
8944 editor.handle_input(" b=", window, cx);
8945 editor.handle_input("{", window, cx);
8946 editor.handle_input("c", window, cx);
8947 editor.handle_input("(", window, cx);
8948 });
8949 cx.assert_editor_state(
8950 &r#"
8951 <body><a b={c(ˇ)}>
8952 <script>
8953 var x = 1;<a b={c(ˇ)}
8954 </script>
8955 </body><a b={c(ˇ)}>
8956 "#
8957 .unindent(),
8958 );
8959
8960 // Brackets that were already autoclosed are skipped.
8961 cx.update_editor(|editor, window, cx| {
8962 editor.handle_input(")", window, cx);
8963 editor.handle_input("d", window, cx);
8964 editor.handle_input("}", window, cx);
8965 });
8966 cx.assert_editor_state(
8967 &r#"
8968 <body><a b={c()d}ˇ>
8969 <script>
8970 var x = 1;<a b={c()d}ˇ
8971 </script>
8972 </body><a b={c()d}ˇ>
8973 "#
8974 .unindent(),
8975 );
8976 cx.update_editor(|editor, window, cx| {
8977 editor.handle_input(">", window, cx);
8978 });
8979 cx.assert_editor_state(
8980 &r#"
8981 <body><a b={c()d}>ˇ
8982 <script>
8983 var x = 1;<a b={c()d}>ˇ
8984 </script>
8985 </body><a b={c()d}>ˇ
8986 "#
8987 .unindent(),
8988 );
8989
8990 // Reset
8991 cx.set_state(
8992 &r#"
8993 <body>ˇ
8994 <script>
8995 var x = 1;ˇ
8996 </script>
8997 </body>ˇ
8998 "#
8999 .unindent(),
9000 );
9001
9002 cx.update_editor(|editor, window, cx| {
9003 editor.handle_input("<", window, cx);
9004 });
9005 cx.assert_editor_state(
9006 &r#"
9007 <body><ˇ>
9008 <script>
9009 var x = 1;<ˇ
9010 </script>
9011 </body><ˇ>
9012 "#
9013 .unindent(),
9014 );
9015
9016 // When backspacing, the closing angle brackets are removed.
9017 cx.update_editor(|editor, window, cx| {
9018 editor.backspace(&Backspace, window, cx);
9019 });
9020 cx.assert_editor_state(
9021 &r#"
9022 <body>ˇ
9023 <script>
9024 var x = 1;ˇ
9025 </script>
9026 </body>ˇ
9027 "#
9028 .unindent(),
9029 );
9030
9031 // Block comments autoclose in JavaScript, but not HTML.
9032 cx.update_editor(|editor, window, cx| {
9033 editor.handle_input("/", window, cx);
9034 editor.handle_input("*", window, cx);
9035 });
9036 cx.assert_editor_state(
9037 &r#"
9038 <body>/*ˇ
9039 <script>
9040 var x = 1;/*ˇ */
9041 </script>
9042 </body>/*ˇ
9043 "#
9044 .unindent(),
9045 );
9046}
9047
9048#[gpui::test]
9049async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9050 init_test(cx, |_| {});
9051
9052 let mut cx = EditorTestContext::new(cx).await;
9053
9054 let rust_language = Arc::new(
9055 Language::new(
9056 LanguageConfig {
9057 name: "Rust".into(),
9058 brackets: serde_json::from_value(json!([
9059 { "start": "{", "end": "}", "close": true, "newline": true },
9060 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9061 ]))
9062 .unwrap(),
9063 autoclose_before: "})]>".into(),
9064 ..Default::default()
9065 },
9066 Some(tree_sitter_rust::LANGUAGE.into()),
9067 )
9068 .with_override_query("(string_literal) @string")
9069 .unwrap(),
9070 );
9071
9072 cx.language_registry().add(rust_language.clone());
9073 cx.update_buffer(|buffer, cx| {
9074 buffer.set_language(Some(rust_language), cx);
9075 });
9076
9077 cx.set_state(
9078 &r#"
9079 let x = ˇ
9080 "#
9081 .unindent(),
9082 );
9083
9084 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9085 cx.update_editor(|editor, window, cx| {
9086 editor.handle_input("\"", window, cx);
9087 });
9088 cx.assert_editor_state(
9089 &r#"
9090 let x = "ˇ"
9091 "#
9092 .unindent(),
9093 );
9094
9095 // Inserting another quotation mark. The cursor moves across the existing
9096 // automatically-inserted quotation mark.
9097 cx.update_editor(|editor, window, cx| {
9098 editor.handle_input("\"", window, cx);
9099 });
9100 cx.assert_editor_state(
9101 &r#"
9102 let x = ""ˇ
9103 "#
9104 .unindent(),
9105 );
9106
9107 // Reset
9108 cx.set_state(
9109 &r#"
9110 let x = ˇ
9111 "#
9112 .unindent(),
9113 );
9114
9115 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9116 cx.update_editor(|editor, window, cx| {
9117 editor.handle_input("\"", window, cx);
9118 editor.handle_input(" ", window, cx);
9119 editor.move_left(&Default::default(), window, cx);
9120 editor.handle_input("\\", window, cx);
9121 editor.handle_input("\"", window, cx);
9122 });
9123 cx.assert_editor_state(
9124 &r#"
9125 let x = "\"ˇ "
9126 "#
9127 .unindent(),
9128 );
9129
9130 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
9131 // mark. Nothing is inserted.
9132 cx.update_editor(|editor, window, cx| {
9133 editor.move_right(&Default::default(), window, cx);
9134 editor.handle_input("\"", window, cx);
9135 });
9136 cx.assert_editor_state(
9137 &r#"
9138 let x = "\" "ˇ
9139 "#
9140 .unindent(),
9141 );
9142}
9143
9144#[gpui::test]
9145async fn test_surround_with_pair(cx: &mut TestAppContext) {
9146 init_test(cx, |_| {});
9147
9148 let language = Arc::new(Language::new(
9149 LanguageConfig {
9150 brackets: BracketPairConfig {
9151 pairs: vec![
9152 BracketPair {
9153 start: "{".to_string(),
9154 end: "}".to_string(),
9155 close: true,
9156 surround: true,
9157 newline: true,
9158 },
9159 BracketPair {
9160 start: "/* ".to_string(),
9161 end: "*/".to_string(),
9162 close: true,
9163 surround: true,
9164 ..Default::default()
9165 },
9166 ],
9167 ..Default::default()
9168 },
9169 ..Default::default()
9170 },
9171 Some(tree_sitter_rust::LANGUAGE.into()),
9172 ));
9173
9174 let text = r#"
9175 a
9176 b
9177 c
9178 "#
9179 .unindent();
9180
9181 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9182 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9183 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9184 editor
9185 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9186 .await;
9187
9188 editor.update_in(cx, |editor, window, cx| {
9189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9190 s.select_display_ranges([
9191 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9192 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9193 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
9194 ])
9195 });
9196
9197 editor.handle_input("{", window, cx);
9198 editor.handle_input("{", window, cx);
9199 editor.handle_input("{", window, cx);
9200 assert_eq!(
9201 editor.text(cx),
9202 "
9203 {{{a}}}
9204 {{{b}}}
9205 {{{c}}}
9206 "
9207 .unindent()
9208 );
9209 assert_eq!(
9210 editor.selections.display_ranges(cx),
9211 [
9212 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
9213 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
9214 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
9215 ]
9216 );
9217
9218 editor.undo(&Undo, window, cx);
9219 editor.undo(&Undo, window, cx);
9220 editor.undo(&Undo, window, cx);
9221 assert_eq!(
9222 editor.text(cx),
9223 "
9224 a
9225 b
9226 c
9227 "
9228 .unindent()
9229 );
9230 assert_eq!(
9231 editor.selections.display_ranges(cx),
9232 [
9233 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9234 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9235 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9236 ]
9237 );
9238
9239 // Ensure inserting the first character of a multi-byte bracket pair
9240 // doesn't surround the selections with the bracket.
9241 editor.handle_input("/", window, cx);
9242 assert_eq!(
9243 editor.text(cx),
9244 "
9245 /
9246 /
9247 /
9248 "
9249 .unindent()
9250 );
9251 assert_eq!(
9252 editor.selections.display_ranges(cx),
9253 [
9254 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9255 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9256 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9257 ]
9258 );
9259
9260 editor.undo(&Undo, window, cx);
9261 assert_eq!(
9262 editor.text(cx),
9263 "
9264 a
9265 b
9266 c
9267 "
9268 .unindent()
9269 );
9270 assert_eq!(
9271 editor.selections.display_ranges(cx),
9272 [
9273 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9274 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9275 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9276 ]
9277 );
9278
9279 // Ensure inserting the last character of a multi-byte bracket pair
9280 // doesn't surround the selections with the bracket.
9281 editor.handle_input("*", window, cx);
9282 assert_eq!(
9283 editor.text(cx),
9284 "
9285 *
9286 *
9287 *
9288 "
9289 .unindent()
9290 );
9291 assert_eq!(
9292 editor.selections.display_ranges(cx),
9293 [
9294 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9295 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9296 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9297 ]
9298 );
9299 });
9300}
9301
9302#[gpui::test]
9303async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9304 init_test(cx, |_| {});
9305
9306 let language = Arc::new(Language::new(
9307 LanguageConfig {
9308 brackets: BracketPairConfig {
9309 pairs: vec![BracketPair {
9310 start: "{".to_string(),
9311 end: "}".to_string(),
9312 close: true,
9313 surround: true,
9314 newline: true,
9315 }],
9316 ..Default::default()
9317 },
9318 autoclose_before: "}".to_string(),
9319 ..Default::default()
9320 },
9321 Some(tree_sitter_rust::LANGUAGE.into()),
9322 ));
9323
9324 let text = r#"
9325 a
9326 b
9327 c
9328 "#
9329 .unindent();
9330
9331 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9332 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9333 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9334 editor
9335 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9336 .await;
9337
9338 editor.update_in(cx, |editor, window, cx| {
9339 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9340 s.select_ranges([
9341 Point::new(0, 1)..Point::new(0, 1),
9342 Point::new(1, 1)..Point::new(1, 1),
9343 Point::new(2, 1)..Point::new(2, 1),
9344 ])
9345 });
9346
9347 editor.handle_input("{", window, cx);
9348 editor.handle_input("{", window, cx);
9349 editor.handle_input("_", window, cx);
9350 assert_eq!(
9351 editor.text(cx),
9352 "
9353 a{{_}}
9354 b{{_}}
9355 c{{_}}
9356 "
9357 .unindent()
9358 );
9359 assert_eq!(
9360 editor.selections.ranges::<Point>(cx),
9361 [
9362 Point::new(0, 4)..Point::new(0, 4),
9363 Point::new(1, 4)..Point::new(1, 4),
9364 Point::new(2, 4)..Point::new(2, 4)
9365 ]
9366 );
9367
9368 editor.backspace(&Default::default(), window, cx);
9369 editor.backspace(&Default::default(), window, cx);
9370 assert_eq!(
9371 editor.text(cx),
9372 "
9373 a{}
9374 b{}
9375 c{}
9376 "
9377 .unindent()
9378 );
9379 assert_eq!(
9380 editor.selections.ranges::<Point>(cx),
9381 [
9382 Point::new(0, 2)..Point::new(0, 2),
9383 Point::new(1, 2)..Point::new(1, 2),
9384 Point::new(2, 2)..Point::new(2, 2)
9385 ]
9386 );
9387
9388 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9389 assert_eq!(
9390 editor.text(cx),
9391 "
9392 a
9393 b
9394 c
9395 "
9396 .unindent()
9397 );
9398 assert_eq!(
9399 editor.selections.ranges::<Point>(cx),
9400 [
9401 Point::new(0, 1)..Point::new(0, 1),
9402 Point::new(1, 1)..Point::new(1, 1),
9403 Point::new(2, 1)..Point::new(2, 1)
9404 ]
9405 );
9406 });
9407}
9408
9409#[gpui::test]
9410async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9411 init_test(cx, |settings| {
9412 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9413 });
9414
9415 let mut cx = EditorTestContext::new(cx).await;
9416
9417 let language = Arc::new(Language::new(
9418 LanguageConfig {
9419 brackets: BracketPairConfig {
9420 pairs: vec![
9421 BracketPair {
9422 start: "{".to_string(),
9423 end: "}".to_string(),
9424 close: true,
9425 surround: true,
9426 newline: true,
9427 },
9428 BracketPair {
9429 start: "(".to_string(),
9430 end: ")".to_string(),
9431 close: true,
9432 surround: true,
9433 newline: true,
9434 },
9435 BracketPair {
9436 start: "[".to_string(),
9437 end: "]".to_string(),
9438 close: false,
9439 surround: true,
9440 newline: true,
9441 },
9442 ],
9443 ..Default::default()
9444 },
9445 autoclose_before: "})]".to_string(),
9446 ..Default::default()
9447 },
9448 Some(tree_sitter_rust::LANGUAGE.into()),
9449 ));
9450
9451 cx.language_registry().add(language.clone());
9452 cx.update_buffer(|buffer, cx| {
9453 buffer.set_language(Some(language), cx);
9454 });
9455
9456 cx.set_state(
9457 &"
9458 {(ˇ)}
9459 [[ˇ]]
9460 {(ˇ)}
9461 "
9462 .unindent(),
9463 );
9464
9465 cx.update_editor(|editor, window, cx| {
9466 editor.backspace(&Default::default(), window, cx);
9467 editor.backspace(&Default::default(), window, cx);
9468 });
9469
9470 cx.assert_editor_state(
9471 &"
9472 ˇ
9473 ˇ]]
9474 ˇ
9475 "
9476 .unindent(),
9477 );
9478
9479 cx.update_editor(|editor, window, cx| {
9480 editor.handle_input("{", window, cx);
9481 editor.handle_input("{", window, cx);
9482 editor.move_right(&MoveRight, window, cx);
9483 editor.move_right(&MoveRight, window, cx);
9484 editor.move_left(&MoveLeft, window, cx);
9485 editor.move_left(&MoveLeft, window, cx);
9486 editor.backspace(&Default::default(), window, cx);
9487 });
9488
9489 cx.assert_editor_state(
9490 &"
9491 {ˇ}
9492 {ˇ}]]
9493 {ˇ}
9494 "
9495 .unindent(),
9496 );
9497
9498 cx.update_editor(|editor, window, cx| {
9499 editor.backspace(&Default::default(), window, cx);
9500 });
9501
9502 cx.assert_editor_state(
9503 &"
9504 ˇ
9505 ˇ]]
9506 ˇ
9507 "
9508 .unindent(),
9509 );
9510}
9511
9512#[gpui::test]
9513async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9514 init_test(cx, |_| {});
9515
9516 let language = Arc::new(Language::new(
9517 LanguageConfig::default(),
9518 Some(tree_sitter_rust::LANGUAGE.into()),
9519 ));
9520
9521 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9522 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9523 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9524 editor
9525 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9526 .await;
9527
9528 editor.update_in(cx, |editor, window, cx| {
9529 editor.set_auto_replace_emoji_shortcode(true);
9530
9531 editor.handle_input("Hello ", window, cx);
9532 editor.handle_input(":wave", window, cx);
9533 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9534
9535 editor.handle_input(":", window, cx);
9536 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9537
9538 editor.handle_input(" :smile", window, cx);
9539 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9540
9541 editor.handle_input(":", window, cx);
9542 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9543
9544 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9545 editor.handle_input(":wave", window, cx);
9546 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9547
9548 editor.handle_input(":", window, cx);
9549 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9550
9551 editor.handle_input(":1", window, cx);
9552 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9553
9554 editor.handle_input(":", window, cx);
9555 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9556
9557 // Ensure shortcode does not get replaced when it is part of a word
9558 editor.handle_input(" Test:wave", window, cx);
9559 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9560
9561 editor.handle_input(":", window, cx);
9562 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9563
9564 editor.set_auto_replace_emoji_shortcode(false);
9565
9566 // Ensure shortcode does not get replaced when auto replace is off
9567 editor.handle_input(" :wave", window, cx);
9568 assert_eq!(
9569 editor.text(cx),
9570 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9571 );
9572
9573 editor.handle_input(":", window, cx);
9574 assert_eq!(
9575 editor.text(cx),
9576 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9577 );
9578 });
9579}
9580
9581#[gpui::test]
9582async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9583 init_test(cx, |_| {});
9584
9585 let (text, insertion_ranges) = marked_text_ranges(
9586 indoc! {"
9587 ˇ
9588 "},
9589 false,
9590 );
9591
9592 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9593 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9594
9595 _ = editor.update_in(cx, |editor, window, cx| {
9596 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9597
9598 editor
9599 .insert_snippet(&insertion_ranges, snippet, window, cx)
9600 .unwrap();
9601
9602 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9603 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9604 assert_eq!(editor.text(cx), expected_text);
9605 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9606 }
9607
9608 assert(
9609 editor,
9610 cx,
9611 indoc! {"
9612 type «» =•
9613 "},
9614 );
9615
9616 assert!(editor.context_menu_visible(), "There should be a matches");
9617 });
9618}
9619
9620#[gpui::test]
9621async fn test_snippets(cx: &mut TestAppContext) {
9622 init_test(cx, |_| {});
9623
9624 let mut cx = EditorTestContext::new(cx).await;
9625
9626 cx.set_state(indoc! {"
9627 a.ˇ b
9628 a.ˇ b
9629 a.ˇ b
9630 "});
9631
9632 cx.update_editor(|editor, window, cx| {
9633 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9634 let insertion_ranges = editor
9635 .selections
9636 .all(cx)
9637 .iter()
9638 .map(|s| s.range())
9639 .collect::<Vec<_>>();
9640 editor
9641 .insert_snippet(&insertion_ranges, snippet, window, cx)
9642 .unwrap();
9643 });
9644
9645 cx.assert_editor_state(indoc! {"
9646 a.f(«oneˇ», two, «threeˇ») b
9647 a.f(«oneˇ», two, «threeˇ») b
9648 a.f(«oneˇ», two, «threeˇ») b
9649 "});
9650
9651 // Can't move earlier than the first tab stop
9652 cx.update_editor(|editor, window, cx| {
9653 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9654 });
9655 cx.assert_editor_state(indoc! {"
9656 a.f(«oneˇ», two, «threeˇ») b
9657 a.f(«oneˇ», two, «threeˇ») b
9658 a.f(«oneˇ», two, «threeˇ») b
9659 "});
9660
9661 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9662 cx.assert_editor_state(indoc! {"
9663 a.f(one, «twoˇ», three) b
9664 a.f(one, «twoˇ», three) b
9665 a.f(one, «twoˇ», three) b
9666 "});
9667
9668 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9669 cx.assert_editor_state(indoc! {"
9670 a.f(«oneˇ», two, «threeˇ») b
9671 a.f(«oneˇ», two, «threeˇ») b
9672 a.f(«oneˇ», two, «threeˇ») b
9673 "});
9674
9675 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9676 cx.assert_editor_state(indoc! {"
9677 a.f(one, «twoˇ», three) b
9678 a.f(one, «twoˇ», three) b
9679 a.f(one, «twoˇ», three) b
9680 "});
9681 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9682 cx.assert_editor_state(indoc! {"
9683 a.f(one, two, three)ˇ b
9684 a.f(one, two, three)ˇ b
9685 a.f(one, two, three)ˇ b
9686 "});
9687
9688 // As soon as the last tab stop is reached, snippet state is gone
9689 cx.update_editor(|editor, window, cx| {
9690 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9691 });
9692 cx.assert_editor_state(indoc! {"
9693 a.f(one, two, three)ˇ b
9694 a.f(one, two, three)ˇ b
9695 a.f(one, two, three)ˇ b
9696 "});
9697}
9698
9699#[gpui::test]
9700async fn test_snippet_indentation(cx: &mut TestAppContext) {
9701 init_test(cx, |_| {});
9702
9703 let mut cx = EditorTestContext::new(cx).await;
9704
9705 cx.update_editor(|editor, window, cx| {
9706 let snippet = Snippet::parse(indoc! {"
9707 /*
9708 * Multiline comment with leading indentation
9709 *
9710 * $1
9711 */
9712 $0"})
9713 .unwrap();
9714 let insertion_ranges = editor
9715 .selections
9716 .all(cx)
9717 .iter()
9718 .map(|s| s.range())
9719 .collect::<Vec<_>>();
9720 editor
9721 .insert_snippet(&insertion_ranges, snippet, window, cx)
9722 .unwrap();
9723 });
9724
9725 cx.assert_editor_state(indoc! {"
9726 /*
9727 * Multiline comment with leading indentation
9728 *
9729 * ˇ
9730 */
9731 "});
9732
9733 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9734 cx.assert_editor_state(indoc! {"
9735 /*
9736 * Multiline comment with leading indentation
9737 *
9738 *•
9739 */
9740 ˇ"});
9741}
9742
9743#[gpui::test]
9744async fn test_document_format_during_save(cx: &mut TestAppContext) {
9745 init_test(cx, |_| {});
9746
9747 let fs = FakeFs::new(cx.executor());
9748 fs.insert_file(path!("/file.rs"), Default::default()).await;
9749
9750 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9751
9752 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9753 language_registry.add(rust_lang());
9754 let mut fake_servers = language_registry.register_fake_lsp(
9755 "Rust",
9756 FakeLspAdapter {
9757 capabilities: lsp::ServerCapabilities {
9758 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9759 ..Default::default()
9760 },
9761 ..Default::default()
9762 },
9763 );
9764
9765 let buffer = project
9766 .update(cx, |project, cx| {
9767 project.open_local_buffer(path!("/file.rs"), cx)
9768 })
9769 .await
9770 .unwrap();
9771
9772 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9773 let (editor, cx) = cx.add_window_view(|window, cx| {
9774 build_editor_with_project(project.clone(), buffer, window, cx)
9775 });
9776 editor.update_in(cx, |editor, window, cx| {
9777 editor.set_text("one\ntwo\nthree\n", window, cx)
9778 });
9779 assert!(cx.read(|cx| editor.is_dirty(cx)));
9780
9781 cx.executor().start_waiting();
9782 let fake_server = fake_servers.next().await.unwrap();
9783
9784 {
9785 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9786 move |params, _| async move {
9787 assert_eq!(
9788 params.text_document.uri,
9789 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9790 );
9791 assert_eq!(params.options.tab_size, 4);
9792 Ok(Some(vec![lsp::TextEdit::new(
9793 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9794 ", ".to_string(),
9795 )]))
9796 },
9797 );
9798 let save = editor
9799 .update_in(cx, |editor, window, cx| {
9800 editor.save(
9801 SaveOptions {
9802 format: true,
9803 autosave: false,
9804 },
9805 project.clone(),
9806 window,
9807 cx,
9808 )
9809 })
9810 .unwrap();
9811 cx.executor().start_waiting();
9812 save.await;
9813
9814 assert_eq!(
9815 editor.update(cx, |editor, cx| editor.text(cx)),
9816 "one, two\nthree\n"
9817 );
9818 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9819 }
9820
9821 {
9822 editor.update_in(cx, |editor, window, cx| {
9823 editor.set_text("one\ntwo\nthree\n", window, cx)
9824 });
9825 assert!(cx.read(|cx| editor.is_dirty(cx)));
9826
9827 // Ensure we can still save even if formatting hangs.
9828 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9829 move |params, _| async move {
9830 assert_eq!(
9831 params.text_document.uri,
9832 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9833 );
9834 futures::future::pending::<()>().await;
9835 unreachable!()
9836 },
9837 );
9838 let save = editor
9839 .update_in(cx, |editor, window, cx| {
9840 editor.save(
9841 SaveOptions {
9842 format: true,
9843 autosave: false,
9844 },
9845 project.clone(),
9846 window,
9847 cx,
9848 )
9849 })
9850 .unwrap();
9851 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9852 cx.executor().start_waiting();
9853 save.await;
9854 assert_eq!(
9855 editor.update(cx, |editor, cx| editor.text(cx)),
9856 "one\ntwo\nthree\n"
9857 );
9858 }
9859
9860 // Set rust language override and assert overridden tabsize is sent to language server
9861 update_test_language_settings(cx, |settings| {
9862 settings.languages.0.insert(
9863 "Rust".into(),
9864 LanguageSettingsContent {
9865 tab_size: NonZeroU32::new(8),
9866 ..Default::default()
9867 },
9868 );
9869 });
9870
9871 {
9872 editor.update_in(cx, |editor, window, cx| {
9873 editor.set_text("somehting_new\n", window, cx)
9874 });
9875 assert!(cx.read(|cx| editor.is_dirty(cx)));
9876 let _formatting_request_signal = fake_server
9877 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9878 assert_eq!(
9879 params.text_document.uri,
9880 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9881 );
9882 assert_eq!(params.options.tab_size, 8);
9883 Ok(Some(vec![]))
9884 });
9885 let save = editor
9886 .update_in(cx, |editor, window, cx| {
9887 editor.save(
9888 SaveOptions {
9889 format: true,
9890 autosave: false,
9891 },
9892 project.clone(),
9893 window,
9894 cx,
9895 )
9896 })
9897 .unwrap();
9898 cx.executor().start_waiting();
9899 save.await;
9900 }
9901}
9902
9903#[gpui::test]
9904async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9905 init_test(cx, |settings| {
9906 settings.defaults.ensure_final_newline_on_save = Some(false);
9907 });
9908
9909 let fs = FakeFs::new(cx.executor());
9910 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9911
9912 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9913
9914 let buffer = project
9915 .update(cx, |project, cx| {
9916 project.open_local_buffer(path!("/file.txt"), cx)
9917 })
9918 .await
9919 .unwrap();
9920
9921 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9922 let (editor, cx) = cx.add_window_view(|window, cx| {
9923 build_editor_with_project(project.clone(), buffer, window, cx)
9924 });
9925 editor.update_in(cx, |editor, window, cx| {
9926 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9927 s.select_ranges([0..0])
9928 });
9929 });
9930 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9931
9932 editor.update_in(cx, |editor, window, cx| {
9933 editor.handle_input("\n", window, cx)
9934 });
9935 cx.run_until_parked();
9936 save(&editor, &project, cx).await;
9937 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9938
9939 editor.update_in(cx, |editor, window, cx| {
9940 editor.undo(&Default::default(), window, cx);
9941 });
9942 save(&editor, &project, cx).await;
9943 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9944
9945 editor.update_in(cx, |editor, window, cx| {
9946 editor.redo(&Default::default(), window, cx);
9947 });
9948 cx.run_until_parked();
9949 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9950
9951 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9952 let save = editor
9953 .update_in(cx, |editor, window, cx| {
9954 editor.save(
9955 SaveOptions {
9956 format: true,
9957 autosave: false,
9958 },
9959 project.clone(),
9960 window,
9961 cx,
9962 )
9963 })
9964 .unwrap();
9965 cx.executor().start_waiting();
9966 save.await;
9967 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9968 }
9969}
9970
9971#[gpui::test]
9972async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9973 init_test(cx, |_| {});
9974
9975 let cols = 4;
9976 let rows = 10;
9977 let sample_text_1 = sample_text(rows, cols, 'a');
9978 assert_eq!(
9979 sample_text_1,
9980 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9981 );
9982 let sample_text_2 = sample_text(rows, cols, 'l');
9983 assert_eq!(
9984 sample_text_2,
9985 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9986 );
9987 let sample_text_3 = sample_text(rows, cols, 'v');
9988 assert_eq!(
9989 sample_text_3,
9990 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9991 );
9992
9993 let fs = FakeFs::new(cx.executor());
9994 fs.insert_tree(
9995 path!("/a"),
9996 json!({
9997 "main.rs": sample_text_1,
9998 "other.rs": sample_text_2,
9999 "lib.rs": sample_text_3,
10000 }),
10001 )
10002 .await;
10003
10004 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10005 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10006 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10007
10008 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10009 language_registry.add(rust_lang());
10010 let mut fake_servers = language_registry.register_fake_lsp(
10011 "Rust",
10012 FakeLspAdapter {
10013 capabilities: lsp::ServerCapabilities {
10014 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10015 ..Default::default()
10016 },
10017 ..Default::default()
10018 },
10019 );
10020
10021 let worktree = project.update(cx, |project, cx| {
10022 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10023 assert_eq!(worktrees.len(), 1);
10024 worktrees.pop().unwrap()
10025 });
10026 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10027
10028 let buffer_1 = project
10029 .update(cx, |project, cx| {
10030 project.open_buffer((worktree_id, "main.rs"), cx)
10031 })
10032 .await
10033 .unwrap();
10034 let buffer_2 = project
10035 .update(cx, |project, cx| {
10036 project.open_buffer((worktree_id, "other.rs"), cx)
10037 })
10038 .await
10039 .unwrap();
10040 let buffer_3 = project
10041 .update(cx, |project, cx| {
10042 project.open_buffer((worktree_id, "lib.rs"), cx)
10043 })
10044 .await
10045 .unwrap();
10046
10047 let multi_buffer = cx.new(|cx| {
10048 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10049 multi_buffer.push_excerpts(
10050 buffer_1.clone(),
10051 [
10052 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10053 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10054 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10055 ],
10056 cx,
10057 );
10058 multi_buffer.push_excerpts(
10059 buffer_2.clone(),
10060 [
10061 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10062 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10063 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10064 ],
10065 cx,
10066 );
10067 multi_buffer.push_excerpts(
10068 buffer_3.clone(),
10069 [
10070 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10071 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10072 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10073 ],
10074 cx,
10075 );
10076 multi_buffer
10077 });
10078 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10079 Editor::new(
10080 EditorMode::full(),
10081 multi_buffer,
10082 Some(project.clone()),
10083 window,
10084 cx,
10085 )
10086 });
10087
10088 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10089 editor.change_selections(
10090 SelectionEffects::scroll(Autoscroll::Next),
10091 window,
10092 cx,
10093 |s| s.select_ranges(Some(1..2)),
10094 );
10095 editor.insert("|one|two|three|", window, cx);
10096 });
10097 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10098 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10099 editor.change_selections(
10100 SelectionEffects::scroll(Autoscroll::Next),
10101 window,
10102 cx,
10103 |s| s.select_ranges(Some(60..70)),
10104 );
10105 editor.insert("|four|five|six|", window, cx);
10106 });
10107 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10108
10109 // First two buffers should be edited, but not the third one.
10110 assert_eq!(
10111 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10112 "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}",
10113 );
10114 buffer_1.update(cx, |buffer, _| {
10115 assert!(buffer.is_dirty());
10116 assert_eq!(
10117 buffer.text(),
10118 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10119 )
10120 });
10121 buffer_2.update(cx, |buffer, _| {
10122 assert!(buffer.is_dirty());
10123 assert_eq!(
10124 buffer.text(),
10125 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
10126 )
10127 });
10128 buffer_3.update(cx, |buffer, _| {
10129 assert!(!buffer.is_dirty());
10130 assert_eq!(buffer.text(), sample_text_3,)
10131 });
10132 cx.executor().run_until_parked();
10133
10134 cx.executor().start_waiting();
10135 let save = multi_buffer_editor
10136 .update_in(cx, |editor, window, cx| {
10137 editor.save(
10138 SaveOptions {
10139 format: true,
10140 autosave: false,
10141 },
10142 project.clone(),
10143 window,
10144 cx,
10145 )
10146 })
10147 .unwrap();
10148
10149 let fake_server = fake_servers.next().await.unwrap();
10150 fake_server
10151 .server
10152 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
10153 Ok(Some(vec![lsp::TextEdit::new(
10154 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10155 format!("[{} formatted]", params.text_document.uri),
10156 )]))
10157 })
10158 .detach();
10159 save.await;
10160
10161 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
10162 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
10163 assert_eq!(
10164 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10165 uri!(
10166 "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}"
10167 ),
10168 );
10169 buffer_1.update(cx, |buffer, _| {
10170 assert!(!buffer.is_dirty());
10171 assert_eq!(
10172 buffer.text(),
10173 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
10174 )
10175 });
10176 buffer_2.update(cx, |buffer, _| {
10177 assert!(!buffer.is_dirty());
10178 assert_eq!(
10179 buffer.text(),
10180 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
10181 )
10182 });
10183 buffer_3.update(cx, |buffer, _| {
10184 assert!(!buffer.is_dirty());
10185 assert_eq!(buffer.text(), sample_text_3,)
10186 });
10187}
10188
10189#[gpui::test]
10190async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
10191 init_test(cx, |_| {});
10192
10193 let fs = FakeFs::new(cx.executor());
10194 fs.insert_tree(
10195 path!("/dir"),
10196 json!({
10197 "file1.rs": "fn main() { println!(\"hello\"); }",
10198 "file2.rs": "fn test() { println!(\"test\"); }",
10199 "file3.rs": "fn other() { println!(\"other\"); }\n",
10200 }),
10201 )
10202 .await;
10203
10204 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
10205 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10206 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10207
10208 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10209 language_registry.add(rust_lang());
10210
10211 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
10212 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10213
10214 // Open three buffers
10215 let buffer_1 = project
10216 .update(cx, |project, cx| {
10217 project.open_buffer((worktree_id, "file1.rs"), cx)
10218 })
10219 .await
10220 .unwrap();
10221 let buffer_2 = project
10222 .update(cx, |project, cx| {
10223 project.open_buffer((worktree_id, "file2.rs"), cx)
10224 })
10225 .await
10226 .unwrap();
10227 let buffer_3 = project
10228 .update(cx, |project, cx| {
10229 project.open_buffer((worktree_id, "file3.rs"), cx)
10230 })
10231 .await
10232 .unwrap();
10233
10234 // Create a multi-buffer with all three buffers
10235 let multi_buffer = cx.new(|cx| {
10236 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10237 multi_buffer.push_excerpts(
10238 buffer_1.clone(),
10239 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10240 cx,
10241 );
10242 multi_buffer.push_excerpts(
10243 buffer_2.clone(),
10244 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10245 cx,
10246 );
10247 multi_buffer.push_excerpts(
10248 buffer_3.clone(),
10249 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10250 cx,
10251 );
10252 multi_buffer
10253 });
10254
10255 let editor = cx.new_window_entity(|window, cx| {
10256 Editor::new(
10257 EditorMode::full(),
10258 multi_buffer,
10259 Some(project.clone()),
10260 window,
10261 cx,
10262 )
10263 });
10264
10265 // Edit only the first buffer
10266 editor.update_in(cx, |editor, window, cx| {
10267 editor.change_selections(
10268 SelectionEffects::scroll(Autoscroll::Next),
10269 window,
10270 cx,
10271 |s| s.select_ranges(Some(10..10)),
10272 );
10273 editor.insert("// edited", window, cx);
10274 });
10275
10276 // Verify that only buffer 1 is dirty
10277 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10278 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10279 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10280
10281 // Get write counts after file creation (files were created with initial content)
10282 // We expect each file to have been written once during creation
10283 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10284 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10285 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10286
10287 // Perform autosave
10288 let save_task = editor.update_in(cx, |editor, window, cx| {
10289 editor.save(
10290 SaveOptions {
10291 format: true,
10292 autosave: true,
10293 },
10294 project.clone(),
10295 window,
10296 cx,
10297 )
10298 });
10299 save_task.await.unwrap();
10300
10301 // Only the dirty buffer should have been saved
10302 assert_eq!(
10303 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10304 1,
10305 "Buffer 1 was dirty, so it should have been written once during autosave"
10306 );
10307 assert_eq!(
10308 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10309 0,
10310 "Buffer 2 was clean, so it should not have been written during autosave"
10311 );
10312 assert_eq!(
10313 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10314 0,
10315 "Buffer 3 was clean, so it should not have been written during autosave"
10316 );
10317
10318 // Verify buffer states after autosave
10319 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10320 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10321 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10322
10323 // Now perform a manual save (format = true)
10324 let save_task = editor.update_in(cx, |editor, window, cx| {
10325 editor.save(
10326 SaveOptions {
10327 format: true,
10328 autosave: false,
10329 },
10330 project.clone(),
10331 window,
10332 cx,
10333 )
10334 });
10335 save_task.await.unwrap();
10336
10337 // During manual save, clean buffers don't get written to disk
10338 // They just get did_save called for language server notifications
10339 assert_eq!(
10340 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10341 1,
10342 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10343 );
10344 assert_eq!(
10345 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10346 0,
10347 "Buffer 2 should not have been written at all"
10348 );
10349 assert_eq!(
10350 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10351 0,
10352 "Buffer 3 should not have been written at all"
10353 );
10354}
10355
10356async fn setup_range_format_test(
10357 cx: &mut TestAppContext,
10358) -> (
10359 Entity<Project>,
10360 Entity<Editor>,
10361 &mut gpui::VisualTestContext,
10362 lsp::FakeLanguageServer,
10363) {
10364 init_test(cx, |_| {});
10365
10366 let fs = FakeFs::new(cx.executor());
10367 fs.insert_file(path!("/file.rs"), Default::default()).await;
10368
10369 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10370
10371 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10372 language_registry.add(rust_lang());
10373 let mut fake_servers = language_registry.register_fake_lsp(
10374 "Rust",
10375 FakeLspAdapter {
10376 capabilities: lsp::ServerCapabilities {
10377 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10378 ..lsp::ServerCapabilities::default()
10379 },
10380 ..FakeLspAdapter::default()
10381 },
10382 );
10383
10384 let buffer = project
10385 .update(cx, |project, cx| {
10386 project.open_local_buffer(path!("/file.rs"), cx)
10387 })
10388 .await
10389 .unwrap();
10390
10391 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10392 let (editor, cx) = cx.add_window_view(|window, cx| {
10393 build_editor_with_project(project.clone(), buffer, window, cx)
10394 });
10395
10396 cx.executor().start_waiting();
10397 let fake_server = fake_servers.next().await.unwrap();
10398
10399 (project, editor, cx, fake_server)
10400}
10401
10402#[gpui::test]
10403async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10404 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10405
10406 editor.update_in(cx, |editor, window, cx| {
10407 editor.set_text("one\ntwo\nthree\n", window, cx)
10408 });
10409 assert!(cx.read(|cx| editor.is_dirty(cx)));
10410
10411 let save = editor
10412 .update_in(cx, |editor, window, cx| {
10413 editor.save(
10414 SaveOptions {
10415 format: true,
10416 autosave: false,
10417 },
10418 project.clone(),
10419 window,
10420 cx,
10421 )
10422 })
10423 .unwrap();
10424 fake_server
10425 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10426 assert_eq!(
10427 params.text_document.uri,
10428 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10429 );
10430 assert_eq!(params.options.tab_size, 4);
10431 Ok(Some(vec![lsp::TextEdit::new(
10432 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10433 ", ".to_string(),
10434 )]))
10435 })
10436 .next()
10437 .await;
10438 cx.executor().start_waiting();
10439 save.await;
10440 assert_eq!(
10441 editor.update(cx, |editor, cx| editor.text(cx)),
10442 "one, two\nthree\n"
10443 );
10444 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10445}
10446
10447#[gpui::test]
10448async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10449 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10450
10451 editor.update_in(cx, |editor, window, cx| {
10452 editor.set_text("one\ntwo\nthree\n", window, cx)
10453 });
10454 assert!(cx.read(|cx| editor.is_dirty(cx)));
10455
10456 // Test that save still works when formatting hangs
10457 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10458 move |params, _| async move {
10459 assert_eq!(
10460 params.text_document.uri,
10461 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10462 );
10463 futures::future::pending::<()>().await;
10464 unreachable!()
10465 },
10466 );
10467 let save = editor
10468 .update_in(cx, |editor, window, cx| {
10469 editor.save(
10470 SaveOptions {
10471 format: true,
10472 autosave: false,
10473 },
10474 project.clone(),
10475 window,
10476 cx,
10477 )
10478 })
10479 .unwrap();
10480 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10481 cx.executor().start_waiting();
10482 save.await;
10483 assert_eq!(
10484 editor.update(cx, |editor, cx| editor.text(cx)),
10485 "one\ntwo\nthree\n"
10486 );
10487 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10488}
10489
10490#[gpui::test]
10491async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10492 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10493
10494 // Buffer starts clean, no formatting should be requested
10495 let save = editor
10496 .update_in(cx, |editor, window, cx| {
10497 editor.save(
10498 SaveOptions {
10499 format: false,
10500 autosave: false,
10501 },
10502 project.clone(),
10503 window,
10504 cx,
10505 )
10506 })
10507 .unwrap();
10508 let _pending_format_request = fake_server
10509 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10510 panic!("Should not be invoked");
10511 })
10512 .next();
10513 cx.executor().start_waiting();
10514 save.await;
10515 cx.run_until_parked();
10516}
10517
10518#[gpui::test]
10519async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10520 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10521
10522 // Set Rust language override and assert overridden tabsize is sent to language server
10523 update_test_language_settings(cx, |settings| {
10524 settings.languages.0.insert(
10525 "Rust".into(),
10526 LanguageSettingsContent {
10527 tab_size: NonZeroU32::new(8),
10528 ..Default::default()
10529 },
10530 );
10531 });
10532
10533 editor.update_in(cx, |editor, window, cx| {
10534 editor.set_text("something_new\n", window, cx)
10535 });
10536 assert!(cx.read(|cx| editor.is_dirty(cx)));
10537 let save = editor
10538 .update_in(cx, |editor, window, cx| {
10539 editor.save(
10540 SaveOptions {
10541 format: true,
10542 autosave: false,
10543 },
10544 project.clone(),
10545 window,
10546 cx,
10547 )
10548 })
10549 .unwrap();
10550 fake_server
10551 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10552 assert_eq!(
10553 params.text_document.uri,
10554 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10555 );
10556 assert_eq!(params.options.tab_size, 8);
10557 Ok(Some(Vec::new()))
10558 })
10559 .next()
10560 .await;
10561 save.await;
10562}
10563
10564#[gpui::test]
10565async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10566 init_test(cx, |settings| {
10567 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10568 Formatter::LanguageServer { name: None },
10569 )))
10570 });
10571
10572 let fs = FakeFs::new(cx.executor());
10573 fs.insert_file(path!("/file.rs"), Default::default()).await;
10574
10575 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10576
10577 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10578 language_registry.add(Arc::new(Language::new(
10579 LanguageConfig {
10580 name: "Rust".into(),
10581 matcher: LanguageMatcher {
10582 path_suffixes: vec!["rs".to_string()],
10583 ..Default::default()
10584 },
10585 ..LanguageConfig::default()
10586 },
10587 Some(tree_sitter_rust::LANGUAGE.into()),
10588 )));
10589 update_test_language_settings(cx, |settings| {
10590 // Enable Prettier formatting for the same buffer, and ensure
10591 // LSP is called instead of Prettier.
10592 settings.defaults.prettier = Some(PrettierSettings {
10593 allowed: true,
10594 ..PrettierSettings::default()
10595 });
10596 });
10597 let mut fake_servers = language_registry.register_fake_lsp(
10598 "Rust",
10599 FakeLspAdapter {
10600 capabilities: lsp::ServerCapabilities {
10601 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10602 ..Default::default()
10603 },
10604 ..Default::default()
10605 },
10606 );
10607
10608 let buffer = project
10609 .update(cx, |project, cx| {
10610 project.open_local_buffer(path!("/file.rs"), cx)
10611 })
10612 .await
10613 .unwrap();
10614
10615 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10616 let (editor, cx) = cx.add_window_view(|window, cx| {
10617 build_editor_with_project(project.clone(), buffer, window, cx)
10618 });
10619 editor.update_in(cx, |editor, window, cx| {
10620 editor.set_text("one\ntwo\nthree\n", window, cx)
10621 });
10622
10623 cx.executor().start_waiting();
10624 let fake_server = fake_servers.next().await.unwrap();
10625
10626 let format = editor
10627 .update_in(cx, |editor, window, cx| {
10628 editor.perform_format(
10629 project.clone(),
10630 FormatTrigger::Manual,
10631 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10632 window,
10633 cx,
10634 )
10635 })
10636 .unwrap();
10637 fake_server
10638 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10639 assert_eq!(
10640 params.text_document.uri,
10641 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10642 );
10643 assert_eq!(params.options.tab_size, 4);
10644 Ok(Some(vec![lsp::TextEdit::new(
10645 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10646 ", ".to_string(),
10647 )]))
10648 })
10649 .next()
10650 .await;
10651 cx.executor().start_waiting();
10652 format.await;
10653 assert_eq!(
10654 editor.update(cx, |editor, cx| editor.text(cx)),
10655 "one, two\nthree\n"
10656 );
10657
10658 editor.update_in(cx, |editor, window, cx| {
10659 editor.set_text("one\ntwo\nthree\n", window, cx)
10660 });
10661 // Ensure we don't lock if formatting hangs.
10662 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10663 move |params, _| async move {
10664 assert_eq!(
10665 params.text_document.uri,
10666 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10667 );
10668 futures::future::pending::<()>().await;
10669 unreachable!()
10670 },
10671 );
10672 let format = editor
10673 .update_in(cx, |editor, window, cx| {
10674 editor.perform_format(
10675 project,
10676 FormatTrigger::Manual,
10677 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10678 window,
10679 cx,
10680 )
10681 })
10682 .unwrap();
10683 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10684 cx.executor().start_waiting();
10685 format.await;
10686 assert_eq!(
10687 editor.update(cx, |editor, cx| editor.text(cx)),
10688 "one\ntwo\nthree\n"
10689 );
10690}
10691
10692#[gpui::test]
10693async fn test_multiple_formatters(cx: &mut TestAppContext) {
10694 init_test(cx, |settings| {
10695 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10696 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10697 Formatter::LanguageServer { name: None },
10698 Formatter::CodeActions(
10699 [
10700 ("code-action-1".into(), true),
10701 ("code-action-2".into(), true),
10702 ]
10703 .into_iter()
10704 .collect(),
10705 ),
10706 ])))
10707 });
10708
10709 let fs = FakeFs::new(cx.executor());
10710 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10711 .await;
10712
10713 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10714 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10715 language_registry.add(rust_lang());
10716
10717 let mut fake_servers = language_registry.register_fake_lsp(
10718 "Rust",
10719 FakeLspAdapter {
10720 capabilities: lsp::ServerCapabilities {
10721 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10722 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10723 commands: vec!["the-command-for-code-action-1".into()],
10724 ..Default::default()
10725 }),
10726 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10727 ..Default::default()
10728 },
10729 ..Default::default()
10730 },
10731 );
10732
10733 let buffer = project
10734 .update(cx, |project, cx| {
10735 project.open_local_buffer(path!("/file.rs"), cx)
10736 })
10737 .await
10738 .unwrap();
10739
10740 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10741 let (editor, cx) = cx.add_window_view(|window, cx| {
10742 build_editor_with_project(project.clone(), buffer, window, cx)
10743 });
10744
10745 cx.executor().start_waiting();
10746
10747 let fake_server = fake_servers.next().await.unwrap();
10748 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10749 move |_params, _| async move {
10750 Ok(Some(vec![lsp::TextEdit::new(
10751 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10752 "applied-formatting\n".to_string(),
10753 )]))
10754 },
10755 );
10756 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10757 move |params, _| async move {
10758 assert_eq!(
10759 params.context.only,
10760 Some(vec!["code-action-1".into(), "code-action-2".into()])
10761 );
10762 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10763 Ok(Some(vec![
10764 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10765 kind: Some("code-action-1".into()),
10766 edit: Some(lsp::WorkspaceEdit::new(
10767 [(
10768 uri.clone(),
10769 vec![lsp::TextEdit::new(
10770 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10771 "applied-code-action-1-edit\n".to_string(),
10772 )],
10773 )]
10774 .into_iter()
10775 .collect(),
10776 )),
10777 command: Some(lsp::Command {
10778 command: "the-command-for-code-action-1".into(),
10779 ..Default::default()
10780 }),
10781 ..Default::default()
10782 }),
10783 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10784 kind: Some("code-action-2".into()),
10785 edit: Some(lsp::WorkspaceEdit::new(
10786 [(
10787 uri,
10788 vec![lsp::TextEdit::new(
10789 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10790 "applied-code-action-2-edit\n".to_string(),
10791 )],
10792 )]
10793 .into_iter()
10794 .collect(),
10795 )),
10796 ..Default::default()
10797 }),
10798 ]))
10799 },
10800 );
10801
10802 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10803 move |params, _| async move { Ok(params) }
10804 });
10805
10806 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10807 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10808 let fake = fake_server.clone();
10809 let lock = command_lock.clone();
10810 move |params, _| {
10811 assert_eq!(params.command, "the-command-for-code-action-1");
10812 let fake = fake.clone();
10813 let lock = lock.clone();
10814 async move {
10815 lock.lock().await;
10816 fake.server
10817 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10818 label: None,
10819 edit: lsp::WorkspaceEdit {
10820 changes: Some(
10821 [(
10822 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10823 vec![lsp::TextEdit {
10824 range: lsp::Range::new(
10825 lsp::Position::new(0, 0),
10826 lsp::Position::new(0, 0),
10827 ),
10828 new_text: "applied-code-action-1-command\n".into(),
10829 }],
10830 )]
10831 .into_iter()
10832 .collect(),
10833 ),
10834 ..Default::default()
10835 },
10836 })
10837 .await
10838 .into_response()
10839 .unwrap();
10840 Ok(Some(json!(null)))
10841 }
10842 }
10843 });
10844
10845 cx.executor().start_waiting();
10846 editor
10847 .update_in(cx, |editor, window, cx| {
10848 editor.perform_format(
10849 project.clone(),
10850 FormatTrigger::Manual,
10851 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10852 window,
10853 cx,
10854 )
10855 })
10856 .unwrap()
10857 .await;
10858 editor.update(cx, |editor, cx| {
10859 assert_eq!(
10860 editor.text(cx),
10861 r#"
10862 applied-code-action-2-edit
10863 applied-code-action-1-command
10864 applied-code-action-1-edit
10865 applied-formatting
10866 one
10867 two
10868 three
10869 "#
10870 .unindent()
10871 );
10872 });
10873
10874 editor.update_in(cx, |editor, window, cx| {
10875 editor.undo(&Default::default(), window, cx);
10876 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10877 });
10878
10879 // Perform a manual edit while waiting for an LSP command
10880 // that's being run as part of a formatting code action.
10881 let lock_guard = command_lock.lock().await;
10882 let format = editor
10883 .update_in(cx, |editor, window, cx| {
10884 editor.perform_format(
10885 project.clone(),
10886 FormatTrigger::Manual,
10887 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10888 window,
10889 cx,
10890 )
10891 })
10892 .unwrap();
10893 cx.run_until_parked();
10894 editor.update(cx, |editor, cx| {
10895 assert_eq!(
10896 editor.text(cx),
10897 r#"
10898 applied-code-action-1-edit
10899 applied-formatting
10900 one
10901 two
10902 three
10903 "#
10904 .unindent()
10905 );
10906
10907 editor.buffer.update(cx, |buffer, cx| {
10908 let ix = buffer.len(cx);
10909 buffer.edit([(ix..ix, "edited\n")], None, cx);
10910 });
10911 });
10912
10913 // Allow the LSP command to proceed. Because the buffer was edited,
10914 // the second code action will not be run.
10915 drop(lock_guard);
10916 format.await;
10917 editor.update_in(cx, |editor, window, cx| {
10918 assert_eq!(
10919 editor.text(cx),
10920 r#"
10921 applied-code-action-1-command
10922 applied-code-action-1-edit
10923 applied-formatting
10924 one
10925 two
10926 three
10927 edited
10928 "#
10929 .unindent()
10930 );
10931
10932 // The manual edit is undone first, because it is the last thing the user did
10933 // (even though the command completed afterwards).
10934 editor.undo(&Default::default(), window, cx);
10935 assert_eq!(
10936 editor.text(cx),
10937 r#"
10938 applied-code-action-1-command
10939 applied-code-action-1-edit
10940 applied-formatting
10941 one
10942 two
10943 three
10944 "#
10945 .unindent()
10946 );
10947
10948 // All the formatting (including the command, which completed after the manual edit)
10949 // is undone together.
10950 editor.undo(&Default::default(), window, cx);
10951 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10952 });
10953}
10954
10955#[gpui::test]
10956async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10957 init_test(cx, |settings| {
10958 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10959 Formatter::LanguageServer { name: None },
10960 ])))
10961 });
10962
10963 let fs = FakeFs::new(cx.executor());
10964 fs.insert_file(path!("/file.ts"), Default::default()).await;
10965
10966 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10967
10968 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10969 language_registry.add(Arc::new(Language::new(
10970 LanguageConfig {
10971 name: "TypeScript".into(),
10972 matcher: LanguageMatcher {
10973 path_suffixes: vec!["ts".to_string()],
10974 ..Default::default()
10975 },
10976 ..LanguageConfig::default()
10977 },
10978 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10979 )));
10980 update_test_language_settings(cx, |settings| {
10981 settings.defaults.prettier = Some(PrettierSettings {
10982 allowed: true,
10983 ..PrettierSettings::default()
10984 });
10985 });
10986 let mut fake_servers = language_registry.register_fake_lsp(
10987 "TypeScript",
10988 FakeLspAdapter {
10989 capabilities: lsp::ServerCapabilities {
10990 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10991 ..Default::default()
10992 },
10993 ..Default::default()
10994 },
10995 );
10996
10997 let buffer = project
10998 .update(cx, |project, cx| {
10999 project.open_local_buffer(path!("/file.ts"), cx)
11000 })
11001 .await
11002 .unwrap();
11003
11004 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11005 let (editor, cx) = cx.add_window_view(|window, cx| {
11006 build_editor_with_project(project.clone(), buffer, window, cx)
11007 });
11008 editor.update_in(cx, |editor, window, cx| {
11009 editor.set_text(
11010 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11011 window,
11012 cx,
11013 )
11014 });
11015
11016 cx.executor().start_waiting();
11017 let fake_server = fake_servers.next().await.unwrap();
11018
11019 let format = editor
11020 .update_in(cx, |editor, window, cx| {
11021 editor.perform_code_action_kind(
11022 project.clone(),
11023 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11024 window,
11025 cx,
11026 )
11027 })
11028 .unwrap();
11029 fake_server
11030 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11031 assert_eq!(
11032 params.text_document.uri,
11033 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
11034 );
11035 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11036 lsp::CodeAction {
11037 title: "Organize Imports".to_string(),
11038 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11039 edit: Some(lsp::WorkspaceEdit {
11040 changes: Some(
11041 [(
11042 params.text_document.uri.clone(),
11043 vec![lsp::TextEdit::new(
11044 lsp::Range::new(
11045 lsp::Position::new(1, 0),
11046 lsp::Position::new(2, 0),
11047 ),
11048 "".to_string(),
11049 )],
11050 )]
11051 .into_iter()
11052 .collect(),
11053 ),
11054 ..Default::default()
11055 }),
11056 ..Default::default()
11057 },
11058 )]))
11059 })
11060 .next()
11061 .await;
11062 cx.executor().start_waiting();
11063 format.await;
11064 assert_eq!(
11065 editor.update(cx, |editor, cx| editor.text(cx)),
11066 "import { a } from 'module';\n\nconst x = a;\n"
11067 );
11068
11069 editor.update_in(cx, |editor, window, cx| {
11070 editor.set_text(
11071 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11072 window,
11073 cx,
11074 )
11075 });
11076 // Ensure we don't lock if code action hangs.
11077 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11078 move |params, _| async move {
11079 assert_eq!(
11080 params.text_document.uri,
11081 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
11082 );
11083 futures::future::pending::<()>().await;
11084 unreachable!()
11085 },
11086 );
11087 let format = editor
11088 .update_in(cx, |editor, window, cx| {
11089 editor.perform_code_action_kind(
11090 project,
11091 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11092 window,
11093 cx,
11094 )
11095 })
11096 .unwrap();
11097 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11098 cx.executor().start_waiting();
11099 format.await;
11100 assert_eq!(
11101 editor.update(cx, |editor, cx| editor.text(cx)),
11102 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11103 );
11104}
11105
11106#[gpui::test]
11107async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11108 init_test(cx, |_| {});
11109
11110 let mut cx = EditorLspTestContext::new_rust(
11111 lsp::ServerCapabilities {
11112 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11113 ..Default::default()
11114 },
11115 cx,
11116 )
11117 .await;
11118
11119 cx.set_state(indoc! {"
11120 one.twoˇ
11121 "});
11122
11123 // The format request takes a long time. When it completes, it inserts
11124 // a newline and an indent before the `.`
11125 cx.lsp
11126 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
11127 let executor = cx.background_executor().clone();
11128 async move {
11129 executor.timer(Duration::from_millis(100)).await;
11130 Ok(Some(vec![lsp::TextEdit {
11131 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
11132 new_text: "\n ".into(),
11133 }]))
11134 }
11135 });
11136
11137 // Submit a format request.
11138 let format_1 = cx
11139 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11140 .unwrap();
11141 cx.executor().run_until_parked();
11142
11143 // Submit a second format request.
11144 let format_2 = cx
11145 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11146 .unwrap();
11147 cx.executor().run_until_parked();
11148
11149 // Wait for both format requests to complete
11150 cx.executor().advance_clock(Duration::from_millis(200));
11151 cx.executor().start_waiting();
11152 format_1.await.unwrap();
11153 cx.executor().start_waiting();
11154 format_2.await.unwrap();
11155
11156 // The formatting edits only happens once.
11157 cx.assert_editor_state(indoc! {"
11158 one
11159 .twoˇ
11160 "});
11161}
11162
11163#[gpui::test]
11164async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
11165 init_test(cx, |settings| {
11166 settings.defaults.formatter = Some(SelectedFormatter::Auto)
11167 });
11168
11169 let mut cx = EditorLspTestContext::new_rust(
11170 lsp::ServerCapabilities {
11171 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11172 ..Default::default()
11173 },
11174 cx,
11175 )
11176 .await;
11177
11178 // Set up a buffer white some trailing whitespace and no trailing newline.
11179 cx.set_state(
11180 &[
11181 "one ", //
11182 "twoˇ", //
11183 "three ", //
11184 "four", //
11185 ]
11186 .join("\n"),
11187 );
11188
11189 // Submit a format request.
11190 let format = cx
11191 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11192 .unwrap();
11193
11194 // Record which buffer changes have been sent to the language server
11195 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
11196 cx.lsp
11197 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
11198 let buffer_changes = buffer_changes.clone();
11199 move |params, _| {
11200 buffer_changes.lock().extend(
11201 params
11202 .content_changes
11203 .into_iter()
11204 .map(|e| (e.range.unwrap(), e.text)),
11205 );
11206 }
11207 });
11208
11209 // Handle formatting requests to the language server.
11210 cx.lsp
11211 .set_request_handler::<lsp::request::Formatting, _, _>({
11212 let buffer_changes = buffer_changes.clone();
11213 move |_, _| {
11214 // When formatting is requested, trailing whitespace has already been stripped,
11215 // and the trailing newline has already been added.
11216 assert_eq!(
11217 &buffer_changes.lock()[1..],
11218 &[
11219 (
11220 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
11221 "".into()
11222 ),
11223 (
11224 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
11225 "".into()
11226 ),
11227 (
11228 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
11229 "\n".into()
11230 ),
11231 ]
11232 );
11233
11234 // Insert blank lines between each line of the buffer.
11235 async move {
11236 Ok(Some(vec![
11237 lsp::TextEdit {
11238 range: lsp::Range::new(
11239 lsp::Position::new(1, 0),
11240 lsp::Position::new(1, 0),
11241 ),
11242 new_text: "\n".into(),
11243 },
11244 lsp::TextEdit {
11245 range: lsp::Range::new(
11246 lsp::Position::new(2, 0),
11247 lsp::Position::new(2, 0),
11248 ),
11249 new_text: "\n".into(),
11250 },
11251 ]))
11252 }
11253 }
11254 });
11255
11256 // After formatting the buffer, the trailing whitespace is stripped,
11257 // a newline is appended, and the edits provided by the language server
11258 // have been applied.
11259 format.await.unwrap();
11260 cx.assert_editor_state(
11261 &[
11262 "one", //
11263 "", //
11264 "twoˇ", //
11265 "", //
11266 "three", //
11267 "four", //
11268 "", //
11269 ]
11270 .join("\n"),
11271 );
11272
11273 // Undoing the formatting undoes the trailing whitespace removal, the
11274 // trailing newline, and the LSP edits.
11275 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11276 cx.assert_editor_state(
11277 &[
11278 "one ", //
11279 "twoˇ", //
11280 "three ", //
11281 "four", //
11282 ]
11283 .join("\n"),
11284 );
11285}
11286
11287#[gpui::test]
11288async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11289 cx: &mut TestAppContext,
11290) {
11291 init_test(cx, |_| {});
11292
11293 cx.update(|cx| {
11294 cx.update_global::<SettingsStore, _>(|settings, cx| {
11295 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11296 settings.auto_signature_help = Some(true);
11297 });
11298 });
11299 });
11300
11301 let mut cx = EditorLspTestContext::new_rust(
11302 lsp::ServerCapabilities {
11303 signature_help_provider: Some(lsp::SignatureHelpOptions {
11304 ..Default::default()
11305 }),
11306 ..Default::default()
11307 },
11308 cx,
11309 )
11310 .await;
11311
11312 let language = Language::new(
11313 LanguageConfig {
11314 name: "Rust".into(),
11315 brackets: BracketPairConfig {
11316 pairs: vec![
11317 BracketPair {
11318 start: "{".to_string(),
11319 end: "}".to_string(),
11320 close: true,
11321 surround: true,
11322 newline: true,
11323 },
11324 BracketPair {
11325 start: "(".to_string(),
11326 end: ")".to_string(),
11327 close: true,
11328 surround: true,
11329 newline: true,
11330 },
11331 BracketPair {
11332 start: "/*".to_string(),
11333 end: " */".to_string(),
11334 close: true,
11335 surround: true,
11336 newline: true,
11337 },
11338 BracketPair {
11339 start: "[".to_string(),
11340 end: "]".to_string(),
11341 close: false,
11342 surround: false,
11343 newline: true,
11344 },
11345 BracketPair {
11346 start: "\"".to_string(),
11347 end: "\"".to_string(),
11348 close: true,
11349 surround: true,
11350 newline: false,
11351 },
11352 BracketPair {
11353 start: "<".to_string(),
11354 end: ">".to_string(),
11355 close: false,
11356 surround: true,
11357 newline: true,
11358 },
11359 ],
11360 ..Default::default()
11361 },
11362 autoclose_before: "})]".to_string(),
11363 ..Default::default()
11364 },
11365 Some(tree_sitter_rust::LANGUAGE.into()),
11366 );
11367 let language = Arc::new(language);
11368
11369 cx.language_registry().add(language.clone());
11370 cx.update_buffer(|buffer, cx| {
11371 buffer.set_language(Some(language), cx);
11372 });
11373
11374 cx.set_state(
11375 &r#"
11376 fn main() {
11377 sampleˇ
11378 }
11379 "#
11380 .unindent(),
11381 );
11382
11383 cx.update_editor(|editor, window, cx| {
11384 editor.handle_input("(", window, cx);
11385 });
11386 cx.assert_editor_state(
11387 &"
11388 fn main() {
11389 sample(ˇ)
11390 }
11391 "
11392 .unindent(),
11393 );
11394
11395 let mocked_response = lsp::SignatureHelp {
11396 signatures: vec![lsp::SignatureInformation {
11397 label: "fn sample(param1: u8, param2: u8)".to_string(),
11398 documentation: None,
11399 parameters: Some(vec![
11400 lsp::ParameterInformation {
11401 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11402 documentation: None,
11403 },
11404 lsp::ParameterInformation {
11405 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11406 documentation: None,
11407 },
11408 ]),
11409 active_parameter: None,
11410 }],
11411 active_signature: Some(0),
11412 active_parameter: Some(0),
11413 };
11414 handle_signature_help_request(&mut cx, mocked_response).await;
11415
11416 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11417 .await;
11418
11419 cx.editor(|editor, _, _| {
11420 let signature_help_state = editor.signature_help_state.popover().cloned();
11421 let signature = signature_help_state.unwrap();
11422 assert_eq!(
11423 signature.signatures[signature.current_signature].label,
11424 "fn sample(param1: u8, param2: u8)"
11425 );
11426 });
11427}
11428
11429#[gpui::test]
11430async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11431 init_test(cx, |_| {});
11432
11433 cx.update(|cx| {
11434 cx.update_global::<SettingsStore, _>(|settings, cx| {
11435 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11436 settings.auto_signature_help = Some(false);
11437 settings.show_signature_help_after_edits = Some(false);
11438 });
11439 });
11440 });
11441
11442 let mut cx = EditorLspTestContext::new_rust(
11443 lsp::ServerCapabilities {
11444 signature_help_provider: Some(lsp::SignatureHelpOptions {
11445 ..Default::default()
11446 }),
11447 ..Default::default()
11448 },
11449 cx,
11450 )
11451 .await;
11452
11453 let language = Language::new(
11454 LanguageConfig {
11455 name: "Rust".into(),
11456 brackets: BracketPairConfig {
11457 pairs: vec![
11458 BracketPair {
11459 start: "{".to_string(),
11460 end: "}".to_string(),
11461 close: true,
11462 surround: true,
11463 newline: true,
11464 },
11465 BracketPair {
11466 start: "(".to_string(),
11467 end: ")".to_string(),
11468 close: true,
11469 surround: true,
11470 newline: true,
11471 },
11472 BracketPair {
11473 start: "/*".to_string(),
11474 end: " */".to_string(),
11475 close: true,
11476 surround: true,
11477 newline: true,
11478 },
11479 BracketPair {
11480 start: "[".to_string(),
11481 end: "]".to_string(),
11482 close: false,
11483 surround: false,
11484 newline: true,
11485 },
11486 BracketPair {
11487 start: "\"".to_string(),
11488 end: "\"".to_string(),
11489 close: true,
11490 surround: true,
11491 newline: false,
11492 },
11493 BracketPair {
11494 start: "<".to_string(),
11495 end: ">".to_string(),
11496 close: false,
11497 surround: true,
11498 newline: true,
11499 },
11500 ],
11501 ..Default::default()
11502 },
11503 autoclose_before: "})]".to_string(),
11504 ..Default::default()
11505 },
11506 Some(tree_sitter_rust::LANGUAGE.into()),
11507 );
11508 let language = Arc::new(language);
11509
11510 cx.language_registry().add(language.clone());
11511 cx.update_buffer(|buffer, cx| {
11512 buffer.set_language(Some(language), cx);
11513 });
11514
11515 // Ensure that signature_help is not called when no signature help is enabled.
11516 cx.set_state(
11517 &r#"
11518 fn main() {
11519 sampleˇ
11520 }
11521 "#
11522 .unindent(),
11523 );
11524 cx.update_editor(|editor, window, cx| {
11525 editor.handle_input("(", window, cx);
11526 });
11527 cx.assert_editor_state(
11528 &"
11529 fn main() {
11530 sample(ˇ)
11531 }
11532 "
11533 .unindent(),
11534 );
11535 cx.editor(|editor, _, _| {
11536 assert!(editor.signature_help_state.task().is_none());
11537 });
11538
11539 let mocked_response = lsp::SignatureHelp {
11540 signatures: vec![lsp::SignatureInformation {
11541 label: "fn sample(param1: u8, param2: u8)".to_string(),
11542 documentation: None,
11543 parameters: Some(vec![
11544 lsp::ParameterInformation {
11545 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11546 documentation: None,
11547 },
11548 lsp::ParameterInformation {
11549 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11550 documentation: None,
11551 },
11552 ]),
11553 active_parameter: None,
11554 }],
11555 active_signature: Some(0),
11556 active_parameter: Some(0),
11557 };
11558
11559 // Ensure that signature_help is called when enabled afte edits
11560 cx.update(|_, cx| {
11561 cx.update_global::<SettingsStore, _>(|settings, cx| {
11562 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11563 settings.auto_signature_help = Some(false);
11564 settings.show_signature_help_after_edits = Some(true);
11565 });
11566 });
11567 });
11568 cx.set_state(
11569 &r#"
11570 fn main() {
11571 sampleˇ
11572 }
11573 "#
11574 .unindent(),
11575 );
11576 cx.update_editor(|editor, window, cx| {
11577 editor.handle_input("(", window, cx);
11578 });
11579 cx.assert_editor_state(
11580 &"
11581 fn main() {
11582 sample(ˇ)
11583 }
11584 "
11585 .unindent(),
11586 );
11587 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11588 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11589 .await;
11590 cx.update_editor(|editor, _, _| {
11591 let signature_help_state = editor.signature_help_state.popover().cloned();
11592 assert!(signature_help_state.is_some());
11593 let signature = signature_help_state.unwrap();
11594 assert_eq!(
11595 signature.signatures[signature.current_signature].label,
11596 "fn sample(param1: u8, param2: u8)"
11597 );
11598 editor.signature_help_state = SignatureHelpState::default();
11599 });
11600
11601 // Ensure that signature_help is called when auto signature help override is enabled
11602 cx.update(|_, cx| {
11603 cx.update_global::<SettingsStore, _>(|settings, cx| {
11604 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11605 settings.auto_signature_help = Some(true);
11606 settings.show_signature_help_after_edits = Some(false);
11607 });
11608 });
11609 });
11610 cx.set_state(
11611 &r#"
11612 fn main() {
11613 sampleˇ
11614 }
11615 "#
11616 .unindent(),
11617 );
11618 cx.update_editor(|editor, window, cx| {
11619 editor.handle_input("(", window, cx);
11620 });
11621 cx.assert_editor_state(
11622 &"
11623 fn main() {
11624 sample(ˇ)
11625 }
11626 "
11627 .unindent(),
11628 );
11629 handle_signature_help_request(&mut cx, mocked_response).await;
11630 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11631 .await;
11632 cx.editor(|editor, _, _| {
11633 let signature_help_state = editor.signature_help_state.popover().cloned();
11634 assert!(signature_help_state.is_some());
11635 let signature = signature_help_state.unwrap();
11636 assert_eq!(
11637 signature.signatures[signature.current_signature].label,
11638 "fn sample(param1: u8, param2: u8)"
11639 );
11640 });
11641}
11642
11643#[gpui::test]
11644async fn test_signature_help(cx: &mut TestAppContext) {
11645 init_test(cx, |_| {});
11646 cx.update(|cx| {
11647 cx.update_global::<SettingsStore, _>(|settings, cx| {
11648 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11649 settings.auto_signature_help = Some(true);
11650 });
11651 });
11652 });
11653
11654 let mut cx = EditorLspTestContext::new_rust(
11655 lsp::ServerCapabilities {
11656 signature_help_provider: Some(lsp::SignatureHelpOptions {
11657 ..Default::default()
11658 }),
11659 ..Default::default()
11660 },
11661 cx,
11662 )
11663 .await;
11664
11665 // A test that directly calls `show_signature_help`
11666 cx.update_editor(|editor, window, cx| {
11667 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11668 });
11669
11670 let mocked_response = lsp::SignatureHelp {
11671 signatures: vec![lsp::SignatureInformation {
11672 label: "fn sample(param1: u8, param2: u8)".to_string(),
11673 documentation: None,
11674 parameters: Some(vec![
11675 lsp::ParameterInformation {
11676 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11677 documentation: None,
11678 },
11679 lsp::ParameterInformation {
11680 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11681 documentation: None,
11682 },
11683 ]),
11684 active_parameter: None,
11685 }],
11686 active_signature: Some(0),
11687 active_parameter: Some(0),
11688 };
11689 handle_signature_help_request(&mut cx, mocked_response).await;
11690
11691 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11692 .await;
11693
11694 cx.editor(|editor, _, _| {
11695 let signature_help_state = editor.signature_help_state.popover().cloned();
11696 assert!(signature_help_state.is_some());
11697 let signature = signature_help_state.unwrap();
11698 assert_eq!(
11699 signature.signatures[signature.current_signature].label,
11700 "fn sample(param1: u8, param2: u8)"
11701 );
11702 });
11703
11704 // When exiting outside from inside the brackets, `signature_help` is closed.
11705 cx.set_state(indoc! {"
11706 fn main() {
11707 sample(ˇ);
11708 }
11709
11710 fn sample(param1: u8, param2: u8) {}
11711 "});
11712
11713 cx.update_editor(|editor, window, cx| {
11714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11715 s.select_ranges([0..0])
11716 });
11717 });
11718
11719 let mocked_response = lsp::SignatureHelp {
11720 signatures: Vec::new(),
11721 active_signature: None,
11722 active_parameter: None,
11723 };
11724 handle_signature_help_request(&mut cx, mocked_response).await;
11725
11726 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11727 .await;
11728
11729 cx.editor(|editor, _, _| {
11730 assert!(!editor.signature_help_state.is_shown());
11731 });
11732
11733 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11734 cx.set_state(indoc! {"
11735 fn main() {
11736 sample(ˇ);
11737 }
11738
11739 fn sample(param1: u8, param2: u8) {}
11740 "});
11741
11742 let mocked_response = lsp::SignatureHelp {
11743 signatures: vec![lsp::SignatureInformation {
11744 label: "fn sample(param1: u8, param2: u8)".to_string(),
11745 documentation: None,
11746 parameters: Some(vec![
11747 lsp::ParameterInformation {
11748 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11749 documentation: None,
11750 },
11751 lsp::ParameterInformation {
11752 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11753 documentation: None,
11754 },
11755 ]),
11756 active_parameter: None,
11757 }],
11758 active_signature: Some(0),
11759 active_parameter: Some(0),
11760 };
11761 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11762 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11763 .await;
11764 cx.editor(|editor, _, _| {
11765 assert!(editor.signature_help_state.is_shown());
11766 });
11767
11768 // Restore the popover with more parameter input
11769 cx.set_state(indoc! {"
11770 fn main() {
11771 sample(param1, param2ˇ);
11772 }
11773
11774 fn sample(param1: u8, param2: u8) {}
11775 "});
11776
11777 let mocked_response = lsp::SignatureHelp {
11778 signatures: vec![lsp::SignatureInformation {
11779 label: "fn sample(param1: u8, param2: u8)".to_string(),
11780 documentation: None,
11781 parameters: Some(vec![
11782 lsp::ParameterInformation {
11783 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11784 documentation: None,
11785 },
11786 lsp::ParameterInformation {
11787 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11788 documentation: None,
11789 },
11790 ]),
11791 active_parameter: None,
11792 }],
11793 active_signature: Some(0),
11794 active_parameter: Some(1),
11795 };
11796 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11797 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11798 .await;
11799
11800 // When selecting a range, the popover is gone.
11801 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11802 cx.update_editor(|editor, window, cx| {
11803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11804 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11805 })
11806 });
11807 cx.assert_editor_state(indoc! {"
11808 fn main() {
11809 sample(param1, «ˇparam2»);
11810 }
11811
11812 fn sample(param1: u8, param2: u8) {}
11813 "});
11814 cx.editor(|editor, _, _| {
11815 assert!(!editor.signature_help_state.is_shown());
11816 });
11817
11818 // When unselecting again, the popover is back if within the brackets.
11819 cx.update_editor(|editor, window, cx| {
11820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11821 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11822 })
11823 });
11824 cx.assert_editor_state(indoc! {"
11825 fn main() {
11826 sample(param1, ˇparam2);
11827 }
11828
11829 fn sample(param1: u8, param2: u8) {}
11830 "});
11831 handle_signature_help_request(&mut cx, mocked_response).await;
11832 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11833 .await;
11834 cx.editor(|editor, _, _| {
11835 assert!(editor.signature_help_state.is_shown());
11836 });
11837
11838 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11839 cx.update_editor(|editor, window, cx| {
11840 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11841 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11842 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11843 })
11844 });
11845 cx.assert_editor_state(indoc! {"
11846 fn main() {
11847 sample(param1, ˇparam2);
11848 }
11849
11850 fn sample(param1: u8, param2: u8) {}
11851 "});
11852
11853 let mocked_response = lsp::SignatureHelp {
11854 signatures: vec![lsp::SignatureInformation {
11855 label: "fn sample(param1: u8, param2: u8)".to_string(),
11856 documentation: None,
11857 parameters: Some(vec![
11858 lsp::ParameterInformation {
11859 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11860 documentation: None,
11861 },
11862 lsp::ParameterInformation {
11863 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11864 documentation: None,
11865 },
11866 ]),
11867 active_parameter: None,
11868 }],
11869 active_signature: Some(0),
11870 active_parameter: Some(1),
11871 };
11872 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11873 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11874 .await;
11875 cx.update_editor(|editor, _, cx| {
11876 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11877 });
11878 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11879 .await;
11880 cx.update_editor(|editor, window, cx| {
11881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11882 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11883 })
11884 });
11885 cx.assert_editor_state(indoc! {"
11886 fn main() {
11887 sample(param1, «ˇparam2»);
11888 }
11889
11890 fn sample(param1: u8, param2: u8) {}
11891 "});
11892 cx.update_editor(|editor, window, cx| {
11893 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11894 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11895 })
11896 });
11897 cx.assert_editor_state(indoc! {"
11898 fn main() {
11899 sample(param1, ˇparam2);
11900 }
11901
11902 fn sample(param1: u8, param2: u8) {}
11903 "});
11904 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11905 .await;
11906}
11907
11908#[gpui::test]
11909async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11910 init_test(cx, |_| {});
11911
11912 let mut cx = EditorLspTestContext::new_rust(
11913 lsp::ServerCapabilities {
11914 signature_help_provider: Some(lsp::SignatureHelpOptions {
11915 ..Default::default()
11916 }),
11917 ..Default::default()
11918 },
11919 cx,
11920 )
11921 .await;
11922
11923 cx.set_state(indoc! {"
11924 fn main() {
11925 overloadedˇ
11926 }
11927 "});
11928
11929 cx.update_editor(|editor, window, cx| {
11930 editor.handle_input("(", window, cx);
11931 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11932 });
11933
11934 // Mock response with 3 signatures
11935 let mocked_response = lsp::SignatureHelp {
11936 signatures: vec![
11937 lsp::SignatureInformation {
11938 label: "fn overloaded(x: i32)".to_string(),
11939 documentation: None,
11940 parameters: Some(vec![lsp::ParameterInformation {
11941 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11942 documentation: None,
11943 }]),
11944 active_parameter: None,
11945 },
11946 lsp::SignatureInformation {
11947 label: "fn overloaded(x: i32, y: i32)".to_string(),
11948 documentation: None,
11949 parameters: Some(vec![
11950 lsp::ParameterInformation {
11951 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11952 documentation: None,
11953 },
11954 lsp::ParameterInformation {
11955 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11956 documentation: None,
11957 },
11958 ]),
11959 active_parameter: None,
11960 },
11961 lsp::SignatureInformation {
11962 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11963 documentation: None,
11964 parameters: Some(vec![
11965 lsp::ParameterInformation {
11966 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11967 documentation: None,
11968 },
11969 lsp::ParameterInformation {
11970 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11971 documentation: None,
11972 },
11973 lsp::ParameterInformation {
11974 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11975 documentation: None,
11976 },
11977 ]),
11978 active_parameter: None,
11979 },
11980 ],
11981 active_signature: Some(1),
11982 active_parameter: Some(0),
11983 };
11984 handle_signature_help_request(&mut cx, mocked_response).await;
11985
11986 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11987 .await;
11988
11989 // Verify we have multiple signatures and the right one is selected
11990 cx.editor(|editor, _, _| {
11991 let popover = editor.signature_help_state.popover().cloned().unwrap();
11992 assert_eq!(popover.signatures.len(), 3);
11993 // active_signature was 1, so that should be the current
11994 assert_eq!(popover.current_signature, 1);
11995 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11996 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11997 assert_eq!(
11998 popover.signatures[2].label,
11999 "fn overloaded(x: i32, y: i32, z: i32)"
12000 );
12001 });
12002
12003 // Test navigation functionality
12004 cx.update_editor(|editor, window, cx| {
12005 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12006 });
12007
12008 cx.editor(|editor, _, _| {
12009 let popover = editor.signature_help_state.popover().cloned().unwrap();
12010 assert_eq!(popover.current_signature, 2);
12011 });
12012
12013 // Test wrap around
12014 cx.update_editor(|editor, window, cx| {
12015 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12016 });
12017
12018 cx.editor(|editor, _, _| {
12019 let popover = editor.signature_help_state.popover().cloned().unwrap();
12020 assert_eq!(popover.current_signature, 0);
12021 });
12022
12023 // Test previous navigation
12024 cx.update_editor(|editor, window, cx| {
12025 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12026 });
12027
12028 cx.editor(|editor, _, _| {
12029 let popover = editor.signature_help_state.popover().cloned().unwrap();
12030 assert_eq!(popover.current_signature, 2);
12031 });
12032}
12033
12034#[gpui::test]
12035async fn test_completion_mode(cx: &mut TestAppContext) {
12036 init_test(cx, |_| {});
12037 let mut cx = EditorLspTestContext::new_rust(
12038 lsp::ServerCapabilities {
12039 completion_provider: Some(lsp::CompletionOptions {
12040 resolve_provider: Some(true),
12041 ..Default::default()
12042 }),
12043 ..Default::default()
12044 },
12045 cx,
12046 )
12047 .await;
12048
12049 struct Run {
12050 run_description: &'static str,
12051 initial_state: String,
12052 buffer_marked_text: String,
12053 completion_label: &'static str,
12054 completion_text: &'static str,
12055 expected_with_insert_mode: String,
12056 expected_with_replace_mode: String,
12057 expected_with_replace_subsequence_mode: String,
12058 expected_with_replace_suffix_mode: String,
12059 }
12060
12061 let runs = [
12062 Run {
12063 run_description: "Start of word matches completion text",
12064 initial_state: "before ediˇ after".into(),
12065 buffer_marked_text: "before <edi|> after".into(),
12066 completion_label: "editor",
12067 completion_text: "editor",
12068 expected_with_insert_mode: "before editorˇ after".into(),
12069 expected_with_replace_mode: "before editorˇ after".into(),
12070 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12071 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12072 },
12073 Run {
12074 run_description: "Accept same text at the middle of the word",
12075 initial_state: "before ediˇtor after".into(),
12076 buffer_marked_text: "before <edi|tor> after".into(),
12077 completion_label: "editor",
12078 completion_text: "editor",
12079 expected_with_insert_mode: "before editorˇtor after".into(),
12080 expected_with_replace_mode: "before editorˇ after".into(),
12081 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12082 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12083 },
12084 Run {
12085 run_description: "End of word matches completion text -- cursor at end",
12086 initial_state: "before torˇ after".into(),
12087 buffer_marked_text: "before <tor|> after".into(),
12088 completion_label: "editor",
12089 completion_text: "editor",
12090 expected_with_insert_mode: "before editorˇ after".into(),
12091 expected_with_replace_mode: "before editorˇ after".into(),
12092 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12093 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12094 },
12095 Run {
12096 run_description: "End of word matches completion text -- cursor at start",
12097 initial_state: "before ˇtor after".into(),
12098 buffer_marked_text: "before <|tor> after".into(),
12099 completion_label: "editor",
12100 completion_text: "editor",
12101 expected_with_insert_mode: "before editorˇtor after".into(),
12102 expected_with_replace_mode: "before editorˇ after".into(),
12103 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12104 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12105 },
12106 Run {
12107 run_description: "Prepend text containing whitespace",
12108 initial_state: "pˇfield: bool".into(),
12109 buffer_marked_text: "<p|field>: bool".into(),
12110 completion_label: "pub ",
12111 completion_text: "pub ",
12112 expected_with_insert_mode: "pub ˇfield: bool".into(),
12113 expected_with_replace_mode: "pub ˇ: bool".into(),
12114 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12115 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12116 },
12117 Run {
12118 run_description: "Add element to start of list",
12119 initial_state: "[element_ˇelement_2]".into(),
12120 buffer_marked_text: "[<element_|element_2>]".into(),
12121 completion_label: "element_1",
12122 completion_text: "element_1",
12123 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
12124 expected_with_replace_mode: "[element_1ˇ]".into(),
12125 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
12126 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
12127 },
12128 Run {
12129 run_description: "Add element to start of list -- first and second elements are equal",
12130 initial_state: "[elˇelement]".into(),
12131 buffer_marked_text: "[<el|element>]".into(),
12132 completion_label: "element",
12133 completion_text: "element",
12134 expected_with_insert_mode: "[elementˇelement]".into(),
12135 expected_with_replace_mode: "[elementˇ]".into(),
12136 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
12137 expected_with_replace_suffix_mode: "[elementˇ]".into(),
12138 },
12139 Run {
12140 run_description: "Ends with matching suffix",
12141 initial_state: "SubˇError".into(),
12142 buffer_marked_text: "<Sub|Error>".into(),
12143 completion_label: "SubscriptionError",
12144 completion_text: "SubscriptionError",
12145 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
12146 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12147 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12148 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
12149 },
12150 Run {
12151 run_description: "Suffix is a subsequence -- contiguous",
12152 initial_state: "SubˇErr".into(),
12153 buffer_marked_text: "<Sub|Err>".into(),
12154 completion_label: "SubscriptionError",
12155 completion_text: "SubscriptionError",
12156 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
12157 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12158 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12159 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
12160 },
12161 Run {
12162 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
12163 initial_state: "Suˇscrirr".into(),
12164 buffer_marked_text: "<Su|scrirr>".into(),
12165 completion_label: "SubscriptionError",
12166 completion_text: "SubscriptionError",
12167 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
12168 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12169 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12170 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
12171 },
12172 Run {
12173 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
12174 initial_state: "foo(indˇix)".into(),
12175 buffer_marked_text: "foo(<ind|ix>)".into(),
12176 completion_label: "node_index",
12177 completion_text: "node_index",
12178 expected_with_insert_mode: "foo(node_indexˇix)".into(),
12179 expected_with_replace_mode: "foo(node_indexˇ)".into(),
12180 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
12181 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
12182 },
12183 Run {
12184 run_description: "Replace range ends before cursor - should extend to cursor",
12185 initial_state: "before editˇo after".into(),
12186 buffer_marked_text: "before <{ed}>it|o after".into(),
12187 completion_label: "editor",
12188 completion_text: "editor",
12189 expected_with_insert_mode: "before editorˇo after".into(),
12190 expected_with_replace_mode: "before editorˇo after".into(),
12191 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
12192 expected_with_replace_suffix_mode: "before editorˇo after".into(),
12193 },
12194 Run {
12195 run_description: "Uses label for suffix matching",
12196 initial_state: "before ediˇtor after".into(),
12197 buffer_marked_text: "before <edi|tor> after".into(),
12198 completion_label: "editor",
12199 completion_text: "editor()",
12200 expected_with_insert_mode: "before editor()ˇtor after".into(),
12201 expected_with_replace_mode: "before editor()ˇ after".into(),
12202 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
12203 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
12204 },
12205 Run {
12206 run_description: "Case insensitive subsequence and suffix matching",
12207 initial_state: "before EDiˇtoR after".into(),
12208 buffer_marked_text: "before <EDi|toR> after".into(),
12209 completion_label: "editor",
12210 completion_text: "editor",
12211 expected_with_insert_mode: "before editorˇtoR after".into(),
12212 expected_with_replace_mode: "before editorˇ after".into(),
12213 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12214 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12215 },
12216 ];
12217
12218 for run in runs {
12219 let run_variations = [
12220 (LspInsertMode::Insert, run.expected_with_insert_mode),
12221 (LspInsertMode::Replace, run.expected_with_replace_mode),
12222 (
12223 LspInsertMode::ReplaceSubsequence,
12224 run.expected_with_replace_subsequence_mode,
12225 ),
12226 (
12227 LspInsertMode::ReplaceSuffix,
12228 run.expected_with_replace_suffix_mode,
12229 ),
12230 ];
12231
12232 for (lsp_insert_mode, expected_text) in run_variations {
12233 eprintln!(
12234 "run = {:?}, mode = {lsp_insert_mode:.?}",
12235 run.run_description,
12236 );
12237
12238 update_test_language_settings(&mut cx, |settings| {
12239 settings.defaults.completions = Some(CompletionSettings {
12240 lsp_insert_mode,
12241 words: WordsCompletionMode::Disabled,
12242 words_min_length: 0,
12243 lsp: true,
12244 lsp_fetch_timeout_ms: 0,
12245 });
12246 });
12247
12248 cx.set_state(&run.initial_state);
12249 cx.update_editor(|editor, window, cx| {
12250 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12251 });
12252
12253 let counter = Arc::new(AtomicUsize::new(0));
12254 handle_completion_request_with_insert_and_replace(
12255 &mut cx,
12256 &run.buffer_marked_text,
12257 vec![(run.completion_label, run.completion_text)],
12258 counter.clone(),
12259 )
12260 .await;
12261 cx.condition(|editor, _| editor.context_menu_visible())
12262 .await;
12263 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12264
12265 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12266 editor
12267 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12268 .unwrap()
12269 });
12270 cx.assert_editor_state(&expected_text);
12271 handle_resolve_completion_request(&mut cx, None).await;
12272 apply_additional_edits.await.unwrap();
12273 }
12274 }
12275}
12276
12277#[gpui::test]
12278async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12279 init_test(cx, |_| {});
12280 let mut cx = EditorLspTestContext::new_rust(
12281 lsp::ServerCapabilities {
12282 completion_provider: Some(lsp::CompletionOptions {
12283 resolve_provider: Some(true),
12284 ..Default::default()
12285 }),
12286 ..Default::default()
12287 },
12288 cx,
12289 )
12290 .await;
12291
12292 let initial_state = "SubˇError";
12293 let buffer_marked_text = "<Sub|Error>";
12294 let completion_text = "SubscriptionError";
12295 let expected_with_insert_mode = "SubscriptionErrorˇError";
12296 let expected_with_replace_mode = "SubscriptionErrorˇ";
12297
12298 update_test_language_settings(&mut cx, |settings| {
12299 settings.defaults.completions = Some(CompletionSettings {
12300 words: WordsCompletionMode::Disabled,
12301 words_min_length: 0,
12302 // set the opposite here to ensure that the action is overriding the default behavior
12303 lsp_insert_mode: LspInsertMode::Insert,
12304 lsp: true,
12305 lsp_fetch_timeout_ms: 0,
12306 });
12307 });
12308
12309 cx.set_state(initial_state);
12310 cx.update_editor(|editor, window, cx| {
12311 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12312 });
12313
12314 let counter = Arc::new(AtomicUsize::new(0));
12315 handle_completion_request_with_insert_and_replace(
12316 &mut cx,
12317 buffer_marked_text,
12318 vec![(completion_text, completion_text)],
12319 counter.clone(),
12320 )
12321 .await;
12322 cx.condition(|editor, _| editor.context_menu_visible())
12323 .await;
12324 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12325
12326 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12327 editor
12328 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12329 .unwrap()
12330 });
12331 cx.assert_editor_state(expected_with_replace_mode);
12332 handle_resolve_completion_request(&mut cx, None).await;
12333 apply_additional_edits.await.unwrap();
12334
12335 update_test_language_settings(&mut cx, |settings| {
12336 settings.defaults.completions = Some(CompletionSettings {
12337 words: WordsCompletionMode::Disabled,
12338 words_min_length: 0,
12339 // set the opposite here to ensure that the action is overriding the default behavior
12340 lsp_insert_mode: LspInsertMode::Replace,
12341 lsp: true,
12342 lsp_fetch_timeout_ms: 0,
12343 });
12344 });
12345
12346 cx.set_state(initial_state);
12347 cx.update_editor(|editor, window, cx| {
12348 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12349 });
12350 handle_completion_request_with_insert_and_replace(
12351 &mut cx,
12352 buffer_marked_text,
12353 vec![(completion_text, completion_text)],
12354 counter.clone(),
12355 )
12356 .await;
12357 cx.condition(|editor, _| editor.context_menu_visible())
12358 .await;
12359 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12360
12361 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12362 editor
12363 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12364 .unwrap()
12365 });
12366 cx.assert_editor_state(expected_with_insert_mode);
12367 handle_resolve_completion_request(&mut cx, None).await;
12368 apply_additional_edits.await.unwrap();
12369}
12370
12371#[gpui::test]
12372async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12373 init_test(cx, |_| {});
12374 let mut cx = EditorLspTestContext::new_rust(
12375 lsp::ServerCapabilities {
12376 completion_provider: Some(lsp::CompletionOptions {
12377 resolve_provider: Some(true),
12378 ..Default::default()
12379 }),
12380 ..Default::default()
12381 },
12382 cx,
12383 )
12384 .await;
12385
12386 // scenario: surrounding text matches completion text
12387 let completion_text = "to_offset";
12388 let initial_state = indoc! {"
12389 1. buf.to_offˇsuffix
12390 2. buf.to_offˇsuf
12391 3. buf.to_offˇfix
12392 4. buf.to_offˇ
12393 5. into_offˇensive
12394 6. ˇsuffix
12395 7. let ˇ //
12396 8. aaˇzz
12397 9. buf.to_off«zzzzzˇ»suffix
12398 10. buf.«ˇzzzzz»suffix
12399 11. to_off«ˇzzzzz»
12400
12401 buf.to_offˇsuffix // newest cursor
12402 "};
12403 let completion_marked_buffer = indoc! {"
12404 1. buf.to_offsuffix
12405 2. buf.to_offsuf
12406 3. buf.to_offfix
12407 4. buf.to_off
12408 5. into_offensive
12409 6. suffix
12410 7. let //
12411 8. aazz
12412 9. buf.to_offzzzzzsuffix
12413 10. buf.zzzzzsuffix
12414 11. to_offzzzzz
12415
12416 buf.<to_off|suffix> // newest cursor
12417 "};
12418 let expected = indoc! {"
12419 1. buf.to_offsetˇ
12420 2. buf.to_offsetˇsuf
12421 3. buf.to_offsetˇfix
12422 4. buf.to_offsetˇ
12423 5. into_offsetˇensive
12424 6. to_offsetˇsuffix
12425 7. let to_offsetˇ //
12426 8. aato_offsetˇzz
12427 9. buf.to_offsetˇ
12428 10. buf.to_offsetˇsuffix
12429 11. to_offsetˇ
12430
12431 buf.to_offsetˇ // newest cursor
12432 "};
12433 cx.set_state(initial_state);
12434 cx.update_editor(|editor, window, cx| {
12435 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12436 });
12437 handle_completion_request_with_insert_and_replace(
12438 &mut cx,
12439 completion_marked_buffer,
12440 vec![(completion_text, completion_text)],
12441 Arc::new(AtomicUsize::new(0)),
12442 )
12443 .await;
12444 cx.condition(|editor, _| editor.context_menu_visible())
12445 .await;
12446 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12447 editor
12448 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12449 .unwrap()
12450 });
12451 cx.assert_editor_state(expected);
12452 handle_resolve_completion_request(&mut cx, None).await;
12453 apply_additional_edits.await.unwrap();
12454
12455 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12456 let completion_text = "foo_and_bar";
12457 let initial_state = indoc! {"
12458 1. ooanbˇ
12459 2. zooanbˇ
12460 3. ooanbˇz
12461 4. zooanbˇz
12462 5. ooanˇ
12463 6. oanbˇ
12464
12465 ooanbˇ
12466 "};
12467 let completion_marked_buffer = indoc! {"
12468 1. ooanb
12469 2. zooanb
12470 3. ooanbz
12471 4. zooanbz
12472 5. ooan
12473 6. oanb
12474
12475 <ooanb|>
12476 "};
12477 let expected = indoc! {"
12478 1. foo_and_barˇ
12479 2. zfoo_and_barˇ
12480 3. foo_and_barˇz
12481 4. zfoo_and_barˇz
12482 5. ooanfoo_and_barˇ
12483 6. oanbfoo_and_barˇ
12484
12485 foo_and_barˇ
12486 "};
12487 cx.set_state(initial_state);
12488 cx.update_editor(|editor, window, cx| {
12489 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12490 });
12491 handle_completion_request_with_insert_and_replace(
12492 &mut cx,
12493 completion_marked_buffer,
12494 vec![(completion_text, completion_text)],
12495 Arc::new(AtomicUsize::new(0)),
12496 )
12497 .await;
12498 cx.condition(|editor, _| editor.context_menu_visible())
12499 .await;
12500 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12501 editor
12502 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12503 .unwrap()
12504 });
12505 cx.assert_editor_state(expected);
12506 handle_resolve_completion_request(&mut cx, None).await;
12507 apply_additional_edits.await.unwrap();
12508
12509 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12510 // (expects the same as if it was inserted at the end)
12511 let completion_text = "foo_and_bar";
12512 let initial_state = indoc! {"
12513 1. ooˇanb
12514 2. zooˇanb
12515 3. ooˇanbz
12516 4. zooˇanbz
12517
12518 ooˇanb
12519 "};
12520 let completion_marked_buffer = indoc! {"
12521 1. ooanb
12522 2. zooanb
12523 3. ooanbz
12524 4. zooanbz
12525
12526 <oo|anb>
12527 "};
12528 let expected = indoc! {"
12529 1. foo_and_barˇ
12530 2. zfoo_and_barˇ
12531 3. foo_and_barˇz
12532 4. zfoo_and_barˇz
12533
12534 foo_and_barˇ
12535 "};
12536 cx.set_state(initial_state);
12537 cx.update_editor(|editor, window, cx| {
12538 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12539 });
12540 handle_completion_request_with_insert_and_replace(
12541 &mut cx,
12542 completion_marked_buffer,
12543 vec![(completion_text, completion_text)],
12544 Arc::new(AtomicUsize::new(0)),
12545 )
12546 .await;
12547 cx.condition(|editor, _| editor.context_menu_visible())
12548 .await;
12549 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12550 editor
12551 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12552 .unwrap()
12553 });
12554 cx.assert_editor_state(expected);
12555 handle_resolve_completion_request(&mut cx, None).await;
12556 apply_additional_edits.await.unwrap();
12557}
12558
12559// This used to crash
12560#[gpui::test]
12561async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12562 init_test(cx, |_| {});
12563
12564 let buffer_text = indoc! {"
12565 fn main() {
12566 10.satu;
12567
12568 //
12569 // separate cursors so they open in different excerpts (manually reproducible)
12570 //
12571
12572 10.satu20;
12573 }
12574 "};
12575 let multibuffer_text_with_selections = indoc! {"
12576 fn main() {
12577 10.satuˇ;
12578
12579 //
12580
12581 //
12582
12583 10.satuˇ20;
12584 }
12585 "};
12586 let expected_multibuffer = indoc! {"
12587 fn main() {
12588 10.saturating_sub()ˇ;
12589
12590 //
12591
12592 //
12593
12594 10.saturating_sub()ˇ;
12595 }
12596 "};
12597
12598 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12599 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12600
12601 let fs = FakeFs::new(cx.executor());
12602 fs.insert_tree(
12603 path!("/a"),
12604 json!({
12605 "main.rs": buffer_text,
12606 }),
12607 )
12608 .await;
12609
12610 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12611 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12612 language_registry.add(rust_lang());
12613 let mut fake_servers = language_registry.register_fake_lsp(
12614 "Rust",
12615 FakeLspAdapter {
12616 capabilities: lsp::ServerCapabilities {
12617 completion_provider: Some(lsp::CompletionOptions {
12618 resolve_provider: None,
12619 ..lsp::CompletionOptions::default()
12620 }),
12621 ..lsp::ServerCapabilities::default()
12622 },
12623 ..FakeLspAdapter::default()
12624 },
12625 );
12626 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12627 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12628 let buffer = project
12629 .update(cx, |project, cx| {
12630 project.open_local_buffer(path!("/a/main.rs"), cx)
12631 })
12632 .await
12633 .unwrap();
12634
12635 let multi_buffer = cx.new(|cx| {
12636 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12637 multi_buffer.push_excerpts(
12638 buffer.clone(),
12639 [ExcerptRange::new(0..first_excerpt_end)],
12640 cx,
12641 );
12642 multi_buffer.push_excerpts(
12643 buffer.clone(),
12644 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12645 cx,
12646 );
12647 multi_buffer
12648 });
12649
12650 let editor = workspace
12651 .update(cx, |_, window, cx| {
12652 cx.new(|cx| {
12653 Editor::new(
12654 EditorMode::Full {
12655 scale_ui_elements_with_buffer_font_size: false,
12656 show_active_line_background: false,
12657 sized_by_content: false,
12658 },
12659 multi_buffer.clone(),
12660 Some(project.clone()),
12661 window,
12662 cx,
12663 )
12664 })
12665 })
12666 .unwrap();
12667
12668 let pane = workspace
12669 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12670 .unwrap();
12671 pane.update_in(cx, |pane, window, cx| {
12672 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12673 });
12674
12675 let fake_server = fake_servers.next().await.unwrap();
12676
12677 editor.update_in(cx, |editor, window, cx| {
12678 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12679 s.select_ranges([
12680 Point::new(1, 11)..Point::new(1, 11),
12681 Point::new(7, 11)..Point::new(7, 11),
12682 ])
12683 });
12684
12685 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12686 });
12687
12688 editor.update_in(cx, |editor, window, cx| {
12689 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12690 });
12691
12692 fake_server
12693 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12694 let completion_item = lsp::CompletionItem {
12695 label: "saturating_sub()".into(),
12696 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12697 lsp::InsertReplaceEdit {
12698 new_text: "saturating_sub()".to_owned(),
12699 insert: lsp::Range::new(
12700 lsp::Position::new(7, 7),
12701 lsp::Position::new(7, 11),
12702 ),
12703 replace: lsp::Range::new(
12704 lsp::Position::new(7, 7),
12705 lsp::Position::new(7, 13),
12706 ),
12707 },
12708 )),
12709 ..lsp::CompletionItem::default()
12710 };
12711
12712 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12713 })
12714 .next()
12715 .await
12716 .unwrap();
12717
12718 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12719 .await;
12720
12721 editor
12722 .update_in(cx, |editor, window, cx| {
12723 editor
12724 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12725 .unwrap()
12726 })
12727 .await
12728 .unwrap();
12729
12730 editor.update(cx, |editor, cx| {
12731 assert_text_with_selections(editor, expected_multibuffer, cx);
12732 })
12733}
12734
12735#[gpui::test]
12736async fn test_completion(cx: &mut TestAppContext) {
12737 init_test(cx, |_| {});
12738
12739 let mut cx = EditorLspTestContext::new_rust(
12740 lsp::ServerCapabilities {
12741 completion_provider: Some(lsp::CompletionOptions {
12742 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12743 resolve_provider: Some(true),
12744 ..Default::default()
12745 }),
12746 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12747 ..Default::default()
12748 },
12749 cx,
12750 )
12751 .await;
12752 let counter = Arc::new(AtomicUsize::new(0));
12753
12754 cx.set_state(indoc! {"
12755 oneˇ
12756 two
12757 three
12758 "});
12759 cx.simulate_keystroke(".");
12760 handle_completion_request(
12761 indoc! {"
12762 one.|<>
12763 two
12764 three
12765 "},
12766 vec!["first_completion", "second_completion"],
12767 true,
12768 counter.clone(),
12769 &mut cx,
12770 )
12771 .await;
12772 cx.condition(|editor, _| editor.context_menu_visible())
12773 .await;
12774 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12775
12776 let _handler = handle_signature_help_request(
12777 &mut cx,
12778 lsp::SignatureHelp {
12779 signatures: vec![lsp::SignatureInformation {
12780 label: "test signature".to_string(),
12781 documentation: None,
12782 parameters: Some(vec![lsp::ParameterInformation {
12783 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12784 documentation: None,
12785 }]),
12786 active_parameter: None,
12787 }],
12788 active_signature: None,
12789 active_parameter: None,
12790 },
12791 );
12792 cx.update_editor(|editor, window, cx| {
12793 assert!(
12794 !editor.signature_help_state.is_shown(),
12795 "No signature help was called for"
12796 );
12797 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12798 });
12799 cx.run_until_parked();
12800 cx.update_editor(|editor, _, _| {
12801 assert!(
12802 !editor.signature_help_state.is_shown(),
12803 "No signature help should be shown when completions menu is open"
12804 );
12805 });
12806
12807 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12808 editor.context_menu_next(&Default::default(), window, cx);
12809 editor
12810 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12811 .unwrap()
12812 });
12813 cx.assert_editor_state(indoc! {"
12814 one.second_completionˇ
12815 two
12816 three
12817 "});
12818
12819 handle_resolve_completion_request(
12820 &mut cx,
12821 Some(vec![
12822 (
12823 //This overlaps with the primary completion edit which is
12824 //misbehavior from the LSP spec, test that we filter it out
12825 indoc! {"
12826 one.second_ˇcompletion
12827 two
12828 threeˇ
12829 "},
12830 "overlapping additional edit",
12831 ),
12832 (
12833 indoc! {"
12834 one.second_completion
12835 two
12836 threeˇ
12837 "},
12838 "\nadditional edit",
12839 ),
12840 ]),
12841 )
12842 .await;
12843 apply_additional_edits.await.unwrap();
12844 cx.assert_editor_state(indoc! {"
12845 one.second_completionˇ
12846 two
12847 three
12848 additional edit
12849 "});
12850
12851 cx.set_state(indoc! {"
12852 one.second_completion
12853 twoˇ
12854 threeˇ
12855 additional edit
12856 "});
12857 cx.simulate_keystroke(" ");
12858 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12859 cx.simulate_keystroke("s");
12860 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12861
12862 cx.assert_editor_state(indoc! {"
12863 one.second_completion
12864 two sˇ
12865 three sˇ
12866 additional edit
12867 "});
12868 handle_completion_request(
12869 indoc! {"
12870 one.second_completion
12871 two s
12872 three <s|>
12873 additional edit
12874 "},
12875 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12876 true,
12877 counter.clone(),
12878 &mut cx,
12879 )
12880 .await;
12881 cx.condition(|editor, _| editor.context_menu_visible())
12882 .await;
12883 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12884
12885 cx.simulate_keystroke("i");
12886
12887 handle_completion_request(
12888 indoc! {"
12889 one.second_completion
12890 two si
12891 three <si|>
12892 additional edit
12893 "},
12894 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12895 true,
12896 counter.clone(),
12897 &mut cx,
12898 )
12899 .await;
12900 cx.condition(|editor, _| editor.context_menu_visible())
12901 .await;
12902 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12903
12904 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12905 editor
12906 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12907 .unwrap()
12908 });
12909 cx.assert_editor_state(indoc! {"
12910 one.second_completion
12911 two sixth_completionˇ
12912 three sixth_completionˇ
12913 additional edit
12914 "});
12915
12916 apply_additional_edits.await.unwrap();
12917
12918 update_test_language_settings(&mut cx, |settings| {
12919 settings.defaults.show_completions_on_input = Some(false);
12920 });
12921 cx.set_state("editorˇ");
12922 cx.simulate_keystroke(".");
12923 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12924 cx.simulate_keystrokes("c l o");
12925 cx.assert_editor_state("editor.cloˇ");
12926 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12927 cx.update_editor(|editor, window, cx| {
12928 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12929 });
12930 handle_completion_request(
12931 "editor.<clo|>",
12932 vec!["close", "clobber"],
12933 true,
12934 counter.clone(),
12935 &mut cx,
12936 )
12937 .await;
12938 cx.condition(|editor, _| editor.context_menu_visible())
12939 .await;
12940 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12941
12942 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12943 editor
12944 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12945 .unwrap()
12946 });
12947 cx.assert_editor_state("editor.clobberˇ");
12948 handle_resolve_completion_request(&mut cx, None).await;
12949 apply_additional_edits.await.unwrap();
12950}
12951
12952#[gpui::test]
12953async fn test_completion_reuse(cx: &mut TestAppContext) {
12954 init_test(cx, |_| {});
12955
12956 let mut cx = EditorLspTestContext::new_rust(
12957 lsp::ServerCapabilities {
12958 completion_provider: Some(lsp::CompletionOptions {
12959 trigger_characters: Some(vec![".".to_string()]),
12960 ..Default::default()
12961 }),
12962 ..Default::default()
12963 },
12964 cx,
12965 )
12966 .await;
12967
12968 let counter = Arc::new(AtomicUsize::new(0));
12969 cx.set_state("objˇ");
12970 cx.simulate_keystroke(".");
12971
12972 // Initial completion request returns complete results
12973 let is_incomplete = false;
12974 handle_completion_request(
12975 "obj.|<>",
12976 vec!["a", "ab", "abc"],
12977 is_incomplete,
12978 counter.clone(),
12979 &mut cx,
12980 )
12981 .await;
12982 cx.run_until_parked();
12983 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12984 cx.assert_editor_state("obj.ˇ");
12985 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12986
12987 // Type "a" - filters existing completions
12988 cx.simulate_keystroke("a");
12989 cx.run_until_parked();
12990 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12991 cx.assert_editor_state("obj.aˇ");
12992 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12993
12994 // Type "b" - filters existing completions
12995 cx.simulate_keystroke("b");
12996 cx.run_until_parked();
12997 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12998 cx.assert_editor_state("obj.abˇ");
12999 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13000
13001 // Type "c" - filters existing completions
13002 cx.simulate_keystroke("c");
13003 cx.run_until_parked();
13004 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13005 cx.assert_editor_state("obj.abcˇ");
13006 check_displayed_completions(vec!["abc"], &mut cx);
13007
13008 // Backspace to delete "c" - filters existing completions
13009 cx.update_editor(|editor, window, cx| {
13010 editor.backspace(&Backspace, window, cx);
13011 });
13012 cx.run_until_parked();
13013 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13014 cx.assert_editor_state("obj.abˇ");
13015 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13016
13017 // Moving cursor to the left dismisses menu.
13018 cx.update_editor(|editor, window, cx| {
13019 editor.move_left(&MoveLeft, window, cx);
13020 });
13021 cx.run_until_parked();
13022 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13023 cx.assert_editor_state("obj.aˇb");
13024 cx.update_editor(|editor, _, _| {
13025 assert_eq!(editor.context_menu_visible(), false);
13026 });
13027
13028 // Type "b" - new request
13029 cx.simulate_keystroke("b");
13030 let is_incomplete = false;
13031 handle_completion_request(
13032 "obj.<ab|>a",
13033 vec!["ab", "abc"],
13034 is_incomplete,
13035 counter.clone(),
13036 &mut cx,
13037 )
13038 .await;
13039 cx.run_until_parked();
13040 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13041 cx.assert_editor_state("obj.abˇb");
13042 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13043
13044 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13045 cx.update_editor(|editor, window, cx| {
13046 editor.backspace(&Backspace, window, cx);
13047 });
13048 let is_incomplete = false;
13049 handle_completion_request(
13050 "obj.<a|>b",
13051 vec!["a", "ab", "abc"],
13052 is_incomplete,
13053 counter.clone(),
13054 &mut cx,
13055 )
13056 .await;
13057 cx.run_until_parked();
13058 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13059 cx.assert_editor_state("obj.aˇb");
13060 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13061
13062 // Backspace to delete "a" - dismisses menu.
13063 cx.update_editor(|editor, window, cx| {
13064 editor.backspace(&Backspace, window, cx);
13065 });
13066 cx.run_until_parked();
13067 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13068 cx.assert_editor_state("obj.ˇb");
13069 cx.update_editor(|editor, _, _| {
13070 assert_eq!(editor.context_menu_visible(), false);
13071 });
13072}
13073
13074#[gpui::test]
13075async fn test_word_completion(cx: &mut TestAppContext) {
13076 let lsp_fetch_timeout_ms = 10;
13077 init_test(cx, |language_settings| {
13078 language_settings.defaults.completions = Some(CompletionSettings {
13079 words: WordsCompletionMode::Fallback,
13080 words_min_length: 0,
13081 lsp: true,
13082 lsp_fetch_timeout_ms: 10,
13083 lsp_insert_mode: LspInsertMode::Insert,
13084 });
13085 });
13086
13087 let mut cx = EditorLspTestContext::new_rust(
13088 lsp::ServerCapabilities {
13089 completion_provider: Some(lsp::CompletionOptions {
13090 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13091 ..lsp::CompletionOptions::default()
13092 }),
13093 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13094 ..lsp::ServerCapabilities::default()
13095 },
13096 cx,
13097 )
13098 .await;
13099
13100 let throttle_completions = Arc::new(AtomicBool::new(false));
13101
13102 let lsp_throttle_completions = throttle_completions.clone();
13103 let _completion_requests_handler =
13104 cx.lsp
13105 .server
13106 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13107 let lsp_throttle_completions = lsp_throttle_completions.clone();
13108 let cx = cx.clone();
13109 async move {
13110 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13111 cx.background_executor()
13112 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13113 .await;
13114 }
13115 Ok(Some(lsp::CompletionResponse::Array(vec![
13116 lsp::CompletionItem {
13117 label: "first".into(),
13118 ..lsp::CompletionItem::default()
13119 },
13120 lsp::CompletionItem {
13121 label: "last".into(),
13122 ..lsp::CompletionItem::default()
13123 },
13124 ])))
13125 }
13126 });
13127
13128 cx.set_state(indoc! {"
13129 oneˇ
13130 two
13131 three
13132 "});
13133 cx.simulate_keystroke(".");
13134 cx.executor().run_until_parked();
13135 cx.condition(|editor, _| editor.context_menu_visible())
13136 .await;
13137 cx.update_editor(|editor, window, cx| {
13138 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13139 {
13140 assert_eq!(
13141 completion_menu_entries(menu),
13142 &["first", "last"],
13143 "When LSP server is fast to reply, no fallback word completions are used"
13144 );
13145 } else {
13146 panic!("expected completion menu to be open");
13147 }
13148 editor.cancel(&Cancel, window, cx);
13149 });
13150 cx.executor().run_until_parked();
13151 cx.condition(|editor, _| !editor.context_menu_visible())
13152 .await;
13153
13154 throttle_completions.store(true, atomic::Ordering::Release);
13155 cx.simulate_keystroke(".");
13156 cx.executor()
13157 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
13158 cx.executor().run_until_parked();
13159 cx.condition(|editor, _| editor.context_menu_visible())
13160 .await;
13161 cx.update_editor(|editor, _, _| {
13162 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13163 {
13164 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
13165 "When LSP server is slow, document words can be shown instead, if configured accordingly");
13166 } else {
13167 panic!("expected completion menu to be open");
13168 }
13169 });
13170}
13171
13172#[gpui::test]
13173async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
13174 init_test(cx, |language_settings| {
13175 language_settings.defaults.completions = Some(CompletionSettings {
13176 words: WordsCompletionMode::Enabled,
13177 words_min_length: 0,
13178 lsp: true,
13179 lsp_fetch_timeout_ms: 0,
13180 lsp_insert_mode: LspInsertMode::Insert,
13181 });
13182 });
13183
13184 let mut cx = EditorLspTestContext::new_rust(
13185 lsp::ServerCapabilities {
13186 completion_provider: Some(lsp::CompletionOptions {
13187 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13188 ..lsp::CompletionOptions::default()
13189 }),
13190 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13191 ..lsp::ServerCapabilities::default()
13192 },
13193 cx,
13194 )
13195 .await;
13196
13197 let _completion_requests_handler =
13198 cx.lsp
13199 .server
13200 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13201 Ok(Some(lsp::CompletionResponse::Array(vec![
13202 lsp::CompletionItem {
13203 label: "first".into(),
13204 ..lsp::CompletionItem::default()
13205 },
13206 lsp::CompletionItem {
13207 label: "last".into(),
13208 ..lsp::CompletionItem::default()
13209 },
13210 ])))
13211 });
13212
13213 cx.set_state(indoc! {"ˇ
13214 first
13215 last
13216 second
13217 "});
13218 cx.simulate_keystroke(".");
13219 cx.executor().run_until_parked();
13220 cx.condition(|editor, _| editor.context_menu_visible())
13221 .await;
13222 cx.update_editor(|editor, _, _| {
13223 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13224 {
13225 assert_eq!(
13226 completion_menu_entries(menu),
13227 &["first", "last", "second"],
13228 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
13229 );
13230 } else {
13231 panic!("expected completion menu to be open");
13232 }
13233 });
13234}
13235
13236#[gpui::test]
13237async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
13238 init_test(cx, |language_settings| {
13239 language_settings.defaults.completions = Some(CompletionSettings {
13240 words: WordsCompletionMode::Disabled,
13241 words_min_length: 0,
13242 lsp: true,
13243 lsp_fetch_timeout_ms: 0,
13244 lsp_insert_mode: LspInsertMode::Insert,
13245 });
13246 });
13247
13248 let mut cx = EditorLspTestContext::new_rust(
13249 lsp::ServerCapabilities {
13250 completion_provider: Some(lsp::CompletionOptions {
13251 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13252 ..lsp::CompletionOptions::default()
13253 }),
13254 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13255 ..lsp::ServerCapabilities::default()
13256 },
13257 cx,
13258 )
13259 .await;
13260
13261 let _completion_requests_handler =
13262 cx.lsp
13263 .server
13264 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13265 panic!("LSP completions should not be queried when dealing with word completions")
13266 });
13267
13268 cx.set_state(indoc! {"ˇ
13269 first
13270 last
13271 second
13272 "});
13273 cx.update_editor(|editor, window, cx| {
13274 editor.show_word_completions(&ShowWordCompletions, window, cx);
13275 });
13276 cx.executor().run_until_parked();
13277 cx.condition(|editor, _| editor.context_menu_visible())
13278 .await;
13279 cx.update_editor(|editor, _, _| {
13280 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13281 {
13282 assert_eq!(
13283 completion_menu_entries(menu),
13284 &["first", "last", "second"],
13285 "`ShowWordCompletions` action should show word completions"
13286 );
13287 } else {
13288 panic!("expected completion menu to be open");
13289 }
13290 });
13291
13292 cx.simulate_keystroke("l");
13293 cx.executor().run_until_parked();
13294 cx.condition(|editor, _| editor.context_menu_visible())
13295 .await;
13296 cx.update_editor(|editor, _, _| {
13297 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13298 {
13299 assert_eq!(
13300 completion_menu_entries(menu),
13301 &["last"],
13302 "After showing word completions, further editing should filter them and not query the LSP"
13303 );
13304 } else {
13305 panic!("expected completion menu to be open");
13306 }
13307 });
13308}
13309
13310#[gpui::test]
13311async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13312 init_test(cx, |language_settings| {
13313 language_settings.defaults.completions = Some(CompletionSettings {
13314 words: WordsCompletionMode::Fallback,
13315 words_min_length: 0,
13316 lsp: false,
13317 lsp_fetch_timeout_ms: 0,
13318 lsp_insert_mode: LspInsertMode::Insert,
13319 });
13320 });
13321
13322 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13323
13324 cx.set_state(indoc! {"ˇ
13325 0_usize
13326 let
13327 33
13328 4.5f32
13329 "});
13330 cx.update_editor(|editor, window, cx| {
13331 editor.show_completions(&ShowCompletions::default(), window, cx);
13332 });
13333 cx.executor().run_until_parked();
13334 cx.condition(|editor, _| editor.context_menu_visible())
13335 .await;
13336 cx.update_editor(|editor, window, cx| {
13337 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13338 {
13339 assert_eq!(
13340 completion_menu_entries(menu),
13341 &["let"],
13342 "With no digits in the completion query, no digits should be in the word completions"
13343 );
13344 } else {
13345 panic!("expected completion menu to be open");
13346 }
13347 editor.cancel(&Cancel, window, cx);
13348 });
13349
13350 cx.set_state(indoc! {"3ˇ
13351 0_usize
13352 let
13353 3
13354 33.35f32
13355 "});
13356 cx.update_editor(|editor, window, cx| {
13357 editor.show_completions(&ShowCompletions::default(), window, cx);
13358 });
13359 cx.executor().run_until_parked();
13360 cx.condition(|editor, _| editor.context_menu_visible())
13361 .await;
13362 cx.update_editor(|editor, _, _| {
13363 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13364 {
13365 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
13366 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13367 } else {
13368 panic!("expected completion menu to be open");
13369 }
13370 });
13371}
13372
13373#[gpui::test]
13374async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
13375 init_test(cx, |language_settings| {
13376 language_settings.defaults.completions = Some(CompletionSettings {
13377 words: WordsCompletionMode::Enabled,
13378 words_min_length: 3,
13379 lsp: true,
13380 lsp_fetch_timeout_ms: 0,
13381 lsp_insert_mode: LspInsertMode::Insert,
13382 });
13383 });
13384
13385 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13386 cx.set_state(indoc! {"ˇ
13387 wow
13388 wowen
13389 wowser
13390 "});
13391 cx.simulate_keystroke("w");
13392 cx.executor().run_until_parked();
13393 cx.update_editor(|editor, _, _| {
13394 if editor.context_menu.borrow_mut().is_some() {
13395 panic!(
13396 "expected completion menu to be hidden, as words completion threshold is not met"
13397 );
13398 }
13399 });
13400
13401 cx.simulate_keystroke("o");
13402 cx.executor().run_until_parked();
13403 cx.update_editor(|editor, _, _| {
13404 if editor.context_menu.borrow_mut().is_some() {
13405 panic!(
13406 "expected completion menu to be hidden, as words completion threshold is not met still"
13407 );
13408 }
13409 });
13410
13411 cx.simulate_keystroke("w");
13412 cx.executor().run_until_parked();
13413 cx.update_editor(|editor, _, _| {
13414 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13415 {
13416 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
13417 } else {
13418 panic!("expected completion menu to be open after the word completions threshold is met");
13419 }
13420 });
13421}
13422
13423fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13424 let position = || lsp::Position {
13425 line: params.text_document_position.position.line,
13426 character: params.text_document_position.position.character,
13427 };
13428 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13429 range: lsp::Range {
13430 start: position(),
13431 end: position(),
13432 },
13433 new_text: text.to_string(),
13434 }))
13435}
13436
13437#[gpui::test]
13438async fn test_multiline_completion(cx: &mut TestAppContext) {
13439 init_test(cx, |_| {});
13440
13441 let fs = FakeFs::new(cx.executor());
13442 fs.insert_tree(
13443 path!("/a"),
13444 json!({
13445 "main.ts": "a",
13446 }),
13447 )
13448 .await;
13449
13450 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13451 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13452 let typescript_language = Arc::new(Language::new(
13453 LanguageConfig {
13454 name: "TypeScript".into(),
13455 matcher: LanguageMatcher {
13456 path_suffixes: vec!["ts".to_string()],
13457 ..LanguageMatcher::default()
13458 },
13459 line_comments: vec!["// ".into()],
13460 ..LanguageConfig::default()
13461 },
13462 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13463 ));
13464 language_registry.add(typescript_language.clone());
13465 let mut fake_servers = language_registry.register_fake_lsp(
13466 "TypeScript",
13467 FakeLspAdapter {
13468 capabilities: lsp::ServerCapabilities {
13469 completion_provider: Some(lsp::CompletionOptions {
13470 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13471 ..lsp::CompletionOptions::default()
13472 }),
13473 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13474 ..lsp::ServerCapabilities::default()
13475 },
13476 // Emulate vtsls label generation
13477 label_for_completion: Some(Box::new(|item, _| {
13478 let text = if let Some(description) = item
13479 .label_details
13480 .as_ref()
13481 .and_then(|label_details| label_details.description.as_ref())
13482 {
13483 format!("{} {}", item.label, description)
13484 } else if let Some(detail) = &item.detail {
13485 format!("{} {}", item.label, detail)
13486 } else {
13487 item.label.clone()
13488 };
13489 let len = text.len();
13490 Some(language::CodeLabel {
13491 text,
13492 runs: Vec::new(),
13493 filter_range: 0..len,
13494 })
13495 })),
13496 ..FakeLspAdapter::default()
13497 },
13498 );
13499 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13500 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13501 let worktree_id = workspace
13502 .update(cx, |workspace, _window, cx| {
13503 workspace.project().update(cx, |project, cx| {
13504 project.worktrees(cx).next().unwrap().read(cx).id()
13505 })
13506 })
13507 .unwrap();
13508 let _buffer = project
13509 .update(cx, |project, cx| {
13510 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13511 })
13512 .await
13513 .unwrap();
13514 let editor = workspace
13515 .update(cx, |workspace, window, cx| {
13516 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13517 })
13518 .unwrap()
13519 .await
13520 .unwrap()
13521 .downcast::<Editor>()
13522 .unwrap();
13523 let fake_server = fake_servers.next().await.unwrap();
13524
13525 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13526 let multiline_label_2 = "a\nb\nc\n";
13527 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13528 let multiline_description = "d\ne\nf\n";
13529 let multiline_detail_2 = "g\nh\ni\n";
13530
13531 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13532 move |params, _| async move {
13533 Ok(Some(lsp::CompletionResponse::Array(vec![
13534 lsp::CompletionItem {
13535 label: multiline_label.to_string(),
13536 text_edit: gen_text_edit(¶ms, "new_text_1"),
13537 ..lsp::CompletionItem::default()
13538 },
13539 lsp::CompletionItem {
13540 label: "single line label 1".to_string(),
13541 detail: Some(multiline_detail.to_string()),
13542 text_edit: gen_text_edit(¶ms, "new_text_2"),
13543 ..lsp::CompletionItem::default()
13544 },
13545 lsp::CompletionItem {
13546 label: "single line label 2".to_string(),
13547 label_details: Some(lsp::CompletionItemLabelDetails {
13548 description: Some(multiline_description.to_string()),
13549 detail: None,
13550 }),
13551 text_edit: gen_text_edit(¶ms, "new_text_2"),
13552 ..lsp::CompletionItem::default()
13553 },
13554 lsp::CompletionItem {
13555 label: multiline_label_2.to_string(),
13556 detail: Some(multiline_detail_2.to_string()),
13557 text_edit: gen_text_edit(¶ms, "new_text_3"),
13558 ..lsp::CompletionItem::default()
13559 },
13560 lsp::CompletionItem {
13561 label: "Label with many spaces and \t but without newlines".to_string(),
13562 detail: Some(
13563 "Details with many spaces and \t but without newlines".to_string(),
13564 ),
13565 text_edit: gen_text_edit(¶ms, "new_text_4"),
13566 ..lsp::CompletionItem::default()
13567 },
13568 ])))
13569 },
13570 );
13571
13572 editor.update_in(cx, |editor, window, cx| {
13573 cx.focus_self(window);
13574 editor.move_to_end(&MoveToEnd, window, cx);
13575 editor.handle_input(".", window, cx);
13576 });
13577 cx.run_until_parked();
13578 completion_handle.next().await.unwrap();
13579
13580 editor.update(cx, |editor, _| {
13581 assert!(editor.context_menu_visible());
13582 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13583 {
13584 let completion_labels = menu
13585 .completions
13586 .borrow()
13587 .iter()
13588 .map(|c| c.label.text.clone())
13589 .collect::<Vec<_>>();
13590 assert_eq!(
13591 completion_labels,
13592 &[
13593 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13594 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13595 "single line label 2 d e f ",
13596 "a b c g h i ",
13597 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13598 ],
13599 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13600 );
13601
13602 for completion in menu
13603 .completions
13604 .borrow()
13605 .iter() {
13606 assert_eq!(
13607 completion.label.filter_range,
13608 0..completion.label.text.len(),
13609 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13610 );
13611 }
13612 } else {
13613 panic!("expected completion menu to be open");
13614 }
13615 });
13616}
13617
13618#[gpui::test]
13619async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13620 init_test(cx, |_| {});
13621 let mut cx = EditorLspTestContext::new_rust(
13622 lsp::ServerCapabilities {
13623 completion_provider: Some(lsp::CompletionOptions {
13624 trigger_characters: Some(vec![".".to_string()]),
13625 ..Default::default()
13626 }),
13627 ..Default::default()
13628 },
13629 cx,
13630 )
13631 .await;
13632 cx.lsp
13633 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13634 Ok(Some(lsp::CompletionResponse::Array(vec![
13635 lsp::CompletionItem {
13636 label: "first".into(),
13637 ..Default::default()
13638 },
13639 lsp::CompletionItem {
13640 label: "last".into(),
13641 ..Default::default()
13642 },
13643 ])))
13644 });
13645 cx.set_state("variableˇ");
13646 cx.simulate_keystroke(".");
13647 cx.executor().run_until_parked();
13648
13649 cx.update_editor(|editor, _, _| {
13650 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13651 {
13652 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
13653 } else {
13654 panic!("expected completion menu to be open");
13655 }
13656 });
13657
13658 cx.update_editor(|editor, window, cx| {
13659 editor.move_page_down(&MovePageDown::default(), window, cx);
13660 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13661 {
13662 assert!(
13663 menu.selected_item == 1,
13664 "expected PageDown to select the last item from the context menu"
13665 );
13666 } else {
13667 panic!("expected completion menu to stay open after PageDown");
13668 }
13669 });
13670
13671 cx.update_editor(|editor, window, cx| {
13672 editor.move_page_up(&MovePageUp::default(), window, cx);
13673 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13674 {
13675 assert!(
13676 menu.selected_item == 0,
13677 "expected PageUp to select the first item from the context menu"
13678 );
13679 } else {
13680 panic!("expected completion menu to stay open after PageUp");
13681 }
13682 });
13683}
13684
13685#[gpui::test]
13686async fn test_as_is_completions(cx: &mut TestAppContext) {
13687 init_test(cx, |_| {});
13688 let mut cx = EditorLspTestContext::new_rust(
13689 lsp::ServerCapabilities {
13690 completion_provider: Some(lsp::CompletionOptions {
13691 ..Default::default()
13692 }),
13693 ..Default::default()
13694 },
13695 cx,
13696 )
13697 .await;
13698 cx.lsp
13699 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13700 Ok(Some(lsp::CompletionResponse::Array(vec![
13701 lsp::CompletionItem {
13702 label: "unsafe".into(),
13703 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13704 range: lsp::Range {
13705 start: lsp::Position {
13706 line: 1,
13707 character: 2,
13708 },
13709 end: lsp::Position {
13710 line: 1,
13711 character: 3,
13712 },
13713 },
13714 new_text: "unsafe".to_string(),
13715 })),
13716 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13717 ..Default::default()
13718 },
13719 ])))
13720 });
13721 cx.set_state("fn a() {}\n nˇ");
13722 cx.executor().run_until_parked();
13723 cx.update_editor(|editor, window, cx| {
13724 editor.show_completions(
13725 &ShowCompletions {
13726 trigger: Some("\n".into()),
13727 },
13728 window,
13729 cx,
13730 );
13731 });
13732 cx.executor().run_until_parked();
13733
13734 cx.update_editor(|editor, window, cx| {
13735 editor.confirm_completion(&Default::default(), window, cx)
13736 });
13737 cx.executor().run_until_parked();
13738 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13739}
13740
13741#[gpui::test]
13742async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13743 init_test(cx, |_| {});
13744 let language =
13745 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13746 let mut cx = EditorLspTestContext::new(
13747 language,
13748 lsp::ServerCapabilities {
13749 completion_provider: Some(lsp::CompletionOptions {
13750 ..lsp::CompletionOptions::default()
13751 }),
13752 ..lsp::ServerCapabilities::default()
13753 },
13754 cx,
13755 )
13756 .await;
13757
13758 cx.set_state(
13759 "#ifndef BAR_H
13760#define BAR_H
13761
13762#include <stdbool.h>
13763
13764int fn_branch(bool do_branch1, bool do_branch2);
13765
13766#endif // BAR_H
13767ˇ",
13768 );
13769 cx.executor().run_until_parked();
13770 cx.update_editor(|editor, window, cx| {
13771 editor.handle_input("#", window, cx);
13772 });
13773 cx.executor().run_until_parked();
13774 cx.update_editor(|editor, window, cx| {
13775 editor.handle_input("i", window, cx);
13776 });
13777 cx.executor().run_until_parked();
13778 cx.update_editor(|editor, window, cx| {
13779 editor.handle_input("n", window, cx);
13780 });
13781 cx.executor().run_until_parked();
13782 cx.assert_editor_state(
13783 "#ifndef BAR_H
13784#define BAR_H
13785
13786#include <stdbool.h>
13787
13788int fn_branch(bool do_branch1, bool do_branch2);
13789
13790#endif // BAR_H
13791#inˇ",
13792 );
13793
13794 cx.lsp
13795 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13796 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13797 is_incomplete: false,
13798 item_defaults: None,
13799 items: vec![lsp::CompletionItem {
13800 kind: Some(lsp::CompletionItemKind::SNIPPET),
13801 label_details: Some(lsp::CompletionItemLabelDetails {
13802 detail: Some("header".to_string()),
13803 description: None,
13804 }),
13805 label: " include".to_string(),
13806 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13807 range: lsp::Range {
13808 start: lsp::Position {
13809 line: 8,
13810 character: 1,
13811 },
13812 end: lsp::Position {
13813 line: 8,
13814 character: 1,
13815 },
13816 },
13817 new_text: "include \"$0\"".to_string(),
13818 })),
13819 sort_text: Some("40b67681include".to_string()),
13820 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13821 filter_text: Some("include".to_string()),
13822 insert_text: Some("include \"$0\"".to_string()),
13823 ..lsp::CompletionItem::default()
13824 }],
13825 })))
13826 });
13827 cx.update_editor(|editor, window, cx| {
13828 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13829 });
13830 cx.executor().run_until_parked();
13831 cx.update_editor(|editor, window, cx| {
13832 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13833 });
13834 cx.executor().run_until_parked();
13835 cx.assert_editor_state(
13836 "#ifndef BAR_H
13837#define BAR_H
13838
13839#include <stdbool.h>
13840
13841int fn_branch(bool do_branch1, bool do_branch2);
13842
13843#endif // BAR_H
13844#include \"ˇ\"",
13845 );
13846
13847 cx.lsp
13848 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13849 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13850 is_incomplete: true,
13851 item_defaults: None,
13852 items: vec![lsp::CompletionItem {
13853 kind: Some(lsp::CompletionItemKind::FILE),
13854 label: "AGL/".to_string(),
13855 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13856 range: lsp::Range {
13857 start: lsp::Position {
13858 line: 8,
13859 character: 10,
13860 },
13861 end: lsp::Position {
13862 line: 8,
13863 character: 11,
13864 },
13865 },
13866 new_text: "AGL/".to_string(),
13867 })),
13868 sort_text: Some("40b67681AGL/".to_string()),
13869 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13870 filter_text: Some("AGL/".to_string()),
13871 insert_text: Some("AGL/".to_string()),
13872 ..lsp::CompletionItem::default()
13873 }],
13874 })))
13875 });
13876 cx.update_editor(|editor, window, cx| {
13877 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13878 });
13879 cx.executor().run_until_parked();
13880 cx.update_editor(|editor, window, cx| {
13881 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13882 });
13883 cx.executor().run_until_parked();
13884 cx.assert_editor_state(
13885 r##"#ifndef BAR_H
13886#define BAR_H
13887
13888#include <stdbool.h>
13889
13890int fn_branch(bool do_branch1, bool do_branch2);
13891
13892#endif // BAR_H
13893#include "AGL/ˇ"##,
13894 );
13895
13896 cx.update_editor(|editor, window, cx| {
13897 editor.handle_input("\"", window, cx);
13898 });
13899 cx.executor().run_until_parked();
13900 cx.assert_editor_state(
13901 r##"#ifndef BAR_H
13902#define BAR_H
13903
13904#include <stdbool.h>
13905
13906int fn_branch(bool do_branch1, bool do_branch2);
13907
13908#endif // BAR_H
13909#include "AGL/"ˇ"##,
13910 );
13911}
13912
13913#[gpui::test]
13914async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13915 init_test(cx, |_| {});
13916
13917 let mut cx = EditorLspTestContext::new_rust(
13918 lsp::ServerCapabilities {
13919 completion_provider: Some(lsp::CompletionOptions {
13920 trigger_characters: Some(vec![".".to_string()]),
13921 resolve_provider: Some(true),
13922 ..Default::default()
13923 }),
13924 ..Default::default()
13925 },
13926 cx,
13927 )
13928 .await;
13929
13930 cx.set_state("fn main() { let a = 2ˇ; }");
13931 cx.simulate_keystroke(".");
13932 let completion_item = lsp::CompletionItem {
13933 label: "Some".into(),
13934 kind: Some(lsp::CompletionItemKind::SNIPPET),
13935 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13936 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13937 kind: lsp::MarkupKind::Markdown,
13938 value: "```rust\nSome(2)\n```".to_string(),
13939 })),
13940 deprecated: Some(false),
13941 sort_text: Some("Some".to_string()),
13942 filter_text: Some("Some".to_string()),
13943 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13944 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13945 range: lsp::Range {
13946 start: lsp::Position {
13947 line: 0,
13948 character: 22,
13949 },
13950 end: lsp::Position {
13951 line: 0,
13952 character: 22,
13953 },
13954 },
13955 new_text: "Some(2)".to_string(),
13956 })),
13957 additional_text_edits: Some(vec![lsp::TextEdit {
13958 range: lsp::Range {
13959 start: lsp::Position {
13960 line: 0,
13961 character: 20,
13962 },
13963 end: lsp::Position {
13964 line: 0,
13965 character: 22,
13966 },
13967 },
13968 new_text: "".to_string(),
13969 }]),
13970 ..Default::default()
13971 };
13972
13973 let closure_completion_item = completion_item.clone();
13974 let counter = Arc::new(AtomicUsize::new(0));
13975 let counter_clone = counter.clone();
13976 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13977 let task_completion_item = closure_completion_item.clone();
13978 counter_clone.fetch_add(1, atomic::Ordering::Release);
13979 async move {
13980 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13981 is_incomplete: true,
13982 item_defaults: None,
13983 items: vec![task_completion_item],
13984 })))
13985 }
13986 });
13987
13988 cx.condition(|editor, _| editor.context_menu_visible())
13989 .await;
13990 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13991 assert!(request.next().await.is_some());
13992 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13993
13994 cx.simulate_keystrokes("S o m");
13995 cx.condition(|editor, _| editor.context_menu_visible())
13996 .await;
13997 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13998 assert!(request.next().await.is_some());
13999 assert!(request.next().await.is_some());
14000 assert!(request.next().await.is_some());
14001 request.close();
14002 assert!(request.next().await.is_none());
14003 assert_eq!(
14004 counter.load(atomic::Ordering::Acquire),
14005 4,
14006 "With the completions menu open, only one LSP request should happen per input"
14007 );
14008}
14009
14010#[gpui::test]
14011async fn test_toggle_comment(cx: &mut TestAppContext) {
14012 init_test(cx, |_| {});
14013 let mut cx = EditorTestContext::new(cx).await;
14014 let language = Arc::new(Language::new(
14015 LanguageConfig {
14016 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14017 ..Default::default()
14018 },
14019 Some(tree_sitter_rust::LANGUAGE.into()),
14020 ));
14021 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14022
14023 // If multiple selections intersect a line, the line is only toggled once.
14024 cx.set_state(indoc! {"
14025 fn a() {
14026 «//b();
14027 ˇ»// «c();
14028 //ˇ» d();
14029 }
14030 "});
14031
14032 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14033
14034 cx.assert_editor_state(indoc! {"
14035 fn a() {
14036 «b();
14037 c();
14038 ˇ» d();
14039 }
14040 "});
14041
14042 // The comment prefix is inserted at the same column for every line in a
14043 // selection.
14044 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14045
14046 cx.assert_editor_state(indoc! {"
14047 fn a() {
14048 // «b();
14049 // c();
14050 ˇ»// d();
14051 }
14052 "});
14053
14054 // If a selection ends at the beginning of a line, that line is not toggled.
14055 cx.set_selections_state(indoc! {"
14056 fn a() {
14057 // b();
14058 «// c();
14059 ˇ» // d();
14060 }
14061 "});
14062
14063 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14064
14065 cx.assert_editor_state(indoc! {"
14066 fn a() {
14067 // b();
14068 «c();
14069 ˇ» // d();
14070 }
14071 "});
14072
14073 // If a selection span a single line and is empty, the line is toggled.
14074 cx.set_state(indoc! {"
14075 fn a() {
14076 a();
14077 b();
14078 ˇ
14079 }
14080 "});
14081
14082 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14083
14084 cx.assert_editor_state(indoc! {"
14085 fn a() {
14086 a();
14087 b();
14088 //•ˇ
14089 }
14090 "});
14091
14092 // If a selection span multiple lines, empty lines are not toggled.
14093 cx.set_state(indoc! {"
14094 fn a() {
14095 «a();
14096
14097 c();ˇ»
14098 }
14099 "});
14100
14101 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14102
14103 cx.assert_editor_state(indoc! {"
14104 fn a() {
14105 // «a();
14106
14107 // c();ˇ»
14108 }
14109 "});
14110
14111 // If a selection includes multiple comment prefixes, all lines are uncommented.
14112 cx.set_state(indoc! {"
14113 fn a() {
14114 «// a();
14115 /// b();
14116 //! c();ˇ»
14117 }
14118 "});
14119
14120 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14121
14122 cx.assert_editor_state(indoc! {"
14123 fn a() {
14124 «a();
14125 b();
14126 c();ˇ»
14127 }
14128 "});
14129}
14130
14131#[gpui::test]
14132async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
14133 init_test(cx, |_| {});
14134 let mut cx = EditorTestContext::new(cx).await;
14135 let language = Arc::new(Language::new(
14136 LanguageConfig {
14137 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14138 ..Default::default()
14139 },
14140 Some(tree_sitter_rust::LANGUAGE.into()),
14141 ));
14142 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14143
14144 let toggle_comments = &ToggleComments {
14145 advance_downwards: false,
14146 ignore_indent: true,
14147 };
14148
14149 // If multiple selections intersect a line, the line is only toggled once.
14150 cx.set_state(indoc! {"
14151 fn a() {
14152 // «b();
14153 // c();
14154 // ˇ» d();
14155 }
14156 "});
14157
14158 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14159
14160 cx.assert_editor_state(indoc! {"
14161 fn a() {
14162 «b();
14163 c();
14164 ˇ» d();
14165 }
14166 "});
14167
14168 // The comment prefix is inserted at the beginning of each line
14169 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14170
14171 cx.assert_editor_state(indoc! {"
14172 fn a() {
14173 // «b();
14174 // c();
14175 // ˇ» d();
14176 }
14177 "});
14178
14179 // If a selection ends at the beginning of a line, that line is not toggled.
14180 cx.set_selections_state(indoc! {"
14181 fn a() {
14182 // b();
14183 // «c();
14184 ˇ»// d();
14185 }
14186 "});
14187
14188 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14189
14190 cx.assert_editor_state(indoc! {"
14191 fn a() {
14192 // b();
14193 «c();
14194 ˇ»// d();
14195 }
14196 "});
14197
14198 // If a selection span a single line and is empty, the line is toggled.
14199 cx.set_state(indoc! {"
14200 fn a() {
14201 a();
14202 b();
14203 ˇ
14204 }
14205 "});
14206
14207 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14208
14209 cx.assert_editor_state(indoc! {"
14210 fn a() {
14211 a();
14212 b();
14213 //ˇ
14214 }
14215 "});
14216
14217 // If a selection span multiple lines, empty lines are not toggled.
14218 cx.set_state(indoc! {"
14219 fn a() {
14220 «a();
14221
14222 c();ˇ»
14223 }
14224 "});
14225
14226 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14227
14228 cx.assert_editor_state(indoc! {"
14229 fn a() {
14230 // «a();
14231
14232 // c();ˇ»
14233 }
14234 "});
14235
14236 // If a selection includes multiple comment prefixes, all lines are uncommented.
14237 cx.set_state(indoc! {"
14238 fn a() {
14239 // «a();
14240 /// b();
14241 //! c();ˇ»
14242 }
14243 "});
14244
14245 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14246
14247 cx.assert_editor_state(indoc! {"
14248 fn a() {
14249 «a();
14250 b();
14251 c();ˇ»
14252 }
14253 "});
14254}
14255
14256#[gpui::test]
14257async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
14258 init_test(cx, |_| {});
14259
14260 let language = Arc::new(Language::new(
14261 LanguageConfig {
14262 line_comments: vec!["// ".into()],
14263 ..Default::default()
14264 },
14265 Some(tree_sitter_rust::LANGUAGE.into()),
14266 ));
14267
14268 let mut cx = EditorTestContext::new(cx).await;
14269
14270 cx.language_registry().add(language.clone());
14271 cx.update_buffer(|buffer, cx| {
14272 buffer.set_language(Some(language), cx);
14273 });
14274
14275 let toggle_comments = &ToggleComments {
14276 advance_downwards: true,
14277 ignore_indent: false,
14278 };
14279
14280 // Single cursor on one line -> advance
14281 // Cursor moves horizontally 3 characters as well on non-blank line
14282 cx.set_state(indoc!(
14283 "fn a() {
14284 ˇdog();
14285 cat();
14286 }"
14287 ));
14288 cx.update_editor(|editor, window, cx| {
14289 editor.toggle_comments(toggle_comments, window, cx);
14290 });
14291 cx.assert_editor_state(indoc!(
14292 "fn a() {
14293 // dog();
14294 catˇ();
14295 }"
14296 ));
14297
14298 // Single selection on one line -> don't advance
14299 cx.set_state(indoc!(
14300 "fn a() {
14301 «dog()ˇ»;
14302 cat();
14303 }"
14304 ));
14305 cx.update_editor(|editor, window, cx| {
14306 editor.toggle_comments(toggle_comments, window, cx);
14307 });
14308 cx.assert_editor_state(indoc!(
14309 "fn a() {
14310 // «dog()ˇ»;
14311 cat();
14312 }"
14313 ));
14314
14315 // Multiple cursors on one line -> advance
14316 cx.set_state(indoc!(
14317 "fn a() {
14318 ˇdˇog();
14319 cat();
14320 }"
14321 ));
14322 cx.update_editor(|editor, window, cx| {
14323 editor.toggle_comments(toggle_comments, window, cx);
14324 });
14325 cx.assert_editor_state(indoc!(
14326 "fn a() {
14327 // dog();
14328 catˇ(ˇ);
14329 }"
14330 ));
14331
14332 // Multiple cursors on one line, with selection -> don't advance
14333 cx.set_state(indoc!(
14334 "fn a() {
14335 ˇdˇog«()ˇ»;
14336 cat();
14337 }"
14338 ));
14339 cx.update_editor(|editor, window, cx| {
14340 editor.toggle_comments(toggle_comments, window, cx);
14341 });
14342 cx.assert_editor_state(indoc!(
14343 "fn a() {
14344 // ˇdˇog«()ˇ»;
14345 cat();
14346 }"
14347 ));
14348
14349 // Single cursor on one line -> advance
14350 // Cursor moves to column 0 on blank line
14351 cx.set_state(indoc!(
14352 "fn a() {
14353 ˇdog();
14354
14355 cat();
14356 }"
14357 ));
14358 cx.update_editor(|editor, window, cx| {
14359 editor.toggle_comments(toggle_comments, window, cx);
14360 });
14361 cx.assert_editor_state(indoc!(
14362 "fn a() {
14363 // dog();
14364 ˇ
14365 cat();
14366 }"
14367 ));
14368
14369 // Single cursor on one line -> advance
14370 // Cursor starts and ends at column 0
14371 cx.set_state(indoc!(
14372 "fn a() {
14373 ˇ dog();
14374 cat();
14375 }"
14376 ));
14377 cx.update_editor(|editor, window, cx| {
14378 editor.toggle_comments(toggle_comments, window, cx);
14379 });
14380 cx.assert_editor_state(indoc!(
14381 "fn a() {
14382 // dog();
14383 ˇ cat();
14384 }"
14385 ));
14386}
14387
14388#[gpui::test]
14389async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14390 init_test(cx, |_| {});
14391
14392 let mut cx = EditorTestContext::new(cx).await;
14393
14394 let html_language = Arc::new(
14395 Language::new(
14396 LanguageConfig {
14397 name: "HTML".into(),
14398 block_comment: Some(BlockCommentConfig {
14399 start: "<!-- ".into(),
14400 prefix: "".into(),
14401 end: " -->".into(),
14402 tab_size: 0,
14403 }),
14404 ..Default::default()
14405 },
14406 Some(tree_sitter_html::LANGUAGE.into()),
14407 )
14408 .with_injection_query(
14409 r#"
14410 (script_element
14411 (raw_text) @injection.content
14412 (#set! injection.language "javascript"))
14413 "#,
14414 )
14415 .unwrap(),
14416 );
14417
14418 let javascript_language = Arc::new(Language::new(
14419 LanguageConfig {
14420 name: "JavaScript".into(),
14421 line_comments: vec!["// ".into()],
14422 ..Default::default()
14423 },
14424 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14425 ));
14426
14427 cx.language_registry().add(html_language.clone());
14428 cx.language_registry().add(javascript_language);
14429 cx.update_buffer(|buffer, cx| {
14430 buffer.set_language(Some(html_language), cx);
14431 });
14432
14433 // Toggle comments for empty selections
14434 cx.set_state(
14435 &r#"
14436 <p>A</p>ˇ
14437 <p>B</p>ˇ
14438 <p>C</p>ˇ
14439 "#
14440 .unindent(),
14441 );
14442 cx.update_editor(|editor, window, cx| {
14443 editor.toggle_comments(&ToggleComments::default(), window, cx)
14444 });
14445 cx.assert_editor_state(
14446 &r#"
14447 <!-- <p>A</p>ˇ -->
14448 <!-- <p>B</p>ˇ -->
14449 <!-- <p>C</p>ˇ -->
14450 "#
14451 .unindent(),
14452 );
14453 cx.update_editor(|editor, window, cx| {
14454 editor.toggle_comments(&ToggleComments::default(), window, cx)
14455 });
14456 cx.assert_editor_state(
14457 &r#"
14458 <p>A</p>ˇ
14459 <p>B</p>ˇ
14460 <p>C</p>ˇ
14461 "#
14462 .unindent(),
14463 );
14464
14465 // Toggle comments for mixture of empty and non-empty selections, where
14466 // multiple selections occupy a given line.
14467 cx.set_state(
14468 &r#"
14469 <p>A«</p>
14470 <p>ˇ»B</p>ˇ
14471 <p>C«</p>
14472 <p>ˇ»D</p>ˇ
14473 "#
14474 .unindent(),
14475 );
14476
14477 cx.update_editor(|editor, window, cx| {
14478 editor.toggle_comments(&ToggleComments::default(), window, cx)
14479 });
14480 cx.assert_editor_state(
14481 &r#"
14482 <!-- <p>A«</p>
14483 <p>ˇ»B</p>ˇ -->
14484 <!-- <p>C«</p>
14485 <p>ˇ»D</p>ˇ -->
14486 "#
14487 .unindent(),
14488 );
14489 cx.update_editor(|editor, window, cx| {
14490 editor.toggle_comments(&ToggleComments::default(), window, cx)
14491 });
14492 cx.assert_editor_state(
14493 &r#"
14494 <p>A«</p>
14495 <p>ˇ»B</p>ˇ
14496 <p>C«</p>
14497 <p>ˇ»D</p>ˇ
14498 "#
14499 .unindent(),
14500 );
14501
14502 // Toggle comments when different languages are active for different
14503 // selections.
14504 cx.set_state(
14505 &r#"
14506 ˇ<script>
14507 ˇvar x = new Y();
14508 ˇ</script>
14509 "#
14510 .unindent(),
14511 );
14512 cx.executor().run_until_parked();
14513 cx.update_editor(|editor, window, cx| {
14514 editor.toggle_comments(&ToggleComments::default(), window, cx)
14515 });
14516 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14517 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14518 cx.assert_editor_state(
14519 &r#"
14520 <!-- ˇ<script> -->
14521 // ˇvar x = new Y();
14522 <!-- ˇ</script> -->
14523 "#
14524 .unindent(),
14525 );
14526}
14527
14528#[gpui::test]
14529fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14530 init_test(cx, |_| {});
14531
14532 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14533 let multibuffer = cx.new(|cx| {
14534 let mut multibuffer = MultiBuffer::new(ReadWrite);
14535 multibuffer.push_excerpts(
14536 buffer.clone(),
14537 [
14538 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14539 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14540 ],
14541 cx,
14542 );
14543 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14544 multibuffer
14545 });
14546
14547 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14548 editor.update_in(cx, |editor, window, cx| {
14549 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14551 s.select_ranges([
14552 Point::new(0, 0)..Point::new(0, 0),
14553 Point::new(1, 0)..Point::new(1, 0),
14554 ])
14555 });
14556
14557 editor.handle_input("X", window, cx);
14558 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14559 assert_eq!(
14560 editor.selections.ranges(cx),
14561 [
14562 Point::new(0, 1)..Point::new(0, 1),
14563 Point::new(1, 1)..Point::new(1, 1),
14564 ]
14565 );
14566
14567 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14568 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14569 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14570 });
14571 editor.backspace(&Default::default(), window, cx);
14572 assert_eq!(editor.text(cx), "Xa\nbbb");
14573 assert_eq!(
14574 editor.selections.ranges(cx),
14575 [Point::new(1, 0)..Point::new(1, 0)]
14576 );
14577
14578 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14579 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14580 });
14581 editor.backspace(&Default::default(), window, cx);
14582 assert_eq!(editor.text(cx), "X\nbb");
14583 assert_eq!(
14584 editor.selections.ranges(cx),
14585 [Point::new(0, 1)..Point::new(0, 1)]
14586 );
14587 });
14588}
14589
14590#[gpui::test]
14591fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14592 init_test(cx, |_| {});
14593
14594 let markers = vec![('[', ']').into(), ('(', ')').into()];
14595 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14596 indoc! {"
14597 [aaaa
14598 (bbbb]
14599 cccc)",
14600 },
14601 markers.clone(),
14602 );
14603 let excerpt_ranges = markers.into_iter().map(|marker| {
14604 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14605 ExcerptRange::new(context)
14606 });
14607 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14608 let multibuffer = cx.new(|cx| {
14609 let mut multibuffer = MultiBuffer::new(ReadWrite);
14610 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14611 multibuffer
14612 });
14613
14614 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14615 editor.update_in(cx, |editor, window, cx| {
14616 let (expected_text, selection_ranges) = marked_text_ranges(
14617 indoc! {"
14618 aaaa
14619 bˇbbb
14620 bˇbbˇb
14621 cccc"
14622 },
14623 true,
14624 );
14625 assert_eq!(editor.text(cx), expected_text);
14626 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14627 s.select_ranges(selection_ranges)
14628 });
14629
14630 editor.handle_input("X", window, cx);
14631
14632 let (expected_text, expected_selections) = marked_text_ranges(
14633 indoc! {"
14634 aaaa
14635 bXˇbbXb
14636 bXˇbbXˇb
14637 cccc"
14638 },
14639 false,
14640 );
14641 assert_eq!(editor.text(cx), expected_text);
14642 assert_eq!(editor.selections.ranges(cx), expected_selections);
14643
14644 editor.newline(&Newline, window, cx);
14645 let (expected_text, expected_selections) = marked_text_ranges(
14646 indoc! {"
14647 aaaa
14648 bX
14649 ˇbbX
14650 b
14651 bX
14652 ˇbbX
14653 ˇb
14654 cccc"
14655 },
14656 false,
14657 );
14658 assert_eq!(editor.text(cx), expected_text);
14659 assert_eq!(editor.selections.ranges(cx), expected_selections);
14660 });
14661}
14662
14663#[gpui::test]
14664fn test_refresh_selections(cx: &mut TestAppContext) {
14665 init_test(cx, |_| {});
14666
14667 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14668 let mut excerpt1_id = None;
14669 let multibuffer = cx.new(|cx| {
14670 let mut multibuffer = MultiBuffer::new(ReadWrite);
14671 excerpt1_id = multibuffer
14672 .push_excerpts(
14673 buffer.clone(),
14674 [
14675 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14676 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14677 ],
14678 cx,
14679 )
14680 .into_iter()
14681 .next();
14682 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14683 multibuffer
14684 });
14685
14686 let editor = cx.add_window(|window, cx| {
14687 let mut editor = build_editor(multibuffer.clone(), window, cx);
14688 let snapshot = editor.snapshot(window, cx);
14689 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14690 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14691 });
14692 editor.begin_selection(
14693 Point::new(2, 1).to_display_point(&snapshot),
14694 true,
14695 1,
14696 window,
14697 cx,
14698 );
14699 assert_eq!(
14700 editor.selections.ranges(cx),
14701 [
14702 Point::new(1, 3)..Point::new(1, 3),
14703 Point::new(2, 1)..Point::new(2, 1),
14704 ]
14705 );
14706 editor
14707 });
14708
14709 // Refreshing selections is a no-op when excerpts haven't changed.
14710 _ = editor.update(cx, |editor, window, cx| {
14711 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14712 assert_eq!(
14713 editor.selections.ranges(cx),
14714 [
14715 Point::new(1, 3)..Point::new(1, 3),
14716 Point::new(2, 1)..Point::new(2, 1),
14717 ]
14718 );
14719 });
14720
14721 multibuffer.update(cx, |multibuffer, cx| {
14722 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14723 });
14724 _ = editor.update(cx, |editor, window, cx| {
14725 // Removing an excerpt causes the first selection to become degenerate.
14726 assert_eq!(
14727 editor.selections.ranges(cx),
14728 [
14729 Point::new(0, 0)..Point::new(0, 0),
14730 Point::new(0, 1)..Point::new(0, 1)
14731 ]
14732 );
14733
14734 // Refreshing selections will relocate the first selection to the original buffer
14735 // location.
14736 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14737 assert_eq!(
14738 editor.selections.ranges(cx),
14739 [
14740 Point::new(0, 1)..Point::new(0, 1),
14741 Point::new(0, 3)..Point::new(0, 3)
14742 ]
14743 );
14744 assert!(editor.selections.pending_anchor().is_some());
14745 });
14746}
14747
14748#[gpui::test]
14749fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14750 init_test(cx, |_| {});
14751
14752 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14753 let mut excerpt1_id = None;
14754 let multibuffer = cx.new(|cx| {
14755 let mut multibuffer = MultiBuffer::new(ReadWrite);
14756 excerpt1_id = multibuffer
14757 .push_excerpts(
14758 buffer.clone(),
14759 [
14760 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14761 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14762 ],
14763 cx,
14764 )
14765 .into_iter()
14766 .next();
14767 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14768 multibuffer
14769 });
14770
14771 let editor = cx.add_window(|window, cx| {
14772 let mut editor = build_editor(multibuffer.clone(), window, cx);
14773 let snapshot = editor.snapshot(window, cx);
14774 editor.begin_selection(
14775 Point::new(1, 3).to_display_point(&snapshot),
14776 false,
14777 1,
14778 window,
14779 cx,
14780 );
14781 assert_eq!(
14782 editor.selections.ranges(cx),
14783 [Point::new(1, 3)..Point::new(1, 3)]
14784 );
14785 editor
14786 });
14787
14788 multibuffer.update(cx, |multibuffer, cx| {
14789 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14790 });
14791 _ = editor.update(cx, |editor, window, cx| {
14792 assert_eq!(
14793 editor.selections.ranges(cx),
14794 [Point::new(0, 0)..Point::new(0, 0)]
14795 );
14796
14797 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14798 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14799 assert_eq!(
14800 editor.selections.ranges(cx),
14801 [Point::new(0, 3)..Point::new(0, 3)]
14802 );
14803 assert!(editor.selections.pending_anchor().is_some());
14804 });
14805}
14806
14807#[gpui::test]
14808async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14809 init_test(cx, |_| {});
14810
14811 let language = Arc::new(
14812 Language::new(
14813 LanguageConfig {
14814 brackets: BracketPairConfig {
14815 pairs: vec![
14816 BracketPair {
14817 start: "{".to_string(),
14818 end: "}".to_string(),
14819 close: true,
14820 surround: true,
14821 newline: true,
14822 },
14823 BracketPair {
14824 start: "/* ".to_string(),
14825 end: " */".to_string(),
14826 close: true,
14827 surround: true,
14828 newline: true,
14829 },
14830 ],
14831 ..Default::default()
14832 },
14833 ..Default::default()
14834 },
14835 Some(tree_sitter_rust::LANGUAGE.into()),
14836 )
14837 .with_indents_query("")
14838 .unwrap(),
14839 );
14840
14841 let text = concat!(
14842 "{ }\n", //
14843 " x\n", //
14844 " /* */\n", //
14845 "x\n", //
14846 "{{} }\n", //
14847 );
14848
14849 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14850 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14851 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14852 editor
14853 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14854 .await;
14855
14856 editor.update_in(cx, |editor, window, cx| {
14857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14858 s.select_display_ranges([
14859 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14860 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14861 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14862 ])
14863 });
14864 editor.newline(&Newline, window, cx);
14865
14866 assert_eq!(
14867 editor.buffer().read(cx).read(cx).text(),
14868 concat!(
14869 "{ \n", // Suppress rustfmt
14870 "\n", //
14871 "}\n", //
14872 " x\n", //
14873 " /* \n", //
14874 " \n", //
14875 " */\n", //
14876 "x\n", //
14877 "{{} \n", //
14878 "}\n", //
14879 )
14880 );
14881 });
14882}
14883
14884#[gpui::test]
14885fn test_highlighted_ranges(cx: &mut TestAppContext) {
14886 init_test(cx, |_| {});
14887
14888 let editor = cx.add_window(|window, cx| {
14889 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14890 build_editor(buffer, window, cx)
14891 });
14892
14893 _ = editor.update(cx, |editor, window, cx| {
14894 struct Type1;
14895 struct Type2;
14896
14897 let buffer = editor.buffer.read(cx).snapshot(cx);
14898
14899 let anchor_range =
14900 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14901
14902 editor.highlight_background::<Type1>(
14903 &[
14904 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14905 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14906 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14907 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14908 ],
14909 |_| Hsla::red(),
14910 cx,
14911 );
14912 editor.highlight_background::<Type2>(
14913 &[
14914 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14915 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14916 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14917 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14918 ],
14919 |_| Hsla::green(),
14920 cx,
14921 );
14922
14923 let snapshot = editor.snapshot(window, cx);
14924 let mut highlighted_ranges = editor.background_highlights_in_range(
14925 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14926 &snapshot,
14927 cx.theme(),
14928 );
14929 // Enforce a consistent ordering based on color without relying on the ordering of the
14930 // highlight's `TypeId` which is non-executor.
14931 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14932 assert_eq!(
14933 highlighted_ranges,
14934 &[
14935 (
14936 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14937 Hsla::red(),
14938 ),
14939 (
14940 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14941 Hsla::red(),
14942 ),
14943 (
14944 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14945 Hsla::green(),
14946 ),
14947 (
14948 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14949 Hsla::green(),
14950 ),
14951 ]
14952 );
14953 assert_eq!(
14954 editor.background_highlights_in_range(
14955 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14956 &snapshot,
14957 cx.theme(),
14958 ),
14959 &[(
14960 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14961 Hsla::red(),
14962 )]
14963 );
14964 });
14965}
14966
14967#[gpui::test]
14968async fn test_following(cx: &mut TestAppContext) {
14969 init_test(cx, |_| {});
14970
14971 let fs = FakeFs::new(cx.executor());
14972 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14973
14974 let buffer = project.update(cx, |project, cx| {
14975 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14976 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14977 });
14978 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14979 let follower = cx.update(|cx| {
14980 cx.open_window(
14981 WindowOptions {
14982 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14983 gpui::Point::new(px(0.), px(0.)),
14984 gpui::Point::new(px(10.), px(80.)),
14985 ))),
14986 ..Default::default()
14987 },
14988 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14989 )
14990 .unwrap()
14991 });
14992
14993 let is_still_following = Rc::new(RefCell::new(true));
14994 let follower_edit_event_count = Rc::new(RefCell::new(0));
14995 let pending_update = Rc::new(RefCell::new(None));
14996 let leader_entity = leader.root(cx).unwrap();
14997 let follower_entity = follower.root(cx).unwrap();
14998 _ = follower.update(cx, {
14999 let update = pending_update.clone();
15000 let is_still_following = is_still_following.clone();
15001 let follower_edit_event_count = follower_edit_event_count.clone();
15002 |_, window, cx| {
15003 cx.subscribe_in(
15004 &leader_entity,
15005 window,
15006 move |_, leader, event, window, cx| {
15007 leader.read(cx).add_event_to_update_proto(
15008 event,
15009 &mut update.borrow_mut(),
15010 window,
15011 cx,
15012 );
15013 },
15014 )
15015 .detach();
15016
15017 cx.subscribe_in(
15018 &follower_entity,
15019 window,
15020 move |_, _, event: &EditorEvent, _window, _cx| {
15021 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
15022 *is_still_following.borrow_mut() = false;
15023 }
15024
15025 if let EditorEvent::BufferEdited = event {
15026 *follower_edit_event_count.borrow_mut() += 1;
15027 }
15028 },
15029 )
15030 .detach();
15031 }
15032 });
15033
15034 // Update the selections only
15035 _ = leader.update(cx, |leader, window, cx| {
15036 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15037 s.select_ranges([1..1])
15038 });
15039 });
15040 follower
15041 .update(cx, |follower, window, cx| {
15042 follower.apply_update_proto(
15043 &project,
15044 pending_update.borrow_mut().take().unwrap(),
15045 window,
15046 cx,
15047 )
15048 })
15049 .unwrap()
15050 .await
15051 .unwrap();
15052 _ = follower.update(cx, |follower, _, cx| {
15053 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15054 });
15055 assert!(*is_still_following.borrow());
15056 assert_eq!(*follower_edit_event_count.borrow(), 0);
15057
15058 // Update the scroll position only
15059 _ = leader.update(cx, |leader, window, cx| {
15060 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15061 });
15062 follower
15063 .update(cx, |follower, window, cx| {
15064 follower.apply_update_proto(
15065 &project,
15066 pending_update.borrow_mut().take().unwrap(),
15067 window,
15068 cx,
15069 )
15070 })
15071 .unwrap()
15072 .await
15073 .unwrap();
15074 assert_eq!(
15075 follower
15076 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15077 .unwrap(),
15078 gpui::Point::new(1.5, 3.5)
15079 );
15080 assert!(*is_still_following.borrow());
15081 assert_eq!(*follower_edit_event_count.borrow(), 0);
15082
15083 // Update the selections and scroll position. The follower's scroll position is updated
15084 // via autoscroll, not via the leader's exact scroll position.
15085 _ = leader.update(cx, |leader, window, cx| {
15086 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15087 s.select_ranges([0..0])
15088 });
15089 leader.request_autoscroll(Autoscroll::newest(), cx);
15090 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15091 });
15092 follower
15093 .update(cx, |follower, window, cx| {
15094 follower.apply_update_proto(
15095 &project,
15096 pending_update.borrow_mut().take().unwrap(),
15097 window,
15098 cx,
15099 )
15100 })
15101 .unwrap()
15102 .await
15103 .unwrap();
15104 _ = follower.update(cx, |follower, _, cx| {
15105 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15106 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15107 });
15108 assert!(*is_still_following.borrow());
15109
15110 // Creating a pending selection that precedes another selection
15111 _ = leader.update(cx, |leader, window, cx| {
15112 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15113 s.select_ranges([1..1])
15114 });
15115 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15116 });
15117 follower
15118 .update(cx, |follower, window, cx| {
15119 follower.apply_update_proto(
15120 &project,
15121 pending_update.borrow_mut().take().unwrap(),
15122 window,
15123 cx,
15124 )
15125 })
15126 .unwrap()
15127 .await
15128 .unwrap();
15129 _ = follower.update(cx, |follower, _, cx| {
15130 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
15131 });
15132 assert!(*is_still_following.borrow());
15133
15134 // Extend the pending selection so that it surrounds another selection
15135 _ = leader.update(cx, |leader, window, cx| {
15136 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
15137 });
15138 follower
15139 .update(cx, |follower, window, cx| {
15140 follower.apply_update_proto(
15141 &project,
15142 pending_update.borrow_mut().take().unwrap(),
15143 window,
15144 cx,
15145 )
15146 })
15147 .unwrap()
15148 .await
15149 .unwrap();
15150 _ = follower.update(cx, |follower, _, cx| {
15151 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
15152 });
15153
15154 // Scrolling locally breaks the follow
15155 _ = follower.update(cx, |follower, window, cx| {
15156 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
15157 follower.set_scroll_anchor(
15158 ScrollAnchor {
15159 anchor: top_anchor,
15160 offset: gpui::Point::new(0.0, 0.5),
15161 },
15162 window,
15163 cx,
15164 );
15165 });
15166 assert!(!(*is_still_following.borrow()));
15167}
15168
15169#[gpui::test]
15170async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
15171 init_test(cx, |_| {});
15172
15173 let fs = FakeFs::new(cx.executor());
15174 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15175 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15176 let pane = workspace
15177 .update(cx, |workspace, _, _| workspace.active_pane().clone())
15178 .unwrap();
15179
15180 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15181
15182 let leader = pane.update_in(cx, |_, window, cx| {
15183 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
15184 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
15185 });
15186
15187 // Start following the editor when it has no excerpts.
15188 let mut state_message =
15189 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15190 let workspace_entity = workspace.root(cx).unwrap();
15191 let follower_1 = cx
15192 .update_window(*workspace.deref(), |_, window, cx| {
15193 Editor::from_state_proto(
15194 workspace_entity,
15195 ViewId {
15196 creator: CollaboratorId::PeerId(PeerId::default()),
15197 id: 0,
15198 },
15199 &mut state_message,
15200 window,
15201 cx,
15202 )
15203 })
15204 .unwrap()
15205 .unwrap()
15206 .await
15207 .unwrap();
15208
15209 let update_message = Rc::new(RefCell::new(None));
15210 follower_1.update_in(cx, {
15211 let update = update_message.clone();
15212 |_, window, cx| {
15213 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
15214 leader.read(cx).add_event_to_update_proto(
15215 event,
15216 &mut update.borrow_mut(),
15217 window,
15218 cx,
15219 );
15220 })
15221 .detach();
15222 }
15223 });
15224
15225 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
15226 (
15227 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
15228 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
15229 )
15230 });
15231
15232 // Insert some excerpts.
15233 leader.update(cx, |leader, cx| {
15234 leader.buffer.update(cx, |multibuffer, cx| {
15235 multibuffer.set_excerpts_for_path(
15236 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
15237 buffer_1.clone(),
15238 vec![
15239 Point::row_range(0..3),
15240 Point::row_range(1..6),
15241 Point::row_range(12..15),
15242 ],
15243 0,
15244 cx,
15245 );
15246 multibuffer.set_excerpts_for_path(
15247 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
15248 buffer_2.clone(),
15249 vec![Point::row_range(0..6), Point::row_range(8..12)],
15250 0,
15251 cx,
15252 );
15253 });
15254 });
15255
15256 // Apply the update of adding the excerpts.
15257 follower_1
15258 .update_in(cx, |follower, window, cx| {
15259 follower.apply_update_proto(
15260 &project,
15261 update_message.borrow().clone().unwrap(),
15262 window,
15263 cx,
15264 )
15265 })
15266 .await
15267 .unwrap();
15268 assert_eq!(
15269 follower_1.update(cx, |editor, cx| editor.text(cx)),
15270 leader.update(cx, |editor, cx| editor.text(cx))
15271 );
15272 update_message.borrow_mut().take();
15273
15274 // Start following separately after it already has excerpts.
15275 let mut state_message =
15276 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15277 let workspace_entity = workspace.root(cx).unwrap();
15278 let follower_2 = cx
15279 .update_window(*workspace.deref(), |_, window, cx| {
15280 Editor::from_state_proto(
15281 workspace_entity,
15282 ViewId {
15283 creator: CollaboratorId::PeerId(PeerId::default()),
15284 id: 0,
15285 },
15286 &mut state_message,
15287 window,
15288 cx,
15289 )
15290 })
15291 .unwrap()
15292 .unwrap()
15293 .await
15294 .unwrap();
15295 assert_eq!(
15296 follower_2.update(cx, |editor, cx| editor.text(cx)),
15297 leader.update(cx, |editor, cx| editor.text(cx))
15298 );
15299
15300 // Remove some excerpts.
15301 leader.update(cx, |leader, cx| {
15302 leader.buffer.update(cx, |multibuffer, cx| {
15303 let excerpt_ids = multibuffer.excerpt_ids();
15304 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
15305 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
15306 });
15307 });
15308
15309 // Apply the update of removing the excerpts.
15310 follower_1
15311 .update_in(cx, |follower, window, cx| {
15312 follower.apply_update_proto(
15313 &project,
15314 update_message.borrow().clone().unwrap(),
15315 window,
15316 cx,
15317 )
15318 })
15319 .await
15320 .unwrap();
15321 follower_2
15322 .update_in(cx, |follower, window, cx| {
15323 follower.apply_update_proto(
15324 &project,
15325 update_message.borrow().clone().unwrap(),
15326 window,
15327 cx,
15328 )
15329 })
15330 .await
15331 .unwrap();
15332 update_message.borrow_mut().take();
15333 assert_eq!(
15334 follower_1.update(cx, |editor, cx| editor.text(cx)),
15335 leader.update(cx, |editor, cx| editor.text(cx))
15336 );
15337}
15338
15339#[gpui::test]
15340async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15341 init_test(cx, |_| {});
15342
15343 let mut cx = EditorTestContext::new(cx).await;
15344 let lsp_store =
15345 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
15346
15347 cx.set_state(indoc! {"
15348 ˇfn func(abc def: i32) -> u32 {
15349 }
15350 "});
15351
15352 cx.update(|_, cx| {
15353 lsp_store.update(cx, |lsp_store, cx| {
15354 lsp_store
15355 .update_diagnostics(
15356 LanguageServerId(0),
15357 lsp::PublishDiagnosticsParams {
15358 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
15359 version: None,
15360 diagnostics: vec![
15361 lsp::Diagnostic {
15362 range: lsp::Range::new(
15363 lsp::Position::new(0, 11),
15364 lsp::Position::new(0, 12),
15365 ),
15366 severity: Some(lsp::DiagnosticSeverity::ERROR),
15367 ..Default::default()
15368 },
15369 lsp::Diagnostic {
15370 range: lsp::Range::new(
15371 lsp::Position::new(0, 12),
15372 lsp::Position::new(0, 15),
15373 ),
15374 severity: Some(lsp::DiagnosticSeverity::ERROR),
15375 ..Default::default()
15376 },
15377 lsp::Diagnostic {
15378 range: lsp::Range::new(
15379 lsp::Position::new(0, 25),
15380 lsp::Position::new(0, 28),
15381 ),
15382 severity: Some(lsp::DiagnosticSeverity::ERROR),
15383 ..Default::default()
15384 },
15385 ],
15386 },
15387 None,
15388 DiagnosticSourceKind::Pushed,
15389 &[],
15390 cx,
15391 )
15392 .unwrap()
15393 });
15394 });
15395
15396 executor.run_until_parked();
15397
15398 cx.update_editor(|editor, window, cx| {
15399 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15400 });
15401
15402 cx.assert_editor_state(indoc! {"
15403 fn func(abc def: i32) -> ˇu32 {
15404 }
15405 "});
15406
15407 cx.update_editor(|editor, window, cx| {
15408 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15409 });
15410
15411 cx.assert_editor_state(indoc! {"
15412 fn func(abc ˇdef: i32) -> u32 {
15413 }
15414 "});
15415
15416 cx.update_editor(|editor, window, cx| {
15417 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15418 });
15419
15420 cx.assert_editor_state(indoc! {"
15421 fn func(abcˇ def: i32) -> u32 {
15422 }
15423 "});
15424
15425 cx.update_editor(|editor, window, cx| {
15426 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15427 });
15428
15429 cx.assert_editor_state(indoc! {"
15430 fn func(abc def: i32) -> ˇu32 {
15431 }
15432 "});
15433}
15434
15435#[gpui::test]
15436async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15437 init_test(cx, |_| {});
15438
15439 let mut cx = EditorTestContext::new(cx).await;
15440
15441 let diff_base = r#"
15442 use some::mod;
15443
15444 const A: u32 = 42;
15445
15446 fn main() {
15447 println!("hello");
15448
15449 println!("world");
15450 }
15451 "#
15452 .unindent();
15453
15454 // Edits are modified, removed, modified, added
15455 cx.set_state(
15456 &r#"
15457 use some::modified;
15458
15459 ˇ
15460 fn main() {
15461 println!("hello there");
15462
15463 println!("around the");
15464 println!("world");
15465 }
15466 "#
15467 .unindent(),
15468 );
15469
15470 cx.set_head_text(&diff_base);
15471 executor.run_until_parked();
15472
15473 cx.update_editor(|editor, window, cx| {
15474 //Wrap around the bottom of the buffer
15475 for _ in 0..3 {
15476 editor.go_to_next_hunk(&GoToHunk, window, cx);
15477 }
15478 });
15479
15480 cx.assert_editor_state(
15481 &r#"
15482 ˇuse some::modified;
15483
15484
15485 fn main() {
15486 println!("hello there");
15487
15488 println!("around the");
15489 println!("world");
15490 }
15491 "#
15492 .unindent(),
15493 );
15494
15495 cx.update_editor(|editor, window, cx| {
15496 //Wrap around the top of the buffer
15497 for _ in 0..2 {
15498 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15499 }
15500 });
15501
15502 cx.assert_editor_state(
15503 &r#"
15504 use some::modified;
15505
15506
15507 fn main() {
15508 ˇ println!("hello there");
15509
15510 println!("around the");
15511 println!("world");
15512 }
15513 "#
15514 .unindent(),
15515 );
15516
15517 cx.update_editor(|editor, window, cx| {
15518 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15519 });
15520
15521 cx.assert_editor_state(
15522 &r#"
15523 use some::modified;
15524
15525 ˇ
15526 fn main() {
15527 println!("hello there");
15528
15529 println!("around the");
15530 println!("world");
15531 }
15532 "#
15533 .unindent(),
15534 );
15535
15536 cx.update_editor(|editor, window, cx| {
15537 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15538 });
15539
15540 cx.assert_editor_state(
15541 &r#"
15542 ˇuse some::modified;
15543
15544
15545 fn main() {
15546 println!("hello there");
15547
15548 println!("around the");
15549 println!("world");
15550 }
15551 "#
15552 .unindent(),
15553 );
15554
15555 cx.update_editor(|editor, window, cx| {
15556 for _ in 0..2 {
15557 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15558 }
15559 });
15560
15561 cx.assert_editor_state(
15562 &r#"
15563 use some::modified;
15564
15565
15566 fn main() {
15567 ˇ println!("hello there");
15568
15569 println!("around the");
15570 println!("world");
15571 }
15572 "#
15573 .unindent(),
15574 );
15575
15576 cx.update_editor(|editor, window, cx| {
15577 editor.fold(&Fold, window, cx);
15578 });
15579
15580 cx.update_editor(|editor, window, cx| {
15581 editor.go_to_next_hunk(&GoToHunk, window, cx);
15582 });
15583
15584 cx.assert_editor_state(
15585 &r#"
15586 ˇuse some::modified;
15587
15588
15589 fn main() {
15590 println!("hello there");
15591
15592 println!("around the");
15593 println!("world");
15594 }
15595 "#
15596 .unindent(),
15597 );
15598}
15599
15600#[test]
15601fn test_split_words() {
15602 fn split(text: &str) -> Vec<&str> {
15603 split_words(text).collect()
15604 }
15605
15606 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15607 assert_eq!(split("hello_world"), &["hello_", "world"]);
15608 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15609 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15610 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15611 assert_eq!(split("helloworld"), &["helloworld"]);
15612
15613 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15614}
15615
15616#[gpui::test]
15617async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15618 init_test(cx, |_| {});
15619
15620 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15621 let mut assert = |before, after| {
15622 let _state_context = cx.set_state(before);
15623 cx.run_until_parked();
15624 cx.update_editor(|editor, window, cx| {
15625 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15626 });
15627 cx.run_until_parked();
15628 cx.assert_editor_state(after);
15629 };
15630
15631 // Outside bracket jumps to outside of matching bracket
15632 assert("console.logˇ(var);", "console.log(var)ˇ;");
15633 assert("console.log(var)ˇ;", "console.logˇ(var);");
15634
15635 // Inside bracket jumps to inside of matching bracket
15636 assert("console.log(ˇvar);", "console.log(varˇ);");
15637 assert("console.log(varˇ);", "console.log(ˇvar);");
15638
15639 // When outside a bracket and inside, favor jumping to the inside bracket
15640 assert(
15641 "console.log('foo', [1, 2, 3]ˇ);",
15642 "console.log(ˇ'foo', [1, 2, 3]);",
15643 );
15644 assert(
15645 "console.log(ˇ'foo', [1, 2, 3]);",
15646 "console.log('foo', [1, 2, 3]ˇ);",
15647 );
15648
15649 // Bias forward if two options are equally likely
15650 assert(
15651 "let result = curried_fun()ˇ();",
15652 "let result = curried_fun()()ˇ;",
15653 );
15654
15655 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15656 assert(
15657 indoc! {"
15658 function test() {
15659 console.log('test')ˇ
15660 }"},
15661 indoc! {"
15662 function test() {
15663 console.logˇ('test')
15664 }"},
15665 );
15666}
15667
15668#[gpui::test]
15669async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15670 init_test(cx, |_| {});
15671
15672 let fs = FakeFs::new(cx.executor());
15673 fs.insert_tree(
15674 path!("/a"),
15675 json!({
15676 "main.rs": "fn main() { let a = 5; }",
15677 "other.rs": "// Test file",
15678 }),
15679 )
15680 .await;
15681 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15682
15683 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15684 language_registry.add(Arc::new(Language::new(
15685 LanguageConfig {
15686 name: "Rust".into(),
15687 matcher: LanguageMatcher {
15688 path_suffixes: vec!["rs".to_string()],
15689 ..Default::default()
15690 },
15691 brackets: BracketPairConfig {
15692 pairs: vec![BracketPair {
15693 start: "{".to_string(),
15694 end: "}".to_string(),
15695 close: true,
15696 surround: true,
15697 newline: true,
15698 }],
15699 disabled_scopes_by_bracket_ix: Vec::new(),
15700 },
15701 ..Default::default()
15702 },
15703 Some(tree_sitter_rust::LANGUAGE.into()),
15704 )));
15705 let mut fake_servers = language_registry.register_fake_lsp(
15706 "Rust",
15707 FakeLspAdapter {
15708 capabilities: lsp::ServerCapabilities {
15709 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15710 first_trigger_character: "{".to_string(),
15711 more_trigger_character: None,
15712 }),
15713 ..Default::default()
15714 },
15715 ..Default::default()
15716 },
15717 );
15718
15719 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15720
15721 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15722
15723 let worktree_id = workspace
15724 .update(cx, |workspace, _, cx| {
15725 workspace.project().update(cx, |project, cx| {
15726 project.worktrees(cx).next().unwrap().read(cx).id()
15727 })
15728 })
15729 .unwrap();
15730
15731 let buffer = project
15732 .update(cx, |project, cx| {
15733 project.open_local_buffer(path!("/a/main.rs"), cx)
15734 })
15735 .await
15736 .unwrap();
15737 let editor_handle = workspace
15738 .update(cx, |workspace, window, cx| {
15739 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15740 })
15741 .unwrap()
15742 .await
15743 .unwrap()
15744 .downcast::<Editor>()
15745 .unwrap();
15746
15747 cx.executor().start_waiting();
15748 let fake_server = fake_servers.next().await.unwrap();
15749
15750 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15751 |params, _| async move {
15752 assert_eq!(
15753 params.text_document_position.text_document.uri,
15754 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15755 );
15756 assert_eq!(
15757 params.text_document_position.position,
15758 lsp::Position::new(0, 21),
15759 );
15760
15761 Ok(Some(vec![lsp::TextEdit {
15762 new_text: "]".to_string(),
15763 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15764 }]))
15765 },
15766 );
15767
15768 editor_handle.update_in(cx, |editor, window, cx| {
15769 window.focus(&editor.focus_handle(cx));
15770 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15771 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15772 });
15773 editor.handle_input("{", window, cx);
15774 });
15775
15776 cx.executor().run_until_parked();
15777
15778 buffer.update(cx, |buffer, _| {
15779 assert_eq!(
15780 buffer.text(),
15781 "fn main() { let a = {5}; }",
15782 "No extra braces from on type formatting should appear in the buffer"
15783 )
15784 });
15785}
15786
15787#[gpui::test(iterations = 20, seeds(31))]
15788async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15789 init_test(cx, |_| {});
15790
15791 let mut cx = EditorLspTestContext::new_rust(
15792 lsp::ServerCapabilities {
15793 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15794 first_trigger_character: ".".to_string(),
15795 more_trigger_character: None,
15796 }),
15797 ..Default::default()
15798 },
15799 cx,
15800 )
15801 .await;
15802
15803 cx.update_buffer(|buffer, _| {
15804 // This causes autoindent to be async.
15805 buffer.set_sync_parse_timeout(Duration::ZERO)
15806 });
15807
15808 cx.set_state("fn c() {\n d()ˇ\n}\n");
15809 cx.simulate_keystroke("\n");
15810 cx.run_until_parked();
15811
15812 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
15813 let mut request =
15814 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15815 let buffer_cloned = buffer_cloned.clone();
15816 async move {
15817 buffer_cloned.update(&mut cx, |buffer, _| {
15818 assert_eq!(
15819 buffer.text(),
15820 "fn c() {\n d()\n .\n}\n",
15821 "OnTypeFormatting should triggered after autoindent applied"
15822 )
15823 })?;
15824
15825 Ok(Some(vec![]))
15826 }
15827 });
15828
15829 cx.simulate_keystroke(".");
15830 cx.run_until_parked();
15831
15832 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15833 assert!(request.next().await.is_some());
15834 request.close();
15835 assert!(request.next().await.is_none());
15836}
15837
15838#[gpui::test]
15839async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15840 init_test(cx, |_| {});
15841
15842 let fs = FakeFs::new(cx.executor());
15843 fs.insert_tree(
15844 path!("/a"),
15845 json!({
15846 "main.rs": "fn main() { let a = 5; }",
15847 "other.rs": "// Test file",
15848 }),
15849 )
15850 .await;
15851
15852 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15853
15854 let server_restarts = Arc::new(AtomicUsize::new(0));
15855 let closure_restarts = Arc::clone(&server_restarts);
15856 let language_server_name = "test language server";
15857 let language_name: LanguageName = "Rust".into();
15858
15859 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15860 language_registry.add(Arc::new(Language::new(
15861 LanguageConfig {
15862 name: language_name.clone(),
15863 matcher: LanguageMatcher {
15864 path_suffixes: vec!["rs".to_string()],
15865 ..Default::default()
15866 },
15867 ..Default::default()
15868 },
15869 Some(tree_sitter_rust::LANGUAGE.into()),
15870 )));
15871 let mut fake_servers = language_registry.register_fake_lsp(
15872 "Rust",
15873 FakeLspAdapter {
15874 name: language_server_name,
15875 initialization_options: Some(json!({
15876 "testOptionValue": true
15877 })),
15878 initializer: Some(Box::new(move |fake_server| {
15879 let task_restarts = Arc::clone(&closure_restarts);
15880 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15881 task_restarts.fetch_add(1, atomic::Ordering::Release);
15882 futures::future::ready(Ok(()))
15883 });
15884 })),
15885 ..Default::default()
15886 },
15887 );
15888
15889 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15890 let _buffer = project
15891 .update(cx, |project, cx| {
15892 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15893 })
15894 .await
15895 .unwrap();
15896 let _fake_server = fake_servers.next().await.unwrap();
15897 update_test_language_settings(cx, |language_settings| {
15898 language_settings.languages.0.insert(
15899 language_name.clone(),
15900 LanguageSettingsContent {
15901 tab_size: NonZeroU32::new(8),
15902 ..Default::default()
15903 },
15904 );
15905 });
15906 cx.executor().run_until_parked();
15907 assert_eq!(
15908 server_restarts.load(atomic::Ordering::Acquire),
15909 0,
15910 "Should not restart LSP server on an unrelated change"
15911 );
15912
15913 update_test_project_settings(cx, |project_settings| {
15914 project_settings.lsp.insert(
15915 "Some other server name".into(),
15916 LspSettings {
15917 binary: None,
15918 settings: None,
15919 initialization_options: Some(json!({
15920 "some other init value": false
15921 })),
15922 enable_lsp_tasks: false,
15923 },
15924 );
15925 });
15926 cx.executor().run_until_parked();
15927 assert_eq!(
15928 server_restarts.load(atomic::Ordering::Acquire),
15929 0,
15930 "Should not restart LSP server on an unrelated LSP settings change"
15931 );
15932
15933 update_test_project_settings(cx, |project_settings| {
15934 project_settings.lsp.insert(
15935 language_server_name.into(),
15936 LspSettings {
15937 binary: None,
15938 settings: None,
15939 initialization_options: Some(json!({
15940 "anotherInitValue": false
15941 })),
15942 enable_lsp_tasks: false,
15943 },
15944 );
15945 });
15946 cx.executor().run_until_parked();
15947 assert_eq!(
15948 server_restarts.load(atomic::Ordering::Acquire),
15949 1,
15950 "Should restart LSP server on a related LSP settings change"
15951 );
15952
15953 update_test_project_settings(cx, |project_settings| {
15954 project_settings.lsp.insert(
15955 language_server_name.into(),
15956 LspSettings {
15957 binary: None,
15958 settings: None,
15959 initialization_options: Some(json!({
15960 "anotherInitValue": false
15961 })),
15962 enable_lsp_tasks: false,
15963 },
15964 );
15965 });
15966 cx.executor().run_until_parked();
15967 assert_eq!(
15968 server_restarts.load(atomic::Ordering::Acquire),
15969 1,
15970 "Should not restart LSP server on a related LSP settings change that is the same"
15971 );
15972
15973 update_test_project_settings(cx, |project_settings| {
15974 project_settings.lsp.insert(
15975 language_server_name.into(),
15976 LspSettings {
15977 binary: None,
15978 settings: None,
15979 initialization_options: None,
15980 enable_lsp_tasks: false,
15981 },
15982 );
15983 });
15984 cx.executor().run_until_parked();
15985 assert_eq!(
15986 server_restarts.load(atomic::Ordering::Acquire),
15987 2,
15988 "Should restart LSP server on another related LSP settings change"
15989 );
15990}
15991
15992#[gpui::test]
15993async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15994 init_test(cx, |_| {});
15995
15996 let mut cx = EditorLspTestContext::new_rust(
15997 lsp::ServerCapabilities {
15998 completion_provider: Some(lsp::CompletionOptions {
15999 trigger_characters: Some(vec![".".to_string()]),
16000 resolve_provider: Some(true),
16001 ..Default::default()
16002 }),
16003 ..Default::default()
16004 },
16005 cx,
16006 )
16007 .await;
16008
16009 cx.set_state("fn main() { let a = 2ˇ; }");
16010 cx.simulate_keystroke(".");
16011 let completion_item = lsp::CompletionItem {
16012 label: "some".into(),
16013 kind: Some(lsp::CompletionItemKind::SNIPPET),
16014 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16015 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16016 kind: lsp::MarkupKind::Markdown,
16017 value: "```rust\nSome(2)\n```".to_string(),
16018 })),
16019 deprecated: Some(false),
16020 sort_text: Some("fffffff2".to_string()),
16021 filter_text: Some("some".to_string()),
16022 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16023 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16024 range: lsp::Range {
16025 start: lsp::Position {
16026 line: 0,
16027 character: 22,
16028 },
16029 end: lsp::Position {
16030 line: 0,
16031 character: 22,
16032 },
16033 },
16034 new_text: "Some(2)".to_string(),
16035 })),
16036 additional_text_edits: Some(vec![lsp::TextEdit {
16037 range: lsp::Range {
16038 start: lsp::Position {
16039 line: 0,
16040 character: 20,
16041 },
16042 end: lsp::Position {
16043 line: 0,
16044 character: 22,
16045 },
16046 },
16047 new_text: "".to_string(),
16048 }]),
16049 ..Default::default()
16050 };
16051
16052 let closure_completion_item = completion_item.clone();
16053 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16054 let task_completion_item = closure_completion_item.clone();
16055 async move {
16056 Ok(Some(lsp::CompletionResponse::Array(vec![
16057 task_completion_item,
16058 ])))
16059 }
16060 });
16061
16062 request.next().await;
16063
16064 cx.condition(|editor, _| editor.context_menu_visible())
16065 .await;
16066 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16067 editor
16068 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16069 .unwrap()
16070 });
16071 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16072
16073 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16074 let task_completion_item = completion_item.clone();
16075 async move { Ok(task_completion_item) }
16076 })
16077 .next()
16078 .await
16079 .unwrap();
16080 apply_additional_edits.await.unwrap();
16081 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16082}
16083
16084#[gpui::test]
16085async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16086 init_test(cx, |_| {});
16087
16088 let mut cx = EditorLspTestContext::new_rust(
16089 lsp::ServerCapabilities {
16090 completion_provider: Some(lsp::CompletionOptions {
16091 trigger_characters: Some(vec![".".to_string()]),
16092 resolve_provider: Some(true),
16093 ..Default::default()
16094 }),
16095 ..Default::default()
16096 },
16097 cx,
16098 )
16099 .await;
16100
16101 cx.set_state("fn main() { let a = 2ˇ; }");
16102 cx.simulate_keystroke(".");
16103
16104 let item1 = lsp::CompletionItem {
16105 label: "method id()".to_string(),
16106 filter_text: Some("id".to_string()),
16107 detail: None,
16108 documentation: None,
16109 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16110 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16111 new_text: ".id".to_string(),
16112 })),
16113 ..lsp::CompletionItem::default()
16114 };
16115
16116 let item2 = lsp::CompletionItem {
16117 label: "other".to_string(),
16118 filter_text: Some("other".to_string()),
16119 detail: None,
16120 documentation: None,
16121 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16122 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16123 new_text: ".other".to_string(),
16124 })),
16125 ..lsp::CompletionItem::default()
16126 };
16127
16128 let item1 = item1.clone();
16129 cx.set_request_handler::<lsp::request::Completion, _, _>({
16130 let item1 = item1.clone();
16131 move |_, _, _| {
16132 let item1 = item1.clone();
16133 let item2 = item2.clone();
16134 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
16135 }
16136 })
16137 .next()
16138 .await;
16139
16140 cx.condition(|editor, _| editor.context_menu_visible())
16141 .await;
16142 cx.update_editor(|editor, _, _| {
16143 let context_menu = editor.context_menu.borrow_mut();
16144 let context_menu = context_menu
16145 .as_ref()
16146 .expect("Should have the context menu deployed");
16147 match context_menu {
16148 CodeContextMenu::Completions(completions_menu) => {
16149 let completions = completions_menu.completions.borrow_mut();
16150 assert_eq!(
16151 completions
16152 .iter()
16153 .map(|completion| &completion.label.text)
16154 .collect::<Vec<_>>(),
16155 vec!["method id()", "other"]
16156 )
16157 }
16158 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16159 }
16160 });
16161
16162 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
16163 let item1 = item1.clone();
16164 move |_, item_to_resolve, _| {
16165 let item1 = item1.clone();
16166 async move {
16167 if item1 == item_to_resolve {
16168 Ok(lsp::CompletionItem {
16169 label: "method id()".to_string(),
16170 filter_text: Some("id".to_string()),
16171 detail: Some("Now resolved!".to_string()),
16172 documentation: Some(lsp::Documentation::String("Docs".to_string())),
16173 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16174 range: lsp::Range::new(
16175 lsp::Position::new(0, 22),
16176 lsp::Position::new(0, 22),
16177 ),
16178 new_text: ".id".to_string(),
16179 })),
16180 ..lsp::CompletionItem::default()
16181 })
16182 } else {
16183 Ok(item_to_resolve)
16184 }
16185 }
16186 }
16187 })
16188 .next()
16189 .await
16190 .unwrap();
16191 cx.run_until_parked();
16192
16193 cx.update_editor(|editor, window, cx| {
16194 editor.context_menu_next(&Default::default(), window, cx);
16195 });
16196
16197 cx.update_editor(|editor, _, _| {
16198 let context_menu = editor.context_menu.borrow_mut();
16199 let context_menu = context_menu
16200 .as_ref()
16201 .expect("Should have the context menu deployed");
16202 match context_menu {
16203 CodeContextMenu::Completions(completions_menu) => {
16204 let completions = completions_menu.completions.borrow_mut();
16205 assert_eq!(
16206 completions
16207 .iter()
16208 .map(|completion| &completion.label.text)
16209 .collect::<Vec<_>>(),
16210 vec!["method id() Now resolved!", "other"],
16211 "Should update first completion label, but not second as the filter text did not match."
16212 );
16213 }
16214 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16215 }
16216 });
16217}
16218
16219#[gpui::test]
16220async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
16221 init_test(cx, |_| {});
16222 let mut cx = EditorLspTestContext::new_rust(
16223 lsp::ServerCapabilities {
16224 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
16225 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
16226 completion_provider: Some(lsp::CompletionOptions {
16227 resolve_provider: Some(true),
16228 ..Default::default()
16229 }),
16230 ..Default::default()
16231 },
16232 cx,
16233 )
16234 .await;
16235 cx.set_state(indoc! {"
16236 struct TestStruct {
16237 field: i32
16238 }
16239
16240 fn mainˇ() {
16241 let unused_var = 42;
16242 let test_struct = TestStruct { field: 42 };
16243 }
16244 "});
16245 let symbol_range = cx.lsp_range(indoc! {"
16246 struct TestStruct {
16247 field: i32
16248 }
16249
16250 «fn main»() {
16251 let unused_var = 42;
16252 let test_struct = TestStruct { field: 42 };
16253 }
16254 "});
16255 let mut hover_requests =
16256 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
16257 Ok(Some(lsp::Hover {
16258 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
16259 kind: lsp::MarkupKind::Markdown,
16260 value: "Function documentation".to_string(),
16261 }),
16262 range: Some(symbol_range),
16263 }))
16264 });
16265
16266 // Case 1: Test that code action menu hide hover popover
16267 cx.dispatch_action(Hover);
16268 hover_requests.next().await;
16269 cx.condition(|editor, _| editor.hover_state.visible()).await;
16270 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
16271 move |_, _, _| async move {
16272 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
16273 lsp::CodeAction {
16274 title: "Remove unused variable".to_string(),
16275 kind: Some(CodeActionKind::QUICKFIX),
16276 edit: Some(lsp::WorkspaceEdit {
16277 changes: Some(
16278 [(
16279 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
16280 vec![lsp::TextEdit {
16281 range: lsp::Range::new(
16282 lsp::Position::new(5, 4),
16283 lsp::Position::new(5, 27),
16284 ),
16285 new_text: "".to_string(),
16286 }],
16287 )]
16288 .into_iter()
16289 .collect(),
16290 ),
16291 ..Default::default()
16292 }),
16293 ..Default::default()
16294 },
16295 )]))
16296 },
16297 );
16298 cx.update_editor(|editor, window, cx| {
16299 editor.toggle_code_actions(
16300 &ToggleCodeActions {
16301 deployed_from: None,
16302 quick_launch: false,
16303 },
16304 window,
16305 cx,
16306 );
16307 });
16308 code_action_requests.next().await;
16309 cx.run_until_parked();
16310 cx.condition(|editor, _| editor.context_menu_visible())
16311 .await;
16312 cx.update_editor(|editor, _, _| {
16313 assert!(
16314 !editor.hover_state.visible(),
16315 "Hover popover should be hidden when code action menu is shown"
16316 );
16317 // Hide code actions
16318 editor.context_menu.take();
16319 });
16320
16321 // Case 2: Test that code completions hide hover popover
16322 cx.dispatch_action(Hover);
16323 hover_requests.next().await;
16324 cx.condition(|editor, _| editor.hover_state.visible()).await;
16325 let counter = Arc::new(AtomicUsize::new(0));
16326 let mut completion_requests =
16327 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16328 let counter = counter.clone();
16329 async move {
16330 counter.fetch_add(1, atomic::Ordering::Release);
16331 Ok(Some(lsp::CompletionResponse::Array(vec![
16332 lsp::CompletionItem {
16333 label: "main".into(),
16334 kind: Some(lsp::CompletionItemKind::FUNCTION),
16335 detail: Some("() -> ()".to_string()),
16336 ..Default::default()
16337 },
16338 lsp::CompletionItem {
16339 label: "TestStruct".into(),
16340 kind: Some(lsp::CompletionItemKind::STRUCT),
16341 detail: Some("struct TestStruct".to_string()),
16342 ..Default::default()
16343 },
16344 ])))
16345 }
16346 });
16347 cx.update_editor(|editor, window, cx| {
16348 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16349 });
16350 completion_requests.next().await;
16351 cx.condition(|editor, _| editor.context_menu_visible())
16352 .await;
16353 cx.update_editor(|editor, _, _| {
16354 assert!(
16355 !editor.hover_state.visible(),
16356 "Hover popover should be hidden when completion menu is shown"
16357 );
16358 });
16359}
16360
16361#[gpui::test]
16362async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16363 init_test(cx, |_| {});
16364
16365 let mut cx = EditorLspTestContext::new_rust(
16366 lsp::ServerCapabilities {
16367 completion_provider: Some(lsp::CompletionOptions {
16368 trigger_characters: Some(vec![".".to_string()]),
16369 resolve_provider: Some(true),
16370 ..Default::default()
16371 }),
16372 ..Default::default()
16373 },
16374 cx,
16375 )
16376 .await;
16377
16378 cx.set_state("fn main() { let a = 2ˇ; }");
16379 cx.simulate_keystroke(".");
16380
16381 let unresolved_item_1 = lsp::CompletionItem {
16382 label: "id".to_string(),
16383 filter_text: Some("id".to_string()),
16384 detail: None,
16385 documentation: None,
16386 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16387 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16388 new_text: ".id".to_string(),
16389 })),
16390 ..lsp::CompletionItem::default()
16391 };
16392 let resolved_item_1 = lsp::CompletionItem {
16393 additional_text_edits: Some(vec![lsp::TextEdit {
16394 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16395 new_text: "!!".to_string(),
16396 }]),
16397 ..unresolved_item_1.clone()
16398 };
16399 let unresolved_item_2 = lsp::CompletionItem {
16400 label: "other".to_string(),
16401 filter_text: Some("other".to_string()),
16402 detail: None,
16403 documentation: None,
16404 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16405 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16406 new_text: ".other".to_string(),
16407 })),
16408 ..lsp::CompletionItem::default()
16409 };
16410 let resolved_item_2 = lsp::CompletionItem {
16411 additional_text_edits: Some(vec![lsp::TextEdit {
16412 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16413 new_text: "??".to_string(),
16414 }]),
16415 ..unresolved_item_2.clone()
16416 };
16417
16418 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16419 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16420 cx.lsp
16421 .server
16422 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16423 let unresolved_item_1 = unresolved_item_1.clone();
16424 let resolved_item_1 = resolved_item_1.clone();
16425 let unresolved_item_2 = unresolved_item_2.clone();
16426 let resolved_item_2 = resolved_item_2.clone();
16427 let resolve_requests_1 = resolve_requests_1.clone();
16428 let resolve_requests_2 = resolve_requests_2.clone();
16429 move |unresolved_request, _| {
16430 let unresolved_item_1 = unresolved_item_1.clone();
16431 let resolved_item_1 = resolved_item_1.clone();
16432 let unresolved_item_2 = unresolved_item_2.clone();
16433 let resolved_item_2 = resolved_item_2.clone();
16434 let resolve_requests_1 = resolve_requests_1.clone();
16435 let resolve_requests_2 = resolve_requests_2.clone();
16436 async move {
16437 if unresolved_request == unresolved_item_1 {
16438 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16439 Ok(resolved_item_1.clone())
16440 } else if unresolved_request == unresolved_item_2 {
16441 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16442 Ok(resolved_item_2.clone())
16443 } else {
16444 panic!("Unexpected completion item {unresolved_request:?}")
16445 }
16446 }
16447 }
16448 })
16449 .detach();
16450
16451 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16452 let unresolved_item_1 = unresolved_item_1.clone();
16453 let unresolved_item_2 = unresolved_item_2.clone();
16454 async move {
16455 Ok(Some(lsp::CompletionResponse::Array(vec![
16456 unresolved_item_1,
16457 unresolved_item_2,
16458 ])))
16459 }
16460 })
16461 .next()
16462 .await;
16463
16464 cx.condition(|editor, _| editor.context_menu_visible())
16465 .await;
16466 cx.update_editor(|editor, _, _| {
16467 let context_menu = editor.context_menu.borrow_mut();
16468 let context_menu = context_menu
16469 .as_ref()
16470 .expect("Should have the context menu deployed");
16471 match context_menu {
16472 CodeContextMenu::Completions(completions_menu) => {
16473 let completions = completions_menu.completions.borrow_mut();
16474 assert_eq!(
16475 completions
16476 .iter()
16477 .map(|completion| &completion.label.text)
16478 .collect::<Vec<_>>(),
16479 vec!["id", "other"]
16480 )
16481 }
16482 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16483 }
16484 });
16485 cx.run_until_parked();
16486
16487 cx.update_editor(|editor, window, cx| {
16488 editor.context_menu_next(&ContextMenuNext, window, cx);
16489 });
16490 cx.run_until_parked();
16491 cx.update_editor(|editor, window, cx| {
16492 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16493 });
16494 cx.run_until_parked();
16495 cx.update_editor(|editor, window, cx| {
16496 editor.context_menu_next(&ContextMenuNext, window, cx);
16497 });
16498 cx.run_until_parked();
16499 cx.update_editor(|editor, window, cx| {
16500 editor
16501 .compose_completion(&ComposeCompletion::default(), window, cx)
16502 .expect("No task returned")
16503 })
16504 .await
16505 .expect("Completion failed");
16506 cx.run_until_parked();
16507
16508 cx.update_editor(|editor, _, cx| {
16509 assert_eq!(
16510 resolve_requests_1.load(atomic::Ordering::Acquire),
16511 1,
16512 "Should always resolve once despite multiple selections"
16513 );
16514 assert_eq!(
16515 resolve_requests_2.load(atomic::Ordering::Acquire),
16516 1,
16517 "Should always resolve once after multiple selections and applying the completion"
16518 );
16519 assert_eq!(
16520 editor.text(cx),
16521 "fn main() { let a = ??.other; }",
16522 "Should use resolved data when applying the completion"
16523 );
16524 });
16525}
16526
16527#[gpui::test]
16528async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16529 init_test(cx, |_| {});
16530
16531 let item_0 = lsp::CompletionItem {
16532 label: "abs".into(),
16533 insert_text: Some("abs".into()),
16534 data: Some(json!({ "very": "special"})),
16535 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16536 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16537 lsp::InsertReplaceEdit {
16538 new_text: "abs".to_string(),
16539 insert: lsp::Range::default(),
16540 replace: lsp::Range::default(),
16541 },
16542 )),
16543 ..lsp::CompletionItem::default()
16544 };
16545 let items = iter::once(item_0.clone())
16546 .chain((11..51).map(|i| lsp::CompletionItem {
16547 label: format!("item_{}", i),
16548 insert_text: Some(format!("item_{}", i)),
16549 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16550 ..lsp::CompletionItem::default()
16551 }))
16552 .collect::<Vec<_>>();
16553
16554 let default_commit_characters = vec!["?".to_string()];
16555 let default_data = json!({ "default": "data"});
16556 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16557 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16558 let default_edit_range = lsp::Range {
16559 start: lsp::Position {
16560 line: 0,
16561 character: 5,
16562 },
16563 end: lsp::Position {
16564 line: 0,
16565 character: 5,
16566 },
16567 };
16568
16569 let mut cx = EditorLspTestContext::new_rust(
16570 lsp::ServerCapabilities {
16571 completion_provider: Some(lsp::CompletionOptions {
16572 trigger_characters: Some(vec![".".to_string()]),
16573 resolve_provider: Some(true),
16574 ..Default::default()
16575 }),
16576 ..Default::default()
16577 },
16578 cx,
16579 )
16580 .await;
16581
16582 cx.set_state("fn main() { let a = 2ˇ; }");
16583 cx.simulate_keystroke(".");
16584
16585 let completion_data = default_data.clone();
16586 let completion_characters = default_commit_characters.clone();
16587 let completion_items = items.clone();
16588 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16589 let default_data = completion_data.clone();
16590 let default_commit_characters = completion_characters.clone();
16591 let items = completion_items.clone();
16592 async move {
16593 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16594 items,
16595 item_defaults: Some(lsp::CompletionListItemDefaults {
16596 data: Some(default_data.clone()),
16597 commit_characters: Some(default_commit_characters.clone()),
16598 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16599 default_edit_range,
16600 )),
16601 insert_text_format: Some(default_insert_text_format),
16602 insert_text_mode: Some(default_insert_text_mode),
16603 }),
16604 ..lsp::CompletionList::default()
16605 })))
16606 }
16607 })
16608 .next()
16609 .await;
16610
16611 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16612 cx.lsp
16613 .server
16614 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16615 let closure_resolved_items = resolved_items.clone();
16616 move |item_to_resolve, _| {
16617 let closure_resolved_items = closure_resolved_items.clone();
16618 async move {
16619 closure_resolved_items.lock().push(item_to_resolve.clone());
16620 Ok(item_to_resolve)
16621 }
16622 }
16623 })
16624 .detach();
16625
16626 cx.condition(|editor, _| editor.context_menu_visible())
16627 .await;
16628 cx.run_until_parked();
16629 cx.update_editor(|editor, _, _| {
16630 let menu = editor.context_menu.borrow_mut();
16631 match menu.as_ref().expect("should have the completions menu") {
16632 CodeContextMenu::Completions(completions_menu) => {
16633 assert_eq!(
16634 completions_menu
16635 .entries
16636 .borrow()
16637 .iter()
16638 .map(|mat| mat.string.clone())
16639 .collect::<Vec<String>>(),
16640 items
16641 .iter()
16642 .map(|completion| completion.label.clone())
16643 .collect::<Vec<String>>()
16644 );
16645 }
16646 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16647 }
16648 });
16649 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16650 // with 4 from the end.
16651 assert_eq!(
16652 *resolved_items.lock(),
16653 [&items[0..16], &items[items.len() - 4..items.len()]]
16654 .concat()
16655 .iter()
16656 .cloned()
16657 .map(|mut item| {
16658 if item.data.is_none() {
16659 item.data = Some(default_data.clone());
16660 }
16661 item
16662 })
16663 .collect::<Vec<lsp::CompletionItem>>(),
16664 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16665 );
16666 resolved_items.lock().clear();
16667
16668 cx.update_editor(|editor, window, cx| {
16669 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16670 });
16671 cx.run_until_parked();
16672 // Completions that have already been resolved are skipped.
16673 assert_eq!(
16674 *resolved_items.lock(),
16675 items[items.len() - 17..items.len() - 4]
16676 .iter()
16677 .cloned()
16678 .map(|mut item| {
16679 if item.data.is_none() {
16680 item.data = Some(default_data.clone());
16681 }
16682 item
16683 })
16684 .collect::<Vec<lsp::CompletionItem>>()
16685 );
16686 resolved_items.lock().clear();
16687}
16688
16689#[gpui::test]
16690async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16691 init_test(cx, |_| {});
16692
16693 let mut cx = EditorLspTestContext::new(
16694 Language::new(
16695 LanguageConfig {
16696 matcher: LanguageMatcher {
16697 path_suffixes: vec!["jsx".into()],
16698 ..Default::default()
16699 },
16700 overrides: [(
16701 "element".into(),
16702 LanguageConfigOverride {
16703 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16704 ..Default::default()
16705 },
16706 )]
16707 .into_iter()
16708 .collect(),
16709 ..Default::default()
16710 },
16711 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16712 )
16713 .with_override_query("(jsx_self_closing_element) @element")
16714 .unwrap(),
16715 lsp::ServerCapabilities {
16716 completion_provider: Some(lsp::CompletionOptions {
16717 trigger_characters: Some(vec![":".to_string()]),
16718 ..Default::default()
16719 }),
16720 ..Default::default()
16721 },
16722 cx,
16723 )
16724 .await;
16725
16726 cx.lsp
16727 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16728 Ok(Some(lsp::CompletionResponse::Array(vec![
16729 lsp::CompletionItem {
16730 label: "bg-blue".into(),
16731 ..Default::default()
16732 },
16733 lsp::CompletionItem {
16734 label: "bg-red".into(),
16735 ..Default::default()
16736 },
16737 lsp::CompletionItem {
16738 label: "bg-yellow".into(),
16739 ..Default::default()
16740 },
16741 ])))
16742 });
16743
16744 cx.set_state(r#"<p class="bgˇ" />"#);
16745
16746 // Trigger completion when typing a dash, because the dash is an extra
16747 // word character in the 'element' scope, which contains the cursor.
16748 cx.simulate_keystroke("-");
16749 cx.executor().run_until_parked();
16750 cx.update_editor(|editor, _, _| {
16751 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16752 {
16753 assert_eq!(
16754 completion_menu_entries(menu),
16755 &["bg-blue", "bg-red", "bg-yellow"]
16756 );
16757 } else {
16758 panic!("expected completion menu to be open");
16759 }
16760 });
16761
16762 cx.simulate_keystroke("l");
16763 cx.executor().run_until_parked();
16764 cx.update_editor(|editor, _, _| {
16765 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16766 {
16767 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
16768 } else {
16769 panic!("expected completion menu to be open");
16770 }
16771 });
16772
16773 // When filtering completions, consider the character after the '-' to
16774 // be the start of a subword.
16775 cx.set_state(r#"<p class="yelˇ" />"#);
16776 cx.simulate_keystroke("l");
16777 cx.executor().run_until_parked();
16778 cx.update_editor(|editor, _, _| {
16779 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16780 {
16781 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
16782 } else {
16783 panic!("expected completion menu to be open");
16784 }
16785 });
16786}
16787
16788fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16789 let entries = menu.entries.borrow();
16790 entries.iter().map(|mat| mat.string.clone()).collect()
16791}
16792
16793#[gpui::test]
16794async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16795 init_test(cx, |settings| {
16796 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16797 Formatter::Prettier,
16798 )))
16799 });
16800
16801 let fs = FakeFs::new(cx.executor());
16802 fs.insert_file(path!("/file.ts"), Default::default()).await;
16803
16804 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16805 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16806
16807 language_registry.add(Arc::new(Language::new(
16808 LanguageConfig {
16809 name: "TypeScript".into(),
16810 matcher: LanguageMatcher {
16811 path_suffixes: vec!["ts".to_string()],
16812 ..Default::default()
16813 },
16814 ..Default::default()
16815 },
16816 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16817 )));
16818 update_test_language_settings(cx, |settings| {
16819 settings.defaults.prettier = Some(PrettierSettings {
16820 allowed: true,
16821 ..PrettierSettings::default()
16822 });
16823 });
16824
16825 let test_plugin = "test_plugin";
16826 let _ = language_registry.register_fake_lsp(
16827 "TypeScript",
16828 FakeLspAdapter {
16829 prettier_plugins: vec![test_plugin],
16830 ..Default::default()
16831 },
16832 );
16833
16834 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16835 let buffer = project
16836 .update(cx, |project, cx| {
16837 project.open_local_buffer(path!("/file.ts"), cx)
16838 })
16839 .await
16840 .unwrap();
16841
16842 let buffer_text = "one\ntwo\nthree\n";
16843 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16844 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16845 editor.update_in(cx, |editor, window, cx| {
16846 editor.set_text(buffer_text, window, cx)
16847 });
16848
16849 editor
16850 .update_in(cx, |editor, window, cx| {
16851 editor.perform_format(
16852 project.clone(),
16853 FormatTrigger::Manual,
16854 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16855 window,
16856 cx,
16857 )
16858 })
16859 .unwrap()
16860 .await;
16861 assert_eq!(
16862 editor.update(cx, |editor, cx| editor.text(cx)),
16863 buffer_text.to_string() + prettier_format_suffix,
16864 "Test prettier formatting was not applied to the original buffer text",
16865 );
16866
16867 update_test_language_settings(cx, |settings| {
16868 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16869 });
16870 let format = editor.update_in(cx, |editor, window, cx| {
16871 editor.perform_format(
16872 project.clone(),
16873 FormatTrigger::Manual,
16874 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16875 window,
16876 cx,
16877 )
16878 });
16879 format.await.unwrap();
16880 assert_eq!(
16881 editor.update(cx, |editor, cx| editor.text(cx)),
16882 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16883 "Autoformatting (via test prettier) was not applied to the original buffer text",
16884 );
16885}
16886
16887#[gpui::test]
16888async fn test_addition_reverts(cx: &mut TestAppContext) {
16889 init_test(cx, |_| {});
16890 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16891 let base_text = indoc! {r#"
16892 struct Row;
16893 struct Row1;
16894 struct Row2;
16895
16896 struct Row4;
16897 struct Row5;
16898 struct Row6;
16899
16900 struct Row8;
16901 struct Row9;
16902 struct Row10;"#};
16903
16904 // When addition hunks are not adjacent to carets, no hunk revert is performed
16905 assert_hunk_revert(
16906 indoc! {r#"struct Row;
16907 struct Row1;
16908 struct Row1.1;
16909 struct Row1.2;
16910 struct Row2;ˇ
16911
16912 struct Row4;
16913 struct Row5;
16914 struct Row6;
16915
16916 struct Row8;
16917 ˇstruct Row9;
16918 struct Row9.1;
16919 struct Row9.2;
16920 struct Row9.3;
16921 struct Row10;"#},
16922 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16923 indoc! {r#"struct Row;
16924 struct Row1;
16925 struct Row1.1;
16926 struct Row1.2;
16927 struct Row2;ˇ
16928
16929 struct Row4;
16930 struct Row5;
16931 struct Row6;
16932
16933 struct Row8;
16934 ˇstruct Row9;
16935 struct Row9.1;
16936 struct Row9.2;
16937 struct Row9.3;
16938 struct Row10;"#},
16939 base_text,
16940 &mut cx,
16941 );
16942 // Same for selections
16943 assert_hunk_revert(
16944 indoc! {r#"struct Row;
16945 struct Row1;
16946 struct Row2;
16947 struct Row2.1;
16948 struct Row2.2;
16949 «ˇ
16950 struct Row4;
16951 struct» Row5;
16952 «struct Row6;
16953 ˇ»
16954 struct Row9.1;
16955 struct Row9.2;
16956 struct Row9.3;
16957 struct Row8;
16958 struct Row9;
16959 struct Row10;"#},
16960 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16961 indoc! {r#"struct Row;
16962 struct Row1;
16963 struct Row2;
16964 struct Row2.1;
16965 struct Row2.2;
16966 «ˇ
16967 struct Row4;
16968 struct» Row5;
16969 «struct Row6;
16970 ˇ»
16971 struct Row9.1;
16972 struct Row9.2;
16973 struct Row9.3;
16974 struct Row8;
16975 struct Row9;
16976 struct Row10;"#},
16977 base_text,
16978 &mut cx,
16979 );
16980
16981 // When carets and selections intersect the addition hunks, those are reverted.
16982 // Adjacent carets got merged.
16983 assert_hunk_revert(
16984 indoc! {r#"struct Row;
16985 ˇ// something on the top
16986 struct Row1;
16987 struct Row2;
16988 struct Roˇw3.1;
16989 struct Row2.2;
16990 struct Row2.3;ˇ
16991
16992 struct Row4;
16993 struct ˇRow5.1;
16994 struct Row5.2;
16995 struct «Rowˇ»5.3;
16996 struct Row5;
16997 struct Row6;
16998 ˇ
16999 struct Row9.1;
17000 struct «Rowˇ»9.2;
17001 struct «ˇRow»9.3;
17002 struct Row8;
17003 struct Row9;
17004 «ˇ// something on bottom»
17005 struct Row10;"#},
17006 vec![
17007 DiffHunkStatusKind::Added,
17008 DiffHunkStatusKind::Added,
17009 DiffHunkStatusKind::Added,
17010 DiffHunkStatusKind::Added,
17011 DiffHunkStatusKind::Added,
17012 ],
17013 indoc! {r#"struct Row;
17014 ˇstruct Row1;
17015 struct Row2;
17016 ˇ
17017 struct Row4;
17018 ˇstruct Row5;
17019 struct Row6;
17020 ˇ
17021 ˇstruct Row8;
17022 struct Row9;
17023 ˇstruct Row10;"#},
17024 base_text,
17025 &mut cx,
17026 );
17027}
17028
17029#[gpui::test]
17030async fn test_modification_reverts(cx: &mut TestAppContext) {
17031 init_test(cx, |_| {});
17032 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17033 let base_text = indoc! {r#"
17034 struct Row;
17035 struct Row1;
17036 struct Row2;
17037
17038 struct Row4;
17039 struct Row5;
17040 struct Row6;
17041
17042 struct Row8;
17043 struct Row9;
17044 struct Row10;"#};
17045
17046 // Modification hunks behave the same as the addition ones.
17047 assert_hunk_revert(
17048 indoc! {r#"struct Row;
17049 struct Row1;
17050 struct Row33;
17051 ˇ
17052 struct Row4;
17053 struct Row5;
17054 struct Row6;
17055 ˇ
17056 struct Row99;
17057 struct Row9;
17058 struct Row10;"#},
17059 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17060 indoc! {r#"struct Row;
17061 struct Row1;
17062 struct Row33;
17063 ˇ
17064 struct Row4;
17065 struct Row5;
17066 struct Row6;
17067 ˇ
17068 struct Row99;
17069 struct Row9;
17070 struct Row10;"#},
17071 base_text,
17072 &mut cx,
17073 );
17074 assert_hunk_revert(
17075 indoc! {r#"struct Row;
17076 struct Row1;
17077 struct Row33;
17078 «ˇ
17079 struct Row4;
17080 struct» Row5;
17081 «struct Row6;
17082 ˇ»
17083 struct Row99;
17084 struct Row9;
17085 struct Row10;"#},
17086 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17087 indoc! {r#"struct Row;
17088 struct Row1;
17089 struct Row33;
17090 «ˇ
17091 struct Row4;
17092 struct» Row5;
17093 «struct Row6;
17094 ˇ»
17095 struct Row99;
17096 struct Row9;
17097 struct Row10;"#},
17098 base_text,
17099 &mut cx,
17100 );
17101
17102 assert_hunk_revert(
17103 indoc! {r#"ˇstruct Row1.1;
17104 struct Row1;
17105 «ˇstr»uct Row22;
17106
17107 struct ˇRow44;
17108 struct Row5;
17109 struct «Rˇ»ow66;ˇ
17110
17111 «struˇ»ct Row88;
17112 struct Row9;
17113 struct Row1011;ˇ"#},
17114 vec![
17115 DiffHunkStatusKind::Modified,
17116 DiffHunkStatusKind::Modified,
17117 DiffHunkStatusKind::Modified,
17118 DiffHunkStatusKind::Modified,
17119 DiffHunkStatusKind::Modified,
17120 DiffHunkStatusKind::Modified,
17121 ],
17122 indoc! {r#"struct Row;
17123 ˇstruct Row1;
17124 struct Row2;
17125 ˇ
17126 struct Row4;
17127 ˇstruct Row5;
17128 struct Row6;
17129 ˇ
17130 struct Row8;
17131 ˇstruct Row9;
17132 struct Row10;ˇ"#},
17133 base_text,
17134 &mut cx,
17135 );
17136}
17137
17138#[gpui::test]
17139async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
17140 init_test(cx, |_| {});
17141 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17142 let base_text = indoc! {r#"
17143 one
17144
17145 two
17146 three
17147 "#};
17148
17149 cx.set_head_text(base_text);
17150 cx.set_state("\nˇ\n");
17151 cx.executor().run_until_parked();
17152 cx.update_editor(|editor, _window, cx| {
17153 editor.expand_selected_diff_hunks(cx);
17154 });
17155 cx.executor().run_until_parked();
17156 cx.update_editor(|editor, window, cx| {
17157 editor.backspace(&Default::default(), window, cx);
17158 });
17159 cx.run_until_parked();
17160 cx.assert_state_with_diff(
17161 indoc! {r#"
17162
17163 - two
17164 - threeˇ
17165 +
17166 "#}
17167 .to_string(),
17168 );
17169}
17170
17171#[gpui::test]
17172async fn test_deletion_reverts(cx: &mut TestAppContext) {
17173 init_test(cx, |_| {});
17174 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17175 let base_text = indoc! {r#"struct Row;
17176struct Row1;
17177struct Row2;
17178
17179struct Row4;
17180struct Row5;
17181struct Row6;
17182
17183struct Row8;
17184struct Row9;
17185struct Row10;"#};
17186
17187 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
17188 assert_hunk_revert(
17189 indoc! {r#"struct Row;
17190 struct Row2;
17191
17192 ˇstruct Row4;
17193 struct Row5;
17194 struct Row6;
17195 ˇ
17196 struct Row8;
17197 struct Row10;"#},
17198 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17199 indoc! {r#"struct Row;
17200 struct Row2;
17201
17202 ˇstruct Row4;
17203 struct Row5;
17204 struct Row6;
17205 ˇ
17206 struct Row8;
17207 struct Row10;"#},
17208 base_text,
17209 &mut cx,
17210 );
17211 assert_hunk_revert(
17212 indoc! {r#"struct Row;
17213 struct Row2;
17214
17215 «ˇstruct Row4;
17216 struct» Row5;
17217 «struct Row6;
17218 ˇ»
17219 struct Row8;
17220 struct Row10;"#},
17221 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17222 indoc! {r#"struct Row;
17223 struct Row2;
17224
17225 «ˇstruct Row4;
17226 struct» Row5;
17227 «struct Row6;
17228 ˇ»
17229 struct Row8;
17230 struct Row10;"#},
17231 base_text,
17232 &mut cx,
17233 );
17234
17235 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
17236 assert_hunk_revert(
17237 indoc! {r#"struct Row;
17238 ˇstruct Row2;
17239
17240 struct Row4;
17241 struct Row5;
17242 struct Row6;
17243
17244 struct Row8;ˇ
17245 struct Row10;"#},
17246 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17247 indoc! {r#"struct Row;
17248 struct Row1;
17249 ˇstruct Row2;
17250
17251 struct Row4;
17252 struct Row5;
17253 struct Row6;
17254
17255 struct Row8;ˇ
17256 struct Row9;
17257 struct Row10;"#},
17258 base_text,
17259 &mut cx,
17260 );
17261 assert_hunk_revert(
17262 indoc! {r#"struct Row;
17263 struct Row2«ˇ;
17264 struct Row4;
17265 struct» Row5;
17266 «struct Row6;
17267
17268 struct Row8;ˇ»
17269 struct Row10;"#},
17270 vec![
17271 DiffHunkStatusKind::Deleted,
17272 DiffHunkStatusKind::Deleted,
17273 DiffHunkStatusKind::Deleted,
17274 ],
17275 indoc! {r#"struct Row;
17276 struct Row1;
17277 struct Row2«ˇ;
17278
17279 struct Row4;
17280 struct» Row5;
17281 «struct Row6;
17282
17283 struct Row8;ˇ»
17284 struct Row9;
17285 struct Row10;"#},
17286 base_text,
17287 &mut cx,
17288 );
17289}
17290
17291#[gpui::test]
17292async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
17293 init_test(cx, |_| {});
17294
17295 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
17296 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
17297 let base_text_3 =
17298 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
17299
17300 let text_1 = edit_first_char_of_every_line(base_text_1);
17301 let text_2 = edit_first_char_of_every_line(base_text_2);
17302 let text_3 = edit_first_char_of_every_line(base_text_3);
17303
17304 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
17305 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
17306 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
17307
17308 let multibuffer = cx.new(|cx| {
17309 let mut multibuffer = MultiBuffer::new(ReadWrite);
17310 multibuffer.push_excerpts(
17311 buffer_1.clone(),
17312 [
17313 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17314 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17315 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17316 ],
17317 cx,
17318 );
17319 multibuffer.push_excerpts(
17320 buffer_2.clone(),
17321 [
17322 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17323 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17324 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17325 ],
17326 cx,
17327 );
17328 multibuffer.push_excerpts(
17329 buffer_3.clone(),
17330 [
17331 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17332 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17333 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17334 ],
17335 cx,
17336 );
17337 multibuffer
17338 });
17339
17340 let fs = FakeFs::new(cx.executor());
17341 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17342 let (editor, cx) = cx
17343 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17344 editor.update_in(cx, |editor, _window, cx| {
17345 for (buffer, diff_base) in [
17346 (buffer_1.clone(), base_text_1),
17347 (buffer_2.clone(), base_text_2),
17348 (buffer_3.clone(), base_text_3),
17349 ] {
17350 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
17351 editor
17352 .buffer
17353 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17354 }
17355 });
17356 cx.executor().run_until_parked();
17357
17358 editor.update_in(cx, |editor, window, cx| {
17359 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}");
17360 editor.select_all(&SelectAll, window, cx);
17361 editor.git_restore(&Default::default(), window, cx);
17362 });
17363 cx.executor().run_until_parked();
17364
17365 // When all ranges are selected, all buffer hunks are reverted.
17366 editor.update(cx, |editor, cx| {
17367 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");
17368 });
17369 buffer_1.update(cx, |buffer, _| {
17370 assert_eq!(buffer.text(), base_text_1);
17371 });
17372 buffer_2.update(cx, |buffer, _| {
17373 assert_eq!(buffer.text(), base_text_2);
17374 });
17375 buffer_3.update(cx, |buffer, _| {
17376 assert_eq!(buffer.text(), base_text_3);
17377 });
17378
17379 editor.update_in(cx, |editor, window, cx| {
17380 editor.undo(&Default::default(), window, cx);
17381 });
17382
17383 editor.update_in(cx, |editor, window, cx| {
17384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17385 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17386 });
17387 editor.git_restore(&Default::default(), window, cx);
17388 });
17389
17390 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17391 // but not affect buffer_2 and its related excerpts.
17392 editor.update(cx, |editor, cx| {
17393 assert_eq!(
17394 editor.text(cx),
17395 "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}"
17396 );
17397 });
17398 buffer_1.update(cx, |buffer, _| {
17399 assert_eq!(buffer.text(), base_text_1);
17400 });
17401 buffer_2.update(cx, |buffer, _| {
17402 assert_eq!(
17403 buffer.text(),
17404 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17405 );
17406 });
17407 buffer_3.update(cx, |buffer, _| {
17408 assert_eq!(
17409 buffer.text(),
17410 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17411 );
17412 });
17413
17414 fn edit_first_char_of_every_line(text: &str) -> String {
17415 text.split('\n')
17416 .map(|line| format!("X{}", &line[1..]))
17417 .collect::<Vec<_>>()
17418 .join("\n")
17419 }
17420}
17421
17422#[gpui::test]
17423async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17424 init_test(cx, |_| {});
17425
17426 let cols = 4;
17427 let rows = 10;
17428 let sample_text_1 = sample_text(rows, cols, 'a');
17429 assert_eq!(
17430 sample_text_1,
17431 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17432 );
17433 let sample_text_2 = sample_text(rows, cols, 'l');
17434 assert_eq!(
17435 sample_text_2,
17436 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17437 );
17438 let sample_text_3 = sample_text(rows, cols, 'v');
17439 assert_eq!(
17440 sample_text_3,
17441 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17442 );
17443
17444 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17445 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17446 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17447
17448 let multi_buffer = cx.new(|cx| {
17449 let mut multibuffer = MultiBuffer::new(ReadWrite);
17450 multibuffer.push_excerpts(
17451 buffer_1.clone(),
17452 [
17453 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17454 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17455 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17456 ],
17457 cx,
17458 );
17459 multibuffer.push_excerpts(
17460 buffer_2.clone(),
17461 [
17462 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17463 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17464 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17465 ],
17466 cx,
17467 );
17468 multibuffer.push_excerpts(
17469 buffer_3.clone(),
17470 [
17471 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17472 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17473 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17474 ],
17475 cx,
17476 );
17477 multibuffer
17478 });
17479
17480 let fs = FakeFs::new(cx.executor());
17481 fs.insert_tree(
17482 "/a",
17483 json!({
17484 "main.rs": sample_text_1,
17485 "other.rs": sample_text_2,
17486 "lib.rs": sample_text_3,
17487 }),
17488 )
17489 .await;
17490 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17491 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17492 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17493 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17494 Editor::new(
17495 EditorMode::full(),
17496 multi_buffer,
17497 Some(project.clone()),
17498 window,
17499 cx,
17500 )
17501 });
17502 let multibuffer_item_id = workspace
17503 .update(cx, |workspace, window, cx| {
17504 assert!(
17505 workspace.active_item(cx).is_none(),
17506 "active item should be None before the first item is added"
17507 );
17508 workspace.add_item_to_active_pane(
17509 Box::new(multi_buffer_editor.clone()),
17510 None,
17511 true,
17512 window,
17513 cx,
17514 );
17515 let active_item = workspace
17516 .active_item(cx)
17517 .expect("should have an active item after adding the multi buffer");
17518 assert!(
17519 !active_item.is_singleton(cx),
17520 "A multi buffer was expected to active after adding"
17521 );
17522 active_item.item_id()
17523 })
17524 .unwrap();
17525 cx.executor().run_until_parked();
17526
17527 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17528 editor.change_selections(
17529 SelectionEffects::scroll(Autoscroll::Next),
17530 window,
17531 cx,
17532 |s| s.select_ranges(Some(1..2)),
17533 );
17534 editor.open_excerpts(&OpenExcerpts, window, cx);
17535 });
17536 cx.executor().run_until_parked();
17537 let first_item_id = workspace
17538 .update(cx, |workspace, window, cx| {
17539 let active_item = workspace
17540 .active_item(cx)
17541 .expect("should have an active item after navigating into the 1st buffer");
17542 let first_item_id = active_item.item_id();
17543 assert_ne!(
17544 first_item_id, multibuffer_item_id,
17545 "Should navigate into the 1st buffer and activate it"
17546 );
17547 assert!(
17548 active_item.is_singleton(cx),
17549 "New active item should be a singleton buffer"
17550 );
17551 assert_eq!(
17552 active_item
17553 .act_as::<Editor>(cx)
17554 .expect("should have navigated into an editor for the 1st buffer")
17555 .read(cx)
17556 .text(cx),
17557 sample_text_1
17558 );
17559
17560 workspace
17561 .go_back(workspace.active_pane().downgrade(), window, cx)
17562 .detach_and_log_err(cx);
17563
17564 first_item_id
17565 })
17566 .unwrap();
17567 cx.executor().run_until_parked();
17568 workspace
17569 .update(cx, |workspace, _, cx| {
17570 let active_item = workspace
17571 .active_item(cx)
17572 .expect("should have an active item after navigating back");
17573 assert_eq!(
17574 active_item.item_id(),
17575 multibuffer_item_id,
17576 "Should navigate back to the multi buffer"
17577 );
17578 assert!(!active_item.is_singleton(cx));
17579 })
17580 .unwrap();
17581
17582 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17583 editor.change_selections(
17584 SelectionEffects::scroll(Autoscroll::Next),
17585 window,
17586 cx,
17587 |s| s.select_ranges(Some(39..40)),
17588 );
17589 editor.open_excerpts(&OpenExcerpts, window, cx);
17590 });
17591 cx.executor().run_until_parked();
17592 let second_item_id = workspace
17593 .update(cx, |workspace, window, cx| {
17594 let active_item = workspace
17595 .active_item(cx)
17596 .expect("should have an active item after navigating into the 2nd buffer");
17597 let second_item_id = active_item.item_id();
17598 assert_ne!(
17599 second_item_id, multibuffer_item_id,
17600 "Should navigate away from the multibuffer"
17601 );
17602 assert_ne!(
17603 second_item_id, first_item_id,
17604 "Should navigate into the 2nd buffer and activate it"
17605 );
17606 assert!(
17607 active_item.is_singleton(cx),
17608 "New active item should be a singleton buffer"
17609 );
17610 assert_eq!(
17611 active_item
17612 .act_as::<Editor>(cx)
17613 .expect("should have navigated into an editor")
17614 .read(cx)
17615 .text(cx),
17616 sample_text_2
17617 );
17618
17619 workspace
17620 .go_back(workspace.active_pane().downgrade(), window, cx)
17621 .detach_and_log_err(cx);
17622
17623 second_item_id
17624 })
17625 .unwrap();
17626 cx.executor().run_until_parked();
17627 workspace
17628 .update(cx, |workspace, _, cx| {
17629 let active_item = workspace
17630 .active_item(cx)
17631 .expect("should have an active item after navigating back from the 2nd buffer");
17632 assert_eq!(
17633 active_item.item_id(),
17634 multibuffer_item_id,
17635 "Should navigate back from the 2nd buffer to the multi buffer"
17636 );
17637 assert!(!active_item.is_singleton(cx));
17638 })
17639 .unwrap();
17640
17641 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17642 editor.change_selections(
17643 SelectionEffects::scroll(Autoscroll::Next),
17644 window,
17645 cx,
17646 |s| s.select_ranges(Some(70..70)),
17647 );
17648 editor.open_excerpts(&OpenExcerpts, window, cx);
17649 });
17650 cx.executor().run_until_parked();
17651 workspace
17652 .update(cx, |workspace, window, cx| {
17653 let active_item = workspace
17654 .active_item(cx)
17655 .expect("should have an active item after navigating into the 3rd buffer");
17656 let third_item_id = active_item.item_id();
17657 assert_ne!(
17658 third_item_id, multibuffer_item_id,
17659 "Should navigate into the 3rd buffer and activate it"
17660 );
17661 assert_ne!(third_item_id, first_item_id);
17662 assert_ne!(third_item_id, second_item_id);
17663 assert!(
17664 active_item.is_singleton(cx),
17665 "New active item should be a singleton buffer"
17666 );
17667 assert_eq!(
17668 active_item
17669 .act_as::<Editor>(cx)
17670 .expect("should have navigated into an editor")
17671 .read(cx)
17672 .text(cx),
17673 sample_text_3
17674 );
17675
17676 workspace
17677 .go_back(workspace.active_pane().downgrade(), window, cx)
17678 .detach_and_log_err(cx);
17679 })
17680 .unwrap();
17681 cx.executor().run_until_parked();
17682 workspace
17683 .update(cx, |workspace, _, cx| {
17684 let active_item = workspace
17685 .active_item(cx)
17686 .expect("should have an active item after navigating back from the 3rd buffer");
17687 assert_eq!(
17688 active_item.item_id(),
17689 multibuffer_item_id,
17690 "Should navigate back from the 3rd buffer to the multi buffer"
17691 );
17692 assert!(!active_item.is_singleton(cx));
17693 })
17694 .unwrap();
17695}
17696
17697#[gpui::test]
17698async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17699 init_test(cx, |_| {});
17700
17701 let mut cx = EditorTestContext::new(cx).await;
17702
17703 let diff_base = r#"
17704 use some::mod;
17705
17706 const A: u32 = 42;
17707
17708 fn main() {
17709 println!("hello");
17710
17711 println!("world");
17712 }
17713 "#
17714 .unindent();
17715
17716 cx.set_state(
17717 &r#"
17718 use some::modified;
17719
17720 ˇ
17721 fn main() {
17722 println!("hello there");
17723
17724 println!("around the");
17725 println!("world");
17726 }
17727 "#
17728 .unindent(),
17729 );
17730
17731 cx.set_head_text(&diff_base);
17732 executor.run_until_parked();
17733
17734 cx.update_editor(|editor, window, cx| {
17735 editor.go_to_next_hunk(&GoToHunk, window, cx);
17736 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17737 });
17738 executor.run_until_parked();
17739 cx.assert_state_with_diff(
17740 r#"
17741 use some::modified;
17742
17743
17744 fn main() {
17745 - println!("hello");
17746 + ˇ println!("hello there");
17747
17748 println!("around the");
17749 println!("world");
17750 }
17751 "#
17752 .unindent(),
17753 );
17754
17755 cx.update_editor(|editor, window, cx| {
17756 for _ in 0..2 {
17757 editor.go_to_next_hunk(&GoToHunk, window, cx);
17758 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17759 }
17760 });
17761 executor.run_until_parked();
17762 cx.assert_state_with_diff(
17763 r#"
17764 - use some::mod;
17765 + ˇuse some::modified;
17766
17767
17768 fn main() {
17769 - println!("hello");
17770 + println!("hello there");
17771
17772 + println!("around the");
17773 println!("world");
17774 }
17775 "#
17776 .unindent(),
17777 );
17778
17779 cx.update_editor(|editor, window, cx| {
17780 editor.go_to_next_hunk(&GoToHunk, window, cx);
17781 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17782 });
17783 executor.run_until_parked();
17784 cx.assert_state_with_diff(
17785 r#"
17786 - use some::mod;
17787 + use some::modified;
17788
17789 - const A: u32 = 42;
17790 ˇ
17791 fn main() {
17792 - println!("hello");
17793 + println!("hello there");
17794
17795 + println!("around the");
17796 println!("world");
17797 }
17798 "#
17799 .unindent(),
17800 );
17801
17802 cx.update_editor(|editor, window, cx| {
17803 editor.cancel(&Cancel, window, cx);
17804 });
17805
17806 cx.assert_state_with_diff(
17807 r#"
17808 use some::modified;
17809
17810 ˇ
17811 fn main() {
17812 println!("hello there");
17813
17814 println!("around the");
17815 println!("world");
17816 }
17817 "#
17818 .unindent(),
17819 );
17820}
17821
17822#[gpui::test]
17823async fn test_diff_base_change_with_expanded_diff_hunks(
17824 executor: BackgroundExecutor,
17825 cx: &mut TestAppContext,
17826) {
17827 init_test(cx, |_| {});
17828
17829 let mut cx = EditorTestContext::new(cx).await;
17830
17831 let diff_base = r#"
17832 use some::mod1;
17833 use some::mod2;
17834
17835 const A: u32 = 42;
17836 const B: u32 = 42;
17837 const C: u32 = 42;
17838
17839 fn main() {
17840 println!("hello");
17841
17842 println!("world");
17843 }
17844 "#
17845 .unindent();
17846
17847 cx.set_state(
17848 &r#"
17849 use some::mod2;
17850
17851 const A: u32 = 42;
17852 const C: u32 = 42;
17853
17854 fn main(ˇ) {
17855 //println!("hello");
17856
17857 println!("world");
17858 //
17859 //
17860 }
17861 "#
17862 .unindent(),
17863 );
17864
17865 cx.set_head_text(&diff_base);
17866 executor.run_until_parked();
17867
17868 cx.update_editor(|editor, window, cx| {
17869 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17870 });
17871 executor.run_until_parked();
17872 cx.assert_state_with_diff(
17873 r#"
17874 - use some::mod1;
17875 use some::mod2;
17876
17877 const A: u32 = 42;
17878 - const B: u32 = 42;
17879 const C: u32 = 42;
17880
17881 fn main(ˇ) {
17882 - println!("hello");
17883 + //println!("hello");
17884
17885 println!("world");
17886 + //
17887 + //
17888 }
17889 "#
17890 .unindent(),
17891 );
17892
17893 cx.set_head_text("new diff base!");
17894 executor.run_until_parked();
17895 cx.assert_state_with_diff(
17896 r#"
17897 - new diff base!
17898 + use some::mod2;
17899 +
17900 + const A: u32 = 42;
17901 + const C: u32 = 42;
17902 +
17903 + fn main(ˇ) {
17904 + //println!("hello");
17905 +
17906 + println!("world");
17907 + //
17908 + //
17909 + }
17910 "#
17911 .unindent(),
17912 );
17913}
17914
17915#[gpui::test]
17916async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17917 init_test(cx, |_| {});
17918
17919 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17920 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17921 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17922 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17923 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17924 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17925
17926 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17927 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17928 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17929
17930 let multi_buffer = cx.new(|cx| {
17931 let mut multibuffer = MultiBuffer::new(ReadWrite);
17932 multibuffer.push_excerpts(
17933 buffer_1.clone(),
17934 [
17935 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17936 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17937 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17938 ],
17939 cx,
17940 );
17941 multibuffer.push_excerpts(
17942 buffer_2.clone(),
17943 [
17944 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17945 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17946 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17947 ],
17948 cx,
17949 );
17950 multibuffer.push_excerpts(
17951 buffer_3.clone(),
17952 [
17953 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17954 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17955 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17956 ],
17957 cx,
17958 );
17959 multibuffer
17960 });
17961
17962 let editor =
17963 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17964 editor
17965 .update(cx, |editor, _window, cx| {
17966 for (buffer, diff_base) in [
17967 (buffer_1.clone(), file_1_old),
17968 (buffer_2.clone(), file_2_old),
17969 (buffer_3.clone(), file_3_old),
17970 ] {
17971 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
17972 editor
17973 .buffer
17974 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17975 }
17976 })
17977 .unwrap();
17978
17979 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17980 cx.run_until_parked();
17981
17982 cx.assert_editor_state(
17983 &"
17984 ˇaaa
17985 ccc
17986 ddd
17987
17988 ggg
17989 hhh
17990
17991
17992 lll
17993 mmm
17994 NNN
17995
17996 qqq
17997 rrr
17998
17999 uuu
18000 111
18001 222
18002 333
18003
18004 666
18005 777
18006
18007 000
18008 !!!"
18009 .unindent(),
18010 );
18011
18012 cx.update_editor(|editor, window, cx| {
18013 editor.select_all(&SelectAll, window, cx);
18014 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18015 });
18016 cx.executor().run_until_parked();
18017
18018 cx.assert_state_with_diff(
18019 "
18020 «aaa
18021 - bbb
18022 ccc
18023 ddd
18024
18025 ggg
18026 hhh
18027
18028
18029 lll
18030 mmm
18031 - nnn
18032 + NNN
18033
18034 qqq
18035 rrr
18036
18037 uuu
18038 111
18039 222
18040 333
18041
18042 + 666
18043 777
18044
18045 000
18046 !!!ˇ»"
18047 .unindent(),
18048 );
18049}
18050
18051#[gpui::test]
18052async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18053 init_test(cx, |_| {});
18054
18055 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
18056 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
18057
18058 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18059 let multi_buffer = cx.new(|cx| {
18060 let mut multibuffer = MultiBuffer::new(ReadWrite);
18061 multibuffer.push_excerpts(
18062 buffer.clone(),
18063 [
18064 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18065 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18066 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18067 ],
18068 cx,
18069 );
18070 multibuffer
18071 });
18072
18073 let editor =
18074 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18075 editor
18076 .update(cx, |editor, _window, cx| {
18077 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18078 editor
18079 .buffer
18080 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18081 })
18082 .unwrap();
18083
18084 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18085 cx.run_until_parked();
18086
18087 cx.update_editor(|editor, window, cx| {
18088 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18089 });
18090 cx.executor().run_until_parked();
18091
18092 // When the start of a hunk coincides with the start of its excerpt,
18093 // the hunk is expanded. When the start of a a hunk is earlier than
18094 // the start of its excerpt, the hunk is not expanded.
18095 cx.assert_state_with_diff(
18096 "
18097 ˇaaa
18098 - bbb
18099 + BBB
18100
18101 - ddd
18102 - eee
18103 + DDD
18104 + EEE
18105 fff
18106
18107 iii
18108 "
18109 .unindent(),
18110 );
18111}
18112
18113#[gpui::test]
18114async fn test_edits_around_expanded_insertion_hunks(
18115 executor: BackgroundExecutor,
18116 cx: &mut TestAppContext,
18117) {
18118 init_test(cx, |_| {});
18119
18120 let mut cx = EditorTestContext::new(cx).await;
18121
18122 let diff_base = r#"
18123 use some::mod1;
18124 use some::mod2;
18125
18126 const A: u32 = 42;
18127
18128 fn main() {
18129 println!("hello");
18130
18131 println!("world");
18132 }
18133 "#
18134 .unindent();
18135 executor.run_until_parked();
18136 cx.set_state(
18137 &r#"
18138 use some::mod1;
18139 use some::mod2;
18140
18141 const A: u32 = 42;
18142 const B: u32 = 42;
18143 const C: u32 = 42;
18144 ˇ
18145
18146 fn main() {
18147 println!("hello");
18148
18149 println!("world");
18150 }
18151 "#
18152 .unindent(),
18153 );
18154
18155 cx.set_head_text(&diff_base);
18156 executor.run_until_parked();
18157
18158 cx.update_editor(|editor, window, cx| {
18159 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18160 });
18161 executor.run_until_parked();
18162
18163 cx.assert_state_with_diff(
18164 r#"
18165 use some::mod1;
18166 use some::mod2;
18167
18168 const A: u32 = 42;
18169 + const B: u32 = 42;
18170 + const C: u32 = 42;
18171 + ˇ
18172
18173 fn main() {
18174 println!("hello");
18175
18176 println!("world");
18177 }
18178 "#
18179 .unindent(),
18180 );
18181
18182 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
18183 executor.run_until_parked();
18184
18185 cx.assert_state_with_diff(
18186 r#"
18187 use some::mod1;
18188 use some::mod2;
18189
18190 const A: u32 = 42;
18191 + const B: u32 = 42;
18192 + const C: u32 = 42;
18193 + const D: u32 = 42;
18194 + ˇ
18195
18196 fn main() {
18197 println!("hello");
18198
18199 println!("world");
18200 }
18201 "#
18202 .unindent(),
18203 );
18204
18205 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
18206 executor.run_until_parked();
18207
18208 cx.assert_state_with_diff(
18209 r#"
18210 use some::mod1;
18211 use some::mod2;
18212
18213 const A: u32 = 42;
18214 + const B: u32 = 42;
18215 + const C: u32 = 42;
18216 + const D: u32 = 42;
18217 + const E: u32 = 42;
18218 + ˇ
18219
18220 fn main() {
18221 println!("hello");
18222
18223 println!("world");
18224 }
18225 "#
18226 .unindent(),
18227 );
18228
18229 cx.update_editor(|editor, window, cx| {
18230 editor.delete_line(&DeleteLine, window, cx);
18231 });
18232 executor.run_until_parked();
18233
18234 cx.assert_state_with_diff(
18235 r#"
18236 use some::mod1;
18237 use some::mod2;
18238
18239 const A: u32 = 42;
18240 + const B: u32 = 42;
18241 + const C: u32 = 42;
18242 + const D: u32 = 42;
18243 + const E: u32 = 42;
18244 ˇ
18245 fn main() {
18246 println!("hello");
18247
18248 println!("world");
18249 }
18250 "#
18251 .unindent(),
18252 );
18253
18254 cx.update_editor(|editor, window, cx| {
18255 editor.move_up(&MoveUp, window, cx);
18256 editor.delete_line(&DeleteLine, window, cx);
18257 editor.move_up(&MoveUp, window, cx);
18258 editor.delete_line(&DeleteLine, window, cx);
18259 editor.move_up(&MoveUp, window, cx);
18260 editor.delete_line(&DeleteLine, window, cx);
18261 });
18262 executor.run_until_parked();
18263 cx.assert_state_with_diff(
18264 r#"
18265 use some::mod1;
18266 use some::mod2;
18267
18268 const A: u32 = 42;
18269 + const B: u32 = 42;
18270 ˇ
18271 fn main() {
18272 println!("hello");
18273
18274 println!("world");
18275 }
18276 "#
18277 .unindent(),
18278 );
18279
18280 cx.update_editor(|editor, window, cx| {
18281 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
18282 editor.delete_line(&DeleteLine, window, cx);
18283 });
18284 executor.run_until_parked();
18285 cx.assert_state_with_diff(
18286 r#"
18287 ˇ
18288 fn main() {
18289 println!("hello");
18290
18291 println!("world");
18292 }
18293 "#
18294 .unindent(),
18295 );
18296}
18297
18298#[gpui::test]
18299async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
18300 init_test(cx, |_| {});
18301
18302 let mut cx = EditorTestContext::new(cx).await;
18303 cx.set_head_text(indoc! { "
18304 one
18305 two
18306 three
18307 four
18308 five
18309 "
18310 });
18311 cx.set_state(indoc! { "
18312 one
18313 ˇthree
18314 five
18315 "});
18316 cx.run_until_parked();
18317 cx.update_editor(|editor, window, cx| {
18318 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18319 });
18320 cx.assert_state_with_diff(
18321 indoc! { "
18322 one
18323 - two
18324 ˇthree
18325 - four
18326 five
18327 "}
18328 .to_string(),
18329 );
18330 cx.update_editor(|editor, window, cx| {
18331 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18332 });
18333
18334 cx.assert_state_with_diff(
18335 indoc! { "
18336 one
18337 ˇthree
18338 five
18339 "}
18340 .to_string(),
18341 );
18342
18343 cx.set_state(indoc! { "
18344 one
18345 ˇTWO
18346 three
18347 four
18348 five
18349 "});
18350 cx.run_until_parked();
18351 cx.update_editor(|editor, window, cx| {
18352 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18353 });
18354
18355 cx.assert_state_with_diff(
18356 indoc! { "
18357 one
18358 - two
18359 + ˇTWO
18360 three
18361 four
18362 five
18363 "}
18364 .to_string(),
18365 );
18366 cx.update_editor(|editor, window, cx| {
18367 editor.move_up(&Default::default(), window, cx);
18368 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18369 });
18370 cx.assert_state_with_diff(
18371 indoc! { "
18372 one
18373 ˇTWO
18374 three
18375 four
18376 five
18377 "}
18378 .to_string(),
18379 );
18380}
18381
18382#[gpui::test]
18383async fn test_edits_around_expanded_deletion_hunks(
18384 executor: BackgroundExecutor,
18385 cx: &mut TestAppContext,
18386) {
18387 init_test(cx, |_| {});
18388
18389 let mut cx = EditorTestContext::new(cx).await;
18390
18391 let diff_base = r#"
18392 use some::mod1;
18393 use some::mod2;
18394
18395 const A: u32 = 42;
18396 const B: u32 = 42;
18397 const C: u32 = 42;
18398
18399
18400 fn main() {
18401 println!("hello");
18402
18403 println!("world");
18404 }
18405 "#
18406 .unindent();
18407 executor.run_until_parked();
18408 cx.set_state(
18409 &r#"
18410 use some::mod1;
18411 use some::mod2;
18412
18413 ˇconst B: u32 = 42;
18414 const C: u32 = 42;
18415
18416
18417 fn main() {
18418 println!("hello");
18419
18420 println!("world");
18421 }
18422 "#
18423 .unindent(),
18424 );
18425
18426 cx.set_head_text(&diff_base);
18427 executor.run_until_parked();
18428
18429 cx.update_editor(|editor, window, cx| {
18430 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18431 });
18432 executor.run_until_parked();
18433
18434 cx.assert_state_with_diff(
18435 r#"
18436 use some::mod1;
18437 use some::mod2;
18438
18439 - const A: u32 = 42;
18440 ˇconst B: u32 = 42;
18441 const C: u32 = 42;
18442
18443
18444 fn main() {
18445 println!("hello");
18446
18447 println!("world");
18448 }
18449 "#
18450 .unindent(),
18451 );
18452
18453 cx.update_editor(|editor, window, cx| {
18454 editor.delete_line(&DeleteLine, window, cx);
18455 });
18456 executor.run_until_parked();
18457 cx.assert_state_with_diff(
18458 r#"
18459 use some::mod1;
18460 use some::mod2;
18461
18462 - const A: u32 = 42;
18463 - const B: u32 = 42;
18464 ˇconst C: u32 = 42;
18465
18466
18467 fn main() {
18468 println!("hello");
18469
18470 println!("world");
18471 }
18472 "#
18473 .unindent(),
18474 );
18475
18476 cx.update_editor(|editor, window, cx| {
18477 editor.delete_line(&DeleteLine, window, cx);
18478 });
18479 executor.run_until_parked();
18480 cx.assert_state_with_diff(
18481 r#"
18482 use some::mod1;
18483 use some::mod2;
18484
18485 - const A: u32 = 42;
18486 - const B: u32 = 42;
18487 - const C: u32 = 42;
18488 ˇ
18489
18490 fn main() {
18491 println!("hello");
18492
18493 println!("world");
18494 }
18495 "#
18496 .unindent(),
18497 );
18498
18499 cx.update_editor(|editor, window, cx| {
18500 editor.handle_input("replacement", window, cx);
18501 });
18502 executor.run_until_parked();
18503 cx.assert_state_with_diff(
18504 r#"
18505 use some::mod1;
18506 use some::mod2;
18507
18508 - const A: u32 = 42;
18509 - const B: u32 = 42;
18510 - const C: u32 = 42;
18511 -
18512 + replacementˇ
18513
18514 fn main() {
18515 println!("hello");
18516
18517 println!("world");
18518 }
18519 "#
18520 .unindent(),
18521 );
18522}
18523
18524#[gpui::test]
18525async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18526 init_test(cx, |_| {});
18527
18528 let mut cx = EditorTestContext::new(cx).await;
18529
18530 let base_text = r#"
18531 one
18532 two
18533 three
18534 four
18535 five
18536 "#
18537 .unindent();
18538 executor.run_until_parked();
18539 cx.set_state(
18540 &r#"
18541 one
18542 two
18543 fˇour
18544 five
18545 "#
18546 .unindent(),
18547 );
18548
18549 cx.set_head_text(&base_text);
18550 executor.run_until_parked();
18551
18552 cx.update_editor(|editor, window, cx| {
18553 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18554 });
18555 executor.run_until_parked();
18556
18557 cx.assert_state_with_diff(
18558 r#"
18559 one
18560 two
18561 - three
18562 fˇour
18563 five
18564 "#
18565 .unindent(),
18566 );
18567
18568 cx.update_editor(|editor, window, cx| {
18569 editor.backspace(&Backspace, window, cx);
18570 editor.backspace(&Backspace, window, cx);
18571 });
18572 executor.run_until_parked();
18573 cx.assert_state_with_diff(
18574 r#"
18575 one
18576 two
18577 - threeˇ
18578 - four
18579 + our
18580 five
18581 "#
18582 .unindent(),
18583 );
18584}
18585
18586#[gpui::test]
18587async fn test_edit_after_expanded_modification_hunk(
18588 executor: BackgroundExecutor,
18589 cx: &mut TestAppContext,
18590) {
18591 init_test(cx, |_| {});
18592
18593 let mut cx = EditorTestContext::new(cx).await;
18594
18595 let diff_base = r#"
18596 use some::mod1;
18597 use some::mod2;
18598
18599 const A: u32 = 42;
18600 const B: u32 = 42;
18601 const C: u32 = 42;
18602 const D: u32 = 42;
18603
18604
18605 fn main() {
18606 println!("hello");
18607
18608 println!("world");
18609 }"#
18610 .unindent();
18611
18612 cx.set_state(
18613 &r#"
18614 use some::mod1;
18615 use some::mod2;
18616
18617 const A: u32 = 42;
18618 const B: u32 = 42;
18619 const C: u32 = 43ˇ
18620 const D: u32 = 42;
18621
18622
18623 fn main() {
18624 println!("hello");
18625
18626 println!("world");
18627 }"#
18628 .unindent(),
18629 );
18630
18631 cx.set_head_text(&diff_base);
18632 executor.run_until_parked();
18633 cx.update_editor(|editor, window, cx| {
18634 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18635 });
18636 executor.run_until_parked();
18637
18638 cx.assert_state_with_diff(
18639 r#"
18640 use some::mod1;
18641 use some::mod2;
18642
18643 const A: u32 = 42;
18644 const B: u32 = 42;
18645 - const C: u32 = 42;
18646 + const C: u32 = 43ˇ
18647 const D: u32 = 42;
18648
18649
18650 fn main() {
18651 println!("hello");
18652
18653 println!("world");
18654 }"#
18655 .unindent(),
18656 );
18657
18658 cx.update_editor(|editor, window, cx| {
18659 editor.handle_input("\nnew_line\n", window, cx);
18660 });
18661 executor.run_until_parked();
18662
18663 cx.assert_state_with_diff(
18664 r#"
18665 use some::mod1;
18666 use some::mod2;
18667
18668 const A: u32 = 42;
18669 const B: u32 = 42;
18670 - const C: u32 = 42;
18671 + const C: u32 = 43
18672 + new_line
18673 + ˇ
18674 const D: u32 = 42;
18675
18676
18677 fn main() {
18678 println!("hello");
18679
18680 println!("world");
18681 }"#
18682 .unindent(),
18683 );
18684}
18685
18686#[gpui::test]
18687async fn test_stage_and_unstage_added_file_hunk(
18688 executor: BackgroundExecutor,
18689 cx: &mut TestAppContext,
18690) {
18691 init_test(cx, |_| {});
18692
18693 let mut cx = EditorTestContext::new(cx).await;
18694 cx.update_editor(|editor, _, cx| {
18695 editor.set_expand_all_diff_hunks(cx);
18696 });
18697
18698 let working_copy = r#"
18699 ˇfn main() {
18700 println!("hello, world!");
18701 }
18702 "#
18703 .unindent();
18704
18705 cx.set_state(&working_copy);
18706 executor.run_until_parked();
18707
18708 cx.assert_state_with_diff(
18709 r#"
18710 + ˇfn main() {
18711 + println!("hello, world!");
18712 + }
18713 "#
18714 .unindent(),
18715 );
18716 cx.assert_index_text(None);
18717
18718 cx.update_editor(|editor, window, cx| {
18719 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18720 });
18721 executor.run_until_parked();
18722 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18723 cx.assert_state_with_diff(
18724 r#"
18725 + ˇfn main() {
18726 + println!("hello, world!");
18727 + }
18728 "#
18729 .unindent(),
18730 );
18731
18732 cx.update_editor(|editor, window, cx| {
18733 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18734 });
18735 executor.run_until_parked();
18736 cx.assert_index_text(None);
18737}
18738
18739async fn setup_indent_guides_editor(
18740 text: &str,
18741 cx: &mut TestAppContext,
18742) -> (BufferId, EditorTestContext) {
18743 init_test(cx, |_| {});
18744
18745 let mut cx = EditorTestContext::new(cx).await;
18746
18747 let buffer_id = cx.update_editor(|editor, window, cx| {
18748 editor.set_text(text, window, cx);
18749 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18750
18751 buffer_ids[0]
18752 });
18753
18754 (buffer_id, cx)
18755}
18756
18757fn assert_indent_guides(
18758 range: Range<u32>,
18759 expected: Vec<IndentGuide>,
18760 active_indices: Option<Vec<usize>>,
18761 cx: &mut EditorTestContext,
18762) {
18763 let indent_guides = cx.update_editor(|editor, window, cx| {
18764 let snapshot = editor.snapshot(window, cx).display_snapshot;
18765 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18766 editor,
18767 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18768 true,
18769 &snapshot,
18770 cx,
18771 );
18772
18773 indent_guides.sort_by(|a, b| {
18774 a.depth.cmp(&b.depth).then(
18775 a.start_row
18776 .cmp(&b.start_row)
18777 .then(a.end_row.cmp(&b.end_row)),
18778 )
18779 });
18780 indent_guides
18781 });
18782
18783 if let Some(expected) = active_indices {
18784 let active_indices = cx.update_editor(|editor, window, cx| {
18785 let snapshot = editor.snapshot(window, cx).display_snapshot;
18786 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18787 });
18788
18789 assert_eq!(
18790 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18791 expected,
18792 "Active indent guide indices do not match"
18793 );
18794 }
18795
18796 assert_eq!(indent_guides, expected, "Indent guides do not match");
18797}
18798
18799fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18800 IndentGuide {
18801 buffer_id,
18802 start_row: MultiBufferRow(start_row),
18803 end_row: MultiBufferRow(end_row),
18804 depth,
18805 tab_size: 4,
18806 settings: IndentGuideSettings {
18807 enabled: true,
18808 line_width: 1,
18809 active_line_width: 1,
18810 ..Default::default()
18811 },
18812 }
18813}
18814
18815#[gpui::test]
18816async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18817 let (buffer_id, mut cx) = setup_indent_guides_editor(
18818 &"
18819 fn main() {
18820 let a = 1;
18821 }"
18822 .unindent(),
18823 cx,
18824 )
18825 .await;
18826
18827 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18828}
18829
18830#[gpui::test]
18831async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18832 let (buffer_id, mut cx) = setup_indent_guides_editor(
18833 &"
18834 fn main() {
18835 let a = 1;
18836 let b = 2;
18837 }"
18838 .unindent(),
18839 cx,
18840 )
18841 .await;
18842
18843 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18844}
18845
18846#[gpui::test]
18847async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18848 let (buffer_id, mut cx) = setup_indent_guides_editor(
18849 &"
18850 fn main() {
18851 let a = 1;
18852 if a == 3 {
18853 let b = 2;
18854 } else {
18855 let c = 3;
18856 }
18857 }"
18858 .unindent(),
18859 cx,
18860 )
18861 .await;
18862
18863 assert_indent_guides(
18864 0..8,
18865 vec![
18866 indent_guide(buffer_id, 1, 6, 0),
18867 indent_guide(buffer_id, 3, 3, 1),
18868 indent_guide(buffer_id, 5, 5, 1),
18869 ],
18870 None,
18871 &mut cx,
18872 );
18873}
18874
18875#[gpui::test]
18876async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18877 let (buffer_id, mut cx) = setup_indent_guides_editor(
18878 &"
18879 fn main() {
18880 let a = 1;
18881 let b = 2;
18882 let c = 3;
18883 }"
18884 .unindent(),
18885 cx,
18886 )
18887 .await;
18888
18889 assert_indent_guides(
18890 0..5,
18891 vec![
18892 indent_guide(buffer_id, 1, 3, 0),
18893 indent_guide(buffer_id, 2, 2, 1),
18894 ],
18895 None,
18896 &mut cx,
18897 );
18898}
18899
18900#[gpui::test]
18901async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18902 let (buffer_id, mut cx) = setup_indent_guides_editor(
18903 &"
18904 fn main() {
18905 let a = 1;
18906
18907 let c = 3;
18908 }"
18909 .unindent(),
18910 cx,
18911 )
18912 .await;
18913
18914 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18915}
18916
18917#[gpui::test]
18918async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18919 let (buffer_id, mut cx) = setup_indent_guides_editor(
18920 &"
18921 fn main() {
18922 let a = 1;
18923
18924 let c = 3;
18925
18926 if a == 3 {
18927 let b = 2;
18928 } else {
18929 let c = 3;
18930 }
18931 }"
18932 .unindent(),
18933 cx,
18934 )
18935 .await;
18936
18937 assert_indent_guides(
18938 0..11,
18939 vec![
18940 indent_guide(buffer_id, 1, 9, 0),
18941 indent_guide(buffer_id, 6, 6, 1),
18942 indent_guide(buffer_id, 8, 8, 1),
18943 ],
18944 None,
18945 &mut cx,
18946 );
18947}
18948
18949#[gpui::test]
18950async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18951 let (buffer_id, mut cx) = setup_indent_guides_editor(
18952 &"
18953 fn main() {
18954 let a = 1;
18955
18956 let c = 3;
18957
18958 if a == 3 {
18959 let b = 2;
18960 } else {
18961 let c = 3;
18962 }
18963 }"
18964 .unindent(),
18965 cx,
18966 )
18967 .await;
18968
18969 assert_indent_guides(
18970 1..11,
18971 vec![
18972 indent_guide(buffer_id, 1, 9, 0),
18973 indent_guide(buffer_id, 6, 6, 1),
18974 indent_guide(buffer_id, 8, 8, 1),
18975 ],
18976 None,
18977 &mut cx,
18978 );
18979}
18980
18981#[gpui::test]
18982async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18983 let (buffer_id, mut cx) = setup_indent_guides_editor(
18984 &"
18985 fn main() {
18986 let a = 1;
18987
18988 let c = 3;
18989
18990 if a == 3 {
18991 let b = 2;
18992 } else {
18993 let c = 3;
18994 }
18995 }"
18996 .unindent(),
18997 cx,
18998 )
18999 .await;
19000
19001 assert_indent_guides(
19002 1..10,
19003 vec![
19004 indent_guide(buffer_id, 1, 9, 0),
19005 indent_guide(buffer_id, 6, 6, 1),
19006 indent_guide(buffer_id, 8, 8, 1),
19007 ],
19008 None,
19009 &mut cx,
19010 );
19011}
19012
19013#[gpui::test]
19014async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
19015 let (buffer_id, mut cx) = setup_indent_guides_editor(
19016 &"
19017 fn main() {
19018 if a {
19019 b(
19020 c,
19021 d,
19022 )
19023 } else {
19024 e(
19025 f
19026 )
19027 }
19028 }"
19029 .unindent(),
19030 cx,
19031 )
19032 .await;
19033
19034 assert_indent_guides(
19035 0..11,
19036 vec![
19037 indent_guide(buffer_id, 1, 10, 0),
19038 indent_guide(buffer_id, 2, 5, 1),
19039 indent_guide(buffer_id, 7, 9, 1),
19040 indent_guide(buffer_id, 3, 4, 2),
19041 indent_guide(buffer_id, 8, 8, 2),
19042 ],
19043 None,
19044 &mut cx,
19045 );
19046
19047 cx.update_editor(|editor, window, cx| {
19048 editor.fold_at(MultiBufferRow(2), window, cx);
19049 assert_eq!(
19050 editor.display_text(cx),
19051 "
19052 fn main() {
19053 if a {
19054 b(⋯
19055 )
19056 } else {
19057 e(
19058 f
19059 )
19060 }
19061 }"
19062 .unindent()
19063 );
19064 });
19065
19066 assert_indent_guides(
19067 0..11,
19068 vec![
19069 indent_guide(buffer_id, 1, 10, 0),
19070 indent_guide(buffer_id, 2, 5, 1),
19071 indent_guide(buffer_id, 7, 9, 1),
19072 indent_guide(buffer_id, 8, 8, 2),
19073 ],
19074 None,
19075 &mut cx,
19076 );
19077}
19078
19079#[gpui::test]
19080async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19081 let (buffer_id, mut cx) = setup_indent_guides_editor(
19082 &"
19083 block1
19084 block2
19085 block3
19086 block4
19087 block2
19088 block1
19089 block1"
19090 .unindent(),
19091 cx,
19092 )
19093 .await;
19094
19095 assert_indent_guides(
19096 1..10,
19097 vec![
19098 indent_guide(buffer_id, 1, 4, 0),
19099 indent_guide(buffer_id, 2, 3, 1),
19100 indent_guide(buffer_id, 3, 3, 2),
19101 ],
19102 None,
19103 &mut cx,
19104 );
19105}
19106
19107#[gpui::test]
19108async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19109 let (buffer_id, mut cx) = setup_indent_guides_editor(
19110 &"
19111 block1
19112 block2
19113 block3
19114
19115 block1
19116 block1"
19117 .unindent(),
19118 cx,
19119 )
19120 .await;
19121
19122 assert_indent_guides(
19123 0..6,
19124 vec![
19125 indent_guide(buffer_id, 1, 2, 0),
19126 indent_guide(buffer_id, 2, 2, 1),
19127 ],
19128 None,
19129 &mut cx,
19130 );
19131}
19132
19133#[gpui::test]
19134async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
19135 let (buffer_id, mut cx) = setup_indent_guides_editor(
19136 &"
19137 function component() {
19138 \treturn (
19139 \t\t\t
19140 \t\t<div>
19141 \t\t\t<abc></abc>
19142 \t\t</div>
19143 \t)
19144 }"
19145 .unindent(),
19146 cx,
19147 )
19148 .await;
19149
19150 assert_indent_guides(
19151 0..8,
19152 vec![
19153 indent_guide(buffer_id, 1, 6, 0),
19154 indent_guide(buffer_id, 2, 5, 1),
19155 indent_guide(buffer_id, 4, 4, 2),
19156 ],
19157 None,
19158 &mut cx,
19159 );
19160}
19161
19162#[gpui::test]
19163async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
19164 let (buffer_id, mut cx) = setup_indent_guides_editor(
19165 &"
19166 function component() {
19167 \treturn (
19168 \t
19169 \t\t<div>
19170 \t\t\t<abc></abc>
19171 \t\t</div>
19172 \t)
19173 }"
19174 .unindent(),
19175 cx,
19176 )
19177 .await;
19178
19179 assert_indent_guides(
19180 0..8,
19181 vec![
19182 indent_guide(buffer_id, 1, 6, 0),
19183 indent_guide(buffer_id, 2, 5, 1),
19184 indent_guide(buffer_id, 4, 4, 2),
19185 ],
19186 None,
19187 &mut cx,
19188 );
19189}
19190
19191#[gpui::test]
19192async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
19193 let (buffer_id, mut cx) = setup_indent_guides_editor(
19194 &"
19195 block1
19196
19197
19198
19199 block2
19200 "
19201 .unindent(),
19202 cx,
19203 )
19204 .await;
19205
19206 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19207}
19208
19209#[gpui::test]
19210async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
19211 let (buffer_id, mut cx) = setup_indent_guides_editor(
19212 &"
19213 def a:
19214 \tb = 3
19215 \tif True:
19216 \t\tc = 4
19217 \t\td = 5
19218 \tprint(b)
19219 "
19220 .unindent(),
19221 cx,
19222 )
19223 .await;
19224
19225 assert_indent_guides(
19226 0..6,
19227 vec![
19228 indent_guide(buffer_id, 1, 5, 0),
19229 indent_guide(buffer_id, 3, 4, 1),
19230 ],
19231 None,
19232 &mut cx,
19233 );
19234}
19235
19236#[gpui::test]
19237async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
19238 let (buffer_id, mut cx) = setup_indent_guides_editor(
19239 &"
19240 fn main() {
19241 let a = 1;
19242 }"
19243 .unindent(),
19244 cx,
19245 )
19246 .await;
19247
19248 cx.update_editor(|editor, window, cx| {
19249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19250 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19251 });
19252 });
19253
19254 assert_indent_guides(
19255 0..3,
19256 vec![indent_guide(buffer_id, 1, 1, 0)],
19257 Some(vec![0]),
19258 &mut cx,
19259 );
19260}
19261
19262#[gpui::test]
19263async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
19264 let (buffer_id, mut cx) = setup_indent_guides_editor(
19265 &"
19266 fn main() {
19267 if 1 == 2 {
19268 let a = 1;
19269 }
19270 }"
19271 .unindent(),
19272 cx,
19273 )
19274 .await;
19275
19276 cx.update_editor(|editor, window, cx| {
19277 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19278 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19279 });
19280 });
19281
19282 assert_indent_guides(
19283 0..4,
19284 vec![
19285 indent_guide(buffer_id, 1, 3, 0),
19286 indent_guide(buffer_id, 2, 2, 1),
19287 ],
19288 Some(vec![1]),
19289 &mut cx,
19290 );
19291
19292 cx.update_editor(|editor, window, cx| {
19293 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19294 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19295 });
19296 });
19297
19298 assert_indent_guides(
19299 0..4,
19300 vec![
19301 indent_guide(buffer_id, 1, 3, 0),
19302 indent_guide(buffer_id, 2, 2, 1),
19303 ],
19304 Some(vec![1]),
19305 &mut cx,
19306 );
19307
19308 cx.update_editor(|editor, window, cx| {
19309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19310 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
19311 });
19312 });
19313
19314 assert_indent_guides(
19315 0..4,
19316 vec![
19317 indent_guide(buffer_id, 1, 3, 0),
19318 indent_guide(buffer_id, 2, 2, 1),
19319 ],
19320 Some(vec![0]),
19321 &mut cx,
19322 );
19323}
19324
19325#[gpui::test]
19326async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
19327 let (buffer_id, mut cx) = setup_indent_guides_editor(
19328 &"
19329 fn main() {
19330 let a = 1;
19331
19332 let b = 2;
19333 }"
19334 .unindent(),
19335 cx,
19336 )
19337 .await;
19338
19339 cx.update_editor(|editor, window, cx| {
19340 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19341 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19342 });
19343 });
19344
19345 assert_indent_guides(
19346 0..5,
19347 vec![indent_guide(buffer_id, 1, 3, 0)],
19348 Some(vec![0]),
19349 &mut cx,
19350 );
19351}
19352
19353#[gpui::test]
19354async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19355 let (buffer_id, mut cx) = setup_indent_guides_editor(
19356 &"
19357 def m:
19358 a = 1
19359 pass"
19360 .unindent(),
19361 cx,
19362 )
19363 .await;
19364
19365 cx.update_editor(|editor, window, cx| {
19366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19367 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19368 });
19369 });
19370
19371 assert_indent_guides(
19372 0..3,
19373 vec![indent_guide(buffer_id, 1, 2, 0)],
19374 Some(vec![0]),
19375 &mut cx,
19376 );
19377}
19378
19379#[gpui::test]
19380async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19381 init_test(cx, |_| {});
19382 let mut cx = EditorTestContext::new(cx).await;
19383 let text = indoc! {
19384 "
19385 impl A {
19386 fn b() {
19387 0;
19388 3;
19389 5;
19390 6;
19391 7;
19392 }
19393 }
19394 "
19395 };
19396 let base_text = indoc! {
19397 "
19398 impl A {
19399 fn b() {
19400 0;
19401 1;
19402 2;
19403 3;
19404 4;
19405 }
19406 fn c() {
19407 5;
19408 6;
19409 7;
19410 }
19411 }
19412 "
19413 };
19414
19415 cx.update_editor(|editor, window, cx| {
19416 editor.set_text(text, window, cx);
19417
19418 editor.buffer().update(cx, |multibuffer, cx| {
19419 let buffer = multibuffer.as_singleton().unwrap();
19420 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19421
19422 multibuffer.set_all_diff_hunks_expanded(cx);
19423 multibuffer.add_diff(diff, cx);
19424
19425 buffer.read(cx).remote_id()
19426 })
19427 });
19428 cx.run_until_parked();
19429
19430 cx.assert_state_with_diff(
19431 indoc! { "
19432 impl A {
19433 fn b() {
19434 0;
19435 - 1;
19436 - 2;
19437 3;
19438 - 4;
19439 - }
19440 - fn c() {
19441 5;
19442 6;
19443 7;
19444 }
19445 }
19446 ˇ"
19447 }
19448 .to_string(),
19449 );
19450
19451 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19452 editor
19453 .snapshot(window, cx)
19454 .buffer_snapshot
19455 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19456 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19457 .collect::<Vec<_>>()
19458 });
19459 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19460 assert_eq!(
19461 actual_guides,
19462 vec![
19463 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19464 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19465 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19466 ]
19467 );
19468}
19469
19470#[gpui::test]
19471async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19472 init_test(cx, |_| {});
19473 let mut cx = EditorTestContext::new(cx).await;
19474
19475 let diff_base = r#"
19476 a
19477 b
19478 c
19479 "#
19480 .unindent();
19481
19482 cx.set_state(
19483 &r#"
19484 ˇA
19485 b
19486 C
19487 "#
19488 .unindent(),
19489 );
19490 cx.set_head_text(&diff_base);
19491 cx.update_editor(|editor, window, cx| {
19492 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19493 });
19494 executor.run_until_parked();
19495
19496 let both_hunks_expanded = r#"
19497 - a
19498 + ˇA
19499 b
19500 - c
19501 + C
19502 "#
19503 .unindent();
19504
19505 cx.assert_state_with_diff(both_hunks_expanded.clone());
19506
19507 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19508 let snapshot = editor.snapshot(window, cx);
19509 let hunks = editor
19510 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19511 .collect::<Vec<_>>();
19512 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19513 let buffer_id = hunks[0].buffer_id;
19514 hunks
19515 .into_iter()
19516 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19517 .collect::<Vec<_>>()
19518 });
19519 assert_eq!(hunk_ranges.len(), 2);
19520
19521 cx.update_editor(|editor, _, cx| {
19522 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19523 });
19524 executor.run_until_parked();
19525
19526 let second_hunk_expanded = r#"
19527 ˇA
19528 b
19529 - c
19530 + C
19531 "#
19532 .unindent();
19533
19534 cx.assert_state_with_diff(second_hunk_expanded);
19535
19536 cx.update_editor(|editor, _, cx| {
19537 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19538 });
19539 executor.run_until_parked();
19540
19541 cx.assert_state_with_diff(both_hunks_expanded.clone());
19542
19543 cx.update_editor(|editor, _, cx| {
19544 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19545 });
19546 executor.run_until_parked();
19547
19548 let first_hunk_expanded = r#"
19549 - a
19550 + ˇA
19551 b
19552 C
19553 "#
19554 .unindent();
19555
19556 cx.assert_state_with_diff(first_hunk_expanded);
19557
19558 cx.update_editor(|editor, _, cx| {
19559 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19560 });
19561 executor.run_until_parked();
19562
19563 cx.assert_state_with_diff(both_hunks_expanded);
19564
19565 cx.set_state(
19566 &r#"
19567 ˇA
19568 b
19569 "#
19570 .unindent(),
19571 );
19572 cx.run_until_parked();
19573
19574 // TODO this cursor position seems bad
19575 cx.assert_state_with_diff(
19576 r#"
19577 - ˇa
19578 + A
19579 b
19580 "#
19581 .unindent(),
19582 );
19583
19584 cx.update_editor(|editor, window, cx| {
19585 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19586 });
19587
19588 cx.assert_state_with_diff(
19589 r#"
19590 - ˇa
19591 + A
19592 b
19593 - c
19594 "#
19595 .unindent(),
19596 );
19597
19598 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19599 let snapshot = editor.snapshot(window, cx);
19600 let hunks = editor
19601 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19602 .collect::<Vec<_>>();
19603 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19604 let buffer_id = hunks[0].buffer_id;
19605 hunks
19606 .into_iter()
19607 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19608 .collect::<Vec<_>>()
19609 });
19610 assert_eq!(hunk_ranges.len(), 2);
19611
19612 cx.update_editor(|editor, _, cx| {
19613 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19614 });
19615 executor.run_until_parked();
19616
19617 cx.assert_state_with_diff(
19618 r#"
19619 - ˇa
19620 + A
19621 b
19622 "#
19623 .unindent(),
19624 );
19625}
19626
19627#[gpui::test]
19628async fn test_toggle_deletion_hunk_at_start_of_file(
19629 executor: BackgroundExecutor,
19630 cx: &mut TestAppContext,
19631) {
19632 init_test(cx, |_| {});
19633 let mut cx = EditorTestContext::new(cx).await;
19634
19635 let diff_base = r#"
19636 a
19637 b
19638 c
19639 "#
19640 .unindent();
19641
19642 cx.set_state(
19643 &r#"
19644 ˇb
19645 c
19646 "#
19647 .unindent(),
19648 );
19649 cx.set_head_text(&diff_base);
19650 cx.update_editor(|editor, window, cx| {
19651 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19652 });
19653 executor.run_until_parked();
19654
19655 let hunk_expanded = r#"
19656 - a
19657 ˇb
19658 c
19659 "#
19660 .unindent();
19661
19662 cx.assert_state_with_diff(hunk_expanded.clone());
19663
19664 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19665 let snapshot = editor.snapshot(window, cx);
19666 let hunks = editor
19667 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19668 .collect::<Vec<_>>();
19669 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19670 let buffer_id = hunks[0].buffer_id;
19671 hunks
19672 .into_iter()
19673 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19674 .collect::<Vec<_>>()
19675 });
19676 assert_eq!(hunk_ranges.len(), 1);
19677
19678 cx.update_editor(|editor, _, cx| {
19679 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19680 });
19681 executor.run_until_parked();
19682
19683 let hunk_collapsed = r#"
19684 ˇb
19685 c
19686 "#
19687 .unindent();
19688
19689 cx.assert_state_with_diff(hunk_collapsed);
19690
19691 cx.update_editor(|editor, _, cx| {
19692 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19693 });
19694 executor.run_until_parked();
19695
19696 cx.assert_state_with_diff(hunk_expanded);
19697}
19698
19699#[gpui::test]
19700async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19701 init_test(cx, |_| {});
19702
19703 let fs = FakeFs::new(cx.executor());
19704 fs.insert_tree(
19705 path!("/test"),
19706 json!({
19707 ".git": {},
19708 "file-1": "ONE\n",
19709 "file-2": "TWO\n",
19710 "file-3": "THREE\n",
19711 }),
19712 )
19713 .await;
19714
19715 fs.set_head_for_repo(
19716 path!("/test/.git").as_ref(),
19717 &[
19718 ("file-1".into(), "one\n".into()),
19719 ("file-2".into(), "two\n".into()),
19720 ("file-3".into(), "three\n".into()),
19721 ],
19722 "deadbeef",
19723 );
19724
19725 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19726 let mut buffers = vec![];
19727 for i in 1..=3 {
19728 let buffer = project
19729 .update(cx, |project, cx| {
19730 let path = format!(path!("/test/file-{}"), i);
19731 project.open_local_buffer(path, cx)
19732 })
19733 .await
19734 .unwrap();
19735 buffers.push(buffer);
19736 }
19737
19738 let multibuffer = cx.new(|cx| {
19739 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19740 multibuffer.set_all_diff_hunks_expanded(cx);
19741 for buffer in &buffers {
19742 let snapshot = buffer.read(cx).snapshot();
19743 multibuffer.set_excerpts_for_path(
19744 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19745 buffer.clone(),
19746 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19747 DEFAULT_MULTIBUFFER_CONTEXT,
19748 cx,
19749 );
19750 }
19751 multibuffer
19752 });
19753
19754 let editor = cx.add_window(|window, cx| {
19755 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19756 });
19757 cx.run_until_parked();
19758
19759 let snapshot = editor
19760 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19761 .unwrap();
19762 let hunks = snapshot
19763 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19764 .map(|hunk| match hunk {
19765 DisplayDiffHunk::Unfolded {
19766 display_row_range, ..
19767 } => display_row_range,
19768 DisplayDiffHunk::Folded { .. } => unreachable!(),
19769 })
19770 .collect::<Vec<_>>();
19771 assert_eq!(
19772 hunks,
19773 [
19774 DisplayRow(2)..DisplayRow(4),
19775 DisplayRow(7)..DisplayRow(9),
19776 DisplayRow(12)..DisplayRow(14),
19777 ]
19778 );
19779}
19780
19781#[gpui::test]
19782async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19783 init_test(cx, |_| {});
19784
19785 let mut cx = EditorTestContext::new(cx).await;
19786 cx.set_head_text(indoc! { "
19787 one
19788 two
19789 three
19790 four
19791 five
19792 "
19793 });
19794 cx.set_index_text(indoc! { "
19795 one
19796 two
19797 three
19798 four
19799 five
19800 "
19801 });
19802 cx.set_state(indoc! {"
19803 one
19804 TWO
19805 ˇTHREE
19806 FOUR
19807 five
19808 "});
19809 cx.run_until_parked();
19810 cx.update_editor(|editor, window, cx| {
19811 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19812 });
19813 cx.run_until_parked();
19814 cx.assert_index_text(Some(indoc! {"
19815 one
19816 TWO
19817 THREE
19818 FOUR
19819 five
19820 "}));
19821 cx.set_state(indoc! { "
19822 one
19823 TWO
19824 ˇTHREE-HUNDRED
19825 FOUR
19826 five
19827 "});
19828 cx.run_until_parked();
19829 cx.update_editor(|editor, window, cx| {
19830 let snapshot = editor.snapshot(window, cx);
19831 let hunks = editor
19832 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19833 .collect::<Vec<_>>();
19834 assert_eq!(hunks.len(), 1);
19835 assert_eq!(
19836 hunks[0].status(),
19837 DiffHunkStatus {
19838 kind: DiffHunkStatusKind::Modified,
19839 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19840 }
19841 );
19842
19843 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19844 });
19845 cx.run_until_parked();
19846 cx.assert_index_text(Some(indoc! {"
19847 one
19848 TWO
19849 THREE-HUNDRED
19850 FOUR
19851 five
19852 "}));
19853}
19854
19855#[gpui::test]
19856fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19857 init_test(cx, |_| {});
19858
19859 let editor = cx.add_window(|window, cx| {
19860 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19861 build_editor(buffer, window, cx)
19862 });
19863
19864 let render_args = Arc::new(Mutex::new(None));
19865 let snapshot = editor
19866 .update(cx, |editor, window, cx| {
19867 let snapshot = editor.buffer().read(cx).snapshot(cx);
19868 let range =
19869 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19870
19871 struct RenderArgs {
19872 row: MultiBufferRow,
19873 folded: bool,
19874 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19875 }
19876
19877 let crease = Crease::inline(
19878 range,
19879 FoldPlaceholder::test(),
19880 {
19881 let toggle_callback = render_args.clone();
19882 move |row, folded, callback, _window, _cx| {
19883 *toggle_callback.lock() = Some(RenderArgs {
19884 row,
19885 folded,
19886 callback,
19887 });
19888 div()
19889 }
19890 },
19891 |_row, _folded, _window, _cx| div(),
19892 );
19893
19894 editor.insert_creases(Some(crease), cx);
19895 let snapshot = editor.snapshot(window, cx);
19896 let _div =
19897 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
19898 snapshot
19899 })
19900 .unwrap();
19901
19902 let render_args = render_args.lock().take().unwrap();
19903 assert_eq!(render_args.row, MultiBufferRow(1));
19904 assert!(!render_args.folded);
19905 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19906
19907 cx.update_window(*editor, |_, window, cx| {
19908 (render_args.callback)(true, window, cx)
19909 })
19910 .unwrap();
19911 let snapshot = editor
19912 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19913 .unwrap();
19914 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19915
19916 cx.update_window(*editor, |_, window, cx| {
19917 (render_args.callback)(false, window, cx)
19918 })
19919 .unwrap();
19920 let snapshot = editor
19921 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19922 .unwrap();
19923 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19924}
19925
19926#[gpui::test]
19927async fn test_input_text(cx: &mut TestAppContext) {
19928 init_test(cx, |_| {});
19929 let mut cx = EditorTestContext::new(cx).await;
19930
19931 cx.set_state(
19932 &r#"ˇone
19933 two
19934
19935 three
19936 fourˇ
19937 five
19938
19939 siˇx"#
19940 .unindent(),
19941 );
19942
19943 cx.dispatch_action(HandleInput(String::new()));
19944 cx.assert_editor_state(
19945 &r#"ˇone
19946 two
19947
19948 three
19949 fourˇ
19950 five
19951
19952 siˇx"#
19953 .unindent(),
19954 );
19955
19956 cx.dispatch_action(HandleInput("AAAA".to_string()));
19957 cx.assert_editor_state(
19958 &r#"AAAAˇone
19959 two
19960
19961 three
19962 fourAAAAˇ
19963 five
19964
19965 siAAAAˇx"#
19966 .unindent(),
19967 );
19968}
19969
19970#[gpui::test]
19971async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19972 init_test(cx, |_| {});
19973
19974 let mut cx = EditorTestContext::new(cx).await;
19975 cx.set_state(
19976 r#"let foo = 1;
19977let foo = 2;
19978let foo = 3;
19979let fooˇ = 4;
19980let foo = 5;
19981let foo = 6;
19982let foo = 7;
19983let foo = 8;
19984let foo = 9;
19985let foo = 10;
19986let foo = 11;
19987let foo = 12;
19988let foo = 13;
19989let foo = 14;
19990let foo = 15;"#,
19991 );
19992
19993 cx.update_editor(|e, window, cx| {
19994 assert_eq!(
19995 e.next_scroll_position,
19996 NextScrollCursorCenterTopBottom::Center,
19997 "Default next scroll direction is center",
19998 );
19999
20000 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20001 assert_eq!(
20002 e.next_scroll_position,
20003 NextScrollCursorCenterTopBottom::Top,
20004 "After center, next scroll direction should be top",
20005 );
20006
20007 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20008 assert_eq!(
20009 e.next_scroll_position,
20010 NextScrollCursorCenterTopBottom::Bottom,
20011 "After top, next scroll direction should be bottom",
20012 );
20013
20014 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20015 assert_eq!(
20016 e.next_scroll_position,
20017 NextScrollCursorCenterTopBottom::Center,
20018 "After bottom, scrolling should start over",
20019 );
20020
20021 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20022 assert_eq!(
20023 e.next_scroll_position,
20024 NextScrollCursorCenterTopBottom::Top,
20025 "Scrolling continues if retriggered fast enough"
20026 );
20027 });
20028
20029 cx.executor()
20030 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
20031 cx.executor().run_until_parked();
20032 cx.update_editor(|e, _, _| {
20033 assert_eq!(
20034 e.next_scroll_position,
20035 NextScrollCursorCenterTopBottom::Center,
20036 "If scrolling is not triggered fast enough, it should reset"
20037 );
20038 });
20039}
20040
20041#[gpui::test]
20042async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
20043 init_test(cx, |_| {});
20044 let mut cx = EditorLspTestContext::new_rust(
20045 lsp::ServerCapabilities {
20046 definition_provider: Some(lsp::OneOf::Left(true)),
20047 references_provider: Some(lsp::OneOf::Left(true)),
20048 ..lsp::ServerCapabilities::default()
20049 },
20050 cx,
20051 )
20052 .await;
20053
20054 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
20055 let go_to_definition = cx
20056 .lsp
20057 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20058 move |params, _| async move {
20059 if empty_go_to_definition {
20060 Ok(None)
20061 } else {
20062 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20063 uri: params.text_document_position_params.text_document.uri,
20064 range: lsp::Range::new(
20065 lsp::Position::new(4, 3),
20066 lsp::Position::new(4, 6),
20067 ),
20068 })))
20069 }
20070 },
20071 );
20072 let references = cx
20073 .lsp
20074 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20075 Ok(Some(vec![lsp::Location {
20076 uri: params.text_document_position.text_document.uri,
20077 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20078 }]))
20079 });
20080 (go_to_definition, references)
20081 };
20082
20083 cx.set_state(
20084 &r#"fn one() {
20085 let mut a = ˇtwo();
20086 }
20087
20088 fn two() {}"#
20089 .unindent(),
20090 );
20091 set_up_lsp_handlers(false, &mut cx);
20092 let navigated = cx
20093 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20094 .await
20095 .expect("Failed to navigate to definition");
20096 assert_eq!(
20097 navigated,
20098 Navigated::Yes,
20099 "Should have navigated to definition from the GetDefinition response"
20100 );
20101 cx.assert_editor_state(
20102 &r#"fn one() {
20103 let mut a = two();
20104 }
20105
20106 fn «twoˇ»() {}"#
20107 .unindent(),
20108 );
20109
20110 let editors = cx.update_workspace(|workspace, _, cx| {
20111 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20112 });
20113 cx.update_editor(|_, _, test_editor_cx| {
20114 assert_eq!(
20115 editors.len(),
20116 1,
20117 "Initially, only one, test, editor should be open in the workspace"
20118 );
20119 assert_eq!(
20120 test_editor_cx.entity(),
20121 editors.last().expect("Asserted len is 1").clone()
20122 );
20123 });
20124
20125 set_up_lsp_handlers(true, &mut cx);
20126 let navigated = cx
20127 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20128 .await
20129 .expect("Failed to navigate to lookup references");
20130 assert_eq!(
20131 navigated,
20132 Navigated::Yes,
20133 "Should have navigated to references as a fallback after empty GoToDefinition response"
20134 );
20135 // We should not change the selections in the existing file,
20136 // if opening another milti buffer with the references
20137 cx.assert_editor_state(
20138 &r#"fn one() {
20139 let mut a = two();
20140 }
20141
20142 fn «twoˇ»() {}"#
20143 .unindent(),
20144 );
20145 let editors = cx.update_workspace(|workspace, _, cx| {
20146 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20147 });
20148 cx.update_editor(|_, _, test_editor_cx| {
20149 assert_eq!(
20150 editors.len(),
20151 2,
20152 "After falling back to references search, we open a new editor with the results"
20153 );
20154 let references_fallback_text = editors
20155 .into_iter()
20156 .find(|new_editor| *new_editor != test_editor_cx.entity())
20157 .expect("Should have one non-test editor now")
20158 .read(test_editor_cx)
20159 .text(test_editor_cx);
20160 assert_eq!(
20161 references_fallback_text, "fn one() {\n let mut a = two();\n}",
20162 "Should use the range from the references response and not the GoToDefinition one"
20163 );
20164 });
20165}
20166
20167#[gpui::test]
20168async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
20169 init_test(cx, |_| {});
20170 cx.update(|cx| {
20171 let mut editor_settings = EditorSettings::get_global(cx).clone();
20172 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
20173 EditorSettings::override_global(editor_settings, cx);
20174 });
20175 let mut cx = EditorLspTestContext::new_rust(
20176 lsp::ServerCapabilities {
20177 definition_provider: Some(lsp::OneOf::Left(true)),
20178 references_provider: Some(lsp::OneOf::Left(true)),
20179 ..lsp::ServerCapabilities::default()
20180 },
20181 cx,
20182 )
20183 .await;
20184 let original_state = r#"fn one() {
20185 let mut a = ˇtwo();
20186 }
20187
20188 fn two() {}"#
20189 .unindent();
20190 cx.set_state(&original_state);
20191
20192 let mut go_to_definition = cx
20193 .lsp
20194 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20195 move |_, _| async move { Ok(None) },
20196 );
20197 let _references = cx
20198 .lsp
20199 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
20200 panic!("Should not call for references with no go to definition fallback")
20201 });
20202
20203 let navigated = cx
20204 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20205 .await
20206 .expect("Failed to navigate to lookup references");
20207 go_to_definition
20208 .next()
20209 .await
20210 .expect("Should have called the go_to_definition handler");
20211
20212 assert_eq!(
20213 navigated,
20214 Navigated::No,
20215 "Should have navigated to references as a fallback after empty GoToDefinition response"
20216 );
20217 cx.assert_editor_state(&original_state);
20218 let editors = cx.update_workspace(|workspace, _, cx| {
20219 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20220 });
20221 cx.update_editor(|_, _, _| {
20222 assert_eq!(
20223 editors.len(),
20224 1,
20225 "After unsuccessful fallback, no other editor should have been opened"
20226 );
20227 });
20228}
20229
20230#[gpui::test]
20231async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
20232 init_test(cx, |_| {});
20233
20234 let language = Arc::new(Language::new(
20235 LanguageConfig::default(),
20236 Some(tree_sitter_rust::LANGUAGE.into()),
20237 ));
20238
20239 let text = r#"
20240 #[cfg(test)]
20241 mod tests() {
20242 #[test]
20243 fn runnable_1() {
20244 let a = 1;
20245 }
20246
20247 #[test]
20248 fn runnable_2() {
20249 let a = 1;
20250 let b = 2;
20251 }
20252 }
20253 "#
20254 .unindent();
20255
20256 let fs = FakeFs::new(cx.executor());
20257 fs.insert_file("/file.rs", Default::default()).await;
20258
20259 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20260 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20261 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20262 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
20263 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
20264
20265 let editor = cx.new_window_entity(|window, cx| {
20266 Editor::new(
20267 EditorMode::full(),
20268 multi_buffer,
20269 Some(project.clone()),
20270 window,
20271 cx,
20272 )
20273 });
20274
20275 editor.update_in(cx, |editor, window, cx| {
20276 let snapshot = editor.buffer().read(cx).snapshot(cx);
20277 editor.tasks.insert(
20278 (buffer.read(cx).remote_id(), 3),
20279 RunnableTasks {
20280 templates: vec![],
20281 offset: snapshot.anchor_before(43),
20282 column: 0,
20283 extra_variables: HashMap::default(),
20284 context_range: BufferOffset(43)..BufferOffset(85),
20285 },
20286 );
20287 editor.tasks.insert(
20288 (buffer.read(cx).remote_id(), 8),
20289 RunnableTasks {
20290 templates: vec![],
20291 offset: snapshot.anchor_before(86),
20292 column: 0,
20293 extra_variables: HashMap::default(),
20294 context_range: BufferOffset(86)..BufferOffset(191),
20295 },
20296 );
20297
20298 // Test finding task when cursor is inside function body
20299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20300 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
20301 });
20302 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20303 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
20304
20305 // Test finding task when cursor is on function name
20306 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20307 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
20308 });
20309 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20310 assert_eq!(row, 8, "Should find task when cursor is on function name");
20311 });
20312}
20313
20314#[gpui::test]
20315async fn test_folding_buffers(cx: &mut TestAppContext) {
20316 init_test(cx, |_| {});
20317
20318 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20319 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
20320 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
20321
20322 let fs = FakeFs::new(cx.executor());
20323 fs.insert_tree(
20324 path!("/a"),
20325 json!({
20326 "first.rs": sample_text_1,
20327 "second.rs": sample_text_2,
20328 "third.rs": sample_text_3,
20329 }),
20330 )
20331 .await;
20332 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20333 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20334 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20335 let worktree = project.update(cx, |project, cx| {
20336 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20337 assert_eq!(worktrees.len(), 1);
20338 worktrees.pop().unwrap()
20339 });
20340 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20341
20342 let buffer_1 = project
20343 .update(cx, |project, cx| {
20344 project.open_buffer((worktree_id, "first.rs"), cx)
20345 })
20346 .await
20347 .unwrap();
20348 let buffer_2 = project
20349 .update(cx, |project, cx| {
20350 project.open_buffer((worktree_id, "second.rs"), cx)
20351 })
20352 .await
20353 .unwrap();
20354 let buffer_3 = project
20355 .update(cx, |project, cx| {
20356 project.open_buffer((worktree_id, "third.rs"), cx)
20357 })
20358 .await
20359 .unwrap();
20360
20361 let multi_buffer = cx.new(|cx| {
20362 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20363 multi_buffer.push_excerpts(
20364 buffer_1.clone(),
20365 [
20366 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20367 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20368 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20369 ],
20370 cx,
20371 );
20372 multi_buffer.push_excerpts(
20373 buffer_2.clone(),
20374 [
20375 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20376 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20377 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20378 ],
20379 cx,
20380 );
20381 multi_buffer.push_excerpts(
20382 buffer_3.clone(),
20383 [
20384 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20385 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20386 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20387 ],
20388 cx,
20389 );
20390 multi_buffer
20391 });
20392 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20393 Editor::new(
20394 EditorMode::full(),
20395 multi_buffer.clone(),
20396 Some(project.clone()),
20397 window,
20398 cx,
20399 )
20400 });
20401
20402 assert_eq!(
20403 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20404 "\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",
20405 );
20406
20407 multi_buffer_editor.update(cx, |editor, cx| {
20408 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20409 });
20410 assert_eq!(
20411 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20412 "\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",
20413 "After folding the first buffer, its text should not be displayed"
20414 );
20415
20416 multi_buffer_editor.update(cx, |editor, cx| {
20417 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20418 });
20419 assert_eq!(
20420 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20421 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20422 "After folding the second buffer, its text should not be displayed"
20423 );
20424
20425 multi_buffer_editor.update(cx, |editor, cx| {
20426 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20427 });
20428 assert_eq!(
20429 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20430 "\n\n\n\n\n",
20431 "After folding the third buffer, its text should not be displayed"
20432 );
20433
20434 // Emulate selection inside the fold logic, that should work
20435 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20436 editor
20437 .snapshot(window, cx)
20438 .next_line_boundary(Point::new(0, 4));
20439 });
20440
20441 multi_buffer_editor.update(cx, |editor, cx| {
20442 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20443 });
20444 assert_eq!(
20445 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20446 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20447 "After unfolding the second buffer, its text should be displayed"
20448 );
20449
20450 // Typing inside of buffer 1 causes that buffer to be unfolded.
20451 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20452 assert_eq!(
20453 multi_buffer
20454 .read(cx)
20455 .snapshot(cx)
20456 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20457 .collect::<String>(),
20458 "bbbb"
20459 );
20460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20461 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20462 });
20463 editor.handle_input("B", window, cx);
20464 });
20465
20466 assert_eq!(
20467 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20468 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20469 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20470 );
20471
20472 multi_buffer_editor.update(cx, |editor, cx| {
20473 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20474 });
20475 assert_eq!(
20476 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20477 "\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",
20478 "After unfolding the all buffers, all original text should be displayed"
20479 );
20480}
20481
20482#[gpui::test]
20483async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20484 init_test(cx, |_| {});
20485
20486 let sample_text_1 = "1111\n2222\n3333".to_string();
20487 let sample_text_2 = "4444\n5555\n6666".to_string();
20488 let sample_text_3 = "7777\n8888\n9999".to_string();
20489
20490 let fs = FakeFs::new(cx.executor());
20491 fs.insert_tree(
20492 path!("/a"),
20493 json!({
20494 "first.rs": sample_text_1,
20495 "second.rs": sample_text_2,
20496 "third.rs": sample_text_3,
20497 }),
20498 )
20499 .await;
20500 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20501 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20502 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20503 let worktree = project.update(cx, |project, cx| {
20504 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20505 assert_eq!(worktrees.len(), 1);
20506 worktrees.pop().unwrap()
20507 });
20508 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20509
20510 let buffer_1 = project
20511 .update(cx, |project, cx| {
20512 project.open_buffer((worktree_id, "first.rs"), cx)
20513 })
20514 .await
20515 .unwrap();
20516 let buffer_2 = project
20517 .update(cx, |project, cx| {
20518 project.open_buffer((worktree_id, "second.rs"), cx)
20519 })
20520 .await
20521 .unwrap();
20522 let buffer_3 = project
20523 .update(cx, |project, cx| {
20524 project.open_buffer((worktree_id, "third.rs"), cx)
20525 })
20526 .await
20527 .unwrap();
20528
20529 let multi_buffer = cx.new(|cx| {
20530 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20531 multi_buffer.push_excerpts(
20532 buffer_1.clone(),
20533 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20534 cx,
20535 );
20536 multi_buffer.push_excerpts(
20537 buffer_2.clone(),
20538 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20539 cx,
20540 );
20541 multi_buffer.push_excerpts(
20542 buffer_3.clone(),
20543 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20544 cx,
20545 );
20546 multi_buffer
20547 });
20548
20549 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20550 Editor::new(
20551 EditorMode::full(),
20552 multi_buffer,
20553 Some(project.clone()),
20554 window,
20555 cx,
20556 )
20557 });
20558
20559 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20560 assert_eq!(
20561 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20562 full_text,
20563 );
20564
20565 multi_buffer_editor.update(cx, |editor, cx| {
20566 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20567 });
20568 assert_eq!(
20569 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20570 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20571 "After folding the first buffer, its text should not be displayed"
20572 );
20573
20574 multi_buffer_editor.update(cx, |editor, cx| {
20575 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20576 });
20577
20578 assert_eq!(
20579 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20580 "\n\n\n\n\n\n7777\n8888\n9999",
20581 "After folding the second buffer, its text should not be displayed"
20582 );
20583
20584 multi_buffer_editor.update(cx, |editor, cx| {
20585 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20586 });
20587 assert_eq!(
20588 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20589 "\n\n\n\n\n",
20590 "After folding the third buffer, its text should not be displayed"
20591 );
20592
20593 multi_buffer_editor.update(cx, |editor, cx| {
20594 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20595 });
20596 assert_eq!(
20597 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20598 "\n\n\n\n4444\n5555\n6666\n\n",
20599 "After unfolding the second buffer, its text should be displayed"
20600 );
20601
20602 multi_buffer_editor.update(cx, |editor, cx| {
20603 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20604 });
20605 assert_eq!(
20606 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20607 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20608 "After unfolding the first buffer, its text should be displayed"
20609 );
20610
20611 multi_buffer_editor.update(cx, |editor, cx| {
20612 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20613 });
20614 assert_eq!(
20615 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20616 full_text,
20617 "After unfolding all buffers, all original text should be displayed"
20618 );
20619}
20620
20621#[gpui::test]
20622async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20623 init_test(cx, |_| {});
20624
20625 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20626
20627 let fs = FakeFs::new(cx.executor());
20628 fs.insert_tree(
20629 path!("/a"),
20630 json!({
20631 "main.rs": sample_text,
20632 }),
20633 )
20634 .await;
20635 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20636 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20637 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20638 let worktree = project.update(cx, |project, cx| {
20639 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20640 assert_eq!(worktrees.len(), 1);
20641 worktrees.pop().unwrap()
20642 });
20643 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20644
20645 let buffer_1 = project
20646 .update(cx, |project, cx| {
20647 project.open_buffer((worktree_id, "main.rs"), cx)
20648 })
20649 .await
20650 .unwrap();
20651
20652 let multi_buffer = cx.new(|cx| {
20653 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20654 multi_buffer.push_excerpts(
20655 buffer_1.clone(),
20656 [ExcerptRange::new(
20657 Point::new(0, 0)
20658 ..Point::new(
20659 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20660 0,
20661 ),
20662 )],
20663 cx,
20664 );
20665 multi_buffer
20666 });
20667 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20668 Editor::new(
20669 EditorMode::full(),
20670 multi_buffer,
20671 Some(project.clone()),
20672 window,
20673 cx,
20674 )
20675 });
20676
20677 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20678 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20679 enum TestHighlight {}
20680 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20681 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20682 editor.highlight_text::<TestHighlight>(
20683 vec![highlight_range.clone()],
20684 HighlightStyle::color(Hsla::green()),
20685 cx,
20686 );
20687 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20688 s.select_ranges(Some(highlight_range))
20689 });
20690 });
20691
20692 let full_text = format!("\n\n{sample_text}");
20693 assert_eq!(
20694 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20695 full_text,
20696 );
20697}
20698
20699#[gpui::test]
20700async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20701 init_test(cx, |_| {});
20702 cx.update(|cx| {
20703 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20704 "keymaps/default-linux.json",
20705 cx,
20706 )
20707 .unwrap();
20708 cx.bind_keys(default_key_bindings);
20709 });
20710
20711 let (editor, cx) = cx.add_window_view(|window, cx| {
20712 let multi_buffer = MultiBuffer::build_multi(
20713 [
20714 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20715 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20716 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20717 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20718 ],
20719 cx,
20720 );
20721 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20722
20723 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20724 // fold all but the second buffer, so that we test navigating between two
20725 // adjacent folded buffers, as well as folded buffers at the start and
20726 // end the multibuffer
20727 editor.fold_buffer(buffer_ids[0], cx);
20728 editor.fold_buffer(buffer_ids[2], cx);
20729 editor.fold_buffer(buffer_ids[3], cx);
20730
20731 editor
20732 });
20733 cx.simulate_resize(size(px(1000.), px(1000.)));
20734
20735 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20736 cx.assert_excerpts_with_selections(indoc! {"
20737 [EXCERPT]
20738 ˇ[FOLDED]
20739 [EXCERPT]
20740 a1
20741 b1
20742 [EXCERPT]
20743 [FOLDED]
20744 [EXCERPT]
20745 [FOLDED]
20746 "
20747 });
20748 cx.simulate_keystroke("down");
20749 cx.assert_excerpts_with_selections(indoc! {"
20750 [EXCERPT]
20751 [FOLDED]
20752 [EXCERPT]
20753 ˇa1
20754 b1
20755 [EXCERPT]
20756 [FOLDED]
20757 [EXCERPT]
20758 [FOLDED]
20759 "
20760 });
20761 cx.simulate_keystroke("down");
20762 cx.assert_excerpts_with_selections(indoc! {"
20763 [EXCERPT]
20764 [FOLDED]
20765 [EXCERPT]
20766 a1
20767 ˇb1
20768 [EXCERPT]
20769 [FOLDED]
20770 [EXCERPT]
20771 [FOLDED]
20772 "
20773 });
20774 cx.simulate_keystroke("down");
20775 cx.assert_excerpts_with_selections(indoc! {"
20776 [EXCERPT]
20777 [FOLDED]
20778 [EXCERPT]
20779 a1
20780 b1
20781 ˇ[EXCERPT]
20782 [FOLDED]
20783 [EXCERPT]
20784 [FOLDED]
20785 "
20786 });
20787 cx.simulate_keystroke("down");
20788 cx.assert_excerpts_with_selections(indoc! {"
20789 [EXCERPT]
20790 [FOLDED]
20791 [EXCERPT]
20792 a1
20793 b1
20794 [EXCERPT]
20795 ˇ[FOLDED]
20796 [EXCERPT]
20797 [FOLDED]
20798 "
20799 });
20800 for _ in 0..5 {
20801 cx.simulate_keystroke("down");
20802 cx.assert_excerpts_with_selections(indoc! {"
20803 [EXCERPT]
20804 [FOLDED]
20805 [EXCERPT]
20806 a1
20807 b1
20808 [EXCERPT]
20809 [FOLDED]
20810 [EXCERPT]
20811 ˇ[FOLDED]
20812 "
20813 });
20814 }
20815
20816 cx.simulate_keystroke("up");
20817 cx.assert_excerpts_with_selections(indoc! {"
20818 [EXCERPT]
20819 [FOLDED]
20820 [EXCERPT]
20821 a1
20822 b1
20823 [EXCERPT]
20824 ˇ[FOLDED]
20825 [EXCERPT]
20826 [FOLDED]
20827 "
20828 });
20829 cx.simulate_keystroke("up");
20830 cx.assert_excerpts_with_selections(indoc! {"
20831 [EXCERPT]
20832 [FOLDED]
20833 [EXCERPT]
20834 a1
20835 b1
20836 ˇ[EXCERPT]
20837 [FOLDED]
20838 [EXCERPT]
20839 [FOLDED]
20840 "
20841 });
20842 cx.simulate_keystroke("up");
20843 cx.assert_excerpts_with_selections(indoc! {"
20844 [EXCERPT]
20845 [FOLDED]
20846 [EXCERPT]
20847 a1
20848 ˇb1
20849 [EXCERPT]
20850 [FOLDED]
20851 [EXCERPT]
20852 [FOLDED]
20853 "
20854 });
20855 cx.simulate_keystroke("up");
20856 cx.assert_excerpts_with_selections(indoc! {"
20857 [EXCERPT]
20858 [FOLDED]
20859 [EXCERPT]
20860 ˇa1
20861 b1
20862 [EXCERPT]
20863 [FOLDED]
20864 [EXCERPT]
20865 [FOLDED]
20866 "
20867 });
20868 for _ in 0..5 {
20869 cx.simulate_keystroke("up");
20870 cx.assert_excerpts_with_selections(indoc! {"
20871 [EXCERPT]
20872 ˇ[FOLDED]
20873 [EXCERPT]
20874 a1
20875 b1
20876 [EXCERPT]
20877 [FOLDED]
20878 [EXCERPT]
20879 [FOLDED]
20880 "
20881 });
20882 }
20883}
20884
20885#[gpui::test]
20886async fn test_edit_prediction_text(cx: &mut TestAppContext) {
20887 init_test(cx, |_| {});
20888
20889 // Simple insertion
20890 assert_highlighted_edits(
20891 "Hello, world!",
20892 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20893 true,
20894 cx,
20895 |highlighted_edits, cx| {
20896 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20897 assert_eq!(highlighted_edits.highlights.len(), 1);
20898 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20899 assert_eq!(
20900 highlighted_edits.highlights[0].1.background_color,
20901 Some(cx.theme().status().created_background)
20902 );
20903 },
20904 )
20905 .await;
20906
20907 // Replacement
20908 assert_highlighted_edits(
20909 "This is a test.",
20910 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20911 false,
20912 cx,
20913 |highlighted_edits, cx| {
20914 assert_eq!(highlighted_edits.text, "That is a test.");
20915 assert_eq!(highlighted_edits.highlights.len(), 1);
20916 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20917 assert_eq!(
20918 highlighted_edits.highlights[0].1.background_color,
20919 Some(cx.theme().status().created_background)
20920 );
20921 },
20922 )
20923 .await;
20924
20925 // Multiple edits
20926 assert_highlighted_edits(
20927 "Hello, world!",
20928 vec![
20929 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20930 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20931 ],
20932 false,
20933 cx,
20934 |highlighted_edits, cx| {
20935 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20936 assert_eq!(highlighted_edits.highlights.len(), 2);
20937 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20938 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20939 assert_eq!(
20940 highlighted_edits.highlights[0].1.background_color,
20941 Some(cx.theme().status().created_background)
20942 );
20943 assert_eq!(
20944 highlighted_edits.highlights[1].1.background_color,
20945 Some(cx.theme().status().created_background)
20946 );
20947 },
20948 )
20949 .await;
20950
20951 // Multiple lines with edits
20952 assert_highlighted_edits(
20953 "First line\nSecond line\nThird line\nFourth line",
20954 vec![
20955 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20956 (
20957 Point::new(2, 0)..Point::new(2, 10),
20958 "New third line".to_string(),
20959 ),
20960 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20961 ],
20962 false,
20963 cx,
20964 |highlighted_edits, cx| {
20965 assert_eq!(
20966 highlighted_edits.text,
20967 "Second modified\nNew third line\nFourth updated line"
20968 );
20969 assert_eq!(highlighted_edits.highlights.len(), 3);
20970 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20971 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20972 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20973 for highlight in &highlighted_edits.highlights {
20974 assert_eq!(
20975 highlight.1.background_color,
20976 Some(cx.theme().status().created_background)
20977 );
20978 }
20979 },
20980 )
20981 .await;
20982}
20983
20984#[gpui::test]
20985async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
20986 init_test(cx, |_| {});
20987
20988 // Deletion
20989 assert_highlighted_edits(
20990 "Hello, world!",
20991 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20992 true,
20993 cx,
20994 |highlighted_edits, cx| {
20995 assert_eq!(highlighted_edits.text, "Hello, world!");
20996 assert_eq!(highlighted_edits.highlights.len(), 1);
20997 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20998 assert_eq!(
20999 highlighted_edits.highlights[0].1.background_color,
21000 Some(cx.theme().status().deleted_background)
21001 );
21002 },
21003 )
21004 .await;
21005
21006 // Insertion
21007 assert_highlighted_edits(
21008 "Hello, world!",
21009 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
21010 true,
21011 cx,
21012 |highlighted_edits, cx| {
21013 assert_eq!(highlighted_edits.highlights.len(), 1);
21014 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
21015 assert_eq!(
21016 highlighted_edits.highlights[0].1.background_color,
21017 Some(cx.theme().status().created_background)
21018 );
21019 },
21020 )
21021 .await;
21022}
21023
21024async fn assert_highlighted_edits(
21025 text: &str,
21026 edits: Vec<(Range<Point>, String)>,
21027 include_deletions: bool,
21028 cx: &mut TestAppContext,
21029 assertion_fn: impl Fn(HighlightedText, &App),
21030) {
21031 let window = cx.add_window(|window, cx| {
21032 let buffer = MultiBuffer::build_simple(text, cx);
21033 Editor::new(EditorMode::full(), buffer, None, window, cx)
21034 });
21035 let cx = &mut VisualTestContext::from_window(*window, cx);
21036
21037 let (buffer, snapshot) = window
21038 .update(cx, |editor, _window, cx| {
21039 (
21040 editor.buffer().clone(),
21041 editor.buffer().read(cx).snapshot(cx),
21042 )
21043 })
21044 .unwrap();
21045
21046 let edits = edits
21047 .into_iter()
21048 .map(|(range, edit)| {
21049 (
21050 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21051 edit,
21052 )
21053 })
21054 .collect::<Vec<_>>();
21055
21056 let text_anchor_edits = edits
21057 .clone()
21058 .into_iter()
21059 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21060 .collect::<Vec<_>>();
21061
21062 let edit_preview = window
21063 .update(cx, |_, _window, cx| {
21064 buffer
21065 .read(cx)
21066 .as_singleton()
21067 .unwrap()
21068 .read(cx)
21069 .preview_edits(text_anchor_edits.into(), cx)
21070 })
21071 .unwrap()
21072 .await;
21073
21074 cx.update(|_window, cx| {
21075 let highlighted_edits = edit_prediction_edit_text(
21076 snapshot.as_singleton().unwrap().2,
21077 &edits,
21078 &edit_preview,
21079 include_deletions,
21080 cx,
21081 );
21082 assertion_fn(highlighted_edits, cx)
21083 });
21084}
21085
21086#[track_caller]
21087fn assert_breakpoint(
21088 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21089 path: &Arc<Path>,
21090 expected: Vec<(u32, Breakpoint)>,
21091) {
21092 if expected.is_empty() {
21093 assert!(!breakpoints.contains_key(path), "{}", path.display());
21094 } else {
21095 let mut breakpoint = breakpoints
21096 .get(path)
21097 .unwrap()
21098 .iter()
21099 .map(|breakpoint| {
21100 (
21101 breakpoint.row,
21102 Breakpoint {
21103 message: breakpoint.message.clone(),
21104 state: breakpoint.state,
21105 condition: breakpoint.condition.clone(),
21106 hit_condition: breakpoint.hit_condition.clone(),
21107 },
21108 )
21109 })
21110 .collect::<Vec<_>>();
21111
21112 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21113
21114 assert_eq!(expected, breakpoint);
21115 }
21116}
21117
21118fn add_log_breakpoint_at_cursor(
21119 editor: &mut Editor,
21120 log_message: &str,
21121 window: &mut Window,
21122 cx: &mut Context<Editor>,
21123) {
21124 let (anchor, bp) = editor
21125 .breakpoints_at_cursors(window, cx)
21126 .first()
21127 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
21128 .unwrap_or_else(|| {
21129 let cursor_position: Point = editor.selections.newest(cx).head();
21130
21131 let breakpoint_position = editor
21132 .snapshot(window, cx)
21133 .display_snapshot
21134 .buffer_snapshot
21135 .anchor_before(Point::new(cursor_position.row, 0));
21136
21137 (breakpoint_position, Breakpoint::new_log(log_message))
21138 });
21139
21140 editor.edit_breakpoint_at_anchor(
21141 anchor,
21142 bp,
21143 BreakpointEditAction::EditLogMessage(log_message.into()),
21144 cx,
21145 );
21146}
21147
21148#[gpui::test]
21149async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
21150 init_test(cx, |_| {});
21151
21152 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21153 let fs = FakeFs::new(cx.executor());
21154 fs.insert_tree(
21155 path!("/a"),
21156 json!({
21157 "main.rs": sample_text,
21158 }),
21159 )
21160 .await;
21161 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21162 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21163 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21164
21165 let fs = FakeFs::new(cx.executor());
21166 fs.insert_tree(
21167 path!("/a"),
21168 json!({
21169 "main.rs": sample_text,
21170 }),
21171 )
21172 .await;
21173 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21174 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21175 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21176 let worktree_id = workspace
21177 .update(cx, |workspace, _window, cx| {
21178 workspace.project().update(cx, |project, cx| {
21179 project.worktrees(cx).next().unwrap().read(cx).id()
21180 })
21181 })
21182 .unwrap();
21183
21184 let buffer = project
21185 .update(cx, |project, cx| {
21186 project.open_buffer((worktree_id, "main.rs"), cx)
21187 })
21188 .await
21189 .unwrap();
21190
21191 let (editor, cx) = cx.add_window_view(|window, cx| {
21192 Editor::new(
21193 EditorMode::full(),
21194 MultiBuffer::build_from_buffer(buffer, cx),
21195 Some(project.clone()),
21196 window,
21197 cx,
21198 )
21199 });
21200
21201 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21202 let abs_path = project.read_with(cx, |project, cx| {
21203 project
21204 .absolute_path(&project_path, cx)
21205 .map(Arc::from)
21206 .unwrap()
21207 });
21208
21209 // assert we can add breakpoint on the first line
21210 editor.update_in(cx, |editor, window, cx| {
21211 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21212 editor.move_to_end(&MoveToEnd, window, cx);
21213 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21214 });
21215
21216 let breakpoints = editor.update(cx, |editor, cx| {
21217 editor
21218 .breakpoint_store()
21219 .as_ref()
21220 .unwrap()
21221 .read(cx)
21222 .all_source_breakpoints(cx)
21223 });
21224
21225 assert_eq!(1, breakpoints.len());
21226 assert_breakpoint(
21227 &breakpoints,
21228 &abs_path,
21229 vec![
21230 (0, Breakpoint::new_standard()),
21231 (3, Breakpoint::new_standard()),
21232 ],
21233 );
21234
21235 editor.update_in(cx, |editor, window, cx| {
21236 editor.move_to_beginning(&MoveToBeginning, window, cx);
21237 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21238 });
21239
21240 let breakpoints = editor.update(cx, |editor, cx| {
21241 editor
21242 .breakpoint_store()
21243 .as_ref()
21244 .unwrap()
21245 .read(cx)
21246 .all_source_breakpoints(cx)
21247 });
21248
21249 assert_eq!(1, breakpoints.len());
21250 assert_breakpoint(
21251 &breakpoints,
21252 &abs_path,
21253 vec![(3, Breakpoint::new_standard())],
21254 );
21255
21256 editor.update_in(cx, |editor, window, cx| {
21257 editor.move_to_end(&MoveToEnd, window, cx);
21258 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21259 });
21260
21261 let breakpoints = editor.update(cx, |editor, cx| {
21262 editor
21263 .breakpoint_store()
21264 .as_ref()
21265 .unwrap()
21266 .read(cx)
21267 .all_source_breakpoints(cx)
21268 });
21269
21270 assert_eq!(0, breakpoints.len());
21271 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21272}
21273
21274#[gpui::test]
21275async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
21276 init_test(cx, |_| {});
21277
21278 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21279
21280 let fs = FakeFs::new(cx.executor());
21281 fs.insert_tree(
21282 path!("/a"),
21283 json!({
21284 "main.rs": sample_text,
21285 }),
21286 )
21287 .await;
21288 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21289 let (workspace, cx) =
21290 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21291
21292 let worktree_id = workspace.update(cx, |workspace, cx| {
21293 workspace.project().update(cx, |project, cx| {
21294 project.worktrees(cx).next().unwrap().read(cx).id()
21295 })
21296 });
21297
21298 let buffer = project
21299 .update(cx, |project, cx| {
21300 project.open_buffer((worktree_id, "main.rs"), cx)
21301 })
21302 .await
21303 .unwrap();
21304
21305 let (editor, cx) = cx.add_window_view(|window, cx| {
21306 Editor::new(
21307 EditorMode::full(),
21308 MultiBuffer::build_from_buffer(buffer, cx),
21309 Some(project.clone()),
21310 window,
21311 cx,
21312 )
21313 });
21314
21315 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21316 let abs_path = project.read_with(cx, |project, cx| {
21317 project
21318 .absolute_path(&project_path, cx)
21319 .map(Arc::from)
21320 .unwrap()
21321 });
21322
21323 editor.update_in(cx, |editor, window, cx| {
21324 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21325 });
21326
21327 let breakpoints = editor.update(cx, |editor, cx| {
21328 editor
21329 .breakpoint_store()
21330 .as_ref()
21331 .unwrap()
21332 .read(cx)
21333 .all_source_breakpoints(cx)
21334 });
21335
21336 assert_breakpoint(
21337 &breakpoints,
21338 &abs_path,
21339 vec![(0, Breakpoint::new_log("hello world"))],
21340 );
21341
21342 // Removing a log message from a log breakpoint should remove it
21343 editor.update_in(cx, |editor, window, cx| {
21344 add_log_breakpoint_at_cursor(editor, "", window, cx);
21345 });
21346
21347 let breakpoints = editor.update(cx, |editor, cx| {
21348 editor
21349 .breakpoint_store()
21350 .as_ref()
21351 .unwrap()
21352 .read(cx)
21353 .all_source_breakpoints(cx)
21354 });
21355
21356 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21357
21358 editor.update_in(cx, |editor, window, cx| {
21359 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21360 editor.move_to_end(&MoveToEnd, window, cx);
21361 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21362 // Not adding a log message to a standard breakpoint shouldn't remove it
21363 add_log_breakpoint_at_cursor(editor, "", window, cx);
21364 });
21365
21366 let breakpoints = editor.update(cx, |editor, cx| {
21367 editor
21368 .breakpoint_store()
21369 .as_ref()
21370 .unwrap()
21371 .read(cx)
21372 .all_source_breakpoints(cx)
21373 });
21374
21375 assert_breakpoint(
21376 &breakpoints,
21377 &abs_path,
21378 vec![
21379 (0, Breakpoint::new_standard()),
21380 (3, Breakpoint::new_standard()),
21381 ],
21382 );
21383
21384 editor.update_in(cx, |editor, window, cx| {
21385 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21386 });
21387
21388 let breakpoints = editor.update(cx, |editor, cx| {
21389 editor
21390 .breakpoint_store()
21391 .as_ref()
21392 .unwrap()
21393 .read(cx)
21394 .all_source_breakpoints(cx)
21395 });
21396
21397 assert_breakpoint(
21398 &breakpoints,
21399 &abs_path,
21400 vec![
21401 (0, Breakpoint::new_standard()),
21402 (3, Breakpoint::new_log("hello world")),
21403 ],
21404 );
21405
21406 editor.update_in(cx, |editor, window, cx| {
21407 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21408 });
21409
21410 let breakpoints = editor.update(cx, |editor, cx| {
21411 editor
21412 .breakpoint_store()
21413 .as_ref()
21414 .unwrap()
21415 .read(cx)
21416 .all_source_breakpoints(cx)
21417 });
21418
21419 assert_breakpoint(
21420 &breakpoints,
21421 &abs_path,
21422 vec![
21423 (0, Breakpoint::new_standard()),
21424 (3, Breakpoint::new_log("hello Earth!!")),
21425 ],
21426 );
21427}
21428
21429/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21430/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21431/// or when breakpoints were placed out of order. This tests for a regression too
21432#[gpui::test]
21433async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21434 init_test(cx, |_| {});
21435
21436 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21437 let fs = FakeFs::new(cx.executor());
21438 fs.insert_tree(
21439 path!("/a"),
21440 json!({
21441 "main.rs": sample_text,
21442 }),
21443 )
21444 .await;
21445 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21446 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21447 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21448
21449 let fs = FakeFs::new(cx.executor());
21450 fs.insert_tree(
21451 path!("/a"),
21452 json!({
21453 "main.rs": sample_text,
21454 }),
21455 )
21456 .await;
21457 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21458 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21459 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21460 let worktree_id = workspace
21461 .update(cx, |workspace, _window, cx| {
21462 workspace.project().update(cx, |project, cx| {
21463 project.worktrees(cx).next().unwrap().read(cx).id()
21464 })
21465 })
21466 .unwrap();
21467
21468 let buffer = project
21469 .update(cx, |project, cx| {
21470 project.open_buffer((worktree_id, "main.rs"), cx)
21471 })
21472 .await
21473 .unwrap();
21474
21475 let (editor, cx) = cx.add_window_view(|window, cx| {
21476 Editor::new(
21477 EditorMode::full(),
21478 MultiBuffer::build_from_buffer(buffer, cx),
21479 Some(project.clone()),
21480 window,
21481 cx,
21482 )
21483 });
21484
21485 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21486 let abs_path = project.read_with(cx, |project, cx| {
21487 project
21488 .absolute_path(&project_path, cx)
21489 .map(Arc::from)
21490 .unwrap()
21491 });
21492
21493 // assert we can add breakpoint on the first line
21494 editor.update_in(cx, |editor, window, cx| {
21495 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21496 editor.move_to_end(&MoveToEnd, window, cx);
21497 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21498 editor.move_up(&MoveUp, window, cx);
21499 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21500 });
21501
21502 let breakpoints = editor.update(cx, |editor, cx| {
21503 editor
21504 .breakpoint_store()
21505 .as_ref()
21506 .unwrap()
21507 .read(cx)
21508 .all_source_breakpoints(cx)
21509 });
21510
21511 assert_eq!(1, breakpoints.len());
21512 assert_breakpoint(
21513 &breakpoints,
21514 &abs_path,
21515 vec![
21516 (0, Breakpoint::new_standard()),
21517 (2, Breakpoint::new_standard()),
21518 (3, Breakpoint::new_standard()),
21519 ],
21520 );
21521
21522 editor.update_in(cx, |editor, window, cx| {
21523 editor.move_to_beginning(&MoveToBeginning, window, cx);
21524 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21525 editor.move_to_end(&MoveToEnd, window, cx);
21526 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21527 // Disabling a breakpoint that doesn't exist should do nothing
21528 editor.move_up(&MoveUp, window, cx);
21529 editor.move_up(&MoveUp, window, cx);
21530 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21531 });
21532
21533 let breakpoints = editor.update(cx, |editor, cx| {
21534 editor
21535 .breakpoint_store()
21536 .as_ref()
21537 .unwrap()
21538 .read(cx)
21539 .all_source_breakpoints(cx)
21540 });
21541
21542 let disable_breakpoint = {
21543 let mut bp = Breakpoint::new_standard();
21544 bp.state = BreakpointState::Disabled;
21545 bp
21546 };
21547
21548 assert_eq!(1, breakpoints.len());
21549 assert_breakpoint(
21550 &breakpoints,
21551 &abs_path,
21552 vec![
21553 (0, disable_breakpoint.clone()),
21554 (2, Breakpoint::new_standard()),
21555 (3, disable_breakpoint.clone()),
21556 ],
21557 );
21558
21559 editor.update_in(cx, |editor, window, cx| {
21560 editor.move_to_beginning(&MoveToBeginning, window, cx);
21561 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21562 editor.move_to_end(&MoveToEnd, window, cx);
21563 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21564 editor.move_up(&MoveUp, window, cx);
21565 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21566 });
21567
21568 let breakpoints = editor.update(cx, |editor, cx| {
21569 editor
21570 .breakpoint_store()
21571 .as_ref()
21572 .unwrap()
21573 .read(cx)
21574 .all_source_breakpoints(cx)
21575 });
21576
21577 assert_eq!(1, breakpoints.len());
21578 assert_breakpoint(
21579 &breakpoints,
21580 &abs_path,
21581 vec![
21582 (0, Breakpoint::new_standard()),
21583 (2, disable_breakpoint),
21584 (3, Breakpoint::new_standard()),
21585 ],
21586 );
21587}
21588
21589#[gpui::test]
21590async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21591 init_test(cx, |_| {});
21592 let capabilities = lsp::ServerCapabilities {
21593 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21594 prepare_provider: Some(true),
21595 work_done_progress_options: Default::default(),
21596 })),
21597 ..Default::default()
21598 };
21599 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21600
21601 cx.set_state(indoc! {"
21602 struct Fˇoo {}
21603 "});
21604
21605 cx.update_editor(|editor, _, cx| {
21606 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21607 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21608 editor.highlight_background::<DocumentHighlightRead>(
21609 &[highlight_range],
21610 |theme| theme.colors().editor_document_highlight_read_background,
21611 cx,
21612 );
21613 });
21614
21615 let mut prepare_rename_handler = cx
21616 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21617 move |_, _, _| async move {
21618 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21619 start: lsp::Position {
21620 line: 0,
21621 character: 7,
21622 },
21623 end: lsp::Position {
21624 line: 0,
21625 character: 10,
21626 },
21627 })))
21628 },
21629 );
21630 let prepare_rename_task = cx
21631 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21632 .expect("Prepare rename was not started");
21633 prepare_rename_handler.next().await.unwrap();
21634 prepare_rename_task.await.expect("Prepare rename failed");
21635
21636 let mut rename_handler =
21637 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21638 let edit = lsp::TextEdit {
21639 range: lsp::Range {
21640 start: lsp::Position {
21641 line: 0,
21642 character: 7,
21643 },
21644 end: lsp::Position {
21645 line: 0,
21646 character: 10,
21647 },
21648 },
21649 new_text: "FooRenamed".to_string(),
21650 };
21651 Ok(Some(lsp::WorkspaceEdit::new(
21652 // Specify the same edit twice
21653 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21654 )))
21655 });
21656 let rename_task = cx
21657 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21658 .expect("Confirm rename was not started");
21659 rename_handler.next().await.unwrap();
21660 rename_task.await.expect("Confirm rename failed");
21661 cx.run_until_parked();
21662
21663 // Despite two edits, only one is actually applied as those are identical
21664 cx.assert_editor_state(indoc! {"
21665 struct FooRenamedˇ {}
21666 "});
21667}
21668
21669#[gpui::test]
21670async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21671 init_test(cx, |_| {});
21672 // These capabilities indicate that the server does not support prepare rename.
21673 let capabilities = lsp::ServerCapabilities {
21674 rename_provider: Some(lsp::OneOf::Left(true)),
21675 ..Default::default()
21676 };
21677 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21678
21679 cx.set_state(indoc! {"
21680 struct Fˇoo {}
21681 "});
21682
21683 cx.update_editor(|editor, _window, cx| {
21684 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21685 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21686 editor.highlight_background::<DocumentHighlightRead>(
21687 &[highlight_range],
21688 |theme| theme.colors().editor_document_highlight_read_background,
21689 cx,
21690 );
21691 });
21692
21693 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21694 .expect("Prepare rename was not started")
21695 .await
21696 .expect("Prepare rename failed");
21697
21698 let mut rename_handler =
21699 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21700 let edit = lsp::TextEdit {
21701 range: lsp::Range {
21702 start: lsp::Position {
21703 line: 0,
21704 character: 7,
21705 },
21706 end: lsp::Position {
21707 line: 0,
21708 character: 10,
21709 },
21710 },
21711 new_text: "FooRenamed".to_string(),
21712 };
21713 Ok(Some(lsp::WorkspaceEdit::new(
21714 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21715 )))
21716 });
21717 let rename_task = cx
21718 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21719 .expect("Confirm rename was not started");
21720 rename_handler.next().await.unwrap();
21721 rename_task.await.expect("Confirm rename failed");
21722 cx.run_until_parked();
21723
21724 // Correct range is renamed, as `surrounding_word` is used to find it.
21725 cx.assert_editor_state(indoc! {"
21726 struct FooRenamedˇ {}
21727 "});
21728}
21729
21730#[gpui::test]
21731async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21732 init_test(cx, |_| {});
21733 let mut cx = EditorTestContext::new(cx).await;
21734
21735 let language = Arc::new(
21736 Language::new(
21737 LanguageConfig::default(),
21738 Some(tree_sitter_html::LANGUAGE.into()),
21739 )
21740 .with_brackets_query(
21741 r#"
21742 ("<" @open "/>" @close)
21743 ("</" @open ">" @close)
21744 ("<" @open ">" @close)
21745 ("\"" @open "\"" @close)
21746 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21747 "#,
21748 )
21749 .unwrap(),
21750 );
21751 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21752
21753 cx.set_state(indoc! {"
21754 <span>ˇ</span>
21755 "});
21756 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21757 cx.assert_editor_state(indoc! {"
21758 <span>
21759 ˇ
21760 </span>
21761 "});
21762
21763 cx.set_state(indoc! {"
21764 <span><span></span>ˇ</span>
21765 "});
21766 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21767 cx.assert_editor_state(indoc! {"
21768 <span><span></span>
21769 ˇ</span>
21770 "});
21771
21772 cx.set_state(indoc! {"
21773 <span>ˇ
21774 </span>
21775 "});
21776 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21777 cx.assert_editor_state(indoc! {"
21778 <span>
21779 ˇ
21780 </span>
21781 "});
21782}
21783
21784#[gpui::test(iterations = 10)]
21785async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21786 init_test(cx, |_| {});
21787
21788 let fs = FakeFs::new(cx.executor());
21789 fs.insert_tree(
21790 path!("/dir"),
21791 json!({
21792 "a.ts": "a",
21793 }),
21794 )
21795 .await;
21796
21797 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21798 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21799 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21800
21801 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21802 language_registry.add(Arc::new(Language::new(
21803 LanguageConfig {
21804 name: "TypeScript".into(),
21805 matcher: LanguageMatcher {
21806 path_suffixes: vec!["ts".to_string()],
21807 ..Default::default()
21808 },
21809 ..Default::default()
21810 },
21811 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21812 )));
21813 let mut fake_language_servers = language_registry.register_fake_lsp(
21814 "TypeScript",
21815 FakeLspAdapter {
21816 capabilities: lsp::ServerCapabilities {
21817 code_lens_provider: Some(lsp::CodeLensOptions {
21818 resolve_provider: Some(true),
21819 }),
21820 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21821 commands: vec!["_the/command".to_string()],
21822 ..lsp::ExecuteCommandOptions::default()
21823 }),
21824 ..lsp::ServerCapabilities::default()
21825 },
21826 ..FakeLspAdapter::default()
21827 },
21828 );
21829
21830 let editor = workspace
21831 .update(cx, |workspace, window, cx| {
21832 workspace.open_abs_path(
21833 PathBuf::from(path!("/dir/a.ts")),
21834 OpenOptions::default(),
21835 window,
21836 cx,
21837 )
21838 })
21839 .unwrap()
21840 .await
21841 .unwrap()
21842 .downcast::<Editor>()
21843 .unwrap();
21844 cx.executor().run_until_parked();
21845
21846 let fake_server = fake_language_servers.next().await.unwrap();
21847
21848 let buffer = editor.update(cx, |editor, cx| {
21849 editor
21850 .buffer()
21851 .read(cx)
21852 .as_singleton()
21853 .expect("have opened a single file by path")
21854 });
21855
21856 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21857 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21858 drop(buffer_snapshot);
21859 let actions = cx
21860 .update_window(*workspace, |_, window, cx| {
21861 project.code_actions(&buffer, anchor..anchor, window, cx)
21862 })
21863 .unwrap();
21864
21865 fake_server
21866 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21867 Ok(Some(vec![
21868 lsp::CodeLens {
21869 range: lsp::Range::default(),
21870 command: Some(lsp::Command {
21871 title: "Code lens command".to_owned(),
21872 command: "_the/command".to_owned(),
21873 arguments: None,
21874 }),
21875 data: None,
21876 },
21877 lsp::CodeLens {
21878 range: lsp::Range::default(),
21879 command: Some(lsp::Command {
21880 title: "Command not in capabilities".to_owned(),
21881 command: "not in capabilities".to_owned(),
21882 arguments: None,
21883 }),
21884 data: None,
21885 },
21886 lsp::CodeLens {
21887 range: lsp::Range {
21888 start: lsp::Position {
21889 line: 1,
21890 character: 1,
21891 },
21892 end: lsp::Position {
21893 line: 1,
21894 character: 1,
21895 },
21896 },
21897 command: Some(lsp::Command {
21898 title: "Command not in range".to_owned(),
21899 command: "_the/command".to_owned(),
21900 arguments: None,
21901 }),
21902 data: None,
21903 },
21904 ]))
21905 })
21906 .next()
21907 .await;
21908
21909 let actions = actions.await.unwrap();
21910 assert_eq!(
21911 actions.len(),
21912 1,
21913 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21914 );
21915 let action = actions[0].clone();
21916 let apply = project.update(cx, |project, cx| {
21917 project.apply_code_action(buffer.clone(), action, true, cx)
21918 });
21919
21920 // Resolving the code action does not populate its edits. In absence of
21921 // edits, we must execute the given command.
21922 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21923 |mut lens, _| async move {
21924 let lens_command = lens.command.as_mut().expect("should have a command");
21925 assert_eq!(lens_command.title, "Code lens command");
21926 lens_command.arguments = Some(vec![json!("the-argument")]);
21927 Ok(lens)
21928 },
21929 );
21930
21931 // While executing the command, the language server sends the editor
21932 // a `workspaceEdit` request.
21933 fake_server
21934 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21935 let fake = fake_server.clone();
21936 move |params, _| {
21937 assert_eq!(params.command, "_the/command");
21938 let fake = fake.clone();
21939 async move {
21940 fake.server
21941 .request::<lsp::request::ApplyWorkspaceEdit>(
21942 lsp::ApplyWorkspaceEditParams {
21943 label: None,
21944 edit: lsp::WorkspaceEdit {
21945 changes: Some(
21946 [(
21947 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21948 vec![lsp::TextEdit {
21949 range: lsp::Range::new(
21950 lsp::Position::new(0, 0),
21951 lsp::Position::new(0, 0),
21952 ),
21953 new_text: "X".into(),
21954 }],
21955 )]
21956 .into_iter()
21957 .collect(),
21958 ),
21959 ..lsp::WorkspaceEdit::default()
21960 },
21961 },
21962 )
21963 .await
21964 .into_response()
21965 .unwrap();
21966 Ok(Some(json!(null)))
21967 }
21968 }
21969 })
21970 .next()
21971 .await;
21972
21973 // Applying the code lens command returns a project transaction containing the edits
21974 // sent by the language server in its `workspaceEdit` request.
21975 let transaction = apply.await.unwrap();
21976 assert!(transaction.0.contains_key(&buffer));
21977 buffer.update(cx, |buffer, cx| {
21978 assert_eq!(buffer.text(), "Xa");
21979 buffer.undo(cx);
21980 assert_eq!(buffer.text(), "a");
21981 });
21982
21983 let actions_after_edits = cx
21984 .update_window(*workspace, |_, window, cx| {
21985 project.code_actions(&buffer, anchor..anchor, window, cx)
21986 })
21987 .unwrap()
21988 .await
21989 .unwrap();
21990 assert_eq!(
21991 actions, actions_after_edits,
21992 "For the same selection, same code lens actions should be returned"
21993 );
21994
21995 let _responses =
21996 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21997 panic!("No more code lens requests are expected");
21998 });
21999 editor.update_in(cx, |editor, window, cx| {
22000 editor.select_all(&SelectAll, window, cx);
22001 });
22002 cx.executor().run_until_parked();
22003 let new_actions = cx
22004 .update_window(*workspace, |_, window, cx| {
22005 project.code_actions(&buffer, anchor..anchor, window, cx)
22006 })
22007 .unwrap()
22008 .await
22009 .unwrap();
22010 assert_eq!(
22011 actions, new_actions,
22012 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
22013 );
22014}
22015
22016#[gpui::test]
22017async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
22018 init_test(cx, |_| {});
22019
22020 let fs = FakeFs::new(cx.executor());
22021 let main_text = r#"fn main() {
22022println!("1");
22023println!("2");
22024println!("3");
22025println!("4");
22026println!("5");
22027}"#;
22028 let lib_text = "mod foo {}";
22029 fs.insert_tree(
22030 path!("/a"),
22031 json!({
22032 "lib.rs": lib_text,
22033 "main.rs": main_text,
22034 }),
22035 )
22036 .await;
22037
22038 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22039 let (workspace, cx) =
22040 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22041 let worktree_id = workspace.update(cx, |workspace, cx| {
22042 workspace.project().update(cx, |project, cx| {
22043 project.worktrees(cx).next().unwrap().read(cx).id()
22044 })
22045 });
22046
22047 let expected_ranges = vec![
22048 Point::new(0, 0)..Point::new(0, 0),
22049 Point::new(1, 0)..Point::new(1, 1),
22050 Point::new(2, 0)..Point::new(2, 2),
22051 Point::new(3, 0)..Point::new(3, 3),
22052 ];
22053
22054 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22055 let editor_1 = workspace
22056 .update_in(cx, |workspace, window, cx| {
22057 workspace.open_path(
22058 (worktree_id, "main.rs"),
22059 Some(pane_1.downgrade()),
22060 true,
22061 window,
22062 cx,
22063 )
22064 })
22065 .unwrap()
22066 .await
22067 .downcast::<Editor>()
22068 .unwrap();
22069 pane_1.update(cx, |pane, cx| {
22070 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22071 open_editor.update(cx, |editor, cx| {
22072 assert_eq!(
22073 editor.display_text(cx),
22074 main_text,
22075 "Original main.rs text on initial open",
22076 );
22077 assert_eq!(
22078 editor
22079 .selections
22080 .all::<Point>(cx)
22081 .into_iter()
22082 .map(|s| s.range())
22083 .collect::<Vec<_>>(),
22084 vec![Point::zero()..Point::zero()],
22085 "Default selections on initial open",
22086 );
22087 })
22088 });
22089 editor_1.update_in(cx, |editor, window, cx| {
22090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22091 s.select_ranges(expected_ranges.clone());
22092 });
22093 });
22094
22095 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22096 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22097 });
22098 let editor_2 = workspace
22099 .update_in(cx, |workspace, window, cx| {
22100 workspace.open_path(
22101 (worktree_id, "main.rs"),
22102 Some(pane_2.downgrade()),
22103 true,
22104 window,
22105 cx,
22106 )
22107 })
22108 .unwrap()
22109 .await
22110 .downcast::<Editor>()
22111 .unwrap();
22112 pane_2.update(cx, |pane, cx| {
22113 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22114 open_editor.update(cx, |editor, cx| {
22115 assert_eq!(
22116 editor.display_text(cx),
22117 main_text,
22118 "Original main.rs text on initial open in another panel",
22119 );
22120 assert_eq!(
22121 editor
22122 .selections
22123 .all::<Point>(cx)
22124 .into_iter()
22125 .map(|s| s.range())
22126 .collect::<Vec<_>>(),
22127 vec![Point::zero()..Point::zero()],
22128 "Default selections on initial open in another panel",
22129 );
22130 })
22131 });
22132
22133 editor_2.update_in(cx, |editor, window, cx| {
22134 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
22135 });
22136
22137 let _other_editor_1 = workspace
22138 .update_in(cx, |workspace, window, cx| {
22139 workspace.open_path(
22140 (worktree_id, "lib.rs"),
22141 Some(pane_1.downgrade()),
22142 true,
22143 window,
22144 cx,
22145 )
22146 })
22147 .unwrap()
22148 .await
22149 .downcast::<Editor>()
22150 .unwrap();
22151 pane_1
22152 .update_in(cx, |pane, window, cx| {
22153 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22154 })
22155 .await
22156 .unwrap();
22157 drop(editor_1);
22158 pane_1.update(cx, |pane, cx| {
22159 pane.active_item()
22160 .unwrap()
22161 .downcast::<Editor>()
22162 .unwrap()
22163 .update(cx, |editor, cx| {
22164 assert_eq!(
22165 editor.display_text(cx),
22166 lib_text,
22167 "Other file should be open and active",
22168 );
22169 });
22170 assert_eq!(pane.items().count(), 1, "No other editors should be open");
22171 });
22172
22173 let _other_editor_2 = workspace
22174 .update_in(cx, |workspace, window, cx| {
22175 workspace.open_path(
22176 (worktree_id, "lib.rs"),
22177 Some(pane_2.downgrade()),
22178 true,
22179 window,
22180 cx,
22181 )
22182 })
22183 .unwrap()
22184 .await
22185 .downcast::<Editor>()
22186 .unwrap();
22187 pane_2
22188 .update_in(cx, |pane, window, cx| {
22189 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22190 })
22191 .await
22192 .unwrap();
22193 drop(editor_2);
22194 pane_2.update(cx, |pane, cx| {
22195 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22196 open_editor.update(cx, |editor, cx| {
22197 assert_eq!(
22198 editor.display_text(cx),
22199 lib_text,
22200 "Other file should be open and active in another panel too",
22201 );
22202 });
22203 assert_eq!(
22204 pane.items().count(),
22205 1,
22206 "No other editors should be open in another pane",
22207 );
22208 });
22209
22210 let _editor_1_reopened = workspace
22211 .update_in(cx, |workspace, window, cx| {
22212 workspace.open_path(
22213 (worktree_id, "main.rs"),
22214 Some(pane_1.downgrade()),
22215 true,
22216 window,
22217 cx,
22218 )
22219 })
22220 .unwrap()
22221 .await
22222 .downcast::<Editor>()
22223 .unwrap();
22224 let _editor_2_reopened = workspace
22225 .update_in(cx, |workspace, window, cx| {
22226 workspace.open_path(
22227 (worktree_id, "main.rs"),
22228 Some(pane_2.downgrade()),
22229 true,
22230 window,
22231 cx,
22232 )
22233 })
22234 .unwrap()
22235 .await
22236 .downcast::<Editor>()
22237 .unwrap();
22238 pane_1.update(cx, |pane, cx| {
22239 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22240 open_editor.update(cx, |editor, cx| {
22241 assert_eq!(
22242 editor.display_text(cx),
22243 main_text,
22244 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
22245 );
22246 assert_eq!(
22247 editor
22248 .selections
22249 .all::<Point>(cx)
22250 .into_iter()
22251 .map(|s| s.range())
22252 .collect::<Vec<_>>(),
22253 expected_ranges,
22254 "Previous editor in the 1st panel had selections and should get them restored on reopen",
22255 );
22256 })
22257 });
22258 pane_2.update(cx, |pane, cx| {
22259 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22260 open_editor.update(cx, |editor, cx| {
22261 assert_eq!(
22262 editor.display_text(cx),
22263 r#"fn main() {
22264⋯rintln!("1");
22265⋯intln!("2");
22266⋯ntln!("3");
22267println!("4");
22268println!("5");
22269}"#,
22270 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
22271 );
22272 assert_eq!(
22273 editor
22274 .selections
22275 .all::<Point>(cx)
22276 .into_iter()
22277 .map(|s| s.range())
22278 .collect::<Vec<_>>(),
22279 vec![Point::zero()..Point::zero()],
22280 "Previous editor in the 2nd pane had no selections changed hence should restore none",
22281 );
22282 })
22283 });
22284}
22285
22286#[gpui::test]
22287async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
22288 init_test(cx, |_| {});
22289
22290 let fs = FakeFs::new(cx.executor());
22291 let main_text = r#"fn main() {
22292println!("1");
22293println!("2");
22294println!("3");
22295println!("4");
22296println!("5");
22297}"#;
22298 let lib_text = "mod foo {}";
22299 fs.insert_tree(
22300 path!("/a"),
22301 json!({
22302 "lib.rs": lib_text,
22303 "main.rs": main_text,
22304 }),
22305 )
22306 .await;
22307
22308 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22309 let (workspace, cx) =
22310 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22311 let worktree_id = workspace.update(cx, |workspace, cx| {
22312 workspace.project().update(cx, |project, cx| {
22313 project.worktrees(cx).next().unwrap().read(cx).id()
22314 })
22315 });
22316
22317 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22318 let editor = workspace
22319 .update_in(cx, |workspace, window, cx| {
22320 workspace.open_path(
22321 (worktree_id, "main.rs"),
22322 Some(pane.downgrade()),
22323 true,
22324 window,
22325 cx,
22326 )
22327 })
22328 .unwrap()
22329 .await
22330 .downcast::<Editor>()
22331 .unwrap();
22332 pane.update(cx, |pane, cx| {
22333 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22334 open_editor.update(cx, |editor, cx| {
22335 assert_eq!(
22336 editor.display_text(cx),
22337 main_text,
22338 "Original main.rs text on initial open",
22339 );
22340 })
22341 });
22342 editor.update_in(cx, |editor, window, cx| {
22343 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22344 });
22345
22346 cx.update_global(|store: &mut SettingsStore, cx| {
22347 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22348 s.restore_on_file_reopen = Some(false);
22349 });
22350 });
22351 editor.update_in(cx, |editor, window, cx| {
22352 editor.fold_ranges(
22353 vec![
22354 Point::new(1, 0)..Point::new(1, 1),
22355 Point::new(2, 0)..Point::new(2, 2),
22356 Point::new(3, 0)..Point::new(3, 3),
22357 ],
22358 false,
22359 window,
22360 cx,
22361 );
22362 });
22363 pane.update_in(cx, |pane, window, cx| {
22364 pane.close_all_items(&CloseAllItems::default(), window, cx)
22365 })
22366 .await
22367 .unwrap();
22368 pane.update(cx, |pane, _| {
22369 assert!(pane.active_item().is_none());
22370 });
22371 cx.update_global(|store: &mut SettingsStore, cx| {
22372 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22373 s.restore_on_file_reopen = Some(true);
22374 });
22375 });
22376
22377 let _editor_reopened = workspace
22378 .update_in(cx, |workspace, window, cx| {
22379 workspace.open_path(
22380 (worktree_id, "main.rs"),
22381 Some(pane.downgrade()),
22382 true,
22383 window,
22384 cx,
22385 )
22386 })
22387 .unwrap()
22388 .await
22389 .downcast::<Editor>()
22390 .unwrap();
22391 pane.update(cx, |pane, cx| {
22392 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22393 open_editor.update(cx, |editor, cx| {
22394 assert_eq!(
22395 editor.display_text(cx),
22396 main_text,
22397 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22398 );
22399 })
22400 });
22401}
22402
22403#[gpui::test]
22404async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22405 struct EmptyModalView {
22406 focus_handle: gpui::FocusHandle,
22407 }
22408 impl EventEmitter<DismissEvent> for EmptyModalView {}
22409 impl Render for EmptyModalView {
22410 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22411 div()
22412 }
22413 }
22414 impl Focusable for EmptyModalView {
22415 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22416 self.focus_handle.clone()
22417 }
22418 }
22419 impl workspace::ModalView for EmptyModalView {}
22420 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22421 EmptyModalView {
22422 focus_handle: cx.focus_handle(),
22423 }
22424 }
22425
22426 init_test(cx, |_| {});
22427
22428 let fs = FakeFs::new(cx.executor());
22429 let project = Project::test(fs, [], cx).await;
22430 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22431 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22432 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22433 let editor = cx.new_window_entity(|window, cx| {
22434 Editor::new(
22435 EditorMode::full(),
22436 buffer,
22437 Some(project.clone()),
22438 window,
22439 cx,
22440 )
22441 });
22442 workspace
22443 .update(cx, |workspace, window, cx| {
22444 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22445 })
22446 .unwrap();
22447 editor.update_in(cx, |editor, window, cx| {
22448 editor.open_context_menu(&OpenContextMenu, window, cx);
22449 assert!(editor.mouse_context_menu.is_some());
22450 });
22451 workspace
22452 .update(cx, |workspace, window, cx| {
22453 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22454 })
22455 .unwrap();
22456 cx.read(|cx| {
22457 assert!(editor.read(cx).mouse_context_menu.is_none());
22458 });
22459}
22460
22461#[gpui::test]
22462async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22463 init_test(cx, |_| {});
22464
22465 let fs = FakeFs::new(cx.executor());
22466 fs.insert_file(path!("/file.html"), Default::default())
22467 .await;
22468
22469 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22470
22471 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22472 let html_language = Arc::new(Language::new(
22473 LanguageConfig {
22474 name: "HTML".into(),
22475 matcher: LanguageMatcher {
22476 path_suffixes: vec!["html".to_string()],
22477 ..LanguageMatcher::default()
22478 },
22479 brackets: BracketPairConfig {
22480 pairs: vec![BracketPair {
22481 start: "<".into(),
22482 end: ">".into(),
22483 close: true,
22484 ..Default::default()
22485 }],
22486 ..Default::default()
22487 },
22488 ..Default::default()
22489 },
22490 Some(tree_sitter_html::LANGUAGE.into()),
22491 ));
22492 language_registry.add(html_language);
22493 let mut fake_servers = language_registry.register_fake_lsp(
22494 "HTML",
22495 FakeLspAdapter {
22496 capabilities: lsp::ServerCapabilities {
22497 completion_provider: Some(lsp::CompletionOptions {
22498 resolve_provider: Some(true),
22499 ..Default::default()
22500 }),
22501 ..Default::default()
22502 },
22503 ..Default::default()
22504 },
22505 );
22506
22507 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22508 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22509
22510 let worktree_id = workspace
22511 .update(cx, |workspace, _window, cx| {
22512 workspace.project().update(cx, |project, cx| {
22513 project.worktrees(cx).next().unwrap().read(cx).id()
22514 })
22515 })
22516 .unwrap();
22517 project
22518 .update(cx, |project, cx| {
22519 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22520 })
22521 .await
22522 .unwrap();
22523 let editor = workspace
22524 .update(cx, |workspace, window, cx| {
22525 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22526 })
22527 .unwrap()
22528 .await
22529 .unwrap()
22530 .downcast::<Editor>()
22531 .unwrap();
22532
22533 let fake_server = fake_servers.next().await.unwrap();
22534 editor.update_in(cx, |editor, window, cx| {
22535 editor.set_text("<ad></ad>", window, cx);
22536 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22537 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22538 });
22539 let Some((buffer, _)) = editor
22540 .buffer
22541 .read(cx)
22542 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22543 else {
22544 panic!("Failed to get buffer for selection position");
22545 };
22546 let buffer = buffer.read(cx);
22547 let buffer_id = buffer.remote_id();
22548 let opening_range =
22549 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22550 let closing_range =
22551 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22552 let mut linked_ranges = HashMap::default();
22553 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
22554 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22555 });
22556 let mut completion_handle =
22557 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22558 Ok(Some(lsp::CompletionResponse::Array(vec![
22559 lsp::CompletionItem {
22560 label: "head".to_string(),
22561 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22562 lsp::InsertReplaceEdit {
22563 new_text: "head".to_string(),
22564 insert: lsp::Range::new(
22565 lsp::Position::new(0, 1),
22566 lsp::Position::new(0, 3),
22567 ),
22568 replace: lsp::Range::new(
22569 lsp::Position::new(0, 1),
22570 lsp::Position::new(0, 3),
22571 ),
22572 },
22573 )),
22574 ..Default::default()
22575 },
22576 ])))
22577 });
22578 editor.update_in(cx, |editor, window, cx| {
22579 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22580 });
22581 cx.run_until_parked();
22582 completion_handle.next().await.unwrap();
22583 editor.update(cx, |editor, _| {
22584 assert!(
22585 editor.context_menu_visible(),
22586 "Completion menu should be visible"
22587 );
22588 });
22589 editor.update_in(cx, |editor, window, cx| {
22590 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22591 });
22592 cx.executor().run_until_parked();
22593 editor.update(cx, |editor, cx| {
22594 assert_eq!(editor.text(cx), "<head></head>");
22595 });
22596}
22597
22598#[gpui::test]
22599async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22600 init_test(cx, |_| {});
22601
22602 let fs = FakeFs::new(cx.executor());
22603 fs.insert_tree(
22604 path!("/root"),
22605 json!({
22606 "a": {
22607 "main.rs": "fn main() {}",
22608 },
22609 "foo": {
22610 "bar": {
22611 "external_file.rs": "pub mod external {}",
22612 }
22613 }
22614 }),
22615 )
22616 .await;
22617
22618 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22619 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22620 language_registry.add(rust_lang());
22621 let _fake_servers = language_registry.register_fake_lsp(
22622 "Rust",
22623 FakeLspAdapter {
22624 ..FakeLspAdapter::default()
22625 },
22626 );
22627 let (workspace, cx) =
22628 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22629 let worktree_id = workspace.update(cx, |workspace, cx| {
22630 workspace.project().update(cx, |project, cx| {
22631 project.worktrees(cx).next().unwrap().read(cx).id()
22632 })
22633 });
22634
22635 let assert_language_servers_count =
22636 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22637 project.update(cx, |project, cx| {
22638 let current = project
22639 .lsp_store()
22640 .read(cx)
22641 .as_local()
22642 .unwrap()
22643 .language_servers
22644 .len();
22645 assert_eq!(expected, current, "{context}");
22646 });
22647 };
22648
22649 assert_language_servers_count(
22650 0,
22651 "No servers should be running before any file is open",
22652 cx,
22653 );
22654 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22655 let main_editor = workspace
22656 .update_in(cx, |workspace, window, cx| {
22657 workspace.open_path(
22658 (worktree_id, "main.rs"),
22659 Some(pane.downgrade()),
22660 true,
22661 window,
22662 cx,
22663 )
22664 })
22665 .unwrap()
22666 .await
22667 .downcast::<Editor>()
22668 .unwrap();
22669 pane.update(cx, |pane, cx| {
22670 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22671 open_editor.update(cx, |editor, cx| {
22672 assert_eq!(
22673 editor.display_text(cx),
22674 "fn main() {}",
22675 "Original main.rs text on initial open",
22676 );
22677 });
22678 assert_eq!(open_editor, main_editor);
22679 });
22680 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22681
22682 let external_editor = workspace
22683 .update_in(cx, |workspace, window, cx| {
22684 workspace.open_abs_path(
22685 PathBuf::from("/root/foo/bar/external_file.rs"),
22686 OpenOptions::default(),
22687 window,
22688 cx,
22689 )
22690 })
22691 .await
22692 .expect("opening external file")
22693 .downcast::<Editor>()
22694 .expect("downcasted external file's open element to editor");
22695 pane.update(cx, |pane, cx| {
22696 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22697 open_editor.update(cx, |editor, cx| {
22698 assert_eq!(
22699 editor.display_text(cx),
22700 "pub mod external {}",
22701 "External file is open now",
22702 );
22703 });
22704 assert_eq!(open_editor, external_editor);
22705 });
22706 assert_language_servers_count(
22707 1,
22708 "Second, external, *.rs file should join the existing server",
22709 cx,
22710 );
22711
22712 pane.update_in(cx, |pane, window, cx| {
22713 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22714 })
22715 .await
22716 .unwrap();
22717 pane.update_in(cx, |pane, window, cx| {
22718 pane.navigate_backward(&Default::default(), window, cx);
22719 });
22720 cx.run_until_parked();
22721 pane.update(cx, |pane, cx| {
22722 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22723 open_editor.update(cx, |editor, cx| {
22724 assert_eq!(
22725 editor.display_text(cx),
22726 "pub mod external {}",
22727 "External file is open now",
22728 );
22729 });
22730 });
22731 assert_language_servers_count(
22732 1,
22733 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22734 cx,
22735 );
22736
22737 cx.update(|_, cx| {
22738 workspace::reload(cx);
22739 });
22740 assert_language_servers_count(
22741 1,
22742 "After reloading the worktree with local and external files opened, only one project should be started",
22743 cx,
22744 );
22745}
22746
22747#[gpui::test]
22748async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22749 init_test(cx, |_| {});
22750
22751 let mut cx = EditorTestContext::new(cx).await;
22752 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22753 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22754
22755 // test cursor move to start of each line on tab
22756 // for `if`, `elif`, `else`, `while`, `with` and `for`
22757 cx.set_state(indoc! {"
22758 def main():
22759 ˇ for item in items:
22760 ˇ while item.active:
22761 ˇ if item.value > 10:
22762 ˇ continue
22763 ˇ elif item.value < 0:
22764 ˇ break
22765 ˇ else:
22766 ˇ with item.context() as ctx:
22767 ˇ yield count
22768 ˇ else:
22769 ˇ log('while else')
22770 ˇ else:
22771 ˇ log('for else')
22772 "});
22773 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22774 cx.assert_editor_state(indoc! {"
22775 def main():
22776 ˇfor item in items:
22777 ˇwhile item.active:
22778 ˇif item.value > 10:
22779 ˇcontinue
22780 ˇelif item.value < 0:
22781 ˇbreak
22782 ˇelse:
22783 ˇwith item.context() as ctx:
22784 ˇyield count
22785 ˇelse:
22786 ˇlog('while else')
22787 ˇelse:
22788 ˇlog('for else')
22789 "});
22790 // test relative indent is preserved when tab
22791 // for `if`, `elif`, `else`, `while`, `with` and `for`
22792 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22793 cx.assert_editor_state(indoc! {"
22794 def main():
22795 ˇfor item in items:
22796 ˇwhile item.active:
22797 ˇif item.value > 10:
22798 ˇcontinue
22799 ˇelif item.value < 0:
22800 ˇbreak
22801 ˇelse:
22802 ˇwith item.context() as ctx:
22803 ˇyield count
22804 ˇelse:
22805 ˇlog('while else')
22806 ˇelse:
22807 ˇlog('for else')
22808 "});
22809
22810 // test cursor move to start of each line on tab
22811 // for `try`, `except`, `else`, `finally`, `match` and `def`
22812 cx.set_state(indoc! {"
22813 def main():
22814 ˇ try:
22815 ˇ fetch()
22816 ˇ except ValueError:
22817 ˇ handle_error()
22818 ˇ else:
22819 ˇ match value:
22820 ˇ case _:
22821 ˇ finally:
22822 ˇ def status():
22823 ˇ return 0
22824 "});
22825 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22826 cx.assert_editor_state(indoc! {"
22827 def main():
22828 ˇtry:
22829 ˇfetch()
22830 ˇexcept ValueError:
22831 ˇhandle_error()
22832 ˇelse:
22833 ˇmatch value:
22834 ˇcase _:
22835 ˇfinally:
22836 ˇdef status():
22837 ˇreturn 0
22838 "});
22839 // test relative indent is preserved when tab
22840 // for `try`, `except`, `else`, `finally`, `match` and `def`
22841 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22842 cx.assert_editor_state(indoc! {"
22843 def main():
22844 ˇtry:
22845 ˇfetch()
22846 ˇexcept ValueError:
22847 ˇhandle_error()
22848 ˇelse:
22849 ˇmatch value:
22850 ˇcase _:
22851 ˇfinally:
22852 ˇdef status():
22853 ˇreturn 0
22854 "});
22855}
22856
22857#[gpui::test]
22858async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22859 init_test(cx, |_| {});
22860
22861 let mut cx = EditorTestContext::new(cx).await;
22862 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22863 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22864
22865 // test `else` auto outdents when typed inside `if` block
22866 cx.set_state(indoc! {"
22867 def main():
22868 if i == 2:
22869 return
22870 ˇ
22871 "});
22872 cx.update_editor(|editor, window, cx| {
22873 editor.handle_input("else:", window, cx);
22874 });
22875 cx.assert_editor_state(indoc! {"
22876 def main():
22877 if i == 2:
22878 return
22879 else:ˇ
22880 "});
22881
22882 // test `except` auto outdents when typed inside `try` block
22883 cx.set_state(indoc! {"
22884 def main():
22885 try:
22886 i = 2
22887 ˇ
22888 "});
22889 cx.update_editor(|editor, window, cx| {
22890 editor.handle_input("except:", window, cx);
22891 });
22892 cx.assert_editor_state(indoc! {"
22893 def main():
22894 try:
22895 i = 2
22896 except:ˇ
22897 "});
22898
22899 // test `else` auto outdents when typed inside `except` block
22900 cx.set_state(indoc! {"
22901 def main():
22902 try:
22903 i = 2
22904 except:
22905 j = 2
22906 ˇ
22907 "});
22908 cx.update_editor(|editor, window, cx| {
22909 editor.handle_input("else:", window, cx);
22910 });
22911 cx.assert_editor_state(indoc! {"
22912 def main():
22913 try:
22914 i = 2
22915 except:
22916 j = 2
22917 else:ˇ
22918 "});
22919
22920 // test `finally` auto outdents when typed inside `else` block
22921 cx.set_state(indoc! {"
22922 def main():
22923 try:
22924 i = 2
22925 except:
22926 j = 2
22927 else:
22928 k = 2
22929 ˇ
22930 "});
22931 cx.update_editor(|editor, window, cx| {
22932 editor.handle_input("finally:", window, cx);
22933 });
22934 cx.assert_editor_state(indoc! {"
22935 def main():
22936 try:
22937 i = 2
22938 except:
22939 j = 2
22940 else:
22941 k = 2
22942 finally:ˇ
22943 "});
22944
22945 // test `else` does not outdents when typed inside `except` block right after for block
22946 cx.set_state(indoc! {"
22947 def main():
22948 try:
22949 i = 2
22950 except:
22951 for i in range(n):
22952 pass
22953 ˇ
22954 "});
22955 cx.update_editor(|editor, window, cx| {
22956 editor.handle_input("else:", window, cx);
22957 });
22958 cx.assert_editor_state(indoc! {"
22959 def main():
22960 try:
22961 i = 2
22962 except:
22963 for i in range(n):
22964 pass
22965 else:ˇ
22966 "});
22967
22968 // test `finally` auto outdents when typed inside `else` block right after for block
22969 cx.set_state(indoc! {"
22970 def main():
22971 try:
22972 i = 2
22973 except:
22974 j = 2
22975 else:
22976 for i in range(n):
22977 pass
22978 ˇ
22979 "});
22980 cx.update_editor(|editor, window, cx| {
22981 editor.handle_input("finally:", window, cx);
22982 });
22983 cx.assert_editor_state(indoc! {"
22984 def main():
22985 try:
22986 i = 2
22987 except:
22988 j = 2
22989 else:
22990 for i in range(n):
22991 pass
22992 finally:ˇ
22993 "});
22994
22995 // test `except` outdents to inner "try" block
22996 cx.set_state(indoc! {"
22997 def main():
22998 try:
22999 i = 2
23000 if i == 2:
23001 try:
23002 i = 3
23003 ˇ
23004 "});
23005 cx.update_editor(|editor, window, cx| {
23006 editor.handle_input("except:", window, cx);
23007 });
23008 cx.assert_editor_state(indoc! {"
23009 def main():
23010 try:
23011 i = 2
23012 if i == 2:
23013 try:
23014 i = 3
23015 except:ˇ
23016 "});
23017
23018 // test `except` outdents to outer "try" block
23019 cx.set_state(indoc! {"
23020 def main():
23021 try:
23022 i = 2
23023 if i == 2:
23024 try:
23025 i = 3
23026 ˇ
23027 "});
23028 cx.update_editor(|editor, window, cx| {
23029 editor.handle_input("except:", window, cx);
23030 });
23031 cx.assert_editor_state(indoc! {"
23032 def main():
23033 try:
23034 i = 2
23035 if i == 2:
23036 try:
23037 i = 3
23038 except:ˇ
23039 "});
23040
23041 // test `else` stays at correct indent when typed after `for` block
23042 cx.set_state(indoc! {"
23043 def main():
23044 for i in range(10):
23045 if i == 3:
23046 break
23047 ˇ
23048 "});
23049 cx.update_editor(|editor, window, cx| {
23050 editor.handle_input("else:", window, cx);
23051 });
23052 cx.assert_editor_state(indoc! {"
23053 def main():
23054 for i in range(10):
23055 if i == 3:
23056 break
23057 else:ˇ
23058 "});
23059
23060 // test does not outdent on typing after line with square brackets
23061 cx.set_state(indoc! {"
23062 def f() -> list[str]:
23063 ˇ
23064 "});
23065 cx.update_editor(|editor, window, cx| {
23066 editor.handle_input("a", window, cx);
23067 });
23068 cx.assert_editor_state(indoc! {"
23069 def f() -> list[str]:
23070 aˇ
23071 "});
23072
23073 // test does not outdent on typing : after case keyword
23074 cx.set_state(indoc! {"
23075 match 1:
23076 caseˇ
23077 "});
23078 cx.update_editor(|editor, window, cx| {
23079 editor.handle_input(":", window, cx);
23080 });
23081 cx.assert_editor_state(indoc! {"
23082 match 1:
23083 case:ˇ
23084 "});
23085}
23086
23087#[gpui::test]
23088async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23089 init_test(cx, |_| {});
23090 update_test_language_settings(cx, |settings| {
23091 settings.defaults.extend_comment_on_newline = Some(false);
23092 });
23093 let mut cx = EditorTestContext::new(cx).await;
23094 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23095 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23096
23097 // test correct indent after newline on comment
23098 cx.set_state(indoc! {"
23099 # COMMENT:ˇ
23100 "});
23101 cx.update_editor(|editor, window, cx| {
23102 editor.newline(&Newline, window, cx);
23103 });
23104 cx.assert_editor_state(indoc! {"
23105 # COMMENT:
23106 ˇ
23107 "});
23108
23109 // test correct indent after newline in brackets
23110 cx.set_state(indoc! {"
23111 {ˇ}
23112 "});
23113 cx.update_editor(|editor, window, cx| {
23114 editor.newline(&Newline, window, cx);
23115 });
23116 cx.run_until_parked();
23117 cx.assert_editor_state(indoc! {"
23118 {
23119 ˇ
23120 }
23121 "});
23122
23123 cx.set_state(indoc! {"
23124 (ˇ)
23125 "});
23126 cx.update_editor(|editor, window, cx| {
23127 editor.newline(&Newline, window, cx);
23128 });
23129 cx.run_until_parked();
23130 cx.assert_editor_state(indoc! {"
23131 (
23132 ˇ
23133 )
23134 "});
23135
23136 // do not indent after empty lists or dictionaries
23137 cx.set_state(indoc! {"
23138 a = []ˇ
23139 "});
23140 cx.update_editor(|editor, window, cx| {
23141 editor.newline(&Newline, window, cx);
23142 });
23143 cx.run_until_parked();
23144 cx.assert_editor_state(indoc! {"
23145 a = []
23146 ˇ
23147 "});
23148}
23149
23150#[gpui::test]
23151async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
23152 init_test(cx, |_| {});
23153
23154 let mut cx = EditorTestContext::new(cx).await;
23155 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23156 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23157
23158 // test cursor move to start of each line on tab
23159 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
23160 cx.set_state(indoc! {"
23161 function main() {
23162 ˇ for item in $items; do
23163 ˇ while [ -n \"$item\" ]; do
23164 ˇ if [ \"$value\" -gt 10 ]; then
23165 ˇ continue
23166 ˇ elif [ \"$value\" -lt 0 ]; then
23167 ˇ break
23168 ˇ else
23169 ˇ echo \"$item\"
23170 ˇ fi
23171 ˇ done
23172 ˇ done
23173 ˇ}
23174 "});
23175 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23176 cx.assert_editor_state(indoc! {"
23177 function main() {
23178 ˇfor item in $items; do
23179 ˇwhile [ -n \"$item\" ]; do
23180 ˇif [ \"$value\" -gt 10 ]; then
23181 ˇcontinue
23182 ˇelif [ \"$value\" -lt 0 ]; then
23183 ˇbreak
23184 ˇelse
23185 ˇecho \"$item\"
23186 ˇfi
23187 ˇdone
23188 ˇdone
23189 ˇ}
23190 "});
23191 // test relative indent is preserved when tab
23192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23193 cx.assert_editor_state(indoc! {"
23194 function main() {
23195 ˇfor item in $items; do
23196 ˇwhile [ -n \"$item\" ]; do
23197 ˇif [ \"$value\" -gt 10 ]; then
23198 ˇcontinue
23199 ˇelif [ \"$value\" -lt 0 ]; then
23200 ˇbreak
23201 ˇelse
23202 ˇecho \"$item\"
23203 ˇfi
23204 ˇdone
23205 ˇdone
23206 ˇ}
23207 "});
23208
23209 // test cursor move to start of each line on tab
23210 // for `case` statement with patterns
23211 cx.set_state(indoc! {"
23212 function handle() {
23213 ˇ case \"$1\" in
23214 ˇ start)
23215 ˇ echo \"a\"
23216 ˇ ;;
23217 ˇ stop)
23218 ˇ echo \"b\"
23219 ˇ ;;
23220 ˇ *)
23221 ˇ echo \"c\"
23222 ˇ ;;
23223 ˇ esac
23224 ˇ}
23225 "});
23226 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23227 cx.assert_editor_state(indoc! {"
23228 function handle() {
23229 ˇcase \"$1\" in
23230 ˇstart)
23231 ˇecho \"a\"
23232 ˇ;;
23233 ˇstop)
23234 ˇecho \"b\"
23235 ˇ;;
23236 ˇ*)
23237 ˇecho \"c\"
23238 ˇ;;
23239 ˇesac
23240 ˇ}
23241 "});
23242}
23243
23244#[gpui::test]
23245async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
23246 init_test(cx, |_| {});
23247
23248 let mut cx = EditorTestContext::new(cx).await;
23249 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23250 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23251
23252 // test indents on comment insert
23253 cx.set_state(indoc! {"
23254 function main() {
23255 ˇ for item in $items; do
23256 ˇ while [ -n \"$item\" ]; do
23257 ˇ if [ \"$value\" -gt 10 ]; then
23258 ˇ continue
23259 ˇ elif [ \"$value\" -lt 0 ]; then
23260 ˇ break
23261 ˇ else
23262 ˇ echo \"$item\"
23263 ˇ fi
23264 ˇ done
23265 ˇ done
23266 ˇ}
23267 "});
23268 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
23269 cx.assert_editor_state(indoc! {"
23270 function main() {
23271 #ˇ for item in $items; do
23272 #ˇ while [ -n \"$item\" ]; do
23273 #ˇ if [ \"$value\" -gt 10 ]; then
23274 #ˇ continue
23275 #ˇ elif [ \"$value\" -lt 0 ]; then
23276 #ˇ break
23277 #ˇ else
23278 #ˇ echo \"$item\"
23279 #ˇ fi
23280 #ˇ done
23281 #ˇ done
23282 #ˇ}
23283 "});
23284}
23285
23286#[gpui::test]
23287async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
23288 init_test(cx, |_| {});
23289
23290 let mut cx = EditorTestContext::new(cx).await;
23291 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23292 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23293
23294 // test `else` auto outdents when typed inside `if` block
23295 cx.set_state(indoc! {"
23296 if [ \"$1\" = \"test\" ]; then
23297 echo \"foo bar\"
23298 ˇ
23299 "});
23300 cx.update_editor(|editor, window, cx| {
23301 editor.handle_input("else", window, cx);
23302 });
23303 cx.assert_editor_state(indoc! {"
23304 if [ \"$1\" = \"test\" ]; then
23305 echo \"foo bar\"
23306 elseˇ
23307 "});
23308
23309 // test `elif` auto outdents when typed inside `if` block
23310 cx.set_state(indoc! {"
23311 if [ \"$1\" = \"test\" ]; then
23312 echo \"foo bar\"
23313 ˇ
23314 "});
23315 cx.update_editor(|editor, window, cx| {
23316 editor.handle_input("elif", window, cx);
23317 });
23318 cx.assert_editor_state(indoc! {"
23319 if [ \"$1\" = \"test\" ]; then
23320 echo \"foo bar\"
23321 elifˇ
23322 "});
23323
23324 // test `fi` auto outdents when typed inside `else` block
23325 cx.set_state(indoc! {"
23326 if [ \"$1\" = \"test\" ]; then
23327 echo \"foo bar\"
23328 else
23329 echo \"bar baz\"
23330 ˇ
23331 "});
23332 cx.update_editor(|editor, window, cx| {
23333 editor.handle_input("fi", window, cx);
23334 });
23335 cx.assert_editor_state(indoc! {"
23336 if [ \"$1\" = \"test\" ]; then
23337 echo \"foo bar\"
23338 else
23339 echo \"bar baz\"
23340 fiˇ
23341 "});
23342
23343 // test `done` auto outdents when typed inside `while` block
23344 cx.set_state(indoc! {"
23345 while read line; do
23346 echo \"$line\"
23347 ˇ
23348 "});
23349 cx.update_editor(|editor, window, cx| {
23350 editor.handle_input("done", window, cx);
23351 });
23352 cx.assert_editor_state(indoc! {"
23353 while read line; do
23354 echo \"$line\"
23355 doneˇ
23356 "});
23357
23358 // test `done` auto outdents when typed inside `for` block
23359 cx.set_state(indoc! {"
23360 for file in *.txt; do
23361 cat \"$file\"
23362 ˇ
23363 "});
23364 cx.update_editor(|editor, window, cx| {
23365 editor.handle_input("done", window, cx);
23366 });
23367 cx.assert_editor_state(indoc! {"
23368 for file in *.txt; do
23369 cat \"$file\"
23370 doneˇ
23371 "});
23372
23373 // test `esac` auto outdents when typed inside `case` block
23374 cx.set_state(indoc! {"
23375 case \"$1\" in
23376 start)
23377 echo \"foo bar\"
23378 ;;
23379 stop)
23380 echo \"bar baz\"
23381 ;;
23382 ˇ
23383 "});
23384 cx.update_editor(|editor, window, cx| {
23385 editor.handle_input("esac", window, cx);
23386 });
23387 cx.assert_editor_state(indoc! {"
23388 case \"$1\" in
23389 start)
23390 echo \"foo bar\"
23391 ;;
23392 stop)
23393 echo \"bar baz\"
23394 ;;
23395 esacˇ
23396 "});
23397
23398 // test `*)` auto outdents when typed inside `case` block
23399 cx.set_state(indoc! {"
23400 case \"$1\" in
23401 start)
23402 echo \"foo bar\"
23403 ;;
23404 ˇ
23405 "});
23406 cx.update_editor(|editor, window, cx| {
23407 editor.handle_input("*)", window, cx);
23408 });
23409 cx.assert_editor_state(indoc! {"
23410 case \"$1\" in
23411 start)
23412 echo \"foo bar\"
23413 ;;
23414 *)ˇ
23415 "});
23416
23417 // test `fi` outdents to correct level with nested if blocks
23418 cx.set_state(indoc! {"
23419 if [ \"$1\" = \"test\" ]; then
23420 echo \"outer if\"
23421 if [ \"$2\" = \"debug\" ]; then
23422 echo \"inner if\"
23423 ˇ
23424 "});
23425 cx.update_editor(|editor, window, cx| {
23426 editor.handle_input("fi", window, cx);
23427 });
23428 cx.assert_editor_state(indoc! {"
23429 if [ \"$1\" = \"test\" ]; then
23430 echo \"outer if\"
23431 if [ \"$2\" = \"debug\" ]; then
23432 echo \"inner if\"
23433 fiˇ
23434 "});
23435}
23436
23437#[gpui::test]
23438async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23439 init_test(cx, |_| {});
23440 update_test_language_settings(cx, |settings| {
23441 settings.defaults.extend_comment_on_newline = Some(false);
23442 });
23443 let mut cx = EditorTestContext::new(cx).await;
23444 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23445 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23446
23447 // test correct indent after newline on comment
23448 cx.set_state(indoc! {"
23449 # COMMENT:ˇ
23450 "});
23451 cx.update_editor(|editor, window, cx| {
23452 editor.newline(&Newline, window, cx);
23453 });
23454 cx.assert_editor_state(indoc! {"
23455 # COMMENT:
23456 ˇ
23457 "});
23458
23459 // test correct indent after newline after `then`
23460 cx.set_state(indoc! {"
23461
23462 if [ \"$1\" = \"test\" ]; thenˇ
23463 "});
23464 cx.update_editor(|editor, window, cx| {
23465 editor.newline(&Newline, window, cx);
23466 });
23467 cx.run_until_parked();
23468 cx.assert_editor_state(indoc! {"
23469
23470 if [ \"$1\" = \"test\" ]; then
23471 ˇ
23472 "});
23473
23474 // test correct indent after newline after `else`
23475 cx.set_state(indoc! {"
23476 if [ \"$1\" = \"test\" ]; then
23477 elseˇ
23478 "});
23479 cx.update_editor(|editor, window, cx| {
23480 editor.newline(&Newline, window, cx);
23481 });
23482 cx.run_until_parked();
23483 cx.assert_editor_state(indoc! {"
23484 if [ \"$1\" = \"test\" ]; then
23485 else
23486 ˇ
23487 "});
23488
23489 // test correct indent after newline after `elif`
23490 cx.set_state(indoc! {"
23491 if [ \"$1\" = \"test\" ]; then
23492 elifˇ
23493 "});
23494 cx.update_editor(|editor, window, cx| {
23495 editor.newline(&Newline, window, cx);
23496 });
23497 cx.run_until_parked();
23498 cx.assert_editor_state(indoc! {"
23499 if [ \"$1\" = \"test\" ]; then
23500 elif
23501 ˇ
23502 "});
23503
23504 // test correct indent after newline after `do`
23505 cx.set_state(indoc! {"
23506 for file in *.txt; doˇ
23507 "});
23508 cx.update_editor(|editor, window, cx| {
23509 editor.newline(&Newline, window, cx);
23510 });
23511 cx.run_until_parked();
23512 cx.assert_editor_state(indoc! {"
23513 for file in *.txt; do
23514 ˇ
23515 "});
23516
23517 // test correct indent after newline after case pattern
23518 cx.set_state(indoc! {"
23519 case \"$1\" in
23520 start)ˇ
23521 "});
23522 cx.update_editor(|editor, window, cx| {
23523 editor.newline(&Newline, window, cx);
23524 });
23525 cx.run_until_parked();
23526 cx.assert_editor_state(indoc! {"
23527 case \"$1\" in
23528 start)
23529 ˇ
23530 "});
23531
23532 // test correct indent after newline after case pattern
23533 cx.set_state(indoc! {"
23534 case \"$1\" in
23535 start)
23536 ;;
23537 *)ˇ
23538 "});
23539 cx.update_editor(|editor, window, cx| {
23540 editor.newline(&Newline, window, cx);
23541 });
23542 cx.run_until_parked();
23543 cx.assert_editor_state(indoc! {"
23544 case \"$1\" in
23545 start)
23546 ;;
23547 *)
23548 ˇ
23549 "});
23550
23551 // test correct indent after newline after function opening brace
23552 cx.set_state(indoc! {"
23553 function test() {ˇ}
23554 "});
23555 cx.update_editor(|editor, window, cx| {
23556 editor.newline(&Newline, window, cx);
23557 });
23558 cx.run_until_parked();
23559 cx.assert_editor_state(indoc! {"
23560 function test() {
23561 ˇ
23562 }
23563 "});
23564
23565 // test no extra indent after semicolon on same line
23566 cx.set_state(indoc! {"
23567 echo \"test\";ˇ
23568 "});
23569 cx.update_editor(|editor, window, cx| {
23570 editor.newline(&Newline, window, cx);
23571 });
23572 cx.run_until_parked();
23573 cx.assert_editor_state(indoc! {"
23574 echo \"test\";
23575 ˇ
23576 "});
23577}
23578
23579fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23580 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23581 point..point
23582}
23583
23584#[track_caller]
23585fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23586 let (text, ranges) = marked_text_ranges(marked_text, true);
23587 assert_eq!(editor.text(cx), text);
23588 assert_eq!(
23589 editor.selections.ranges(cx),
23590 ranges,
23591 "Assert selections are {}",
23592 marked_text
23593 );
23594}
23595
23596pub fn handle_signature_help_request(
23597 cx: &mut EditorLspTestContext,
23598 mocked_response: lsp::SignatureHelp,
23599) -> impl Future<Output = ()> + use<> {
23600 let mut request =
23601 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23602 let mocked_response = mocked_response.clone();
23603 async move { Ok(Some(mocked_response)) }
23604 });
23605
23606 async move {
23607 request.next().await;
23608 }
23609}
23610
23611#[track_caller]
23612pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23613 cx.update_editor(|editor, _, _| {
23614 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23615 let entries = menu.entries.borrow();
23616 let entries = entries
23617 .iter()
23618 .map(|entry| entry.string.as_str())
23619 .collect::<Vec<_>>();
23620 assert_eq!(entries, expected);
23621 } else {
23622 panic!("Expected completions menu");
23623 }
23624 });
23625}
23626
23627/// Handle completion request passing a marked string specifying where the completion
23628/// should be triggered from using '|' character, what range should be replaced, and what completions
23629/// should be returned using '<' and '>' to delimit the range.
23630///
23631/// Also see `handle_completion_request_with_insert_and_replace`.
23632#[track_caller]
23633pub fn handle_completion_request(
23634 marked_string: &str,
23635 completions: Vec<&'static str>,
23636 is_incomplete: bool,
23637 counter: Arc<AtomicUsize>,
23638 cx: &mut EditorLspTestContext,
23639) -> impl Future<Output = ()> {
23640 let complete_from_marker: TextRangeMarker = '|'.into();
23641 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23642 let (_, mut marked_ranges) = marked_text_ranges_by(
23643 marked_string,
23644 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23645 );
23646
23647 let complete_from_position =
23648 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23649 let replace_range =
23650 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23651
23652 let mut request =
23653 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23654 let completions = completions.clone();
23655 counter.fetch_add(1, atomic::Ordering::Release);
23656 async move {
23657 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23658 assert_eq!(
23659 params.text_document_position.position,
23660 complete_from_position
23661 );
23662 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23663 is_incomplete,
23664 item_defaults: None,
23665 items: completions
23666 .iter()
23667 .map(|completion_text| lsp::CompletionItem {
23668 label: completion_text.to_string(),
23669 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23670 range: replace_range,
23671 new_text: completion_text.to_string(),
23672 })),
23673 ..Default::default()
23674 })
23675 .collect(),
23676 })))
23677 }
23678 });
23679
23680 async move {
23681 request.next().await;
23682 }
23683}
23684
23685/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23686/// given instead, which also contains an `insert` range.
23687///
23688/// This function uses markers to define ranges:
23689/// - `|` marks the cursor position
23690/// - `<>` marks the replace range
23691/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23692pub fn handle_completion_request_with_insert_and_replace(
23693 cx: &mut EditorLspTestContext,
23694 marked_string: &str,
23695 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23696 counter: Arc<AtomicUsize>,
23697) -> impl Future<Output = ()> {
23698 let complete_from_marker: TextRangeMarker = '|'.into();
23699 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23700 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23701
23702 let (_, mut marked_ranges) = marked_text_ranges_by(
23703 marked_string,
23704 vec![
23705 complete_from_marker.clone(),
23706 replace_range_marker.clone(),
23707 insert_range_marker.clone(),
23708 ],
23709 );
23710
23711 let complete_from_position =
23712 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23713 let replace_range =
23714 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23715
23716 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23717 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23718 _ => lsp::Range {
23719 start: replace_range.start,
23720 end: complete_from_position,
23721 },
23722 };
23723
23724 let mut request =
23725 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23726 let completions = completions.clone();
23727 counter.fetch_add(1, atomic::Ordering::Release);
23728 async move {
23729 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23730 assert_eq!(
23731 params.text_document_position.position, complete_from_position,
23732 "marker `|` position doesn't match",
23733 );
23734 Ok(Some(lsp::CompletionResponse::Array(
23735 completions
23736 .iter()
23737 .map(|(label, new_text)| lsp::CompletionItem {
23738 label: label.to_string(),
23739 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23740 lsp::InsertReplaceEdit {
23741 insert: insert_range,
23742 replace: replace_range,
23743 new_text: new_text.to_string(),
23744 },
23745 )),
23746 ..Default::default()
23747 })
23748 .collect(),
23749 )))
23750 }
23751 });
23752
23753 async move {
23754 request.next().await;
23755 }
23756}
23757
23758fn handle_resolve_completion_request(
23759 cx: &mut EditorLspTestContext,
23760 edits: Option<Vec<(&'static str, &'static str)>>,
23761) -> impl Future<Output = ()> {
23762 let edits = edits.map(|edits| {
23763 edits
23764 .iter()
23765 .map(|(marked_string, new_text)| {
23766 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23767 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23768 lsp::TextEdit::new(replace_range, new_text.to_string())
23769 })
23770 .collect::<Vec<_>>()
23771 });
23772
23773 let mut request =
23774 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23775 let edits = edits.clone();
23776 async move {
23777 Ok(lsp::CompletionItem {
23778 additional_text_edits: edits,
23779 ..Default::default()
23780 })
23781 }
23782 });
23783
23784 async move {
23785 request.next().await;
23786 }
23787}
23788
23789pub(crate) fn update_test_language_settings(
23790 cx: &mut TestAppContext,
23791 f: impl Fn(&mut AllLanguageSettingsContent),
23792) {
23793 cx.update(|cx| {
23794 SettingsStore::update_global(cx, |store, cx| {
23795 store.update_user_settings::<AllLanguageSettings>(cx, f);
23796 });
23797 });
23798}
23799
23800pub(crate) fn update_test_project_settings(
23801 cx: &mut TestAppContext,
23802 f: impl Fn(&mut ProjectSettings),
23803) {
23804 cx.update(|cx| {
23805 SettingsStore::update_global(cx, |store, cx| {
23806 store.update_user_settings::<ProjectSettings>(cx, f);
23807 });
23808 });
23809}
23810
23811pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23812 cx.update(|cx| {
23813 assets::Assets.load_test_fonts(cx);
23814 let store = SettingsStore::test(cx);
23815 cx.set_global(store);
23816 theme::init(theme::LoadThemes::JustBase, cx);
23817 release_channel::init(SemanticVersion::default(), cx);
23818 client::init_settings(cx);
23819 language::init(cx);
23820 Project::init_settings(cx);
23821 workspace::init_settings(cx);
23822 crate::init(cx);
23823 });
23824 zlog::init_test();
23825 update_test_language_settings(cx, f);
23826}
23827
23828#[track_caller]
23829fn assert_hunk_revert(
23830 not_reverted_text_with_selections: &str,
23831 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23832 expected_reverted_text_with_selections: &str,
23833 base_text: &str,
23834 cx: &mut EditorLspTestContext,
23835) {
23836 cx.set_state(not_reverted_text_with_selections);
23837 cx.set_head_text(base_text);
23838 cx.executor().run_until_parked();
23839
23840 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23841 let snapshot = editor.snapshot(window, cx);
23842 let reverted_hunk_statuses = snapshot
23843 .buffer_snapshot
23844 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23845 .map(|hunk| hunk.status().kind)
23846 .collect::<Vec<_>>();
23847
23848 editor.git_restore(&Default::default(), window, cx);
23849 reverted_hunk_statuses
23850 });
23851 cx.executor().run_until_parked();
23852 cx.assert_editor_state(expected_reverted_text_with_selections);
23853 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23854}
23855
23856#[gpui::test(iterations = 10)]
23857async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23858 init_test(cx, |_| {});
23859
23860 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23861 let counter = diagnostic_requests.clone();
23862
23863 let fs = FakeFs::new(cx.executor());
23864 fs.insert_tree(
23865 path!("/a"),
23866 json!({
23867 "first.rs": "fn main() { let a = 5; }",
23868 "second.rs": "// Test file",
23869 }),
23870 )
23871 .await;
23872
23873 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23874 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23875 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23876
23877 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23878 language_registry.add(rust_lang());
23879 let mut fake_servers = language_registry.register_fake_lsp(
23880 "Rust",
23881 FakeLspAdapter {
23882 capabilities: lsp::ServerCapabilities {
23883 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23884 lsp::DiagnosticOptions {
23885 identifier: None,
23886 inter_file_dependencies: true,
23887 workspace_diagnostics: true,
23888 work_done_progress_options: Default::default(),
23889 },
23890 )),
23891 ..Default::default()
23892 },
23893 ..Default::default()
23894 },
23895 );
23896
23897 let editor = workspace
23898 .update(cx, |workspace, window, cx| {
23899 workspace.open_abs_path(
23900 PathBuf::from(path!("/a/first.rs")),
23901 OpenOptions::default(),
23902 window,
23903 cx,
23904 )
23905 })
23906 .unwrap()
23907 .await
23908 .unwrap()
23909 .downcast::<Editor>()
23910 .unwrap();
23911 let fake_server = fake_servers.next().await.unwrap();
23912 let server_id = fake_server.server.server_id();
23913 let mut first_request = fake_server
23914 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23915 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23916 let result_id = Some(new_result_id.to_string());
23917 assert_eq!(
23918 params.text_document.uri,
23919 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23920 );
23921 async move {
23922 Ok(lsp::DocumentDiagnosticReportResult::Report(
23923 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23924 related_documents: None,
23925 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23926 items: Vec::new(),
23927 result_id,
23928 },
23929 }),
23930 ))
23931 }
23932 });
23933
23934 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23935 project.update(cx, |project, cx| {
23936 let buffer_id = editor
23937 .read(cx)
23938 .buffer()
23939 .read(cx)
23940 .as_singleton()
23941 .expect("created a singleton buffer")
23942 .read(cx)
23943 .remote_id();
23944 let buffer_result_id = project
23945 .lsp_store()
23946 .read(cx)
23947 .result_id(server_id, buffer_id, cx);
23948 assert_eq!(expected, buffer_result_id);
23949 });
23950 };
23951
23952 ensure_result_id(None, cx);
23953 cx.executor().advance_clock(Duration::from_millis(60));
23954 cx.executor().run_until_parked();
23955 assert_eq!(
23956 diagnostic_requests.load(atomic::Ordering::Acquire),
23957 1,
23958 "Opening file should trigger diagnostic request"
23959 );
23960 first_request
23961 .next()
23962 .await
23963 .expect("should have sent the first diagnostics pull request");
23964 ensure_result_id(Some("1".to_string()), cx);
23965
23966 // Editing should trigger diagnostics
23967 editor.update_in(cx, |editor, window, cx| {
23968 editor.handle_input("2", window, cx)
23969 });
23970 cx.executor().advance_clock(Duration::from_millis(60));
23971 cx.executor().run_until_parked();
23972 assert_eq!(
23973 diagnostic_requests.load(atomic::Ordering::Acquire),
23974 2,
23975 "Editing should trigger diagnostic request"
23976 );
23977 ensure_result_id(Some("2".to_string()), cx);
23978
23979 // Moving cursor should not trigger diagnostic request
23980 editor.update_in(cx, |editor, window, cx| {
23981 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23982 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23983 });
23984 });
23985 cx.executor().advance_clock(Duration::from_millis(60));
23986 cx.executor().run_until_parked();
23987 assert_eq!(
23988 diagnostic_requests.load(atomic::Ordering::Acquire),
23989 2,
23990 "Cursor movement should not trigger diagnostic request"
23991 );
23992 ensure_result_id(Some("2".to_string()), cx);
23993 // Multiple rapid edits should be debounced
23994 for _ in 0..5 {
23995 editor.update_in(cx, |editor, window, cx| {
23996 editor.handle_input("x", window, cx)
23997 });
23998 }
23999 cx.executor().advance_clock(Duration::from_millis(60));
24000 cx.executor().run_until_parked();
24001
24002 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
24003 assert!(
24004 final_requests <= 4,
24005 "Multiple rapid edits should be debounced (got {final_requests} requests)",
24006 );
24007 ensure_result_id(Some(final_requests.to_string()), cx);
24008}
24009
24010#[gpui::test]
24011async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
24012 // Regression test for issue #11671
24013 // Previously, adding a cursor after moving multiple cursors would reset
24014 // the cursor count instead of adding to the existing cursors.
24015 init_test(cx, |_| {});
24016 let mut cx = EditorTestContext::new(cx).await;
24017
24018 // Create a simple buffer with cursor at start
24019 cx.set_state(indoc! {"
24020 ˇaaaa
24021 bbbb
24022 cccc
24023 dddd
24024 eeee
24025 ffff
24026 gggg
24027 hhhh"});
24028
24029 // Add 2 cursors below (so we have 3 total)
24030 cx.update_editor(|editor, window, cx| {
24031 editor.add_selection_below(&Default::default(), window, cx);
24032 editor.add_selection_below(&Default::default(), window, cx);
24033 });
24034
24035 // Verify we have 3 cursors
24036 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24037 assert_eq!(
24038 initial_count, 3,
24039 "Should have 3 cursors after adding 2 below"
24040 );
24041
24042 // Move down one line
24043 cx.update_editor(|editor, window, cx| {
24044 editor.move_down(&MoveDown, window, cx);
24045 });
24046
24047 // Add another cursor below
24048 cx.update_editor(|editor, window, cx| {
24049 editor.add_selection_below(&Default::default(), window, cx);
24050 });
24051
24052 // Should now have 4 cursors (3 original + 1 new)
24053 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24054 assert_eq!(
24055 final_count, 4,
24056 "Should have 4 cursors after moving and adding another"
24057 );
24058}
24059
24060#[gpui::test(iterations = 10)]
24061async fn test_document_colors(cx: &mut TestAppContext) {
24062 let expected_color = Rgba {
24063 r: 0.33,
24064 g: 0.33,
24065 b: 0.33,
24066 a: 0.33,
24067 };
24068
24069 init_test(cx, |_| {});
24070
24071 let fs = FakeFs::new(cx.executor());
24072 fs.insert_tree(
24073 path!("/a"),
24074 json!({
24075 "first.rs": "fn main() { let a = 5; }",
24076 }),
24077 )
24078 .await;
24079
24080 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24081 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24082 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24083
24084 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24085 language_registry.add(rust_lang());
24086 let mut fake_servers = language_registry.register_fake_lsp(
24087 "Rust",
24088 FakeLspAdapter {
24089 capabilities: lsp::ServerCapabilities {
24090 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24091 ..lsp::ServerCapabilities::default()
24092 },
24093 name: "rust-analyzer",
24094 ..FakeLspAdapter::default()
24095 },
24096 );
24097 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24098 "Rust",
24099 FakeLspAdapter {
24100 capabilities: lsp::ServerCapabilities {
24101 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24102 ..lsp::ServerCapabilities::default()
24103 },
24104 name: "not-rust-analyzer",
24105 ..FakeLspAdapter::default()
24106 },
24107 );
24108
24109 let editor = workspace
24110 .update(cx, |workspace, window, cx| {
24111 workspace.open_abs_path(
24112 PathBuf::from(path!("/a/first.rs")),
24113 OpenOptions::default(),
24114 window,
24115 cx,
24116 )
24117 })
24118 .unwrap()
24119 .await
24120 .unwrap()
24121 .downcast::<Editor>()
24122 .unwrap();
24123 let fake_language_server = fake_servers.next().await.unwrap();
24124 let fake_language_server_without_capabilities =
24125 fake_servers_without_capabilities.next().await.unwrap();
24126 let requests_made = Arc::new(AtomicUsize::new(0));
24127 let closure_requests_made = Arc::clone(&requests_made);
24128 let mut color_request_handle = fake_language_server
24129 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
24130 let requests_made = Arc::clone(&closure_requests_made);
24131 async move {
24132 assert_eq!(
24133 params.text_document.uri,
24134 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
24135 );
24136 requests_made.fetch_add(1, atomic::Ordering::Release);
24137 Ok(vec![
24138 lsp::ColorInformation {
24139 range: lsp::Range {
24140 start: lsp::Position {
24141 line: 0,
24142 character: 0,
24143 },
24144 end: lsp::Position {
24145 line: 0,
24146 character: 1,
24147 },
24148 },
24149 color: lsp::Color {
24150 red: 0.33,
24151 green: 0.33,
24152 blue: 0.33,
24153 alpha: 0.33,
24154 },
24155 },
24156 lsp::ColorInformation {
24157 range: lsp::Range {
24158 start: lsp::Position {
24159 line: 0,
24160 character: 0,
24161 },
24162 end: lsp::Position {
24163 line: 0,
24164 character: 1,
24165 },
24166 },
24167 color: lsp::Color {
24168 red: 0.33,
24169 green: 0.33,
24170 blue: 0.33,
24171 alpha: 0.33,
24172 },
24173 },
24174 ])
24175 }
24176 });
24177
24178 let _handle = fake_language_server_without_capabilities
24179 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
24180 panic!("Should not be called");
24181 });
24182 cx.executor().advance_clock(Duration::from_millis(100));
24183 color_request_handle.next().await.unwrap();
24184 cx.run_until_parked();
24185 assert_eq!(
24186 1,
24187 requests_made.load(atomic::Ordering::Acquire),
24188 "Should query for colors once per editor open"
24189 );
24190 editor.update_in(cx, |editor, _, cx| {
24191 assert_eq!(
24192 vec![expected_color],
24193 extract_color_inlays(editor, cx),
24194 "Should have an initial inlay"
24195 );
24196 });
24197
24198 // opening another file in a split should not influence the LSP query counter
24199 workspace
24200 .update(cx, |workspace, window, cx| {
24201 assert_eq!(
24202 workspace.panes().len(),
24203 1,
24204 "Should have one pane with one editor"
24205 );
24206 workspace.move_item_to_pane_in_direction(
24207 &MoveItemToPaneInDirection {
24208 direction: SplitDirection::Right,
24209 focus: false,
24210 clone: true,
24211 },
24212 window,
24213 cx,
24214 );
24215 })
24216 .unwrap();
24217 cx.run_until_parked();
24218 workspace
24219 .update(cx, |workspace, _, cx| {
24220 let panes = workspace.panes();
24221 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
24222 for pane in panes {
24223 let editor = pane
24224 .read(cx)
24225 .active_item()
24226 .and_then(|item| item.downcast::<Editor>())
24227 .expect("Should have opened an editor in each split");
24228 let editor_file = editor
24229 .read(cx)
24230 .buffer()
24231 .read(cx)
24232 .as_singleton()
24233 .expect("test deals with singleton buffers")
24234 .read(cx)
24235 .file()
24236 .expect("test buffese should have a file")
24237 .path();
24238 assert_eq!(
24239 editor_file.as_ref(),
24240 Path::new("first.rs"),
24241 "Both editors should be opened for the same file"
24242 )
24243 }
24244 })
24245 .unwrap();
24246
24247 cx.executor().advance_clock(Duration::from_millis(500));
24248 let save = editor.update_in(cx, |editor, window, cx| {
24249 editor.move_to_end(&MoveToEnd, window, cx);
24250 editor.handle_input("dirty", window, cx);
24251 editor.save(
24252 SaveOptions {
24253 format: true,
24254 autosave: true,
24255 },
24256 project.clone(),
24257 window,
24258 cx,
24259 )
24260 });
24261 save.await.unwrap();
24262
24263 color_request_handle.next().await.unwrap();
24264 cx.run_until_parked();
24265 assert_eq!(
24266 3,
24267 requests_made.load(atomic::Ordering::Acquire),
24268 "Should query for colors once per save and once per formatting after save"
24269 );
24270
24271 drop(editor);
24272 let close = workspace
24273 .update(cx, |workspace, window, cx| {
24274 workspace.active_pane().update(cx, |pane, cx| {
24275 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24276 })
24277 })
24278 .unwrap();
24279 close.await.unwrap();
24280 let close = workspace
24281 .update(cx, |workspace, window, cx| {
24282 workspace.active_pane().update(cx, |pane, cx| {
24283 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24284 })
24285 })
24286 .unwrap();
24287 close.await.unwrap();
24288 assert_eq!(
24289 3,
24290 requests_made.load(atomic::Ordering::Acquire),
24291 "After saving and closing all editors, no extra requests should be made"
24292 );
24293 workspace
24294 .update(cx, |workspace, _, cx| {
24295 assert!(
24296 workspace.active_item(cx).is_none(),
24297 "Should close all editors"
24298 )
24299 })
24300 .unwrap();
24301
24302 workspace
24303 .update(cx, |workspace, window, cx| {
24304 workspace.active_pane().update(cx, |pane, cx| {
24305 pane.navigate_backward(&Default::default(), window, cx);
24306 })
24307 })
24308 .unwrap();
24309 cx.executor().advance_clock(Duration::from_millis(100));
24310 cx.run_until_parked();
24311 let editor = workspace
24312 .update(cx, |workspace, _, cx| {
24313 workspace
24314 .active_item(cx)
24315 .expect("Should have reopened the editor again after navigating back")
24316 .downcast::<Editor>()
24317 .expect("Should be an editor")
24318 })
24319 .unwrap();
24320 color_request_handle.next().await.unwrap();
24321 assert_eq!(
24322 3,
24323 requests_made.load(atomic::Ordering::Acquire),
24324 "Cache should be reused on buffer close and reopen"
24325 );
24326 editor.update(cx, |editor, cx| {
24327 assert_eq!(
24328 vec![expected_color],
24329 extract_color_inlays(editor, cx),
24330 "Should have an initial inlay"
24331 );
24332 });
24333}
24334
24335#[gpui::test]
24336async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24337 init_test(cx, |_| {});
24338 let (editor, cx) = cx.add_window_view(Editor::single_line);
24339 editor.update_in(cx, |editor, window, cx| {
24340 editor.set_text("oops\n\nwow\n", window, cx)
24341 });
24342 cx.run_until_parked();
24343 editor.update(cx, |editor, cx| {
24344 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24345 });
24346 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24347 cx.run_until_parked();
24348 editor.update(cx, |editor, cx| {
24349 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24350 });
24351}
24352
24353#[gpui::test]
24354async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
24355 init_test(cx, |_| {});
24356
24357 cx.update(|cx| {
24358 register_project_item::<Editor>(cx);
24359 });
24360
24361 let fs = FakeFs::new(cx.executor());
24362 fs.insert_tree("/root1", json!({})).await;
24363 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
24364 .await;
24365
24366 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
24367 let (workspace, cx) =
24368 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24369
24370 let worktree_id = project.update(cx, |project, cx| {
24371 project.worktrees(cx).next().unwrap().read(cx).id()
24372 });
24373
24374 let handle = workspace
24375 .update_in(cx, |workspace, window, cx| {
24376 let project_path = (worktree_id, "one.pdf");
24377 workspace.open_path(project_path, None, true, window, cx)
24378 })
24379 .await
24380 .unwrap();
24381
24382 assert_eq!(
24383 handle.to_any().entity_type(),
24384 TypeId::of::<InvalidBufferView>()
24385 );
24386}
24387
24388#[track_caller]
24389fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24390 editor
24391 .all_inlays(cx)
24392 .into_iter()
24393 .filter_map(|inlay| inlay.get_color())
24394 .map(Rgba::from)
24395 .collect()
24396}