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_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4408 init_test(cx, |_| {});
4409
4410 let mut cx = EditorTestContext::new(cx).await;
4411
4412 let js_language = Arc::new(Language::new(
4413 LanguageConfig {
4414 name: "JavaScript".into(),
4415 wrap_characters: Some(language::WrapCharactersConfig {
4416 start_prefix: "<".into(),
4417 start_suffix: ">".into(),
4418 end_prefix: "</".into(),
4419 end_suffix: ">".into(),
4420 }),
4421 ..LanguageConfig::default()
4422 },
4423 None,
4424 ));
4425
4426 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4427
4428 cx.set_state(indoc! {"
4429 «testˇ»
4430 "});
4431 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4432 cx.assert_editor_state(indoc! {"
4433 <«ˇ»>test</«ˇ»>
4434 "});
4435
4436 cx.set_state(indoc! {"
4437 «test
4438 testˇ»
4439 "});
4440 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4441 cx.assert_editor_state(indoc! {"
4442 <«ˇ»>test
4443 test</«ˇ»>
4444 "});
4445
4446 cx.set_state(indoc! {"
4447 teˇst
4448 "});
4449 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4450 cx.assert_editor_state(indoc! {"
4451 te<«ˇ»></«ˇ»>st
4452 "});
4453}
4454
4455#[gpui::test]
4456async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4457 init_test(cx, |_| {});
4458
4459 let mut cx = EditorTestContext::new(cx).await;
4460
4461 let js_language = Arc::new(Language::new(
4462 LanguageConfig {
4463 name: "JavaScript".into(),
4464 wrap_characters: Some(language::WrapCharactersConfig {
4465 start_prefix: "<".into(),
4466 start_suffix: ">".into(),
4467 end_prefix: "</".into(),
4468 end_suffix: ">".into(),
4469 }),
4470 ..LanguageConfig::default()
4471 },
4472 None,
4473 ));
4474
4475 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4476
4477 cx.set_state(indoc! {"
4478 «testˇ»
4479 «testˇ» «testˇ»
4480 «testˇ»
4481 "});
4482 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4483 cx.assert_editor_state(indoc! {"
4484 <«ˇ»>test</«ˇ»>
4485 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4486 <«ˇ»>test</«ˇ»>
4487 "});
4488
4489 cx.set_state(indoc! {"
4490 «test
4491 testˇ»
4492 «test
4493 testˇ»
4494 "});
4495 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4496 cx.assert_editor_state(indoc! {"
4497 <«ˇ»>test
4498 test</«ˇ»>
4499 <«ˇ»>test
4500 test</«ˇ»>
4501 "});
4502}
4503
4504#[gpui::test]
4505async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4506 init_test(cx, |_| {});
4507
4508 let mut cx = EditorTestContext::new(cx).await;
4509
4510 let plaintext_language = Arc::new(Language::new(
4511 LanguageConfig {
4512 name: "Plain Text".into(),
4513 ..LanguageConfig::default()
4514 },
4515 None,
4516 ));
4517
4518 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4519
4520 cx.set_state(indoc! {"
4521 «testˇ»
4522 "});
4523 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4524 cx.assert_editor_state(indoc! {"
4525 «testˇ»
4526 "});
4527}
4528
4529#[gpui::test]
4530async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4531 init_test(cx, |_| {});
4532
4533 let mut cx = EditorTestContext::new(cx).await;
4534
4535 // Manipulate with multiple selections on a single line
4536 cx.set_state(indoc! {"
4537 dd«dd
4538 cˇ»c«c
4539 bb
4540 aaaˇ»aa
4541 "});
4542 cx.update_editor(|e, window, cx| {
4543 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4544 });
4545 cx.assert_editor_state(indoc! {"
4546 «aaaaa
4547 bb
4548 ccc
4549 ddddˇ»
4550 "});
4551
4552 // Manipulate with multiple disjoin selections
4553 cx.set_state(indoc! {"
4554 5«
4555 4
4556 3
4557 2
4558 1ˇ»
4559
4560 dd«dd
4561 ccc
4562 bb
4563 aaaˇ»aa
4564 "});
4565 cx.update_editor(|e, window, cx| {
4566 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4567 });
4568 cx.assert_editor_state(indoc! {"
4569 «1
4570 2
4571 3
4572 4
4573 5ˇ»
4574
4575 «aaaaa
4576 bb
4577 ccc
4578 ddddˇ»
4579 "});
4580
4581 // Adding lines on each selection
4582 cx.set_state(indoc! {"
4583 2«
4584 1ˇ»
4585
4586 bb«bb
4587 aaaˇ»aa
4588 "});
4589 cx.update_editor(|e, window, cx| {
4590 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4591 });
4592 cx.assert_editor_state(indoc! {"
4593 «2
4594 1
4595 added lineˇ»
4596
4597 «bbbb
4598 aaaaa
4599 added lineˇ»
4600 "});
4601
4602 // Removing lines on each selection
4603 cx.set_state(indoc! {"
4604 2«
4605 1ˇ»
4606
4607 bb«bb
4608 aaaˇ»aa
4609 "});
4610 cx.update_editor(|e, window, cx| {
4611 e.manipulate_immutable_lines(window, cx, |lines| {
4612 lines.pop();
4613 })
4614 });
4615 cx.assert_editor_state(indoc! {"
4616 «2ˇ»
4617
4618 «bbbbˇ»
4619 "});
4620}
4621
4622#[gpui::test]
4623async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4624 init_test(cx, |settings| {
4625 settings.defaults.tab_size = NonZeroU32::new(3)
4626 });
4627
4628 let mut cx = EditorTestContext::new(cx).await;
4629
4630 // MULTI SELECTION
4631 // Ln.1 "«" tests empty lines
4632 // Ln.9 tests just leading whitespace
4633 cx.set_state(indoc! {"
4634 «
4635 abc // No indentationˇ»
4636 «\tabc // 1 tabˇ»
4637 \t\tabc « ˇ» // 2 tabs
4638 \t ab«c // Tab followed by space
4639 \tabc // Space followed by tab (3 spaces should be the result)
4640 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4641 abˇ»ˇc ˇ ˇ // Already space indented«
4642 \t
4643 \tabc\tdef // Only the leading tab is manipulatedˇ»
4644 "});
4645 cx.update_editor(|e, window, cx| {
4646 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4647 });
4648 cx.assert_editor_state(
4649 indoc! {"
4650 «
4651 abc // No indentation
4652 abc // 1 tab
4653 abc // 2 tabs
4654 abc // Tab followed by space
4655 abc // Space followed by tab (3 spaces should be the result)
4656 abc // Mixed indentation (tab conversion depends on the column)
4657 abc // Already space indented
4658 ·
4659 abc\tdef // Only the leading tab is manipulatedˇ»
4660 "}
4661 .replace("·", "")
4662 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4663 );
4664
4665 // Test on just a few lines, the others should remain unchanged
4666 // Only lines (3, 5, 10, 11) should change
4667 cx.set_state(
4668 indoc! {"
4669 ·
4670 abc // No indentation
4671 \tabcˇ // 1 tab
4672 \t\tabc // 2 tabs
4673 \t abcˇ // Tab followed by space
4674 \tabc // Space followed by tab (3 spaces should be the result)
4675 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4676 abc // Already space indented
4677 «\t
4678 \tabc\tdef // Only the leading tab is manipulatedˇ»
4679 "}
4680 .replace("·", "")
4681 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4682 );
4683 cx.update_editor(|e, window, cx| {
4684 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4685 });
4686 cx.assert_editor_state(
4687 indoc! {"
4688 ·
4689 abc // No indentation
4690 « abc // 1 tabˇ»
4691 \t\tabc // 2 tabs
4692 « abc // Tab followed by spaceˇ»
4693 \tabc // Space followed by tab (3 spaces should be the result)
4694 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4695 abc // Already space indented
4696 « ·
4697 abc\tdef // Only the leading tab is manipulatedˇ»
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.9 tests just leading whitespace
4706 cx.set_state(indoc! {"
4707 «
4708 abc // No indentation
4709 \tabc // 1 tab
4710 \t\tabc // 2 tabs
4711 \t abc // Tab followed by space
4712 \tabc // Space followed by tab (3 spaces should be the result)
4713 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4714 abc // Already space indented
4715 \t
4716 \tabc\tdef // Only the leading tab is manipulatedˇ»
4717 "});
4718 cx.update_editor(|e, window, cx| {
4719 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4720 });
4721 cx.assert_editor_state(
4722 indoc! {"
4723 «
4724 abc // No indentation
4725 abc // 1 tab
4726 abc // 2 tabs
4727 abc // Tab followed by space
4728 abc // Space followed by tab (3 spaces should be the result)
4729 abc // Mixed indentation (tab conversion depends on the column)
4730 abc // Already space indented
4731 ·
4732 abc\tdef // Only the leading tab is manipulatedˇ»
4733 "}
4734 .replace("·", "")
4735 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4736 );
4737}
4738
4739#[gpui::test]
4740async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4741 init_test(cx, |settings| {
4742 settings.defaults.tab_size = NonZeroU32::new(3)
4743 });
4744
4745 let mut cx = EditorTestContext::new(cx).await;
4746
4747 // MULTI SELECTION
4748 // Ln.1 "«" tests empty lines
4749 // Ln.11 tests just leading whitespace
4750 cx.set_state(indoc! {"
4751 «
4752 abˇ»ˇc // No indentation
4753 abc ˇ ˇ // 1 space (< 3 so dont convert)
4754 abc « // 2 spaces (< 3 so dont convert)
4755 abc // 3 spaces (convert)
4756 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4757 «\tˇ»\t«\tˇ»abc // Already tab indented
4758 «\t abc // Tab followed by space
4759 \tabc // Space followed by tab (should be consumed due to tab)
4760 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4761 \tˇ» «\t
4762 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4763 "});
4764 cx.update_editor(|e, window, cx| {
4765 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4766 });
4767 cx.assert_editor_state(indoc! {"
4768 «
4769 abc // No indentation
4770 abc // 1 space (< 3 so dont convert)
4771 abc // 2 spaces (< 3 so dont convert)
4772 \tabc // 3 spaces (convert)
4773 \t abc // 5 spaces (1 tab + 2 spaces)
4774 \t\t\tabc // Already tab indented
4775 \t abc // Tab followed by space
4776 \tabc // Space followed by tab (should be consumed due to tab)
4777 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4778 \t\t\t
4779 \tabc \t // Only the leading spaces should be convertedˇ»
4780 "});
4781
4782 // Test on just a few lines, the other should remain unchanged
4783 // Only lines (4, 8, 11, 12) should change
4784 cx.set_state(
4785 indoc! {"
4786 ·
4787 abc // No indentation
4788 abc // 1 space (< 3 so dont convert)
4789 abc // 2 spaces (< 3 so dont convert)
4790 « abc // 3 spaces (convert)ˇ»
4791 abc // 5 spaces (1 tab + 2 spaces)
4792 \t\t\tabc // Already tab indented
4793 \t abc // Tab followed by space
4794 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4795 \t\t \tabc // Mixed indentation
4796 \t \t \t \tabc // Mixed indentation
4797 \t \tˇ
4798 « abc \t // Only the leading spaces should be convertedˇ»
4799 "}
4800 .replace("·", "")
4801 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4802 );
4803 cx.update_editor(|e, window, cx| {
4804 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4805 });
4806 cx.assert_editor_state(
4807 indoc! {"
4808 ·
4809 abc // No indentation
4810 abc // 1 space (< 3 so dont convert)
4811 abc // 2 spaces (< 3 so dont convert)
4812 «\tabc // 3 spaces (convert)ˇ»
4813 abc // 5 spaces (1 tab + 2 spaces)
4814 \t\t\tabc // Already tab indented
4815 \t abc // Tab followed by space
4816 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4817 \t\t \tabc // Mixed indentation
4818 \t \t \t \tabc // Mixed indentation
4819 «\t\t\t
4820 \tabc \t // Only the leading spaces should be convertedˇ»
4821 "}
4822 .replace("·", "")
4823 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4824 );
4825
4826 // SINGLE SELECTION
4827 // Ln.1 "«" tests empty lines
4828 // Ln.11 tests just leading whitespace
4829 cx.set_state(indoc! {"
4830 «
4831 abc // No indentation
4832 abc // 1 space (< 3 so dont convert)
4833 abc // 2 spaces (< 3 so dont convert)
4834 abc // 3 spaces (convert)
4835 abc // 5 spaces (1 tab + 2 spaces)
4836 \t\t\tabc // Already tab indented
4837 \t abc // Tab followed by space
4838 \tabc // Space followed by tab (should be consumed due to tab)
4839 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4840 \t \t
4841 abc \t // Only the leading spaces should be convertedˇ»
4842 "});
4843 cx.update_editor(|e, window, cx| {
4844 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4845 });
4846 cx.assert_editor_state(indoc! {"
4847 «
4848 abc // No indentation
4849 abc // 1 space (< 3 so dont convert)
4850 abc // 2 spaces (< 3 so dont convert)
4851 \tabc // 3 spaces (convert)
4852 \t abc // 5 spaces (1 tab + 2 spaces)
4853 \t\t\tabc // Already tab indented
4854 \t abc // Tab followed by space
4855 \tabc // Space followed by tab (should be consumed due to tab)
4856 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4857 \t\t\t
4858 \tabc \t // Only the leading spaces should be convertedˇ»
4859 "});
4860}
4861
4862#[gpui::test]
4863async fn test_toggle_case(cx: &mut TestAppContext) {
4864 init_test(cx, |_| {});
4865
4866 let mut cx = EditorTestContext::new(cx).await;
4867
4868 // If all lower case -> upper case
4869 cx.set_state(indoc! {"
4870 «hello worldˇ»
4871 "});
4872 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4873 cx.assert_editor_state(indoc! {"
4874 «HELLO WORLDˇ»
4875 "});
4876
4877 // If all upper case -> lower case
4878 cx.set_state(indoc! {"
4879 «HELLO WORLDˇ»
4880 "});
4881 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4882 cx.assert_editor_state(indoc! {"
4883 «hello worldˇ»
4884 "});
4885
4886 // If any upper case characters are identified -> lower case
4887 // This matches JetBrains IDEs
4888 cx.set_state(indoc! {"
4889 «hEllo worldˇ»
4890 "});
4891 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4892 cx.assert_editor_state(indoc! {"
4893 «hello worldˇ»
4894 "});
4895}
4896
4897#[gpui::test]
4898async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
4899 init_test(cx, |_| {});
4900
4901 let mut cx = EditorTestContext::new(cx).await;
4902
4903 cx.set_state(indoc! {"
4904 «implement-windows-supportˇ»
4905 "});
4906 cx.update_editor(|e, window, cx| {
4907 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
4908 });
4909 cx.assert_editor_state(indoc! {"
4910 «Implement windows supportˇ»
4911 "});
4912}
4913
4914#[gpui::test]
4915async fn test_manipulate_text(cx: &mut TestAppContext) {
4916 init_test(cx, |_| {});
4917
4918 let mut cx = EditorTestContext::new(cx).await;
4919
4920 // Test convert_to_upper_case()
4921 cx.set_state(indoc! {"
4922 «hello worldˇ»
4923 "});
4924 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4925 cx.assert_editor_state(indoc! {"
4926 «HELLO WORLDˇ»
4927 "});
4928
4929 // Test convert_to_lower_case()
4930 cx.set_state(indoc! {"
4931 «HELLO WORLDˇ»
4932 "});
4933 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4934 cx.assert_editor_state(indoc! {"
4935 «hello worldˇ»
4936 "});
4937
4938 // Test multiple line, single selection case
4939 cx.set_state(indoc! {"
4940 «The quick brown
4941 fox jumps over
4942 the lazy dogˇ»
4943 "});
4944 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4945 cx.assert_editor_state(indoc! {"
4946 «The Quick Brown
4947 Fox Jumps Over
4948 The Lazy Dogˇ»
4949 "});
4950
4951 // Test multiple line, single selection case
4952 cx.set_state(indoc! {"
4953 «The quick brown
4954 fox jumps over
4955 the lazy dogˇ»
4956 "});
4957 cx.update_editor(|e, window, cx| {
4958 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4959 });
4960 cx.assert_editor_state(indoc! {"
4961 «TheQuickBrown
4962 FoxJumpsOver
4963 TheLazyDogˇ»
4964 "});
4965
4966 // From here on out, test more complex cases of manipulate_text()
4967
4968 // Test no selection case - should affect words cursors are in
4969 // Cursor at beginning, middle, and end of word
4970 cx.set_state(indoc! {"
4971 ˇhello big beauˇtiful worldˇ
4972 "});
4973 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4974 cx.assert_editor_state(indoc! {"
4975 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4976 "});
4977
4978 // Test multiple selections on a single line and across multiple lines
4979 cx.set_state(indoc! {"
4980 «Theˇ» quick «brown
4981 foxˇ» jumps «overˇ»
4982 the «lazyˇ» dog
4983 "});
4984 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4985 cx.assert_editor_state(indoc! {"
4986 «THEˇ» quick «BROWN
4987 FOXˇ» jumps «OVERˇ»
4988 the «LAZYˇ» dog
4989 "});
4990
4991 // Test case where text length grows
4992 cx.set_state(indoc! {"
4993 «tschüߡ»
4994 "});
4995 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4996 cx.assert_editor_state(indoc! {"
4997 «TSCHÜSSˇ»
4998 "});
4999
5000 // Test to make sure we don't crash when text shrinks
5001 cx.set_state(indoc! {"
5002 aaa_bbbˇ
5003 "});
5004 cx.update_editor(|e, window, cx| {
5005 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5006 });
5007 cx.assert_editor_state(indoc! {"
5008 «aaaBbbˇ»
5009 "});
5010
5011 // Test to make sure we all aware of the fact that each word can grow and shrink
5012 // Final selections should be aware of this fact
5013 cx.set_state(indoc! {"
5014 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5015 "});
5016 cx.update_editor(|e, window, cx| {
5017 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5018 });
5019 cx.assert_editor_state(indoc! {"
5020 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5021 "});
5022
5023 cx.set_state(indoc! {"
5024 «hElLo, WoRld!ˇ»
5025 "});
5026 cx.update_editor(|e, window, cx| {
5027 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5028 });
5029 cx.assert_editor_state(indoc! {"
5030 «HeLlO, wOrLD!ˇ»
5031 "});
5032}
5033
5034#[gpui::test]
5035fn test_duplicate_line(cx: &mut TestAppContext) {
5036 init_test(cx, |_| {});
5037
5038 let editor = cx.add_window(|window, cx| {
5039 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5040 build_editor(buffer, window, cx)
5041 });
5042 _ = editor.update(cx, |editor, window, cx| {
5043 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5044 s.select_display_ranges([
5045 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5046 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5047 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5048 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5049 ])
5050 });
5051 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5052 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5053 assert_eq!(
5054 editor.selections.display_ranges(cx),
5055 vec![
5056 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5057 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5058 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5059 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5060 ]
5061 );
5062 });
5063
5064 let editor = cx.add_window(|window, cx| {
5065 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5066 build_editor(buffer, window, cx)
5067 });
5068 _ = editor.update(cx, |editor, window, cx| {
5069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5070 s.select_display_ranges([
5071 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5072 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5073 ])
5074 });
5075 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5076 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5077 assert_eq!(
5078 editor.selections.display_ranges(cx),
5079 vec![
5080 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5081 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5082 ]
5083 );
5084 });
5085
5086 // With `move_upwards` the selections stay in place, except for
5087 // the lines inserted above them
5088 let editor = cx.add_window(|window, cx| {
5089 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5090 build_editor(buffer, window, cx)
5091 });
5092 _ = editor.update(cx, |editor, window, cx| {
5093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5094 s.select_display_ranges([
5095 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5096 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5097 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5098 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5099 ])
5100 });
5101 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5102 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5103 assert_eq!(
5104 editor.selections.display_ranges(cx),
5105 vec![
5106 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5107 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5108 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5109 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5110 ]
5111 );
5112 });
5113
5114 let editor = cx.add_window(|window, cx| {
5115 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5116 build_editor(buffer, window, cx)
5117 });
5118 _ = editor.update(cx, |editor, window, cx| {
5119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5120 s.select_display_ranges([
5121 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5122 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5123 ])
5124 });
5125 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5126 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5127 assert_eq!(
5128 editor.selections.display_ranges(cx),
5129 vec![
5130 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5131 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5132 ]
5133 );
5134 });
5135
5136 let editor = cx.add_window(|window, cx| {
5137 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5138 build_editor(buffer, window, cx)
5139 });
5140 _ = editor.update(cx, |editor, window, cx| {
5141 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5142 s.select_display_ranges([
5143 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5144 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5145 ])
5146 });
5147 editor.duplicate_selection(&DuplicateSelection, window, cx);
5148 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5149 assert_eq!(
5150 editor.selections.display_ranges(cx),
5151 vec![
5152 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5153 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5154 ]
5155 );
5156 });
5157}
5158
5159#[gpui::test]
5160fn test_move_line_up_down(cx: &mut TestAppContext) {
5161 init_test(cx, |_| {});
5162
5163 let editor = cx.add_window(|window, cx| {
5164 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5165 build_editor(buffer, window, cx)
5166 });
5167 _ = editor.update(cx, |editor, window, cx| {
5168 editor.fold_creases(
5169 vec![
5170 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5171 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5172 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5173 ],
5174 true,
5175 window,
5176 cx,
5177 );
5178 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5179 s.select_display_ranges([
5180 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5181 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5182 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5183 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5184 ])
5185 });
5186 assert_eq!(
5187 editor.display_text(cx),
5188 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5189 );
5190
5191 editor.move_line_up(&MoveLineUp, window, cx);
5192 assert_eq!(
5193 editor.display_text(cx),
5194 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5195 );
5196 assert_eq!(
5197 editor.selections.display_ranges(cx),
5198 vec![
5199 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5200 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5201 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5202 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5203 ]
5204 );
5205 });
5206
5207 _ = editor.update(cx, |editor, window, cx| {
5208 editor.move_line_down(&MoveLineDown, window, cx);
5209 assert_eq!(
5210 editor.display_text(cx),
5211 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5212 );
5213 assert_eq!(
5214 editor.selections.display_ranges(cx),
5215 vec![
5216 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5217 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5218 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5219 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5220 ]
5221 );
5222 });
5223
5224 _ = editor.update(cx, |editor, window, cx| {
5225 editor.move_line_down(&MoveLineDown, window, cx);
5226 assert_eq!(
5227 editor.display_text(cx),
5228 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5229 );
5230 assert_eq!(
5231 editor.selections.display_ranges(cx),
5232 vec![
5233 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5234 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5235 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5236 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5237 ]
5238 );
5239 });
5240
5241 _ = editor.update(cx, |editor, window, cx| {
5242 editor.move_line_up(&MoveLineUp, window, cx);
5243 assert_eq!(
5244 editor.display_text(cx),
5245 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5246 );
5247 assert_eq!(
5248 editor.selections.display_ranges(cx),
5249 vec![
5250 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5251 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5252 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5253 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5254 ]
5255 );
5256 });
5257}
5258
5259#[gpui::test]
5260fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5261 init_test(cx, |_| {});
5262 let editor = cx.add_window(|window, cx| {
5263 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5264 build_editor(buffer, window, cx)
5265 });
5266 _ = editor.update(cx, |editor, window, cx| {
5267 editor.fold_creases(
5268 vec![Crease::simple(
5269 Point::new(6, 4)..Point::new(7, 4),
5270 FoldPlaceholder::test(),
5271 )],
5272 true,
5273 window,
5274 cx,
5275 );
5276 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5277 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5278 });
5279 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5280 editor.move_line_up(&MoveLineUp, window, cx);
5281 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5282 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5283 });
5284}
5285
5286#[gpui::test]
5287fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5288 init_test(cx, |_| {});
5289
5290 let editor = cx.add_window(|window, cx| {
5291 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5292 build_editor(buffer, window, cx)
5293 });
5294 _ = editor.update(cx, |editor, window, cx| {
5295 let snapshot = editor.buffer.read(cx).snapshot(cx);
5296 editor.insert_blocks(
5297 [BlockProperties {
5298 style: BlockStyle::Fixed,
5299 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5300 height: Some(1),
5301 render: Arc::new(|_| div().into_any()),
5302 priority: 0,
5303 }],
5304 Some(Autoscroll::fit()),
5305 cx,
5306 );
5307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5308 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5309 });
5310 editor.move_line_down(&MoveLineDown, window, cx);
5311 });
5312}
5313
5314#[gpui::test]
5315async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5316 init_test(cx, |_| {});
5317
5318 let mut cx = EditorTestContext::new(cx).await;
5319 cx.set_state(
5320 &"
5321 ˇzero
5322 one
5323 two
5324 three
5325 four
5326 five
5327 "
5328 .unindent(),
5329 );
5330
5331 // Create a four-line block that replaces three lines of text.
5332 cx.update_editor(|editor, window, cx| {
5333 let snapshot = editor.snapshot(window, cx);
5334 let snapshot = &snapshot.buffer_snapshot;
5335 let placement = BlockPlacement::Replace(
5336 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5337 );
5338 editor.insert_blocks(
5339 [BlockProperties {
5340 placement,
5341 height: Some(4),
5342 style: BlockStyle::Sticky,
5343 render: Arc::new(|_| gpui::div().into_any_element()),
5344 priority: 0,
5345 }],
5346 None,
5347 cx,
5348 );
5349 });
5350
5351 // Move down so that the cursor touches the block.
5352 cx.update_editor(|editor, window, cx| {
5353 editor.move_down(&Default::default(), window, cx);
5354 });
5355 cx.assert_editor_state(
5356 &"
5357 zero
5358 «one
5359 two
5360 threeˇ»
5361 four
5362 five
5363 "
5364 .unindent(),
5365 );
5366
5367 // Move down past the block.
5368 cx.update_editor(|editor, window, cx| {
5369 editor.move_down(&Default::default(), window, cx);
5370 });
5371 cx.assert_editor_state(
5372 &"
5373 zero
5374 one
5375 two
5376 three
5377 ˇfour
5378 five
5379 "
5380 .unindent(),
5381 );
5382}
5383
5384#[gpui::test]
5385fn test_transpose(cx: &mut TestAppContext) {
5386 init_test(cx, |_| {});
5387
5388 _ = cx.add_window(|window, cx| {
5389 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5390 editor.set_style(EditorStyle::default(), window, cx);
5391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5392 s.select_ranges([1..1])
5393 });
5394 editor.transpose(&Default::default(), window, cx);
5395 assert_eq!(editor.text(cx), "bac");
5396 assert_eq!(editor.selections.ranges(cx), [2..2]);
5397
5398 editor.transpose(&Default::default(), window, cx);
5399 assert_eq!(editor.text(cx), "bca");
5400 assert_eq!(editor.selections.ranges(cx), [3..3]);
5401
5402 editor.transpose(&Default::default(), window, cx);
5403 assert_eq!(editor.text(cx), "bac");
5404 assert_eq!(editor.selections.ranges(cx), [3..3]);
5405
5406 editor
5407 });
5408
5409 _ = cx.add_window(|window, cx| {
5410 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5411 editor.set_style(EditorStyle::default(), window, cx);
5412 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5413 s.select_ranges([3..3])
5414 });
5415 editor.transpose(&Default::default(), window, cx);
5416 assert_eq!(editor.text(cx), "acb\nde");
5417 assert_eq!(editor.selections.ranges(cx), [3..3]);
5418
5419 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5420 s.select_ranges([4..4])
5421 });
5422 editor.transpose(&Default::default(), window, cx);
5423 assert_eq!(editor.text(cx), "acbd\ne");
5424 assert_eq!(editor.selections.ranges(cx), [5..5]);
5425
5426 editor.transpose(&Default::default(), window, cx);
5427 assert_eq!(editor.text(cx), "acbde\n");
5428 assert_eq!(editor.selections.ranges(cx), [6..6]);
5429
5430 editor.transpose(&Default::default(), window, cx);
5431 assert_eq!(editor.text(cx), "acbd\ne");
5432 assert_eq!(editor.selections.ranges(cx), [6..6]);
5433
5434 editor
5435 });
5436
5437 _ = cx.add_window(|window, cx| {
5438 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5439 editor.set_style(EditorStyle::default(), window, cx);
5440 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5441 s.select_ranges([1..1, 2..2, 4..4])
5442 });
5443 editor.transpose(&Default::default(), window, cx);
5444 assert_eq!(editor.text(cx), "bacd\ne");
5445 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5446
5447 editor.transpose(&Default::default(), window, cx);
5448 assert_eq!(editor.text(cx), "bcade\n");
5449 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5450
5451 editor.transpose(&Default::default(), window, cx);
5452 assert_eq!(editor.text(cx), "bcda\ne");
5453 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5454
5455 editor.transpose(&Default::default(), window, cx);
5456 assert_eq!(editor.text(cx), "bcade\n");
5457 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5458
5459 editor.transpose(&Default::default(), window, cx);
5460 assert_eq!(editor.text(cx), "bcaed\n");
5461 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5462
5463 editor
5464 });
5465
5466 _ = cx.add_window(|window, cx| {
5467 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5468 editor.set_style(EditorStyle::default(), window, cx);
5469 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5470 s.select_ranges([4..4])
5471 });
5472 editor.transpose(&Default::default(), window, cx);
5473 assert_eq!(editor.text(cx), "🏀🍐✋");
5474 assert_eq!(editor.selections.ranges(cx), [8..8]);
5475
5476 editor.transpose(&Default::default(), window, cx);
5477 assert_eq!(editor.text(cx), "🏀✋🍐");
5478 assert_eq!(editor.selections.ranges(cx), [11..11]);
5479
5480 editor.transpose(&Default::default(), window, cx);
5481 assert_eq!(editor.text(cx), "🏀🍐✋");
5482 assert_eq!(editor.selections.ranges(cx), [11..11]);
5483
5484 editor
5485 });
5486}
5487
5488#[gpui::test]
5489async fn test_rewrap(cx: &mut TestAppContext) {
5490 init_test(cx, |settings| {
5491 settings.languages.0.extend([
5492 (
5493 "Markdown".into(),
5494 LanguageSettingsContent {
5495 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5496 preferred_line_length: Some(40),
5497 ..Default::default()
5498 },
5499 ),
5500 (
5501 "Plain Text".into(),
5502 LanguageSettingsContent {
5503 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5504 preferred_line_length: Some(40),
5505 ..Default::default()
5506 },
5507 ),
5508 (
5509 "C++".into(),
5510 LanguageSettingsContent {
5511 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5512 preferred_line_length: Some(40),
5513 ..Default::default()
5514 },
5515 ),
5516 (
5517 "Python".into(),
5518 LanguageSettingsContent {
5519 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5520 preferred_line_length: Some(40),
5521 ..Default::default()
5522 },
5523 ),
5524 (
5525 "Rust".into(),
5526 LanguageSettingsContent {
5527 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5528 preferred_line_length: Some(40),
5529 ..Default::default()
5530 },
5531 ),
5532 ])
5533 });
5534
5535 let mut cx = EditorTestContext::new(cx).await;
5536
5537 let cpp_language = Arc::new(Language::new(
5538 LanguageConfig {
5539 name: "C++".into(),
5540 line_comments: vec!["// ".into()],
5541 ..LanguageConfig::default()
5542 },
5543 None,
5544 ));
5545 let python_language = Arc::new(Language::new(
5546 LanguageConfig {
5547 name: "Python".into(),
5548 line_comments: vec!["# ".into()],
5549 ..LanguageConfig::default()
5550 },
5551 None,
5552 ));
5553 let markdown_language = Arc::new(Language::new(
5554 LanguageConfig {
5555 name: "Markdown".into(),
5556 rewrap_prefixes: vec![
5557 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5558 regex::Regex::new("[-*+]\\s+").unwrap(),
5559 ],
5560 ..LanguageConfig::default()
5561 },
5562 None,
5563 ));
5564 let rust_language = Arc::new(
5565 Language::new(
5566 LanguageConfig {
5567 name: "Rust".into(),
5568 line_comments: vec!["// ".into(), "/// ".into()],
5569 ..LanguageConfig::default()
5570 },
5571 Some(tree_sitter_rust::LANGUAGE.into()),
5572 )
5573 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5574 .unwrap(),
5575 );
5576
5577 let plaintext_language = Arc::new(Language::new(
5578 LanguageConfig {
5579 name: "Plain Text".into(),
5580 ..LanguageConfig::default()
5581 },
5582 None,
5583 ));
5584
5585 // Test basic rewrapping of a long line with a cursor
5586 assert_rewrap(
5587 indoc! {"
5588 // ˇThis is a long comment that needs to be wrapped.
5589 "},
5590 indoc! {"
5591 // ˇThis is a long comment that needs to
5592 // be wrapped.
5593 "},
5594 cpp_language.clone(),
5595 &mut cx,
5596 );
5597
5598 // Test rewrapping a full selection
5599 assert_rewrap(
5600 indoc! {"
5601 «// This selected long comment needs to be wrapped.ˇ»"
5602 },
5603 indoc! {"
5604 «// This selected long comment needs to
5605 // be wrapped.ˇ»"
5606 },
5607 cpp_language.clone(),
5608 &mut cx,
5609 );
5610
5611 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5612 assert_rewrap(
5613 indoc! {"
5614 // ˇThis is the first line.
5615 // Thisˇ is the second line.
5616 // This is the thirdˇ line, all part of one paragraph.
5617 "},
5618 indoc! {"
5619 // ˇThis is the first line. Thisˇ is the
5620 // second line. This is the thirdˇ line,
5621 // all part of one paragraph.
5622 "},
5623 cpp_language.clone(),
5624 &mut cx,
5625 );
5626
5627 // Test multiple cursors in different paragraphs trigger separate rewraps
5628 assert_rewrap(
5629 indoc! {"
5630 // ˇThis is the first paragraph, first line.
5631 // ˇThis is the first paragraph, second line.
5632
5633 // ˇThis is the second paragraph, first line.
5634 // ˇThis is the second paragraph, second line.
5635 "},
5636 indoc! {"
5637 // ˇThis is the first paragraph, first
5638 // line. ˇThis is the first paragraph,
5639 // second line.
5640
5641 // ˇThis is the second paragraph, first
5642 // line. ˇThis is the second paragraph,
5643 // second line.
5644 "},
5645 cpp_language.clone(),
5646 &mut cx,
5647 );
5648
5649 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5650 assert_rewrap(
5651 indoc! {"
5652 «// A regular long long comment to be wrapped.
5653 /// A documentation long comment to be wrapped.ˇ»
5654 "},
5655 indoc! {"
5656 «// A regular long long comment to be
5657 // wrapped.
5658 /// A documentation long comment to be
5659 /// wrapped.ˇ»
5660 "},
5661 rust_language.clone(),
5662 &mut cx,
5663 );
5664
5665 // Test that change in indentation level trigger seperate rewraps
5666 assert_rewrap(
5667 indoc! {"
5668 fn foo() {
5669 «// This is a long comment at the base indent.
5670 // This is a long comment at the next indent.ˇ»
5671 }
5672 "},
5673 indoc! {"
5674 fn foo() {
5675 «// This is a long comment at the
5676 // base indent.
5677 // This is a long comment at the
5678 // next indent.ˇ»
5679 }
5680 "},
5681 rust_language.clone(),
5682 &mut cx,
5683 );
5684
5685 // Test that different comment prefix characters (e.g., '#') are handled correctly
5686 assert_rewrap(
5687 indoc! {"
5688 # ˇThis is a long comment using a pound sign.
5689 "},
5690 indoc! {"
5691 # ˇThis is a long comment using a pound
5692 # sign.
5693 "},
5694 python_language,
5695 &mut cx,
5696 );
5697
5698 // Test rewrapping only affects comments, not code even when selected
5699 assert_rewrap(
5700 indoc! {"
5701 «/// This doc comment is long and should be wrapped.
5702 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5703 "},
5704 indoc! {"
5705 «/// This doc comment is long and should
5706 /// be wrapped.
5707 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5708 "},
5709 rust_language.clone(),
5710 &mut cx,
5711 );
5712
5713 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5714 assert_rewrap(
5715 indoc! {"
5716 # Header
5717
5718 A long long long line of markdown text to wrap.ˇ
5719 "},
5720 indoc! {"
5721 # Header
5722
5723 A long long long line of markdown text
5724 to wrap.ˇ
5725 "},
5726 markdown_language.clone(),
5727 &mut cx,
5728 );
5729
5730 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5731 assert_rewrap(
5732 indoc! {"
5733 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5734 2. This is a numbered list item that is very long and needs to be wrapped properly.
5735 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5736 "},
5737 indoc! {"
5738 «1. This is a numbered list item that is
5739 very long and needs to be wrapped
5740 properly.
5741 2. This is a numbered list item that is
5742 very long and needs to be wrapped
5743 properly.
5744 - This is an unordered list item that is
5745 also very long and should not merge
5746 with the numbered item.ˇ»
5747 "},
5748 markdown_language.clone(),
5749 &mut cx,
5750 );
5751
5752 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5753 assert_rewrap(
5754 indoc! {"
5755 «1. This is a numbered list item that is
5756 very long and needs to be wrapped
5757 properly.
5758 2. This is a numbered list item that is
5759 very long and needs to be wrapped
5760 properly.
5761 - This is an unordered list item that is
5762 also very long and should not merge with
5763 the numbered item.ˇ»
5764 "},
5765 indoc! {"
5766 «1. This is a numbered list item that is
5767 very long and needs to be wrapped
5768 properly.
5769 2. This is a numbered list item that is
5770 very long and needs to be wrapped
5771 properly.
5772 - This is an unordered list item that is
5773 also very long and should not merge
5774 with the numbered item.ˇ»
5775 "},
5776 markdown_language.clone(),
5777 &mut cx,
5778 );
5779
5780 // Test that rewrapping maintain indents even when they already exists.
5781 assert_rewrap(
5782 indoc! {"
5783 «1. This is a numbered list
5784 item that is very long and needs to be wrapped properly.
5785 2. This is a numbered list
5786 item that is very long and needs to be wrapped properly.
5787 - This is an unordered list item that is also very long and
5788 should not merge with the numbered item.ˇ»
5789 "},
5790 indoc! {"
5791 «1. This is a numbered list item that is
5792 very long and needs to be wrapped
5793 properly.
5794 2. This is a numbered list item that is
5795 very long and needs to be wrapped
5796 properly.
5797 - This is an unordered list item that is
5798 also very long and should not merge
5799 with the numbered item.ˇ»
5800 "},
5801 markdown_language,
5802 &mut cx,
5803 );
5804
5805 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5806 assert_rewrap(
5807 indoc! {"
5808 ˇThis is a very long line of plain text that will be wrapped.
5809 "},
5810 indoc! {"
5811 ˇThis is a very long line of plain text
5812 that will be wrapped.
5813 "},
5814 plaintext_language.clone(),
5815 &mut cx,
5816 );
5817
5818 // Test that non-commented code acts as a paragraph boundary within a selection
5819 assert_rewrap(
5820 indoc! {"
5821 «// This is the first long comment block to be wrapped.
5822 fn my_func(a: u32);
5823 // This is the second long comment block to be wrapped.ˇ»
5824 "},
5825 indoc! {"
5826 «// This is the first long comment block
5827 // to be wrapped.
5828 fn my_func(a: u32);
5829 // This is the second long comment block
5830 // to be wrapped.ˇ»
5831 "},
5832 rust_language,
5833 &mut cx,
5834 );
5835
5836 // Test rewrapping multiple selections, including ones with blank lines or tabs
5837 assert_rewrap(
5838 indoc! {"
5839 «ˇThis is a very long line that will be wrapped.
5840
5841 This is another paragraph in the same selection.»
5842
5843 «\tThis is a very long indented line that will be wrapped.ˇ»
5844 "},
5845 indoc! {"
5846 «ˇThis is a very long line that will be
5847 wrapped.
5848
5849 This is another paragraph in the same
5850 selection.»
5851
5852 «\tThis is a very long indented line
5853 \tthat will be wrapped.ˇ»
5854 "},
5855 plaintext_language,
5856 &mut cx,
5857 );
5858
5859 // Test that an empty comment line acts as a paragraph boundary
5860 assert_rewrap(
5861 indoc! {"
5862 // ˇThis is a long comment that will be wrapped.
5863 //
5864 // And this is another long comment that will also be wrapped.ˇ
5865 "},
5866 indoc! {"
5867 // ˇThis is a long comment that will be
5868 // wrapped.
5869 //
5870 // And this is another long comment that
5871 // will also be wrapped.ˇ
5872 "},
5873 cpp_language,
5874 &mut cx,
5875 );
5876
5877 #[track_caller]
5878 fn assert_rewrap(
5879 unwrapped_text: &str,
5880 wrapped_text: &str,
5881 language: Arc<Language>,
5882 cx: &mut EditorTestContext,
5883 ) {
5884 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5885 cx.set_state(unwrapped_text);
5886 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5887 cx.assert_editor_state(wrapped_text);
5888 }
5889}
5890
5891#[gpui::test]
5892async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
5893 init_test(cx, |settings| {
5894 settings.languages.0.extend([(
5895 "Rust".into(),
5896 LanguageSettingsContent {
5897 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5898 preferred_line_length: Some(40),
5899 ..Default::default()
5900 },
5901 )])
5902 });
5903
5904 let mut cx = EditorTestContext::new(cx).await;
5905
5906 let rust_lang = Arc::new(
5907 Language::new(
5908 LanguageConfig {
5909 name: "Rust".into(),
5910 line_comments: vec!["// ".into()],
5911 block_comment: Some(BlockCommentConfig {
5912 start: "/*".into(),
5913 end: "*/".into(),
5914 prefix: "* ".into(),
5915 tab_size: 1,
5916 }),
5917 documentation_comment: Some(BlockCommentConfig {
5918 start: "/**".into(),
5919 end: "*/".into(),
5920 prefix: "* ".into(),
5921 tab_size: 1,
5922 }),
5923
5924 ..LanguageConfig::default()
5925 },
5926 Some(tree_sitter_rust::LANGUAGE.into()),
5927 )
5928 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
5929 .unwrap(),
5930 );
5931
5932 // regular block comment
5933 assert_rewrap(
5934 indoc! {"
5935 /*
5936 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5937 */
5938 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
5939 "},
5940 indoc! {"
5941 /*
5942 *ˇ Lorem ipsum dolor sit amet,
5943 * consectetur adipiscing elit.
5944 */
5945 /*
5946 *ˇ Lorem ipsum dolor sit amet,
5947 * consectetur adipiscing elit.
5948 */
5949 "},
5950 rust_lang.clone(),
5951 &mut cx,
5952 );
5953
5954 // indent is respected
5955 assert_rewrap(
5956 indoc! {"
5957 {}
5958 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
5959 "},
5960 indoc! {"
5961 {}
5962 /*
5963 *ˇ Lorem ipsum dolor sit amet,
5964 * consectetur adipiscing elit.
5965 */
5966 "},
5967 rust_lang.clone(),
5968 &mut cx,
5969 );
5970
5971 // short block comments with inline delimiters
5972 assert_rewrap(
5973 indoc! {"
5974 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
5975 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5976 */
5977 /*
5978 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
5979 "},
5980 indoc! {"
5981 /*
5982 *ˇ Lorem ipsum dolor sit amet,
5983 * consectetur adipiscing elit.
5984 */
5985 /*
5986 *ˇ Lorem ipsum dolor sit amet,
5987 * consectetur adipiscing elit.
5988 */
5989 /*
5990 *ˇ Lorem ipsum dolor sit amet,
5991 * consectetur adipiscing elit.
5992 */
5993 "},
5994 rust_lang.clone(),
5995 &mut cx,
5996 );
5997
5998 // multiline block comment with inline start/end delimiters
5999 assert_rewrap(
6000 indoc! {"
6001 /*ˇ Lorem ipsum dolor sit amet,
6002 * consectetur adipiscing elit. */
6003 "},
6004 indoc! {"
6005 /*
6006 *ˇ Lorem ipsum dolor sit amet,
6007 * consectetur adipiscing elit.
6008 */
6009 "},
6010 rust_lang.clone(),
6011 &mut cx,
6012 );
6013
6014 // block comment rewrap still respects paragraph bounds
6015 assert_rewrap(
6016 indoc! {"
6017 /*
6018 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6019 *
6020 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6021 */
6022 "},
6023 indoc! {"
6024 /*
6025 *ˇ Lorem ipsum dolor sit amet,
6026 * consectetur adipiscing elit.
6027 *
6028 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6029 */
6030 "},
6031 rust_lang.clone(),
6032 &mut cx,
6033 );
6034
6035 // documentation comments
6036 assert_rewrap(
6037 indoc! {"
6038 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6039 /**
6040 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6041 */
6042 "},
6043 indoc! {"
6044 /**
6045 *ˇ Lorem ipsum dolor sit amet,
6046 * consectetur adipiscing elit.
6047 */
6048 /**
6049 *ˇ Lorem ipsum dolor sit amet,
6050 * consectetur adipiscing elit.
6051 */
6052 "},
6053 rust_lang.clone(),
6054 &mut cx,
6055 );
6056
6057 // different, adjacent comments
6058 assert_rewrap(
6059 indoc! {"
6060 /**
6061 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6062 */
6063 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6064 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6065 "},
6066 indoc! {"
6067 /**
6068 *ˇ Lorem ipsum dolor sit amet,
6069 * consectetur adipiscing elit.
6070 */
6071 /*
6072 *ˇ Lorem ipsum dolor sit amet,
6073 * consectetur adipiscing elit.
6074 */
6075 //ˇ Lorem ipsum dolor sit amet,
6076 // consectetur adipiscing elit.
6077 "},
6078 rust_lang.clone(),
6079 &mut cx,
6080 );
6081
6082 // selection w/ single short block comment
6083 assert_rewrap(
6084 indoc! {"
6085 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6086 "},
6087 indoc! {"
6088 «/*
6089 * Lorem ipsum dolor sit amet,
6090 * consectetur adipiscing elit.
6091 */ˇ»
6092 "},
6093 rust_lang.clone(),
6094 &mut cx,
6095 );
6096
6097 // rewrapping a single comment w/ abutting comments
6098 assert_rewrap(
6099 indoc! {"
6100 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6101 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6102 "},
6103 indoc! {"
6104 /*
6105 * ˇLorem ipsum dolor sit amet,
6106 * consectetur adipiscing elit.
6107 */
6108 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6109 "},
6110 rust_lang.clone(),
6111 &mut cx,
6112 );
6113
6114 // selection w/ non-abutting short block comments
6115 assert_rewrap(
6116 indoc! {"
6117 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6118
6119 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6120 "},
6121 indoc! {"
6122 «/*
6123 * Lorem ipsum dolor sit amet,
6124 * consectetur adipiscing elit.
6125 */
6126
6127 /*
6128 * Lorem ipsum dolor sit amet,
6129 * consectetur adipiscing elit.
6130 */ˇ»
6131 "},
6132 rust_lang.clone(),
6133 &mut cx,
6134 );
6135
6136 // selection of multiline block comments
6137 assert_rewrap(
6138 indoc! {"
6139 «/* Lorem ipsum dolor sit amet,
6140 * consectetur adipiscing elit. */ˇ»
6141 "},
6142 indoc! {"
6143 «/*
6144 * Lorem ipsum dolor sit amet,
6145 * consectetur adipiscing elit.
6146 */ˇ»
6147 "},
6148 rust_lang.clone(),
6149 &mut cx,
6150 );
6151
6152 // partial selection of multiline block comments
6153 assert_rewrap(
6154 indoc! {"
6155 «/* Lorem ipsum dolor sit amet,ˇ»
6156 * consectetur adipiscing elit. */
6157 /* Lorem ipsum dolor sit amet,
6158 «* consectetur adipiscing elit. */ˇ»
6159 "},
6160 indoc! {"
6161 «/*
6162 * Lorem ipsum dolor sit amet,ˇ»
6163 * consectetur adipiscing elit. */
6164 /* Lorem ipsum dolor sit amet,
6165 «* consectetur adipiscing elit.
6166 */ˇ»
6167 "},
6168 rust_lang.clone(),
6169 &mut cx,
6170 );
6171
6172 // selection w/ abutting short block comments
6173 // TODO: should not be combined; should rewrap as 2 comments
6174 assert_rewrap(
6175 indoc! {"
6176 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6177 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6178 "},
6179 // desired behavior:
6180 // indoc! {"
6181 // «/*
6182 // * Lorem ipsum dolor sit amet,
6183 // * consectetur adipiscing elit.
6184 // */
6185 // /*
6186 // * Lorem ipsum dolor sit amet,
6187 // * consectetur adipiscing elit.
6188 // */ˇ»
6189 // "},
6190 // actual behaviour:
6191 indoc! {"
6192 «/*
6193 * Lorem ipsum dolor sit amet,
6194 * consectetur adipiscing elit. Lorem
6195 * ipsum dolor sit amet, consectetur
6196 * adipiscing elit.
6197 */ˇ»
6198 "},
6199 rust_lang.clone(),
6200 &mut cx,
6201 );
6202
6203 // TODO: same as above, but with delimiters on separate line
6204 // assert_rewrap(
6205 // indoc! {"
6206 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6207 // */
6208 // /*
6209 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6210 // "},
6211 // // desired:
6212 // // indoc! {"
6213 // // «/*
6214 // // * Lorem ipsum dolor sit amet,
6215 // // * consectetur adipiscing elit.
6216 // // */
6217 // // /*
6218 // // * Lorem ipsum dolor sit amet,
6219 // // * consectetur adipiscing elit.
6220 // // */ˇ»
6221 // // "},
6222 // // actual: (but with trailing w/s on the empty lines)
6223 // indoc! {"
6224 // «/*
6225 // * Lorem ipsum dolor sit amet,
6226 // * consectetur adipiscing elit.
6227 // *
6228 // */
6229 // /*
6230 // *
6231 // * Lorem ipsum dolor sit amet,
6232 // * consectetur adipiscing elit.
6233 // */ˇ»
6234 // "},
6235 // rust_lang.clone(),
6236 // &mut cx,
6237 // );
6238
6239 // TODO these are unhandled edge cases; not correct, just documenting known issues
6240 assert_rewrap(
6241 indoc! {"
6242 /*
6243 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6244 */
6245 /*
6246 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6247 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6248 "},
6249 // desired:
6250 // indoc! {"
6251 // /*
6252 // *ˇ Lorem ipsum dolor sit amet,
6253 // * consectetur adipiscing elit.
6254 // */
6255 // /*
6256 // *ˇ Lorem ipsum dolor sit amet,
6257 // * consectetur adipiscing elit.
6258 // */
6259 // /*
6260 // *ˇ Lorem ipsum dolor sit amet
6261 // */ /* consectetur adipiscing elit. */
6262 // "},
6263 // actual:
6264 indoc! {"
6265 /*
6266 //ˇ Lorem ipsum dolor sit amet,
6267 // consectetur adipiscing elit.
6268 */
6269 /*
6270 * //ˇ Lorem ipsum dolor sit amet,
6271 * consectetur adipiscing elit.
6272 */
6273 /*
6274 *ˇ Lorem ipsum dolor sit amet */ /*
6275 * consectetur adipiscing elit.
6276 */
6277 "},
6278 rust_lang,
6279 &mut cx,
6280 );
6281
6282 #[track_caller]
6283 fn assert_rewrap(
6284 unwrapped_text: &str,
6285 wrapped_text: &str,
6286 language: Arc<Language>,
6287 cx: &mut EditorTestContext,
6288 ) {
6289 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6290 cx.set_state(unwrapped_text);
6291 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6292 cx.assert_editor_state(wrapped_text);
6293 }
6294}
6295
6296#[gpui::test]
6297async fn test_hard_wrap(cx: &mut TestAppContext) {
6298 init_test(cx, |_| {});
6299 let mut cx = EditorTestContext::new(cx).await;
6300
6301 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6302 cx.update_editor(|editor, _, cx| {
6303 editor.set_hard_wrap(Some(14), cx);
6304 });
6305
6306 cx.set_state(indoc!(
6307 "
6308 one two three ˇ
6309 "
6310 ));
6311 cx.simulate_input("four");
6312 cx.run_until_parked();
6313
6314 cx.assert_editor_state(indoc!(
6315 "
6316 one two three
6317 fourˇ
6318 "
6319 ));
6320
6321 cx.update_editor(|editor, window, cx| {
6322 editor.newline(&Default::default(), window, cx);
6323 });
6324 cx.run_until_parked();
6325 cx.assert_editor_state(indoc!(
6326 "
6327 one two three
6328 four
6329 ˇ
6330 "
6331 ));
6332
6333 cx.simulate_input("five");
6334 cx.run_until_parked();
6335 cx.assert_editor_state(indoc!(
6336 "
6337 one two three
6338 four
6339 fiveˇ
6340 "
6341 ));
6342
6343 cx.update_editor(|editor, window, cx| {
6344 editor.newline(&Default::default(), window, cx);
6345 });
6346 cx.run_until_parked();
6347 cx.simulate_input("# ");
6348 cx.run_until_parked();
6349 cx.assert_editor_state(indoc!(
6350 "
6351 one two three
6352 four
6353 five
6354 # ˇ
6355 "
6356 ));
6357
6358 cx.update_editor(|editor, window, cx| {
6359 editor.newline(&Default::default(), window, cx);
6360 });
6361 cx.run_until_parked();
6362 cx.assert_editor_state(indoc!(
6363 "
6364 one two three
6365 four
6366 five
6367 #\x20
6368 #ˇ
6369 "
6370 ));
6371
6372 cx.simulate_input(" 6");
6373 cx.run_until_parked();
6374 cx.assert_editor_state(indoc!(
6375 "
6376 one two three
6377 four
6378 five
6379 #
6380 # 6ˇ
6381 "
6382 ));
6383}
6384
6385#[gpui::test]
6386async fn test_clipboard(cx: &mut TestAppContext) {
6387 init_test(cx, |_| {});
6388
6389 let mut cx = EditorTestContext::new(cx).await;
6390
6391 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6392 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6393 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6394
6395 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6396 cx.set_state("two ˇfour ˇsix ˇ");
6397 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6398 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6399
6400 // Paste again but with only two cursors. Since the number of cursors doesn't
6401 // match the number of slices in the clipboard, the entire clipboard text
6402 // is pasted at each cursor.
6403 cx.set_state("ˇtwo one✅ four three six five ˇ");
6404 cx.update_editor(|e, window, cx| {
6405 e.handle_input("( ", window, cx);
6406 e.paste(&Paste, window, cx);
6407 e.handle_input(") ", window, cx);
6408 });
6409 cx.assert_editor_state(
6410 &([
6411 "( one✅ ",
6412 "three ",
6413 "five ) ˇtwo one✅ four three six five ( one✅ ",
6414 "three ",
6415 "five ) ˇ",
6416 ]
6417 .join("\n")),
6418 );
6419
6420 // Cut with three selections, one of which is full-line.
6421 cx.set_state(indoc! {"
6422 1«2ˇ»3
6423 4ˇ567
6424 «8ˇ»9"});
6425 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6426 cx.assert_editor_state(indoc! {"
6427 1ˇ3
6428 ˇ9"});
6429
6430 // Paste with three selections, noticing how the copied selection that was full-line
6431 // gets inserted before the second cursor.
6432 cx.set_state(indoc! {"
6433 1ˇ3
6434 9ˇ
6435 «oˇ»ne"});
6436 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6437 cx.assert_editor_state(indoc! {"
6438 12ˇ3
6439 4567
6440 9ˇ
6441 8ˇne"});
6442
6443 // Copy with a single cursor only, which writes the whole line into the clipboard.
6444 cx.set_state(indoc! {"
6445 The quick brown
6446 fox juˇmps over
6447 the lazy dog"});
6448 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6449 assert_eq!(
6450 cx.read_from_clipboard()
6451 .and_then(|item| item.text().as_deref().map(str::to_string)),
6452 Some("fox jumps over\n".to_string())
6453 );
6454
6455 // Paste with three selections, noticing how the copied full-line selection is inserted
6456 // before the empty selections but replaces the selection that is non-empty.
6457 cx.set_state(indoc! {"
6458 Tˇhe quick brown
6459 «foˇ»x jumps over
6460 tˇhe lazy dog"});
6461 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6462 cx.assert_editor_state(indoc! {"
6463 fox jumps over
6464 Tˇhe quick brown
6465 fox jumps over
6466 ˇx jumps over
6467 fox jumps over
6468 tˇhe lazy dog"});
6469}
6470
6471#[gpui::test]
6472async fn test_copy_trim(cx: &mut TestAppContext) {
6473 init_test(cx, |_| {});
6474
6475 let mut cx = EditorTestContext::new(cx).await;
6476 cx.set_state(
6477 r#" «for selection in selections.iter() {
6478 let mut start = selection.start;
6479 let mut end = selection.end;
6480 let is_entire_line = selection.is_empty();
6481 if is_entire_line {
6482 start = Point::new(start.row, 0);ˇ»
6483 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6484 }
6485 "#,
6486 );
6487 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6488 assert_eq!(
6489 cx.read_from_clipboard()
6490 .and_then(|item| item.text().as_deref().map(str::to_string)),
6491 Some(
6492 "for selection in selections.iter() {
6493 let mut start = selection.start;
6494 let mut end = selection.end;
6495 let is_entire_line = selection.is_empty();
6496 if is_entire_line {
6497 start = Point::new(start.row, 0);"
6498 .to_string()
6499 ),
6500 "Regular copying preserves all indentation selected",
6501 );
6502 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6503 assert_eq!(
6504 cx.read_from_clipboard()
6505 .and_then(|item| item.text().as_deref().map(str::to_string)),
6506 Some(
6507 "for selection in selections.iter() {
6508let mut start = selection.start;
6509let mut end = selection.end;
6510let is_entire_line = selection.is_empty();
6511if is_entire_line {
6512 start = Point::new(start.row, 0);"
6513 .to_string()
6514 ),
6515 "Copying with stripping should strip all leading whitespaces"
6516 );
6517
6518 cx.set_state(
6519 r#" « for selection in selections.iter() {
6520 let mut start = selection.start;
6521 let mut end = selection.end;
6522 let is_entire_line = selection.is_empty();
6523 if is_entire_line {
6524 start = Point::new(start.row, 0);ˇ»
6525 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6526 }
6527 "#,
6528 );
6529 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6530 assert_eq!(
6531 cx.read_from_clipboard()
6532 .and_then(|item| item.text().as_deref().map(str::to_string)),
6533 Some(
6534 " for selection in selections.iter() {
6535 let mut start = selection.start;
6536 let mut end = selection.end;
6537 let is_entire_line = selection.is_empty();
6538 if is_entire_line {
6539 start = Point::new(start.row, 0);"
6540 .to_string()
6541 ),
6542 "Regular copying preserves all indentation selected",
6543 );
6544 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6545 assert_eq!(
6546 cx.read_from_clipboard()
6547 .and_then(|item| item.text().as_deref().map(str::to_string)),
6548 Some(
6549 "for selection in selections.iter() {
6550let mut start = selection.start;
6551let mut end = selection.end;
6552let is_entire_line = selection.is_empty();
6553if is_entire_line {
6554 start = Point::new(start.row, 0);"
6555 .to_string()
6556 ),
6557 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6558 );
6559
6560 cx.set_state(
6561 r#" «ˇ for selection in selections.iter() {
6562 let mut start = selection.start;
6563 let mut end = selection.end;
6564 let is_entire_line = selection.is_empty();
6565 if is_entire_line {
6566 start = Point::new(start.row, 0);»
6567 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6568 }
6569 "#,
6570 );
6571 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6572 assert_eq!(
6573 cx.read_from_clipboard()
6574 .and_then(|item| item.text().as_deref().map(str::to_string)),
6575 Some(
6576 " for selection in selections.iter() {
6577 let mut start = selection.start;
6578 let mut end = selection.end;
6579 let is_entire_line = selection.is_empty();
6580 if is_entire_line {
6581 start = Point::new(start.row, 0);"
6582 .to_string()
6583 ),
6584 "Regular copying for reverse selection works the same",
6585 );
6586 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6587 assert_eq!(
6588 cx.read_from_clipboard()
6589 .and_then(|item| item.text().as_deref().map(str::to_string)),
6590 Some(
6591 "for selection in selections.iter() {
6592let mut start = selection.start;
6593let mut end = selection.end;
6594let is_entire_line = selection.is_empty();
6595if is_entire_line {
6596 start = Point::new(start.row, 0);"
6597 .to_string()
6598 ),
6599 "Copying with stripping for reverse selection works the same"
6600 );
6601
6602 cx.set_state(
6603 r#" for selection «in selections.iter() {
6604 let mut start = selection.start;
6605 let mut end = selection.end;
6606 let is_entire_line = selection.is_empty();
6607 if is_entire_line {
6608 start = Point::new(start.row, 0);ˇ»
6609 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6610 }
6611 "#,
6612 );
6613 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6614 assert_eq!(
6615 cx.read_from_clipboard()
6616 .and_then(|item| item.text().as_deref().map(str::to_string)),
6617 Some(
6618 "in selections.iter() {
6619 let mut start = selection.start;
6620 let mut end = selection.end;
6621 let is_entire_line = selection.is_empty();
6622 if is_entire_line {
6623 start = Point::new(start.row, 0);"
6624 .to_string()
6625 ),
6626 "When selecting past the indent, the copying works as usual",
6627 );
6628 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6629 assert_eq!(
6630 cx.read_from_clipboard()
6631 .and_then(|item| item.text().as_deref().map(str::to_string)),
6632 Some(
6633 "in selections.iter() {
6634 let mut start = selection.start;
6635 let mut end = selection.end;
6636 let is_entire_line = selection.is_empty();
6637 if is_entire_line {
6638 start = Point::new(start.row, 0);"
6639 .to_string()
6640 ),
6641 "When selecting past the indent, nothing is trimmed"
6642 );
6643
6644 cx.set_state(
6645 r#" «for selection in selections.iter() {
6646 let mut start = selection.start;
6647
6648 let mut end = selection.end;
6649 let is_entire_line = selection.is_empty();
6650 if is_entire_line {
6651 start = Point::new(start.row, 0);
6652ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6653 }
6654 "#,
6655 );
6656 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6657 assert_eq!(
6658 cx.read_from_clipboard()
6659 .and_then(|item| item.text().as_deref().map(str::to_string)),
6660 Some(
6661 "for selection in selections.iter() {
6662let mut start = selection.start;
6663
6664let mut end = selection.end;
6665let is_entire_line = selection.is_empty();
6666if is_entire_line {
6667 start = Point::new(start.row, 0);
6668"
6669 .to_string()
6670 ),
6671 "Copying with stripping should ignore empty lines"
6672 );
6673}
6674
6675#[gpui::test]
6676async fn test_paste_multiline(cx: &mut TestAppContext) {
6677 init_test(cx, |_| {});
6678
6679 let mut cx = EditorTestContext::new(cx).await;
6680 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6681
6682 // Cut an indented block, without the leading whitespace.
6683 cx.set_state(indoc! {"
6684 const a: B = (
6685 c(),
6686 «d(
6687 e,
6688 f
6689 )ˇ»
6690 );
6691 "});
6692 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6693 cx.assert_editor_state(indoc! {"
6694 const a: B = (
6695 c(),
6696 ˇ
6697 );
6698 "});
6699
6700 // Paste it at the same position.
6701 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6702 cx.assert_editor_state(indoc! {"
6703 const a: B = (
6704 c(),
6705 d(
6706 e,
6707 f
6708 )ˇ
6709 );
6710 "});
6711
6712 // Paste it at a line with a lower indent level.
6713 cx.set_state(indoc! {"
6714 ˇ
6715 const a: B = (
6716 c(),
6717 );
6718 "});
6719 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6720 cx.assert_editor_state(indoc! {"
6721 d(
6722 e,
6723 f
6724 )ˇ
6725 const a: B = (
6726 c(),
6727 );
6728 "});
6729
6730 // Cut an indented block, with the leading whitespace.
6731 cx.set_state(indoc! {"
6732 const a: B = (
6733 c(),
6734 « d(
6735 e,
6736 f
6737 )
6738 ˇ»);
6739 "});
6740 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6741 cx.assert_editor_state(indoc! {"
6742 const a: B = (
6743 c(),
6744 ˇ);
6745 "});
6746
6747 // Paste it at the same position.
6748 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6749 cx.assert_editor_state(indoc! {"
6750 const a: B = (
6751 c(),
6752 d(
6753 e,
6754 f
6755 )
6756 ˇ);
6757 "});
6758
6759 // Paste it at a line with a higher indent level.
6760 cx.set_state(indoc! {"
6761 const a: B = (
6762 c(),
6763 d(
6764 e,
6765 fˇ
6766 )
6767 );
6768 "});
6769 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6770 cx.assert_editor_state(indoc! {"
6771 const a: B = (
6772 c(),
6773 d(
6774 e,
6775 f d(
6776 e,
6777 f
6778 )
6779 ˇ
6780 )
6781 );
6782 "});
6783
6784 // Copy an indented block, starting mid-line
6785 cx.set_state(indoc! {"
6786 const a: B = (
6787 c(),
6788 somethin«g(
6789 e,
6790 f
6791 )ˇ»
6792 );
6793 "});
6794 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6795
6796 // Paste it on a line with a lower indent level
6797 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6798 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6799 cx.assert_editor_state(indoc! {"
6800 const a: B = (
6801 c(),
6802 something(
6803 e,
6804 f
6805 )
6806 );
6807 g(
6808 e,
6809 f
6810 )ˇ"});
6811}
6812
6813#[gpui::test]
6814async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6815 init_test(cx, |_| {});
6816
6817 cx.write_to_clipboard(ClipboardItem::new_string(
6818 " d(\n e\n );\n".into(),
6819 ));
6820
6821 let mut cx = EditorTestContext::new(cx).await;
6822 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6823
6824 cx.set_state(indoc! {"
6825 fn a() {
6826 b();
6827 if c() {
6828 ˇ
6829 }
6830 }
6831 "});
6832
6833 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6834 cx.assert_editor_state(indoc! {"
6835 fn a() {
6836 b();
6837 if c() {
6838 d(
6839 e
6840 );
6841 ˇ
6842 }
6843 }
6844 "});
6845
6846 cx.set_state(indoc! {"
6847 fn a() {
6848 b();
6849 ˇ
6850 }
6851 "});
6852
6853 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6854 cx.assert_editor_state(indoc! {"
6855 fn a() {
6856 b();
6857 d(
6858 e
6859 );
6860 ˇ
6861 }
6862 "});
6863}
6864
6865#[gpui::test]
6866fn test_select_all(cx: &mut TestAppContext) {
6867 init_test(cx, |_| {});
6868
6869 let editor = cx.add_window(|window, cx| {
6870 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6871 build_editor(buffer, window, cx)
6872 });
6873 _ = editor.update(cx, |editor, window, cx| {
6874 editor.select_all(&SelectAll, window, cx);
6875 assert_eq!(
6876 editor.selections.display_ranges(cx),
6877 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6878 );
6879 });
6880}
6881
6882#[gpui::test]
6883fn test_select_line(cx: &mut TestAppContext) {
6884 init_test(cx, |_| {});
6885
6886 let editor = cx.add_window(|window, cx| {
6887 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6888 build_editor(buffer, window, cx)
6889 });
6890 _ = editor.update(cx, |editor, window, cx| {
6891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6892 s.select_display_ranges([
6893 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6894 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6895 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6896 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6897 ])
6898 });
6899 editor.select_line(&SelectLine, window, cx);
6900 assert_eq!(
6901 editor.selections.display_ranges(cx),
6902 vec![
6903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6904 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6905 ]
6906 );
6907 });
6908
6909 _ = editor.update(cx, |editor, window, cx| {
6910 editor.select_line(&SelectLine, window, cx);
6911 assert_eq!(
6912 editor.selections.display_ranges(cx),
6913 vec![
6914 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6915 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6916 ]
6917 );
6918 });
6919
6920 _ = editor.update(cx, |editor, window, cx| {
6921 editor.select_line(&SelectLine, window, cx);
6922 assert_eq!(
6923 editor.selections.display_ranges(cx),
6924 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6925 );
6926 });
6927}
6928
6929#[gpui::test]
6930async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6931 init_test(cx, |_| {});
6932 let mut cx = EditorTestContext::new(cx).await;
6933
6934 #[track_caller]
6935 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6936 cx.set_state(initial_state);
6937 cx.update_editor(|e, window, cx| {
6938 e.split_selection_into_lines(&Default::default(), window, cx)
6939 });
6940 cx.assert_editor_state(expected_state);
6941 }
6942
6943 // Selection starts and ends at the middle of lines, left-to-right
6944 test(
6945 &mut cx,
6946 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6947 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6948 );
6949 // Same thing, right-to-left
6950 test(
6951 &mut cx,
6952 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6953 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6954 );
6955
6956 // Whole buffer, left-to-right, last line *doesn't* end with newline
6957 test(
6958 &mut cx,
6959 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6960 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6961 );
6962 // Same thing, right-to-left
6963 test(
6964 &mut cx,
6965 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6966 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6967 );
6968
6969 // Whole buffer, left-to-right, last line ends with newline
6970 test(
6971 &mut cx,
6972 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6973 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6974 );
6975 // Same thing, right-to-left
6976 test(
6977 &mut cx,
6978 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6979 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6980 );
6981
6982 // Starts at the end of a line, ends at the start of another
6983 test(
6984 &mut cx,
6985 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6986 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6987 );
6988}
6989
6990#[gpui::test]
6991async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6992 init_test(cx, |_| {});
6993
6994 let editor = cx.add_window(|window, cx| {
6995 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6996 build_editor(buffer, window, cx)
6997 });
6998
6999 // setup
7000 _ = editor.update(cx, |editor, window, cx| {
7001 editor.fold_creases(
7002 vec![
7003 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7004 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7005 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7006 ],
7007 true,
7008 window,
7009 cx,
7010 );
7011 assert_eq!(
7012 editor.display_text(cx),
7013 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7014 );
7015 });
7016
7017 _ = editor.update(cx, |editor, window, cx| {
7018 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7019 s.select_display_ranges([
7020 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7021 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7022 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7023 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7024 ])
7025 });
7026 editor.split_selection_into_lines(&Default::default(), window, cx);
7027 assert_eq!(
7028 editor.display_text(cx),
7029 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7030 );
7031 });
7032 EditorTestContext::for_editor(editor, cx)
7033 .await
7034 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7035
7036 _ = editor.update(cx, |editor, window, cx| {
7037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7038 s.select_display_ranges([
7039 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7040 ])
7041 });
7042 editor.split_selection_into_lines(&Default::default(), window, cx);
7043 assert_eq!(
7044 editor.display_text(cx),
7045 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7046 );
7047 assert_eq!(
7048 editor.selections.display_ranges(cx),
7049 [
7050 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7051 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7052 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7053 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7054 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7055 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7056 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7057 ]
7058 );
7059 });
7060 EditorTestContext::for_editor(editor, cx)
7061 .await
7062 .assert_editor_state(
7063 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7064 );
7065}
7066
7067#[gpui::test]
7068async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7069 init_test(cx, |_| {});
7070
7071 let mut cx = EditorTestContext::new(cx).await;
7072
7073 cx.set_state(indoc!(
7074 r#"abc
7075 defˇghi
7076
7077 jk
7078 nlmo
7079 "#
7080 ));
7081
7082 cx.update_editor(|editor, window, cx| {
7083 editor.add_selection_above(&Default::default(), window, cx);
7084 });
7085
7086 cx.assert_editor_state(indoc!(
7087 r#"abcˇ
7088 defˇghi
7089
7090 jk
7091 nlmo
7092 "#
7093 ));
7094
7095 cx.update_editor(|editor, window, cx| {
7096 editor.add_selection_above(&Default::default(), window, cx);
7097 });
7098
7099 cx.assert_editor_state(indoc!(
7100 r#"abcˇ
7101 defˇghi
7102
7103 jk
7104 nlmo
7105 "#
7106 ));
7107
7108 cx.update_editor(|editor, window, cx| {
7109 editor.add_selection_below(&Default::default(), window, cx);
7110 });
7111
7112 cx.assert_editor_state(indoc!(
7113 r#"abc
7114 defˇghi
7115
7116 jk
7117 nlmo
7118 "#
7119 ));
7120
7121 cx.update_editor(|editor, window, cx| {
7122 editor.undo_selection(&Default::default(), window, cx);
7123 });
7124
7125 cx.assert_editor_state(indoc!(
7126 r#"abcˇ
7127 defˇghi
7128
7129 jk
7130 nlmo
7131 "#
7132 ));
7133
7134 cx.update_editor(|editor, window, cx| {
7135 editor.redo_selection(&Default::default(), window, cx);
7136 });
7137
7138 cx.assert_editor_state(indoc!(
7139 r#"abc
7140 defˇghi
7141
7142 jk
7143 nlmo
7144 "#
7145 ));
7146
7147 cx.update_editor(|editor, window, cx| {
7148 editor.add_selection_below(&Default::default(), window, cx);
7149 });
7150
7151 cx.assert_editor_state(indoc!(
7152 r#"abc
7153 defˇghi
7154 ˇ
7155 jk
7156 nlmo
7157 "#
7158 ));
7159
7160 cx.update_editor(|editor, window, cx| {
7161 editor.add_selection_below(&Default::default(), window, cx);
7162 });
7163
7164 cx.assert_editor_state(indoc!(
7165 r#"abc
7166 defˇghi
7167 ˇ
7168 jkˇ
7169 nlmo
7170 "#
7171 ));
7172
7173 cx.update_editor(|editor, window, cx| {
7174 editor.add_selection_below(&Default::default(), window, cx);
7175 });
7176
7177 cx.assert_editor_state(indoc!(
7178 r#"abc
7179 defˇghi
7180 ˇ
7181 jkˇ
7182 nlmˇo
7183 "#
7184 ));
7185
7186 cx.update_editor(|editor, window, cx| {
7187 editor.add_selection_below(&Default::default(), window, cx);
7188 });
7189
7190 cx.assert_editor_state(indoc!(
7191 r#"abc
7192 defˇghi
7193 ˇ
7194 jkˇ
7195 nlmˇo
7196 ˇ"#
7197 ));
7198
7199 // change selections
7200 cx.set_state(indoc!(
7201 r#"abc
7202 def«ˇg»hi
7203
7204 jk
7205 nlmo
7206 "#
7207 ));
7208
7209 cx.update_editor(|editor, window, cx| {
7210 editor.add_selection_below(&Default::default(), window, cx);
7211 });
7212
7213 cx.assert_editor_state(indoc!(
7214 r#"abc
7215 def«ˇg»hi
7216
7217 jk
7218 nlm«ˇo»
7219 "#
7220 ));
7221
7222 cx.update_editor(|editor, window, cx| {
7223 editor.add_selection_below(&Default::default(), window, cx);
7224 });
7225
7226 cx.assert_editor_state(indoc!(
7227 r#"abc
7228 def«ˇg»hi
7229
7230 jk
7231 nlm«ˇo»
7232 "#
7233 ));
7234
7235 cx.update_editor(|editor, window, cx| {
7236 editor.add_selection_above(&Default::default(), window, cx);
7237 });
7238
7239 cx.assert_editor_state(indoc!(
7240 r#"abc
7241 def«ˇg»hi
7242
7243 jk
7244 nlmo
7245 "#
7246 ));
7247
7248 cx.update_editor(|editor, window, cx| {
7249 editor.add_selection_above(&Default::default(), window, cx);
7250 });
7251
7252 cx.assert_editor_state(indoc!(
7253 r#"abc
7254 def«ˇg»hi
7255
7256 jk
7257 nlmo
7258 "#
7259 ));
7260
7261 // Change selections again
7262 cx.set_state(indoc!(
7263 r#"a«bc
7264 defgˇ»hi
7265
7266 jk
7267 nlmo
7268 "#
7269 ));
7270
7271 cx.update_editor(|editor, window, cx| {
7272 editor.add_selection_below(&Default::default(), window, cx);
7273 });
7274
7275 cx.assert_editor_state(indoc!(
7276 r#"a«bcˇ»
7277 d«efgˇ»hi
7278
7279 j«kˇ»
7280 nlmo
7281 "#
7282 ));
7283
7284 cx.update_editor(|editor, window, cx| {
7285 editor.add_selection_below(&Default::default(), window, cx);
7286 });
7287 cx.assert_editor_state(indoc!(
7288 r#"a«bcˇ»
7289 d«efgˇ»hi
7290
7291 j«kˇ»
7292 n«lmoˇ»
7293 "#
7294 ));
7295 cx.update_editor(|editor, window, cx| {
7296 editor.add_selection_above(&Default::default(), window, cx);
7297 });
7298
7299 cx.assert_editor_state(indoc!(
7300 r#"a«bcˇ»
7301 d«efgˇ»hi
7302
7303 j«kˇ»
7304 nlmo
7305 "#
7306 ));
7307
7308 // Change selections again
7309 cx.set_state(indoc!(
7310 r#"abc
7311 d«ˇefghi
7312
7313 jk
7314 nlm»o
7315 "#
7316 ));
7317
7318 cx.update_editor(|editor, window, cx| {
7319 editor.add_selection_above(&Default::default(), window, cx);
7320 });
7321
7322 cx.assert_editor_state(indoc!(
7323 r#"a«ˇbc»
7324 d«ˇef»ghi
7325
7326 j«ˇk»
7327 n«ˇlm»o
7328 "#
7329 ));
7330
7331 cx.update_editor(|editor, window, cx| {
7332 editor.add_selection_below(&Default::default(), window, cx);
7333 });
7334
7335 cx.assert_editor_state(indoc!(
7336 r#"abc
7337 d«ˇef»ghi
7338
7339 j«ˇk»
7340 n«ˇlm»o
7341 "#
7342 ));
7343}
7344
7345#[gpui::test]
7346async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7347 init_test(cx, |_| {});
7348 let mut cx = EditorTestContext::new(cx).await;
7349
7350 cx.set_state(indoc!(
7351 r#"line onˇe
7352 liˇne two
7353 line three
7354 line four"#
7355 ));
7356
7357 cx.update_editor(|editor, window, cx| {
7358 editor.add_selection_below(&Default::default(), window, cx);
7359 });
7360
7361 // test multiple cursors expand in the same direction
7362 cx.assert_editor_state(indoc!(
7363 r#"line onˇe
7364 liˇne twˇo
7365 liˇne three
7366 line four"#
7367 ));
7368
7369 cx.update_editor(|editor, window, cx| {
7370 editor.add_selection_below(&Default::default(), window, cx);
7371 });
7372
7373 cx.update_editor(|editor, window, cx| {
7374 editor.add_selection_below(&Default::default(), window, cx);
7375 });
7376
7377 // test multiple cursors expand below overflow
7378 cx.assert_editor_state(indoc!(
7379 r#"line onˇe
7380 liˇne twˇo
7381 liˇne thˇree
7382 liˇne foˇur"#
7383 ));
7384
7385 cx.update_editor(|editor, window, cx| {
7386 editor.add_selection_above(&Default::default(), window, cx);
7387 });
7388
7389 // test multiple cursors retrieves back correctly
7390 cx.assert_editor_state(indoc!(
7391 r#"line onˇe
7392 liˇne twˇo
7393 liˇne thˇree
7394 line four"#
7395 ));
7396
7397 cx.update_editor(|editor, window, cx| {
7398 editor.add_selection_above(&Default::default(), window, cx);
7399 });
7400
7401 cx.update_editor(|editor, window, cx| {
7402 editor.add_selection_above(&Default::default(), window, cx);
7403 });
7404
7405 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7406 cx.assert_editor_state(indoc!(
7407 r#"liˇne onˇe
7408 liˇne two
7409 line three
7410 line four"#
7411 ));
7412
7413 cx.update_editor(|editor, window, cx| {
7414 editor.undo_selection(&Default::default(), window, cx);
7415 });
7416
7417 // test undo
7418 cx.assert_editor_state(indoc!(
7419 r#"line onˇe
7420 liˇne twˇo
7421 line three
7422 line four"#
7423 ));
7424
7425 cx.update_editor(|editor, window, cx| {
7426 editor.redo_selection(&Default::default(), window, cx);
7427 });
7428
7429 // test redo
7430 cx.assert_editor_state(indoc!(
7431 r#"liˇne onˇe
7432 liˇne two
7433 line three
7434 line four"#
7435 ));
7436
7437 cx.set_state(indoc!(
7438 r#"abcd
7439 ef«ghˇ»
7440 ijkl
7441 «mˇ»nop"#
7442 ));
7443
7444 cx.update_editor(|editor, window, cx| {
7445 editor.add_selection_above(&Default::default(), window, cx);
7446 });
7447
7448 // test multiple selections expand in the same direction
7449 cx.assert_editor_state(indoc!(
7450 r#"ab«cdˇ»
7451 ef«ghˇ»
7452 «iˇ»jkl
7453 «mˇ»nop"#
7454 ));
7455
7456 cx.update_editor(|editor, window, cx| {
7457 editor.add_selection_above(&Default::default(), window, cx);
7458 });
7459
7460 // test multiple selection upward overflow
7461 cx.assert_editor_state(indoc!(
7462 r#"ab«cdˇ»
7463 «eˇ»f«ghˇ»
7464 «iˇ»jkl
7465 «mˇ»nop"#
7466 ));
7467
7468 cx.update_editor(|editor, window, cx| {
7469 editor.add_selection_below(&Default::default(), window, cx);
7470 });
7471
7472 // test multiple selection retrieves back correctly
7473 cx.assert_editor_state(indoc!(
7474 r#"abcd
7475 ef«ghˇ»
7476 «iˇ»jkl
7477 «mˇ»nop"#
7478 ));
7479
7480 cx.update_editor(|editor, window, cx| {
7481 editor.add_selection_below(&Default::default(), window, cx);
7482 });
7483
7484 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7485 cx.assert_editor_state(indoc!(
7486 r#"abcd
7487 ef«ghˇ»
7488 ij«klˇ»
7489 «mˇ»nop"#
7490 ));
7491
7492 cx.update_editor(|editor, window, cx| {
7493 editor.undo_selection(&Default::default(), window, cx);
7494 });
7495
7496 // test undo
7497 cx.assert_editor_state(indoc!(
7498 r#"abcd
7499 ef«ghˇ»
7500 «iˇ»jkl
7501 «mˇ»nop"#
7502 ));
7503
7504 cx.update_editor(|editor, window, cx| {
7505 editor.redo_selection(&Default::default(), window, cx);
7506 });
7507
7508 // test redo
7509 cx.assert_editor_state(indoc!(
7510 r#"abcd
7511 ef«ghˇ»
7512 ij«klˇ»
7513 «mˇ»nop"#
7514 ));
7515}
7516
7517#[gpui::test]
7518async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7519 init_test(cx, |_| {});
7520 let mut cx = EditorTestContext::new(cx).await;
7521
7522 cx.set_state(indoc!(
7523 r#"line onˇe
7524 liˇne two
7525 line three
7526 line four"#
7527 ));
7528
7529 cx.update_editor(|editor, window, cx| {
7530 editor.add_selection_below(&Default::default(), window, cx);
7531 editor.add_selection_below(&Default::default(), window, cx);
7532 editor.add_selection_below(&Default::default(), window, cx);
7533 });
7534
7535 // initial state with two multi cursor groups
7536 cx.assert_editor_state(indoc!(
7537 r#"line onˇe
7538 liˇne twˇo
7539 liˇne thˇree
7540 liˇne foˇur"#
7541 ));
7542
7543 // add single cursor in middle - simulate opt click
7544 cx.update_editor(|editor, window, cx| {
7545 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7546 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7547 editor.end_selection(window, cx);
7548 });
7549
7550 cx.assert_editor_state(indoc!(
7551 r#"line onˇe
7552 liˇne twˇo
7553 liˇneˇ thˇree
7554 liˇne foˇur"#
7555 ));
7556
7557 cx.update_editor(|editor, window, cx| {
7558 editor.add_selection_above(&Default::default(), window, cx);
7559 });
7560
7561 // test new added selection expands above and existing selection shrinks
7562 cx.assert_editor_state(indoc!(
7563 r#"line onˇe
7564 liˇneˇ twˇo
7565 liˇneˇ thˇree
7566 line four"#
7567 ));
7568
7569 cx.update_editor(|editor, window, cx| {
7570 editor.add_selection_above(&Default::default(), window, cx);
7571 });
7572
7573 // test new added selection expands above and existing selection shrinks
7574 cx.assert_editor_state(indoc!(
7575 r#"lineˇ onˇe
7576 liˇneˇ twˇo
7577 lineˇ three
7578 line four"#
7579 ));
7580
7581 // intial state with two selection groups
7582 cx.set_state(indoc!(
7583 r#"abcd
7584 ef«ghˇ»
7585 ijkl
7586 «mˇ»nop"#
7587 ));
7588
7589 cx.update_editor(|editor, window, cx| {
7590 editor.add_selection_above(&Default::default(), window, cx);
7591 editor.add_selection_above(&Default::default(), window, cx);
7592 });
7593
7594 cx.assert_editor_state(indoc!(
7595 r#"ab«cdˇ»
7596 «eˇ»f«ghˇ»
7597 «iˇ»jkl
7598 «mˇ»nop"#
7599 ));
7600
7601 // add single selection in middle - simulate opt drag
7602 cx.update_editor(|editor, window, cx| {
7603 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7604 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7605 editor.update_selection(
7606 DisplayPoint::new(DisplayRow(2), 4),
7607 0,
7608 gpui::Point::<f32>::default(),
7609 window,
7610 cx,
7611 );
7612 editor.end_selection(window, cx);
7613 });
7614
7615 cx.assert_editor_state(indoc!(
7616 r#"ab«cdˇ»
7617 «eˇ»f«ghˇ»
7618 «iˇ»jk«lˇ»
7619 «mˇ»nop"#
7620 ));
7621
7622 cx.update_editor(|editor, window, cx| {
7623 editor.add_selection_below(&Default::default(), window, cx);
7624 });
7625
7626 // test new added selection expands below, others shrinks from above
7627 cx.assert_editor_state(indoc!(
7628 r#"abcd
7629 ef«ghˇ»
7630 «iˇ»jk«lˇ»
7631 «mˇ»no«pˇ»"#
7632 ));
7633}
7634
7635#[gpui::test]
7636async fn test_select_next(cx: &mut TestAppContext) {
7637 init_test(cx, |_| {});
7638
7639 let mut cx = EditorTestContext::new(cx).await;
7640 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7641
7642 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7643 .unwrap();
7644 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7645
7646 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7647 .unwrap();
7648 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7649
7650 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7651 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7652
7653 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7654 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7655
7656 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7657 .unwrap();
7658 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7659
7660 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7661 .unwrap();
7662 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7663
7664 // Test selection direction should be preserved
7665 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7666
7667 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7668 .unwrap();
7669 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7670}
7671
7672#[gpui::test]
7673async fn test_select_all_matches(cx: &mut TestAppContext) {
7674 init_test(cx, |_| {});
7675
7676 let mut cx = EditorTestContext::new(cx).await;
7677
7678 // Test caret-only selections
7679 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7680 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7681 .unwrap();
7682 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7683
7684 // Test left-to-right selections
7685 cx.set_state("abc\n«abcˇ»\nabc");
7686 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7687 .unwrap();
7688 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7689
7690 // Test right-to-left selections
7691 cx.set_state("abc\n«ˇabc»\nabc");
7692 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7693 .unwrap();
7694 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7695
7696 // Test selecting whitespace with caret selection
7697 cx.set_state("abc\nˇ abc\nabc");
7698 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7699 .unwrap();
7700 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7701
7702 // Test selecting whitespace with left-to-right selection
7703 cx.set_state("abc\n«ˇ »abc\nabc");
7704 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7705 .unwrap();
7706 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7707
7708 // Test no matches with right-to-left selection
7709 cx.set_state("abc\n« ˇ»abc\nabc");
7710 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7711 .unwrap();
7712 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7713
7714 // Test with a single word and clip_at_line_ends=true (#29823)
7715 cx.set_state("aˇbc");
7716 cx.update_editor(|e, window, cx| {
7717 e.set_clip_at_line_ends(true, cx);
7718 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7719 e.set_clip_at_line_ends(false, cx);
7720 });
7721 cx.assert_editor_state("«abcˇ»");
7722}
7723
7724#[gpui::test]
7725async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7726 init_test(cx, |_| {});
7727
7728 let mut cx = EditorTestContext::new(cx).await;
7729
7730 let large_body_1 = "\nd".repeat(200);
7731 let large_body_2 = "\ne".repeat(200);
7732
7733 cx.set_state(&format!(
7734 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7735 ));
7736 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7737 let scroll_position = editor.scroll_position(cx);
7738 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7739 scroll_position
7740 });
7741
7742 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7743 .unwrap();
7744 cx.assert_editor_state(&format!(
7745 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7746 ));
7747 let scroll_position_after_selection =
7748 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7749 assert_eq!(
7750 initial_scroll_position, scroll_position_after_selection,
7751 "Scroll position should not change after selecting all matches"
7752 );
7753}
7754
7755#[gpui::test]
7756async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7757 init_test(cx, |_| {});
7758
7759 let mut cx = EditorLspTestContext::new_rust(
7760 lsp::ServerCapabilities {
7761 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7762 ..Default::default()
7763 },
7764 cx,
7765 )
7766 .await;
7767
7768 cx.set_state(indoc! {"
7769 line 1
7770 line 2
7771 linˇe 3
7772 line 4
7773 line 5
7774 "});
7775
7776 // Make an edit
7777 cx.update_editor(|editor, window, cx| {
7778 editor.handle_input("X", window, cx);
7779 });
7780
7781 // Move cursor to a different position
7782 cx.update_editor(|editor, window, cx| {
7783 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7784 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7785 });
7786 });
7787
7788 cx.assert_editor_state(indoc! {"
7789 line 1
7790 line 2
7791 linXe 3
7792 line 4
7793 liˇne 5
7794 "});
7795
7796 cx.lsp
7797 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7798 Ok(Some(vec![lsp::TextEdit::new(
7799 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7800 "PREFIX ".to_string(),
7801 )]))
7802 });
7803
7804 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7805 .unwrap()
7806 .await
7807 .unwrap();
7808
7809 cx.assert_editor_state(indoc! {"
7810 PREFIX line 1
7811 line 2
7812 linXe 3
7813 line 4
7814 liˇne 5
7815 "});
7816
7817 // Undo formatting
7818 cx.update_editor(|editor, window, cx| {
7819 editor.undo(&Default::default(), window, cx);
7820 });
7821
7822 // Verify cursor moved back to position after edit
7823 cx.assert_editor_state(indoc! {"
7824 line 1
7825 line 2
7826 linXˇe 3
7827 line 4
7828 line 5
7829 "});
7830}
7831
7832#[gpui::test]
7833async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7834 init_test(cx, |_| {});
7835
7836 let mut cx = EditorTestContext::new(cx).await;
7837
7838 let provider = cx.new(|_| FakeEditPredictionProvider::default());
7839 cx.update_editor(|editor, window, cx| {
7840 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7841 });
7842
7843 cx.set_state(indoc! {"
7844 line 1
7845 line 2
7846 linˇe 3
7847 line 4
7848 line 5
7849 line 6
7850 line 7
7851 line 8
7852 line 9
7853 line 10
7854 "});
7855
7856 let snapshot = cx.buffer_snapshot();
7857 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7858
7859 cx.update(|_, cx| {
7860 provider.update(cx, |provider, _| {
7861 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
7862 id: None,
7863 edits: vec![(edit_position..edit_position, "X".into())],
7864 edit_preview: None,
7865 }))
7866 })
7867 });
7868
7869 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
7870 cx.update_editor(|editor, window, cx| {
7871 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7872 });
7873
7874 cx.assert_editor_state(indoc! {"
7875 line 1
7876 line 2
7877 lineXˇ 3
7878 line 4
7879 line 5
7880 line 6
7881 line 7
7882 line 8
7883 line 9
7884 line 10
7885 "});
7886
7887 cx.update_editor(|editor, window, cx| {
7888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7889 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7890 });
7891 });
7892
7893 cx.assert_editor_state(indoc! {"
7894 line 1
7895 line 2
7896 lineX 3
7897 line 4
7898 line 5
7899 line 6
7900 line 7
7901 line 8
7902 line 9
7903 liˇne 10
7904 "});
7905
7906 cx.update_editor(|editor, window, cx| {
7907 editor.undo(&Default::default(), window, cx);
7908 });
7909
7910 cx.assert_editor_state(indoc! {"
7911 line 1
7912 line 2
7913 lineˇ 3
7914 line 4
7915 line 5
7916 line 6
7917 line 7
7918 line 8
7919 line 9
7920 line 10
7921 "});
7922}
7923
7924#[gpui::test]
7925async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7926 init_test(cx, |_| {});
7927
7928 let mut cx = EditorTestContext::new(cx).await;
7929 cx.set_state(
7930 r#"let foo = 2;
7931lˇet foo = 2;
7932let fooˇ = 2;
7933let foo = 2;
7934let foo = ˇ2;"#,
7935 );
7936
7937 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7938 .unwrap();
7939 cx.assert_editor_state(
7940 r#"let foo = 2;
7941«letˇ» foo = 2;
7942let «fooˇ» = 2;
7943let foo = 2;
7944let foo = «2ˇ»;"#,
7945 );
7946
7947 // noop for multiple selections with different contents
7948 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7949 .unwrap();
7950 cx.assert_editor_state(
7951 r#"let foo = 2;
7952«letˇ» foo = 2;
7953let «fooˇ» = 2;
7954let foo = 2;
7955let foo = «2ˇ»;"#,
7956 );
7957
7958 // Test last selection direction should be preserved
7959 cx.set_state(
7960 r#"let foo = 2;
7961let foo = 2;
7962let «fooˇ» = 2;
7963let «ˇfoo» = 2;
7964let foo = 2;"#,
7965 );
7966
7967 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7968 .unwrap();
7969 cx.assert_editor_state(
7970 r#"let foo = 2;
7971let foo = 2;
7972let «fooˇ» = 2;
7973let «ˇfoo» = 2;
7974let «ˇfoo» = 2;"#,
7975 );
7976}
7977
7978#[gpui::test]
7979async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7980 init_test(cx, |_| {});
7981
7982 let mut cx =
7983 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7984
7985 cx.assert_editor_state(indoc! {"
7986 ˇbbb
7987 ccc
7988
7989 bbb
7990 ccc
7991 "});
7992 cx.dispatch_action(SelectPrevious::default());
7993 cx.assert_editor_state(indoc! {"
7994 «bbbˇ»
7995 ccc
7996
7997 bbb
7998 ccc
7999 "});
8000 cx.dispatch_action(SelectPrevious::default());
8001 cx.assert_editor_state(indoc! {"
8002 «bbbˇ»
8003 ccc
8004
8005 «bbbˇ»
8006 ccc
8007 "});
8008}
8009
8010#[gpui::test]
8011async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8012 init_test(cx, |_| {});
8013
8014 let mut cx = EditorTestContext::new(cx).await;
8015 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8016
8017 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8018 .unwrap();
8019 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8020
8021 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8022 .unwrap();
8023 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8024
8025 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8026 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8027
8028 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8029 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8030
8031 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8032 .unwrap();
8033 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8034
8035 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8036 .unwrap();
8037 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8038}
8039
8040#[gpui::test]
8041async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8042 init_test(cx, |_| {});
8043
8044 let mut cx = EditorTestContext::new(cx).await;
8045 cx.set_state("aˇ");
8046
8047 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8048 .unwrap();
8049 cx.assert_editor_state("«aˇ»");
8050 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8051 .unwrap();
8052 cx.assert_editor_state("«aˇ»");
8053}
8054
8055#[gpui::test]
8056async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8057 init_test(cx, |_| {});
8058
8059 let mut cx = EditorTestContext::new(cx).await;
8060 cx.set_state(
8061 r#"let foo = 2;
8062lˇet foo = 2;
8063let fooˇ = 2;
8064let foo = 2;
8065let foo = ˇ2;"#,
8066 );
8067
8068 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8069 .unwrap();
8070 cx.assert_editor_state(
8071 r#"let foo = 2;
8072«letˇ» foo = 2;
8073let «fooˇ» = 2;
8074let foo = 2;
8075let foo = «2ˇ»;"#,
8076 );
8077
8078 // noop for multiple selections with different contents
8079 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8080 .unwrap();
8081 cx.assert_editor_state(
8082 r#"let foo = 2;
8083«letˇ» foo = 2;
8084let «fooˇ» = 2;
8085let foo = 2;
8086let foo = «2ˇ»;"#,
8087 );
8088}
8089
8090#[gpui::test]
8091async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8092 init_test(cx, |_| {});
8093
8094 let mut cx = EditorTestContext::new(cx).await;
8095 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8096
8097 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8098 .unwrap();
8099 // selection direction is preserved
8100 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8101
8102 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8103 .unwrap();
8104 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8105
8106 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8107 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8108
8109 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8110 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8111
8112 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8113 .unwrap();
8114 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8115
8116 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8117 .unwrap();
8118 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8119}
8120
8121#[gpui::test]
8122async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8123 init_test(cx, |_| {});
8124
8125 let language = Arc::new(Language::new(
8126 LanguageConfig::default(),
8127 Some(tree_sitter_rust::LANGUAGE.into()),
8128 ));
8129
8130 let text = r#"
8131 use mod1::mod2::{mod3, mod4};
8132
8133 fn fn_1(param1: bool, param2: &str) {
8134 let var1 = "text";
8135 }
8136 "#
8137 .unindent();
8138
8139 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8140 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8141 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8142
8143 editor
8144 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8145 .await;
8146
8147 editor.update_in(cx, |editor, window, cx| {
8148 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8149 s.select_display_ranges([
8150 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8151 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8152 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8153 ]);
8154 });
8155 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8156 });
8157 editor.update(cx, |editor, cx| {
8158 assert_text_with_selections(
8159 editor,
8160 indoc! {r#"
8161 use mod1::mod2::{mod3, «mod4ˇ»};
8162
8163 fn fn_1«ˇ(param1: bool, param2: &str)» {
8164 let var1 = "«ˇtext»";
8165 }
8166 "#},
8167 cx,
8168 );
8169 });
8170
8171 editor.update_in(cx, |editor, window, cx| {
8172 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8173 });
8174 editor.update(cx, |editor, cx| {
8175 assert_text_with_selections(
8176 editor,
8177 indoc! {r#"
8178 use mod1::mod2::«{mod3, mod4}ˇ»;
8179
8180 «ˇfn fn_1(param1: bool, param2: &str) {
8181 let var1 = "text";
8182 }»
8183 "#},
8184 cx,
8185 );
8186 });
8187
8188 editor.update_in(cx, |editor, window, cx| {
8189 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8190 });
8191 assert_eq!(
8192 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8193 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8194 );
8195
8196 // Trying to expand the selected syntax node one more time has no effect.
8197 editor.update_in(cx, |editor, window, cx| {
8198 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8199 });
8200 assert_eq!(
8201 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8202 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8203 );
8204
8205 editor.update_in(cx, |editor, window, cx| {
8206 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8207 });
8208 editor.update(cx, |editor, cx| {
8209 assert_text_with_selections(
8210 editor,
8211 indoc! {r#"
8212 use mod1::mod2::«{mod3, mod4}ˇ»;
8213
8214 «ˇfn fn_1(param1: bool, param2: &str) {
8215 let var1 = "text";
8216 }»
8217 "#},
8218 cx,
8219 );
8220 });
8221
8222 editor.update_in(cx, |editor, window, cx| {
8223 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8224 });
8225 editor.update(cx, |editor, cx| {
8226 assert_text_with_selections(
8227 editor,
8228 indoc! {r#"
8229 use mod1::mod2::{mod3, «mod4ˇ»};
8230
8231 fn fn_1«ˇ(param1: bool, param2: &str)» {
8232 let var1 = "«ˇtext»";
8233 }
8234 "#},
8235 cx,
8236 );
8237 });
8238
8239 editor.update_in(cx, |editor, window, cx| {
8240 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8241 });
8242 editor.update(cx, |editor, cx| {
8243 assert_text_with_selections(
8244 editor,
8245 indoc! {r#"
8246 use mod1::mod2::{mod3, mo«ˇ»d4};
8247
8248 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8249 let var1 = "te«ˇ»xt";
8250 }
8251 "#},
8252 cx,
8253 );
8254 });
8255
8256 // Trying to shrink the selected syntax node one more time has no effect.
8257 editor.update_in(cx, |editor, window, cx| {
8258 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8259 });
8260 editor.update_in(cx, |editor, _, cx| {
8261 assert_text_with_selections(
8262 editor,
8263 indoc! {r#"
8264 use mod1::mod2::{mod3, mo«ˇ»d4};
8265
8266 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8267 let var1 = "te«ˇ»xt";
8268 }
8269 "#},
8270 cx,
8271 );
8272 });
8273
8274 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8275 // a fold.
8276 editor.update_in(cx, |editor, window, cx| {
8277 editor.fold_creases(
8278 vec![
8279 Crease::simple(
8280 Point::new(0, 21)..Point::new(0, 24),
8281 FoldPlaceholder::test(),
8282 ),
8283 Crease::simple(
8284 Point::new(3, 20)..Point::new(3, 22),
8285 FoldPlaceholder::test(),
8286 ),
8287 ],
8288 true,
8289 window,
8290 cx,
8291 );
8292 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8293 });
8294 editor.update(cx, |editor, cx| {
8295 assert_text_with_selections(
8296 editor,
8297 indoc! {r#"
8298 use mod1::mod2::«{mod3, mod4}ˇ»;
8299
8300 fn fn_1«ˇ(param1: bool, param2: &str)» {
8301 let var1 = "«ˇtext»";
8302 }
8303 "#},
8304 cx,
8305 );
8306 });
8307}
8308
8309#[gpui::test]
8310async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8311 init_test(cx, |_| {});
8312
8313 let language = Arc::new(Language::new(
8314 LanguageConfig::default(),
8315 Some(tree_sitter_rust::LANGUAGE.into()),
8316 ));
8317
8318 let text = "let a = 2;";
8319
8320 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8321 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8322 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8323
8324 editor
8325 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8326 .await;
8327
8328 // Test case 1: Cursor at end of word
8329 editor.update_in(cx, |editor, window, cx| {
8330 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8331 s.select_display_ranges([
8332 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8333 ]);
8334 });
8335 });
8336 editor.update(cx, |editor, cx| {
8337 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8338 });
8339 editor.update_in(cx, |editor, window, cx| {
8340 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8341 });
8342 editor.update(cx, |editor, cx| {
8343 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8344 });
8345 editor.update_in(cx, |editor, window, cx| {
8346 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8347 });
8348 editor.update(cx, |editor, cx| {
8349 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8350 });
8351
8352 // Test case 2: Cursor at end of statement
8353 editor.update_in(cx, |editor, window, cx| {
8354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8355 s.select_display_ranges([
8356 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8357 ]);
8358 });
8359 });
8360 editor.update(cx, |editor, cx| {
8361 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8362 });
8363 editor.update_in(cx, |editor, window, cx| {
8364 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8365 });
8366 editor.update(cx, |editor, cx| {
8367 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8368 });
8369}
8370
8371#[gpui::test]
8372async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8373 init_test(cx, |_| {});
8374
8375 let language = Arc::new(Language::new(
8376 LanguageConfig::default(),
8377 Some(tree_sitter_rust::LANGUAGE.into()),
8378 ));
8379
8380 let text = r#"
8381 use mod1::mod2::{mod3, mod4};
8382
8383 fn fn_1(param1: bool, param2: &str) {
8384 let var1 = "hello world";
8385 }
8386 "#
8387 .unindent();
8388
8389 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8390 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8391 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8392
8393 editor
8394 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8395 .await;
8396
8397 // Test 1: Cursor on a letter of a string word
8398 editor.update_in(cx, |editor, window, cx| {
8399 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8400 s.select_display_ranges([
8401 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8402 ]);
8403 });
8404 });
8405 editor.update_in(cx, |editor, window, cx| {
8406 assert_text_with_selections(
8407 editor,
8408 indoc! {r#"
8409 use mod1::mod2::{mod3, mod4};
8410
8411 fn fn_1(param1: bool, param2: &str) {
8412 let var1 = "hˇello world";
8413 }
8414 "#},
8415 cx,
8416 );
8417 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8418 assert_text_with_selections(
8419 editor,
8420 indoc! {r#"
8421 use mod1::mod2::{mod3, mod4};
8422
8423 fn fn_1(param1: bool, param2: &str) {
8424 let var1 = "«ˇhello» world";
8425 }
8426 "#},
8427 cx,
8428 );
8429 });
8430
8431 // Test 2: Partial selection within a word
8432 editor.update_in(cx, |editor, window, cx| {
8433 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8434 s.select_display_ranges([
8435 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
8436 ]);
8437 });
8438 });
8439 editor.update_in(cx, |editor, window, cx| {
8440 assert_text_with_selections(
8441 editor,
8442 indoc! {r#"
8443 use mod1::mod2::{mod3, mod4};
8444
8445 fn fn_1(param1: bool, param2: &str) {
8446 let var1 = "h«elˇ»lo world";
8447 }
8448 "#},
8449 cx,
8450 );
8451 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8452 assert_text_with_selections(
8453 editor,
8454 indoc! {r#"
8455 use mod1::mod2::{mod3, mod4};
8456
8457 fn fn_1(param1: bool, param2: &str) {
8458 let var1 = "«ˇhello» world";
8459 }
8460 "#},
8461 cx,
8462 );
8463 });
8464
8465 // Test 3: Complete word already selected
8466 editor.update_in(cx, |editor, window, cx| {
8467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8468 s.select_display_ranges([
8469 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
8470 ]);
8471 });
8472 });
8473 editor.update_in(cx, |editor, window, cx| {
8474 assert_text_with_selections(
8475 editor,
8476 indoc! {r#"
8477 use mod1::mod2::{mod3, mod4};
8478
8479 fn fn_1(param1: bool, param2: &str) {
8480 let var1 = "«helloˇ» world";
8481 }
8482 "#},
8483 cx,
8484 );
8485 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8486 assert_text_with_selections(
8487 editor,
8488 indoc! {r#"
8489 use mod1::mod2::{mod3, mod4};
8490
8491 fn fn_1(param1: bool, param2: &str) {
8492 let var1 = "«hello worldˇ»";
8493 }
8494 "#},
8495 cx,
8496 );
8497 });
8498
8499 // Test 4: Selection spanning across words
8500 editor.update_in(cx, |editor, window, cx| {
8501 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8502 s.select_display_ranges([
8503 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
8504 ]);
8505 });
8506 });
8507 editor.update_in(cx, |editor, window, cx| {
8508 assert_text_with_selections(
8509 editor,
8510 indoc! {r#"
8511 use mod1::mod2::{mod3, mod4};
8512
8513 fn fn_1(param1: bool, param2: &str) {
8514 let var1 = "hel«lo woˇ»rld";
8515 }
8516 "#},
8517 cx,
8518 );
8519 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8520 assert_text_with_selections(
8521 editor,
8522 indoc! {r#"
8523 use mod1::mod2::{mod3, mod4};
8524
8525 fn fn_1(param1: bool, param2: &str) {
8526 let var1 = "«ˇhello world»";
8527 }
8528 "#},
8529 cx,
8530 );
8531 });
8532
8533 // Test 5: Expansion beyond string
8534 editor.update_in(cx, |editor, window, cx| {
8535 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8536 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8537 assert_text_with_selections(
8538 editor,
8539 indoc! {r#"
8540 use mod1::mod2::{mod3, mod4};
8541
8542 fn fn_1(param1: bool, param2: &str) {
8543 «ˇlet var1 = "hello world";»
8544 }
8545 "#},
8546 cx,
8547 );
8548 });
8549}
8550
8551#[gpui::test]
8552async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
8553 init_test(cx, |_| {});
8554
8555 let mut cx = EditorTestContext::new(cx).await;
8556
8557 let language = Arc::new(Language::new(
8558 LanguageConfig::default(),
8559 Some(tree_sitter_rust::LANGUAGE.into()),
8560 ));
8561
8562 cx.update_buffer(|buffer, cx| {
8563 buffer.set_language(Some(language), cx);
8564 });
8565
8566 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
8567 cx.update_editor(|editor, window, cx| {
8568 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8569 });
8570
8571 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
8572}
8573
8574#[gpui::test]
8575async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8576 init_test(cx, |_| {});
8577
8578 let base_text = r#"
8579 impl A {
8580 // this is an uncommitted comment
8581
8582 fn b() {
8583 c();
8584 }
8585
8586 // this is another uncommitted comment
8587
8588 fn d() {
8589 // e
8590 // f
8591 }
8592 }
8593
8594 fn g() {
8595 // h
8596 }
8597 "#
8598 .unindent();
8599
8600 let text = r#"
8601 ˇimpl A {
8602
8603 fn b() {
8604 c();
8605 }
8606
8607 fn d() {
8608 // e
8609 // f
8610 }
8611 }
8612
8613 fn g() {
8614 // h
8615 }
8616 "#
8617 .unindent();
8618
8619 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8620 cx.set_state(&text);
8621 cx.set_head_text(&base_text);
8622 cx.update_editor(|editor, window, cx| {
8623 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8624 });
8625
8626 cx.assert_state_with_diff(
8627 "
8628 ˇimpl A {
8629 - // this is an uncommitted comment
8630
8631 fn b() {
8632 c();
8633 }
8634
8635 - // this is another uncommitted comment
8636 -
8637 fn d() {
8638 // e
8639 // f
8640 }
8641 }
8642
8643 fn g() {
8644 // h
8645 }
8646 "
8647 .unindent(),
8648 );
8649
8650 let expected_display_text = "
8651 impl A {
8652 // this is an uncommitted comment
8653
8654 fn b() {
8655 ⋯
8656 }
8657
8658 // this is another uncommitted comment
8659
8660 fn d() {
8661 ⋯
8662 }
8663 }
8664
8665 fn g() {
8666 ⋯
8667 }
8668 "
8669 .unindent();
8670
8671 cx.update_editor(|editor, window, cx| {
8672 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8673 assert_eq!(editor.display_text(cx), expected_display_text);
8674 });
8675}
8676
8677#[gpui::test]
8678async fn test_autoindent(cx: &mut TestAppContext) {
8679 init_test(cx, |_| {});
8680
8681 let language = Arc::new(
8682 Language::new(
8683 LanguageConfig {
8684 brackets: BracketPairConfig {
8685 pairs: vec![
8686 BracketPair {
8687 start: "{".to_string(),
8688 end: "}".to_string(),
8689 close: false,
8690 surround: false,
8691 newline: true,
8692 },
8693 BracketPair {
8694 start: "(".to_string(),
8695 end: ")".to_string(),
8696 close: false,
8697 surround: false,
8698 newline: true,
8699 },
8700 ],
8701 ..Default::default()
8702 },
8703 ..Default::default()
8704 },
8705 Some(tree_sitter_rust::LANGUAGE.into()),
8706 )
8707 .with_indents_query(
8708 r#"
8709 (_ "(" ")" @end) @indent
8710 (_ "{" "}" @end) @indent
8711 "#,
8712 )
8713 .unwrap(),
8714 );
8715
8716 let text = "fn a() {}";
8717
8718 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8719 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8720 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8721 editor
8722 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8723 .await;
8724
8725 editor.update_in(cx, |editor, window, cx| {
8726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8727 s.select_ranges([5..5, 8..8, 9..9])
8728 });
8729 editor.newline(&Newline, window, cx);
8730 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8731 assert_eq!(
8732 editor.selections.ranges(cx),
8733 &[
8734 Point::new(1, 4)..Point::new(1, 4),
8735 Point::new(3, 4)..Point::new(3, 4),
8736 Point::new(5, 0)..Point::new(5, 0)
8737 ]
8738 );
8739 });
8740}
8741
8742#[gpui::test]
8743async fn test_autoindent_disabled(cx: &mut TestAppContext) {
8744 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
8745
8746 let language = Arc::new(
8747 Language::new(
8748 LanguageConfig {
8749 brackets: BracketPairConfig {
8750 pairs: vec![
8751 BracketPair {
8752 start: "{".to_string(),
8753 end: "}".to_string(),
8754 close: false,
8755 surround: false,
8756 newline: true,
8757 },
8758 BracketPair {
8759 start: "(".to_string(),
8760 end: ")".to_string(),
8761 close: false,
8762 surround: false,
8763 newline: true,
8764 },
8765 ],
8766 ..Default::default()
8767 },
8768 ..Default::default()
8769 },
8770 Some(tree_sitter_rust::LANGUAGE.into()),
8771 )
8772 .with_indents_query(
8773 r#"
8774 (_ "(" ")" @end) @indent
8775 (_ "{" "}" @end) @indent
8776 "#,
8777 )
8778 .unwrap(),
8779 );
8780
8781 let text = "fn a() {}";
8782
8783 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8784 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8785 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8786 editor
8787 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8788 .await;
8789
8790 editor.update_in(cx, |editor, window, cx| {
8791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8792 s.select_ranges([5..5, 8..8, 9..9])
8793 });
8794 editor.newline(&Newline, window, cx);
8795 assert_eq!(
8796 editor.text(cx),
8797 indoc!(
8798 "
8799 fn a(
8800
8801 ) {
8802
8803 }
8804 "
8805 )
8806 );
8807 assert_eq!(
8808 editor.selections.ranges(cx),
8809 &[
8810 Point::new(1, 0)..Point::new(1, 0),
8811 Point::new(3, 0)..Point::new(3, 0),
8812 Point::new(5, 0)..Point::new(5, 0)
8813 ]
8814 );
8815 });
8816}
8817
8818#[gpui::test]
8819async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
8820 init_test(cx, |settings| {
8821 settings.defaults.auto_indent = Some(true);
8822 settings.languages.0.insert(
8823 "python".into(),
8824 LanguageSettingsContent {
8825 auto_indent: Some(false),
8826 ..Default::default()
8827 },
8828 );
8829 });
8830
8831 let mut cx = EditorTestContext::new(cx).await;
8832
8833 let injected_language = Arc::new(
8834 Language::new(
8835 LanguageConfig {
8836 brackets: BracketPairConfig {
8837 pairs: vec![
8838 BracketPair {
8839 start: "{".to_string(),
8840 end: "}".to_string(),
8841 close: false,
8842 surround: false,
8843 newline: true,
8844 },
8845 BracketPair {
8846 start: "(".to_string(),
8847 end: ")".to_string(),
8848 close: true,
8849 surround: false,
8850 newline: true,
8851 },
8852 ],
8853 ..Default::default()
8854 },
8855 name: "python".into(),
8856 ..Default::default()
8857 },
8858 Some(tree_sitter_python::LANGUAGE.into()),
8859 )
8860 .with_indents_query(
8861 r#"
8862 (_ "(" ")" @end) @indent
8863 (_ "{" "}" @end) @indent
8864 "#,
8865 )
8866 .unwrap(),
8867 );
8868
8869 let language = Arc::new(
8870 Language::new(
8871 LanguageConfig {
8872 brackets: BracketPairConfig {
8873 pairs: vec![
8874 BracketPair {
8875 start: "{".to_string(),
8876 end: "}".to_string(),
8877 close: false,
8878 surround: false,
8879 newline: true,
8880 },
8881 BracketPair {
8882 start: "(".to_string(),
8883 end: ")".to_string(),
8884 close: true,
8885 surround: false,
8886 newline: true,
8887 },
8888 ],
8889 ..Default::default()
8890 },
8891 name: LanguageName::new("rust"),
8892 ..Default::default()
8893 },
8894 Some(tree_sitter_rust::LANGUAGE.into()),
8895 )
8896 .with_indents_query(
8897 r#"
8898 (_ "(" ")" @end) @indent
8899 (_ "{" "}" @end) @indent
8900 "#,
8901 )
8902 .unwrap()
8903 .with_injection_query(
8904 r#"
8905 (macro_invocation
8906 macro: (identifier) @_macro_name
8907 (token_tree) @injection.content
8908 (#set! injection.language "python"))
8909 "#,
8910 )
8911 .unwrap(),
8912 );
8913
8914 cx.language_registry().add(injected_language);
8915 cx.language_registry().add(language.clone());
8916
8917 cx.update_buffer(|buffer, cx| {
8918 buffer.set_language(Some(language), cx);
8919 });
8920
8921 cx.set_state(r#"struct A {ˇ}"#);
8922
8923 cx.update_editor(|editor, window, cx| {
8924 editor.newline(&Default::default(), window, cx);
8925 });
8926
8927 cx.assert_editor_state(indoc!(
8928 "struct A {
8929 ˇ
8930 }"
8931 ));
8932
8933 cx.set_state(r#"select_biased!(ˇ)"#);
8934
8935 cx.update_editor(|editor, window, cx| {
8936 editor.newline(&Default::default(), window, cx);
8937 editor.handle_input("def ", window, cx);
8938 editor.handle_input("(", window, cx);
8939 editor.newline(&Default::default(), window, cx);
8940 editor.handle_input("a", window, cx);
8941 });
8942
8943 cx.assert_editor_state(indoc!(
8944 "select_biased!(
8945 def (
8946 aˇ
8947 )
8948 )"
8949 ));
8950}
8951
8952#[gpui::test]
8953async fn test_autoindent_selections(cx: &mut TestAppContext) {
8954 init_test(cx, |_| {});
8955
8956 {
8957 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8958 cx.set_state(indoc! {"
8959 impl A {
8960
8961 fn b() {}
8962
8963 «fn c() {
8964
8965 }ˇ»
8966 }
8967 "});
8968
8969 cx.update_editor(|editor, window, cx| {
8970 editor.autoindent(&Default::default(), window, cx);
8971 });
8972
8973 cx.assert_editor_state(indoc! {"
8974 impl A {
8975
8976 fn b() {}
8977
8978 «fn c() {
8979
8980 }ˇ»
8981 }
8982 "});
8983 }
8984
8985 {
8986 let mut cx = EditorTestContext::new_multibuffer(
8987 cx,
8988 [indoc! { "
8989 impl A {
8990 «
8991 // a
8992 fn b(){}
8993 »
8994 «
8995 }
8996 fn c(){}
8997 »
8998 "}],
8999 );
9000
9001 let buffer = cx.update_editor(|editor, _, cx| {
9002 let buffer = editor.buffer().update(cx, |buffer, _| {
9003 buffer.all_buffers().iter().next().unwrap().clone()
9004 });
9005 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9006 buffer
9007 });
9008
9009 cx.run_until_parked();
9010 cx.update_editor(|editor, window, cx| {
9011 editor.select_all(&Default::default(), window, cx);
9012 editor.autoindent(&Default::default(), window, cx)
9013 });
9014 cx.run_until_parked();
9015
9016 cx.update(|_, cx| {
9017 assert_eq!(
9018 buffer.read(cx).text(),
9019 indoc! { "
9020 impl A {
9021
9022 // a
9023 fn b(){}
9024
9025
9026 }
9027 fn c(){}
9028
9029 " }
9030 )
9031 });
9032 }
9033}
9034
9035#[gpui::test]
9036async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9037 init_test(cx, |_| {});
9038
9039 let mut cx = EditorTestContext::new(cx).await;
9040
9041 let language = Arc::new(Language::new(
9042 LanguageConfig {
9043 brackets: BracketPairConfig {
9044 pairs: vec![
9045 BracketPair {
9046 start: "{".to_string(),
9047 end: "}".to_string(),
9048 close: true,
9049 surround: true,
9050 newline: true,
9051 },
9052 BracketPair {
9053 start: "(".to_string(),
9054 end: ")".to_string(),
9055 close: true,
9056 surround: true,
9057 newline: true,
9058 },
9059 BracketPair {
9060 start: "/*".to_string(),
9061 end: " */".to_string(),
9062 close: true,
9063 surround: true,
9064 newline: true,
9065 },
9066 BracketPair {
9067 start: "[".to_string(),
9068 end: "]".to_string(),
9069 close: false,
9070 surround: false,
9071 newline: true,
9072 },
9073 BracketPair {
9074 start: "\"".to_string(),
9075 end: "\"".to_string(),
9076 close: true,
9077 surround: true,
9078 newline: false,
9079 },
9080 BracketPair {
9081 start: "<".to_string(),
9082 end: ">".to_string(),
9083 close: false,
9084 surround: true,
9085 newline: true,
9086 },
9087 ],
9088 ..Default::default()
9089 },
9090 autoclose_before: "})]".to_string(),
9091 ..Default::default()
9092 },
9093 Some(tree_sitter_rust::LANGUAGE.into()),
9094 ));
9095
9096 cx.language_registry().add(language.clone());
9097 cx.update_buffer(|buffer, cx| {
9098 buffer.set_language(Some(language), cx);
9099 });
9100
9101 cx.set_state(
9102 &r#"
9103 🏀ˇ
9104 εˇ
9105 ❤️ˇ
9106 "#
9107 .unindent(),
9108 );
9109
9110 // autoclose multiple nested brackets at multiple cursors
9111 cx.update_editor(|editor, window, cx| {
9112 editor.handle_input("{", window, cx);
9113 editor.handle_input("{", window, cx);
9114 editor.handle_input("{", window, cx);
9115 });
9116 cx.assert_editor_state(
9117 &"
9118 🏀{{{ˇ}}}
9119 ε{{{ˇ}}}
9120 ❤️{{{ˇ}}}
9121 "
9122 .unindent(),
9123 );
9124
9125 // insert a different closing bracket
9126 cx.update_editor(|editor, window, cx| {
9127 editor.handle_input(")", window, cx);
9128 });
9129 cx.assert_editor_state(
9130 &"
9131 🏀{{{)ˇ}}}
9132 ε{{{)ˇ}}}
9133 ❤️{{{)ˇ}}}
9134 "
9135 .unindent(),
9136 );
9137
9138 // skip over the auto-closed brackets when typing a closing bracket
9139 cx.update_editor(|editor, window, cx| {
9140 editor.move_right(&MoveRight, window, cx);
9141 editor.handle_input("}", window, cx);
9142 editor.handle_input("}", window, cx);
9143 editor.handle_input("}", window, cx);
9144 });
9145 cx.assert_editor_state(
9146 &"
9147 🏀{{{)}}}}ˇ
9148 ε{{{)}}}}ˇ
9149 ❤️{{{)}}}}ˇ
9150 "
9151 .unindent(),
9152 );
9153
9154 // autoclose multi-character pairs
9155 cx.set_state(
9156 &"
9157 ˇ
9158 ˇ
9159 "
9160 .unindent(),
9161 );
9162 cx.update_editor(|editor, window, cx| {
9163 editor.handle_input("/", window, cx);
9164 editor.handle_input("*", window, cx);
9165 });
9166 cx.assert_editor_state(
9167 &"
9168 /*ˇ */
9169 /*ˇ */
9170 "
9171 .unindent(),
9172 );
9173
9174 // one cursor autocloses a multi-character pair, one cursor
9175 // does not autoclose.
9176 cx.set_state(
9177 &"
9178 /ˇ
9179 ˇ
9180 "
9181 .unindent(),
9182 );
9183 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9184 cx.assert_editor_state(
9185 &"
9186 /*ˇ */
9187 *ˇ
9188 "
9189 .unindent(),
9190 );
9191
9192 // Don't autoclose if the next character isn't whitespace and isn't
9193 // listed in the language's "autoclose_before" section.
9194 cx.set_state("ˇa b");
9195 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9196 cx.assert_editor_state("{ˇa b");
9197
9198 // Don't autoclose if `close` is false for the bracket pair
9199 cx.set_state("ˇ");
9200 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9201 cx.assert_editor_state("[ˇ");
9202
9203 // Surround with brackets if text is selected
9204 cx.set_state("«aˇ» b");
9205 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9206 cx.assert_editor_state("{«aˇ»} b");
9207
9208 // Autoclose when not immediately after a word character
9209 cx.set_state("a ˇ");
9210 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9211 cx.assert_editor_state("a \"ˇ\"");
9212
9213 // Autoclose pair where the start and end characters are the same
9214 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9215 cx.assert_editor_state("a \"\"ˇ");
9216
9217 // Don't autoclose when immediately after a word character
9218 cx.set_state("aˇ");
9219 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9220 cx.assert_editor_state("a\"ˇ");
9221
9222 // Do autoclose when after a non-word character
9223 cx.set_state("{ˇ");
9224 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9225 cx.assert_editor_state("{\"ˇ\"");
9226
9227 // Non identical pairs autoclose regardless of preceding character
9228 cx.set_state("aˇ");
9229 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9230 cx.assert_editor_state("a{ˇ}");
9231
9232 // Don't autoclose pair if autoclose is disabled
9233 cx.set_state("ˇ");
9234 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9235 cx.assert_editor_state("<ˇ");
9236
9237 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9238 cx.set_state("«aˇ» b");
9239 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9240 cx.assert_editor_state("<«aˇ»> b");
9241}
9242
9243#[gpui::test]
9244async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9245 init_test(cx, |settings| {
9246 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9247 });
9248
9249 let mut cx = EditorTestContext::new(cx).await;
9250
9251 let language = Arc::new(Language::new(
9252 LanguageConfig {
9253 brackets: BracketPairConfig {
9254 pairs: vec![
9255 BracketPair {
9256 start: "{".to_string(),
9257 end: "}".to_string(),
9258 close: true,
9259 surround: true,
9260 newline: true,
9261 },
9262 BracketPair {
9263 start: "(".to_string(),
9264 end: ")".to_string(),
9265 close: true,
9266 surround: true,
9267 newline: true,
9268 },
9269 BracketPair {
9270 start: "[".to_string(),
9271 end: "]".to_string(),
9272 close: false,
9273 surround: false,
9274 newline: true,
9275 },
9276 ],
9277 ..Default::default()
9278 },
9279 autoclose_before: "})]".to_string(),
9280 ..Default::default()
9281 },
9282 Some(tree_sitter_rust::LANGUAGE.into()),
9283 ));
9284
9285 cx.language_registry().add(language.clone());
9286 cx.update_buffer(|buffer, cx| {
9287 buffer.set_language(Some(language), cx);
9288 });
9289
9290 cx.set_state(
9291 &"
9292 ˇ
9293 ˇ
9294 ˇ
9295 "
9296 .unindent(),
9297 );
9298
9299 // ensure only matching closing brackets are skipped over
9300 cx.update_editor(|editor, window, cx| {
9301 editor.handle_input("}", window, cx);
9302 editor.move_left(&MoveLeft, window, cx);
9303 editor.handle_input(")", window, cx);
9304 editor.move_left(&MoveLeft, window, cx);
9305 });
9306 cx.assert_editor_state(
9307 &"
9308 ˇ)}
9309 ˇ)}
9310 ˇ)}
9311 "
9312 .unindent(),
9313 );
9314
9315 // skip-over closing brackets at multiple cursors
9316 cx.update_editor(|editor, window, cx| {
9317 editor.handle_input(")", window, cx);
9318 editor.handle_input("}", window, cx);
9319 });
9320 cx.assert_editor_state(
9321 &"
9322 )}ˇ
9323 )}ˇ
9324 )}ˇ
9325 "
9326 .unindent(),
9327 );
9328
9329 // ignore non-close brackets
9330 cx.update_editor(|editor, window, cx| {
9331 editor.handle_input("]", window, cx);
9332 editor.move_left(&MoveLeft, window, cx);
9333 editor.handle_input("]", window, cx);
9334 });
9335 cx.assert_editor_state(
9336 &"
9337 )}]ˇ]
9338 )}]ˇ]
9339 )}]ˇ]
9340 "
9341 .unindent(),
9342 );
9343}
9344
9345#[gpui::test]
9346async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9347 init_test(cx, |_| {});
9348
9349 let mut cx = EditorTestContext::new(cx).await;
9350
9351 let html_language = Arc::new(
9352 Language::new(
9353 LanguageConfig {
9354 name: "HTML".into(),
9355 brackets: BracketPairConfig {
9356 pairs: vec![
9357 BracketPair {
9358 start: "<".into(),
9359 end: ">".into(),
9360 close: true,
9361 ..Default::default()
9362 },
9363 BracketPair {
9364 start: "{".into(),
9365 end: "}".into(),
9366 close: true,
9367 ..Default::default()
9368 },
9369 BracketPair {
9370 start: "(".into(),
9371 end: ")".into(),
9372 close: true,
9373 ..Default::default()
9374 },
9375 ],
9376 ..Default::default()
9377 },
9378 autoclose_before: "})]>".into(),
9379 ..Default::default()
9380 },
9381 Some(tree_sitter_html::LANGUAGE.into()),
9382 )
9383 .with_injection_query(
9384 r#"
9385 (script_element
9386 (raw_text) @injection.content
9387 (#set! injection.language "javascript"))
9388 "#,
9389 )
9390 .unwrap(),
9391 );
9392
9393 let javascript_language = Arc::new(Language::new(
9394 LanguageConfig {
9395 name: "JavaScript".into(),
9396 brackets: BracketPairConfig {
9397 pairs: vec![
9398 BracketPair {
9399 start: "/*".into(),
9400 end: " */".into(),
9401 close: true,
9402 ..Default::default()
9403 },
9404 BracketPair {
9405 start: "{".into(),
9406 end: "}".into(),
9407 close: true,
9408 ..Default::default()
9409 },
9410 BracketPair {
9411 start: "(".into(),
9412 end: ")".into(),
9413 close: true,
9414 ..Default::default()
9415 },
9416 ],
9417 ..Default::default()
9418 },
9419 autoclose_before: "})]>".into(),
9420 ..Default::default()
9421 },
9422 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9423 ));
9424
9425 cx.language_registry().add(html_language.clone());
9426 cx.language_registry().add(javascript_language);
9427 cx.executor().run_until_parked();
9428
9429 cx.update_buffer(|buffer, cx| {
9430 buffer.set_language(Some(html_language), cx);
9431 });
9432
9433 cx.set_state(
9434 &r#"
9435 <body>ˇ
9436 <script>
9437 var x = 1;ˇ
9438 </script>
9439 </body>ˇ
9440 "#
9441 .unindent(),
9442 );
9443
9444 // Precondition: different languages are active at different locations.
9445 cx.update_editor(|editor, window, cx| {
9446 let snapshot = editor.snapshot(window, cx);
9447 let cursors = editor.selections.ranges::<usize>(cx);
9448 let languages = cursors
9449 .iter()
9450 .map(|c| snapshot.language_at(c.start).unwrap().name())
9451 .collect::<Vec<_>>();
9452 assert_eq!(
9453 languages,
9454 &["HTML".into(), "JavaScript".into(), "HTML".into()]
9455 );
9456 });
9457
9458 // Angle brackets autoclose in HTML, but not JavaScript.
9459 cx.update_editor(|editor, window, cx| {
9460 editor.handle_input("<", window, cx);
9461 editor.handle_input("a", window, cx);
9462 });
9463 cx.assert_editor_state(
9464 &r#"
9465 <body><aˇ>
9466 <script>
9467 var x = 1;<aˇ
9468 </script>
9469 </body><aˇ>
9470 "#
9471 .unindent(),
9472 );
9473
9474 // Curly braces and parens autoclose in both HTML and JavaScript.
9475 cx.update_editor(|editor, window, cx| {
9476 editor.handle_input(" b=", window, cx);
9477 editor.handle_input("{", window, cx);
9478 editor.handle_input("c", window, cx);
9479 editor.handle_input("(", window, cx);
9480 });
9481 cx.assert_editor_state(
9482 &r#"
9483 <body><a b={c(ˇ)}>
9484 <script>
9485 var x = 1;<a b={c(ˇ)}
9486 </script>
9487 </body><a b={c(ˇ)}>
9488 "#
9489 .unindent(),
9490 );
9491
9492 // Brackets that were already autoclosed are skipped.
9493 cx.update_editor(|editor, window, cx| {
9494 editor.handle_input(")", window, cx);
9495 editor.handle_input("d", window, cx);
9496 editor.handle_input("}", window, cx);
9497 });
9498 cx.assert_editor_state(
9499 &r#"
9500 <body><a b={c()d}ˇ>
9501 <script>
9502 var x = 1;<a b={c()d}ˇ
9503 </script>
9504 </body><a b={c()d}ˇ>
9505 "#
9506 .unindent(),
9507 );
9508 cx.update_editor(|editor, window, cx| {
9509 editor.handle_input(">", window, cx);
9510 });
9511 cx.assert_editor_state(
9512 &r#"
9513 <body><a b={c()d}>ˇ
9514 <script>
9515 var x = 1;<a b={c()d}>ˇ
9516 </script>
9517 </body><a b={c()d}>ˇ
9518 "#
9519 .unindent(),
9520 );
9521
9522 // Reset
9523 cx.set_state(
9524 &r#"
9525 <body>ˇ
9526 <script>
9527 var x = 1;ˇ
9528 </script>
9529 </body>ˇ
9530 "#
9531 .unindent(),
9532 );
9533
9534 cx.update_editor(|editor, window, cx| {
9535 editor.handle_input("<", window, cx);
9536 });
9537 cx.assert_editor_state(
9538 &r#"
9539 <body><ˇ>
9540 <script>
9541 var x = 1;<ˇ
9542 </script>
9543 </body><ˇ>
9544 "#
9545 .unindent(),
9546 );
9547
9548 // When backspacing, the closing angle brackets are removed.
9549 cx.update_editor(|editor, window, cx| {
9550 editor.backspace(&Backspace, window, cx);
9551 });
9552 cx.assert_editor_state(
9553 &r#"
9554 <body>ˇ
9555 <script>
9556 var x = 1;ˇ
9557 </script>
9558 </body>ˇ
9559 "#
9560 .unindent(),
9561 );
9562
9563 // Block comments autoclose in JavaScript, but not HTML.
9564 cx.update_editor(|editor, window, cx| {
9565 editor.handle_input("/", window, cx);
9566 editor.handle_input("*", window, cx);
9567 });
9568 cx.assert_editor_state(
9569 &r#"
9570 <body>/*ˇ
9571 <script>
9572 var x = 1;/*ˇ */
9573 </script>
9574 </body>/*ˇ
9575 "#
9576 .unindent(),
9577 );
9578}
9579
9580#[gpui::test]
9581async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9582 init_test(cx, |_| {});
9583
9584 let mut cx = EditorTestContext::new(cx).await;
9585
9586 let rust_language = Arc::new(
9587 Language::new(
9588 LanguageConfig {
9589 name: "Rust".into(),
9590 brackets: serde_json::from_value(json!([
9591 { "start": "{", "end": "}", "close": true, "newline": true },
9592 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9593 ]))
9594 .unwrap(),
9595 autoclose_before: "})]>".into(),
9596 ..Default::default()
9597 },
9598 Some(tree_sitter_rust::LANGUAGE.into()),
9599 )
9600 .with_override_query("(string_literal) @string")
9601 .unwrap(),
9602 );
9603
9604 cx.language_registry().add(rust_language.clone());
9605 cx.update_buffer(|buffer, cx| {
9606 buffer.set_language(Some(rust_language), cx);
9607 });
9608
9609 cx.set_state(
9610 &r#"
9611 let x = ˇ
9612 "#
9613 .unindent(),
9614 );
9615
9616 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9617 cx.update_editor(|editor, window, cx| {
9618 editor.handle_input("\"", window, cx);
9619 });
9620 cx.assert_editor_state(
9621 &r#"
9622 let x = "ˇ"
9623 "#
9624 .unindent(),
9625 );
9626
9627 // Inserting another quotation mark. The cursor moves across the existing
9628 // automatically-inserted quotation mark.
9629 cx.update_editor(|editor, window, cx| {
9630 editor.handle_input("\"", window, cx);
9631 });
9632 cx.assert_editor_state(
9633 &r#"
9634 let x = ""ˇ
9635 "#
9636 .unindent(),
9637 );
9638
9639 // Reset
9640 cx.set_state(
9641 &r#"
9642 let x = ˇ
9643 "#
9644 .unindent(),
9645 );
9646
9647 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9648 cx.update_editor(|editor, window, cx| {
9649 editor.handle_input("\"", window, cx);
9650 editor.handle_input(" ", window, cx);
9651 editor.move_left(&Default::default(), window, cx);
9652 editor.handle_input("\\", window, cx);
9653 editor.handle_input("\"", window, cx);
9654 });
9655 cx.assert_editor_state(
9656 &r#"
9657 let x = "\"ˇ "
9658 "#
9659 .unindent(),
9660 );
9661
9662 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
9663 // mark. Nothing is inserted.
9664 cx.update_editor(|editor, window, cx| {
9665 editor.move_right(&Default::default(), window, cx);
9666 editor.handle_input("\"", window, cx);
9667 });
9668 cx.assert_editor_state(
9669 &r#"
9670 let x = "\" "ˇ
9671 "#
9672 .unindent(),
9673 );
9674}
9675
9676#[gpui::test]
9677async fn test_surround_with_pair(cx: &mut TestAppContext) {
9678 init_test(cx, |_| {});
9679
9680 let language = Arc::new(Language::new(
9681 LanguageConfig {
9682 brackets: BracketPairConfig {
9683 pairs: vec![
9684 BracketPair {
9685 start: "{".to_string(),
9686 end: "}".to_string(),
9687 close: true,
9688 surround: true,
9689 newline: true,
9690 },
9691 BracketPair {
9692 start: "/* ".to_string(),
9693 end: "*/".to_string(),
9694 close: true,
9695 surround: true,
9696 ..Default::default()
9697 },
9698 ],
9699 ..Default::default()
9700 },
9701 ..Default::default()
9702 },
9703 Some(tree_sitter_rust::LANGUAGE.into()),
9704 ));
9705
9706 let text = r#"
9707 a
9708 b
9709 c
9710 "#
9711 .unindent();
9712
9713 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9714 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9715 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9716 editor
9717 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9718 .await;
9719
9720 editor.update_in(cx, |editor, window, cx| {
9721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9722 s.select_display_ranges([
9723 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9724 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9725 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
9726 ])
9727 });
9728
9729 editor.handle_input("{", window, cx);
9730 editor.handle_input("{", window, cx);
9731 editor.handle_input("{", window, cx);
9732 assert_eq!(
9733 editor.text(cx),
9734 "
9735 {{{a}}}
9736 {{{b}}}
9737 {{{c}}}
9738 "
9739 .unindent()
9740 );
9741 assert_eq!(
9742 editor.selections.display_ranges(cx),
9743 [
9744 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
9745 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
9746 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
9747 ]
9748 );
9749
9750 editor.undo(&Undo, window, cx);
9751 editor.undo(&Undo, window, cx);
9752 editor.undo(&Undo, window, cx);
9753 assert_eq!(
9754 editor.text(cx),
9755 "
9756 a
9757 b
9758 c
9759 "
9760 .unindent()
9761 );
9762 assert_eq!(
9763 editor.selections.display_ranges(cx),
9764 [
9765 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9766 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9768 ]
9769 );
9770
9771 // Ensure inserting the first character of a multi-byte bracket pair
9772 // doesn't surround the selections with the bracket.
9773 editor.handle_input("/", window, cx);
9774 assert_eq!(
9775 editor.text(cx),
9776 "
9777 /
9778 /
9779 /
9780 "
9781 .unindent()
9782 );
9783 assert_eq!(
9784 editor.selections.display_ranges(cx),
9785 [
9786 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9787 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9788 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9789 ]
9790 );
9791
9792 editor.undo(&Undo, window, cx);
9793 assert_eq!(
9794 editor.text(cx),
9795 "
9796 a
9797 b
9798 c
9799 "
9800 .unindent()
9801 );
9802 assert_eq!(
9803 editor.selections.display_ranges(cx),
9804 [
9805 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9806 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9807 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9808 ]
9809 );
9810
9811 // Ensure inserting the last character of a multi-byte bracket pair
9812 // doesn't surround the selections with the bracket.
9813 editor.handle_input("*", window, cx);
9814 assert_eq!(
9815 editor.text(cx),
9816 "
9817 *
9818 *
9819 *
9820 "
9821 .unindent()
9822 );
9823 assert_eq!(
9824 editor.selections.display_ranges(cx),
9825 [
9826 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9827 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9828 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9829 ]
9830 );
9831 });
9832}
9833
9834#[gpui::test]
9835async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9836 init_test(cx, |_| {});
9837
9838 let language = Arc::new(Language::new(
9839 LanguageConfig {
9840 brackets: BracketPairConfig {
9841 pairs: vec![BracketPair {
9842 start: "{".to_string(),
9843 end: "}".to_string(),
9844 close: true,
9845 surround: true,
9846 newline: true,
9847 }],
9848 ..Default::default()
9849 },
9850 autoclose_before: "}".to_string(),
9851 ..Default::default()
9852 },
9853 Some(tree_sitter_rust::LANGUAGE.into()),
9854 ));
9855
9856 let text = r#"
9857 a
9858 b
9859 c
9860 "#
9861 .unindent();
9862
9863 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9864 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9865 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9866 editor
9867 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9868 .await;
9869
9870 editor.update_in(cx, |editor, window, cx| {
9871 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9872 s.select_ranges([
9873 Point::new(0, 1)..Point::new(0, 1),
9874 Point::new(1, 1)..Point::new(1, 1),
9875 Point::new(2, 1)..Point::new(2, 1),
9876 ])
9877 });
9878
9879 editor.handle_input("{", window, cx);
9880 editor.handle_input("{", window, cx);
9881 editor.handle_input("_", window, cx);
9882 assert_eq!(
9883 editor.text(cx),
9884 "
9885 a{{_}}
9886 b{{_}}
9887 c{{_}}
9888 "
9889 .unindent()
9890 );
9891 assert_eq!(
9892 editor.selections.ranges::<Point>(cx),
9893 [
9894 Point::new(0, 4)..Point::new(0, 4),
9895 Point::new(1, 4)..Point::new(1, 4),
9896 Point::new(2, 4)..Point::new(2, 4)
9897 ]
9898 );
9899
9900 editor.backspace(&Default::default(), window, cx);
9901 editor.backspace(&Default::default(), window, cx);
9902 assert_eq!(
9903 editor.text(cx),
9904 "
9905 a{}
9906 b{}
9907 c{}
9908 "
9909 .unindent()
9910 );
9911 assert_eq!(
9912 editor.selections.ranges::<Point>(cx),
9913 [
9914 Point::new(0, 2)..Point::new(0, 2),
9915 Point::new(1, 2)..Point::new(1, 2),
9916 Point::new(2, 2)..Point::new(2, 2)
9917 ]
9918 );
9919
9920 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9921 assert_eq!(
9922 editor.text(cx),
9923 "
9924 a
9925 b
9926 c
9927 "
9928 .unindent()
9929 );
9930 assert_eq!(
9931 editor.selections.ranges::<Point>(cx),
9932 [
9933 Point::new(0, 1)..Point::new(0, 1),
9934 Point::new(1, 1)..Point::new(1, 1),
9935 Point::new(2, 1)..Point::new(2, 1)
9936 ]
9937 );
9938 });
9939}
9940
9941#[gpui::test]
9942async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9943 init_test(cx, |settings| {
9944 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9945 });
9946
9947 let mut cx = EditorTestContext::new(cx).await;
9948
9949 let language = Arc::new(Language::new(
9950 LanguageConfig {
9951 brackets: BracketPairConfig {
9952 pairs: vec![
9953 BracketPair {
9954 start: "{".to_string(),
9955 end: "}".to_string(),
9956 close: true,
9957 surround: true,
9958 newline: true,
9959 },
9960 BracketPair {
9961 start: "(".to_string(),
9962 end: ")".to_string(),
9963 close: true,
9964 surround: true,
9965 newline: true,
9966 },
9967 BracketPair {
9968 start: "[".to_string(),
9969 end: "]".to_string(),
9970 close: false,
9971 surround: true,
9972 newline: true,
9973 },
9974 ],
9975 ..Default::default()
9976 },
9977 autoclose_before: "})]".to_string(),
9978 ..Default::default()
9979 },
9980 Some(tree_sitter_rust::LANGUAGE.into()),
9981 ));
9982
9983 cx.language_registry().add(language.clone());
9984 cx.update_buffer(|buffer, cx| {
9985 buffer.set_language(Some(language), cx);
9986 });
9987
9988 cx.set_state(
9989 &"
9990 {(ˇ)}
9991 [[ˇ]]
9992 {(ˇ)}
9993 "
9994 .unindent(),
9995 );
9996
9997 cx.update_editor(|editor, window, cx| {
9998 editor.backspace(&Default::default(), window, cx);
9999 editor.backspace(&Default::default(), window, cx);
10000 });
10001
10002 cx.assert_editor_state(
10003 &"
10004 ˇ
10005 ˇ]]
10006 ˇ
10007 "
10008 .unindent(),
10009 );
10010
10011 cx.update_editor(|editor, window, cx| {
10012 editor.handle_input("{", window, cx);
10013 editor.handle_input("{", window, cx);
10014 editor.move_right(&MoveRight, window, cx);
10015 editor.move_right(&MoveRight, window, cx);
10016 editor.move_left(&MoveLeft, window, cx);
10017 editor.move_left(&MoveLeft, window, cx);
10018 editor.backspace(&Default::default(), window, cx);
10019 });
10020
10021 cx.assert_editor_state(
10022 &"
10023 {ˇ}
10024 {ˇ}]]
10025 {ˇ}
10026 "
10027 .unindent(),
10028 );
10029
10030 cx.update_editor(|editor, window, cx| {
10031 editor.backspace(&Default::default(), window, cx);
10032 });
10033
10034 cx.assert_editor_state(
10035 &"
10036 ˇ
10037 ˇ]]
10038 ˇ
10039 "
10040 .unindent(),
10041 );
10042}
10043
10044#[gpui::test]
10045async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10046 init_test(cx, |_| {});
10047
10048 let language = Arc::new(Language::new(
10049 LanguageConfig::default(),
10050 Some(tree_sitter_rust::LANGUAGE.into()),
10051 ));
10052
10053 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10054 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10055 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10056 editor
10057 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10058 .await;
10059
10060 editor.update_in(cx, |editor, window, cx| {
10061 editor.set_auto_replace_emoji_shortcode(true);
10062
10063 editor.handle_input("Hello ", window, cx);
10064 editor.handle_input(":wave", window, cx);
10065 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10066
10067 editor.handle_input(":", window, cx);
10068 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10069
10070 editor.handle_input(" :smile", window, cx);
10071 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10072
10073 editor.handle_input(":", window, cx);
10074 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10075
10076 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10077 editor.handle_input(":wave", window, cx);
10078 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10079
10080 editor.handle_input(":", window, cx);
10081 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10082
10083 editor.handle_input(":1", window, cx);
10084 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10085
10086 editor.handle_input(":", window, cx);
10087 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10088
10089 // Ensure shortcode does not get replaced when it is part of a word
10090 editor.handle_input(" Test:wave", window, cx);
10091 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10092
10093 editor.handle_input(":", window, cx);
10094 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10095
10096 editor.set_auto_replace_emoji_shortcode(false);
10097
10098 // Ensure shortcode does not get replaced when auto replace is off
10099 editor.handle_input(" :wave", window, cx);
10100 assert_eq!(
10101 editor.text(cx),
10102 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10103 );
10104
10105 editor.handle_input(":", window, cx);
10106 assert_eq!(
10107 editor.text(cx),
10108 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10109 );
10110 });
10111}
10112
10113#[gpui::test]
10114async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10115 init_test(cx, |_| {});
10116
10117 let (text, insertion_ranges) = marked_text_ranges(
10118 indoc! {"
10119 ˇ
10120 "},
10121 false,
10122 );
10123
10124 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10125 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10126
10127 _ = editor.update_in(cx, |editor, window, cx| {
10128 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10129
10130 editor
10131 .insert_snippet(&insertion_ranges, snippet, window, cx)
10132 .unwrap();
10133
10134 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10135 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10136 assert_eq!(editor.text(cx), expected_text);
10137 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10138 }
10139
10140 assert(
10141 editor,
10142 cx,
10143 indoc! {"
10144 type «» =•
10145 "},
10146 );
10147
10148 assert!(editor.context_menu_visible(), "There should be a matches");
10149 });
10150}
10151
10152#[gpui::test]
10153async fn test_snippets(cx: &mut TestAppContext) {
10154 init_test(cx, |_| {});
10155
10156 let mut cx = EditorTestContext::new(cx).await;
10157
10158 cx.set_state(indoc! {"
10159 a.ˇ b
10160 a.ˇ b
10161 a.ˇ b
10162 "});
10163
10164 cx.update_editor(|editor, window, cx| {
10165 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10166 let insertion_ranges = editor
10167 .selections
10168 .all(cx)
10169 .iter()
10170 .map(|s| s.range())
10171 .collect::<Vec<_>>();
10172 editor
10173 .insert_snippet(&insertion_ranges, snippet, window, cx)
10174 .unwrap();
10175 });
10176
10177 cx.assert_editor_state(indoc! {"
10178 a.f(«oneˇ», two, «threeˇ») b
10179 a.f(«oneˇ», two, «threeˇ») b
10180 a.f(«oneˇ», two, «threeˇ») b
10181 "});
10182
10183 // Can't move earlier than the first tab stop
10184 cx.update_editor(|editor, window, cx| {
10185 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10186 });
10187 cx.assert_editor_state(indoc! {"
10188 a.f(«oneˇ», two, «threeˇ») b
10189 a.f(«oneˇ», two, «threeˇ») b
10190 a.f(«oneˇ», two, «threeˇ») b
10191 "});
10192
10193 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10194 cx.assert_editor_state(indoc! {"
10195 a.f(one, «twoˇ», three) b
10196 a.f(one, «twoˇ», three) b
10197 a.f(one, «twoˇ», three) b
10198 "});
10199
10200 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10201 cx.assert_editor_state(indoc! {"
10202 a.f(«oneˇ», two, «threeˇ») b
10203 a.f(«oneˇ», two, «threeˇ») b
10204 a.f(«oneˇ», two, «threeˇ») b
10205 "});
10206
10207 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10208 cx.assert_editor_state(indoc! {"
10209 a.f(one, «twoˇ», three) b
10210 a.f(one, «twoˇ», three) b
10211 a.f(one, «twoˇ», three) b
10212 "});
10213 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10214 cx.assert_editor_state(indoc! {"
10215 a.f(one, two, three)ˇ b
10216 a.f(one, two, three)ˇ b
10217 a.f(one, two, three)ˇ b
10218 "});
10219
10220 // As soon as the last tab stop is reached, snippet state is gone
10221 cx.update_editor(|editor, window, cx| {
10222 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10223 });
10224 cx.assert_editor_state(indoc! {"
10225 a.f(one, two, three)ˇ b
10226 a.f(one, two, three)ˇ b
10227 a.f(one, two, three)ˇ b
10228 "});
10229}
10230
10231#[gpui::test]
10232async fn test_snippet_indentation(cx: &mut TestAppContext) {
10233 init_test(cx, |_| {});
10234
10235 let mut cx = EditorTestContext::new(cx).await;
10236
10237 cx.update_editor(|editor, window, cx| {
10238 let snippet = Snippet::parse(indoc! {"
10239 /*
10240 * Multiline comment with leading indentation
10241 *
10242 * $1
10243 */
10244 $0"})
10245 .unwrap();
10246 let insertion_ranges = editor
10247 .selections
10248 .all(cx)
10249 .iter()
10250 .map(|s| s.range())
10251 .collect::<Vec<_>>();
10252 editor
10253 .insert_snippet(&insertion_ranges, snippet, window, cx)
10254 .unwrap();
10255 });
10256
10257 cx.assert_editor_state(indoc! {"
10258 /*
10259 * Multiline comment with leading indentation
10260 *
10261 * ˇ
10262 */
10263 "});
10264
10265 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10266 cx.assert_editor_state(indoc! {"
10267 /*
10268 * Multiline comment with leading indentation
10269 *
10270 *•
10271 */
10272 ˇ"});
10273}
10274
10275#[gpui::test]
10276async fn test_document_format_during_save(cx: &mut TestAppContext) {
10277 init_test(cx, |_| {});
10278
10279 let fs = FakeFs::new(cx.executor());
10280 fs.insert_file(path!("/file.rs"), Default::default()).await;
10281
10282 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10283
10284 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10285 language_registry.add(rust_lang());
10286 let mut fake_servers = language_registry.register_fake_lsp(
10287 "Rust",
10288 FakeLspAdapter {
10289 capabilities: lsp::ServerCapabilities {
10290 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10291 ..Default::default()
10292 },
10293 ..Default::default()
10294 },
10295 );
10296
10297 let buffer = project
10298 .update(cx, |project, cx| {
10299 project.open_local_buffer(path!("/file.rs"), cx)
10300 })
10301 .await
10302 .unwrap();
10303
10304 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10305 let (editor, cx) = cx.add_window_view(|window, cx| {
10306 build_editor_with_project(project.clone(), buffer, window, cx)
10307 });
10308 editor.update_in(cx, |editor, window, cx| {
10309 editor.set_text("one\ntwo\nthree\n", window, cx)
10310 });
10311 assert!(cx.read(|cx| editor.is_dirty(cx)));
10312
10313 cx.executor().start_waiting();
10314 let fake_server = fake_servers.next().await.unwrap();
10315
10316 {
10317 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10318 move |params, _| async move {
10319 assert_eq!(
10320 params.text_document.uri,
10321 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10322 );
10323 assert_eq!(params.options.tab_size, 4);
10324 Ok(Some(vec![lsp::TextEdit::new(
10325 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10326 ", ".to_string(),
10327 )]))
10328 },
10329 );
10330 let save = editor
10331 .update_in(cx, |editor, window, cx| {
10332 editor.save(
10333 SaveOptions {
10334 format: true,
10335 autosave: false,
10336 },
10337 project.clone(),
10338 window,
10339 cx,
10340 )
10341 })
10342 .unwrap();
10343 cx.executor().start_waiting();
10344 save.await;
10345
10346 assert_eq!(
10347 editor.update(cx, |editor, cx| editor.text(cx)),
10348 "one, two\nthree\n"
10349 );
10350 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10351 }
10352
10353 {
10354 editor.update_in(cx, |editor, window, cx| {
10355 editor.set_text("one\ntwo\nthree\n", window, cx)
10356 });
10357 assert!(cx.read(|cx| editor.is_dirty(cx)));
10358
10359 // Ensure we can still save even if formatting hangs.
10360 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10361 move |params, _| async move {
10362 assert_eq!(
10363 params.text_document.uri,
10364 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10365 );
10366 futures::future::pending::<()>().await;
10367 unreachable!()
10368 },
10369 );
10370 let save = editor
10371 .update_in(cx, |editor, window, cx| {
10372 editor.save(
10373 SaveOptions {
10374 format: true,
10375 autosave: false,
10376 },
10377 project.clone(),
10378 window,
10379 cx,
10380 )
10381 })
10382 .unwrap();
10383 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10384 cx.executor().start_waiting();
10385 save.await;
10386 assert_eq!(
10387 editor.update(cx, |editor, cx| editor.text(cx)),
10388 "one\ntwo\nthree\n"
10389 );
10390 }
10391
10392 // Set rust language override and assert overridden tabsize is sent to language server
10393 update_test_language_settings(cx, |settings| {
10394 settings.languages.0.insert(
10395 "Rust".into(),
10396 LanguageSettingsContent {
10397 tab_size: NonZeroU32::new(8),
10398 ..Default::default()
10399 },
10400 );
10401 });
10402
10403 {
10404 editor.update_in(cx, |editor, window, cx| {
10405 editor.set_text("somehting_new\n", window, cx)
10406 });
10407 assert!(cx.read(|cx| editor.is_dirty(cx)));
10408 let _formatting_request_signal = fake_server
10409 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10410 assert_eq!(
10411 params.text_document.uri,
10412 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10413 );
10414 assert_eq!(params.options.tab_size, 8);
10415 Ok(Some(vec![]))
10416 });
10417 let save = editor
10418 .update_in(cx, |editor, window, cx| {
10419 editor.save(
10420 SaveOptions {
10421 format: true,
10422 autosave: false,
10423 },
10424 project.clone(),
10425 window,
10426 cx,
10427 )
10428 })
10429 .unwrap();
10430 cx.executor().start_waiting();
10431 save.await;
10432 }
10433}
10434
10435#[gpui::test]
10436async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
10437 init_test(cx, |settings| {
10438 settings.defaults.ensure_final_newline_on_save = Some(false);
10439 });
10440
10441 let fs = FakeFs::new(cx.executor());
10442 fs.insert_file(path!("/file.txt"), "foo".into()).await;
10443
10444 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
10445
10446 let buffer = project
10447 .update(cx, |project, cx| {
10448 project.open_local_buffer(path!("/file.txt"), cx)
10449 })
10450 .await
10451 .unwrap();
10452
10453 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10454 let (editor, cx) = cx.add_window_view(|window, cx| {
10455 build_editor_with_project(project.clone(), buffer, window, cx)
10456 });
10457 editor.update_in(cx, |editor, window, cx| {
10458 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10459 s.select_ranges([0..0])
10460 });
10461 });
10462 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10463
10464 editor.update_in(cx, |editor, window, cx| {
10465 editor.handle_input("\n", window, cx)
10466 });
10467 cx.run_until_parked();
10468 save(&editor, &project, cx).await;
10469 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10470
10471 editor.update_in(cx, |editor, window, cx| {
10472 editor.undo(&Default::default(), window, cx);
10473 });
10474 save(&editor, &project, cx).await;
10475 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10476
10477 editor.update_in(cx, |editor, window, cx| {
10478 editor.redo(&Default::default(), window, cx);
10479 });
10480 cx.run_until_parked();
10481 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10482
10483 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
10484 let save = editor
10485 .update_in(cx, |editor, window, cx| {
10486 editor.save(
10487 SaveOptions {
10488 format: true,
10489 autosave: false,
10490 },
10491 project.clone(),
10492 window,
10493 cx,
10494 )
10495 })
10496 .unwrap();
10497 cx.executor().start_waiting();
10498 save.await;
10499 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10500 }
10501}
10502
10503#[gpui::test]
10504async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
10505 init_test(cx, |_| {});
10506
10507 let cols = 4;
10508 let rows = 10;
10509 let sample_text_1 = sample_text(rows, cols, 'a');
10510 assert_eq!(
10511 sample_text_1,
10512 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
10513 );
10514 let sample_text_2 = sample_text(rows, cols, 'l');
10515 assert_eq!(
10516 sample_text_2,
10517 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
10518 );
10519 let sample_text_3 = sample_text(rows, cols, 'v');
10520 assert_eq!(
10521 sample_text_3,
10522 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
10523 );
10524
10525 let fs = FakeFs::new(cx.executor());
10526 fs.insert_tree(
10527 path!("/a"),
10528 json!({
10529 "main.rs": sample_text_1,
10530 "other.rs": sample_text_2,
10531 "lib.rs": sample_text_3,
10532 }),
10533 )
10534 .await;
10535
10536 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10537 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10538 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10539
10540 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10541 language_registry.add(rust_lang());
10542 let mut fake_servers = language_registry.register_fake_lsp(
10543 "Rust",
10544 FakeLspAdapter {
10545 capabilities: lsp::ServerCapabilities {
10546 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10547 ..Default::default()
10548 },
10549 ..Default::default()
10550 },
10551 );
10552
10553 let worktree = project.update(cx, |project, cx| {
10554 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10555 assert_eq!(worktrees.len(), 1);
10556 worktrees.pop().unwrap()
10557 });
10558 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10559
10560 let buffer_1 = project
10561 .update(cx, |project, cx| {
10562 project.open_buffer((worktree_id, "main.rs"), cx)
10563 })
10564 .await
10565 .unwrap();
10566 let buffer_2 = project
10567 .update(cx, |project, cx| {
10568 project.open_buffer((worktree_id, "other.rs"), cx)
10569 })
10570 .await
10571 .unwrap();
10572 let buffer_3 = project
10573 .update(cx, |project, cx| {
10574 project.open_buffer((worktree_id, "lib.rs"), cx)
10575 })
10576 .await
10577 .unwrap();
10578
10579 let multi_buffer = cx.new(|cx| {
10580 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10581 multi_buffer.push_excerpts(
10582 buffer_1.clone(),
10583 [
10584 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10585 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10586 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10587 ],
10588 cx,
10589 );
10590 multi_buffer.push_excerpts(
10591 buffer_2.clone(),
10592 [
10593 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10594 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10595 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10596 ],
10597 cx,
10598 );
10599 multi_buffer.push_excerpts(
10600 buffer_3.clone(),
10601 [
10602 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10603 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10604 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10605 ],
10606 cx,
10607 );
10608 multi_buffer
10609 });
10610 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10611 Editor::new(
10612 EditorMode::full(),
10613 multi_buffer,
10614 Some(project.clone()),
10615 window,
10616 cx,
10617 )
10618 });
10619
10620 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10621 editor.change_selections(
10622 SelectionEffects::scroll(Autoscroll::Next),
10623 window,
10624 cx,
10625 |s| s.select_ranges(Some(1..2)),
10626 );
10627 editor.insert("|one|two|three|", window, cx);
10628 });
10629 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10630 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10631 editor.change_selections(
10632 SelectionEffects::scroll(Autoscroll::Next),
10633 window,
10634 cx,
10635 |s| s.select_ranges(Some(60..70)),
10636 );
10637 editor.insert("|four|five|six|", window, cx);
10638 });
10639 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10640
10641 // First two buffers should be edited, but not the third one.
10642 assert_eq!(
10643 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10644 "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}",
10645 );
10646 buffer_1.update(cx, |buffer, _| {
10647 assert!(buffer.is_dirty());
10648 assert_eq!(
10649 buffer.text(),
10650 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10651 )
10652 });
10653 buffer_2.update(cx, |buffer, _| {
10654 assert!(buffer.is_dirty());
10655 assert_eq!(
10656 buffer.text(),
10657 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
10658 )
10659 });
10660 buffer_3.update(cx, |buffer, _| {
10661 assert!(!buffer.is_dirty());
10662 assert_eq!(buffer.text(), sample_text_3,)
10663 });
10664 cx.executor().run_until_parked();
10665
10666 cx.executor().start_waiting();
10667 let save = multi_buffer_editor
10668 .update_in(cx, |editor, window, cx| {
10669 editor.save(
10670 SaveOptions {
10671 format: true,
10672 autosave: false,
10673 },
10674 project.clone(),
10675 window,
10676 cx,
10677 )
10678 })
10679 .unwrap();
10680
10681 let fake_server = fake_servers.next().await.unwrap();
10682 fake_server
10683 .server
10684 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
10685 Ok(Some(vec![lsp::TextEdit::new(
10686 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10687 format!("[{} formatted]", params.text_document.uri),
10688 )]))
10689 })
10690 .detach();
10691 save.await;
10692
10693 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
10694 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
10695 assert_eq!(
10696 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10697 uri!(
10698 "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}"
10699 ),
10700 );
10701 buffer_1.update(cx, |buffer, _| {
10702 assert!(!buffer.is_dirty());
10703 assert_eq!(
10704 buffer.text(),
10705 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
10706 )
10707 });
10708 buffer_2.update(cx, |buffer, _| {
10709 assert!(!buffer.is_dirty());
10710 assert_eq!(
10711 buffer.text(),
10712 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
10713 )
10714 });
10715 buffer_3.update(cx, |buffer, _| {
10716 assert!(!buffer.is_dirty());
10717 assert_eq!(buffer.text(), sample_text_3,)
10718 });
10719}
10720
10721#[gpui::test]
10722async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
10723 init_test(cx, |_| {});
10724
10725 let fs = FakeFs::new(cx.executor());
10726 fs.insert_tree(
10727 path!("/dir"),
10728 json!({
10729 "file1.rs": "fn main() { println!(\"hello\"); }",
10730 "file2.rs": "fn test() { println!(\"test\"); }",
10731 "file3.rs": "fn other() { println!(\"other\"); }\n",
10732 }),
10733 )
10734 .await;
10735
10736 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
10737 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10738 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10739
10740 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10741 language_registry.add(rust_lang());
10742
10743 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
10744 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10745
10746 // Open three buffers
10747 let buffer_1 = project
10748 .update(cx, |project, cx| {
10749 project.open_buffer((worktree_id, "file1.rs"), cx)
10750 })
10751 .await
10752 .unwrap();
10753 let buffer_2 = project
10754 .update(cx, |project, cx| {
10755 project.open_buffer((worktree_id, "file2.rs"), cx)
10756 })
10757 .await
10758 .unwrap();
10759 let buffer_3 = project
10760 .update(cx, |project, cx| {
10761 project.open_buffer((worktree_id, "file3.rs"), cx)
10762 })
10763 .await
10764 .unwrap();
10765
10766 // Create a multi-buffer with all three buffers
10767 let multi_buffer = cx.new(|cx| {
10768 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10769 multi_buffer.push_excerpts(
10770 buffer_1.clone(),
10771 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10772 cx,
10773 );
10774 multi_buffer.push_excerpts(
10775 buffer_2.clone(),
10776 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10777 cx,
10778 );
10779 multi_buffer.push_excerpts(
10780 buffer_3.clone(),
10781 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10782 cx,
10783 );
10784 multi_buffer
10785 });
10786
10787 let editor = cx.new_window_entity(|window, cx| {
10788 Editor::new(
10789 EditorMode::full(),
10790 multi_buffer,
10791 Some(project.clone()),
10792 window,
10793 cx,
10794 )
10795 });
10796
10797 // Edit only the first buffer
10798 editor.update_in(cx, |editor, window, cx| {
10799 editor.change_selections(
10800 SelectionEffects::scroll(Autoscroll::Next),
10801 window,
10802 cx,
10803 |s| s.select_ranges(Some(10..10)),
10804 );
10805 editor.insert("// edited", window, cx);
10806 });
10807
10808 // Verify that only buffer 1 is dirty
10809 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10810 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10811 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10812
10813 // Get write counts after file creation (files were created with initial content)
10814 // We expect each file to have been written once during creation
10815 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10816 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10817 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10818
10819 // Perform autosave
10820 let save_task = editor.update_in(cx, |editor, window, cx| {
10821 editor.save(
10822 SaveOptions {
10823 format: true,
10824 autosave: true,
10825 },
10826 project.clone(),
10827 window,
10828 cx,
10829 )
10830 });
10831 save_task.await.unwrap();
10832
10833 // Only the dirty buffer should have been saved
10834 assert_eq!(
10835 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10836 1,
10837 "Buffer 1 was dirty, so it should have been written once during autosave"
10838 );
10839 assert_eq!(
10840 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10841 0,
10842 "Buffer 2 was clean, so it should not have been written during autosave"
10843 );
10844 assert_eq!(
10845 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10846 0,
10847 "Buffer 3 was clean, so it should not have been written during autosave"
10848 );
10849
10850 // Verify buffer states after autosave
10851 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10852 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10853 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10854
10855 // Now perform a manual save (format = true)
10856 let save_task = editor.update_in(cx, |editor, window, cx| {
10857 editor.save(
10858 SaveOptions {
10859 format: true,
10860 autosave: false,
10861 },
10862 project.clone(),
10863 window,
10864 cx,
10865 )
10866 });
10867 save_task.await.unwrap();
10868
10869 // During manual save, clean buffers don't get written to disk
10870 // They just get did_save called for language server notifications
10871 assert_eq!(
10872 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10873 1,
10874 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10875 );
10876 assert_eq!(
10877 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10878 0,
10879 "Buffer 2 should not have been written at all"
10880 );
10881 assert_eq!(
10882 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10883 0,
10884 "Buffer 3 should not have been written at all"
10885 );
10886}
10887
10888async fn setup_range_format_test(
10889 cx: &mut TestAppContext,
10890) -> (
10891 Entity<Project>,
10892 Entity<Editor>,
10893 &mut gpui::VisualTestContext,
10894 lsp::FakeLanguageServer,
10895) {
10896 init_test(cx, |_| {});
10897
10898 let fs = FakeFs::new(cx.executor());
10899 fs.insert_file(path!("/file.rs"), Default::default()).await;
10900
10901 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10902
10903 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10904 language_registry.add(rust_lang());
10905 let mut fake_servers = language_registry.register_fake_lsp(
10906 "Rust",
10907 FakeLspAdapter {
10908 capabilities: lsp::ServerCapabilities {
10909 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10910 ..lsp::ServerCapabilities::default()
10911 },
10912 ..FakeLspAdapter::default()
10913 },
10914 );
10915
10916 let buffer = project
10917 .update(cx, |project, cx| {
10918 project.open_local_buffer(path!("/file.rs"), cx)
10919 })
10920 .await
10921 .unwrap();
10922
10923 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10924 let (editor, cx) = cx.add_window_view(|window, cx| {
10925 build_editor_with_project(project.clone(), buffer, window, cx)
10926 });
10927
10928 cx.executor().start_waiting();
10929 let fake_server = fake_servers.next().await.unwrap();
10930
10931 (project, editor, cx, fake_server)
10932}
10933
10934#[gpui::test]
10935async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10936 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10937
10938 editor.update_in(cx, |editor, window, cx| {
10939 editor.set_text("one\ntwo\nthree\n", window, cx)
10940 });
10941 assert!(cx.read(|cx| editor.is_dirty(cx)));
10942
10943 let save = editor
10944 .update_in(cx, |editor, window, cx| {
10945 editor.save(
10946 SaveOptions {
10947 format: true,
10948 autosave: false,
10949 },
10950 project.clone(),
10951 window,
10952 cx,
10953 )
10954 })
10955 .unwrap();
10956 fake_server
10957 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10958 assert_eq!(
10959 params.text_document.uri,
10960 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10961 );
10962 assert_eq!(params.options.tab_size, 4);
10963 Ok(Some(vec![lsp::TextEdit::new(
10964 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10965 ", ".to_string(),
10966 )]))
10967 })
10968 .next()
10969 .await;
10970 cx.executor().start_waiting();
10971 save.await;
10972 assert_eq!(
10973 editor.update(cx, |editor, cx| editor.text(cx)),
10974 "one, two\nthree\n"
10975 );
10976 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10977}
10978
10979#[gpui::test]
10980async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10981 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10982
10983 editor.update_in(cx, |editor, window, cx| {
10984 editor.set_text("one\ntwo\nthree\n", window, cx)
10985 });
10986 assert!(cx.read(|cx| editor.is_dirty(cx)));
10987
10988 // Test that save still works when formatting hangs
10989 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10990 move |params, _| async move {
10991 assert_eq!(
10992 params.text_document.uri,
10993 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10994 );
10995 futures::future::pending::<()>().await;
10996 unreachable!()
10997 },
10998 );
10999 let save = editor
11000 .update_in(cx, |editor, window, cx| {
11001 editor.save(
11002 SaveOptions {
11003 format: true,
11004 autosave: false,
11005 },
11006 project.clone(),
11007 window,
11008 cx,
11009 )
11010 })
11011 .unwrap();
11012 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11013 cx.executor().start_waiting();
11014 save.await;
11015 assert_eq!(
11016 editor.update(cx, |editor, cx| editor.text(cx)),
11017 "one\ntwo\nthree\n"
11018 );
11019 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11020}
11021
11022#[gpui::test]
11023async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11024 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11025
11026 // Buffer starts clean, no formatting should be requested
11027 let save = editor
11028 .update_in(cx, |editor, window, cx| {
11029 editor.save(
11030 SaveOptions {
11031 format: false,
11032 autosave: false,
11033 },
11034 project.clone(),
11035 window,
11036 cx,
11037 )
11038 })
11039 .unwrap();
11040 let _pending_format_request = fake_server
11041 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11042 panic!("Should not be invoked");
11043 })
11044 .next();
11045 cx.executor().start_waiting();
11046 save.await;
11047 cx.run_until_parked();
11048}
11049
11050#[gpui::test]
11051async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11052 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11053
11054 // Set Rust language override and assert overridden tabsize is sent to language server
11055 update_test_language_settings(cx, |settings| {
11056 settings.languages.0.insert(
11057 "Rust".into(),
11058 LanguageSettingsContent {
11059 tab_size: NonZeroU32::new(8),
11060 ..Default::default()
11061 },
11062 );
11063 });
11064
11065 editor.update_in(cx, |editor, window, cx| {
11066 editor.set_text("something_new\n", window, cx)
11067 });
11068 assert!(cx.read(|cx| editor.is_dirty(cx)));
11069 let save = editor
11070 .update_in(cx, |editor, window, cx| {
11071 editor.save(
11072 SaveOptions {
11073 format: true,
11074 autosave: false,
11075 },
11076 project.clone(),
11077 window,
11078 cx,
11079 )
11080 })
11081 .unwrap();
11082 fake_server
11083 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11084 assert_eq!(
11085 params.text_document.uri,
11086 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11087 );
11088 assert_eq!(params.options.tab_size, 8);
11089 Ok(Some(Vec::new()))
11090 })
11091 .next()
11092 .await;
11093 save.await;
11094}
11095
11096#[gpui::test]
11097async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11098 init_test(cx, |settings| {
11099 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11100 Formatter::LanguageServer { name: None },
11101 )))
11102 });
11103
11104 let fs = FakeFs::new(cx.executor());
11105 fs.insert_file(path!("/file.rs"), Default::default()).await;
11106
11107 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11108
11109 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11110 language_registry.add(Arc::new(Language::new(
11111 LanguageConfig {
11112 name: "Rust".into(),
11113 matcher: LanguageMatcher {
11114 path_suffixes: vec!["rs".to_string()],
11115 ..Default::default()
11116 },
11117 ..LanguageConfig::default()
11118 },
11119 Some(tree_sitter_rust::LANGUAGE.into()),
11120 )));
11121 update_test_language_settings(cx, |settings| {
11122 // Enable Prettier formatting for the same buffer, and ensure
11123 // LSP is called instead of Prettier.
11124 settings.defaults.prettier = Some(PrettierSettings {
11125 allowed: true,
11126 ..PrettierSettings::default()
11127 });
11128 });
11129 let mut fake_servers = language_registry.register_fake_lsp(
11130 "Rust",
11131 FakeLspAdapter {
11132 capabilities: lsp::ServerCapabilities {
11133 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11134 ..Default::default()
11135 },
11136 ..Default::default()
11137 },
11138 );
11139
11140 let buffer = project
11141 .update(cx, |project, cx| {
11142 project.open_local_buffer(path!("/file.rs"), cx)
11143 })
11144 .await
11145 .unwrap();
11146
11147 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11148 let (editor, cx) = cx.add_window_view(|window, cx| {
11149 build_editor_with_project(project.clone(), buffer, window, cx)
11150 });
11151 editor.update_in(cx, |editor, window, cx| {
11152 editor.set_text("one\ntwo\nthree\n", window, cx)
11153 });
11154
11155 cx.executor().start_waiting();
11156 let fake_server = fake_servers.next().await.unwrap();
11157
11158 let format = editor
11159 .update_in(cx, |editor, window, cx| {
11160 editor.perform_format(
11161 project.clone(),
11162 FormatTrigger::Manual,
11163 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11164 window,
11165 cx,
11166 )
11167 })
11168 .unwrap();
11169 fake_server
11170 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11171 assert_eq!(
11172 params.text_document.uri,
11173 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11174 );
11175 assert_eq!(params.options.tab_size, 4);
11176 Ok(Some(vec![lsp::TextEdit::new(
11177 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11178 ", ".to_string(),
11179 )]))
11180 })
11181 .next()
11182 .await;
11183 cx.executor().start_waiting();
11184 format.await;
11185 assert_eq!(
11186 editor.update(cx, |editor, cx| editor.text(cx)),
11187 "one, two\nthree\n"
11188 );
11189
11190 editor.update_in(cx, |editor, window, cx| {
11191 editor.set_text("one\ntwo\nthree\n", window, cx)
11192 });
11193 // Ensure we don't lock if formatting hangs.
11194 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11195 move |params, _| async move {
11196 assert_eq!(
11197 params.text_document.uri,
11198 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11199 );
11200 futures::future::pending::<()>().await;
11201 unreachable!()
11202 },
11203 );
11204 let format = editor
11205 .update_in(cx, |editor, window, cx| {
11206 editor.perform_format(
11207 project,
11208 FormatTrigger::Manual,
11209 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11210 window,
11211 cx,
11212 )
11213 })
11214 .unwrap();
11215 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11216 cx.executor().start_waiting();
11217 format.await;
11218 assert_eq!(
11219 editor.update(cx, |editor, cx| editor.text(cx)),
11220 "one\ntwo\nthree\n"
11221 );
11222}
11223
11224#[gpui::test]
11225async fn test_multiple_formatters(cx: &mut TestAppContext) {
11226 init_test(cx, |settings| {
11227 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11228 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11229 Formatter::LanguageServer { name: None },
11230 Formatter::CodeActions(
11231 [
11232 ("code-action-1".into(), true),
11233 ("code-action-2".into(), true),
11234 ]
11235 .into_iter()
11236 .collect(),
11237 ),
11238 ])))
11239 });
11240
11241 let fs = FakeFs::new(cx.executor());
11242 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11243 .await;
11244
11245 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11246 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11247 language_registry.add(rust_lang());
11248
11249 let mut fake_servers = language_registry.register_fake_lsp(
11250 "Rust",
11251 FakeLspAdapter {
11252 capabilities: lsp::ServerCapabilities {
11253 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11254 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11255 commands: vec!["the-command-for-code-action-1".into()],
11256 ..Default::default()
11257 }),
11258 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11259 ..Default::default()
11260 },
11261 ..Default::default()
11262 },
11263 );
11264
11265 let buffer = project
11266 .update(cx, |project, cx| {
11267 project.open_local_buffer(path!("/file.rs"), cx)
11268 })
11269 .await
11270 .unwrap();
11271
11272 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11273 let (editor, cx) = cx.add_window_view(|window, cx| {
11274 build_editor_with_project(project.clone(), buffer, window, cx)
11275 });
11276
11277 cx.executor().start_waiting();
11278
11279 let fake_server = fake_servers.next().await.unwrap();
11280 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11281 move |_params, _| async move {
11282 Ok(Some(vec![lsp::TextEdit::new(
11283 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11284 "applied-formatting\n".to_string(),
11285 )]))
11286 },
11287 );
11288 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11289 move |params, _| async move {
11290 assert_eq!(
11291 params.context.only,
11292 Some(vec!["code-action-1".into(), "code-action-2".into()])
11293 );
11294 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11295 Ok(Some(vec![
11296 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11297 kind: Some("code-action-1".into()),
11298 edit: Some(lsp::WorkspaceEdit::new(
11299 [(
11300 uri.clone(),
11301 vec![lsp::TextEdit::new(
11302 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11303 "applied-code-action-1-edit\n".to_string(),
11304 )],
11305 )]
11306 .into_iter()
11307 .collect(),
11308 )),
11309 command: Some(lsp::Command {
11310 command: "the-command-for-code-action-1".into(),
11311 ..Default::default()
11312 }),
11313 ..Default::default()
11314 }),
11315 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11316 kind: Some("code-action-2".into()),
11317 edit: Some(lsp::WorkspaceEdit::new(
11318 [(
11319 uri,
11320 vec![lsp::TextEdit::new(
11321 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11322 "applied-code-action-2-edit\n".to_string(),
11323 )],
11324 )]
11325 .into_iter()
11326 .collect(),
11327 )),
11328 ..Default::default()
11329 }),
11330 ]))
11331 },
11332 );
11333
11334 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11335 move |params, _| async move { Ok(params) }
11336 });
11337
11338 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11339 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11340 let fake = fake_server.clone();
11341 let lock = command_lock.clone();
11342 move |params, _| {
11343 assert_eq!(params.command, "the-command-for-code-action-1");
11344 let fake = fake.clone();
11345 let lock = lock.clone();
11346 async move {
11347 lock.lock().await;
11348 fake.server
11349 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11350 label: None,
11351 edit: lsp::WorkspaceEdit {
11352 changes: Some(
11353 [(
11354 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11355 vec![lsp::TextEdit {
11356 range: lsp::Range::new(
11357 lsp::Position::new(0, 0),
11358 lsp::Position::new(0, 0),
11359 ),
11360 new_text: "applied-code-action-1-command\n".into(),
11361 }],
11362 )]
11363 .into_iter()
11364 .collect(),
11365 ),
11366 ..Default::default()
11367 },
11368 })
11369 .await
11370 .into_response()
11371 .unwrap();
11372 Ok(Some(json!(null)))
11373 }
11374 }
11375 });
11376
11377 cx.executor().start_waiting();
11378 editor
11379 .update_in(cx, |editor, window, cx| {
11380 editor.perform_format(
11381 project.clone(),
11382 FormatTrigger::Manual,
11383 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11384 window,
11385 cx,
11386 )
11387 })
11388 .unwrap()
11389 .await;
11390 editor.update(cx, |editor, cx| {
11391 assert_eq!(
11392 editor.text(cx),
11393 r#"
11394 applied-code-action-2-edit
11395 applied-code-action-1-command
11396 applied-code-action-1-edit
11397 applied-formatting
11398 one
11399 two
11400 three
11401 "#
11402 .unindent()
11403 );
11404 });
11405
11406 editor.update_in(cx, |editor, window, cx| {
11407 editor.undo(&Default::default(), window, cx);
11408 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11409 });
11410
11411 // Perform a manual edit while waiting for an LSP command
11412 // that's being run as part of a formatting code action.
11413 let lock_guard = command_lock.lock().await;
11414 let format = editor
11415 .update_in(cx, |editor, window, cx| {
11416 editor.perform_format(
11417 project.clone(),
11418 FormatTrigger::Manual,
11419 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11420 window,
11421 cx,
11422 )
11423 })
11424 .unwrap();
11425 cx.run_until_parked();
11426 editor.update(cx, |editor, cx| {
11427 assert_eq!(
11428 editor.text(cx),
11429 r#"
11430 applied-code-action-1-edit
11431 applied-formatting
11432 one
11433 two
11434 three
11435 "#
11436 .unindent()
11437 );
11438
11439 editor.buffer.update(cx, |buffer, cx| {
11440 let ix = buffer.len(cx);
11441 buffer.edit([(ix..ix, "edited\n")], None, cx);
11442 });
11443 });
11444
11445 // Allow the LSP command to proceed. Because the buffer was edited,
11446 // the second code action will not be run.
11447 drop(lock_guard);
11448 format.await;
11449 editor.update_in(cx, |editor, window, cx| {
11450 assert_eq!(
11451 editor.text(cx),
11452 r#"
11453 applied-code-action-1-command
11454 applied-code-action-1-edit
11455 applied-formatting
11456 one
11457 two
11458 three
11459 edited
11460 "#
11461 .unindent()
11462 );
11463
11464 // The manual edit is undone first, because it is the last thing the user did
11465 // (even though the command completed afterwards).
11466 editor.undo(&Default::default(), window, cx);
11467 assert_eq!(
11468 editor.text(cx),
11469 r#"
11470 applied-code-action-1-command
11471 applied-code-action-1-edit
11472 applied-formatting
11473 one
11474 two
11475 three
11476 "#
11477 .unindent()
11478 );
11479
11480 // All the formatting (including the command, which completed after the manual edit)
11481 // is undone together.
11482 editor.undo(&Default::default(), window, cx);
11483 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11484 });
11485}
11486
11487#[gpui::test]
11488async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
11489 init_test(cx, |settings| {
11490 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11491 Formatter::LanguageServer { name: None },
11492 ])))
11493 });
11494
11495 let fs = FakeFs::new(cx.executor());
11496 fs.insert_file(path!("/file.ts"), Default::default()).await;
11497
11498 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11499
11500 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11501 language_registry.add(Arc::new(Language::new(
11502 LanguageConfig {
11503 name: "TypeScript".into(),
11504 matcher: LanguageMatcher {
11505 path_suffixes: vec!["ts".to_string()],
11506 ..Default::default()
11507 },
11508 ..LanguageConfig::default()
11509 },
11510 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11511 )));
11512 update_test_language_settings(cx, |settings| {
11513 settings.defaults.prettier = Some(PrettierSettings {
11514 allowed: true,
11515 ..PrettierSettings::default()
11516 });
11517 });
11518 let mut fake_servers = language_registry.register_fake_lsp(
11519 "TypeScript",
11520 FakeLspAdapter {
11521 capabilities: lsp::ServerCapabilities {
11522 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11523 ..Default::default()
11524 },
11525 ..Default::default()
11526 },
11527 );
11528
11529 let buffer = project
11530 .update(cx, |project, cx| {
11531 project.open_local_buffer(path!("/file.ts"), cx)
11532 })
11533 .await
11534 .unwrap();
11535
11536 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11537 let (editor, cx) = cx.add_window_view(|window, cx| {
11538 build_editor_with_project(project.clone(), buffer, window, cx)
11539 });
11540 editor.update_in(cx, |editor, window, cx| {
11541 editor.set_text(
11542 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11543 window,
11544 cx,
11545 )
11546 });
11547
11548 cx.executor().start_waiting();
11549 let fake_server = fake_servers.next().await.unwrap();
11550
11551 let format = editor
11552 .update_in(cx, |editor, window, cx| {
11553 editor.perform_code_action_kind(
11554 project.clone(),
11555 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11556 window,
11557 cx,
11558 )
11559 })
11560 .unwrap();
11561 fake_server
11562 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11563 assert_eq!(
11564 params.text_document.uri,
11565 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11566 );
11567 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11568 lsp::CodeAction {
11569 title: "Organize Imports".to_string(),
11570 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11571 edit: Some(lsp::WorkspaceEdit {
11572 changes: Some(
11573 [(
11574 params.text_document.uri.clone(),
11575 vec![lsp::TextEdit::new(
11576 lsp::Range::new(
11577 lsp::Position::new(1, 0),
11578 lsp::Position::new(2, 0),
11579 ),
11580 "".to_string(),
11581 )],
11582 )]
11583 .into_iter()
11584 .collect(),
11585 ),
11586 ..Default::default()
11587 }),
11588 ..Default::default()
11589 },
11590 )]))
11591 })
11592 .next()
11593 .await;
11594 cx.executor().start_waiting();
11595 format.await;
11596 assert_eq!(
11597 editor.update(cx, |editor, cx| editor.text(cx)),
11598 "import { a } from 'module';\n\nconst x = a;\n"
11599 );
11600
11601 editor.update_in(cx, |editor, window, cx| {
11602 editor.set_text(
11603 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11604 window,
11605 cx,
11606 )
11607 });
11608 // Ensure we don't lock if code action hangs.
11609 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11610 move |params, _| async move {
11611 assert_eq!(
11612 params.text_document.uri,
11613 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11614 );
11615 futures::future::pending::<()>().await;
11616 unreachable!()
11617 },
11618 );
11619 let format = editor
11620 .update_in(cx, |editor, window, cx| {
11621 editor.perform_code_action_kind(
11622 project,
11623 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11624 window,
11625 cx,
11626 )
11627 })
11628 .unwrap();
11629 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11630 cx.executor().start_waiting();
11631 format.await;
11632 assert_eq!(
11633 editor.update(cx, |editor, cx| editor.text(cx)),
11634 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11635 );
11636}
11637
11638#[gpui::test]
11639async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11640 init_test(cx, |_| {});
11641
11642 let mut cx = EditorLspTestContext::new_rust(
11643 lsp::ServerCapabilities {
11644 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11645 ..Default::default()
11646 },
11647 cx,
11648 )
11649 .await;
11650
11651 cx.set_state(indoc! {"
11652 one.twoˇ
11653 "});
11654
11655 // The format request takes a long time. When it completes, it inserts
11656 // a newline and an indent before the `.`
11657 cx.lsp
11658 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
11659 let executor = cx.background_executor().clone();
11660 async move {
11661 executor.timer(Duration::from_millis(100)).await;
11662 Ok(Some(vec![lsp::TextEdit {
11663 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
11664 new_text: "\n ".into(),
11665 }]))
11666 }
11667 });
11668
11669 // Submit a format request.
11670 let format_1 = cx
11671 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11672 .unwrap();
11673 cx.executor().run_until_parked();
11674
11675 // Submit a second format request.
11676 let format_2 = cx
11677 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11678 .unwrap();
11679 cx.executor().run_until_parked();
11680
11681 // Wait for both format requests to complete
11682 cx.executor().advance_clock(Duration::from_millis(200));
11683 cx.executor().start_waiting();
11684 format_1.await.unwrap();
11685 cx.executor().start_waiting();
11686 format_2.await.unwrap();
11687
11688 // The formatting edits only happens once.
11689 cx.assert_editor_state(indoc! {"
11690 one
11691 .twoˇ
11692 "});
11693}
11694
11695#[gpui::test]
11696async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
11697 init_test(cx, |settings| {
11698 settings.defaults.formatter = Some(SelectedFormatter::Auto)
11699 });
11700
11701 let mut cx = EditorLspTestContext::new_rust(
11702 lsp::ServerCapabilities {
11703 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11704 ..Default::default()
11705 },
11706 cx,
11707 )
11708 .await;
11709
11710 // Set up a buffer white some trailing whitespace and no trailing newline.
11711 cx.set_state(
11712 &[
11713 "one ", //
11714 "twoˇ", //
11715 "three ", //
11716 "four", //
11717 ]
11718 .join("\n"),
11719 );
11720
11721 // Submit a format request.
11722 let format = cx
11723 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11724 .unwrap();
11725
11726 // Record which buffer changes have been sent to the language server
11727 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
11728 cx.lsp
11729 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
11730 let buffer_changes = buffer_changes.clone();
11731 move |params, _| {
11732 buffer_changes.lock().extend(
11733 params
11734 .content_changes
11735 .into_iter()
11736 .map(|e| (e.range.unwrap(), e.text)),
11737 );
11738 }
11739 });
11740
11741 // Handle formatting requests to the language server.
11742 cx.lsp
11743 .set_request_handler::<lsp::request::Formatting, _, _>({
11744 let buffer_changes = buffer_changes.clone();
11745 move |_, _| {
11746 // When formatting is requested, trailing whitespace has already been stripped,
11747 // and the trailing newline has already been added.
11748 assert_eq!(
11749 &buffer_changes.lock()[1..],
11750 &[
11751 (
11752 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
11753 "".into()
11754 ),
11755 (
11756 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
11757 "".into()
11758 ),
11759 (
11760 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
11761 "\n".into()
11762 ),
11763 ]
11764 );
11765
11766 // Insert blank lines between each line of the buffer.
11767 async move {
11768 Ok(Some(vec![
11769 lsp::TextEdit {
11770 range: lsp::Range::new(
11771 lsp::Position::new(1, 0),
11772 lsp::Position::new(1, 0),
11773 ),
11774 new_text: "\n".into(),
11775 },
11776 lsp::TextEdit {
11777 range: lsp::Range::new(
11778 lsp::Position::new(2, 0),
11779 lsp::Position::new(2, 0),
11780 ),
11781 new_text: "\n".into(),
11782 },
11783 ]))
11784 }
11785 }
11786 });
11787
11788 // After formatting the buffer, the trailing whitespace is stripped,
11789 // a newline is appended, and the edits provided by the language server
11790 // have been applied.
11791 format.await.unwrap();
11792 cx.assert_editor_state(
11793 &[
11794 "one", //
11795 "", //
11796 "twoˇ", //
11797 "", //
11798 "three", //
11799 "four", //
11800 "", //
11801 ]
11802 .join("\n"),
11803 );
11804
11805 // Undoing the formatting undoes the trailing whitespace removal, the
11806 // trailing newline, and the LSP edits.
11807 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11808 cx.assert_editor_state(
11809 &[
11810 "one ", //
11811 "twoˇ", //
11812 "three ", //
11813 "four", //
11814 ]
11815 .join("\n"),
11816 );
11817}
11818
11819#[gpui::test]
11820async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11821 cx: &mut TestAppContext,
11822) {
11823 init_test(cx, |_| {});
11824
11825 cx.update(|cx| {
11826 cx.update_global::<SettingsStore, _>(|settings, cx| {
11827 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11828 settings.auto_signature_help = Some(true);
11829 });
11830 });
11831 });
11832
11833 let mut cx = EditorLspTestContext::new_rust(
11834 lsp::ServerCapabilities {
11835 signature_help_provider: Some(lsp::SignatureHelpOptions {
11836 ..Default::default()
11837 }),
11838 ..Default::default()
11839 },
11840 cx,
11841 )
11842 .await;
11843
11844 let language = Language::new(
11845 LanguageConfig {
11846 name: "Rust".into(),
11847 brackets: BracketPairConfig {
11848 pairs: vec![
11849 BracketPair {
11850 start: "{".to_string(),
11851 end: "}".to_string(),
11852 close: true,
11853 surround: true,
11854 newline: true,
11855 },
11856 BracketPair {
11857 start: "(".to_string(),
11858 end: ")".to_string(),
11859 close: true,
11860 surround: true,
11861 newline: true,
11862 },
11863 BracketPair {
11864 start: "/*".to_string(),
11865 end: " */".to_string(),
11866 close: true,
11867 surround: true,
11868 newline: true,
11869 },
11870 BracketPair {
11871 start: "[".to_string(),
11872 end: "]".to_string(),
11873 close: false,
11874 surround: false,
11875 newline: true,
11876 },
11877 BracketPair {
11878 start: "\"".to_string(),
11879 end: "\"".to_string(),
11880 close: true,
11881 surround: true,
11882 newline: false,
11883 },
11884 BracketPair {
11885 start: "<".to_string(),
11886 end: ">".to_string(),
11887 close: false,
11888 surround: true,
11889 newline: true,
11890 },
11891 ],
11892 ..Default::default()
11893 },
11894 autoclose_before: "})]".to_string(),
11895 ..Default::default()
11896 },
11897 Some(tree_sitter_rust::LANGUAGE.into()),
11898 );
11899 let language = Arc::new(language);
11900
11901 cx.language_registry().add(language.clone());
11902 cx.update_buffer(|buffer, cx| {
11903 buffer.set_language(Some(language), cx);
11904 });
11905
11906 cx.set_state(
11907 &r#"
11908 fn main() {
11909 sampleˇ
11910 }
11911 "#
11912 .unindent(),
11913 );
11914
11915 cx.update_editor(|editor, window, cx| {
11916 editor.handle_input("(", window, cx);
11917 });
11918 cx.assert_editor_state(
11919 &"
11920 fn main() {
11921 sample(ˇ)
11922 }
11923 "
11924 .unindent(),
11925 );
11926
11927 let mocked_response = lsp::SignatureHelp {
11928 signatures: vec![lsp::SignatureInformation {
11929 label: "fn sample(param1: u8, param2: u8)".to_string(),
11930 documentation: None,
11931 parameters: Some(vec![
11932 lsp::ParameterInformation {
11933 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11934 documentation: None,
11935 },
11936 lsp::ParameterInformation {
11937 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11938 documentation: None,
11939 },
11940 ]),
11941 active_parameter: None,
11942 }],
11943 active_signature: Some(0),
11944 active_parameter: Some(0),
11945 };
11946 handle_signature_help_request(&mut cx, mocked_response).await;
11947
11948 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11949 .await;
11950
11951 cx.editor(|editor, _, _| {
11952 let signature_help_state = editor.signature_help_state.popover().cloned();
11953 let signature = signature_help_state.unwrap();
11954 assert_eq!(
11955 signature.signatures[signature.current_signature].label,
11956 "fn sample(param1: u8, param2: u8)"
11957 );
11958 });
11959}
11960
11961#[gpui::test]
11962async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11963 init_test(cx, |_| {});
11964
11965 cx.update(|cx| {
11966 cx.update_global::<SettingsStore, _>(|settings, cx| {
11967 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11968 settings.auto_signature_help = Some(false);
11969 settings.show_signature_help_after_edits = Some(false);
11970 });
11971 });
11972 });
11973
11974 let mut cx = EditorLspTestContext::new_rust(
11975 lsp::ServerCapabilities {
11976 signature_help_provider: Some(lsp::SignatureHelpOptions {
11977 ..Default::default()
11978 }),
11979 ..Default::default()
11980 },
11981 cx,
11982 )
11983 .await;
11984
11985 let language = Language::new(
11986 LanguageConfig {
11987 name: "Rust".into(),
11988 brackets: BracketPairConfig {
11989 pairs: vec![
11990 BracketPair {
11991 start: "{".to_string(),
11992 end: "}".to_string(),
11993 close: true,
11994 surround: true,
11995 newline: true,
11996 },
11997 BracketPair {
11998 start: "(".to_string(),
11999 end: ")".to_string(),
12000 close: true,
12001 surround: true,
12002 newline: true,
12003 },
12004 BracketPair {
12005 start: "/*".to_string(),
12006 end: " */".to_string(),
12007 close: true,
12008 surround: true,
12009 newline: true,
12010 },
12011 BracketPair {
12012 start: "[".to_string(),
12013 end: "]".to_string(),
12014 close: false,
12015 surround: false,
12016 newline: true,
12017 },
12018 BracketPair {
12019 start: "\"".to_string(),
12020 end: "\"".to_string(),
12021 close: true,
12022 surround: true,
12023 newline: false,
12024 },
12025 BracketPair {
12026 start: "<".to_string(),
12027 end: ">".to_string(),
12028 close: false,
12029 surround: true,
12030 newline: true,
12031 },
12032 ],
12033 ..Default::default()
12034 },
12035 autoclose_before: "})]".to_string(),
12036 ..Default::default()
12037 },
12038 Some(tree_sitter_rust::LANGUAGE.into()),
12039 );
12040 let language = Arc::new(language);
12041
12042 cx.language_registry().add(language.clone());
12043 cx.update_buffer(|buffer, cx| {
12044 buffer.set_language(Some(language), cx);
12045 });
12046
12047 // Ensure that signature_help is not called when no signature help is enabled.
12048 cx.set_state(
12049 &r#"
12050 fn main() {
12051 sampleˇ
12052 }
12053 "#
12054 .unindent(),
12055 );
12056 cx.update_editor(|editor, window, cx| {
12057 editor.handle_input("(", window, cx);
12058 });
12059 cx.assert_editor_state(
12060 &"
12061 fn main() {
12062 sample(ˇ)
12063 }
12064 "
12065 .unindent(),
12066 );
12067 cx.editor(|editor, _, _| {
12068 assert!(editor.signature_help_state.task().is_none());
12069 });
12070
12071 let mocked_response = lsp::SignatureHelp {
12072 signatures: vec![lsp::SignatureInformation {
12073 label: "fn sample(param1: u8, param2: u8)".to_string(),
12074 documentation: None,
12075 parameters: Some(vec![
12076 lsp::ParameterInformation {
12077 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12078 documentation: None,
12079 },
12080 lsp::ParameterInformation {
12081 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12082 documentation: None,
12083 },
12084 ]),
12085 active_parameter: None,
12086 }],
12087 active_signature: Some(0),
12088 active_parameter: Some(0),
12089 };
12090
12091 // Ensure that signature_help is called when enabled afte edits
12092 cx.update(|_, cx| {
12093 cx.update_global::<SettingsStore, _>(|settings, cx| {
12094 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12095 settings.auto_signature_help = Some(false);
12096 settings.show_signature_help_after_edits = Some(true);
12097 });
12098 });
12099 });
12100 cx.set_state(
12101 &r#"
12102 fn main() {
12103 sampleˇ
12104 }
12105 "#
12106 .unindent(),
12107 );
12108 cx.update_editor(|editor, window, cx| {
12109 editor.handle_input("(", window, cx);
12110 });
12111 cx.assert_editor_state(
12112 &"
12113 fn main() {
12114 sample(ˇ)
12115 }
12116 "
12117 .unindent(),
12118 );
12119 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12120 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12121 .await;
12122 cx.update_editor(|editor, _, _| {
12123 let signature_help_state = editor.signature_help_state.popover().cloned();
12124 assert!(signature_help_state.is_some());
12125 let signature = signature_help_state.unwrap();
12126 assert_eq!(
12127 signature.signatures[signature.current_signature].label,
12128 "fn sample(param1: u8, param2: u8)"
12129 );
12130 editor.signature_help_state = SignatureHelpState::default();
12131 });
12132
12133 // Ensure that signature_help is called when auto signature help override is enabled
12134 cx.update(|_, cx| {
12135 cx.update_global::<SettingsStore, _>(|settings, cx| {
12136 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12137 settings.auto_signature_help = Some(true);
12138 settings.show_signature_help_after_edits = Some(false);
12139 });
12140 });
12141 });
12142 cx.set_state(
12143 &r#"
12144 fn main() {
12145 sampleˇ
12146 }
12147 "#
12148 .unindent(),
12149 );
12150 cx.update_editor(|editor, window, cx| {
12151 editor.handle_input("(", window, cx);
12152 });
12153 cx.assert_editor_state(
12154 &"
12155 fn main() {
12156 sample(ˇ)
12157 }
12158 "
12159 .unindent(),
12160 );
12161 handle_signature_help_request(&mut cx, mocked_response).await;
12162 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12163 .await;
12164 cx.editor(|editor, _, _| {
12165 let signature_help_state = editor.signature_help_state.popover().cloned();
12166 assert!(signature_help_state.is_some());
12167 let signature = signature_help_state.unwrap();
12168 assert_eq!(
12169 signature.signatures[signature.current_signature].label,
12170 "fn sample(param1: u8, param2: u8)"
12171 );
12172 });
12173}
12174
12175#[gpui::test]
12176async fn test_signature_help(cx: &mut TestAppContext) {
12177 init_test(cx, |_| {});
12178 cx.update(|cx| {
12179 cx.update_global::<SettingsStore, _>(|settings, cx| {
12180 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12181 settings.auto_signature_help = Some(true);
12182 });
12183 });
12184 });
12185
12186 let mut cx = EditorLspTestContext::new_rust(
12187 lsp::ServerCapabilities {
12188 signature_help_provider: Some(lsp::SignatureHelpOptions {
12189 ..Default::default()
12190 }),
12191 ..Default::default()
12192 },
12193 cx,
12194 )
12195 .await;
12196
12197 // A test that directly calls `show_signature_help`
12198 cx.update_editor(|editor, window, cx| {
12199 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12200 });
12201
12202 let mocked_response = lsp::SignatureHelp {
12203 signatures: vec![lsp::SignatureInformation {
12204 label: "fn sample(param1: u8, param2: u8)".to_string(),
12205 documentation: None,
12206 parameters: Some(vec![
12207 lsp::ParameterInformation {
12208 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12209 documentation: None,
12210 },
12211 lsp::ParameterInformation {
12212 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12213 documentation: None,
12214 },
12215 ]),
12216 active_parameter: None,
12217 }],
12218 active_signature: Some(0),
12219 active_parameter: Some(0),
12220 };
12221 handle_signature_help_request(&mut cx, mocked_response).await;
12222
12223 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12224 .await;
12225
12226 cx.editor(|editor, _, _| {
12227 let signature_help_state = editor.signature_help_state.popover().cloned();
12228 assert!(signature_help_state.is_some());
12229 let signature = signature_help_state.unwrap();
12230 assert_eq!(
12231 signature.signatures[signature.current_signature].label,
12232 "fn sample(param1: u8, param2: u8)"
12233 );
12234 });
12235
12236 // When exiting outside from inside the brackets, `signature_help` is closed.
12237 cx.set_state(indoc! {"
12238 fn main() {
12239 sample(ˇ);
12240 }
12241
12242 fn sample(param1: u8, param2: u8) {}
12243 "});
12244
12245 cx.update_editor(|editor, window, cx| {
12246 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12247 s.select_ranges([0..0])
12248 });
12249 });
12250
12251 let mocked_response = lsp::SignatureHelp {
12252 signatures: Vec::new(),
12253 active_signature: None,
12254 active_parameter: None,
12255 };
12256 handle_signature_help_request(&mut cx, mocked_response).await;
12257
12258 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12259 .await;
12260
12261 cx.editor(|editor, _, _| {
12262 assert!(!editor.signature_help_state.is_shown());
12263 });
12264
12265 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12266 cx.set_state(indoc! {"
12267 fn main() {
12268 sample(ˇ);
12269 }
12270
12271 fn sample(param1: u8, param2: u8) {}
12272 "});
12273
12274 let mocked_response = lsp::SignatureHelp {
12275 signatures: vec![lsp::SignatureInformation {
12276 label: "fn sample(param1: u8, param2: u8)".to_string(),
12277 documentation: None,
12278 parameters: Some(vec![
12279 lsp::ParameterInformation {
12280 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12281 documentation: None,
12282 },
12283 lsp::ParameterInformation {
12284 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12285 documentation: None,
12286 },
12287 ]),
12288 active_parameter: None,
12289 }],
12290 active_signature: Some(0),
12291 active_parameter: Some(0),
12292 };
12293 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12294 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12295 .await;
12296 cx.editor(|editor, _, _| {
12297 assert!(editor.signature_help_state.is_shown());
12298 });
12299
12300 // Restore the popover with more parameter input
12301 cx.set_state(indoc! {"
12302 fn main() {
12303 sample(param1, param2ˇ);
12304 }
12305
12306 fn sample(param1: u8, param2: u8) {}
12307 "});
12308
12309 let mocked_response = lsp::SignatureHelp {
12310 signatures: vec![lsp::SignatureInformation {
12311 label: "fn sample(param1: u8, param2: u8)".to_string(),
12312 documentation: None,
12313 parameters: Some(vec![
12314 lsp::ParameterInformation {
12315 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12316 documentation: None,
12317 },
12318 lsp::ParameterInformation {
12319 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12320 documentation: None,
12321 },
12322 ]),
12323 active_parameter: None,
12324 }],
12325 active_signature: Some(0),
12326 active_parameter: Some(1),
12327 };
12328 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12329 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12330 .await;
12331
12332 // When selecting a range, the popover is gone.
12333 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12334 cx.update_editor(|editor, window, cx| {
12335 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12336 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12337 })
12338 });
12339 cx.assert_editor_state(indoc! {"
12340 fn main() {
12341 sample(param1, «ˇparam2»);
12342 }
12343
12344 fn sample(param1: u8, param2: u8) {}
12345 "});
12346 cx.editor(|editor, _, _| {
12347 assert!(!editor.signature_help_state.is_shown());
12348 });
12349
12350 // When unselecting again, the popover is back if within the brackets.
12351 cx.update_editor(|editor, window, cx| {
12352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12353 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12354 })
12355 });
12356 cx.assert_editor_state(indoc! {"
12357 fn main() {
12358 sample(param1, ˇparam2);
12359 }
12360
12361 fn sample(param1: u8, param2: u8) {}
12362 "});
12363 handle_signature_help_request(&mut cx, mocked_response).await;
12364 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12365 .await;
12366 cx.editor(|editor, _, _| {
12367 assert!(editor.signature_help_state.is_shown());
12368 });
12369
12370 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12371 cx.update_editor(|editor, window, cx| {
12372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12373 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12374 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12375 })
12376 });
12377 cx.assert_editor_state(indoc! {"
12378 fn main() {
12379 sample(param1, ˇparam2);
12380 }
12381
12382 fn sample(param1: u8, param2: u8) {}
12383 "});
12384
12385 let mocked_response = lsp::SignatureHelp {
12386 signatures: vec![lsp::SignatureInformation {
12387 label: "fn sample(param1: u8, param2: u8)".to_string(),
12388 documentation: None,
12389 parameters: Some(vec![
12390 lsp::ParameterInformation {
12391 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12392 documentation: None,
12393 },
12394 lsp::ParameterInformation {
12395 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12396 documentation: None,
12397 },
12398 ]),
12399 active_parameter: None,
12400 }],
12401 active_signature: Some(0),
12402 active_parameter: Some(1),
12403 };
12404 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12405 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12406 .await;
12407 cx.update_editor(|editor, _, cx| {
12408 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12409 });
12410 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12411 .await;
12412 cx.update_editor(|editor, window, cx| {
12413 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12414 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12415 })
12416 });
12417 cx.assert_editor_state(indoc! {"
12418 fn main() {
12419 sample(param1, «ˇparam2»);
12420 }
12421
12422 fn sample(param1: u8, param2: u8) {}
12423 "});
12424 cx.update_editor(|editor, window, cx| {
12425 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12426 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12427 })
12428 });
12429 cx.assert_editor_state(indoc! {"
12430 fn main() {
12431 sample(param1, ˇparam2);
12432 }
12433
12434 fn sample(param1: u8, param2: u8) {}
12435 "});
12436 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
12437 .await;
12438}
12439
12440#[gpui::test]
12441async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
12442 init_test(cx, |_| {});
12443
12444 let mut cx = EditorLspTestContext::new_rust(
12445 lsp::ServerCapabilities {
12446 signature_help_provider: Some(lsp::SignatureHelpOptions {
12447 ..Default::default()
12448 }),
12449 ..Default::default()
12450 },
12451 cx,
12452 )
12453 .await;
12454
12455 cx.set_state(indoc! {"
12456 fn main() {
12457 overloadedˇ
12458 }
12459 "});
12460
12461 cx.update_editor(|editor, window, cx| {
12462 editor.handle_input("(", window, cx);
12463 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12464 });
12465
12466 // Mock response with 3 signatures
12467 let mocked_response = lsp::SignatureHelp {
12468 signatures: vec![
12469 lsp::SignatureInformation {
12470 label: "fn overloaded(x: i32)".to_string(),
12471 documentation: None,
12472 parameters: Some(vec![lsp::ParameterInformation {
12473 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12474 documentation: None,
12475 }]),
12476 active_parameter: None,
12477 },
12478 lsp::SignatureInformation {
12479 label: "fn overloaded(x: i32, y: i32)".to_string(),
12480 documentation: None,
12481 parameters: Some(vec![
12482 lsp::ParameterInformation {
12483 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12484 documentation: None,
12485 },
12486 lsp::ParameterInformation {
12487 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12488 documentation: None,
12489 },
12490 ]),
12491 active_parameter: None,
12492 },
12493 lsp::SignatureInformation {
12494 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
12495 documentation: None,
12496 parameters: Some(vec![
12497 lsp::ParameterInformation {
12498 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12499 documentation: None,
12500 },
12501 lsp::ParameterInformation {
12502 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12503 documentation: None,
12504 },
12505 lsp::ParameterInformation {
12506 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
12507 documentation: None,
12508 },
12509 ]),
12510 active_parameter: None,
12511 },
12512 ],
12513 active_signature: Some(1),
12514 active_parameter: Some(0),
12515 };
12516 handle_signature_help_request(&mut cx, mocked_response).await;
12517
12518 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12519 .await;
12520
12521 // Verify we have multiple signatures and the right one is selected
12522 cx.editor(|editor, _, _| {
12523 let popover = editor.signature_help_state.popover().cloned().unwrap();
12524 assert_eq!(popover.signatures.len(), 3);
12525 // active_signature was 1, so that should be the current
12526 assert_eq!(popover.current_signature, 1);
12527 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
12528 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
12529 assert_eq!(
12530 popover.signatures[2].label,
12531 "fn overloaded(x: i32, y: i32, z: i32)"
12532 );
12533 });
12534
12535 // Test navigation functionality
12536 cx.update_editor(|editor, window, cx| {
12537 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12538 });
12539
12540 cx.editor(|editor, _, _| {
12541 let popover = editor.signature_help_state.popover().cloned().unwrap();
12542 assert_eq!(popover.current_signature, 2);
12543 });
12544
12545 // Test wrap around
12546 cx.update_editor(|editor, window, cx| {
12547 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12548 });
12549
12550 cx.editor(|editor, _, _| {
12551 let popover = editor.signature_help_state.popover().cloned().unwrap();
12552 assert_eq!(popover.current_signature, 0);
12553 });
12554
12555 // Test previous navigation
12556 cx.update_editor(|editor, window, cx| {
12557 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12558 });
12559
12560 cx.editor(|editor, _, _| {
12561 let popover = editor.signature_help_state.popover().cloned().unwrap();
12562 assert_eq!(popover.current_signature, 2);
12563 });
12564}
12565
12566#[gpui::test]
12567async fn test_completion_mode(cx: &mut TestAppContext) {
12568 init_test(cx, |_| {});
12569 let mut cx = EditorLspTestContext::new_rust(
12570 lsp::ServerCapabilities {
12571 completion_provider: Some(lsp::CompletionOptions {
12572 resolve_provider: Some(true),
12573 ..Default::default()
12574 }),
12575 ..Default::default()
12576 },
12577 cx,
12578 )
12579 .await;
12580
12581 struct Run {
12582 run_description: &'static str,
12583 initial_state: String,
12584 buffer_marked_text: String,
12585 completion_label: &'static str,
12586 completion_text: &'static str,
12587 expected_with_insert_mode: String,
12588 expected_with_replace_mode: String,
12589 expected_with_replace_subsequence_mode: String,
12590 expected_with_replace_suffix_mode: String,
12591 }
12592
12593 let runs = [
12594 Run {
12595 run_description: "Start of word matches completion text",
12596 initial_state: "before ediˇ after".into(),
12597 buffer_marked_text: "before <edi|> after".into(),
12598 completion_label: "editor",
12599 completion_text: "editor",
12600 expected_with_insert_mode: "before editorˇ after".into(),
12601 expected_with_replace_mode: "before editorˇ after".into(),
12602 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12603 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12604 },
12605 Run {
12606 run_description: "Accept same text at the middle of the word",
12607 initial_state: "before ediˇtor after".into(),
12608 buffer_marked_text: "before <edi|tor> after".into(),
12609 completion_label: "editor",
12610 completion_text: "editor",
12611 expected_with_insert_mode: "before editorˇtor after".into(),
12612 expected_with_replace_mode: "before editorˇ after".into(),
12613 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12614 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12615 },
12616 Run {
12617 run_description: "End of word matches completion text -- cursor at end",
12618 initial_state: "before torˇ after".into(),
12619 buffer_marked_text: "before <tor|> after".into(),
12620 completion_label: "editor",
12621 completion_text: "editor",
12622 expected_with_insert_mode: "before editorˇ after".into(),
12623 expected_with_replace_mode: "before editorˇ after".into(),
12624 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12625 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12626 },
12627 Run {
12628 run_description: "End of word matches completion text -- cursor at start",
12629 initial_state: "before ˇtor after".into(),
12630 buffer_marked_text: "before <|tor> after".into(),
12631 completion_label: "editor",
12632 completion_text: "editor",
12633 expected_with_insert_mode: "before editorˇtor after".into(),
12634 expected_with_replace_mode: "before editorˇ after".into(),
12635 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12636 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12637 },
12638 Run {
12639 run_description: "Prepend text containing whitespace",
12640 initial_state: "pˇfield: bool".into(),
12641 buffer_marked_text: "<p|field>: bool".into(),
12642 completion_label: "pub ",
12643 completion_text: "pub ",
12644 expected_with_insert_mode: "pub ˇfield: bool".into(),
12645 expected_with_replace_mode: "pub ˇ: bool".into(),
12646 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12647 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12648 },
12649 Run {
12650 run_description: "Add element to start of list",
12651 initial_state: "[element_ˇelement_2]".into(),
12652 buffer_marked_text: "[<element_|element_2>]".into(),
12653 completion_label: "element_1",
12654 completion_text: "element_1",
12655 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
12656 expected_with_replace_mode: "[element_1ˇ]".into(),
12657 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
12658 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
12659 },
12660 Run {
12661 run_description: "Add element to start of list -- first and second elements are equal",
12662 initial_state: "[elˇelement]".into(),
12663 buffer_marked_text: "[<el|element>]".into(),
12664 completion_label: "element",
12665 completion_text: "element",
12666 expected_with_insert_mode: "[elementˇelement]".into(),
12667 expected_with_replace_mode: "[elementˇ]".into(),
12668 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
12669 expected_with_replace_suffix_mode: "[elementˇ]".into(),
12670 },
12671 Run {
12672 run_description: "Ends with matching suffix",
12673 initial_state: "SubˇError".into(),
12674 buffer_marked_text: "<Sub|Error>".into(),
12675 completion_label: "SubscriptionError",
12676 completion_text: "SubscriptionError",
12677 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
12678 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12679 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12680 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
12681 },
12682 Run {
12683 run_description: "Suffix is a subsequence -- contiguous",
12684 initial_state: "SubˇErr".into(),
12685 buffer_marked_text: "<Sub|Err>".into(),
12686 completion_label: "SubscriptionError",
12687 completion_text: "SubscriptionError",
12688 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
12689 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12690 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12691 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
12692 },
12693 Run {
12694 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
12695 initial_state: "Suˇscrirr".into(),
12696 buffer_marked_text: "<Su|scrirr>".into(),
12697 completion_label: "SubscriptionError",
12698 completion_text: "SubscriptionError",
12699 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
12700 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12701 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12702 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
12703 },
12704 Run {
12705 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
12706 initial_state: "foo(indˇix)".into(),
12707 buffer_marked_text: "foo(<ind|ix>)".into(),
12708 completion_label: "node_index",
12709 completion_text: "node_index",
12710 expected_with_insert_mode: "foo(node_indexˇix)".into(),
12711 expected_with_replace_mode: "foo(node_indexˇ)".into(),
12712 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
12713 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
12714 },
12715 Run {
12716 run_description: "Replace range ends before cursor - should extend to cursor",
12717 initial_state: "before editˇo after".into(),
12718 buffer_marked_text: "before <{ed}>it|o after".into(),
12719 completion_label: "editor",
12720 completion_text: "editor",
12721 expected_with_insert_mode: "before editorˇo after".into(),
12722 expected_with_replace_mode: "before editorˇo after".into(),
12723 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
12724 expected_with_replace_suffix_mode: "before editorˇo after".into(),
12725 },
12726 Run {
12727 run_description: "Uses label for suffix matching",
12728 initial_state: "before ediˇtor after".into(),
12729 buffer_marked_text: "before <edi|tor> after".into(),
12730 completion_label: "editor",
12731 completion_text: "editor()",
12732 expected_with_insert_mode: "before editor()ˇtor after".into(),
12733 expected_with_replace_mode: "before editor()ˇ after".into(),
12734 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
12735 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
12736 },
12737 Run {
12738 run_description: "Case insensitive subsequence and suffix matching",
12739 initial_state: "before EDiˇtoR after".into(),
12740 buffer_marked_text: "before <EDi|toR> after".into(),
12741 completion_label: "editor",
12742 completion_text: "editor",
12743 expected_with_insert_mode: "before editorˇtoR after".into(),
12744 expected_with_replace_mode: "before editorˇ after".into(),
12745 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12746 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12747 },
12748 ];
12749
12750 for run in runs {
12751 let run_variations = [
12752 (LspInsertMode::Insert, run.expected_with_insert_mode),
12753 (LspInsertMode::Replace, run.expected_with_replace_mode),
12754 (
12755 LspInsertMode::ReplaceSubsequence,
12756 run.expected_with_replace_subsequence_mode,
12757 ),
12758 (
12759 LspInsertMode::ReplaceSuffix,
12760 run.expected_with_replace_suffix_mode,
12761 ),
12762 ];
12763
12764 for (lsp_insert_mode, expected_text) in run_variations {
12765 eprintln!(
12766 "run = {:?}, mode = {lsp_insert_mode:.?}",
12767 run.run_description,
12768 );
12769
12770 update_test_language_settings(&mut cx, |settings| {
12771 settings.defaults.completions = Some(CompletionSettings {
12772 lsp_insert_mode,
12773 words: WordsCompletionMode::Disabled,
12774 words_min_length: 0,
12775 lsp: true,
12776 lsp_fetch_timeout_ms: 0,
12777 });
12778 });
12779
12780 cx.set_state(&run.initial_state);
12781 cx.update_editor(|editor, window, cx| {
12782 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12783 });
12784
12785 let counter = Arc::new(AtomicUsize::new(0));
12786 handle_completion_request_with_insert_and_replace(
12787 &mut cx,
12788 &run.buffer_marked_text,
12789 vec![(run.completion_label, run.completion_text)],
12790 counter.clone(),
12791 )
12792 .await;
12793 cx.condition(|editor, _| editor.context_menu_visible())
12794 .await;
12795 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12796
12797 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12798 editor
12799 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12800 .unwrap()
12801 });
12802 cx.assert_editor_state(&expected_text);
12803 handle_resolve_completion_request(&mut cx, None).await;
12804 apply_additional_edits.await.unwrap();
12805 }
12806 }
12807}
12808
12809#[gpui::test]
12810async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12811 init_test(cx, |_| {});
12812 let mut cx = EditorLspTestContext::new_rust(
12813 lsp::ServerCapabilities {
12814 completion_provider: Some(lsp::CompletionOptions {
12815 resolve_provider: Some(true),
12816 ..Default::default()
12817 }),
12818 ..Default::default()
12819 },
12820 cx,
12821 )
12822 .await;
12823
12824 let initial_state = "SubˇError";
12825 let buffer_marked_text = "<Sub|Error>";
12826 let completion_text = "SubscriptionError";
12827 let expected_with_insert_mode = "SubscriptionErrorˇError";
12828 let expected_with_replace_mode = "SubscriptionErrorˇ";
12829
12830 update_test_language_settings(&mut cx, |settings| {
12831 settings.defaults.completions = Some(CompletionSettings {
12832 words: WordsCompletionMode::Disabled,
12833 words_min_length: 0,
12834 // set the opposite here to ensure that the action is overriding the default behavior
12835 lsp_insert_mode: LspInsertMode::Insert,
12836 lsp: true,
12837 lsp_fetch_timeout_ms: 0,
12838 });
12839 });
12840
12841 cx.set_state(initial_state);
12842 cx.update_editor(|editor, window, cx| {
12843 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12844 });
12845
12846 let counter = Arc::new(AtomicUsize::new(0));
12847 handle_completion_request_with_insert_and_replace(
12848 &mut cx,
12849 buffer_marked_text,
12850 vec![(completion_text, completion_text)],
12851 counter.clone(),
12852 )
12853 .await;
12854 cx.condition(|editor, _| editor.context_menu_visible())
12855 .await;
12856 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12857
12858 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12859 editor
12860 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12861 .unwrap()
12862 });
12863 cx.assert_editor_state(expected_with_replace_mode);
12864 handle_resolve_completion_request(&mut cx, None).await;
12865 apply_additional_edits.await.unwrap();
12866
12867 update_test_language_settings(&mut cx, |settings| {
12868 settings.defaults.completions = Some(CompletionSettings {
12869 words: WordsCompletionMode::Disabled,
12870 words_min_length: 0,
12871 // set the opposite here to ensure that the action is overriding the default behavior
12872 lsp_insert_mode: LspInsertMode::Replace,
12873 lsp: true,
12874 lsp_fetch_timeout_ms: 0,
12875 });
12876 });
12877
12878 cx.set_state(initial_state);
12879 cx.update_editor(|editor, window, cx| {
12880 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12881 });
12882 handle_completion_request_with_insert_and_replace(
12883 &mut cx,
12884 buffer_marked_text,
12885 vec![(completion_text, completion_text)],
12886 counter.clone(),
12887 )
12888 .await;
12889 cx.condition(|editor, _| editor.context_menu_visible())
12890 .await;
12891 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12892
12893 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12894 editor
12895 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12896 .unwrap()
12897 });
12898 cx.assert_editor_state(expected_with_insert_mode);
12899 handle_resolve_completion_request(&mut cx, None).await;
12900 apply_additional_edits.await.unwrap();
12901}
12902
12903#[gpui::test]
12904async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12905 init_test(cx, |_| {});
12906 let mut cx = EditorLspTestContext::new_rust(
12907 lsp::ServerCapabilities {
12908 completion_provider: Some(lsp::CompletionOptions {
12909 resolve_provider: Some(true),
12910 ..Default::default()
12911 }),
12912 ..Default::default()
12913 },
12914 cx,
12915 )
12916 .await;
12917
12918 // scenario: surrounding text matches completion text
12919 let completion_text = "to_offset";
12920 let initial_state = indoc! {"
12921 1. buf.to_offˇsuffix
12922 2. buf.to_offˇsuf
12923 3. buf.to_offˇfix
12924 4. buf.to_offˇ
12925 5. into_offˇensive
12926 6. ˇsuffix
12927 7. let ˇ //
12928 8. aaˇzz
12929 9. buf.to_off«zzzzzˇ»suffix
12930 10. buf.«ˇzzzzz»suffix
12931 11. to_off«ˇzzzzz»
12932
12933 buf.to_offˇsuffix // newest cursor
12934 "};
12935 let completion_marked_buffer = indoc! {"
12936 1. buf.to_offsuffix
12937 2. buf.to_offsuf
12938 3. buf.to_offfix
12939 4. buf.to_off
12940 5. into_offensive
12941 6. suffix
12942 7. let //
12943 8. aazz
12944 9. buf.to_offzzzzzsuffix
12945 10. buf.zzzzzsuffix
12946 11. to_offzzzzz
12947
12948 buf.<to_off|suffix> // newest cursor
12949 "};
12950 let expected = indoc! {"
12951 1. buf.to_offsetˇ
12952 2. buf.to_offsetˇsuf
12953 3. buf.to_offsetˇfix
12954 4. buf.to_offsetˇ
12955 5. into_offsetˇensive
12956 6. to_offsetˇsuffix
12957 7. let to_offsetˇ //
12958 8. aato_offsetˇzz
12959 9. buf.to_offsetˇ
12960 10. buf.to_offsetˇsuffix
12961 11. to_offsetˇ
12962
12963 buf.to_offsetˇ // newest cursor
12964 "};
12965 cx.set_state(initial_state);
12966 cx.update_editor(|editor, window, cx| {
12967 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12968 });
12969 handle_completion_request_with_insert_and_replace(
12970 &mut cx,
12971 completion_marked_buffer,
12972 vec![(completion_text, completion_text)],
12973 Arc::new(AtomicUsize::new(0)),
12974 )
12975 .await;
12976 cx.condition(|editor, _| editor.context_menu_visible())
12977 .await;
12978 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12979 editor
12980 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12981 .unwrap()
12982 });
12983 cx.assert_editor_state(expected);
12984 handle_resolve_completion_request(&mut cx, None).await;
12985 apply_additional_edits.await.unwrap();
12986
12987 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12988 let completion_text = "foo_and_bar";
12989 let initial_state = indoc! {"
12990 1. ooanbˇ
12991 2. zooanbˇ
12992 3. ooanbˇz
12993 4. zooanbˇz
12994 5. ooanˇ
12995 6. oanbˇ
12996
12997 ooanbˇ
12998 "};
12999 let completion_marked_buffer = indoc! {"
13000 1. ooanb
13001 2. zooanb
13002 3. ooanbz
13003 4. zooanbz
13004 5. ooan
13005 6. oanb
13006
13007 <ooanb|>
13008 "};
13009 let expected = indoc! {"
13010 1. foo_and_barˇ
13011 2. zfoo_and_barˇ
13012 3. foo_and_barˇz
13013 4. zfoo_and_barˇz
13014 5. ooanfoo_and_barˇ
13015 6. oanbfoo_and_barˇ
13016
13017 foo_and_barˇ
13018 "};
13019 cx.set_state(initial_state);
13020 cx.update_editor(|editor, window, cx| {
13021 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13022 });
13023 handle_completion_request_with_insert_and_replace(
13024 &mut cx,
13025 completion_marked_buffer,
13026 vec![(completion_text, completion_text)],
13027 Arc::new(AtomicUsize::new(0)),
13028 )
13029 .await;
13030 cx.condition(|editor, _| editor.context_menu_visible())
13031 .await;
13032 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13033 editor
13034 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13035 .unwrap()
13036 });
13037 cx.assert_editor_state(expected);
13038 handle_resolve_completion_request(&mut cx, None).await;
13039 apply_additional_edits.await.unwrap();
13040
13041 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13042 // (expects the same as if it was inserted at the end)
13043 let completion_text = "foo_and_bar";
13044 let initial_state = indoc! {"
13045 1. ooˇanb
13046 2. zooˇanb
13047 3. ooˇanbz
13048 4. zooˇanbz
13049
13050 ooˇanb
13051 "};
13052 let completion_marked_buffer = indoc! {"
13053 1. ooanb
13054 2. zooanb
13055 3. ooanbz
13056 4. zooanbz
13057
13058 <oo|anb>
13059 "};
13060 let expected = indoc! {"
13061 1. foo_and_barˇ
13062 2. zfoo_and_barˇ
13063 3. foo_and_barˇz
13064 4. zfoo_and_barˇz
13065
13066 foo_and_barˇ
13067 "};
13068 cx.set_state(initial_state);
13069 cx.update_editor(|editor, window, cx| {
13070 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13071 });
13072 handle_completion_request_with_insert_and_replace(
13073 &mut cx,
13074 completion_marked_buffer,
13075 vec![(completion_text, completion_text)],
13076 Arc::new(AtomicUsize::new(0)),
13077 )
13078 .await;
13079 cx.condition(|editor, _| editor.context_menu_visible())
13080 .await;
13081 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13082 editor
13083 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13084 .unwrap()
13085 });
13086 cx.assert_editor_state(expected);
13087 handle_resolve_completion_request(&mut cx, None).await;
13088 apply_additional_edits.await.unwrap();
13089}
13090
13091// This used to crash
13092#[gpui::test]
13093async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13094 init_test(cx, |_| {});
13095
13096 let buffer_text = indoc! {"
13097 fn main() {
13098 10.satu;
13099
13100 //
13101 // separate cursors so they open in different excerpts (manually reproducible)
13102 //
13103
13104 10.satu20;
13105 }
13106 "};
13107 let multibuffer_text_with_selections = indoc! {"
13108 fn main() {
13109 10.satuˇ;
13110
13111 //
13112
13113 //
13114
13115 10.satuˇ20;
13116 }
13117 "};
13118 let expected_multibuffer = indoc! {"
13119 fn main() {
13120 10.saturating_sub()ˇ;
13121
13122 //
13123
13124 //
13125
13126 10.saturating_sub()ˇ;
13127 }
13128 "};
13129
13130 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13131 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13132
13133 let fs = FakeFs::new(cx.executor());
13134 fs.insert_tree(
13135 path!("/a"),
13136 json!({
13137 "main.rs": buffer_text,
13138 }),
13139 )
13140 .await;
13141
13142 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13143 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13144 language_registry.add(rust_lang());
13145 let mut fake_servers = language_registry.register_fake_lsp(
13146 "Rust",
13147 FakeLspAdapter {
13148 capabilities: lsp::ServerCapabilities {
13149 completion_provider: Some(lsp::CompletionOptions {
13150 resolve_provider: None,
13151 ..lsp::CompletionOptions::default()
13152 }),
13153 ..lsp::ServerCapabilities::default()
13154 },
13155 ..FakeLspAdapter::default()
13156 },
13157 );
13158 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13159 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13160 let buffer = project
13161 .update(cx, |project, cx| {
13162 project.open_local_buffer(path!("/a/main.rs"), cx)
13163 })
13164 .await
13165 .unwrap();
13166
13167 let multi_buffer = cx.new(|cx| {
13168 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13169 multi_buffer.push_excerpts(
13170 buffer.clone(),
13171 [ExcerptRange::new(0..first_excerpt_end)],
13172 cx,
13173 );
13174 multi_buffer.push_excerpts(
13175 buffer.clone(),
13176 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13177 cx,
13178 );
13179 multi_buffer
13180 });
13181
13182 let editor = workspace
13183 .update(cx, |_, window, cx| {
13184 cx.new(|cx| {
13185 Editor::new(
13186 EditorMode::Full {
13187 scale_ui_elements_with_buffer_font_size: false,
13188 show_active_line_background: false,
13189 sized_by_content: false,
13190 },
13191 multi_buffer.clone(),
13192 Some(project.clone()),
13193 window,
13194 cx,
13195 )
13196 })
13197 })
13198 .unwrap();
13199
13200 let pane = workspace
13201 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13202 .unwrap();
13203 pane.update_in(cx, |pane, window, cx| {
13204 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13205 });
13206
13207 let fake_server = fake_servers.next().await.unwrap();
13208
13209 editor.update_in(cx, |editor, window, cx| {
13210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13211 s.select_ranges([
13212 Point::new(1, 11)..Point::new(1, 11),
13213 Point::new(7, 11)..Point::new(7, 11),
13214 ])
13215 });
13216
13217 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13218 });
13219
13220 editor.update_in(cx, |editor, window, cx| {
13221 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13222 });
13223
13224 fake_server
13225 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13226 let completion_item = lsp::CompletionItem {
13227 label: "saturating_sub()".into(),
13228 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13229 lsp::InsertReplaceEdit {
13230 new_text: "saturating_sub()".to_owned(),
13231 insert: lsp::Range::new(
13232 lsp::Position::new(7, 7),
13233 lsp::Position::new(7, 11),
13234 ),
13235 replace: lsp::Range::new(
13236 lsp::Position::new(7, 7),
13237 lsp::Position::new(7, 13),
13238 ),
13239 },
13240 )),
13241 ..lsp::CompletionItem::default()
13242 };
13243
13244 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13245 })
13246 .next()
13247 .await
13248 .unwrap();
13249
13250 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13251 .await;
13252
13253 editor
13254 .update_in(cx, |editor, window, cx| {
13255 editor
13256 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13257 .unwrap()
13258 })
13259 .await
13260 .unwrap();
13261
13262 editor.update(cx, |editor, cx| {
13263 assert_text_with_selections(editor, expected_multibuffer, cx);
13264 })
13265}
13266
13267#[gpui::test]
13268async fn test_completion(cx: &mut TestAppContext) {
13269 init_test(cx, |_| {});
13270
13271 let mut cx = EditorLspTestContext::new_rust(
13272 lsp::ServerCapabilities {
13273 completion_provider: Some(lsp::CompletionOptions {
13274 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13275 resolve_provider: Some(true),
13276 ..Default::default()
13277 }),
13278 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13279 ..Default::default()
13280 },
13281 cx,
13282 )
13283 .await;
13284 let counter = Arc::new(AtomicUsize::new(0));
13285
13286 cx.set_state(indoc! {"
13287 oneˇ
13288 two
13289 three
13290 "});
13291 cx.simulate_keystroke(".");
13292 handle_completion_request(
13293 indoc! {"
13294 one.|<>
13295 two
13296 three
13297 "},
13298 vec!["first_completion", "second_completion"],
13299 true,
13300 counter.clone(),
13301 &mut cx,
13302 )
13303 .await;
13304 cx.condition(|editor, _| editor.context_menu_visible())
13305 .await;
13306 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13307
13308 let _handler = handle_signature_help_request(
13309 &mut cx,
13310 lsp::SignatureHelp {
13311 signatures: vec![lsp::SignatureInformation {
13312 label: "test signature".to_string(),
13313 documentation: None,
13314 parameters: Some(vec![lsp::ParameterInformation {
13315 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13316 documentation: None,
13317 }]),
13318 active_parameter: None,
13319 }],
13320 active_signature: None,
13321 active_parameter: None,
13322 },
13323 );
13324 cx.update_editor(|editor, window, cx| {
13325 assert!(
13326 !editor.signature_help_state.is_shown(),
13327 "No signature help was called for"
13328 );
13329 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13330 });
13331 cx.run_until_parked();
13332 cx.update_editor(|editor, _, _| {
13333 assert!(
13334 !editor.signature_help_state.is_shown(),
13335 "No signature help should be shown when completions menu is open"
13336 );
13337 });
13338
13339 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13340 editor.context_menu_next(&Default::default(), window, cx);
13341 editor
13342 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13343 .unwrap()
13344 });
13345 cx.assert_editor_state(indoc! {"
13346 one.second_completionˇ
13347 two
13348 three
13349 "});
13350
13351 handle_resolve_completion_request(
13352 &mut cx,
13353 Some(vec![
13354 (
13355 //This overlaps with the primary completion edit which is
13356 //misbehavior from the LSP spec, test that we filter it out
13357 indoc! {"
13358 one.second_ˇcompletion
13359 two
13360 threeˇ
13361 "},
13362 "overlapping additional edit",
13363 ),
13364 (
13365 indoc! {"
13366 one.second_completion
13367 two
13368 threeˇ
13369 "},
13370 "\nadditional edit",
13371 ),
13372 ]),
13373 )
13374 .await;
13375 apply_additional_edits.await.unwrap();
13376 cx.assert_editor_state(indoc! {"
13377 one.second_completionˇ
13378 two
13379 three
13380 additional edit
13381 "});
13382
13383 cx.set_state(indoc! {"
13384 one.second_completion
13385 twoˇ
13386 threeˇ
13387 additional edit
13388 "});
13389 cx.simulate_keystroke(" ");
13390 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13391 cx.simulate_keystroke("s");
13392 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13393
13394 cx.assert_editor_state(indoc! {"
13395 one.second_completion
13396 two sˇ
13397 three sˇ
13398 additional edit
13399 "});
13400 handle_completion_request(
13401 indoc! {"
13402 one.second_completion
13403 two s
13404 three <s|>
13405 additional edit
13406 "},
13407 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13408 true,
13409 counter.clone(),
13410 &mut cx,
13411 )
13412 .await;
13413 cx.condition(|editor, _| editor.context_menu_visible())
13414 .await;
13415 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13416
13417 cx.simulate_keystroke("i");
13418
13419 handle_completion_request(
13420 indoc! {"
13421 one.second_completion
13422 two si
13423 three <si|>
13424 additional edit
13425 "},
13426 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13427 true,
13428 counter.clone(),
13429 &mut cx,
13430 )
13431 .await;
13432 cx.condition(|editor, _| editor.context_menu_visible())
13433 .await;
13434 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13435
13436 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13437 editor
13438 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13439 .unwrap()
13440 });
13441 cx.assert_editor_state(indoc! {"
13442 one.second_completion
13443 two sixth_completionˇ
13444 three sixth_completionˇ
13445 additional edit
13446 "});
13447
13448 apply_additional_edits.await.unwrap();
13449
13450 update_test_language_settings(&mut cx, |settings| {
13451 settings.defaults.show_completions_on_input = Some(false);
13452 });
13453 cx.set_state("editorˇ");
13454 cx.simulate_keystroke(".");
13455 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13456 cx.simulate_keystrokes("c l o");
13457 cx.assert_editor_state("editor.cloˇ");
13458 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13459 cx.update_editor(|editor, window, cx| {
13460 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13461 });
13462 handle_completion_request(
13463 "editor.<clo|>",
13464 vec!["close", "clobber"],
13465 true,
13466 counter.clone(),
13467 &mut cx,
13468 )
13469 .await;
13470 cx.condition(|editor, _| editor.context_menu_visible())
13471 .await;
13472 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
13473
13474 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13475 editor
13476 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13477 .unwrap()
13478 });
13479 cx.assert_editor_state("editor.clobberˇ");
13480 handle_resolve_completion_request(&mut cx, None).await;
13481 apply_additional_edits.await.unwrap();
13482}
13483
13484#[gpui::test]
13485async fn test_completion_reuse(cx: &mut TestAppContext) {
13486 init_test(cx, |_| {});
13487
13488 let mut cx = EditorLspTestContext::new_rust(
13489 lsp::ServerCapabilities {
13490 completion_provider: Some(lsp::CompletionOptions {
13491 trigger_characters: Some(vec![".".to_string()]),
13492 ..Default::default()
13493 }),
13494 ..Default::default()
13495 },
13496 cx,
13497 )
13498 .await;
13499
13500 let counter = Arc::new(AtomicUsize::new(0));
13501 cx.set_state("objˇ");
13502 cx.simulate_keystroke(".");
13503
13504 // Initial completion request returns complete results
13505 let is_incomplete = false;
13506 handle_completion_request(
13507 "obj.|<>",
13508 vec!["a", "ab", "abc"],
13509 is_incomplete,
13510 counter.clone(),
13511 &mut cx,
13512 )
13513 .await;
13514 cx.run_until_parked();
13515 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13516 cx.assert_editor_state("obj.ˇ");
13517 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13518
13519 // Type "a" - filters existing completions
13520 cx.simulate_keystroke("a");
13521 cx.run_until_parked();
13522 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13523 cx.assert_editor_state("obj.aˇ");
13524 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13525
13526 // Type "b" - filters existing completions
13527 cx.simulate_keystroke("b");
13528 cx.run_until_parked();
13529 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13530 cx.assert_editor_state("obj.abˇ");
13531 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13532
13533 // Type "c" - filters existing completions
13534 cx.simulate_keystroke("c");
13535 cx.run_until_parked();
13536 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13537 cx.assert_editor_state("obj.abcˇ");
13538 check_displayed_completions(vec!["abc"], &mut cx);
13539
13540 // Backspace to delete "c" - filters existing completions
13541 cx.update_editor(|editor, window, cx| {
13542 editor.backspace(&Backspace, window, cx);
13543 });
13544 cx.run_until_parked();
13545 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13546 cx.assert_editor_state("obj.abˇ");
13547 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13548
13549 // Moving cursor to the left dismisses menu.
13550 cx.update_editor(|editor, window, cx| {
13551 editor.move_left(&MoveLeft, window, cx);
13552 });
13553 cx.run_until_parked();
13554 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13555 cx.assert_editor_state("obj.aˇb");
13556 cx.update_editor(|editor, _, _| {
13557 assert_eq!(editor.context_menu_visible(), false);
13558 });
13559
13560 // Type "b" - new request
13561 cx.simulate_keystroke("b");
13562 let is_incomplete = false;
13563 handle_completion_request(
13564 "obj.<ab|>a",
13565 vec!["ab", "abc"],
13566 is_incomplete,
13567 counter.clone(),
13568 &mut cx,
13569 )
13570 .await;
13571 cx.run_until_parked();
13572 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13573 cx.assert_editor_state("obj.abˇb");
13574 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13575
13576 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13577 cx.update_editor(|editor, window, cx| {
13578 editor.backspace(&Backspace, window, cx);
13579 });
13580 let is_incomplete = false;
13581 handle_completion_request(
13582 "obj.<a|>b",
13583 vec!["a", "ab", "abc"],
13584 is_incomplete,
13585 counter.clone(),
13586 &mut cx,
13587 )
13588 .await;
13589 cx.run_until_parked();
13590 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13591 cx.assert_editor_state("obj.aˇb");
13592 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13593
13594 // Backspace to delete "a" - dismisses menu.
13595 cx.update_editor(|editor, window, cx| {
13596 editor.backspace(&Backspace, window, cx);
13597 });
13598 cx.run_until_parked();
13599 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13600 cx.assert_editor_state("obj.ˇb");
13601 cx.update_editor(|editor, _, _| {
13602 assert_eq!(editor.context_menu_visible(), false);
13603 });
13604}
13605
13606#[gpui::test]
13607async fn test_word_completion(cx: &mut TestAppContext) {
13608 let lsp_fetch_timeout_ms = 10;
13609 init_test(cx, |language_settings| {
13610 language_settings.defaults.completions = Some(CompletionSettings {
13611 words: WordsCompletionMode::Fallback,
13612 words_min_length: 0,
13613 lsp: true,
13614 lsp_fetch_timeout_ms: 10,
13615 lsp_insert_mode: LspInsertMode::Insert,
13616 });
13617 });
13618
13619 let mut cx = EditorLspTestContext::new_rust(
13620 lsp::ServerCapabilities {
13621 completion_provider: Some(lsp::CompletionOptions {
13622 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13623 ..lsp::CompletionOptions::default()
13624 }),
13625 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13626 ..lsp::ServerCapabilities::default()
13627 },
13628 cx,
13629 )
13630 .await;
13631
13632 let throttle_completions = Arc::new(AtomicBool::new(false));
13633
13634 let lsp_throttle_completions = throttle_completions.clone();
13635 let _completion_requests_handler =
13636 cx.lsp
13637 .server
13638 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13639 let lsp_throttle_completions = lsp_throttle_completions.clone();
13640 let cx = cx.clone();
13641 async move {
13642 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13643 cx.background_executor()
13644 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13645 .await;
13646 }
13647 Ok(Some(lsp::CompletionResponse::Array(vec![
13648 lsp::CompletionItem {
13649 label: "first".into(),
13650 ..lsp::CompletionItem::default()
13651 },
13652 lsp::CompletionItem {
13653 label: "last".into(),
13654 ..lsp::CompletionItem::default()
13655 },
13656 ])))
13657 }
13658 });
13659
13660 cx.set_state(indoc! {"
13661 oneˇ
13662 two
13663 three
13664 "});
13665 cx.simulate_keystroke(".");
13666 cx.executor().run_until_parked();
13667 cx.condition(|editor, _| editor.context_menu_visible())
13668 .await;
13669 cx.update_editor(|editor, window, cx| {
13670 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13671 {
13672 assert_eq!(
13673 completion_menu_entries(menu),
13674 &["first", "last"],
13675 "When LSP server is fast to reply, no fallback word completions are used"
13676 );
13677 } else {
13678 panic!("expected completion menu to be open");
13679 }
13680 editor.cancel(&Cancel, window, cx);
13681 });
13682 cx.executor().run_until_parked();
13683 cx.condition(|editor, _| !editor.context_menu_visible())
13684 .await;
13685
13686 throttle_completions.store(true, atomic::Ordering::Release);
13687 cx.simulate_keystroke(".");
13688 cx.executor()
13689 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
13690 cx.executor().run_until_parked();
13691 cx.condition(|editor, _| editor.context_menu_visible())
13692 .await;
13693 cx.update_editor(|editor, _, _| {
13694 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13695 {
13696 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
13697 "When LSP server is slow, document words can be shown instead, if configured accordingly");
13698 } else {
13699 panic!("expected completion menu to be open");
13700 }
13701 });
13702}
13703
13704#[gpui::test]
13705async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
13706 init_test(cx, |language_settings| {
13707 language_settings.defaults.completions = Some(CompletionSettings {
13708 words: WordsCompletionMode::Enabled,
13709 words_min_length: 0,
13710 lsp: true,
13711 lsp_fetch_timeout_ms: 0,
13712 lsp_insert_mode: LspInsertMode::Insert,
13713 });
13714 });
13715
13716 let mut cx = EditorLspTestContext::new_rust(
13717 lsp::ServerCapabilities {
13718 completion_provider: Some(lsp::CompletionOptions {
13719 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13720 ..lsp::CompletionOptions::default()
13721 }),
13722 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13723 ..lsp::ServerCapabilities::default()
13724 },
13725 cx,
13726 )
13727 .await;
13728
13729 let _completion_requests_handler =
13730 cx.lsp
13731 .server
13732 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13733 Ok(Some(lsp::CompletionResponse::Array(vec![
13734 lsp::CompletionItem {
13735 label: "first".into(),
13736 ..lsp::CompletionItem::default()
13737 },
13738 lsp::CompletionItem {
13739 label: "last".into(),
13740 ..lsp::CompletionItem::default()
13741 },
13742 ])))
13743 });
13744
13745 cx.set_state(indoc! {"ˇ
13746 first
13747 last
13748 second
13749 "});
13750 cx.simulate_keystroke(".");
13751 cx.executor().run_until_parked();
13752 cx.condition(|editor, _| editor.context_menu_visible())
13753 .await;
13754 cx.update_editor(|editor, _, _| {
13755 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13756 {
13757 assert_eq!(
13758 completion_menu_entries(menu),
13759 &["first", "last", "second"],
13760 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
13761 );
13762 } else {
13763 panic!("expected completion menu to be open");
13764 }
13765 });
13766}
13767
13768#[gpui::test]
13769async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
13770 init_test(cx, |language_settings| {
13771 language_settings.defaults.completions = Some(CompletionSettings {
13772 words: WordsCompletionMode::Disabled,
13773 words_min_length: 0,
13774 lsp: true,
13775 lsp_fetch_timeout_ms: 0,
13776 lsp_insert_mode: LspInsertMode::Insert,
13777 });
13778 });
13779
13780 let mut cx = EditorLspTestContext::new_rust(
13781 lsp::ServerCapabilities {
13782 completion_provider: Some(lsp::CompletionOptions {
13783 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13784 ..lsp::CompletionOptions::default()
13785 }),
13786 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13787 ..lsp::ServerCapabilities::default()
13788 },
13789 cx,
13790 )
13791 .await;
13792
13793 let _completion_requests_handler =
13794 cx.lsp
13795 .server
13796 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13797 panic!("LSP completions should not be queried when dealing with word completions")
13798 });
13799
13800 cx.set_state(indoc! {"ˇ
13801 first
13802 last
13803 second
13804 "});
13805 cx.update_editor(|editor, window, cx| {
13806 editor.show_word_completions(&ShowWordCompletions, window, cx);
13807 });
13808 cx.executor().run_until_parked();
13809 cx.condition(|editor, _| editor.context_menu_visible())
13810 .await;
13811 cx.update_editor(|editor, _, _| {
13812 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13813 {
13814 assert_eq!(
13815 completion_menu_entries(menu),
13816 &["first", "last", "second"],
13817 "`ShowWordCompletions` action should show word completions"
13818 );
13819 } else {
13820 panic!("expected completion menu to be open");
13821 }
13822 });
13823
13824 cx.simulate_keystroke("l");
13825 cx.executor().run_until_parked();
13826 cx.condition(|editor, _| editor.context_menu_visible())
13827 .await;
13828 cx.update_editor(|editor, _, _| {
13829 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13830 {
13831 assert_eq!(
13832 completion_menu_entries(menu),
13833 &["last"],
13834 "After showing word completions, further editing should filter them and not query the LSP"
13835 );
13836 } else {
13837 panic!("expected completion menu to be open");
13838 }
13839 });
13840}
13841
13842#[gpui::test]
13843async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13844 init_test(cx, |language_settings| {
13845 language_settings.defaults.completions = Some(CompletionSettings {
13846 words: WordsCompletionMode::Fallback,
13847 words_min_length: 0,
13848 lsp: false,
13849 lsp_fetch_timeout_ms: 0,
13850 lsp_insert_mode: LspInsertMode::Insert,
13851 });
13852 });
13853
13854 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13855
13856 cx.set_state(indoc! {"ˇ
13857 0_usize
13858 let
13859 33
13860 4.5f32
13861 "});
13862 cx.update_editor(|editor, window, cx| {
13863 editor.show_completions(&ShowCompletions::default(), window, cx);
13864 });
13865 cx.executor().run_until_parked();
13866 cx.condition(|editor, _| editor.context_menu_visible())
13867 .await;
13868 cx.update_editor(|editor, window, cx| {
13869 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13870 {
13871 assert_eq!(
13872 completion_menu_entries(menu),
13873 &["let"],
13874 "With no digits in the completion query, no digits should be in the word completions"
13875 );
13876 } else {
13877 panic!("expected completion menu to be open");
13878 }
13879 editor.cancel(&Cancel, window, cx);
13880 });
13881
13882 cx.set_state(indoc! {"3ˇ
13883 0_usize
13884 let
13885 3
13886 33.35f32
13887 "});
13888 cx.update_editor(|editor, window, cx| {
13889 editor.show_completions(&ShowCompletions::default(), window, cx);
13890 });
13891 cx.executor().run_until_parked();
13892 cx.condition(|editor, _| editor.context_menu_visible())
13893 .await;
13894 cx.update_editor(|editor, _, _| {
13895 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13896 {
13897 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
13898 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13899 } else {
13900 panic!("expected completion menu to be open");
13901 }
13902 });
13903}
13904
13905#[gpui::test]
13906async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
13907 init_test(cx, |language_settings| {
13908 language_settings.defaults.completions = Some(CompletionSettings {
13909 words: WordsCompletionMode::Enabled,
13910 words_min_length: 3,
13911 lsp: true,
13912 lsp_fetch_timeout_ms: 0,
13913 lsp_insert_mode: LspInsertMode::Insert,
13914 });
13915 });
13916
13917 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13918 cx.set_state(indoc! {"ˇ
13919 wow
13920 wowen
13921 wowser
13922 "});
13923 cx.simulate_keystroke("w");
13924 cx.executor().run_until_parked();
13925 cx.update_editor(|editor, _, _| {
13926 if editor.context_menu.borrow_mut().is_some() {
13927 panic!(
13928 "expected completion menu to be hidden, as words completion threshold is not met"
13929 );
13930 }
13931 });
13932
13933 cx.simulate_keystroke("o");
13934 cx.executor().run_until_parked();
13935 cx.update_editor(|editor, _, _| {
13936 if editor.context_menu.borrow_mut().is_some() {
13937 panic!(
13938 "expected completion menu to be hidden, as words completion threshold is not met still"
13939 );
13940 }
13941 });
13942
13943 cx.simulate_keystroke("w");
13944 cx.executor().run_until_parked();
13945 cx.update_editor(|editor, _, _| {
13946 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13947 {
13948 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
13949 } else {
13950 panic!("expected completion menu to be open after the word completions threshold is met");
13951 }
13952 });
13953}
13954
13955fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13956 let position = || lsp::Position {
13957 line: params.text_document_position.position.line,
13958 character: params.text_document_position.position.character,
13959 };
13960 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13961 range: lsp::Range {
13962 start: position(),
13963 end: position(),
13964 },
13965 new_text: text.to_string(),
13966 }))
13967}
13968
13969#[gpui::test]
13970async fn test_multiline_completion(cx: &mut TestAppContext) {
13971 init_test(cx, |_| {});
13972
13973 let fs = FakeFs::new(cx.executor());
13974 fs.insert_tree(
13975 path!("/a"),
13976 json!({
13977 "main.ts": "a",
13978 }),
13979 )
13980 .await;
13981
13982 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13983 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13984 let typescript_language = Arc::new(Language::new(
13985 LanguageConfig {
13986 name: "TypeScript".into(),
13987 matcher: LanguageMatcher {
13988 path_suffixes: vec!["ts".to_string()],
13989 ..LanguageMatcher::default()
13990 },
13991 line_comments: vec!["// ".into()],
13992 ..LanguageConfig::default()
13993 },
13994 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13995 ));
13996 language_registry.add(typescript_language.clone());
13997 let mut fake_servers = language_registry.register_fake_lsp(
13998 "TypeScript",
13999 FakeLspAdapter {
14000 capabilities: lsp::ServerCapabilities {
14001 completion_provider: Some(lsp::CompletionOptions {
14002 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14003 ..lsp::CompletionOptions::default()
14004 }),
14005 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14006 ..lsp::ServerCapabilities::default()
14007 },
14008 // Emulate vtsls label generation
14009 label_for_completion: Some(Box::new(|item, _| {
14010 let text = if let Some(description) = item
14011 .label_details
14012 .as_ref()
14013 .and_then(|label_details| label_details.description.as_ref())
14014 {
14015 format!("{} {}", item.label, description)
14016 } else if let Some(detail) = &item.detail {
14017 format!("{} {}", item.label, detail)
14018 } else {
14019 item.label.clone()
14020 };
14021 let len = text.len();
14022 Some(language::CodeLabel {
14023 text,
14024 runs: Vec::new(),
14025 filter_range: 0..len,
14026 })
14027 })),
14028 ..FakeLspAdapter::default()
14029 },
14030 );
14031 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14032 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14033 let worktree_id = workspace
14034 .update(cx, |workspace, _window, cx| {
14035 workspace.project().update(cx, |project, cx| {
14036 project.worktrees(cx).next().unwrap().read(cx).id()
14037 })
14038 })
14039 .unwrap();
14040 let _buffer = project
14041 .update(cx, |project, cx| {
14042 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14043 })
14044 .await
14045 .unwrap();
14046 let editor = workspace
14047 .update(cx, |workspace, window, cx| {
14048 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14049 })
14050 .unwrap()
14051 .await
14052 .unwrap()
14053 .downcast::<Editor>()
14054 .unwrap();
14055 let fake_server = fake_servers.next().await.unwrap();
14056
14057 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14058 let multiline_label_2 = "a\nb\nc\n";
14059 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14060 let multiline_description = "d\ne\nf\n";
14061 let multiline_detail_2 = "g\nh\ni\n";
14062
14063 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14064 move |params, _| async move {
14065 Ok(Some(lsp::CompletionResponse::Array(vec![
14066 lsp::CompletionItem {
14067 label: multiline_label.to_string(),
14068 text_edit: gen_text_edit(¶ms, "new_text_1"),
14069 ..lsp::CompletionItem::default()
14070 },
14071 lsp::CompletionItem {
14072 label: "single line label 1".to_string(),
14073 detail: Some(multiline_detail.to_string()),
14074 text_edit: gen_text_edit(¶ms, "new_text_2"),
14075 ..lsp::CompletionItem::default()
14076 },
14077 lsp::CompletionItem {
14078 label: "single line label 2".to_string(),
14079 label_details: Some(lsp::CompletionItemLabelDetails {
14080 description: Some(multiline_description.to_string()),
14081 detail: None,
14082 }),
14083 text_edit: gen_text_edit(¶ms, "new_text_2"),
14084 ..lsp::CompletionItem::default()
14085 },
14086 lsp::CompletionItem {
14087 label: multiline_label_2.to_string(),
14088 detail: Some(multiline_detail_2.to_string()),
14089 text_edit: gen_text_edit(¶ms, "new_text_3"),
14090 ..lsp::CompletionItem::default()
14091 },
14092 lsp::CompletionItem {
14093 label: "Label with many spaces and \t but without newlines".to_string(),
14094 detail: Some(
14095 "Details with many spaces and \t but without newlines".to_string(),
14096 ),
14097 text_edit: gen_text_edit(¶ms, "new_text_4"),
14098 ..lsp::CompletionItem::default()
14099 },
14100 ])))
14101 },
14102 );
14103
14104 editor.update_in(cx, |editor, window, cx| {
14105 cx.focus_self(window);
14106 editor.move_to_end(&MoveToEnd, window, cx);
14107 editor.handle_input(".", window, cx);
14108 });
14109 cx.run_until_parked();
14110 completion_handle.next().await.unwrap();
14111
14112 editor.update(cx, |editor, _| {
14113 assert!(editor.context_menu_visible());
14114 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14115 {
14116 let completion_labels = menu
14117 .completions
14118 .borrow()
14119 .iter()
14120 .map(|c| c.label.text.clone())
14121 .collect::<Vec<_>>();
14122 assert_eq!(
14123 completion_labels,
14124 &[
14125 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14126 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14127 "single line label 2 d e f ",
14128 "a b c g h i ",
14129 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14130 ],
14131 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14132 );
14133
14134 for completion in menu
14135 .completions
14136 .borrow()
14137 .iter() {
14138 assert_eq!(
14139 completion.label.filter_range,
14140 0..completion.label.text.len(),
14141 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14142 );
14143 }
14144 } else {
14145 panic!("expected completion menu to be open");
14146 }
14147 });
14148}
14149
14150#[gpui::test]
14151async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14152 init_test(cx, |_| {});
14153 let mut cx = EditorLspTestContext::new_rust(
14154 lsp::ServerCapabilities {
14155 completion_provider: Some(lsp::CompletionOptions {
14156 trigger_characters: Some(vec![".".to_string()]),
14157 ..Default::default()
14158 }),
14159 ..Default::default()
14160 },
14161 cx,
14162 )
14163 .await;
14164 cx.lsp
14165 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14166 Ok(Some(lsp::CompletionResponse::Array(vec![
14167 lsp::CompletionItem {
14168 label: "first".into(),
14169 ..Default::default()
14170 },
14171 lsp::CompletionItem {
14172 label: "last".into(),
14173 ..Default::default()
14174 },
14175 ])))
14176 });
14177 cx.set_state("variableˇ");
14178 cx.simulate_keystroke(".");
14179 cx.executor().run_until_parked();
14180
14181 cx.update_editor(|editor, _, _| {
14182 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14183 {
14184 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14185 } else {
14186 panic!("expected completion menu to be open");
14187 }
14188 });
14189
14190 cx.update_editor(|editor, window, cx| {
14191 editor.move_page_down(&MovePageDown::default(), window, cx);
14192 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14193 {
14194 assert!(
14195 menu.selected_item == 1,
14196 "expected PageDown to select the last item from the context menu"
14197 );
14198 } else {
14199 panic!("expected completion menu to stay open after PageDown");
14200 }
14201 });
14202
14203 cx.update_editor(|editor, window, cx| {
14204 editor.move_page_up(&MovePageUp::default(), window, cx);
14205 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14206 {
14207 assert!(
14208 menu.selected_item == 0,
14209 "expected PageUp to select the first item from the context menu"
14210 );
14211 } else {
14212 panic!("expected completion menu to stay open after PageUp");
14213 }
14214 });
14215}
14216
14217#[gpui::test]
14218async fn test_as_is_completions(cx: &mut TestAppContext) {
14219 init_test(cx, |_| {});
14220 let mut cx = EditorLspTestContext::new_rust(
14221 lsp::ServerCapabilities {
14222 completion_provider: Some(lsp::CompletionOptions {
14223 ..Default::default()
14224 }),
14225 ..Default::default()
14226 },
14227 cx,
14228 )
14229 .await;
14230 cx.lsp
14231 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14232 Ok(Some(lsp::CompletionResponse::Array(vec![
14233 lsp::CompletionItem {
14234 label: "unsafe".into(),
14235 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14236 range: lsp::Range {
14237 start: lsp::Position {
14238 line: 1,
14239 character: 2,
14240 },
14241 end: lsp::Position {
14242 line: 1,
14243 character: 3,
14244 },
14245 },
14246 new_text: "unsafe".to_string(),
14247 })),
14248 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14249 ..Default::default()
14250 },
14251 ])))
14252 });
14253 cx.set_state("fn a() {}\n nˇ");
14254 cx.executor().run_until_parked();
14255 cx.update_editor(|editor, window, cx| {
14256 editor.show_completions(
14257 &ShowCompletions {
14258 trigger: Some("\n".into()),
14259 },
14260 window,
14261 cx,
14262 );
14263 });
14264 cx.executor().run_until_parked();
14265
14266 cx.update_editor(|editor, window, cx| {
14267 editor.confirm_completion(&Default::default(), window, cx)
14268 });
14269 cx.executor().run_until_parked();
14270 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14271}
14272
14273#[gpui::test]
14274async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14275 init_test(cx, |_| {});
14276 let language =
14277 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14278 let mut cx = EditorLspTestContext::new(
14279 language,
14280 lsp::ServerCapabilities {
14281 completion_provider: Some(lsp::CompletionOptions {
14282 ..lsp::CompletionOptions::default()
14283 }),
14284 ..lsp::ServerCapabilities::default()
14285 },
14286 cx,
14287 )
14288 .await;
14289
14290 cx.set_state(
14291 "#ifndef BAR_H
14292#define BAR_H
14293
14294#include <stdbool.h>
14295
14296int fn_branch(bool do_branch1, bool do_branch2);
14297
14298#endif // BAR_H
14299ˇ",
14300 );
14301 cx.executor().run_until_parked();
14302 cx.update_editor(|editor, window, cx| {
14303 editor.handle_input("#", window, cx);
14304 });
14305 cx.executor().run_until_parked();
14306 cx.update_editor(|editor, window, cx| {
14307 editor.handle_input("i", window, cx);
14308 });
14309 cx.executor().run_until_parked();
14310 cx.update_editor(|editor, window, cx| {
14311 editor.handle_input("n", window, cx);
14312 });
14313 cx.executor().run_until_parked();
14314 cx.assert_editor_state(
14315 "#ifndef BAR_H
14316#define BAR_H
14317
14318#include <stdbool.h>
14319
14320int fn_branch(bool do_branch1, bool do_branch2);
14321
14322#endif // BAR_H
14323#inˇ",
14324 );
14325
14326 cx.lsp
14327 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14328 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14329 is_incomplete: false,
14330 item_defaults: None,
14331 items: vec![lsp::CompletionItem {
14332 kind: Some(lsp::CompletionItemKind::SNIPPET),
14333 label_details: Some(lsp::CompletionItemLabelDetails {
14334 detail: Some("header".to_string()),
14335 description: None,
14336 }),
14337 label: " include".to_string(),
14338 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14339 range: lsp::Range {
14340 start: lsp::Position {
14341 line: 8,
14342 character: 1,
14343 },
14344 end: lsp::Position {
14345 line: 8,
14346 character: 1,
14347 },
14348 },
14349 new_text: "include \"$0\"".to_string(),
14350 })),
14351 sort_text: Some("40b67681include".to_string()),
14352 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14353 filter_text: Some("include".to_string()),
14354 insert_text: Some("include \"$0\"".to_string()),
14355 ..lsp::CompletionItem::default()
14356 }],
14357 })))
14358 });
14359 cx.update_editor(|editor, window, cx| {
14360 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14361 });
14362 cx.executor().run_until_parked();
14363 cx.update_editor(|editor, window, cx| {
14364 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14365 });
14366 cx.executor().run_until_parked();
14367 cx.assert_editor_state(
14368 "#ifndef BAR_H
14369#define BAR_H
14370
14371#include <stdbool.h>
14372
14373int fn_branch(bool do_branch1, bool do_branch2);
14374
14375#endif // BAR_H
14376#include \"ˇ\"",
14377 );
14378
14379 cx.lsp
14380 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14381 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14382 is_incomplete: true,
14383 item_defaults: None,
14384 items: vec![lsp::CompletionItem {
14385 kind: Some(lsp::CompletionItemKind::FILE),
14386 label: "AGL/".to_string(),
14387 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14388 range: lsp::Range {
14389 start: lsp::Position {
14390 line: 8,
14391 character: 10,
14392 },
14393 end: lsp::Position {
14394 line: 8,
14395 character: 11,
14396 },
14397 },
14398 new_text: "AGL/".to_string(),
14399 })),
14400 sort_text: Some("40b67681AGL/".to_string()),
14401 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14402 filter_text: Some("AGL/".to_string()),
14403 insert_text: Some("AGL/".to_string()),
14404 ..lsp::CompletionItem::default()
14405 }],
14406 })))
14407 });
14408 cx.update_editor(|editor, window, cx| {
14409 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14410 });
14411 cx.executor().run_until_parked();
14412 cx.update_editor(|editor, window, cx| {
14413 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14414 });
14415 cx.executor().run_until_parked();
14416 cx.assert_editor_state(
14417 r##"#ifndef BAR_H
14418#define BAR_H
14419
14420#include <stdbool.h>
14421
14422int fn_branch(bool do_branch1, bool do_branch2);
14423
14424#endif // BAR_H
14425#include "AGL/ˇ"##,
14426 );
14427
14428 cx.update_editor(|editor, window, cx| {
14429 editor.handle_input("\"", window, cx);
14430 });
14431 cx.executor().run_until_parked();
14432 cx.assert_editor_state(
14433 r##"#ifndef BAR_H
14434#define BAR_H
14435
14436#include <stdbool.h>
14437
14438int fn_branch(bool do_branch1, bool do_branch2);
14439
14440#endif // BAR_H
14441#include "AGL/"ˇ"##,
14442 );
14443}
14444
14445#[gpui::test]
14446async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
14447 init_test(cx, |_| {});
14448
14449 let mut cx = EditorLspTestContext::new_rust(
14450 lsp::ServerCapabilities {
14451 completion_provider: Some(lsp::CompletionOptions {
14452 trigger_characters: Some(vec![".".to_string()]),
14453 resolve_provider: Some(true),
14454 ..Default::default()
14455 }),
14456 ..Default::default()
14457 },
14458 cx,
14459 )
14460 .await;
14461
14462 cx.set_state("fn main() { let a = 2ˇ; }");
14463 cx.simulate_keystroke(".");
14464 let completion_item = lsp::CompletionItem {
14465 label: "Some".into(),
14466 kind: Some(lsp::CompletionItemKind::SNIPPET),
14467 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14468 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14469 kind: lsp::MarkupKind::Markdown,
14470 value: "```rust\nSome(2)\n```".to_string(),
14471 })),
14472 deprecated: Some(false),
14473 sort_text: Some("Some".to_string()),
14474 filter_text: Some("Some".to_string()),
14475 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14476 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14477 range: lsp::Range {
14478 start: lsp::Position {
14479 line: 0,
14480 character: 22,
14481 },
14482 end: lsp::Position {
14483 line: 0,
14484 character: 22,
14485 },
14486 },
14487 new_text: "Some(2)".to_string(),
14488 })),
14489 additional_text_edits: Some(vec![lsp::TextEdit {
14490 range: lsp::Range {
14491 start: lsp::Position {
14492 line: 0,
14493 character: 20,
14494 },
14495 end: lsp::Position {
14496 line: 0,
14497 character: 22,
14498 },
14499 },
14500 new_text: "".to_string(),
14501 }]),
14502 ..Default::default()
14503 };
14504
14505 let closure_completion_item = completion_item.clone();
14506 let counter = Arc::new(AtomicUsize::new(0));
14507 let counter_clone = counter.clone();
14508 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14509 let task_completion_item = closure_completion_item.clone();
14510 counter_clone.fetch_add(1, atomic::Ordering::Release);
14511 async move {
14512 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14513 is_incomplete: true,
14514 item_defaults: None,
14515 items: vec![task_completion_item],
14516 })))
14517 }
14518 });
14519
14520 cx.condition(|editor, _| editor.context_menu_visible())
14521 .await;
14522 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
14523 assert!(request.next().await.is_some());
14524 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14525
14526 cx.simulate_keystrokes("S o m");
14527 cx.condition(|editor, _| editor.context_menu_visible())
14528 .await;
14529 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
14530 assert!(request.next().await.is_some());
14531 assert!(request.next().await.is_some());
14532 assert!(request.next().await.is_some());
14533 request.close();
14534 assert!(request.next().await.is_none());
14535 assert_eq!(
14536 counter.load(atomic::Ordering::Acquire),
14537 4,
14538 "With the completions menu open, only one LSP request should happen per input"
14539 );
14540}
14541
14542#[gpui::test]
14543async fn test_toggle_comment(cx: &mut TestAppContext) {
14544 init_test(cx, |_| {});
14545 let mut cx = EditorTestContext::new(cx).await;
14546 let language = Arc::new(Language::new(
14547 LanguageConfig {
14548 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14549 ..Default::default()
14550 },
14551 Some(tree_sitter_rust::LANGUAGE.into()),
14552 ));
14553 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14554
14555 // If multiple selections intersect a line, the line is only toggled once.
14556 cx.set_state(indoc! {"
14557 fn a() {
14558 «//b();
14559 ˇ»// «c();
14560 //ˇ» d();
14561 }
14562 "});
14563
14564 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14565
14566 cx.assert_editor_state(indoc! {"
14567 fn a() {
14568 «b();
14569 c();
14570 ˇ» d();
14571 }
14572 "});
14573
14574 // The comment prefix is inserted at the same column for every line in a
14575 // selection.
14576 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14577
14578 cx.assert_editor_state(indoc! {"
14579 fn a() {
14580 // «b();
14581 // c();
14582 ˇ»// d();
14583 }
14584 "});
14585
14586 // If a selection ends at the beginning of a line, that line is not toggled.
14587 cx.set_selections_state(indoc! {"
14588 fn a() {
14589 // b();
14590 «// c();
14591 ˇ» // d();
14592 }
14593 "});
14594
14595 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14596
14597 cx.assert_editor_state(indoc! {"
14598 fn a() {
14599 // b();
14600 «c();
14601 ˇ» // d();
14602 }
14603 "});
14604
14605 // If a selection span a single line and is empty, the line is toggled.
14606 cx.set_state(indoc! {"
14607 fn a() {
14608 a();
14609 b();
14610 ˇ
14611 }
14612 "});
14613
14614 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14615
14616 cx.assert_editor_state(indoc! {"
14617 fn a() {
14618 a();
14619 b();
14620 //•ˇ
14621 }
14622 "});
14623
14624 // If a selection span multiple lines, empty lines are not toggled.
14625 cx.set_state(indoc! {"
14626 fn a() {
14627 «a();
14628
14629 c();ˇ»
14630 }
14631 "});
14632
14633 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14634
14635 cx.assert_editor_state(indoc! {"
14636 fn a() {
14637 // «a();
14638
14639 // c();ˇ»
14640 }
14641 "});
14642
14643 // If a selection includes multiple comment prefixes, all lines are uncommented.
14644 cx.set_state(indoc! {"
14645 fn a() {
14646 «// a();
14647 /// b();
14648 //! c();ˇ»
14649 }
14650 "});
14651
14652 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14653
14654 cx.assert_editor_state(indoc! {"
14655 fn a() {
14656 «a();
14657 b();
14658 c();ˇ»
14659 }
14660 "});
14661}
14662
14663#[gpui::test]
14664async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
14665 init_test(cx, |_| {});
14666 let mut cx = EditorTestContext::new(cx).await;
14667 let language = Arc::new(Language::new(
14668 LanguageConfig {
14669 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14670 ..Default::default()
14671 },
14672 Some(tree_sitter_rust::LANGUAGE.into()),
14673 ));
14674 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14675
14676 let toggle_comments = &ToggleComments {
14677 advance_downwards: false,
14678 ignore_indent: true,
14679 };
14680
14681 // If multiple selections intersect a line, the line is only toggled once.
14682 cx.set_state(indoc! {"
14683 fn a() {
14684 // «b();
14685 // c();
14686 // ˇ» d();
14687 }
14688 "});
14689
14690 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14691
14692 cx.assert_editor_state(indoc! {"
14693 fn a() {
14694 «b();
14695 c();
14696 ˇ» d();
14697 }
14698 "});
14699
14700 // The comment prefix is inserted at the beginning of each line
14701 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14702
14703 cx.assert_editor_state(indoc! {"
14704 fn a() {
14705 // «b();
14706 // c();
14707 // ˇ» d();
14708 }
14709 "});
14710
14711 // If a selection ends at the beginning of a line, that line is not toggled.
14712 cx.set_selections_state(indoc! {"
14713 fn a() {
14714 // b();
14715 // «c();
14716 ˇ»// d();
14717 }
14718 "});
14719
14720 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14721
14722 cx.assert_editor_state(indoc! {"
14723 fn a() {
14724 // b();
14725 «c();
14726 ˇ»// d();
14727 }
14728 "});
14729
14730 // If a selection span a single line and is empty, the line is toggled.
14731 cx.set_state(indoc! {"
14732 fn a() {
14733 a();
14734 b();
14735 ˇ
14736 }
14737 "});
14738
14739 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14740
14741 cx.assert_editor_state(indoc! {"
14742 fn a() {
14743 a();
14744 b();
14745 //ˇ
14746 }
14747 "});
14748
14749 // If a selection span multiple lines, empty lines are not toggled.
14750 cx.set_state(indoc! {"
14751 fn a() {
14752 «a();
14753
14754 c();ˇ»
14755 }
14756 "});
14757
14758 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14759
14760 cx.assert_editor_state(indoc! {"
14761 fn a() {
14762 // «a();
14763
14764 // c();ˇ»
14765 }
14766 "});
14767
14768 // If a selection includes multiple comment prefixes, all lines are uncommented.
14769 cx.set_state(indoc! {"
14770 fn a() {
14771 // «a();
14772 /// b();
14773 //! c();ˇ»
14774 }
14775 "});
14776
14777 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14778
14779 cx.assert_editor_state(indoc! {"
14780 fn a() {
14781 «a();
14782 b();
14783 c();ˇ»
14784 }
14785 "});
14786}
14787
14788#[gpui::test]
14789async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
14790 init_test(cx, |_| {});
14791
14792 let language = Arc::new(Language::new(
14793 LanguageConfig {
14794 line_comments: vec!["// ".into()],
14795 ..Default::default()
14796 },
14797 Some(tree_sitter_rust::LANGUAGE.into()),
14798 ));
14799
14800 let mut cx = EditorTestContext::new(cx).await;
14801
14802 cx.language_registry().add(language.clone());
14803 cx.update_buffer(|buffer, cx| {
14804 buffer.set_language(Some(language), cx);
14805 });
14806
14807 let toggle_comments = &ToggleComments {
14808 advance_downwards: true,
14809 ignore_indent: false,
14810 };
14811
14812 // Single cursor on one line -> advance
14813 // Cursor moves horizontally 3 characters as well on non-blank line
14814 cx.set_state(indoc!(
14815 "fn a() {
14816 ˇdog();
14817 cat();
14818 }"
14819 ));
14820 cx.update_editor(|editor, window, cx| {
14821 editor.toggle_comments(toggle_comments, window, cx);
14822 });
14823 cx.assert_editor_state(indoc!(
14824 "fn a() {
14825 // dog();
14826 catˇ();
14827 }"
14828 ));
14829
14830 // Single selection on one line -> don't advance
14831 cx.set_state(indoc!(
14832 "fn a() {
14833 «dog()ˇ»;
14834 cat();
14835 }"
14836 ));
14837 cx.update_editor(|editor, window, cx| {
14838 editor.toggle_comments(toggle_comments, window, cx);
14839 });
14840 cx.assert_editor_state(indoc!(
14841 "fn a() {
14842 // «dog()ˇ»;
14843 cat();
14844 }"
14845 ));
14846
14847 // Multiple cursors on one line -> advance
14848 cx.set_state(indoc!(
14849 "fn a() {
14850 ˇdˇog();
14851 cat();
14852 }"
14853 ));
14854 cx.update_editor(|editor, window, cx| {
14855 editor.toggle_comments(toggle_comments, window, cx);
14856 });
14857 cx.assert_editor_state(indoc!(
14858 "fn a() {
14859 // dog();
14860 catˇ(ˇ);
14861 }"
14862 ));
14863
14864 // Multiple cursors on one line, with selection -> don't advance
14865 cx.set_state(indoc!(
14866 "fn a() {
14867 ˇdˇog«()ˇ»;
14868 cat();
14869 }"
14870 ));
14871 cx.update_editor(|editor, window, cx| {
14872 editor.toggle_comments(toggle_comments, window, cx);
14873 });
14874 cx.assert_editor_state(indoc!(
14875 "fn a() {
14876 // ˇdˇog«()ˇ»;
14877 cat();
14878 }"
14879 ));
14880
14881 // Single cursor on one line -> advance
14882 // Cursor moves to column 0 on blank line
14883 cx.set_state(indoc!(
14884 "fn a() {
14885 ˇdog();
14886
14887 cat();
14888 }"
14889 ));
14890 cx.update_editor(|editor, window, cx| {
14891 editor.toggle_comments(toggle_comments, window, cx);
14892 });
14893 cx.assert_editor_state(indoc!(
14894 "fn a() {
14895 // dog();
14896 ˇ
14897 cat();
14898 }"
14899 ));
14900
14901 // Single cursor on one line -> advance
14902 // Cursor starts and ends at column 0
14903 cx.set_state(indoc!(
14904 "fn a() {
14905 ˇ dog();
14906 cat();
14907 }"
14908 ));
14909 cx.update_editor(|editor, window, cx| {
14910 editor.toggle_comments(toggle_comments, window, cx);
14911 });
14912 cx.assert_editor_state(indoc!(
14913 "fn a() {
14914 // dog();
14915 ˇ cat();
14916 }"
14917 ));
14918}
14919
14920#[gpui::test]
14921async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14922 init_test(cx, |_| {});
14923
14924 let mut cx = EditorTestContext::new(cx).await;
14925
14926 let html_language = Arc::new(
14927 Language::new(
14928 LanguageConfig {
14929 name: "HTML".into(),
14930 block_comment: Some(BlockCommentConfig {
14931 start: "<!-- ".into(),
14932 prefix: "".into(),
14933 end: " -->".into(),
14934 tab_size: 0,
14935 }),
14936 ..Default::default()
14937 },
14938 Some(tree_sitter_html::LANGUAGE.into()),
14939 )
14940 .with_injection_query(
14941 r#"
14942 (script_element
14943 (raw_text) @injection.content
14944 (#set! injection.language "javascript"))
14945 "#,
14946 )
14947 .unwrap(),
14948 );
14949
14950 let javascript_language = Arc::new(Language::new(
14951 LanguageConfig {
14952 name: "JavaScript".into(),
14953 line_comments: vec!["// ".into()],
14954 ..Default::default()
14955 },
14956 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14957 ));
14958
14959 cx.language_registry().add(html_language.clone());
14960 cx.language_registry().add(javascript_language);
14961 cx.update_buffer(|buffer, cx| {
14962 buffer.set_language(Some(html_language), cx);
14963 });
14964
14965 // Toggle comments for empty selections
14966 cx.set_state(
14967 &r#"
14968 <p>A</p>ˇ
14969 <p>B</p>ˇ
14970 <p>C</p>ˇ
14971 "#
14972 .unindent(),
14973 );
14974 cx.update_editor(|editor, window, cx| {
14975 editor.toggle_comments(&ToggleComments::default(), window, cx)
14976 });
14977 cx.assert_editor_state(
14978 &r#"
14979 <!-- <p>A</p>ˇ -->
14980 <!-- <p>B</p>ˇ -->
14981 <!-- <p>C</p>ˇ -->
14982 "#
14983 .unindent(),
14984 );
14985 cx.update_editor(|editor, window, cx| {
14986 editor.toggle_comments(&ToggleComments::default(), window, cx)
14987 });
14988 cx.assert_editor_state(
14989 &r#"
14990 <p>A</p>ˇ
14991 <p>B</p>ˇ
14992 <p>C</p>ˇ
14993 "#
14994 .unindent(),
14995 );
14996
14997 // Toggle comments for mixture of empty and non-empty selections, where
14998 // multiple selections occupy a given line.
14999 cx.set_state(
15000 &r#"
15001 <p>A«</p>
15002 <p>ˇ»B</p>ˇ
15003 <p>C«</p>
15004 <p>ˇ»D</p>ˇ
15005 "#
15006 .unindent(),
15007 );
15008
15009 cx.update_editor(|editor, window, cx| {
15010 editor.toggle_comments(&ToggleComments::default(), window, cx)
15011 });
15012 cx.assert_editor_state(
15013 &r#"
15014 <!-- <p>A«</p>
15015 <p>ˇ»B</p>ˇ -->
15016 <!-- <p>C«</p>
15017 <p>ˇ»D</p>ˇ -->
15018 "#
15019 .unindent(),
15020 );
15021 cx.update_editor(|editor, window, cx| {
15022 editor.toggle_comments(&ToggleComments::default(), window, cx)
15023 });
15024 cx.assert_editor_state(
15025 &r#"
15026 <p>A«</p>
15027 <p>ˇ»B</p>ˇ
15028 <p>C«</p>
15029 <p>ˇ»D</p>ˇ
15030 "#
15031 .unindent(),
15032 );
15033
15034 // Toggle comments when different languages are active for different
15035 // selections.
15036 cx.set_state(
15037 &r#"
15038 ˇ<script>
15039 ˇvar x = new Y();
15040 ˇ</script>
15041 "#
15042 .unindent(),
15043 );
15044 cx.executor().run_until_parked();
15045 cx.update_editor(|editor, window, cx| {
15046 editor.toggle_comments(&ToggleComments::default(), window, cx)
15047 });
15048 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15049 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15050 cx.assert_editor_state(
15051 &r#"
15052 <!-- ˇ<script> -->
15053 // ˇvar x = new Y();
15054 <!-- ˇ</script> -->
15055 "#
15056 .unindent(),
15057 );
15058}
15059
15060#[gpui::test]
15061fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15062 init_test(cx, |_| {});
15063
15064 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15065 let multibuffer = cx.new(|cx| {
15066 let mut multibuffer = MultiBuffer::new(ReadWrite);
15067 multibuffer.push_excerpts(
15068 buffer.clone(),
15069 [
15070 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15071 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15072 ],
15073 cx,
15074 );
15075 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15076 multibuffer
15077 });
15078
15079 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15080 editor.update_in(cx, |editor, window, cx| {
15081 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15082 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15083 s.select_ranges([
15084 Point::new(0, 0)..Point::new(0, 0),
15085 Point::new(1, 0)..Point::new(1, 0),
15086 ])
15087 });
15088
15089 editor.handle_input("X", window, cx);
15090 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15091 assert_eq!(
15092 editor.selections.ranges(cx),
15093 [
15094 Point::new(0, 1)..Point::new(0, 1),
15095 Point::new(1, 1)..Point::new(1, 1),
15096 ]
15097 );
15098
15099 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15101 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15102 });
15103 editor.backspace(&Default::default(), window, cx);
15104 assert_eq!(editor.text(cx), "Xa\nbbb");
15105 assert_eq!(
15106 editor.selections.ranges(cx),
15107 [Point::new(1, 0)..Point::new(1, 0)]
15108 );
15109
15110 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15111 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15112 });
15113 editor.backspace(&Default::default(), window, cx);
15114 assert_eq!(editor.text(cx), "X\nbb");
15115 assert_eq!(
15116 editor.selections.ranges(cx),
15117 [Point::new(0, 1)..Point::new(0, 1)]
15118 );
15119 });
15120}
15121
15122#[gpui::test]
15123fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15124 init_test(cx, |_| {});
15125
15126 let markers = vec![('[', ']').into(), ('(', ')').into()];
15127 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15128 indoc! {"
15129 [aaaa
15130 (bbbb]
15131 cccc)",
15132 },
15133 markers.clone(),
15134 );
15135 let excerpt_ranges = markers.into_iter().map(|marker| {
15136 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15137 ExcerptRange::new(context)
15138 });
15139 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15140 let multibuffer = cx.new(|cx| {
15141 let mut multibuffer = MultiBuffer::new(ReadWrite);
15142 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15143 multibuffer
15144 });
15145
15146 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15147 editor.update_in(cx, |editor, window, cx| {
15148 let (expected_text, selection_ranges) = marked_text_ranges(
15149 indoc! {"
15150 aaaa
15151 bˇbbb
15152 bˇbbˇb
15153 cccc"
15154 },
15155 true,
15156 );
15157 assert_eq!(editor.text(cx), expected_text);
15158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15159 s.select_ranges(selection_ranges)
15160 });
15161
15162 editor.handle_input("X", window, cx);
15163
15164 let (expected_text, expected_selections) = marked_text_ranges(
15165 indoc! {"
15166 aaaa
15167 bXˇbbXb
15168 bXˇbbXˇb
15169 cccc"
15170 },
15171 false,
15172 );
15173 assert_eq!(editor.text(cx), expected_text);
15174 assert_eq!(editor.selections.ranges(cx), expected_selections);
15175
15176 editor.newline(&Newline, window, cx);
15177 let (expected_text, expected_selections) = marked_text_ranges(
15178 indoc! {"
15179 aaaa
15180 bX
15181 ˇbbX
15182 b
15183 bX
15184 ˇbbX
15185 ˇb
15186 cccc"
15187 },
15188 false,
15189 );
15190 assert_eq!(editor.text(cx), expected_text);
15191 assert_eq!(editor.selections.ranges(cx), expected_selections);
15192 });
15193}
15194
15195#[gpui::test]
15196fn test_refresh_selections(cx: &mut TestAppContext) {
15197 init_test(cx, |_| {});
15198
15199 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15200 let mut excerpt1_id = None;
15201 let multibuffer = cx.new(|cx| {
15202 let mut multibuffer = MultiBuffer::new(ReadWrite);
15203 excerpt1_id = multibuffer
15204 .push_excerpts(
15205 buffer.clone(),
15206 [
15207 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15208 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15209 ],
15210 cx,
15211 )
15212 .into_iter()
15213 .next();
15214 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15215 multibuffer
15216 });
15217
15218 let editor = cx.add_window(|window, cx| {
15219 let mut editor = build_editor(multibuffer.clone(), window, cx);
15220 let snapshot = editor.snapshot(window, cx);
15221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15222 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15223 });
15224 editor.begin_selection(
15225 Point::new(2, 1).to_display_point(&snapshot),
15226 true,
15227 1,
15228 window,
15229 cx,
15230 );
15231 assert_eq!(
15232 editor.selections.ranges(cx),
15233 [
15234 Point::new(1, 3)..Point::new(1, 3),
15235 Point::new(2, 1)..Point::new(2, 1),
15236 ]
15237 );
15238 editor
15239 });
15240
15241 // Refreshing selections is a no-op when excerpts haven't changed.
15242 _ = editor.update(cx, |editor, window, cx| {
15243 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15244 assert_eq!(
15245 editor.selections.ranges(cx),
15246 [
15247 Point::new(1, 3)..Point::new(1, 3),
15248 Point::new(2, 1)..Point::new(2, 1),
15249 ]
15250 );
15251 });
15252
15253 multibuffer.update(cx, |multibuffer, cx| {
15254 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15255 });
15256 _ = editor.update(cx, |editor, window, cx| {
15257 // Removing an excerpt causes the first selection to become degenerate.
15258 assert_eq!(
15259 editor.selections.ranges(cx),
15260 [
15261 Point::new(0, 0)..Point::new(0, 0),
15262 Point::new(0, 1)..Point::new(0, 1)
15263 ]
15264 );
15265
15266 // Refreshing selections will relocate the first selection to the original buffer
15267 // location.
15268 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15269 assert_eq!(
15270 editor.selections.ranges(cx),
15271 [
15272 Point::new(0, 1)..Point::new(0, 1),
15273 Point::new(0, 3)..Point::new(0, 3)
15274 ]
15275 );
15276 assert!(editor.selections.pending_anchor().is_some());
15277 });
15278}
15279
15280#[gpui::test]
15281fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15282 init_test(cx, |_| {});
15283
15284 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15285 let mut excerpt1_id = None;
15286 let multibuffer = cx.new(|cx| {
15287 let mut multibuffer = MultiBuffer::new(ReadWrite);
15288 excerpt1_id = multibuffer
15289 .push_excerpts(
15290 buffer.clone(),
15291 [
15292 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15293 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15294 ],
15295 cx,
15296 )
15297 .into_iter()
15298 .next();
15299 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15300 multibuffer
15301 });
15302
15303 let editor = cx.add_window(|window, cx| {
15304 let mut editor = build_editor(multibuffer.clone(), window, cx);
15305 let snapshot = editor.snapshot(window, cx);
15306 editor.begin_selection(
15307 Point::new(1, 3).to_display_point(&snapshot),
15308 false,
15309 1,
15310 window,
15311 cx,
15312 );
15313 assert_eq!(
15314 editor.selections.ranges(cx),
15315 [Point::new(1, 3)..Point::new(1, 3)]
15316 );
15317 editor
15318 });
15319
15320 multibuffer.update(cx, |multibuffer, cx| {
15321 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15322 });
15323 _ = editor.update(cx, |editor, window, cx| {
15324 assert_eq!(
15325 editor.selections.ranges(cx),
15326 [Point::new(0, 0)..Point::new(0, 0)]
15327 );
15328
15329 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15330 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15331 assert_eq!(
15332 editor.selections.ranges(cx),
15333 [Point::new(0, 3)..Point::new(0, 3)]
15334 );
15335 assert!(editor.selections.pending_anchor().is_some());
15336 });
15337}
15338
15339#[gpui::test]
15340async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15341 init_test(cx, |_| {});
15342
15343 let language = Arc::new(
15344 Language::new(
15345 LanguageConfig {
15346 brackets: BracketPairConfig {
15347 pairs: vec![
15348 BracketPair {
15349 start: "{".to_string(),
15350 end: "}".to_string(),
15351 close: true,
15352 surround: true,
15353 newline: true,
15354 },
15355 BracketPair {
15356 start: "/* ".to_string(),
15357 end: " */".to_string(),
15358 close: true,
15359 surround: true,
15360 newline: true,
15361 },
15362 ],
15363 ..Default::default()
15364 },
15365 ..Default::default()
15366 },
15367 Some(tree_sitter_rust::LANGUAGE.into()),
15368 )
15369 .with_indents_query("")
15370 .unwrap(),
15371 );
15372
15373 let text = concat!(
15374 "{ }\n", //
15375 " x\n", //
15376 " /* */\n", //
15377 "x\n", //
15378 "{{} }\n", //
15379 );
15380
15381 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15382 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15383 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15384 editor
15385 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
15386 .await;
15387
15388 editor.update_in(cx, |editor, window, cx| {
15389 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15390 s.select_display_ranges([
15391 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
15392 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
15393 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
15394 ])
15395 });
15396 editor.newline(&Newline, window, cx);
15397
15398 assert_eq!(
15399 editor.buffer().read(cx).read(cx).text(),
15400 concat!(
15401 "{ \n", // Suppress rustfmt
15402 "\n", //
15403 "}\n", //
15404 " x\n", //
15405 " /* \n", //
15406 " \n", //
15407 " */\n", //
15408 "x\n", //
15409 "{{} \n", //
15410 "}\n", //
15411 )
15412 );
15413 });
15414}
15415
15416#[gpui::test]
15417fn test_highlighted_ranges(cx: &mut TestAppContext) {
15418 init_test(cx, |_| {});
15419
15420 let editor = cx.add_window(|window, cx| {
15421 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
15422 build_editor(buffer, window, cx)
15423 });
15424
15425 _ = editor.update(cx, |editor, window, cx| {
15426 struct Type1;
15427 struct Type2;
15428
15429 let buffer = editor.buffer.read(cx).snapshot(cx);
15430
15431 let anchor_range =
15432 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
15433
15434 editor.highlight_background::<Type1>(
15435 &[
15436 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
15437 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
15438 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
15439 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
15440 ],
15441 |_| Hsla::red(),
15442 cx,
15443 );
15444 editor.highlight_background::<Type2>(
15445 &[
15446 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
15447 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
15448 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
15449 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
15450 ],
15451 |_| Hsla::green(),
15452 cx,
15453 );
15454
15455 let snapshot = editor.snapshot(window, cx);
15456 let mut highlighted_ranges = editor.background_highlights_in_range(
15457 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
15458 &snapshot,
15459 cx.theme(),
15460 );
15461 // Enforce a consistent ordering based on color without relying on the ordering of the
15462 // highlight's `TypeId` which is non-executor.
15463 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
15464 assert_eq!(
15465 highlighted_ranges,
15466 &[
15467 (
15468 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
15469 Hsla::red(),
15470 ),
15471 (
15472 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15473 Hsla::red(),
15474 ),
15475 (
15476 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
15477 Hsla::green(),
15478 ),
15479 (
15480 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
15481 Hsla::green(),
15482 ),
15483 ]
15484 );
15485 assert_eq!(
15486 editor.background_highlights_in_range(
15487 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
15488 &snapshot,
15489 cx.theme(),
15490 ),
15491 &[(
15492 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15493 Hsla::red(),
15494 )]
15495 );
15496 });
15497}
15498
15499#[gpui::test]
15500async fn test_following(cx: &mut TestAppContext) {
15501 init_test(cx, |_| {});
15502
15503 let fs = FakeFs::new(cx.executor());
15504 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15505
15506 let buffer = project.update(cx, |project, cx| {
15507 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
15508 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
15509 });
15510 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
15511 let follower = cx.update(|cx| {
15512 cx.open_window(
15513 WindowOptions {
15514 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
15515 gpui::Point::new(px(0.), px(0.)),
15516 gpui::Point::new(px(10.), px(80.)),
15517 ))),
15518 ..Default::default()
15519 },
15520 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
15521 )
15522 .unwrap()
15523 });
15524
15525 let is_still_following = Rc::new(RefCell::new(true));
15526 let follower_edit_event_count = Rc::new(RefCell::new(0));
15527 let pending_update = Rc::new(RefCell::new(None));
15528 let leader_entity = leader.root(cx).unwrap();
15529 let follower_entity = follower.root(cx).unwrap();
15530 _ = follower.update(cx, {
15531 let update = pending_update.clone();
15532 let is_still_following = is_still_following.clone();
15533 let follower_edit_event_count = follower_edit_event_count.clone();
15534 |_, window, cx| {
15535 cx.subscribe_in(
15536 &leader_entity,
15537 window,
15538 move |_, leader, event, window, cx| {
15539 leader.read(cx).add_event_to_update_proto(
15540 event,
15541 &mut update.borrow_mut(),
15542 window,
15543 cx,
15544 );
15545 },
15546 )
15547 .detach();
15548
15549 cx.subscribe_in(
15550 &follower_entity,
15551 window,
15552 move |_, _, event: &EditorEvent, _window, _cx| {
15553 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
15554 *is_still_following.borrow_mut() = false;
15555 }
15556
15557 if let EditorEvent::BufferEdited = event {
15558 *follower_edit_event_count.borrow_mut() += 1;
15559 }
15560 },
15561 )
15562 .detach();
15563 }
15564 });
15565
15566 // Update the selections only
15567 _ = leader.update(cx, |leader, window, cx| {
15568 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15569 s.select_ranges([1..1])
15570 });
15571 });
15572 follower
15573 .update(cx, |follower, window, cx| {
15574 follower.apply_update_proto(
15575 &project,
15576 pending_update.borrow_mut().take().unwrap(),
15577 window,
15578 cx,
15579 )
15580 })
15581 .unwrap()
15582 .await
15583 .unwrap();
15584 _ = follower.update(cx, |follower, _, cx| {
15585 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15586 });
15587 assert!(*is_still_following.borrow());
15588 assert_eq!(*follower_edit_event_count.borrow(), 0);
15589
15590 // Update the scroll position only
15591 _ = leader.update(cx, |leader, window, cx| {
15592 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15593 });
15594 follower
15595 .update(cx, |follower, window, cx| {
15596 follower.apply_update_proto(
15597 &project,
15598 pending_update.borrow_mut().take().unwrap(),
15599 window,
15600 cx,
15601 )
15602 })
15603 .unwrap()
15604 .await
15605 .unwrap();
15606 assert_eq!(
15607 follower
15608 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15609 .unwrap(),
15610 gpui::Point::new(1.5, 3.5)
15611 );
15612 assert!(*is_still_following.borrow());
15613 assert_eq!(*follower_edit_event_count.borrow(), 0);
15614
15615 // Update the selections and scroll position. The follower's scroll position is updated
15616 // via autoscroll, not via the leader's exact scroll position.
15617 _ = leader.update(cx, |leader, window, cx| {
15618 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15619 s.select_ranges([0..0])
15620 });
15621 leader.request_autoscroll(Autoscroll::newest(), cx);
15622 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15623 });
15624 follower
15625 .update(cx, |follower, window, cx| {
15626 follower.apply_update_proto(
15627 &project,
15628 pending_update.borrow_mut().take().unwrap(),
15629 window,
15630 cx,
15631 )
15632 })
15633 .unwrap()
15634 .await
15635 .unwrap();
15636 _ = follower.update(cx, |follower, _, cx| {
15637 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15638 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15639 });
15640 assert!(*is_still_following.borrow());
15641
15642 // Creating a pending selection that precedes another selection
15643 _ = leader.update(cx, |leader, window, cx| {
15644 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15645 s.select_ranges([1..1])
15646 });
15647 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15648 });
15649 follower
15650 .update(cx, |follower, window, cx| {
15651 follower.apply_update_proto(
15652 &project,
15653 pending_update.borrow_mut().take().unwrap(),
15654 window,
15655 cx,
15656 )
15657 })
15658 .unwrap()
15659 .await
15660 .unwrap();
15661 _ = follower.update(cx, |follower, _, cx| {
15662 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
15663 });
15664 assert!(*is_still_following.borrow());
15665
15666 // Extend the pending selection so that it surrounds another selection
15667 _ = leader.update(cx, |leader, window, cx| {
15668 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
15669 });
15670 follower
15671 .update(cx, |follower, window, cx| {
15672 follower.apply_update_proto(
15673 &project,
15674 pending_update.borrow_mut().take().unwrap(),
15675 window,
15676 cx,
15677 )
15678 })
15679 .unwrap()
15680 .await
15681 .unwrap();
15682 _ = follower.update(cx, |follower, _, cx| {
15683 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
15684 });
15685
15686 // Scrolling locally breaks the follow
15687 _ = follower.update(cx, |follower, window, cx| {
15688 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
15689 follower.set_scroll_anchor(
15690 ScrollAnchor {
15691 anchor: top_anchor,
15692 offset: gpui::Point::new(0.0, 0.5),
15693 },
15694 window,
15695 cx,
15696 );
15697 });
15698 assert!(!(*is_still_following.borrow()));
15699}
15700
15701#[gpui::test]
15702async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
15703 init_test(cx, |_| {});
15704
15705 let fs = FakeFs::new(cx.executor());
15706 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15707 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15708 let pane = workspace
15709 .update(cx, |workspace, _, _| workspace.active_pane().clone())
15710 .unwrap();
15711
15712 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15713
15714 let leader = pane.update_in(cx, |_, window, cx| {
15715 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
15716 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
15717 });
15718
15719 // Start following the editor when it has no excerpts.
15720 let mut state_message =
15721 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15722 let workspace_entity = workspace.root(cx).unwrap();
15723 let follower_1 = cx
15724 .update_window(*workspace.deref(), |_, window, cx| {
15725 Editor::from_state_proto(
15726 workspace_entity,
15727 ViewId {
15728 creator: CollaboratorId::PeerId(PeerId::default()),
15729 id: 0,
15730 },
15731 &mut state_message,
15732 window,
15733 cx,
15734 )
15735 })
15736 .unwrap()
15737 .unwrap()
15738 .await
15739 .unwrap();
15740
15741 let update_message = Rc::new(RefCell::new(None));
15742 follower_1.update_in(cx, {
15743 let update = update_message.clone();
15744 |_, window, cx| {
15745 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
15746 leader.read(cx).add_event_to_update_proto(
15747 event,
15748 &mut update.borrow_mut(),
15749 window,
15750 cx,
15751 );
15752 })
15753 .detach();
15754 }
15755 });
15756
15757 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
15758 (
15759 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
15760 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
15761 )
15762 });
15763
15764 // Insert some excerpts.
15765 leader.update(cx, |leader, cx| {
15766 leader.buffer.update(cx, |multibuffer, cx| {
15767 multibuffer.set_excerpts_for_path(
15768 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
15769 buffer_1.clone(),
15770 vec![
15771 Point::row_range(0..3),
15772 Point::row_range(1..6),
15773 Point::row_range(12..15),
15774 ],
15775 0,
15776 cx,
15777 );
15778 multibuffer.set_excerpts_for_path(
15779 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
15780 buffer_2.clone(),
15781 vec![Point::row_range(0..6), Point::row_range(8..12)],
15782 0,
15783 cx,
15784 );
15785 });
15786 });
15787
15788 // Apply the update of adding the excerpts.
15789 follower_1
15790 .update_in(cx, |follower, window, cx| {
15791 follower.apply_update_proto(
15792 &project,
15793 update_message.borrow().clone().unwrap(),
15794 window,
15795 cx,
15796 )
15797 })
15798 .await
15799 .unwrap();
15800 assert_eq!(
15801 follower_1.update(cx, |editor, cx| editor.text(cx)),
15802 leader.update(cx, |editor, cx| editor.text(cx))
15803 );
15804 update_message.borrow_mut().take();
15805
15806 // Start following separately after it already has excerpts.
15807 let mut state_message =
15808 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15809 let workspace_entity = workspace.root(cx).unwrap();
15810 let follower_2 = cx
15811 .update_window(*workspace.deref(), |_, window, cx| {
15812 Editor::from_state_proto(
15813 workspace_entity,
15814 ViewId {
15815 creator: CollaboratorId::PeerId(PeerId::default()),
15816 id: 0,
15817 },
15818 &mut state_message,
15819 window,
15820 cx,
15821 )
15822 })
15823 .unwrap()
15824 .unwrap()
15825 .await
15826 .unwrap();
15827 assert_eq!(
15828 follower_2.update(cx, |editor, cx| editor.text(cx)),
15829 leader.update(cx, |editor, cx| editor.text(cx))
15830 );
15831
15832 // Remove some excerpts.
15833 leader.update(cx, |leader, cx| {
15834 leader.buffer.update(cx, |multibuffer, cx| {
15835 let excerpt_ids = multibuffer.excerpt_ids();
15836 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
15837 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
15838 });
15839 });
15840
15841 // Apply the update of removing the excerpts.
15842 follower_1
15843 .update_in(cx, |follower, window, cx| {
15844 follower.apply_update_proto(
15845 &project,
15846 update_message.borrow().clone().unwrap(),
15847 window,
15848 cx,
15849 )
15850 })
15851 .await
15852 .unwrap();
15853 follower_2
15854 .update_in(cx, |follower, window, cx| {
15855 follower.apply_update_proto(
15856 &project,
15857 update_message.borrow().clone().unwrap(),
15858 window,
15859 cx,
15860 )
15861 })
15862 .await
15863 .unwrap();
15864 update_message.borrow_mut().take();
15865 assert_eq!(
15866 follower_1.update(cx, |editor, cx| editor.text(cx)),
15867 leader.update(cx, |editor, cx| editor.text(cx))
15868 );
15869}
15870
15871#[gpui::test]
15872async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15873 init_test(cx, |_| {});
15874
15875 let mut cx = EditorTestContext::new(cx).await;
15876 let lsp_store =
15877 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
15878
15879 cx.set_state(indoc! {"
15880 ˇfn func(abc def: i32) -> u32 {
15881 }
15882 "});
15883
15884 cx.update(|_, cx| {
15885 lsp_store.update(cx, |lsp_store, cx| {
15886 lsp_store
15887 .update_diagnostics(
15888 LanguageServerId(0),
15889 lsp::PublishDiagnosticsParams {
15890 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
15891 version: None,
15892 diagnostics: vec![
15893 lsp::Diagnostic {
15894 range: lsp::Range::new(
15895 lsp::Position::new(0, 11),
15896 lsp::Position::new(0, 12),
15897 ),
15898 severity: Some(lsp::DiagnosticSeverity::ERROR),
15899 ..Default::default()
15900 },
15901 lsp::Diagnostic {
15902 range: lsp::Range::new(
15903 lsp::Position::new(0, 12),
15904 lsp::Position::new(0, 15),
15905 ),
15906 severity: Some(lsp::DiagnosticSeverity::ERROR),
15907 ..Default::default()
15908 },
15909 lsp::Diagnostic {
15910 range: lsp::Range::new(
15911 lsp::Position::new(0, 25),
15912 lsp::Position::new(0, 28),
15913 ),
15914 severity: Some(lsp::DiagnosticSeverity::ERROR),
15915 ..Default::default()
15916 },
15917 ],
15918 },
15919 None,
15920 DiagnosticSourceKind::Pushed,
15921 &[],
15922 cx,
15923 )
15924 .unwrap()
15925 });
15926 });
15927
15928 executor.run_until_parked();
15929
15930 cx.update_editor(|editor, window, cx| {
15931 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15932 });
15933
15934 cx.assert_editor_state(indoc! {"
15935 fn func(abc def: i32) -> ˇu32 {
15936 }
15937 "});
15938
15939 cx.update_editor(|editor, window, cx| {
15940 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15941 });
15942
15943 cx.assert_editor_state(indoc! {"
15944 fn func(abc ˇdef: i32) -> u32 {
15945 }
15946 "});
15947
15948 cx.update_editor(|editor, window, cx| {
15949 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15950 });
15951
15952 cx.assert_editor_state(indoc! {"
15953 fn func(abcˇ def: i32) -> u32 {
15954 }
15955 "});
15956
15957 cx.update_editor(|editor, window, cx| {
15958 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15959 });
15960
15961 cx.assert_editor_state(indoc! {"
15962 fn func(abc def: i32) -> ˇu32 {
15963 }
15964 "});
15965}
15966
15967#[gpui::test]
15968async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15969 init_test(cx, |_| {});
15970
15971 let mut cx = EditorTestContext::new(cx).await;
15972
15973 let diff_base = r#"
15974 use some::mod;
15975
15976 const A: u32 = 42;
15977
15978 fn main() {
15979 println!("hello");
15980
15981 println!("world");
15982 }
15983 "#
15984 .unindent();
15985
15986 // Edits are modified, removed, modified, added
15987 cx.set_state(
15988 &r#"
15989 use some::modified;
15990
15991 ˇ
15992 fn main() {
15993 println!("hello there");
15994
15995 println!("around the");
15996 println!("world");
15997 }
15998 "#
15999 .unindent(),
16000 );
16001
16002 cx.set_head_text(&diff_base);
16003 executor.run_until_parked();
16004
16005 cx.update_editor(|editor, window, cx| {
16006 //Wrap around the bottom of the buffer
16007 for _ in 0..3 {
16008 editor.go_to_next_hunk(&GoToHunk, window, cx);
16009 }
16010 });
16011
16012 cx.assert_editor_state(
16013 &r#"
16014 ˇuse some::modified;
16015
16016
16017 fn main() {
16018 println!("hello there");
16019
16020 println!("around the");
16021 println!("world");
16022 }
16023 "#
16024 .unindent(),
16025 );
16026
16027 cx.update_editor(|editor, window, cx| {
16028 //Wrap around the top of the buffer
16029 for _ in 0..2 {
16030 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16031 }
16032 });
16033
16034 cx.assert_editor_state(
16035 &r#"
16036 use some::modified;
16037
16038
16039 fn main() {
16040 ˇ println!("hello there");
16041
16042 println!("around the");
16043 println!("world");
16044 }
16045 "#
16046 .unindent(),
16047 );
16048
16049 cx.update_editor(|editor, window, cx| {
16050 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16051 });
16052
16053 cx.assert_editor_state(
16054 &r#"
16055 use some::modified;
16056
16057 ˇ
16058 fn main() {
16059 println!("hello there");
16060
16061 println!("around the");
16062 println!("world");
16063 }
16064 "#
16065 .unindent(),
16066 );
16067
16068 cx.update_editor(|editor, window, cx| {
16069 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16070 });
16071
16072 cx.assert_editor_state(
16073 &r#"
16074 ˇuse some::modified;
16075
16076
16077 fn main() {
16078 println!("hello there");
16079
16080 println!("around the");
16081 println!("world");
16082 }
16083 "#
16084 .unindent(),
16085 );
16086
16087 cx.update_editor(|editor, window, cx| {
16088 for _ in 0..2 {
16089 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16090 }
16091 });
16092
16093 cx.assert_editor_state(
16094 &r#"
16095 use some::modified;
16096
16097
16098 fn main() {
16099 ˇ println!("hello there");
16100
16101 println!("around the");
16102 println!("world");
16103 }
16104 "#
16105 .unindent(),
16106 );
16107
16108 cx.update_editor(|editor, window, cx| {
16109 editor.fold(&Fold, window, cx);
16110 });
16111
16112 cx.update_editor(|editor, window, cx| {
16113 editor.go_to_next_hunk(&GoToHunk, window, cx);
16114 });
16115
16116 cx.assert_editor_state(
16117 &r#"
16118 ˇuse some::modified;
16119
16120
16121 fn main() {
16122 println!("hello there");
16123
16124 println!("around the");
16125 println!("world");
16126 }
16127 "#
16128 .unindent(),
16129 );
16130}
16131
16132#[test]
16133fn test_split_words() {
16134 fn split(text: &str) -> Vec<&str> {
16135 split_words(text).collect()
16136 }
16137
16138 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16139 assert_eq!(split("hello_world"), &["hello_", "world"]);
16140 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16141 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16142 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16143 assert_eq!(split("helloworld"), &["helloworld"]);
16144
16145 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16146}
16147
16148#[gpui::test]
16149async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16150 init_test(cx, |_| {});
16151
16152 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16153 let mut assert = |before, after| {
16154 let _state_context = cx.set_state(before);
16155 cx.run_until_parked();
16156 cx.update_editor(|editor, window, cx| {
16157 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16158 });
16159 cx.run_until_parked();
16160 cx.assert_editor_state(after);
16161 };
16162
16163 // Outside bracket jumps to outside of matching bracket
16164 assert("console.logˇ(var);", "console.log(var)ˇ;");
16165 assert("console.log(var)ˇ;", "console.logˇ(var);");
16166
16167 // Inside bracket jumps to inside of matching bracket
16168 assert("console.log(ˇvar);", "console.log(varˇ);");
16169 assert("console.log(varˇ);", "console.log(ˇvar);");
16170
16171 // When outside a bracket and inside, favor jumping to the inside bracket
16172 assert(
16173 "console.log('foo', [1, 2, 3]ˇ);",
16174 "console.log(ˇ'foo', [1, 2, 3]);",
16175 );
16176 assert(
16177 "console.log(ˇ'foo', [1, 2, 3]);",
16178 "console.log('foo', [1, 2, 3]ˇ);",
16179 );
16180
16181 // Bias forward if two options are equally likely
16182 assert(
16183 "let result = curried_fun()ˇ();",
16184 "let result = curried_fun()()ˇ;",
16185 );
16186
16187 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16188 assert(
16189 indoc! {"
16190 function test() {
16191 console.log('test')ˇ
16192 }"},
16193 indoc! {"
16194 function test() {
16195 console.logˇ('test')
16196 }"},
16197 );
16198}
16199
16200#[gpui::test]
16201async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16202 init_test(cx, |_| {});
16203
16204 let fs = FakeFs::new(cx.executor());
16205 fs.insert_tree(
16206 path!("/a"),
16207 json!({
16208 "main.rs": "fn main() { let a = 5; }",
16209 "other.rs": "// Test file",
16210 }),
16211 )
16212 .await;
16213 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16214
16215 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16216 language_registry.add(Arc::new(Language::new(
16217 LanguageConfig {
16218 name: "Rust".into(),
16219 matcher: LanguageMatcher {
16220 path_suffixes: vec!["rs".to_string()],
16221 ..Default::default()
16222 },
16223 brackets: BracketPairConfig {
16224 pairs: vec![BracketPair {
16225 start: "{".to_string(),
16226 end: "}".to_string(),
16227 close: true,
16228 surround: true,
16229 newline: true,
16230 }],
16231 disabled_scopes_by_bracket_ix: Vec::new(),
16232 },
16233 ..Default::default()
16234 },
16235 Some(tree_sitter_rust::LANGUAGE.into()),
16236 )));
16237 let mut fake_servers = language_registry.register_fake_lsp(
16238 "Rust",
16239 FakeLspAdapter {
16240 capabilities: lsp::ServerCapabilities {
16241 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16242 first_trigger_character: "{".to_string(),
16243 more_trigger_character: None,
16244 }),
16245 ..Default::default()
16246 },
16247 ..Default::default()
16248 },
16249 );
16250
16251 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16252
16253 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16254
16255 let worktree_id = workspace
16256 .update(cx, |workspace, _, cx| {
16257 workspace.project().update(cx, |project, cx| {
16258 project.worktrees(cx).next().unwrap().read(cx).id()
16259 })
16260 })
16261 .unwrap();
16262
16263 let buffer = project
16264 .update(cx, |project, cx| {
16265 project.open_local_buffer(path!("/a/main.rs"), cx)
16266 })
16267 .await
16268 .unwrap();
16269 let editor_handle = workspace
16270 .update(cx, |workspace, window, cx| {
16271 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16272 })
16273 .unwrap()
16274 .await
16275 .unwrap()
16276 .downcast::<Editor>()
16277 .unwrap();
16278
16279 cx.executor().start_waiting();
16280 let fake_server = fake_servers.next().await.unwrap();
16281
16282 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16283 |params, _| async move {
16284 assert_eq!(
16285 params.text_document_position.text_document.uri,
16286 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16287 );
16288 assert_eq!(
16289 params.text_document_position.position,
16290 lsp::Position::new(0, 21),
16291 );
16292
16293 Ok(Some(vec![lsp::TextEdit {
16294 new_text: "]".to_string(),
16295 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16296 }]))
16297 },
16298 );
16299
16300 editor_handle.update_in(cx, |editor, window, cx| {
16301 window.focus(&editor.focus_handle(cx));
16302 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16303 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16304 });
16305 editor.handle_input("{", window, cx);
16306 });
16307
16308 cx.executor().run_until_parked();
16309
16310 buffer.update(cx, |buffer, _| {
16311 assert_eq!(
16312 buffer.text(),
16313 "fn main() { let a = {5}; }",
16314 "No extra braces from on type formatting should appear in the buffer"
16315 )
16316 });
16317}
16318
16319#[gpui::test(iterations = 20, seeds(31))]
16320async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16321 init_test(cx, |_| {});
16322
16323 let mut cx = EditorLspTestContext::new_rust(
16324 lsp::ServerCapabilities {
16325 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16326 first_trigger_character: ".".to_string(),
16327 more_trigger_character: None,
16328 }),
16329 ..Default::default()
16330 },
16331 cx,
16332 )
16333 .await;
16334
16335 cx.update_buffer(|buffer, _| {
16336 // This causes autoindent to be async.
16337 buffer.set_sync_parse_timeout(Duration::ZERO)
16338 });
16339
16340 cx.set_state("fn c() {\n d()ˇ\n}\n");
16341 cx.simulate_keystroke("\n");
16342 cx.run_until_parked();
16343
16344 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16345 let mut request =
16346 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16347 let buffer_cloned = buffer_cloned.clone();
16348 async move {
16349 buffer_cloned.update(&mut cx, |buffer, _| {
16350 assert_eq!(
16351 buffer.text(),
16352 "fn c() {\n d()\n .\n}\n",
16353 "OnTypeFormatting should triggered after autoindent applied"
16354 )
16355 })?;
16356
16357 Ok(Some(vec![]))
16358 }
16359 });
16360
16361 cx.simulate_keystroke(".");
16362 cx.run_until_parked();
16363
16364 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16365 assert!(request.next().await.is_some());
16366 request.close();
16367 assert!(request.next().await.is_none());
16368}
16369
16370#[gpui::test]
16371async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
16372 init_test(cx, |_| {});
16373
16374 let fs = FakeFs::new(cx.executor());
16375 fs.insert_tree(
16376 path!("/a"),
16377 json!({
16378 "main.rs": "fn main() { let a = 5; }",
16379 "other.rs": "// Test file",
16380 }),
16381 )
16382 .await;
16383
16384 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16385
16386 let server_restarts = Arc::new(AtomicUsize::new(0));
16387 let closure_restarts = Arc::clone(&server_restarts);
16388 let language_server_name = "test language server";
16389 let language_name: LanguageName = "Rust".into();
16390
16391 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16392 language_registry.add(Arc::new(Language::new(
16393 LanguageConfig {
16394 name: language_name.clone(),
16395 matcher: LanguageMatcher {
16396 path_suffixes: vec!["rs".to_string()],
16397 ..Default::default()
16398 },
16399 ..Default::default()
16400 },
16401 Some(tree_sitter_rust::LANGUAGE.into()),
16402 )));
16403 let mut fake_servers = language_registry.register_fake_lsp(
16404 "Rust",
16405 FakeLspAdapter {
16406 name: language_server_name,
16407 initialization_options: Some(json!({
16408 "testOptionValue": true
16409 })),
16410 initializer: Some(Box::new(move |fake_server| {
16411 let task_restarts = Arc::clone(&closure_restarts);
16412 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
16413 task_restarts.fetch_add(1, atomic::Ordering::Release);
16414 futures::future::ready(Ok(()))
16415 });
16416 })),
16417 ..Default::default()
16418 },
16419 );
16420
16421 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16422 let _buffer = project
16423 .update(cx, |project, cx| {
16424 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
16425 })
16426 .await
16427 .unwrap();
16428 let _fake_server = fake_servers.next().await.unwrap();
16429 update_test_language_settings(cx, |language_settings| {
16430 language_settings.languages.0.insert(
16431 language_name.clone(),
16432 LanguageSettingsContent {
16433 tab_size: NonZeroU32::new(8),
16434 ..Default::default()
16435 },
16436 );
16437 });
16438 cx.executor().run_until_parked();
16439 assert_eq!(
16440 server_restarts.load(atomic::Ordering::Acquire),
16441 0,
16442 "Should not restart LSP server on an unrelated change"
16443 );
16444
16445 update_test_project_settings(cx, |project_settings| {
16446 project_settings.lsp.insert(
16447 "Some other server name".into(),
16448 LspSettings {
16449 binary: None,
16450 settings: None,
16451 initialization_options: Some(json!({
16452 "some other init value": false
16453 })),
16454 enable_lsp_tasks: false,
16455 },
16456 );
16457 });
16458 cx.executor().run_until_parked();
16459 assert_eq!(
16460 server_restarts.load(atomic::Ordering::Acquire),
16461 0,
16462 "Should not restart LSP server on an unrelated LSP settings change"
16463 );
16464
16465 update_test_project_settings(cx, |project_settings| {
16466 project_settings.lsp.insert(
16467 language_server_name.into(),
16468 LspSettings {
16469 binary: None,
16470 settings: None,
16471 initialization_options: Some(json!({
16472 "anotherInitValue": false
16473 })),
16474 enable_lsp_tasks: false,
16475 },
16476 );
16477 });
16478 cx.executor().run_until_parked();
16479 assert_eq!(
16480 server_restarts.load(atomic::Ordering::Acquire),
16481 1,
16482 "Should restart LSP server on a related LSP settings change"
16483 );
16484
16485 update_test_project_settings(cx, |project_settings| {
16486 project_settings.lsp.insert(
16487 language_server_name.into(),
16488 LspSettings {
16489 binary: None,
16490 settings: None,
16491 initialization_options: Some(json!({
16492 "anotherInitValue": false
16493 })),
16494 enable_lsp_tasks: false,
16495 },
16496 );
16497 });
16498 cx.executor().run_until_parked();
16499 assert_eq!(
16500 server_restarts.load(atomic::Ordering::Acquire),
16501 1,
16502 "Should not restart LSP server on a related LSP settings change that is the same"
16503 );
16504
16505 update_test_project_settings(cx, |project_settings| {
16506 project_settings.lsp.insert(
16507 language_server_name.into(),
16508 LspSettings {
16509 binary: None,
16510 settings: None,
16511 initialization_options: None,
16512 enable_lsp_tasks: false,
16513 },
16514 );
16515 });
16516 cx.executor().run_until_parked();
16517 assert_eq!(
16518 server_restarts.load(atomic::Ordering::Acquire),
16519 2,
16520 "Should restart LSP server on another related LSP settings change"
16521 );
16522}
16523
16524#[gpui::test]
16525async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
16526 init_test(cx, |_| {});
16527
16528 let mut cx = EditorLspTestContext::new_rust(
16529 lsp::ServerCapabilities {
16530 completion_provider: Some(lsp::CompletionOptions {
16531 trigger_characters: Some(vec![".".to_string()]),
16532 resolve_provider: Some(true),
16533 ..Default::default()
16534 }),
16535 ..Default::default()
16536 },
16537 cx,
16538 )
16539 .await;
16540
16541 cx.set_state("fn main() { let a = 2ˇ; }");
16542 cx.simulate_keystroke(".");
16543 let completion_item = lsp::CompletionItem {
16544 label: "some".into(),
16545 kind: Some(lsp::CompletionItemKind::SNIPPET),
16546 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16547 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16548 kind: lsp::MarkupKind::Markdown,
16549 value: "```rust\nSome(2)\n```".to_string(),
16550 })),
16551 deprecated: Some(false),
16552 sort_text: Some("fffffff2".to_string()),
16553 filter_text: Some("some".to_string()),
16554 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16555 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16556 range: lsp::Range {
16557 start: lsp::Position {
16558 line: 0,
16559 character: 22,
16560 },
16561 end: lsp::Position {
16562 line: 0,
16563 character: 22,
16564 },
16565 },
16566 new_text: "Some(2)".to_string(),
16567 })),
16568 additional_text_edits: Some(vec![lsp::TextEdit {
16569 range: lsp::Range {
16570 start: lsp::Position {
16571 line: 0,
16572 character: 20,
16573 },
16574 end: lsp::Position {
16575 line: 0,
16576 character: 22,
16577 },
16578 },
16579 new_text: "".to_string(),
16580 }]),
16581 ..Default::default()
16582 };
16583
16584 let closure_completion_item = completion_item.clone();
16585 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16586 let task_completion_item = closure_completion_item.clone();
16587 async move {
16588 Ok(Some(lsp::CompletionResponse::Array(vec![
16589 task_completion_item,
16590 ])))
16591 }
16592 });
16593
16594 request.next().await;
16595
16596 cx.condition(|editor, _| editor.context_menu_visible())
16597 .await;
16598 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16599 editor
16600 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16601 .unwrap()
16602 });
16603 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16604
16605 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16606 let task_completion_item = completion_item.clone();
16607 async move { Ok(task_completion_item) }
16608 })
16609 .next()
16610 .await
16611 .unwrap();
16612 apply_additional_edits.await.unwrap();
16613 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16614}
16615
16616#[gpui::test]
16617async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16618 init_test(cx, |_| {});
16619
16620 let mut cx = EditorLspTestContext::new_rust(
16621 lsp::ServerCapabilities {
16622 completion_provider: Some(lsp::CompletionOptions {
16623 trigger_characters: Some(vec![".".to_string()]),
16624 resolve_provider: Some(true),
16625 ..Default::default()
16626 }),
16627 ..Default::default()
16628 },
16629 cx,
16630 )
16631 .await;
16632
16633 cx.set_state("fn main() { let a = 2ˇ; }");
16634 cx.simulate_keystroke(".");
16635
16636 let item1 = lsp::CompletionItem {
16637 label: "method id()".to_string(),
16638 filter_text: Some("id".to_string()),
16639 detail: None,
16640 documentation: None,
16641 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16642 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16643 new_text: ".id".to_string(),
16644 })),
16645 ..lsp::CompletionItem::default()
16646 };
16647
16648 let item2 = lsp::CompletionItem {
16649 label: "other".to_string(),
16650 filter_text: Some("other".to_string()),
16651 detail: None,
16652 documentation: None,
16653 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16654 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16655 new_text: ".other".to_string(),
16656 })),
16657 ..lsp::CompletionItem::default()
16658 };
16659
16660 let item1 = item1.clone();
16661 cx.set_request_handler::<lsp::request::Completion, _, _>({
16662 let item1 = item1.clone();
16663 move |_, _, _| {
16664 let item1 = item1.clone();
16665 let item2 = item2.clone();
16666 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
16667 }
16668 })
16669 .next()
16670 .await;
16671
16672 cx.condition(|editor, _| editor.context_menu_visible())
16673 .await;
16674 cx.update_editor(|editor, _, _| {
16675 let context_menu = editor.context_menu.borrow_mut();
16676 let context_menu = context_menu
16677 .as_ref()
16678 .expect("Should have the context menu deployed");
16679 match context_menu {
16680 CodeContextMenu::Completions(completions_menu) => {
16681 let completions = completions_menu.completions.borrow_mut();
16682 assert_eq!(
16683 completions
16684 .iter()
16685 .map(|completion| &completion.label.text)
16686 .collect::<Vec<_>>(),
16687 vec!["method id()", "other"]
16688 )
16689 }
16690 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16691 }
16692 });
16693
16694 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
16695 let item1 = item1.clone();
16696 move |_, item_to_resolve, _| {
16697 let item1 = item1.clone();
16698 async move {
16699 if item1 == item_to_resolve {
16700 Ok(lsp::CompletionItem {
16701 label: "method id()".to_string(),
16702 filter_text: Some("id".to_string()),
16703 detail: Some("Now resolved!".to_string()),
16704 documentation: Some(lsp::Documentation::String("Docs".to_string())),
16705 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16706 range: lsp::Range::new(
16707 lsp::Position::new(0, 22),
16708 lsp::Position::new(0, 22),
16709 ),
16710 new_text: ".id".to_string(),
16711 })),
16712 ..lsp::CompletionItem::default()
16713 })
16714 } else {
16715 Ok(item_to_resolve)
16716 }
16717 }
16718 }
16719 })
16720 .next()
16721 .await
16722 .unwrap();
16723 cx.run_until_parked();
16724
16725 cx.update_editor(|editor, window, cx| {
16726 editor.context_menu_next(&Default::default(), window, cx);
16727 });
16728
16729 cx.update_editor(|editor, _, _| {
16730 let context_menu = editor.context_menu.borrow_mut();
16731 let context_menu = context_menu
16732 .as_ref()
16733 .expect("Should have the context menu deployed");
16734 match context_menu {
16735 CodeContextMenu::Completions(completions_menu) => {
16736 let completions = completions_menu.completions.borrow_mut();
16737 assert_eq!(
16738 completions
16739 .iter()
16740 .map(|completion| &completion.label.text)
16741 .collect::<Vec<_>>(),
16742 vec!["method id() Now resolved!", "other"],
16743 "Should update first completion label, but not second as the filter text did not match."
16744 );
16745 }
16746 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16747 }
16748 });
16749}
16750
16751#[gpui::test]
16752async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
16753 init_test(cx, |_| {});
16754 let mut cx = EditorLspTestContext::new_rust(
16755 lsp::ServerCapabilities {
16756 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
16757 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
16758 completion_provider: Some(lsp::CompletionOptions {
16759 resolve_provider: Some(true),
16760 ..Default::default()
16761 }),
16762 ..Default::default()
16763 },
16764 cx,
16765 )
16766 .await;
16767 cx.set_state(indoc! {"
16768 struct TestStruct {
16769 field: i32
16770 }
16771
16772 fn mainˇ() {
16773 let unused_var = 42;
16774 let test_struct = TestStruct { field: 42 };
16775 }
16776 "});
16777 let symbol_range = cx.lsp_range(indoc! {"
16778 struct TestStruct {
16779 field: i32
16780 }
16781
16782 «fn main»() {
16783 let unused_var = 42;
16784 let test_struct = TestStruct { field: 42 };
16785 }
16786 "});
16787 let mut hover_requests =
16788 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
16789 Ok(Some(lsp::Hover {
16790 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
16791 kind: lsp::MarkupKind::Markdown,
16792 value: "Function documentation".to_string(),
16793 }),
16794 range: Some(symbol_range),
16795 }))
16796 });
16797
16798 // Case 1: Test that code action menu hide hover popover
16799 cx.dispatch_action(Hover);
16800 hover_requests.next().await;
16801 cx.condition(|editor, _| editor.hover_state.visible()).await;
16802 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
16803 move |_, _, _| async move {
16804 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
16805 lsp::CodeAction {
16806 title: "Remove unused variable".to_string(),
16807 kind: Some(CodeActionKind::QUICKFIX),
16808 edit: Some(lsp::WorkspaceEdit {
16809 changes: Some(
16810 [(
16811 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
16812 vec![lsp::TextEdit {
16813 range: lsp::Range::new(
16814 lsp::Position::new(5, 4),
16815 lsp::Position::new(5, 27),
16816 ),
16817 new_text: "".to_string(),
16818 }],
16819 )]
16820 .into_iter()
16821 .collect(),
16822 ),
16823 ..Default::default()
16824 }),
16825 ..Default::default()
16826 },
16827 )]))
16828 },
16829 );
16830 cx.update_editor(|editor, window, cx| {
16831 editor.toggle_code_actions(
16832 &ToggleCodeActions {
16833 deployed_from: None,
16834 quick_launch: false,
16835 },
16836 window,
16837 cx,
16838 );
16839 });
16840 code_action_requests.next().await;
16841 cx.run_until_parked();
16842 cx.condition(|editor, _| editor.context_menu_visible())
16843 .await;
16844 cx.update_editor(|editor, _, _| {
16845 assert!(
16846 !editor.hover_state.visible(),
16847 "Hover popover should be hidden when code action menu is shown"
16848 );
16849 // Hide code actions
16850 editor.context_menu.take();
16851 });
16852
16853 // Case 2: Test that code completions hide hover popover
16854 cx.dispatch_action(Hover);
16855 hover_requests.next().await;
16856 cx.condition(|editor, _| editor.hover_state.visible()).await;
16857 let counter = Arc::new(AtomicUsize::new(0));
16858 let mut completion_requests =
16859 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16860 let counter = counter.clone();
16861 async move {
16862 counter.fetch_add(1, atomic::Ordering::Release);
16863 Ok(Some(lsp::CompletionResponse::Array(vec![
16864 lsp::CompletionItem {
16865 label: "main".into(),
16866 kind: Some(lsp::CompletionItemKind::FUNCTION),
16867 detail: Some("() -> ()".to_string()),
16868 ..Default::default()
16869 },
16870 lsp::CompletionItem {
16871 label: "TestStruct".into(),
16872 kind: Some(lsp::CompletionItemKind::STRUCT),
16873 detail: Some("struct TestStruct".to_string()),
16874 ..Default::default()
16875 },
16876 ])))
16877 }
16878 });
16879 cx.update_editor(|editor, window, cx| {
16880 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16881 });
16882 completion_requests.next().await;
16883 cx.condition(|editor, _| editor.context_menu_visible())
16884 .await;
16885 cx.update_editor(|editor, _, _| {
16886 assert!(
16887 !editor.hover_state.visible(),
16888 "Hover popover should be hidden when completion menu is shown"
16889 );
16890 });
16891}
16892
16893#[gpui::test]
16894async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16895 init_test(cx, |_| {});
16896
16897 let mut cx = EditorLspTestContext::new_rust(
16898 lsp::ServerCapabilities {
16899 completion_provider: Some(lsp::CompletionOptions {
16900 trigger_characters: Some(vec![".".to_string()]),
16901 resolve_provider: Some(true),
16902 ..Default::default()
16903 }),
16904 ..Default::default()
16905 },
16906 cx,
16907 )
16908 .await;
16909
16910 cx.set_state("fn main() { let a = 2ˇ; }");
16911 cx.simulate_keystroke(".");
16912
16913 let unresolved_item_1 = lsp::CompletionItem {
16914 label: "id".to_string(),
16915 filter_text: Some("id".to_string()),
16916 detail: None,
16917 documentation: None,
16918 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16919 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16920 new_text: ".id".to_string(),
16921 })),
16922 ..lsp::CompletionItem::default()
16923 };
16924 let resolved_item_1 = lsp::CompletionItem {
16925 additional_text_edits: Some(vec![lsp::TextEdit {
16926 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16927 new_text: "!!".to_string(),
16928 }]),
16929 ..unresolved_item_1.clone()
16930 };
16931 let unresolved_item_2 = lsp::CompletionItem {
16932 label: "other".to_string(),
16933 filter_text: Some("other".to_string()),
16934 detail: None,
16935 documentation: None,
16936 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16937 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16938 new_text: ".other".to_string(),
16939 })),
16940 ..lsp::CompletionItem::default()
16941 };
16942 let resolved_item_2 = lsp::CompletionItem {
16943 additional_text_edits: Some(vec![lsp::TextEdit {
16944 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16945 new_text: "??".to_string(),
16946 }]),
16947 ..unresolved_item_2.clone()
16948 };
16949
16950 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16951 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16952 cx.lsp
16953 .server
16954 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16955 let unresolved_item_1 = unresolved_item_1.clone();
16956 let resolved_item_1 = resolved_item_1.clone();
16957 let unresolved_item_2 = unresolved_item_2.clone();
16958 let resolved_item_2 = resolved_item_2.clone();
16959 let resolve_requests_1 = resolve_requests_1.clone();
16960 let resolve_requests_2 = resolve_requests_2.clone();
16961 move |unresolved_request, _| {
16962 let unresolved_item_1 = unresolved_item_1.clone();
16963 let resolved_item_1 = resolved_item_1.clone();
16964 let unresolved_item_2 = unresolved_item_2.clone();
16965 let resolved_item_2 = resolved_item_2.clone();
16966 let resolve_requests_1 = resolve_requests_1.clone();
16967 let resolve_requests_2 = resolve_requests_2.clone();
16968 async move {
16969 if unresolved_request == unresolved_item_1 {
16970 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16971 Ok(resolved_item_1.clone())
16972 } else if unresolved_request == unresolved_item_2 {
16973 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16974 Ok(resolved_item_2.clone())
16975 } else {
16976 panic!("Unexpected completion item {unresolved_request:?}")
16977 }
16978 }
16979 }
16980 })
16981 .detach();
16982
16983 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16984 let unresolved_item_1 = unresolved_item_1.clone();
16985 let unresolved_item_2 = unresolved_item_2.clone();
16986 async move {
16987 Ok(Some(lsp::CompletionResponse::Array(vec![
16988 unresolved_item_1,
16989 unresolved_item_2,
16990 ])))
16991 }
16992 })
16993 .next()
16994 .await;
16995
16996 cx.condition(|editor, _| editor.context_menu_visible())
16997 .await;
16998 cx.update_editor(|editor, _, _| {
16999 let context_menu = editor.context_menu.borrow_mut();
17000 let context_menu = context_menu
17001 .as_ref()
17002 .expect("Should have the context menu deployed");
17003 match context_menu {
17004 CodeContextMenu::Completions(completions_menu) => {
17005 let completions = completions_menu.completions.borrow_mut();
17006 assert_eq!(
17007 completions
17008 .iter()
17009 .map(|completion| &completion.label.text)
17010 .collect::<Vec<_>>(),
17011 vec!["id", "other"]
17012 )
17013 }
17014 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17015 }
17016 });
17017 cx.run_until_parked();
17018
17019 cx.update_editor(|editor, window, cx| {
17020 editor.context_menu_next(&ContextMenuNext, window, cx);
17021 });
17022 cx.run_until_parked();
17023 cx.update_editor(|editor, window, cx| {
17024 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17025 });
17026 cx.run_until_parked();
17027 cx.update_editor(|editor, window, cx| {
17028 editor.context_menu_next(&ContextMenuNext, window, cx);
17029 });
17030 cx.run_until_parked();
17031 cx.update_editor(|editor, window, cx| {
17032 editor
17033 .compose_completion(&ComposeCompletion::default(), window, cx)
17034 .expect("No task returned")
17035 })
17036 .await
17037 .expect("Completion failed");
17038 cx.run_until_parked();
17039
17040 cx.update_editor(|editor, _, cx| {
17041 assert_eq!(
17042 resolve_requests_1.load(atomic::Ordering::Acquire),
17043 1,
17044 "Should always resolve once despite multiple selections"
17045 );
17046 assert_eq!(
17047 resolve_requests_2.load(atomic::Ordering::Acquire),
17048 1,
17049 "Should always resolve once after multiple selections and applying the completion"
17050 );
17051 assert_eq!(
17052 editor.text(cx),
17053 "fn main() { let a = ??.other; }",
17054 "Should use resolved data when applying the completion"
17055 );
17056 });
17057}
17058
17059#[gpui::test]
17060async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17061 init_test(cx, |_| {});
17062
17063 let item_0 = lsp::CompletionItem {
17064 label: "abs".into(),
17065 insert_text: Some("abs".into()),
17066 data: Some(json!({ "very": "special"})),
17067 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17068 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17069 lsp::InsertReplaceEdit {
17070 new_text: "abs".to_string(),
17071 insert: lsp::Range::default(),
17072 replace: lsp::Range::default(),
17073 },
17074 )),
17075 ..lsp::CompletionItem::default()
17076 };
17077 let items = iter::once(item_0.clone())
17078 .chain((11..51).map(|i| lsp::CompletionItem {
17079 label: format!("item_{}", i),
17080 insert_text: Some(format!("item_{}", i)),
17081 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17082 ..lsp::CompletionItem::default()
17083 }))
17084 .collect::<Vec<_>>();
17085
17086 let default_commit_characters = vec!["?".to_string()];
17087 let default_data = json!({ "default": "data"});
17088 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17089 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17090 let default_edit_range = lsp::Range {
17091 start: lsp::Position {
17092 line: 0,
17093 character: 5,
17094 },
17095 end: lsp::Position {
17096 line: 0,
17097 character: 5,
17098 },
17099 };
17100
17101 let mut cx = EditorLspTestContext::new_rust(
17102 lsp::ServerCapabilities {
17103 completion_provider: Some(lsp::CompletionOptions {
17104 trigger_characters: Some(vec![".".to_string()]),
17105 resolve_provider: Some(true),
17106 ..Default::default()
17107 }),
17108 ..Default::default()
17109 },
17110 cx,
17111 )
17112 .await;
17113
17114 cx.set_state("fn main() { let a = 2ˇ; }");
17115 cx.simulate_keystroke(".");
17116
17117 let completion_data = default_data.clone();
17118 let completion_characters = default_commit_characters.clone();
17119 let completion_items = items.clone();
17120 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17121 let default_data = completion_data.clone();
17122 let default_commit_characters = completion_characters.clone();
17123 let items = completion_items.clone();
17124 async move {
17125 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17126 items,
17127 item_defaults: Some(lsp::CompletionListItemDefaults {
17128 data: Some(default_data.clone()),
17129 commit_characters: Some(default_commit_characters.clone()),
17130 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17131 default_edit_range,
17132 )),
17133 insert_text_format: Some(default_insert_text_format),
17134 insert_text_mode: Some(default_insert_text_mode),
17135 }),
17136 ..lsp::CompletionList::default()
17137 })))
17138 }
17139 })
17140 .next()
17141 .await;
17142
17143 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17144 cx.lsp
17145 .server
17146 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17147 let closure_resolved_items = resolved_items.clone();
17148 move |item_to_resolve, _| {
17149 let closure_resolved_items = closure_resolved_items.clone();
17150 async move {
17151 closure_resolved_items.lock().push(item_to_resolve.clone());
17152 Ok(item_to_resolve)
17153 }
17154 }
17155 })
17156 .detach();
17157
17158 cx.condition(|editor, _| editor.context_menu_visible())
17159 .await;
17160 cx.run_until_parked();
17161 cx.update_editor(|editor, _, _| {
17162 let menu = editor.context_menu.borrow_mut();
17163 match menu.as_ref().expect("should have the completions menu") {
17164 CodeContextMenu::Completions(completions_menu) => {
17165 assert_eq!(
17166 completions_menu
17167 .entries
17168 .borrow()
17169 .iter()
17170 .map(|mat| mat.string.clone())
17171 .collect::<Vec<String>>(),
17172 items
17173 .iter()
17174 .map(|completion| completion.label.clone())
17175 .collect::<Vec<String>>()
17176 );
17177 }
17178 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17179 }
17180 });
17181 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17182 // with 4 from the end.
17183 assert_eq!(
17184 *resolved_items.lock(),
17185 [&items[0..16], &items[items.len() - 4..items.len()]]
17186 .concat()
17187 .iter()
17188 .cloned()
17189 .map(|mut item| {
17190 if item.data.is_none() {
17191 item.data = Some(default_data.clone());
17192 }
17193 item
17194 })
17195 .collect::<Vec<lsp::CompletionItem>>(),
17196 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17197 );
17198 resolved_items.lock().clear();
17199
17200 cx.update_editor(|editor, window, cx| {
17201 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17202 });
17203 cx.run_until_parked();
17204 // Completions that have already been resolved are skipped.
17205 assert_eq!(
17206 *resolved_items.lock(),
17207 items[items.len() - 17..items.len() - 4]
17208 .iter()
17209 .cloned()
17210 .map(|mut item| {
17211 if item.data.is_none() {
17212 item.data = Some(default_data.clone());
17213 }
17214 item
17215 })
17216 .collect::<Vec<lsp::CompletionItem>>()
17217 );
17218 resolved_items.lock().clear();
17219}
17220
17221#[gpui::test]
17222async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17223 init_test(cx, |_| {});
17224
17225 let mut cx = EditorLspTestContext::new(
17226 Language::new(
17227 LanguageConfig {
17228 matcher: LanguageMatcher {
17229 path_suffixes: vec!["jsx".into()],
17230 ..Default::default()
17231 },
17232 overrides: [(
17233 "element".into(),
17234 LanguageConfigOverride {
17235 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17236 ..Default::default()
17237 },
17238 )]
17239 .into_iter()
17240 .collect(),
17241 ..Default::default()
17242 },
17243 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17244 )
17245 .with_override_query("(jsx_self_closing_element) @element")
17246 .unwrap(),
17247 lsp::ServerCapabilities {
17248 completion_provider: Some(lsp::CompletionOptions {
17249 trigger_characters: Some(vec![":".to_string()]),
17250 ..Default::default()
17251 }),
17252 ..Default::default()
17253 },
17254 cx,
17255 )
17256 .await;
17257
17258 cx.lsp
17259 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17260 Ok(Some(lsp::CompletionResponse::Array(vec![
17261 lsp::CompletionItem {
17262 label: "bg-blue".into(),
17263 ..Default::default()
17264 },
17265 lsp::CompletionItem {
17266 label: "bg-red".into(),
17267 ..Default::default()
17268 },
17269 lsp::CompletionItem {
17270 label: "bg-yellow".into(),
17271 ..Default::default()
17272 },
17273 ])))
17274 });
17275
17276 cx.set_state(r#"<p class="bgˇ" />"#);
17277
17278 // Trigger completion when typing a dash, because the dash is an extra
17279 // word character in the 'element' scope, which contains the cursor.
17280 cx.simulate_keystroke("-");
17281 cx.executor().run_until_parked();
17282 cx.update_editor(|editor, _, _| {
17283 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17284 {
17285 assert_eq!(
17286 completion_menu_entries(menu),
17287 &["bg-blue", "bg-red", "bg-yellow"]
17288 );
17289 } else {
17290 panic!("expected completion menu to be open");
17291 }
17292 });
17293
17294 cx.simulate_keystroke("l");
17295 cx.executor().run_until_parked();
17296 cx.update_editor(|editor, _, _| {
17297 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17298 {
17299 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17300 } else {
17301 panic!("expected completion menu to be open");
17302 }
17303 });
17304
17305 // When filtering completions, consider the character after the '-' to
17306 // be the start of a subword.
17307 cx.set_state(r#"<p class="yelˇ" />"#);
17308 cx.simulate_keystroke("l");
17309 cx.executor().run_until_parked();
17310 cx.update_editor(|editor, _, _| {
17311 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17312 {
17313 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17314 } else {
17315 panic!("expected completion menu to be open");
17316 }
17317 });
17318}
17319
17320fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17321 let entries = menu.entries.borrow();
17322 entries.iter().map(|mat| mat.string.clone()).collect()
17323}
17324
17325#[gpui::test]
17326async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17327 init_test(cx, |settings| {
17328 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17329 Formatter::Prettier,
17330 )))
17331 });
17332
17333 let fs = FakeFs::new(cx.executor());
17334 fs.insert_file(path!("/file.ts"), Default::default()).await;
17335
17336 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17337 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17338
17339 language_registry.add(Arc::new(Language::new(
17340 LanguageConfig {
17341 name: "TypeScript".into(),
17342 matcher: LanguageMatcher {
17343 path_suffixes: vec!["ts".to_string()],
17344 ..Default::default()
17345 },
17346 ..Default::default()
17347 },
17348 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17349 )));
17350 update_test_language_settings(cx, |settings| {
17351 settings.defaults.prettier = Some(PrettierSettings {
17352 allowed: true,
17353 ..PrettierSettings::default()
17354 });
17355 });
17356
17357 let test_plugin = "test_plugin";
17358 let _ = language_registry.register_fake_lsp(
17359 "TypeScript",
17360 FakeLspAdapter {
17361 prettier_plugins: vec![test_plugin],
17362 ..Default::default()
17363 },
17364 );
17365
17366 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17367 let buffer = project
17368 .update(cx, |project, cx| {
17369 project.open_local_buffer(path!("/file.ts"), cx)
17370 })
17371 .await
17372 .unwrap();
17373
17374 let buffer_text = "one\ntwo\nthree\n";
17375 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17376 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17377 editor.update_in(cx, |editor, window, cx| {
17378 editor.set_text(buffer_text, window, cx)
17379 });
17380
17381 editor
17382 .update_in(cx, |editor, window, cx| {
17383 editor.perform_format(
17384 project.clone(),
17385 FormatTrigger::Manual,
17386 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17387 window,
17388 cx,
17389 )
17390 })
17391 .unwrap()
17392 .await;
17393 assert_eq!(
17394 editor.update(cx, |editor, cx| editor.text(cx)),
17395 buffer_text.to_string() + prettier_format_suffix,
17396 "Test prettier formatting was not applied to the original buffer text",
17397 );
17398
17399 update_test_language_settings(cx, |settings| {
17400 settings.defaults.formatter = Some(SelectedFormatter::Auto)
17401 });
17402 let format = editor.update_in(cx, |editor, window, cx| {
17403 editor.perform_format(
17404 project.clone(),
17405 FormatTrigger::Manual,
17406 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17407 window,
17408 cx,
17409 )
17410 });
17411 format.await.unwrap();
17412 assert_eq!(
17413 editor.update(cx, |editor, cx| editor.text(cx)),
17414 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
17415 "Autoformatting (via test prettier) was not applied to the original buffer text",
17416 );
17417}
17418
17419#[gpui::test]
17420async fn test_addition_reverts(cx: &mut TestAppContext) {
17421 init_test(cx, |_| {});
17422 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17423 let base_text = indoc! {r#"
17424 struct Row;
17425 struct Row1;
17426 struct Row2;
17427
17428 struct Row4;
17429 struct Row5;
17430 struct Row6;
17431
17432 struct Row8;
17433 struct Row9;
17434 struct Row10;"#};
17435
17436 // When addition hunks are not adjacent to carets, no hunk revert is performed
17437 assert_hunk_revert(
17438 indoc! {r#"struct Row;
17439 struct Row1;
17440 struct Row1.1;
17441 struct Row1.2;
17442 struct Row2;ˇ
17443
17444 struct Row4;
17445 struct Row5;
17446 struct Row6;
17447
17448 struct Row8;
17449 ˇstruct Row9;
17450 struct Row9.1;
17451 struct Row9.2;
17452 struct Row9.3;
17453 struct Row10;"#},
17454 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17455 indoc! {r#"struct Row;
17456 struct Row1;
17457 struct Row1.1;
17458 struct Row1.2;
17459 struct Row2;ˇ
17460
17461 struct Row4;
17462 struct Row5;
17463 struct Row6;
17464
17465 struct Row8;
17466 ˇstruct Row9;
17467 struct Row9.1;
17468 struct Row9.2;
17469 struct Row9.3;
17470 struct Row10;"#},
17471 base_text,
17472 &mut cx,
17473 );
17474 // Same for selections
17475 assert_hunk_revert(
17476 indoc! {r#"struct Row;
17477 struct Row1;
17478 struct Row2;
17479 struct Row2.1;
17480 struct Row2.2;
17481 «ˇ
17482 struct Row4;
17483 struct» Row5;
17484 «struct Row6;
17485 ˇ»
17486 struct Row9.1;
17487 struct Row9.2;
17488 struct Row9.3;
17489 struct Row8;
17490 struct Row9;
17491 struct Row10;"#},
17492 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17493 indoc! {r#"struct Row;
17494 struct Row1;
17495 struct Row2;
17496 struct Row2.1;
17497 struct Row2.2;
17498 «ˇ
17499 struct Row4;
17500 struct» Row5;
17501 «struct Row6;
17502 ˇ»
17503 struct Row9.1;
17504 struct Row9.2;
17505 struct Row9.3;
17506 struct Row8;
17507 struct Row9;
17508 struct Row10;"#},
17509 base_text,
17510 &mut cx,
17511 );
17512
17513 // When carets and selections intersect the addition hunks, those are reverted.
17514 // Adjacent carets got merged.
17515 assert_hunk_revert(
17516 indoc! {r#"struct Row;
17517 ˇ// something on the top
17518 struct Row1;
17519 struct Row2;
17520 struct Roˇw3.1;
17521 struct Row2.2;
17522 struct Row2.3;ˇ
17523
17524 struct Row4;
17525 struct ˇRow5.1;
17526 struct Row5.2;
17527 struct «Rowˇ»5.3;
17528 struct Row5;
17529 struct Row6;
17530 ˇ
17531 struct Row9.1;
17532 struct «Rowˇ»9.2;
17533 struct «ˇRow»9.3;
17534 struct Row8;
17535 struct Row9;
17536 «ˇ// something on bottom»
17537 struct Row10;"#},
17538 vec![
17539 DiffHunkStatusKind::Added,
17540 DiffHunkStatusKind::Added,
17541 DiffHunkStatusKind::Added,
17542 DiffHunkStatusKind::Added,
17543 DiffHunkStatusKind::Added,
17544 ],
17545 indoc! {r#"struct Row;
17546 ˇstruct Row1;
17547 struct Row2;
17548 ˇ
17549 struct Row4;
17550 ˇstruct Row5;
17551 struct Row6;
17552 ˇ
17553 ˇstruct Row8;
17554 struct Row9;
17555 ˇstruct Row10;"#},
17556 base_text,
17557 &mut cx,
17558 );
17559}
17560
17561#[gpui::test]
17562async fn test_modification_reverts(cx: &mut TestAppContext) {
17563 init_test(cx, |_| {});
17564 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17565 let base_text = indoc! {r#"
17566 struct Row;
17567 struct Row1;
17568 struct Row2;
17569
17570 struct Row4;
17571 struct Row5;
17572 struct Row6;
17573
17574 struct Row8;
17575 struct Row9;
17576 struct Row10;"#};
17577
17578 // Modification hunks behave the same as the addition ones.
17579 assert_hunk_revert(
17580 indoc! {r#"struct Row;
17581 struct Row1;
17582 struct Row33;
17583 ˇ
17584 struct Row4;
17585 struct Row5;
17586 struct Row6;
17587 ˇ
17588 struct Row99;
17589 struct Row9;
17590 struct Row10;"#},
17591 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17592 indoc! {r#"struct Row;
17593 struct Row1;
17594 struct Row33;
17595 ˇ
17596 struct Row4;
17597 struct Row5;
17598 struct Row6;
17599 ˇ
17600 struct Row99;
17601 struct Row9;
17602 struct Row10;"#},
17603 base_text,
17604 &mut cx,
17605 );
17606 assert_hunk_revert(
17607 indoc! {r#"struct Row;
17608 struct Row1;
17609 struct Row33;
17610 «ˇ
17611 struct Row4;
17612 struct» Row5;
17613 «struct Row6;
17614 ˇ»
17615 struct Row99;
17616 struct Row9;
17617 struct Row10;"#},
17618 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17619 indoc! {r#"struct Row;
17620 struct Row1;
17621 struct Row33;
17622 «ˇ
17623 struct Row4;
17624 struct» Row5;
17625 «struct Row6;
17626 ˇ»
17627 struct Row99;
17628 struct Row9;
17629 struct Row10;"#},
17630 base_text,
17631 &mut cx,
17632 );
17633
17634 assert_hunk_revert(
17635 indoc! {r#"ˇstruct Row1.1;
17636 struct Row1;
17637 «ˇstr»uct Row22;
17638
17639 struct ˇRow44;
17640 struct Row5;
17641 struct «Rˇ»ow66;ˇ
17642
17643 «struˇ»ct Row88;
17644 struct Row9;
17645 struct Row1011;ˇ"#},
17646 vec![
17647 DiffHunkStatusKind::Modified,
17648 DiffHunkStatusKind::Modified,
17649 DiffHunkStatusKind::Modified,
17650 DiffHunkStatusKind::Modified,
17651 DiffHunkStatusKind::Modified,
17652 DiffHunkStatusKind::Modified,
17653 ],
17654 indoc! {r#"struct Row;
17655 ˇstruct Row1;
17656 struct Row2;
17657 ˇ
17658 struct Row4;
17659 ˇstruct Row5;
17660 struct Row6;
17661 ˇ
17662 struct Row8;
17663 ˇstruct Row9;
17664 struct Row10;ˇ"#},
17665 base_text,
17666 &mut cx,
17667 );
17668}
17669
17670#[gpui::test]
17671async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
17672 init_test(cx, |_| {});
17673 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17674 let base_text = indoc! {r#"
17675 one
17676
17677 two
17678 three
17679 "#};
17680
17681 cx.set_head_text(base_text);
17682 cx.set_state("\nˇ\n");
17683 cx.executor().run_until_parked();
17684 cx.update_editor(|editor, _window, cx| {
17685 editor.expand_selected_diff_hunks(cx);
17686 });
17687 cx.executor().run_until_parked();
17688 cx.update_editor(|editor, window, cx| {
17689 editor.backspace(&Default::default(), window, cx);
17690 });
17691 cx.run_until_parked();
17692 cx.assert_state_with_diff(
17693 indoc! {r#"
17694
17695 - two
17696 - threeˇ
17697 +
17698 "#}
17699 .to_string(),
17700 );
17701}
17702
17703#[gpui::test]
17704async fn test_deletion_reverts(cx: &mut TestAppContext) {
17705 init_test(cx, |_| {});
17706 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17707 let base_text = indoc! {r#"struct Row;
17708struct Row1;
17709struct Row2;
17710
17711struct Row4;
17712struct Row5;
17713struct Row6;
17714
17715struct Row8;
17716struct Row9;
17717struct Row10;"#};
17718
17719 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
17720 assert_hunk_revert(
17721 indoc! {r#"struct Row;
17722 struct Row2;
17723
17724 ˇstruct Row4;
17725 struct Row5;
17726 struct Row6;
17727 ˇ
17728 struct Row8;
17729 struct Row10;"#},
17730 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17731 indoc! {r#"struct Row;
17732 struct Row2;
17733
17734 ˇstruct Row4;
17735 struct Row5;
17736 struct Row6;
17737 ˇ
17738 struct Row8;
17739 struct Row10;"#},
17740 base_text,
17741 &mut cx,
17742 );
17743 assert_hunk_revert(
17744 indoc! {r#"struct Row;
17745 struct Row2;
17746
17747 «ˇstruct Row4;
17748 struct» Row5;
17749 «struct Row6;
17750 ˇ»
17751 struct Row8;
17752 struct Row10;"#},
17753 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17754 indoc! {r#"struct Row;
17755 struct Row2;
17756
17757 «ˇstruct Row4;
17758 struct» Row5;
17759 «struct Row6;
17760 ˇ»
17761 struct Row8;
17762 struct Row10;"#},
17763 base_text,
17764 &mut cx,
17765 );
17766
17767 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
17768 assert_hunk_revert(
17769 indoc! {r#"struct Row;
17770 ˇstruct Row2;
17771
17772 struct Row4;
17773 struct Row5;
17774 struct Row6;
17775
17776 struct Row8;ˇ
17777 struct Row10;"#},
17778 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17779 indoc! {r#"struct Row;
17780 struct Row1;
17781 ˇstruct Row2;
17782
17783 struct Row4;
17784 struct Row5;
17785 struct Row6;
17786
17787 struct Row8;ˇ
17788 struct Row9;
17789 struct Row10;"#},
17790 base_text,
17791 &mut cx,
17792 );
17793 assert_hunk_revert(
17794 indoc! {r#"struct Row;
17795 struct Row2«ˇ;
17796 struct Row4;
17797 struct» Row5;
17798 «struct Row6;
17799
17800 struct Row8;ˇ»
17801 struct Row10;"#},
17802 vec![
17803 DiffHunkStatusKind::Deleted,
17804 DiffHunkStatusKind::Deleted,
17805 DiffHunkStatusKind::Deleted,
17806 ],
17807 indoc! {r#"struct Row;
17808 struct Row1;
17809 struct Row2«ˇ;
17810
17811 struct Row4;
17812 struct» Row5;
17813 «struct Row6;
17814
17815 struct Row8;ˇ»
17816 struct Row9;
17817 struct Row10;"#},
17818 base_text,
17819 &mut cx,
17820 );
17821}
17822
17823#[gpui::test]
17824async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
17825 init_test(cx, |_| {});
17826
17827 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
17828 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
17829 let base_text_3 =
17830 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
17831
17832 let text_1 = edit_first_char_of_every_line(base_text_1);
17833 let text_2 = edit_first_char_of_every_line(base_text_2);
17834 let text_3 = edit_first_char_of_every_line(base_text_3);
17835
17836 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
17837 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
17838 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
17839
17840 let multibuffer = cx.new(|cx| {
17841 let mut multibuffer = MultiBuffer::new(ReadWrite);
17842 multibuffer.push_excerpts(
17843 buffer_1.clone(),
17844 [
17845 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17846 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17847 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17848 ],
17849 cx,
17850 );
17851 multibuffer.push_excerpts(
17852 buffer_2.clone(),
17853 [
17854 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17855 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17856 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17857 ],
17858 cx,
17859 );
17860 multibuffer.push_excerpts(
17861 buffer_3.clone(),
17862 [
17863 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17864 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17865 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17866 ],
17867 cx,
17868 );
17869 multibuffer
17870 });
17871
17872 let fs = FakeFs::new(cx.executor());
17873 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17874 let (editor, cx) = cx
17875 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17876 editor.update_in(cx, |editor, _window, cx| {
17877 for (buffer, diff_base) in [
17878 (buffer_1.clone(), base_text_1),
17879 (buffer_2.clone(), base_text_2),
17880 (buffer_3.clone(), base_text_3),
17881 ] {
17882 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
17883 editor
17884 .buffer
17885 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17886 }
17887 });
17888 cx.executor().run_until_parked();
17889
17890 editor.update_in(cx, |editor, window, cx| {
17891 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}");
17892 editor.select_all(&SelectAll, window, cx);
17893 editor.git_restore(&Default::default(), window, cx);
17894 });
17895 cx.executor().run_until_parked();
17896
17897 // When all ranges are selected, all buffer hunks are reverted.
17898 editor.update(cx, |editor, cx| {
17899 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");
17900 });
17901 buffer_1.update(cx, |buffer, _| {
17902 assert_eq!(buffer.text(), base_text_1);
17903 });
17904 buffer_2.update(cx, |buffer, _| {
17905 assert_eq!(buffer.text(), base_text_2);
17906 });
17907 buffer_3.update(cx, |buffer, _| {
17908 assert_eq!(buffer.text(), base_text_3);
17909 });
17910
17911 editor.update_in(cx, |editor, window, cx| {
17912 editor.undo(&Default::default(), window, cx);
17913 });
17914
17915 editor.update_in(cx, |editor, window, cx| {
17916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17917 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17918 });
17919 editor.git_restore(&Default::default(), window, cx);
17920 });
17921
17922 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17923 // but not affect buffer_2 and its related excerpts.
17924 editor.update(cx, |editor, cx| {
17925 assert_eq!(
17926 editor.text(cx),
17927 "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}"
17928 );
17929 });
17930 buffer_1.update(cx, |buffer, _| {
17931 assert_eq!(buffer.text(), base_text_1);
17932 });
17933 buffer_2.update(cx, |buffer, _| {
17934 assert_eq!(
17935 buffer.text(),
17936 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17937 );
17938 });
17939 buffer_3.update(cx, |buffer, _| {
17940 assert_eq!(
17941 buffer.text(),
17942 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17943 );
17944 });
17945
17946 fn edit_first_char_of_every_line(text: &str) -> String {
17947 text.split('\n')
17948 .map(|line| format!("X{}", &line[1..]))
17949 .collect::<Vec<_>>()
17950 .join("\n")
17951 }
17952}
17953
17954#[gpui::test]
17955async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17956 init_test(cx, |_| {});
17957
17958 let cols = 4;
17959 let rows = 10;
17960 let sample_text_1 = sample_text(rows, cols, 'a');
17961 assert_eq!(
17962 sample_text_1,
17963 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17964 );
17965 let sample_text_2 = sample_text(rows, cols, 'l');
17966 assert_eq!(
17967 sample_text_2,
17968 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17969 );
17970 let sample_text_3 = sample_text(rows, cols, 'v');
17971 assert_eq!(
17972 sample_text_3,
17973 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17974 );
17975
17976 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17977 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17978 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17979
17980 let multi_buffer = cx.new(|cx| {
17981 let mut multibuffer = MultiBuffer::new(ReadWrite);
17982 multibuffer.push_excerpts(
17983 buffer_1.clone(),
17984 [
17985 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17986 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17987 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17988 ],
17989 cx,
17990 );
17991 multibuffer.push_excerpts(
17992 buffer_2.clone(),
17993 [
17994 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17995 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17996 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17997 ],
17998 cx,
17999 );
18000 multibuffer.push_excerpts(
18001 buffer_3.clone(),
18002 [
18003 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18004 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18005 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18006 ],
18007 cx,
18008 );
18009 multibuffer
18010 });
18011
18012 let fs = FakeFs::new(cx.executor());
18013 fs.insert_tree(
18014 "/a",
18015 json!({
18016 "main.rs": sample_text_1,
18017 "other.rs": sample_text_2,
18018 "lib.rs": sample_text_3,
18019 }),
18020 )
18021 .await;
18022 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18023 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18024 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18025 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18026 Editor::new(
18027 EditorMode::full(),
18028 multi_buffer,
18029 Some(project.clone()),
18030 window,
18031 cx,
18032 )
18033 });
18034 let multibuffer_item_id = workspace
18035 .update(cx, |workspace, window, cx| {
18036 assert!(
18037 workspace.active_item(cx).is_none(),
18038 "active item should be None before the first item is added"
18039 );
18040 workspace.add_item_to_active_pane(
18041 Box::new(multi_buffer_editor.clone()),
18042 None,
18043 true,
18044 window,
18045 cx,
18046 );
18047 let active_item = workspace
18048 .active_item(cx)
18049 .expect("should have an active item after adding the multi buffer");
18050 assert!(
18051 !active_item.is_singleton(cx),
18052 "A multi buffer was expected to active after adding"
18053 );
18054 active_item.item_id()
18055 })
18056 .unwrap();
18057 cx.executor().run_until_parked();
18058
18059 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18060 editor.change_selections(
18061 SelectionEffects::scroll(Autoscroll::Next),
18062 window,
18063 cx,
18064 |s| s.select_ranges(Some(1..2)),
18065 );
18066 editor.open_excerpts(&OpenExcerpts, window, cx);
18067 });
18068 cx.executor().run_until_parked();
18069 let first_item_id = workspace
18070 .update(cx, |workspace, window, cx| {
18071 let active_item = workspace
18072 .active_item(cx)
18073 .expect("should have an active item after navigating into the 1st buffer");
18074 let first_item_id = active_item.item_id();
18075 assert_ne!(
18076 first_item_id, multibuffer_item_id,
18077 "Should navigate into the 1st buffer and activate it"
18078 );
18079 assert!(
18080 active_item.is_singleton(cx),
18081 "New active item should be a singleton buffer"
18082 );
18083 assert_eq!(
18084 active_item
18085 .act_as::<Editor>(cx)
18086 .expect("should have navigated into an editor for the 1st buffer")
18087 .read(cx)
18088 .text(cx),
18089 sample_text_1
18090 );
18091
18092 workspace
18093 .go_back(workspace.active_pane().downgrade(), window, cx)
18094 .detach_and_log_err(cx);
18095
18096 first_item_id
18097 })
18098 .unwrap();
18099 cx.executor().run_until_parked();
18100 workspace
18101 .update(cx, |workspace, _, cx| {
18102 let active_item = workspace
18103 .active_item(cx)
18104 .expect("should have an active item after navigating back");
18105 assert_eq!(
18106 active_item.item_id(),
18107 multibuffer_item_id,
18108 "Should navigate back to the multi buffer"
18109 );
18110 assert!(!active_item.is_singleton(cx));
18111 })
18112 .unwrap();
18113
18114 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18115 editor.change_selections(
18116 SelectionEffects::scroll(Autoscroll::Next),
18117 window,
18118 cx,
18119 |s| s.select_ranges(Some(39..40)),
18120 );
18121 editor.open_excerpts(&OpenExcerpts, window, cx);
18122 });
18123 cx.executor().run_until_parked();
18124 let second_item_id = workspace
18125 .update(cx, |workspace, window, cx| {
18126 let active_item = workspace
18127 .active_item(cx)
18128 .expect("should have an active item after navigating into the 2nd buffer");
18129 let second_item_id = active_item.item_id();
18130 assert_ne!(
18131 second_item_id, multibuffer_item_id,
18132 "Should navigate away from the multibuffer"
18133 );
18134 assert_ne!(
18135 second_item_id, first_item_id,
18136 "Should navigate into the 2nd buffer and activate it"
18137 );
18138 assert!(
18139 active_item.is_singleton(cx),
18140 "New active item should be a singleton buffer"
18141 );
18142 assert_eq!(
18143 active_item
18144 .act_as::<Editor>(cx)
18145 .expect("should have navigated into an editor")
18146 .read(cx)
18147 .text(cx),
18148 sample_text_2
18149 );
18150
18151 workspace
18152 .go_back(workspace.active_pane().downgrade(), window, cx)
18153 .detach_and_log_err(cx);
18154
18155 second_item_id
18156 })
18157 .unwrap();
18158 cx.executor().run_until_parked();
18159 workspace
18160 .update(cx, |workspace, _, cx| {
18161 let active_item = workspace
18162 .active_item(cx)
18163 .expect("should have an active item after navigating back from the 2nd buffer");
18164 assert_eq!(
18165 active_item.item_id(),
18166 multibuffer_item_id,
18167 "Should navigate back from the 2nd buffer to the multi buffer"
18168 );
18169 assert!(!active_item.is_singleton(cx));
18170 })
18171 .unwrap();
18172
18173 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18174 editor.change_selections(
18175 SelectionEffects::scroll(Autoscroll::Next),
18176 window,
18177 cx,
18178 |s| s.select_ranges(Some(70..70)),
18179 );
18180 editor.open_excerpts(&OpenExcerpts, window, cx);
18181 });
18182 cx.executor().run_until_parked();
18183 workspace
18184 .update(cx, |workspace, window, cx| {
18185 let active_item = workspace
18186 .active_item(cx)
18187 .expect("should have an active item after navigating into the 3rd buffer");
18188 let third_item_id = active_item.item_id();
18189 assert_ne!(
18190 third_item_id, multibuffer_item_id,
18191 "Should navigate into the 3rd buffer and activate it"
18192 );
18193 assert_ne!(third_item_id, first_item_id);
18194 assert_ne!(third_item_id, second_item_id);
18195 assert!(
18196 active_item.is_singleton(cx),
18197 "New active item should be a singleton buffer"
18198 );
18199 assert_eq!(
18200 active_item
18201 .act_as::<Editor>(cx)
18202 .expect("should have navigated into an editor")
18203 .read(cx)
18204 .text(cx),
18205 sample_text_3
18206 );
18207
18208 workspace
18209 .go_back(workspace.active_pane().downgrade(), window, cx)
18210 .detach_and_log_err(cx);
18211 })
18212 .unwrap();
18213 cx.executor().run_until_parked();
18214 workspace
18215 .update(cx, |workspace, _, cx| {
18216 let active_item = workspace
18217 .active_item(cx)
18218 .expect("should have an active item after navigating back from the 3rd buffer");
18219 assert_eq!(
18220 active_item.item_id(),
18221 multibuffer_item_id,
18222 "Should navigate back from the 3rd buffer to the multi buffer"
18223 );
18224 assert!(!active_item.is_singleton(cx));
18225 })
18226 .unwrap();
18227}
18228
18229#[gpui::test]
18230async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18231 init_test(cx, |_| {});
18232
18233 let mut cx = EditorTestContext::new(cx).await;
18234
18235 let diff_base = r#"
18236 use some::mod;
18237
18238 const A: u32 = 42;
18239
18240 fn main() {
18241 println!("hello");
18242
18243 println!("world");
18244 }
18245 "#
18246 .unindent();
18247
18248 cx.set_state(
18249 &r#"
18250 use some::modified;
18251
18252 ˇ
18253 fn main() {
18254 println!("hello there");
18255
18256 println!("around the");
18257 println!("world");
18258 }
18259 "#
18260 .unindent(),
18261 );
18262
18263 cx.set_head_text(&diff_base);
18264 executor.run_until_parked();
18265
18266 cx.update_editor(|editor, window, cx| {
18267 editor.go_to_next_hunk(&GoToHunk, window, cx);
18268 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18269 });
18270 executor.run_until_parked();
18271 cx.assert_state_with_diff(
18272 r#"
18273 use some::modified;
18274
18275
18276 fn main() {
18277 - println!("hello");
18278 + ˇ println!("hello there");
18279
18280 println!("around the");
18281 println!("world");
18282 }
18283 "#
18284 .unindent(),
18285 );
18286
18287 cx.update_editor(|editor, window, cx| {
18288 for _ in 0..2 {
18289 editor.go_to_next_hunk(&GoToHunk, window, cx);
18290 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18291 }
18292 });
18293 executor.run_until_parked();
18294 cx.assert_state_with_diff(
18295 r#"
18296 - use some::mod;
18297 + ˇuse some::modified;
18298
18299
18300 fn main() {
18301 - println!("hello");
18302 + println!("hello there");
18303
18304 + println!("around the");
18305 println!("world");
18306 }
18307 "#
18308 .unindent(),
18309 );
18310
18311 cx.update_editor(|editor, window, cx| {
18312 editor.go_to_next_hunk(&GoToHunk, window, cx);
18313 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18314 });
18315 executor.run_until_parked();
18316 cx.assert_state_with_diff(
18317 r#"
18318 - use some::mod;
18319 + use some::modified;
18320
18321 - const A: u32 = 42;
18322 ˇ
18323 fn main() {
18324 - println!("hello");
18325 + println!("hello there");
18326
18327 + println!("around the");
18328 println!("world");
18329 }
18330 "#
18331 .unindent(),
18332 );
18333
18334 cx.update_editor(|editor, window, cx| {
18335 editor.cancel(&Cancel, window, cx);
18336 });
18337
18338 cx.assert_state_with_diff(
18339 r#"
18340 use some::modified;
18341
18342 ˇ
18343 fn main() {
18344 println!("hello there");
18345
18346 println!("around the");
18347 println!("world");
18348 }
18349 "#
18350 .unindent(),
18351 );
18352}
18353
18354#[gpui::test]
18355async fn test_diff_base_change_with_expanded_diff_hunks(
18356 executor: BackgroundExecutor,
18357 cx: &mut TestAppContext,
18358) {
18359 init_test(cx, |_| {});
18360
18361 let mut cx = EditorTestContext::new(cx).await;
18362
18363 let diff_base = r#"
18364 use some::mod1;
18365 use some::mod2;
18366
18367 const A: u32 = 42;
18368 const B: u32 = 42;
18369 const C: u32 = 42;
18370
18371 fn main() {
18372 println!("hello");
18373
18374 println!("world");
18375 }
18376 "#
18377 .unindent();
18378
18379 cx.set_state(
18380 &r#"
18381 use some::mod2;
18382
18383 const A: u32 = 42;
18384 const C: u32 = 42;
18385
18386 fn main(ˇ) {
18387 //println!("hello");
18388
18389 println!("world");
18390 //
18391 //
18392 }
18393 "#
18394 .unindent(),
18395 );
18396
18397 cx.set_head_text(&diff_base);
18398 executor.run_until_parked();
18399
18400 cx.update_editor(|editor, window, cx| {
18401 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18402 });
18403 executor.run_until_parked();
18404 cx.assert_state_with_diff(
18405 r#"
18406 - use some::mod1;
18407 use some::mod2;
18408
18409 const A: u32 = 42;
18410 - const B: u32 = 42;
18411 const C: u32 = 42;
18412
18413 fn main(ˇ) {
18414 - println!("hello");
18415 + //println!("hello");
18416
18417 println!("world");
18418 + //
18419 + //
18420 }
18421 "#
18422 .unindent(),
18423 );
18424
18425 cx.set_head_text("new diff base!");
18426 executor.run_until_parked();
18427 cx.assert_state_with_diff(
18428 r#"
18429 - new diff base!
18430 + use some::mod2;
18431 +
18432 + const A: u32 = 42;
18433 + const C: u32 = 42;
18434 +
18435 + fn main(ˇ) {
18436 + //println!("hello");
18437 +
18438 + println!("world");
18439 + //
18440 + //
18441 + }
18442 "#
18443 .unindent(),
18444 );
18445}
18446
18447#[gpui::test]
18448async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
18449 init_test(cx, |_| {});
18450
18451 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18452 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18453 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18454 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18455 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
18456 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
18457
18458 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
18459 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
18460 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
18461
18462 let multi_buffer = cx.new(|cx| {
18463 let mut multibuffer = MultiBuffer::new(ReadWrite);
18464 multibuffer.push_excerpts(
18465 buffer_1.clone(),
18466 [
18467 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18468 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18469 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18470 ],
18471 cx,
18472 );
18473 multibuffer.push_excerpts(
18474 buffer_2.clone(),
18475 [
18476 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18477 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18478 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18479 ],
18480 cx,
18481 );
18482 multibuffer.push_excerpts(
18483 buffer_3.clone(),
18484 [
18485 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18486 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18487 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18488 ],
18489 cx,
18490 );
18491 multibuffer
18492 });
18493
18494 let editor =
18495 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18496 editor
18497 .update(cx, |editor, _window, cx| {
18498 for (buffer, diff_base) in [
18499 (buffer_1.clone(), file_1_old),
18500 (buffer_2.clone(), file_2_old),
18501 (buffer_3.clone(), file_3_old),
18502 ] {
18503 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18504 editor
18505 .buffer
18506 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18507 }
18508 })
18509 .unwrap();
18510
18511 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18512 cx.run_until_parked();
18513
18514 cx.assert_editor_state(
18515 &"
18516 ˇaaa
18517 ccc
18518 ddd
18519
18520 ggg
18521 hhh
18522
18523
18524 lll
18525 mmm
18526 NNN
18527
18528 qqq
18529 rrr
18530
18531 uuu
18532 111
18533 222
18534 333
18535
18536 666
18537 777
18538
18539 000
18540 !!!"
18541 .unindent(),
18542 );
18543
18544 cx.update_editor(|editor, window, cx| {
18545 editor.select_all(&SelectAll, window, cx);
18546 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18547 });
18548 cx.executor().run_until_parked();
18549
18550 cx.assert_state_with_diff(
18551 "
18552 «aaa
18553 - bbb
18554 ccc
18555 ddd
18556
18557 ggg
18558 hhh
18559
18560
18561 lll
18562 mmm
18563 - nnn
18564 + NNN
18565
18566 qqq
18567 rrr
18568
18569 uuu
18570 111
18571 222
18572 333
18573
18574 + 666
18575 777
18576
18577 000
18578 !!!ˇ»"
18579 .unindent(),
18580 );
18581}
18582
18583#[gpui::test]
18584async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18585 init_test(cx, |_| {});
18586
18587 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
18588 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
18589
18590 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18591 let multi_buffer = cx.new(|cx| {
18592 let mut multibuffer = MultiBuffer::new(ReadWrite);
18593 multibuffer.push_excerpts(
18594 buffer.clone(),
18595 [
18596 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18597 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18598 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18599 ],
18600 cx,
18601 );
18602 multibuffer
18603 });
18604
18605 let editor =
18606 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18607 editor
18608 .update(cx, |editor, _window, cx| {
18609 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18610 editor
18611 .buffer
18612 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18613 })
18614 .unwrap();
18615
18616 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18617 cx.run_until_parked();
18618
18619 cx.update_editor(|editor, window, cx| {
18620 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18621 });
18622 cx.executor().run_until_parked();
18623
18624 // When the start of a hunk coincides with the start of its excerpt,
18625 // the hunk is expanded. When the start of a a hunk is earlier than
18626 // the start of its excerpt, the hunk is not expanded.
18627 cx.assert_state_with_diff(
18628 "
18629 ˇaaa
18630 - bbb
18631 + BBB
18632
18633 - ddd
18634 - eee
18635 + DDD
18636 + EEE
18637 fff
18638
18639 iii
18640 "
18641 .unindent(),
18642 );
18643}
18644
18645#[gpui::test]
18646async fn test_edits_around_expanded_insertion_hunks(
18647 executor: BackgroundExecutor,
18648 cx: &mut TestAppContext,
18649) {
18650 init_test(cx, |_| {});
18651
18652 let mut cx = EditorTestContext::new(cx).await;
18653
18654 let diff_base = r#"
18655 use some::mod1;
18656 use some::mod2;
18657
18658 const A: u32 = 42;
18659
18660 fn main() {
18661 println!("hello");
18662
18663 println!("world");
18664 }
18665 "#
18666 .unindent();
18667 executor.run_until_parked();
18668 cx.set_state(
18669 &r#"
18670 use some::mod1;
18671 use some::mod2;
18672
18673 const A: u32 = 42;
18674 const B: u32 = 42;
18675 const C: u32 = 42;
18676 ˇ
18677
18678 fn main() {
18679 println!("hello");
18680
18681 println!("world");
18682 }
18683 "#
18684 .unindent(),
18685 );
18686
18687 cx.set_head_text(&diff_base);
18688 executor.run_until_parked();
18689
18690 cx.update_editor(|editor, window, cx| {
18691 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18692 });
18693 executor.run_until_parked();
18694
18695 cx.assert_state_with_diff(
18696 r#"
18697 use some::mod1;
18698 use some::mod2;
18699
18700 const A: u32 = 42;
18701 + const B: u32 = 42;
18702 + const C: u32 = 42;
18703 + ˇ
18704
18705 fn main() {
18706 println!("hello");
18707
18708 println!("world");
18709 }
18710 "#
18711 .unindent(),
18712 );
18713
18714 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
18715 executor.run_until_parked();
18716
18717 cx.assert_state_with_diff(
18718 r#"
18719 use some::mod1;
18720 use some::mod2;
18721
18722 const A: u32 = 42;
18723 + const B: u32 = 42;
18724 + const C: u32 = 42;
18725 + const D: u32 = 42;
18726 + ˇ
18727
18728 fn main() {
18729 println!("hello");
18730
18731 println!("world");
18732 }
18733 "#
18734 .unindent(),
18735 );
18736
18737 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
18738 executor.run_until_parked();
18739
18740 cx.assert_state_with_diff(
18741 r#"
18742 use some::mod1;
18743 use some::mod2;
18744
18745 const A: u32 = 42;
18746 + const B: u32 = 42;
18747 + const C: u32 = 42;
18748 + const D: u32 = 42;
18749 + const E: u32 = 42;
18750 + ˇ
18751
18752 fn main() {
18753 println!("hello");
18754
18755 println!("world");
18756 }
18757 "#
18758 .unindent(),
18759 );
18760
18761 cx.update_editor(|editor, window, cx| {
18762 editor.delete_line(&DeleteLine, window, cx);
18763 });
18764 executor.run_until_parked();
18765
18766 cx.assert_state_with_diff(
18767 r#"
18768 use some::mod1;
18769 use some::mod2;
18770
18771 const A: u32 = 42;
18772 + const B: u32 = 42;
18773 + const C: u32 = 42;
18774 + const D: u32 = 42;
18775 + const E: u32 = 42;
18776 ˇ
18777 fn main() {
18778 println!("hello");
18779
18780 println!("world");
18781 }
18782 "#
18783 .unindent(),
18784 );
18785
18786 cx.update_editor(|editor, window, cx| {
18787 editor.move_up(&MoveUp, window, cx);
18788 editor.delete_line(&DeleteLine, window, cx);
18789 editor.move_up(&MoveUp, window, cx);
18790 editor.delete_line(&DeleteLine, window, cx);
18791 editor.move_up(&MoveUp, window, cx);
18792 editor.delete_line(&DeleteLine, window, cx);
18793 });
18794 executor.run_until_parked();
18795 cx.assert_state_with_diff(
18796 r#"
18797 use some::mod1;
18798 use some::mod2;
18799
18800 const A: u32 = 42;
18801 + const B: u32 = 42;
18802 ˇ
18803 fn main() {
18804 println!("hello");
18805
18806 println!("world");
18807 }
18808 "#
18809 .unindent(),
18810 );
18811
18812 cx.update_editor(|editor, window, cx| {
18813 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
18814 editor.delete_line(&DeleteLine, window, cx);
18815 });
18816 executor.run_until_parked();
18817 cx.assert_state_with_diff(
18818 r#"
18819 ˇ
18820 fn main() {
18821 println!("hello");
18822
18823 println!("world");
18824 }
18825 "#
18826 .unindent(),
18827 );
18828}
18829
18830#[gpui::test]
18831async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
18832 init_test(cx, |_| {});
18833
18834 let mut cx = EditorTestContext::new(cx).await;
18835 cx.set_head_text(indoc! { "
18836 one
18837 two
18838 three
18839 four
18840 five
18841 "
18842 });
18843 cx.set_state(indoc! { "
18844 one
18845 ˇthree
18846 five
18847 "});
18848 cx.run_until_parked();
18849 cx.update_editor(|editor, window, cx| {
18850 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18851 });
18852 cx.assert_state_with_diff(
18853 indoc! { "
18854 one
18855 - two
18856 ˇthree
18857 - four
18858 five
18859 "}
18860 .to_string(),
18861 );
18862 cx.update_editor(|editor, window, cx| {
18863 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18864 });
18865
18866 cx.assert_state_with_diff(
18867 indoc! { "
18868 one
18869 ˇthree
18870 five
18871 "}
18872 .to_string(),
18873 );
18874
18875 cx.set_state(indoc! { "
18876 one
18877 ˇTWO
18878 three
18879 four
18880 five
18881 "});
18882 cx.run_until_parked();
18883 cx.update_editor(|editor, window, cx| {
18884 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18885 });
18886
18887 cx.assert_state_with_diff(
18888 indoc! { "
18889 one
18890 - two
18891 + ˇTWO
18892 three
18893 four
18894 five
18895 "}
18896 .to_string(),
18897 );
18898 cx.update_editor(|editor, window, cx| {
18899 editor.move_up(&Default::default(), window, cx);
18900 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18901 });
18902 cx.assert_state_with_diff(
18903 indoc! { "
18904 one
18905 ˇTWO
18906 three
18907 four
18908 five
18909 "}
18910 .to_string(),
18911 );
18912}
18913
18914#[gpui::test]
18915async fn test_edits_around_expanded_deletion_hunks(
18916 executor: BackgroundExecutor,
18917 cx: &mut TestAppContext,
18918) {
18919 init_test(cx, |_| {});
18920
18921 let mut cx = EditorTestContext::new(cx).await;
18922
18923 let diff_base = r#"
18924 use some::mod1;
18925 use some::mod2;
18926
18927 const A: u32 = 42;
18928 const B: u32 = 42;
18929 const C: u32 = 42;
18930
18931
18932 fn main() {
18933 println!("hello");
18934
18935 println!("world");
18936 }
18937 "#
18938 .unindent();
18939 executor.run_until_parked();
18940 cx.set_state(
18941 &r#"
18942 use some::mod1;
18943 use some::mod2;
18944
18945 ˇconst B: u32 = 42;
18946 const C: u32 = 42;
18947
18948
18949 fn main() {
18950 println!("hello");
18951
18952 println!("world");
18953 }
18954 "#
18955 .unindent(),
18956 );
18957
18958 cx.set_head_text(&diff_base);
18959 executor.run_until_parked();
18960
18961 cx.update_editor(|editor, window, cx| {
18962 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18963 });
18964 executor.run_until_parked();
18965
18966 cx.assert_state_with_diff(
18967 r#"
18968 use some::mod1;
18969 use some::mod2;
18970
18971 - const A: u32 = 42;
18972 ˇconst B: u32 = 42;
18973 const C: u32 = 42;
18974
18975
18976 fn main() {
18977 println!("hello");
18978
18979 println!("world");
18980 }
18981 "#
18982 .unindent(),
18983 );
18984
18985 cx.update_editor(|editor, window, cx| {
18986 editor.delete_line(&DeleteLine, window, cx);
18987 });
18988 executor.run_until_parked();
18989 cx.assert_state_with_diff(
18990 r#"
18991 use some::mod1;
18992 use some::mod2;
18993
18994 - const A: u32 = 42;
18995 - const B: u32 = 42;
18996 ˇconst C: u32 = 42;
18997
18998
18999 fn main() {
19000 println!("hello");
19001
19002 println!("world");
19003 }
19004 "#
19005 .unindent(),
19006 );
19007
19008 cx.update_editor(|editor, window, cx| {
19009 editor.delete_line(&DeleteLine, window, cx);
19010 });
19011 executor.run_until_parked();
19012 cx.assert_state_with_diff(
19013 r#"
19014 use some::mod1;
19015 use some::mod2;
19016
19017 - const A: u32 = 42;
19018 - const B: u32 = 42;
19019 - const C: u32 = 42;
19020 ˇ
19021
19022 fn main() {
19023 println!("hello");
19024
19025 println!("world");
19026 }
19027 "#
19028 .unindent(),
19029 );
19030
19031 cx.update_editor(|editor, window, cx| {
19032 editor.handle_input("replacement", window, cx);
19033 });
19034 executor.run_until_parked();
19035 cx.assert_state_with_diff(
19036 r#"
19037 use some::mod1;
19038 use some::mod2;
19039
19040 - const A: u32 = 42;
19041 - const B: u32 = 42;
19042 - const C: u32 = 42;
19043 -
19044 + replacementˇ
19045
19046 fn main() {
19047 println!("hello");
19048
19049 println!("world");
19050 }
19051 "#
19052 .unindent(),
19053 );
19054}
19055
19056#[gpui::test]
19057async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19058 init_test(cx, |_| {});
19059
19060 let mut cx = EditorTestContext::new(cx).await;
19061
19062 let base_text = r#"
19063 one
19064 two
19065 three
19066 four
19067 five
19068 "#
19069 .unindent();
19070 executor.run_until_parked();
19071 cx.set_state(
19072 &r#"
19073 one
19074 two
19075 fˇour
19076 five
19077 "#
19078 .unindent(),
19079 );
19080
19081 cx.set_head_text(&base_text);
19082 executor.run_until_parked();
19083
19084 cx.update_editor(|editor, window, cx| {
19085 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19086 });
19087 executor.run_until_parked();
19088
19089 cx.assert_state_with_diff(
19090 r#"
19091 one
19092 two
19093 - three
19094 fˇour
19095 five
19096 "#
19097 .unindent(),
19098 );
19099
19100 cx.update_editor(|editor, window, cx| {
19101 editor.backspace(&Backspace, window, cx);
19102 editor.backspace(&Backspace, window, cx);
19103 });
19104 executor.run_until_parked();
19105 cx.assert_state_with_diff(
19106 r#"
19107 one
19108 two
19109 - threeˇ
19110 - four
19111 + our
19112 five
19113 "#
19114 .unindent(),
19115 );
19116}
19117
19118#[gpui::test]
19119async fn test_edit_after_expanded_modification_hunk(
19120 executor: BackgroundExecutor,
19121 cx: &mut TestAppContext,
19122) {
19123 init_test(cx, |_| {});
19124
19125 let mut cx = EditorTestContext::new(cx).await;
19126
19127 let diff_base = r#"
19128 use some::mod1;
19129 use some::mod2;
19130
19131 const A: u32 = 42;
19132 const B: u32 = 42;
19133 const C: u32 = 42;
19134 const D: u32 = 42;
19135
19136
19137 fn main() {
19138 println!("hello");
19139
19140 println!("world");
19141 }"#
19142 .unindent();
19143
19144 cx.set_state(
19145 &r#"
19146 use some::mod1;
19147 use some::mod2;
19148
19149 const A: u32 = 42;
19150 const B: u32 = 42;
19151 const C: u32 = 43ˇ
19152 const D: u32 = 42;
19153
19154
19155 fn main() {
19156 println!("hello");
19157
19158 println!("world");
19159 }"#
19160 .unindent(),
19161 );
19162
19163 cx.set_head_text(&diff_base);
19164 executor.run_until_parked();
19165 cx.update_editor(|editor, window, cx| {
19166 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19167 });
19168 executor.run_until_parked();
19169
19170 cx.assert_state_with_diff(
19171 r#"
19172 use some::mod1;
19173 use some::mod2;
19174
19175 const A: u32 = 42;
19176 const B: u32 = 42;
19177 - const C: u32 = 42;
19178 + const C: u32 = 43ˇ
19179 const D: u32 = 42;
19180
19181
19182 fn main() {
19183 println!("hello");
19184
19185 println!("world");
19186 }"#
19187 .unindent(),
19188 );
19189
19190 cx.update_editor(|editor, window, cx| {
19191 editor.handle_input("\nnew_line\n", window, cx);
19192 });
19193 executor.run_until_parked();
19194
19195 cx.assert_state_with_diff(
19196 r#"
19197 use some::mod1;
19198 use some::mod2;
19199
19200 const A: u32 = 42;
19201 const B: u32 = 42;
19202 - const C: u32 = 42;
19203 + const C: u32 = 43
19204 + new_line
19205 + ˇ
19206 const D: u32 = 42;
19207
19208
19209 fn main() {
19210 println!("hello");
19211
19212 println!("world");
19213 }"#
19214 .unindent(),
19215 );
19216}
19217
19218#[gpui::test]
19219async fn test_stage_and_unstage_added_file_hunk(
19220 executor: BackgroundExecutor,
19221 cx: &mut TestAppContext,
19222) {
19223 init_test(cx, |_| {});
19224
19225 let mut cx = EditorTestContext::new(cx).await;
19226 cx.update_editor(|editor, _, cx| {
19227 editor.set_expand_all_diff_hunks(cx);
19228 });
19229
19230 let working_copy = r#"
19231 ˇfn main() {
19232 println!("hello, world!");
19233 }
19234 "#
19235 .unindent();
19236
19237 cx.set_state(&working_copy);
19238 executor.run_until_parked();
19239
19240 cx.assert_state_with_diff(
19241 r#"
19242 + ˇfn main() {
19243 + println!("hello, world!");
19244 + }
19245 "#
19246 .unindent(),
19247 );
19248 cx.assert_index_text(None);
19249
19250 cx.update_editor(|editor, window, cx| {
19251 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19252 });
19253 executor.run_until_parked();
19254 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19255 cx.assert_state_with_diff(
19256 r#"
19257 + ˇfn main() {
19258 + println!("hello, world!");
19259 + }
19260 "#
19261 .unindent(),
19262 );
19263
19264 cx.update_editor(|editor, window, cx| {
19265 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19266 });
19267 executor.run_until_parked();
19268 cx.assert_index_text(None);
19269}
19270
19271async fn setup_indent_guides_editor(
19272 text: &str,
19273 cx: &mut TestAppContext,
19274) -> (BufferId, EditorTestContext) {
19275 init_test(cx, |_| {});
19276
19277 let mut cx = EditorTestContext::new(cx).await;
19278
19279 let buffer_id = cx.update_editor(|editor, window, cx| {
19280 editor.set_text(text, window, cx);
19281 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19282
19283 buffer_ids[0]
19284 });
19285
19286 (buffer_id, cx)
19287}
19288
19289fn assert_indent_guides(
19290 range: Range<u32>,
19291 expected: Vec<IndentGuide>,
19292 active_indices: Option<Vec<usize>>,
19293 cx: &mut EditorTestContext,
19294) {
19295 let indent_guides = cx.update_editor(|editor, window, cx| {
19296 let snapshot = editor.snapshot(window, cx).display_snapshot;
19297 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19298 editor,
19299 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19300 true,
19301 &snapshot,
19302 cx,
19303 );
19304
19305 indent_guides.sort_by(|a, b| {
19306 a.depth.cmp(&b.depth).then(
19307 a.start_row
19308 .cmp(&b.start_row)
19309 .then(a.end_row.cmp(&b.end_row)),
19310 )
19311 });
19312 indent_guides
19313 });
19314
19315 if let Some(expected) = active_indices {
19316 let active_indices = cx.update_editor(|editor, window, cx| {
19317 let snapshot = editor.snapshot(window, cx).display_snapshot;
19318 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19319 });
19320
19321 assert_eq!(
19322 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19323 expected,
19324 "Active indent guide indices do not match"
19325 );
19326 }
19327
19328 assert_eq!(indent_guides, expected, "Indent guides do not match");
19329}
19330
19331fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19332 IndentGuide {
19333 buffer_id,
19334 start_row: MultiBufferRow(start_row),
19335 end_row: MultiBufferRow(end_row),
19336 depth,
19337 tab_size: 4,
19338 settings: IndentGuideSettings {
19339 enabled: true,
19340 line_width: 1,
19341 active_line_width: 1,
19342 ..Default::default()
19343 },
19344 }
19345}
19346
19347#[gpui::test]
19348async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19349 let (buffer_id, mut cx) = setup_indent_guides_editor(
19350 &"
19351 fn main() {
19352 let a = 1;
19353 }"
19354 .unindent(),
19355 cx,
19356 )
19357 .await;
19358
19359 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19360}
19361
19362#[gpui::test]
19363async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19364 let (buffer_id, mut cx) = setup_indent_guides_editor(
19365 &"
19366 fn main() {
19367 let a = 1;
19368 let b = 2;
19369 }"
19370 .unindent(),
19371 cx,
19372 )
19373 .await;
19374
19375 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
19376}
19377
19378#[gpui::test]
19379async fn test_indent_guide_nested(cx: &mut TestAppContext) {
19380 let (buffer_id, mut cx) = setup_indent_guides_editor(
19381 &"
19382 fn main() {
19383 let a = 1;
19384 if a == 3 {
19385 let b = 2;
19386 } else {
19387 let c = 3;
19388 }
19389 }"
19390 .unindent(),
19391 cx,
19392 )
19393 .await;
19394
19395 assert_indent_guides(
19396 0..8,
19397 vec![
19398 indent_guide(buffer_id, 1, 6, 0),
19399 indent_guide(buffer_id, 3, 3, 1),
19400 indent_guide(buffer_id, 5, 5, 1),
19401 ],
19402 None,
19403 &mut cx,
19404 );
19405}
19406
19407#[gpui::test]
19408async fn test_indent_guide_tab(cx: &mut TestAppContext) {
19409 let (buffer_id, mut cx) = setup_indent_guides_editor(
19410 &"
19411 fn main() {
19412 let a = 1;
19413 let b = 2;
19414 let c = 3;
19415 }"
19416 .unindent(),
19417 cx,
19418 )
19419 .await;
19420
19421 assert_indent_guides(
19422 0..5,
19423 vec![
19424 indent_guide(buffer_id, 1, 3, 0),
19425 indent_guide(buffer_id, 2, 2, 1),
19426 ],
19427 None,
19428 &mut cx,
19429 );
19430}
19431
19432#[gpui::test]
19433async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
19434 let (buffer_id, mut cx) = setup_indent_guides_editor(
19435 &"
19436 fn main() {
19437 let a = 1;
19438
19439 let c = 3;
19440 }"
19441 .unindent(),
19442 cx,
19443 )
19444 .await;
19445
19446 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
19447}
19448
19449#[gpui::test]
19450async fn test_indent_guide_complex(cx: &mut TestAppContext) {
19451 let (buffer_id, mut cx) = setup_indent_guides_editor(
19452 &"
19453 fn main() {
19454 let a = 1;
19455
19456 let c = 3;
19457
19458 if a == 3 {
19459 let b = 2;
19460 } else {
19461 let c = 3;
19462 }
19463 }"
19464 .unindent(),
19465 cx,
19466 )
19467 .await;
19468
19469 assert_indent_guides(
19470 0..11,
19471 vec![
19472 indent_guide(buffer_id, 1, 9, 0),
19473 indent_guide(buffer_id, 6, 6, 1),
19474 indent_guide(buffer_id, 8, 8, 1),
19475 ],
19476 None,
19477 &mut cx,
19478 );
19479}
19480
19481#[gpui::test]
19482async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
19483 let (buffer_id, mut cx) = setup_indent_guides_editor(
19484 &"
19485 fn main() {
19486 let a = 1;
19487
19488 let c = 3;
19489
19490 if a == 3 {
19491 let b = 2;
19492 } else {
19493 let c = 3;
19494 }
19495 }"
19496 .unindent(),
19497 cx,
19498 )
19499 .await;
19500
19501 assert_indent_guides(
19502 1..11,
19503 vec![
19504 indent_guide(buffer_id, 1, 9, 0),
19505 indent_guide(buffer_id, 6, 6, 1),
19506 indent_guide(buffer_id, 8, 8, 1),
19507 ],
19508 None,
19509 &mut cx,
19510 );
19511}
19512
19513#[gpui::test]
19514async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
19515 let (buffer_id, mut cx) = setup_indent_guides_editor(
19516 &"
19517 fn main() {
19518 let a = 1;
19519
19520 let c = 3;
19521
19522 if a == 3 {
19523 let b = 2;
19524 } else {
19525 let c = 3;
19526 }
19527 }"
19528 .unindent(),
19529 cx,
19530 )
19531 .await;
19532
19533 assert_indent_guides(
19534 1..10,
19535 vec![
19536 indent_guide(buffer_id, 1, 9, 0),
19537 indent_guide(buffer_id, 6, 6, 1),
19538 indent_guide(buffer_id, 8, 8, 1),
19539 ],
19540 None,
19541 &mut cx,
19542 );
19543}
19544
19545#[gpui::test]
19546async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
19547 let (buffer_id, mut cx) = setup_indent_guides_editor(
19548 &"
19549 fn main() {
19550 if a {
19551 b(
19552 c,
19553 d,
19554 )
19555 } else {
19556 e(
19557 f
19558 )
19559 }
19560 }"
19561 .unindent(),
19562 cx,
19563 )
19564 .await;
19565
19566 assert_indent_guides(
19567 0..11,
19568 vec![
19569 indent_guide(buffer_id, 1, 10, 0),
19570 indent_guide(buffer_id, 2, 5, 1),
19571 indent_guide(buffer_id, 7, 9, 1),
19572 indent_guide(buffer_id, 3, 4, 2),
19573 indent_guide(buffer_id, 8, 8, 2),
19574 ],
19575 None,
19576 &mut cx,
19577 );
19578
19579 cx.update_editor(|editor, window, cx| {
19580 editor.fold_at(MultiBufferRow(2), window, cx);
19581 assert_eq!(
19582 editor.display_text(cx),
19583 "
19584 fn main() {
19585 if a {
19586 b(⋯
19587 )
19588 } else {
19589 e(
19590 f
19591 )
19592 }
19593 }"
19594 .unindent()
19595 );
19596 });
19597
19598 assert_indent_guides(
19599 0..11,
19600 vec![
19601 indent_guide(buffer_id, 1, 10, 0),
19602 indent_guide(buffer_id, 2, 5, 1),
19603 indent_guide(buffer_id, 7, 9, 1),
19604 indent_guide(buffer_id, 8, 8, 2),
19605 ],
19606 None,
19607 &mut cx,
19608 );
19609}
19610
19611#[gpui::test]
19612async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19613 let (buffer_id, mut cx) = setup_indent_guides_editor(
19614 &"
19615 block1
19616 block2
19617 block3
19618 block4
19619 block2
19620 block1
19621 block1"
19622 .unindent(),
19623 cx,
19624 )
19625 .await;
19626
19627 assert_indent_guides(
19628 1..10,
19629 vec![
19630 indent_guide(buffer_id, 1, 4, 0),
19631 indent_guide(buffer_id, 2, 3, 1),
19632 indent_guide(buffer_id, 3, 3, 2),
19633 ],
19634 None,
19635 &mut cx,
19636 );
19637}
19638
19639#[gpui::test]
19640async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19641 let (buffer_id, mut cx) = setup_indent_guides_editor(
19642 &"
19643 block1
19644 block2
19645 block3
19646
19647 block1
19648 block1"
19649 .unindent(),
19650 cx,
19651 )
19652 .await;
19653
19654 assert_indent_guides(
19655 0..6,
19656 vec![
19657 indent_guide(buffer_id, 1, 2, 0),
19658 indent_guide(buffer_id, 2, 2, 1),
19659 ],
19660 None,
19661 &mut cx,
19662 );
19663}
19664
19665#[gpui::test]
19666async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
19667 let (buffer_id, mut cx) = setup_indent_guides_editor(
19668 &"
19669 function component() {
19670 \treturn (
19671 \t\t\t
19672 \t\t<div>
19673 \t\t\t<abc></abc>
19674 \t\t</div>
19675 \t)
19676 }"
19677 .unindent(),
19678 cx,
19679 )
19680 .await;
19681
19682 assert_indent_guides(
19683 0..8,
19684 vec![
19685 indent_guide(buffer_id, 1, 6, 0),
19686 indent_guide(buffer_id, 2, 5, 1),
19687 indent_guide(buffer_id, 4, 4, 2),
19688 ],
19689 None,
19690 &mut cx,
19691 );
19692}
19693
19694#[gpui::test]
19695async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
19696 let (buffer_id, mut cx) = setup_indent_guides_editor(
19697 &"
19698 function component() {
19699 \treturn (
19700 \t
19701 \t\t<div>
19702 \t\t\t<abc></abc>
19703 \t\t</div>
19704 \t)
19705 }"
19706 .unindent(),
19707 cx,
19708 )
19709 .await;
19710
19711 assert_indent_guides(
19712 0..8,
19713 vec![
19714 indent_guide(buffer_id, 1, 6, 0),
19715 indent_guide(buffer_id, 2, 5, 1),
19716 indent_guide(buffer_id, 4, 4, 2),
19717 ],
19718 None,
19719 &mut cx,
19720 );
19721}
19722
19723#[gpui::test]
19724async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
19725 let (buffer_id, mut cx) = setup_indent_guides_editor(
19726 &"
19727 block1
19728
19729
19730
19731 block2
19732 "
19733 .unindent(),
19734 cx,
19735 )
19736 .await;
19737
19738 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19739}
19740
19741#[gpui::test]
19742async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
19743 let (buffer_id, mut cx) = setup_indent_guides_editor(
19744 &"
19745 def a:
19746 \tb = 3
19747 \tif True:
19748 \t\tc = 4
19749 \t\td = 5
19750 \tprint(b)
19751 "
19752 .unindent(),
19753 cx,
19754 )
19755 .await;
19756
19757 assert_indent_guides(
19758 0..6,
19759 vec![
19760 indent_guide(buffer_id, 1, 5, 0),
19761 indent_guide(buffer_id, 3, 4, 1),
19762 ],
19763 None,
19764 &mut cx,
19765 );
19766}
19767
19768#[gpui::test]
19769async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
19770 let (buffer_id, mut cx) = setup_indent_guides_editor(
19771 &"
19772 fn main() {
19773 let a = 1;
19774 }"
19775 .unindent(),
19776 cx,
19777 )
19778 .await;
19779
19780 cx.update_editor(|editor, window, cx| {
19781 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19782 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19783 });
19784 });
19785
19786 assert_indent_guides(
19787 0..3,
19788 vec![indent_guide(buffer_id, 1, 1, 0)],
19789 Some(vec![0]),
19790 &mut cx,
19791 );
19792}
19793
19794#[gpui::test]
19795async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
19796 let (buffer_id, mut cx) = setup_indent_guides_editor(
19797 &"
19798 fn main() {
19799 if 1 == 2 {
19800 let a = 1;
19801 }
19802 }"
19803 .unindent(),
19804 cx,
19805 )
19806 .await;
19807
19808 cx.update_editor(|editor, window, cx| {
19809 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19810 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19811 });
19812 });
19813
19814 assert_indent_guides(
19815 0..4,
19816 vec![
19817 indent_guide(buffer_id, 1, 3, 0),
19818 indent_guide(buffer_id, 2, 2, 1),
19819 ],
19820 Some(vec![1]),
19821 &mut cx,
19822 );
19823
19824 cx.update_editor(|editor, window, cx| {
19825 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19826 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19827 });
19828 });
19829
19830 assert_indent_guides(
19831 0..4,
19832 vec![
19833 indent_guide(buffer_id, 1, 3, 0),
19834 indent_guide(buffer_id, 2, 2, 1),
19835 ],
19836 Some(vec![1]),
19837 &mut cx,
19838 );
19839
19840 cx.update_editor(|editor, window, cx| {
19841 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19842 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
19843 });
19844 });
19845
19846 assert_indent_guides(
19847 0..4,
19848 vec![
19849 indent_guide(buffer_id, 1, 3, 0),
19850 indent_guide(buffer_id, 2, 2, 1),
19851 ],
19852 Some(vec![0]),
19853 &mut cx,
19854 );
19855}
19856
19857#[gpui::test]
19858async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
19859 let (buffer_id, mut cx) = setup_indent_guides_editor(
19860 &"
19861 fn main() {
19862 let a = 1;
19863
19864 let b = 2;
19865 }"
19866 .unindent(),
19867 cx,
19868 )
19869 .await;
19870
19871 cx.update_editor(|editor, window, cx| {
19872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19873 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19874 });
19875 });
19876
19877 assert_indent_guides(
19878 0..5,
19879 vec![indent_guide(buffer_id, 1, 3, 0)],
19880 Some(vec![0]),
19881 &mut cx,
19882 );
19883}
19884
19885#[gpui::test]
19886async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19887 let (buffer_id, mut cx) = setup_indent_guides_editor(
19888 &"
19889 def m:
19890 a = 1
19891 pass"
19892 .unindent(),
19893 cx,
19894 )
19895 .await;
19896
19897 cx.update_editor(|editor, window, cx| {
19898 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19899 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19900 });
19901 });
19902
19903 assert_indent_guides(
19904 0..3,
19905 vec![indent_guide(buffer_id, 1, 2, 0)],
19906 Some(vec![0]),
19907 &mut cx,
19908 );
19909}
19910
19911#[gpui::test]
19912async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19913 init_test(cx, |_| {});
19914 let mut cx = EditorTestContext::new(cx).await;
19915 let text = indoc! {
19916 "
19917 impl A {
19918 fn b() {
19919 0;
19920 3;
19921 5;
19922 6;
19923 7;
19924 }
19925 }
19926 "
19927 };
19928 let base_text = indoc! {
19929 "
19930 impl A {
19931 fn b() {
19932 0;
19933 1;
19934 2;
19935 3;
19936 4;
19937 }
19938 fn c() {
19939 5;
19940 6;
19941 7;
19942 }
19943 }
19944 "
19945 };
19946
19947 cx.update_editor(|editor, window, cx| {
19948 editor.set_text(text, window, cx);
19949
19950 editor.buffer().update(cx, |multibuffer, cx| {
19951 let buffer = multibuffer.as_singleton().unwrap();
19952 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19953
19954 multibuffer.set_all_diff_hunks_expanded(cx);
19955 multibuffer.add_diff(diff, cx);
19956
19957 buffer.read(cx).remote_id()
19958 })
19959 });
19960 cx.run_until_parked();
19961
19962 cx.assert_state_with_diff(
19963 indoc! { "
19964 impl A {
19965 fn b() {
19966 0;
19967 - 1;
19968 - 2;
19969 3;
19970 - 4;
19971 - }
19972 - fn c() {
19973 5;
19974 6;
19975 7;
19976 }
19977 }
19978 ˇ"
19979 }
19980 .to_string(),
19981 );
19982
19983 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19984 editor
19985 .snapshot(window, cx)
19986 .buffer_snapshot
19987 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19988 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19989 .collect::<Vec<_>>()
19990 });
19991 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19992 assert_eq!(
19993 actual_guides,
19994 vec![
19995 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19996 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19997 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19998 ]
19999 );
20000}
20001
20002#[gpui::test]
20003async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20004 init_test(cx, |_| {});
20005 let mut cx = EditorTestContext::new(cx).await;
20006
20007 let diff_base = r#"
20008 a
20009 b
20010 c
20011 "#
20012 .unindent();
20013
20014 cx.set_state(
20015 &r#"
20016 ˇA
20017 b
20018 C
20019 "#
20020 .unindent(),
20021 );
20022 cx.set_head_text(&diff_base);
20023 cx.update_editor(|editor, window, cx| {
20024 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20025 });
20026 executor.run_until_parked();
20027
20028 let both_hunks_expanded = r#"
20029 - a
20030 + ˇA
20031 b
20032 - c
20033 + C
20034 "#
20035 .unindent();
20036
20037 cx.assert_state_with_diff(both_hunks_expanded.clone());
20038
20039 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20040 let snapshot = editor.snapshot(window, cx);
20041 let hunks = editor
20042 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20043 .collect::<Vec<_>>();
20044 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20045 let buffer_id = hunks[0].buffer_id;
20046 hunks
20047 .into_iter()
20048 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20049 .collect::<Vec<_>>()
20050 });
20051 assert_eq!(hunk_ranges.len(), 2);
20052
20053 cx.update_editor(|editor, _, cx| {
20054 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20055 });
20056 executor.run_until_parked();
20057
20058 let second_hunk_expanded = r#"
20059 ˇA
20060 b
20061 - c
20062 + C
20063 "#
20064 .unindent();
20065
20066 cx.assert_state_with_diff(second_hunk_expanded);
20067
20068 cx.update_editor(|editor, _, cx| {
20069 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20070 });
20071 executor.run_until_parked();
20072
20073 cx.assert_state_with_diff(both_hunks_expanded.clone());
20074
20075 cx.update_editor(|editor, _, cx| {
20076 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20077 });
20078 executor.run_until_parked();
20079
20080 let first_hunk_expanded = r#"
20081 - a
20082 + ˇA
20083 b
20084 C
20085 "#
20086 .unindent();
20087
20088 cx.assert_state_with_diff(first_hunk_expanded);
20089
20090 cx.update_editor(|editor, _, cx| {
20091 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20092 });
20093 executor.run_until_parked();
20094
20095 cx.assert_state_with_diff(both_hunks_expanded);
20096
20097 cx.set_state(
20098 &r#"
20099 ˇA
20100 b
20101 "#
20102 .unindent(),
20103 );
20104 cx.run_until_parked();
20105
20106 // TODO this cursor position seems bad
20107 cx.assert_state_with_diff(
20108 r#"
20109 - ˇa
20110 + A
20111 b
20112 "#
20113 .unindent(),
20114 );
20115
20116 cx.update_editor(|editor, window, cx| {
20117 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20118 });
20119
20120 cx.assert_state_with_diff(
20121 r#"
20122 - ˇa
20123 + A
20124 b
20125 - c
20126 "#
20127 .unindent(),
20128 );
20129
20130 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20131 let snapshot = editor.snapshot(window, cx);
20132 let hunks = editor
20133 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20134 .collect::<Vec<_>>();
20135 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20136 let buffer_id = hunks[0].buffer_id;
20137 hunks
20138 .into_iter()
20139 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20140 .collect::<Vec<_>>()
20141 });
20142 assert_eq!(hunk_ranges.len(), 2);
20143
20144 cx.update_editor(|editor, _, cx| {
20145 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20146 });
20147 executor.run_until_parked();
20148
20149 cx.assert_state_with_diff(
20150 r#"
20151 - ˇa
20152 + A
20153 b
20154 "#
20155 .unindent(),
20156 );
20157}
20158
20159#[gpui::test]
20160async fn test_toggle_deletion_hunk_at_start_of_file(
20161 executor: BackgroundExecutor,
20162 cx: &mut TestAppContext,
20163) {
20164 init_test(cx, |_| {});
20165 let mut cx = EditorTestContext::new(cx).await;
20166
20167 let diff_base = r#"
20168 a
20169 b
20170 c
20171 "#
20172 .unindent();
20173
20174 cx.set_state(
20175 &r#"
20176 ˇb
20177 c
20178 "#
20179 .unindent(),
20180 );
20181 cx.set_head_text(&diff_base);
20182 cx.update_editor(|editor, window, cx| {
20183 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20184 });
20185 executor.run_until_parked();
20186
20187 let hunk_expanded = r#"
20188 - a
20189 ˇb
20190 c
20191 "#
20192 .unindent();
20193
20194 cx.assert_state_with_diff(hunk_expanded.clone());
20195
20196 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20197 let snapshot = editor.snapshot(window, cx);
20198 let hunks = editor
20199 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20200 .collect::<Vec<_>>();
20201 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20202 let buffer_id = hunks[0].buffer_id;
20203 hunks
20204 .into_iter()
20205 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20206 .collect::<Vec<_>>()
20207 });
20208 assert_eq!(hunk_ranges.len(), 1);
20209
20210 cx.update_editor(|editor, _, cx| {
20211 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20212 });
20213 executor.run_until_parked();
20214
20215 let hunk_collapsed = r#"
20216 ˇb
20217 c
20218 "#
20219 .unindent();
20220
20221 cx.assert_state_with_diff(hunk_collapsed);
20222
20223 cx.update_editor(|editor, _, cx| {
20224 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20225 });
20226 executor.run_until_parked();
20227
20228 cx.assert_state_with_diff(hunk_expanded);
20229}
20230
20231#[gpui::test]
20232async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20233 init_test(cx, |_| {});
20234
20235 let fs = FakeFs::new(cx.executor());
20236 fs.insert_tree(
20237 path!("/test"),
20238 json!({
20239 ".git": {},
20240 "file-1": "ONE\n",
20241 "file-2": "TWO\n",
20242 "file-3": "THREE\n",
20243 }),
20244 )
20245 .await;
20246
20247 fs.set_head_for_repo(
20248 path!("/test/.git").as_ref(),
20249 &[
20250 ("file-1".into(), "one\n".into()),
20251 ("file-2".into(), "two\n".into()),
20252 ("file-3".into(), "three\n".into()),
20253 ],
20254 "deadbeef",
20255 );
20256
20257 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20258 let mut buffers = vec![];
20259 for i in 1..=3 {
20260 let buffer = project
20261 .update(cx, |project, cx| {
20262 let path = format!(path!("/test/file-{}"), i);
20263 project.open_local_buffer(path, cx)
20264 })
20265 .await
20266 .unwrap();
20267 buffers.push(buffer);
20268 }
20269
20270 let multibuffer = cx.new(|cx| {
20271 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20272 multibuffer.set_all_diff_hunks_expanded(cx);
20273 for buffer in &buffers {
20274 let snapshot = buffer.read(cx).snapshot();
20275 multibuffer.set_excerpts_for_path(
20276 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20277 buffer.clone(),
20278 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20279 2,
20280 cx,
20281 );
20282 }
20283 multibuffer
20284 });
20285
20286 let editor = cx.add_window(|window, cx| {
20287 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20288 });
20289 cx.run_until_parked();
20290
20291 let snapshot = editor
20292 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20293 .unwrap();
20294 let hunks = snapshot
20295 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20296 .map(|hunk| match hunk {
20297 DisplayDiffHunk::Unfolded {
20298 display_row_range, ..
20299 } => display_row_range,
20300 DisplayDiffHunk::Folded { .. } => unreachable!(),
20301 })
20302 .collect::<Vec<_>>();
20303 assert_eq!(
20304 hunks,
20305 [
20306 DisplayRow(2)..DisplayRow(4),
20307 DisplayRow(7)..DisplayRow(9),
20308 DisplayRow(12)..DisplayRow(14),
20309 ]
20310 );
20311}
20312
20313#[gpui::test]
20314async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20315 init_test(cx, |_| {});
20316
20317 let mut cx = EditorTestContext::new(cx).await;
20318 cx.set_head_text(indoc! { "
20319 one
20320 two
20321 three
20322 four
20323 five
20324 "
20325 });
20326 cx.set_index_text(indoc! { "
20327 one
20328 two
20329 three
20330 four
20331 five
20332 "
20333 });
20334 cx.set_state(indoc! {"
20335 one
20336 TWO
20337 ˇTHREE
20338 FOUR
20339 five
20340 "});
20341 cx.run_until_parked();
20342 cx.update_editor(|editor, window, cx| {
20343 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20344 });
20345 cx.run_until_parked();
20346 cx.assert_index_text(Some(indoc! {"
20347 one
20348 TWO
20349 THREE
20350 FOUR
20351 five
20352 "}));
20353 cx.set_state(indoc! { "
20354 one
20355 TWO
20356 ˇTHREE-HUNDRED
20357 FOUR
20358 five
20359 "});
20360 cx.run_until_parked();
20361 cx.update_editor(|editor, window, cx| {
20362 let snapshot = editor.snapshot(window, cx);
20363 let hunks = editor
20364 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20365 .collect::<Vec<_>>();
20366 assert_eq!(hunks.len(), 1);
20367 assert_eq!(
20368 hunks[0].status(),
20369 DiffHunkStatus {
20370 kind: DiffHunkStatusKind::Modified,
20371 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
20372 }
20373 );
20374
20375 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20376 });
20377 cx.run_until_parked();
20378 cx.assert_index_text(Some(indoc! {"
20379 one
20380 TWO
20381 THREE-HUNDRED
20382 FOUR
20383 five
20384 "}));
20385}
20386
20387#[gpui::test]
20388fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
20389 init_test(cx, |_| {});
20390
20391 let editor = cx.add_window(|window, cx| {
20392 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
20393 build_editor(buffer, window, cx)
20394 });
20395
20396 let render_args = Arc::new(Mutex::new(None));
20397 let snapshot = editor
20398 .update(cx, |editor, window, cx| {
20399 let snapshot = editor.buffer().read(cx).snapshot(cx);
20400 let range =
20401 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
20402
20403 struct RenderArgs {
20404 row: MultiBufferRow,
20405 folded: bool,
20406 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
20407 }
20408
20409 let crease = Crease::inline(
20410 range,
20411 FoldPlaceholder::test(),
20412 {
20413 let toggle_callback = render_args.clone();
20414 move |row, folded, callback, _window, _cx| {
20415 *toggle_callback.lock() = Some(RenderArgs {
20416 row,
20417 folded,
20418 callback,
20419 });
20420 div()
20421 }
20422 },
20423 |_row, _folded, _window, _cx| div(),
20424 );
20425
20426 editor.insert_creases(Some(crease), cx);
20427 let snapshot = editor.snapshot(window, cx);
20428 let _div =
20429 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
20430 snapshot
20431 })
20432 .unwrap();
20433
20434 let render_args = render_args.lock().take().unwrap();
20435 assert_eq!(render_args.row, MultiBufferRow(1));
20436 assert!(!render_args.folded);
20437 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20438
20439 cx.update_window(*editor, |_, window, cx| {
20440 (render_args.callback)(true, window, cx)
20441 })
20442 .unwrap();
20443 let snapshot = editor
20444 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20445 .unwrap();
20446 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
20447
20448 cx.update_window(*editor, |_, window, cx| {
20449 (render_args.callback)(false, window, cx)
20450 })
20451 .unwrap();
20452 let snapshot = editor
20453 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20454 .unwrap();
20455 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20456}
20457
20458#[gpui::test]
20459async fn test_input_text(cx: &mut TestAppContext) {
20460 init_test(cx, |_| {});
20461 let mut cx = EditorTestContext::new(cx).await;
20462
20463 cx.set_state(
20464 &r#"ˇone
20465 two
20466
20467 three
20468 fourˇ
20469 five
20470
20471 siˇx"#
20472 .unindent(),
20473 );
20474
20475 cx.dispatch_action(HandleInput(String::new()));
20476 cx.assert_editor_state(
20477 &r#"ˇone
20478 two
20479
20480 three
20481 fourˇ
20482 five
20483
20484 siˇx"#
20485 .unindent(),
20486 );
20487
20488 cx.dispatch_action(HandleInput("AAAA".to_string()));
20489 cx.assert_editor_state(
20490 &r#"AAAAˇone
20491 two
20492
20493 three
20494 fourAAAAˇ
20495 five
20496
20497 siAAAAˇx"#
20498 .unindent(),
20499 );
20500}
20501
20502#[gpui::test]
20503async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
20504 init_test(cx, |_| {});
20505
20506 let mut cx = EditorTestContext::new(cx).await;
20507 cx.set_state(
20508 r#"let foo = 1;
20509let foo = 2;
20510let foo = 3;
20511let fooˇ = 4;
20512let foo = 5;
20513let foo = 6;
20514let foo = 7;
20515let foo = 8;
20516let foo = 9;
20517let foo = 10;
20518let foo = 11;
20519let foo = 12;
20520let foo = 13;
20521let foo = 14;
20522let foo = 15;"#,
20523 );
20524
20525 cx.update_editor(|e, window, cx| {
20526 assert_eq!(
20527 e.next_scroll_position,
20528 NextScrollCursorCenterTopBottom::Center,
20529 "Default next scroll direction is center",
20530 );
20531
20532 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20533 assert_eq!(
20534 e.next_scroll_position,
20535 NextScrollCursorCenterTopBottom::Top,
20536 "After center, next scroll direction should be top",
20537 );
20538
20539 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20540 assert_eq!(
20541 e.next_scroll_position,
20542 NextScrollCursorCenterTopBottom::Bottom,
20543 "After top, next scroll direction should be bottom",
20544 );
20545
20546 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20547 assert_eq!(
20548 e.next_scroll_position,
20549 NextScrollCursorCenterTopBottom::Center,
20550 "After bottom, scrolling should start over",
20551 );
20552
20553 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20554 assert_eq!(
20555 e.next_scroll_position,
20556 NextScrollCursorCenterTopBottom::Top,
20557 "Scrolling continues if retriggered fast enough"
20558 );
20559 });
20560
20561 cx.executor()
20562 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
20563 cx.executor().run_until_parked();
20564 cx.update_editor(|e, _, _| {
20565 assert_eq!(
20566 e.next_scroll_position,
20567 NextScrollCursorCenterTopBottom::Center,
20568 "If scrolling is not triggered fast enough, it should reset"
20569 );
20570 });
20571}
20572
20573#[gpui::test]
20574async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
20575 init_test(cx, |_| {});
20576 let mut cx = EditorLspTestContext::new_rust(
20577 lsp::ServerCapabilities {
20578 definition_provider: Some(lsp::OneOf::Left(true)),
20579 references_provider: Some(lsp::OneOf::Left(true)),
20580 ..lsp::ServerCapabilities::default()
20581 },
20582 cx,
20583 )
20584 .await;
20585
20586 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
20587 let go_to_definition = cx
20588 .lsp
20589 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20590 move |params, _| async move {
20591 if empty_go_to_definition {
20592 Ok(None)
20593 } else {
20594 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20595 uri: params.text_document_position_params.text_document.uri,
20596 range: lsp::Range::new(
20597 lsp::Position::new(4, 3),
20598 lsp::Position::new(4, 6),
20599 ),
20600 })))
20601 }
20602 },
20603 );
20604 let references = cx
20605 .lsp
20606 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20607 Ok(Some(vec![lsp::Location {
20608 uri: params.text_document_position.text_document.uri,
20609 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20610 }]))
20611 });
20612 (go_to_definition, references)
20613 };
20614
20615 cx.set_state(
20616 &r#"fn one() {
20617 let mut a = ˇtwo();
20618 }
20619
20620 fn two() {}"#
20621 .unindent(),
20622 );
20623 set_up_lsp_handlers(false, &mut cx);
20624 let navigated = cx
20625 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20626 .await
20627 .expect("Failed to navigate to definition");
20628 assert_eq!(
20629 navigated,
20630 Navigated::Yes,
20631 "Should have navigated to definition from the GetDefinition response"
20632 );
20633 cx.assert_editor_state(
20634 &r#"fn one() {
20635 let mut a = two();
20636 }
20637
20638 fn «twoˇ»() {}"#
20639 .unindent(),
20640 );
20641
20642 let editors = cx.update_workspace(|workspace, _, cx| {
20643 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20644 });
20645 cx.update_editor(|_, _, test_editor_cx| {
20646 assert_eq!(
20647 editors.len(),
20648 1,
20649 "Initially, only one, test, editor should be open in the workspace"
20650 );
20651 assert_eq!(
20652 test_editor_cx.entity(),
20653 editors.last().expect("Asserted len is 1").clone()
20654 );
20655 });
20656
20657 set_up_lsp_handlers(true, &mut cx);
20658 let navigated = cx
20659 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20660 .await
20661 .expect("Failed to navigate to lookup references");
20662 assert_eq!(
20663 navigated,
20664 Navigated::Yes,
20665 "Should have navigated to references as a fallback after empty GoToDefinition response"
20666 );
20667 // We should not change the selections in the existing file,
20668 // if opening another milti buffer with the references
20669 cx.assert_editor_state(
20670 &r#"fn one() {
20671 let mut a = two();
20672 }
20673
20674 fn «twoˇ»() {}"#
20675 .unindent(),
20676 );
20677 let editors = cx.update_workspace(|workspace, _, cx| {
20678 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20679 });
20680 cx.update_editor(|_, _, test_editor_cx| {
20681 assert_eq!(
20682 editors.len(),
20683 2,
20684 "After falling back to references search, we open a new editor with the results"
20685 );
20686 let references_fallback_text = editors
20687 .into_iter()
20688 .find(|new_editor| *new_editor != test_editor_cx.entity())
20689 .expect("Should have one non-test editor now")
20690 .read(test_editor_cx)
20691 .text(test_editor_cx);
20692 assert_eq!(
20693 references_fallback_text, "fn one() {\n let mut a = two();\n}",
20694 "Should use the range from the references response and not the GoToDefinition one"
20695 );
20696 });
20697}
20698
20699#[gpui::test]
20700async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
20701 init_test(cx, |_| {});
20702 cx.update(|cx| {
20703 let mut editor_settings = EditorSettings::get_global(cx).clone();
20704 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
20705 EditorSettings::override_global(editor_settings, cx);
20706 });
20707 let mut cx = EditorLspTestContext::new_rust(
20708 lsp::ServerCapabilities {
20709 definition_provider: Some(lsp::OneOf::Left(true)),
20710 references_provider: Some(lsp::OneOf::Left(true)),
20711 ..lsp::ServerCapabilities::default()
20712 },
20713 cx,
20714 )
20715 .await;
20716 let original_state = r#"fn one() {
20717 let mut a = ˇtwo();
20718 }
20719
20720 fn two() {}"#
20721 .unindent();
20722 cx.set_state(&original_state);
20723
20724 let mut go_to_definition = cx
20725 .lsp
20726 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20727 move |_, _| async move { Ok(None) },
20728 );
20729 let _references = cx
20730 .lsp
20731 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
20732 panic!("Should not call for references with no go to definition fallback")
20733 });
20734
20735 let navigated = cx
20736 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20737 .await
20738 .expect("Failed to navigate to lookup references");
20739 go_to_definition
20740 .next()
20741 .await
20742 .expect("Should have called the go_to_definition handler");
20743
20744 assert_eq!(
20745 navigated,
20746 Navigated::No,
20747 "Should have navigated to references as a fallback after empty GoToDefinition response"
20748 );
20749 cx.assert_editor_state(&original_state);
20750 let editors = cx.update_workspace(|workspace, _, cx| {
20751 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20752 });
20753 cx.update_editor(|_, _, _| {
20754 assert_eq!(
20755 editors.len(),
20756 1,
20757 "After unsuccessful fallback, no other editor should have been opened"
20758 );
20759 });
20760}
20761
20762#[gpui::test]
20763async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
20764 init_test(cx, |_| {});
20765
20766 let language = Arc::new(Language::new(
20767 LanguageConfig::default(),
20768 Some(tree_sitter_rust::LANGUAGE.into()),
20769 ));
20770
20771 let text = r#"
20772 #[cfg(test)]
20773 mod tests() {
20774 #[test]
20775 fn runnable_1() {
20776 let a = 1;
20777 }
20778
20779 #[test]
20780 fn runnable_2() {
20781 let a = 1;
20782 let b = 2;
20783 }
20784 }
20785 "#
20786 .unindent();
20787
20788 let fs = FakeFs::new(cx.executor());
20789 fs.insert_file("/file.rs", Default::default()).await;
20790
20791 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20792 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20793 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20794 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
20795 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
20796
20797 let editor = cx.new_window_entity(|window, cx| {
20798 Editor::new(
20799 EditorMode::full(),
20800 multi_buffer,
20801 Some(project.clone()),
20802 window,
20803 cx,
20804 )
20805 });
20806
20807 editor.update_in(cx, |editor, window, cx| {
20808 let snapshot = editor.buffer().read(cx).snapshot(cx);
20809 editor.tasks.insert(
20810 (buffer.read(cx).remote_id(), 3),
20811 RunnableTasks {
20812 templates: vec![],
20813 offset: snapshot.anchor_before(43),
20814 column: 0,
20815 extra_variables: HashMap::default(),
20816 context_range: BufferOffset(43)..BufferOffset(85),
20817 },
20818 );
20819 editor.tasks.insert(
20820 (buffer.read(cx).remote_id(), 8),
20821 RunnableTasks {
20822 templates: vec![],
20823 offset: snapshot.anchor_before(86),
20824 column: 0,
20825 extra_variables: HashMap::default(),
20826 context_range: BufferOffset(86)..BufferOffset(191),
20827 },
20828 );
20829
20830 // Test finding task when cursor is inside function body
20831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20832 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
20833 });
20834 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20835 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
20836
20837 // Test finding task when cursor is on function name
20838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20839 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
20840 });
20841 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20842 assert_eq!(row, 8, "Should find task when cursor is on function name");
20843 });
20844}
20845
20846#[gpui::test]
20847async fn test_folding_buffers(cx: &mut TestAppContext) {
20848 init_test(cx, |_| {});
20849
20850 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20851 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
20852 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
20853
20854 let fs = FakeFs::new(cx.executor());
20855 fs.insert_tree(
20856 path!("/a"),
20857 json!({
20858 "first.rs": sample_text_1,
20859 "second.rs": sample_text_2,
20860 "third.rs": sample_text_3,
20861 }),
20862 )
20863 .await;
20864 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20865 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20866 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20867 let worktree = project.update(cx, |project, cx| {
20868 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20869 assert_eq!(worktrees.len(), 1);
20870 worktrees.pop().unwrap()
20871 });
20872 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20873
20874 let buffer_1 = project
20875 .update(cx, |project, cx| {
20876 project.open_buffer((worktree_id, "first.rs"), cx)
20877 })
20878 .await
20879 .unwrap();
20880 let buffer_2 = project
20881 .update(cx, |project, cx| {
20882 project.open_buffer((worktree_id, "second.rs"), cx)
20883 })
20884 .await
20885 .unwrap();
20886 let buffer_3 = project
20887 .update(cx, |project, cx| {
20888 project.open_buffer((worktree_id, "third.rs"), cx)
20889 })
20890 .await
20891 .unwrap();
20892
20893 let multi_buffer = cx.new(|cx| {
20894 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20895 multi_buffer.push_excerpts(
20896 buffer_1.clone(),
20897 [
20898 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20899 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20900 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20901 ],
20902 cx,
20903 );
20904 multi_buffer.push_excerpts(
20905 buffer_2.clone(),
20906 [
20907 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20908 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20909 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20910 ],
20911 cx,
20912 );
20913 multi_buffer.push_excerpts(
20914 buffer_3.clone(),
20915 [
20916 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20917 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20918 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20919 ],
20920 cx,
20921 );
20922 multi_buffer
20923 });
20924 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20925 Editor::new(
20926 EditorMode::full(),
20927 multi_buffer.clone(),
20928 Some(project.clone()),
20929 window,
20930 cx,
20931 )
20932 });
20933
20934 assert_eq!(
20935 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20936 "\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",
20937 );
20938
20939 multi_buffer_editor.update(cx, |editor, cx| {
20940 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20941 });
20942 assert_eq!(
20943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20944 "\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",
20945 "After folding the first buffer, its text should not be displayed"
20946 );
20947
20948 multi_buffer_editor.update(cx, |editor, cx| {
20949 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20950 });
20951 assert_eq!(
20952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20953 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20954 "After folding the second buffer, its text should not be displayed"
20955 );
20956
20957 multi_buffer_editor.update(cx, |editor, cx| {
20958 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20959 });
20960 assert_eq!(
20961 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20962 "\n\n\n\n\n",
20963 "After folding the third buffer, its text should not be displayed"
20964 );
20965
20966 // Emulate selection inside the fold logic, that should work
20967 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20968 editor
20969 .snapshot(window, cx)
20970 .next_line_boundary(Point::new(0, 4));
20971 });
20972
20973 multi_buffer_editor.update(cx, |editor, cx| {
20974 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20975 });
20976 assert_eq!(
20977 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20978 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20979 "After unfolding the second buffer, its text should be displayed"
20980 );
20981
20982 // Typing inside of buffer 1 causes that buffer to be unfolded.
20983 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20984 assert_eq!(
20985 multi_buffer
20986 .read(cx)
20987 .snapshot(cx)
20988 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20989 .collect::<String>(),
20990 "bbbb"
20991 );
20992 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20993 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20994 });
20995 editor.handle_input("B", window, cx);
20996 });
20997
20998 assert_eq!(
20999 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21000 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21001 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21002 );
21003
21004 multi_buffer_editor.update(cx, |editor, cx| {
21005 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21006 });
21007 assert_eq!(
21008 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21009 "\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",
21010 "After unfolding the all buffers, all original text should be displayed"
21011 );
21012}
21013
21014#[gpui::test]
21015async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21016 init_test(cx, |_| {});
21017
21018 let sample_text_1 = "1111\n2222\n3333".to_string();
21019 let sample_text_2 = "4444\n5555\n6666".to_string();
21020 let sample_text_3 = "7777\n8888\n9999".to_string();
21021
21022 let fs = FakeFs::new(cx.executor());
21023 fs.insert_tree(
21024 path!("/a"),
21025 json!({
21026 "first.rs": sample_text_1,
21027 "second.rs": sample_text_2,
21028 "third.rs": sample_text_3,
21029 }),
21030 )
21031 .await;
21032 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21033 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21034 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21035 let worktree = project.update(cx, |project, cx| {
21036 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21037 assert_eq!(worktrees.len(), 1);
21038 worktrees.pop().unwrap()
21039 });
21040 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21041
21042 let buffer_1 = project
21043 .update(cx, |project, cx| {
21044 project.open_buffer((worktree_id, "first.rs"), cx)
21045 })
21046 .await
21047 .unwrap();
21048 let buffer_2 = project
21049 .update(cx, |project, cx| {
21050 project.open_buffer((worktree_id, "second.rs"), cx)
21051 })
21052 .await
21053 .unwrap();
21054 let buffer_3 = project
21055 .update(cx, |project, cx| {
21056 project.open_buffer((worktree_id, "third.rs"), cx)
21057 })
21058 .await
21059 .unwrap();
21060
21061 let multi_buffer = cx.new(|cx| {
21062 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21063 multi_buffer.push_excerpts(
21064 buffer_1.clone(),
21065 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21066 cx,
21067 );
21068 multi_buffer.push_excerpts(
21069 buffer_2.clone(),
21070 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21071 cx,
21072 );
21073 multi_buffer.push_excerpts(
21074 buffer_3.clone(),
21075 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21076 cx,
21077 );
21078 multi_buffer
21079 });
21080
21081 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21082 Editor::new(
21083 EditorMode::full(),
21084 multi_buffer,
21085 Some(project.clone()),
21086 window,
21087 cx,
21088 )
21089 });
21090
21091 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21092 assert_eq!(
21093 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21094 full_text,
21095 );
21096
21097 multi_buffer_editor.update(cx, |editor, cx| {
21098 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21099 });
21100 assert_eq!(
21101 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21102 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21103 "After folding the first buffer, its text should not be displayed"
21104 );
21105
21106 multi_buffer_editor.update(cx, |editor, cx| {
21107 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21108 });
21109
21110 assert_eq!(
21111 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21112 "\n\n\n\n\n\n7777\n8888\n9999",
21113 "After folding the second buffer, its text should not be displayed"
21114 );
21115
21116 multi_buffer_editor.update(cx, |editor, cx| {
21117 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21118 });
21119 assert_eq!(
21120 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21121 "\n\n\n\n\n",
21122 "After folding the third buffer, its text should not be displayed"
21123 );
21124
21125 multi_buffer_editor.update(cx, |editor, cx| {
21126 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21127 });
21128 assert_eq!(
21129 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21130 "\n\n\n\n4444\n5555\n6666\n\n",
21131 "After unfolding the second buffer, its text should be displayed"
21132 );
21133
21134 multi_buffer_editor.update(cx, |editor, cx| {
21135 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21136 });
21137 assert_eq!(
21138 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21139 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21140 "After unfolding the first buffer, its text should be displayed"
21141 );
21142
21143 multi_buffer_editor.update(cx, |editor, cx| {
21144 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21145 });
21146 assert_eq!(
21147 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21148 full_text,
21149 "After unfolding all buffers, all original text should be displayed"
21150 );
21151}
21152
21153#[gpui::test]
21154async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21155 init_test(cx, |_| {});
21156
21157 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21158
21159 let fs = FakeFs::new(cx.executor());
21160 fs.insert_tree(
21161 path!("/a"),
21162 json!({
21163 "main.rs": sample_text,
21164 }),
21165 )
21166 .await;
21167 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21168 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21169 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21170 let worktree = project.update(cx, |project, cx| {
21171 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21172 assert_eq!(worktrees.len(), 1);
21173 worktrees.pop().unwrap()
21174 });
21175 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21176
21177 let buffer_1 = project
21178 .update(cx, |project, cx| {
21179 project.open_buffer((worktree_id, "main.rs"), cx)
21180 })
21181 .await
21182 .unwrap();
21183
21184 let multi_buffer = cx.new(|cx| {
21185 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21186 multi_buffer.push_excerpts(
21187 buffer_1.clone(),
21188 [ExcerptRange::new(
21189 Point::new(0, 0)
21190 ..Point::new(
21191 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21192 0,
21193 ),
21194 )],
21195 cx,
21196 );
21197 multi_buffer
21198 });
21199 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21200 Editor::new(
21201 EditorMode::full(),
21202 multi_buffer,
21203 Some(project.clone()),
21204 window,
21205 cx,
21206 )
21207 });
21208
21209 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21210 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21211 enum TestHighlight {}
21212 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21213 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21214 editor.highlight_text::<TestHighlight>(
21215 vec![highlight_range.clone()],
21216 HighlightStyle::color(Hsla::green()),
21217 cx,
21218 );
21219 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21220 s.select_ranges(Some(highlight_range))
21221 });
21222 });
21223
21224 let full_text = format!("\n\n{sample_text}");
21225 assert_eq!(
21226 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21227 full_text,
21228 );
21229}
21230
21231#[gpui::test]
21232async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
21233 init_test(cx, |_| {});
21234 cx.update(|cx| {
21235 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
21236 "keymaps/default-linux.json",
21237 cx,
21238 )
21239 .unwrap();
21240 cx.bind_keys(default_key_bindings);
21241 });
21242
21243 let (editor, cx) = cx.add_window_view(|window, cx| {
21244 let multi_buffer = MultiBuffer::build_multi(
21245 [
21246 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
21247 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
21248 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
21249 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
21250 ],
21251 cx,
21252 );
21253 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
21254
21255 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
21256 // fold all but the second buffer, so that we test navigating between two
21257 // adjacent folded buffers, as well as folded buffers at the start and
21258 // end the multibuffer
21259 editor.fold_buffer(buffer_ids[0], cx);
21260 editor.fold_buffer(buffer_ids[2], cx);
21261 editor.fold_buffer(buffer_ids[3], cx);
21262
21263 editor
21264 });
21265 cx.simulate_resize(size(px(1000.), px(1000.)));
21266
21267 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
21268 cx.assert_excerpts_with_selections(indoc! {"
21269 [EXCERPT]
21270 ˇ[FOLDED]
21271 [EXCERPT]
21272 a1
21273 b1
21274 [EXCERPT]
21275 [FOLDED]
21276 [EXCERPT]
21277 [FOLDED]
21278 "
21279 });
21280 cx.simulate_keystroke("down");
21281 cx.assert_excerpts_with_selections(indoc! {"
21282 [EXCERPT]
21283 [FOLDED]
21284 [EXCERPT]
21285 ˇa1
21286 b1
21287 [EXCERPT]
21288 [FOLDED]
21289 [EXCERPT]
21290 [FOLDED]
21291 "
21292 });
21293 cx.simulate_keystroke("down");
21294 cx.assert_excerpts_with_selections(indoc! {"
21295 [EXCERPT]
21296 [FOLDED]
21297 [EXCERPT]
21298 a1
21299 ˇb1
21300 [EXCERPT]
21301 [FOLDED]
21302 [EXCERPT]
21303 [FOLDED]
21304 "
21305 });
21306 cx.simulate_keystroke("down");
21307 cx.assert_excerpts_with_selections(indoc! {"
21308 [EXCERPT]
21309 [FOLDED]
21310 [EXCERPT]
21311 a1
21312 b1
21313 ˇ[EXCERPT]
21314 [FOLDED]
21315 [EXCERPT]
21316 [FOLDED]
21317 "
21318 });
21319 cx.simulate_keystroke("down");
21320 cx.assert_excerpts_with_selections(indoc! {"
21321 [EXCERPT]
21322 [FOLDED]
21323 [EXCERPT]
21324 a1
21325 b1
21326 [EXCERPT]
21327 ˇ[FOLDED]
21328 [EXCERPT]
21329 [FOLDED]
21330 "
21331 });
21332 for _ in 0..5 {
21333 cx.simulate_keystroke("down");
21334 cx.assert_excerpts_with_selections(indoc! {"
21335 [EXCERPT]
21336 [FOLDED]
21337 [EXCERPT]
21338 a1
21339 b1
21340 [EXCERPT]
21341 [FOLDED]
21342 [EXCERPT]
21343 ˇ[FOLDED]
21344 "
21345 });
21346 }
21347
21348 cx.simulate_keystroke("up");
21349 cx.assert_excerpts_with_selections(indoc! {"
21350 [EXCERPT]
21351 [FOLDED]
21352 [EXCERPT]
21353 a1
21354 b1
21355 [EXCERPT]
21356 ˇ[FOLDED]
21357 [EXCERPT]
21358 [FOLDED]
21359 "
21360 });
21361 cx.simulate_keystroke("up");
21362 cx.assert_excerpts_with_selections(indoc! {"
21363 [EXCERPT]
21364 [FOLDED]
21365 [EXCERPT]
21366 a1
21367 b1
21368 ˇ[EXCERPT]
21369 [FOLDED]
21370 [EXCERPT]
21371 [FOLDED]
21372 "
21373 });
21374 cx.simulate_keystroke("up");
21375 cx.assert_excerpts_with_selections(indoc! {"
21376 [EXCERPT]
21377 [FOLDED]
21378 [EXCERPT]
21379 a1
21380 ˇb1
21381 [EXCERPT]
21382 [FOLDED]
21383 [EXCERPT]
21384 [FOLDED]
21385 "
21386 });
21387 cx.simulate_keystroke("up");
21388 cx.assert_excerpts_with_selections(indoc! {"
21389 [EXCERPT]
21390 [FOLDED]
21391 [EXCERPT]
21392 ˇa1
21393 b1
21394 [EXCERPT]
21395 [FOLDED]
21396 [EXCERPT]
21397 [FOLDED]
21398 "
21399 });
21400 for _ in 0..5 {
21401 cx.simulate_keystroke("up");
21402 cx.assert_excerpts_with_selections(indoc! {"
21403 [EXCERPT]
21404 ˇ[FOLDED]
21405 [EXCERPT]
21406 a1
21407 b1
21408 [EXCERPT]
21409 [FOLDED]
21410 [EXCERPT]
21411 [FOLDED]
21412 "
21413 });
21414 }
21415}
21416
21417#[gpui::test]
21418async fn test_edit_prediction_text(cx: &mut TestAppContext) {
21419 init_test(cx, |_| {});
21420
21421 // Simple insertion
21422 assert_highlighted_edits(
21423 "Hello, world!",
21424 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
21425 true,
21426 cx,
21427 |highlighted_edits, cx| {
21428 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
21429 assert_eq!(highlighted_edits.highlights.len(), 1);
21430 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
21431 assert_eq!(
21432 highlighted_edits.highlights[0].1.background_color,
21433 Some(cx.theme().status().created_background)
21434 );
21435 },
21436 )
21437 .await;
21438
21439 // Replacement
21440 assert_highlighted_edits(
21441 "This is a test.",
21442 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
21443 false,
21444 cx,
21445 |highlighted_edits, cx| {
21446 assert_eq!(highlighted_edits.text, "That is a test.");
21447 assert_eq!(highlighted_edits.highlights.len(), 1);
21448 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
21449 assert_eq!(
21450 highlighted_edits.highlights[0].1.background_color,
21451 Some(cx.theme().status().created_background)
21452 );
21453 },
21454 )
21455 .await;
21456
21457 // Multiple edits
21458 assert_highlighted_edits(
21459 "Hello, world!",
21460 vec![
21461 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
21462 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
21463 ],
21464 false,
21465 cx,
21466 |highlighted_edits, cx| {
21467 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
21468 assert_eq!(highlighted_edits.highlights.len(), 2);
21469 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
21470 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
21471 assert_eq!(
21472 highlighted_edits.highlights[0].1.background_color,
21473 Some(cx.theme().status().created_background)
21474 );
21475 assert_eq!(
21476 highlighted_edits.highlights[1].1.background_color,
21477 Some(cx.theme().status().created_background)
21478 );
21479 },
21480 )
21481 .await;
21482
21483 // Multiple lines with edits
21484 assert_highlighted_edits(
21485 "First line\nSecond line\nThird line\nFourth line",
21486 vec![
21487 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
21488 (
21489 Point::new(2, 0)..Point::new(2, 10),
21490 "New third line".to_string(),
21491 ),
21492 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
21493 ],
21494 false,
21495 cx,
21496 |highlighted_edits, cx| {
21497 assert_eq!(
21498 highlighted_edits.text,
21499 "Second modified\nNew third line\nFourth updated line"
21500 );
21501 assert_eq!(highlighted_edits.highlights.len(), 3);
21502 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
21503 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
21504 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
21505 for highlight in &highlighted_edits.highlights {
21506 assert_eq!(
21507 highlight.1.background_color,
21508 Some(cx.theme().status().created_background)
21509 );
21510 }
21511 },
21512 )
21513 .await;
21514}
21515
21516#[gpui::test]
21517async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
21518 init_test(cx, |_| {});
21519
21520 // Deletion
21521 assert_highlighted_edits(
21522 "Hello, world!",
21523 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
21524 true,
21525 cx,
21526 |highlighted_edits, cx| {
21527 assert_eq!(highlighted_edits.text, "Hello, world!");
21528 assert_eq!(highlighted_edits.highlights.len(), 1);
21529 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
21530 assert_eq!(
21531 highlighted_edits.highlights[0].1.background_color,
21532 Some(cx.theme().status().deleted_background)
21533 );
21534 },
21535 )
21536 .await;
21537
21538 // Insertion
21539 assert_highlighted_edits(
21540 "Hello, world!",
21541 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
21542 true,
21543 cx,
21544 |highlighted_edits, cx| {
21545 assert_eq!(highlighted_edits.highlights.len(), 1);
21546 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
21547 assert_eq!(
21548 highlighted_edits.highlights[0].1.background_color,
21549 Some(cx.theme().status().created_background)
21550 );
21551 },
21552 )
21553 .await;
21554}
21555
21556async fn assert_highlighted_edits(
21557 text: &str,
21558 edits: Vec<(Range<Point>, String)>,
21559 include_deletions: bool,
21560 cx: &mut TestAppContext,
21561 assertion_fn: impl Fn(HighlightedText, &App),
21562) {
21563 let window = cx.add_window(|window, cx| {
21564 let buffer = MultiBuffer::build_simple(text, cx);
21565 Editor::new(EditorMode::full(), buffer, None, window, cx)
21566 });
21567 let cx = &mut VisualTestContext::from_window(*window, cx);
21568
21569 let (buffer, snapshot) = window
21570 .update(cx, |editor, _window, cx| {
21571 (
21572 editor.buffer().clone(),
21573 editor.buffer().read(cx).snapshot(cx),
21574 )
21575 })
21576 .unwrap();
21577
21578 let edits = edits
21579 .into_iter()
21580 .map(|(range, edit)| {
21581 (
21582 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21583 edit,
21584 )
21585 })
21586 .collect::<Vec<_>>();
21587
21588 let text_anchor_edits = edits
21589 .clone()
21590 .into_iter()
21591 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21592 .collect::<Vec<_>>();
21593
21594 let edit_preview = window
21595 .update(cx, |_, _window, cx| {
21596 buffer
21597 .read(cx)
21598 .as_singleton()
21599 .unwrap()
21600 .read(cx)
21601 .preview_edits(text_anchor_edits.into(), cx)
21602 })
21603 .unwrap()
21604 .await;
21605
21606 cx.update(|_window, cx| {
21607 let highlighted_edits = edit_prediction_edit_text(
21608 snapshot.as_singleton().unwrap().2,
21609 &edits,
21610 &edit_preview,
21611 include_deletions,
21612 cx,
21613 );
21614 assertion_fn(highlighted_edits, cx)
21615 });
21616}
21617
21618#[track_caller]
21619fn assert_breakpoint(
21620 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21621 path: &Arc<Path>,
21622 expected: Vec<(u32, Breakpoint)>,
21623) {
21624 if expected.is_empty() {
21625 assert!(!breakpoints.contains_key(path), "{}", path.display());
21626 } else {
21627 let mut breakpoint = breakpoints
21628 .get(path)
21629 .unwrap()
21630 .iter()
21631 .map(|breakpoint| {
21632 (
21633 breakpoint.row,
21634 Breakpoint {
21635 message: breakpoint.message.clone(),
21636 state: breakpoint.state,
21637 condition: breakpoint.condition.clone(),
21638 hit_condition: breakpoint.hit_condition.clone(),
21639 },
21640 )
21641 })
21642 .collect::<Vec<_>>();
21643
21644 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21645
21646 assert_eq!(expected, breakpoint);
21647 }
21648}
21649
21650fn add_log_breakpoint_at_cursor(
21651 editor: &mut Editor,
21652 log_message: &str,
21653 window: &mut Window,
21654 cx: &mut Context<Editor>,
21655) {
21656 let (anchor, bp) = editor
21657 .breakpoints_at_cursors(window, cx)
21658 .first()
21659 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
21660 .unwrap_or_else(|| {
21661 let cursor_position: Point = editor.selections.newest(cx).head();
21662
21663 let breakpoint_position = editor
21664 .snapshot(window, cx)
21665 .display_snapshot
21666 .buffer_snapshot
21667 .anchor_before(Point::new(cursor_position.row, 0));
21668
21669 (breakpoint_position, Breakpoint::new_log(log_message))
21670 });
21671
21672 editor.edit_breakpoint_at_anchor(
21673 anchor,
21674 bp,
21675 BreakpointEditAction::EditLogMessage(log_message.into()),
21676 cx,
21677 );
21678}
21679
21680#[gpui::test]
21681async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
21682 init_test(cx, |_| {});
21683
21684 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21685 let fs = FakeFs::new(cx.executor());
21686 fs.insert_tree(
21687 path!("/a"),
21688 json!({
21689 "main.rs": sample_text,
21690 }),
21691 )
21692 .await;
21693 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21695 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21696
21697 let fs = FakeFs::new(cx.executor());
21698 fs.insert_tree(
21699 path!("/a"),
21700 json!({
21701 "main.rs": sample_text,
21702 }),
21703 )
21704 .await;
21705 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21706 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21707 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21708 let worktree_id = workspace
21709 .update(cx, |workspace, _window, cx| {
21710 workspace.project().update(cx, |project, cx| {
21711 project.worktrees(cx).next().unwrap().read(cx).id()
21712 })
21713 })
21714 .unwrap();
21715
21716 let buffer = project
21717 .update(cx, |project, cx| {
21718 project.open_buffer((worktree_id, "main.rs"), cx)
21719 })
21720 .await
21721 .unwrap();
21722
21723 let (editor, cx) = cx.add_window_view(|window, cx| {
21724 Editor::new(
21725 EditorMode::full(),
21726 MultiBuffer::build_from_buffer(buffer, cx),
21727 Some(project.clone()),
21728 window,
21729 cx,
21730 )
21731 });
21732
21733 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21734 let abs_path = project.read_with(cx, |project, cx| {
21735 project
21736 .absolute_path(&project_path, cx)
21737 .map(Arc::from)
21738 .unwrap()
21739 });
21740
21741 // assert we can add breakpoint on the first line
21742 editor.update_in(cx, |editor, window, cx| {
21743 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21744 editor.move_to_end(&MoveToEnd, window, cx);
21745 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21746 });
21747
21748 let breakpoints = editor.update(cx, |editor, cx| {
21749 editor
21750 .breakpoint_store()
21751 .as_ref()
21752 .unwrap()
21753 .read(cx)
21754 .all_source_breakpoints(cx)
21755 });
21756
21757 assert_eq!(1, breakpoints.len());
21758 assert_breakpoint(
21759 &breakpoints,
21760 &abs_path,
21761 vec![
21762 (0, Breakpoint::new_standard()),
21763 (3, Breakpoint::new_standard()),
21764 ],
21765 );
21766
21767 editor.update_in(cx, |editor, window, cx| {
21768 editor.move_to_beginning(&MoveToBeginning, window, cx);
21769 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21770 });
21771
21772 let breakpoints = editor.update(cx, |editor, cx| {
21773 editor
21774 .breakpoint_store()
21775 .as_ref()
21776 .unwrap()
21777 .read(cx)
21778 .all_source_breakpoints(cx)
21779 });
21780
21781 assert_eq!(1, breakpoints.len());
21782 assert_breakpoint(
21783 &breakpoints,
21784 &abs_path,
21785 vec![(3, Breakpoint::new_standard())],
21786 );
21787
21788 editor.update_in(cx, |editor, window, cx| {
21789 editor.move_to_end(&MoveToEnd, window, cx);
21790 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21791 });
21792
21793 let breakpoints = editor.update(cx, |editor, cx| {
21794 editor
21795 .breakpoint_store()
21796 .as_ref()
21797 .unwrap()
21798 .read(cx)
21799 .all_source_breakpoints(cx)
21800 });
21801
21802 assert_eq!(0, breakpoints.len());
21803 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21804}
21805
21806#[gpui::test]
21807async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
21808 init_test(cx, |_| {});
21809
21810 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21811
21812 let fs = FakeFs::new(cx.executor());
21813 fs.insert_tree(
21814 path!("/a"),
21815 json!({
21816 "main.rs": sample_text,
21817 }),
21818 )
21819 .await;
21820 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21821 let (workspace, cx) =
21822 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21823
21824 let worktree_id = workspace.update(cx, |workspace, cx| {
21825 workspace.project().update(cx, |project, cx| {
21826 project.worktrees(cx).next().unwrap().read(cx).id()
21827 })
21828 });
21829
21830 let buffer = project
21831 .update(cx, |project, cx| {
21832 project.open_buffer((worktree_id, "main.rs"), cx)
21833 })
21834 .await
21835 .unwrap();
21836
21837 let (editor, cx) = cx.add_window_view(|window, cx| {
21838 Editor::new(
21839 EditorMode::full(),
21840 MultiBuffer::build_from_buffer(buffer, cx),
21841 Some(project.clone()),
21842 window,
21843 cx,
21844 )
21845 });
21846
21847 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21848 let abs_path = project.read_with(cx, |project, cx| {
21849 project
21850 .absolute_path(&project_path, cx)
21851 .map(Arc::from)
21852 .unwrap()
21853 });
21854
21855 editor.update_in(cx, |editor, window, cx| {
21856 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21857 });
21858
21859 let breakpoints = editor.update(cx, |editor, cx| {
21860 editor
21861 .breakpoint_store()
21862 .as_ref()
21863 .unwrap()
21864 .read(cx)
21865 .all_source_breakpoints(cx)
21866 });
21867
21868 assert_breakpoint(
21869 &breakpoints,
21870 &abs_path,
21871 vec![(0, Breakpoint::new_log("hello world"))],
21872 );
21873
21874 // Removing a log message from a log breakpoint should remove it
21875 editor.update_in(cx, |editor, window, cx| {
21876 add_log_breakpoint_at_cursor(editor, "", window, cx);
21877 });
21878
21879 let breakpoints = editor.update(cx, |editor, cx| {
21880 editor
21881 .breakpoint_store()
21882 .as_ref()
21883 .unwrap()
21884 .read(cx)
21885 .all_source_breakpoints(cx)
21886 });
21887
21888 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21889
21890 editor.update_in(cx, |editor, window, cx| {
21891 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21892 editor.move_to_end(&MoveToEnd, window, cx);
21893 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21894 // Not adding a log message to a standard breakpoint shouldn't remove it
21895 add_log_breakpoint_at_cursor(editor, "", window, cx);
21896 });
21897
21898 let breakpoints = editor.update(cx, |editor, cx| {
21899 editor
21900 .breakpoint_store()
21901 .as_ref()
21902 .unwrap()
21903 .read(cx)
21904 .all_source_breakpoints(cx)
21905 });
21906
21907 assert_breakpoint(
21908 &breakpoints,
21909 &abs_path,
21910 vec![
21911 (0, Breakpoint::new_standard()),
21912 (3, Breakpoint::new_standard()),
21913 ],
21914 );
21915
21916 editor.update_in(cx, |editor, window, cx| {
21917 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21918 });
21919
21920 let breakpoints = editor.update(cx, |editor, cx| {
21921 editor
21922 .breakpoint_store()
21923 .as_ref()
21924 .unwrap()
21925 .read(cx)
21926 .all_source_breakpoints(cx)
21927 });
21928
21929 assert_breakpoint(
21930 &breakpoints,
21931 &abs_path,
21932 vec![
21933 (0, Breakpoint::new_standard()),
21934 (3, Breakpoint::new_log("hello world")),
21935 ],
21936 );
21937
21938 editor.update_in(cx, |editor, window, cx| {
21939 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21940 });
21941
21942 let breakpoints = editor.update(cx, |editor, cx| {
21943 editor
21944 .breakpoint_store()
21945 .as_ref()
21946 .unwrap()
21947 .read(cx)
21948 .all_source_breakpoints(cx)
21949 });
21950
21951 assert_breakpoint(
21952 &breakpoints,
21953 &abs_path,
21954 vec![
21955 (0, Breakpoint::new_standard()),
21956 (3, Breakpoint::new_log("hello Earth!!")),
21957 ],
21958 );
21959}
21960
21961/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21962/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21963/// or when breakpoints were placed out of order. This tests for a regression too
21964#[gpui::test]
21965async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21966 init_test(cx, |_| {});
21967
21968 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21969 let fs = FakeFs::new(cx.executor());
21970 fs.insert_tree(
21971 path!("/a"),
21972 json!({
21973 "main.rs": sample_text,
21974 }),
21975 )
21976 .await;
21977 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21978 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21979 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21980
21981 let fs = FakeFs::new(cx.executor());
21982 fs.insert_tree(
21983 path!("/a"),
21984 json!({
21985 "main.rs": sample_text,
21986 }),
21987 )
21988 .await;
21989 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21990 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21991 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21992 let worktree_id = workspace
21993 .update(cx, |workspace, _window, cx| {
21994 workspace.project().update(cx, |project, cx| {
21995 project.worktrees(cx).next().unwrap().read(cx).id()
21996 })
21997 })
21998 .unwrap();
21999
22000 let buffer = project
22001 .update(cx, |project, cx| {
22002 project.open_buffer((worktree_id, "main.rs"), cx)
22003 })
22004 .await
22005 .unwrap();
22006
22007 let (editor, cx) = cx.add_window_view(|window, cx| {
22008 Editor::new(
22009 EditorMode::full(),
22010 MultiBuffer::build_from_buffer(buffer, cx),
22011 Some(project.clone()),
22012 window,
22013 cx,
22014 )
22015 });
22016
22017 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22018 let abs_path = project.read_with(cx, |project, cx| {
22019 project
22020 .absolute_path(&project_path, cx)
22021 .map(Arc::from)
22022 .unwrap()
22023 });
22024
22025 // assert we can add breakpoint on the first line
22026 editor.update_in(cx, |editor, window, cx| {
22027 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22028 editor.move_to_end(&MoveToEnd, window, cx);
22029 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22030 editor.move_up(&MoveUp, window, cx);
22031 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22032 });
22033
22034 let breakpoints = editor.update(cx, |editor, cx| {
22035 editor
22036 .breakpoint_store()
22037 .as_ref()
22038 .unwrap()
22039 .read(cx)
22040 .all_source_breakpoints(cx)
22041 });
22042
22043 assert_eq!(1, breakpoints.len());
22044 assert_breakpoint(
22045 &breakpoints,
22046 &abs_path,
22047 vec![
22048 (0, Breakpoint::new_standard()),
22049 (2, Breakpoint::new_standard()),
22050 (3, Breakpoint::new_standard()),
22051 ],
22052 );
22053
22054 editor.update_in(cx, |editor, window, cx| {
22055 editor.move_to_beginning(&MoveToBeginning, window, cx);
22056 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22057 editor.move_to_end(&MoveToEnd, window, cx);
22058 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22059 // Disabling a breakpoint that doesn't exist should do nothing
22060 editor.move_up(&MoveUp, window, cx);
22061 editor.move_up(&MoveUp, window, cx);
22062 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22063 });
22064
22065 let breakpoints = editor.update(cx, |editor, cx| {
22066 editor
22067 .breakpoint_store()
22068 .as_ref()
22069 .unwrap()
22070 .read(cx)
22071 .all_source_breakpoints(cx)
22072 });
22073
22074 let disable_breakpoint = {
22075 let mut bp = Breakpoint::new_standard();
22076 bp.state = BreakpointState::Disabled;
22077 bp
22078 };
22079
22080 assert_eq!(1, breakpoints.len());
22081 assert_breakpoint(
22082 &breakpoints,
22083 &abs_path,
22084 vec![
22085 (0, disable_breakpoint.clone()),
22086 (2, Breakpoint::new_standard()),
22087 (3, disable_breakpoint.clone()),
22088 ],
22089 );
22090
22091 editor.update_in(cx, |editor, window, cx| {
22092 editor.move_to_beginning(&MoveToBeginning, window, cx);
22093 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22094 editor.move_to_end(&MoveToEnd, window, cx);
22095 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22096 editor.move_up(&MoveUp, window, cx);
22097 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22098 });
22099
22100 let breakpoints = editor.update(cx, |editor, cx| {
22101 editor
22102 .breakpoint_store()
22103 .as_ref()
22104 .unwrap()
22105 .read(cx)
22106 .all_source_breakpoints(cx)
22107 });
22108
22109 assert_eq!(1, breakpoints.len());
22110 assert_breakpoint(
22111 &breakpoints,
22112 &abs_path,
22113 vec![
22114 (0, Breakpoint::new_standard()),
22115 (2, disable_breakpoint),
22116 (3, Breakpoint::new_standard()),
22117 ],
22118 );
22119}
22120
22121#[gpui::test]
22122async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22123 init_test(cx, |_| {});
22124 let capabilities = lsp::ServerCapabilities {
22125 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22126 prepare_provider: Some(true),
22127 work_done_progress_options: Default::default(),
22128 })),
22129 ..Default::default()
22130 };
22131 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22132
22133 cx.set_state(indoc! {"
22134 struct Fˇoo {}
22135 "});
22136
22137 cx.update_editor(|editor, _, cx| {
22138 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22139 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22140 editor.highlight_background::<DocumentHighlightRead>(
22141 &[highlight_range],
22142 |theme| theme.colors().editor_document_highlight_read_background,
22143 cx,
22144 );
22145 });
22146
22147 let mut prepare_rename_handler = cx
22148 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22149 move |_, _, _| async move {
22150 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22151 start: lsp::Position {
22152 line: 0,
22153 character: 7,
22154 },
22155 end: lsp::Position {
22156 line: 0,
22157 character: 10,
22158 },
22159 })))
22160 },
22161 );
22162 let prepare_rename_task = cx
22163 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22164 .expect("Prepare rename was not started");
22165 prepare_rename_handler.next().await.unwrap();
22166 prepare_rename_task.await.expect("Prepare rename failed");
22167
22168 let mut rename_handler =
22169 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22170 let edit = lsp::TextEdit {
22171 range: lsp::Range {
22172 start: lsp::Position {
22173 line: 0,
22174 character: 7,
22175 },
22176 end: lsp::Position {
22177 line: 0,
22178 character: 10,
22179 },
22180 },
22181 new_text: "FooRenamed".to_string(),
22182 };
22183 Ok(Some(lsp::WorkspaceEdit::new(
22184 // Specify the same edit twice
22185 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22186 )))
22187 });
22188 let rename_task = cx
22189 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22190 .expect("Confirm rename was not started");
22191 rename_handler.next().await.unwrap();
22192 rename_task.await.expect("Confirm rename failed");
22193 cx.run_until_parked();
22194
22195 // Despite two edits, only one is actually applied as those are identical
22196 cx.assert_editor_state(indoc! {"
22197 struct FooRenamedˇ {}
22198 "});
22199}
22200
22201#[gpui::test]
22202async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22203 init_test(cx, |_| {});
22204 // These capabilities indicate that the server does not support prepare rename.
22205 let capabilities = lsp::ServerCapabilities {
22206 rename_provider: Some(lsp::OneOf::Left(true)),
22207 ..Default::default()
22208 };
22209 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22210
22211 cx.set_state(indoc! {"
22212 struct Fˇoo {}
22213 "});
22214
22215 cx.update_editor(|editor, _window, cx| {
22216 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22217 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22218 editor.highlight_background::<DocumentHighlightRead>(
22219 &[highlight_range],
22220 |theme| theme.colors().editor_document_highlight_read_background,
22221 cx,
22222 );
22223 });
22224
22225 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22226 .expect("Prepare rename was not started")
22227 .await
22228 .expect("Prepare rename failed");
22229
22230 let mut rename_handler =
22231 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22232 let edit = lsp::TextEdit {
22233 range: lsp::Range {
22234 start: lsp::Position {
22235 line: 0,
22236 character: 7,
22237 },
22238 end: lsp::Position {
22239 line: 0,
22240 character: 10,
22241 },
22242 },
22243 new_text: "FooRenamed".to_string(),
22244 };
22245 Ok(Some(lsp::WorkspaceEdit::new(
22246 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
22247 )))
22248 });
22249 let rename_task = cx
22250 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22251 .expect("Confirm rename was not started");
22252 rename_handler.next().await.unwrap();
22253 rename_task.await.expect("Confirm rename failed");
22254 cx.run_until_parked();
22255
22256 // Correct range is renamed, as `surrounding_word` is used to find it.
22257 cx.assert_editor_state(indoc! {"
22258 struct FooRenamedˇ {}
22259 "});
22260}
22261
22262#[gpui::test]
22263async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
22264 init_test(cx, |_| {});
22265 let mut cx = EditorTestContext::new(cx).await;
22266
22267 let language = Arc::new(
22268 Language::new(
22269 LanguageConfig::default(),
22270 Some(tree_sitter_html::LANGUAGE.into()),
22271 )
22272 .with_brackets_query(
22273 r#"
22274 ("<" @open "/>" @close)
22275 ("</" @open ">" @close)
22276 ("<" @open ">" @close)
22277 ("\"" @open "\"" @close)
22278 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
22279 "#,
22280 )
22281 .unwrap(),
22282 );
22283 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22284
22285 cx.set_state(indoc! {"
22286 <span>ˇ</span>
22287 "});
22288 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22289 cx.assert_editor_state(indoc! {"
22290 <span>
22291 ˇ
22292 </span>
22293 "});
22294
22295 cx.set_state(indoc! {"
22296 <span><span></span>ˇ</span>
22297 "});
22298 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22299 cx.assert_editor_state(indoc! {"
22300 <span><span></span>
22301 ˇ</span>
22302 "});
22303
22304 cx.set_state(indoc! {"
22305 <span>ˇ
22306 </span>
22307 "});
22308 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22309 cx.assert_editor_state(indoc! {"
22310 <span>
22311 ˇ
22312 </span>
22313 "});
22314}
22315
22316#[gpui::test(iterations = 10)]
22317async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
22318 init_test(cx, |_| {});
22319
22320 let fs = FakeFs::new(cx.executor());
22321 fs.insert_tree(
22322 path!("/dir"),
22323 json!({
22324 "a.ts": "a",
22325 }),
22326 )
22327 .await;
22328
22329 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
22330 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22331 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22332
22333 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22334 language_registry.add(Arc::new(Language::new(
22335 LanguageConfig {
22336 name: "TypeScript".into(),
22337 matcher: LanguageMatcher {
22338 path_suffixes: vec!["ts".to_string()],
22339 ..Default::default()
22340 },
22341 ..Default::default()
22342 },
22343 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
22344 )));
22345 let mut fake_language_servers = language_registry.register_fake_lsp(
22346 "TypeScript",
22347 FakeLspAdapter {
22348 capabilities: lsp::ServerCapabilities {
22349 code_lens_provider: Some(lsp::CodeLensOptions {
22350 resolve_provider: Some(true),
22351 }),
22352 execute_command_provider: Some(lsp::ExecuteCommandOptions {
22353 commands: vec!["_the/command".to_string()],
22354 ..lsp::ExecuteCommandOptions::default()
22355 }),
22356 ..lsp::ServerCapabilities::default()
22357 },
22358 ..FakeLspAdapter::default()
22359 },
22360 );
22361
22362 let editor = workspace
22363 .update(cx, |workspace, window, cx| {
22364 workspace.open_abs_path(
22365 PathBuf::from(path!("/dir/a.ts")),
22366 OpenOptions::default(),
22367 window,
22368 cx,
22369 )
22370 })
22371 .unwrap()
22372 .await
22373 .unwrap()
22374 .downcast::<Editor>()
22375 .unwrap();
22376 cx.executor().run_until_parked();
22377
22378 let fake_server = fake_language_servers.next().await.unwrap();
22379
22380 let buffer = editor.update(cx, |editor, cx| {
22381 editor
22382 .buffer()
22383 .read(cx)
22384 .as_singleton()
22385 .expect("have opened a single file by path")
22386 });
22387
22388 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
22389 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
22390 drop(buffer_snapshot);
22391 let actions = cx
22392 .update_window(*workspace, |_, window, cx| {
22393 project.code_actions(&buffer, anchor..anchor, window, cx)
22394 })
22395 .unwrap();
22396
22397 fake_server
22398 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22399 Ok(Some(vec![
22400 lsp::CodeLens {
22401 range: lsp::Range::default(),
22402 command: Some(lsp::Command {
22403 title: "Code lens command".to_owned(),
22404 command: "_the/command".to_owned(),
22405 arguments: None,
22406 }),
22407 data: None,
22408 },
22409 lsp::CodeLens {
22410 range: lsp::Range::default(),
22411 command: Some(lsp::Command {
22412 title: "Command not in capabilities".to_owned(),
22413 command: "not in capabilities".to_owned(),
22414 arguments: None,
22415 }),
22416 data: None,
22417 },
22418 lsp::CodeLens {
22419 range: lsp::Range {
22420 start: lsp::Position {
22421 line: 1,
22422 character: 1,
22423 },
22424 end: lsp::Position {
22425 line: 1,
22426 character: 1,
22427 },
22428 },
22429 command: Some(lsp::Command {
22430 title: "Command not in range".to_owned(),
22431 command: "_the/command".to_owned(),
22432 arguments: None,
22433 }),
22434 data: None,
22435 },
22436 ]))
22437 })
22438 .next()
22439 .await;
22440
22441 let actions = actions.await.unwrap();
22442 assert_eq!(
22443 actions.len(),
22444 1,
22445 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
22446 );
22447 let action = actions[0].clone();
22448 let apply = project.update(cx, |project, cx| {
22449 project.apply_code_action(buffer.clone(), action, true, cx)
22450 });
22451
22452 // Resolving the code action does not populate its edits. In absence of
22453 // edits, we must execute the given command.
22454 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
22455 |mut lens, _| async move {
22456 let lens_command = lens.command.as_mut().expect("should have a command");
22457 assert_eq!(lens_command.title, "Code lens command");
22458 lens_command.arguments = Some(vec![json!("the-argument")]);
22459 Ok(lens)
22460 },
22461 );
22462
22463 // While executing the command, the language server sends the editor
22464 // a `workspaceEdit` request.
22465 fake_server
22466 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
22467 let fake = fake_server.clone();
22468 move |params, _| {
22469 assert_eq!(params.command, "_the/command");
22470 let fake = fake.clone();
22471 async move {
22472 fake.server
22473 .request::<lsp::request::ApplyWorkspaceEdit>(
22474 lsp::ApplyWorkspaceEditParams {
22475 label: None,
22476 edit: lsp::WorkspaceEdit {
22477 changes: Some(
22478 [(
22479 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
22480 vec![lsp::TextEdit {
22481 range: lsp::Range::new(
22482 lsp::Position::new(0, 0),
22483 lsp::Position::new(0, 0),
22484 ),
22485 new_text: "X".into(),
22486 }],
22487 )]
22488 .into_iter()
22489 .collect(),
22490 ),
22491 ..lsp::WorkspaceEdit::default()
22492 },
22493 },
22494 )
22495 .await
22496 .into_response()
22497 .unwrap();
22498 Ok(Some(json!(null)))
22499 }
22500 }
22501 })
22502 .next()
22503 .await;
22504
22505 // Applying the code lens command returns a project transaction containing the edits
22506 // sent by the language server in its `workspaceEdit` request.
22507 let transaction = apply.await.unwrap();
22508 assert!(transaction.0.contains_key(&buffer));
22509 buffer.update(cx, |buffer, cx| {
22510 assert_eq!(buffer.text(), "Xa");
22511 buffer.undo(cx);
22512 assert_eq!(buffer.text(), "a");
22513 });
22514
22515 let actions_after_edits = cx
22516 .update_window(*workspace, |_, window, cx| {
22517 project.code_actions(&buffer, anchor..anchor, window, cx)
22518 })
22519 .unwrap()
22520 .await
22521 .unwrap();
22522 assert_eq!(
22523 actions, actions_after_edits,
22524 "For the same selection, same code lens actions should be returned"
22525 );
22526
22527 let _responses =
22528 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22529 panic!("No more code lens requests are expected");
22530 });
22531 editor.update_in(cx, |editor, window, cx| {
22532 editor.select_all(&SelectAll, window, cx);
22533 });
22534 cx.executor().run_until_parked();
22535 let new_actions = cx
22536 .update_window(*workspace, |_, window, cx| {
22537 project.code_actions(&buffer, anchor..anchor, window, cx)
22538 })
22539 .unwrap()
22540 .await
22541 .unwrap();
22542 assert_eq!(
22543 actions, new_actions,
22544 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
22545 );
22546}
22547
22548#[gpui::test]
22549async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
22550 init_test(cx, |_| {});
22551
22552 let fs = FakeFs::new(cx.executor());
22553 let main_text = r#"fn main() {
22554println!("1");
22555println!("2");
22556println!("3");
22557println!("4");
22558println!("5");
22559}"#;
22560 let lib_text = "mod foo {}";
22561 fs.insert_tree(
22562 path!("/a"),
22563 json!({
22564 "lib.rs": lib_text,
22565 "main.rs": main_text,
22566 }),
22567 )
22568 .await;
22569
22570 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22571 let (workspace, cx) =
22572 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22573 let worktree_id = workspace.update(cx, |workspace, cx| {
22574 workspace.project().update(cx, |project, cx| {
22575 project.worktrees(cx).next().unwrap().read(cx).id()
22576 })
22577 });
22578
22579 let expected_ranges = vec![
22580 Point::new(0, 0)..Point::new(0, 0),
22581 Point::new(1, 0)..Point::new(1, 1),
22582 Point::new(2, 0)..Point::new(2, 2),
22583 Point::new(3, 0)..Point::new(3, 3),
22584 ];
22585
22586 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22587 let editor_1 = workspace
22588 .update_in(cx, |workspace, window, cx| {
22589 workspace.open_path(
22590 (worktree_id, "main.rs"),
22591 Some(pane_1.downgrade()),
22592 true,
22593 window,
22594 cx,
22595 )
22596 })
22597 .unwrap()
22598 .await
22599 .downcast::<Editor>()
22600 .unwrap();
22601 pane_1.update(cx, |pane, cx| {
22602 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22603 open_editor.update(cx, |editor, cx| {
22604 assert_eq!(
22605 editor.display_text(cx),
22606 main_text,
22607 "Original main.rs text on initial open",
22608 );
22609 assert_eq!(
22610 editor
22611 .selections
22612 .all::<Point>(cx)
22613 .into_iter()
22614 .map(|s| s.range())
22615 .collect::<Vec<_>>(),
22616 vec![Point::zero()..Point::zero()],
22617 "Default selections on initial open",
22618 );
22619 })
22620 });
22621 editor_1.update_in(cx, |editor, window, cx| {
22622 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22623 s.select_ranges(expected_ranges.clone());
22624 });
22625 });
22626
22627 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22628 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22629 });
22630 let editor_2 = workspace
22631 .update_in(cx, |workspace, window, cx| {
22632 workspace.open_path(
22633 (worktree_id, "main.rs"),
22634 Some(pane_2.downgrade()),
22635 true,
22636 window,
22637 cx,
22638 )
22639 })
22640 .unwrap()
22641 .await
22642 .downcast::<Editor>()
22643 .unwrap();
22644 pane_2.update(cx, |pane, cx| {
22645 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22646 open_editor.update(cx, |editor, cx| {
22647 assert_eq!(
22648 editor.display_text(cx),
22649 main_text,
22650 "Original main.rs text on initial open in another panel",
22651 );
22652 assert_eq!(
22653 editor
22654 .selections
22655 .all::<Point>(cx)
22656 .into_iter()
22657 .map(|s| s.range())
22658 .collect::<Vec<_>>(),
22659 vec![Point::zero()..Point::zero()],
22660 "Default selections on initial open in another panel",
22661 );
22662 })
22663 });
22664
22665 editor_2.update_in(cx, |editor, window, cx| {
22666 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
22667 });
22668
22669 let _other_editor_1 = workspace
22670 .update_in(cx, |workspace, window, cx| {
22671 workspace.open_path(
22672 (worktree_id, "lib.rs"),
22673 Some(pane_1.downgrade()),
22674 true,
22675 window,
22676 cx,
22677 )
22678 })
22679 .unwrap()
22680 .await
22681 .downcast::<Editor>()
22682 .unwrap();
22683 pane_1
22684 .update_in(cx, |pane, window, cx| {
22685 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22686 })
22687 .await
22688 .unwrap();
22689 drop(editor_1);
22690 pane_1.update(cx, |pane, cx| {
22691 pane.active_item()
22692 .unwrap()
22693 .downcast::<Editor>()
22694 .unwrap()
22695 .update(cx, |editor, cx| {
22696 assert_eq!(
22697 editor.display_text(cx),
22698 lib_text,
22699 "Other file should be open and active",
22700 );
22701 });
22702 assert_eq!(pane.items().count(), 1, "No other editors should be open");
22703 });
22704
22705 let _other_editor_2 = workspace
22706 .update_in(cx, |workspace, window, cx| {
22707 workspace.open_path(
22708 (worktree_id, "lib.rs"),
22709 Some(pane_2.downgrade()),
22710 true,
22711 window,
22712 cx,
22713 )
22714 })
22715 .unwrap()
22716 .await
22717 .downcast::<Editor>()
22718 .unwrap();
22719 pane_2
22720 .update_in(cx, |pane, window, cx| {
22721 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22722 })
22723 .await
22724 .unwrap();
22725 drop(editor_2);
22726 pane_2.update(cx, |pane, cx| {
22727 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22728 open_editor.update(cx, |editor, cx| {
22729 assert_eq!(
22730 editor.display_text(cx),
22731 lib_text,
22732 "Other file should be open and active in another panel too",
22733 );
22734 });
22735 assert_eq!(
22736 pane.items().count(),
22737 1,
22738 "No other editors should be open in another pane",
22739 );
22740 });
22741
22742 let _editor_1_reopened = workspace
22743 .update_in(cx, |workspace, window, cx| {
22744 workspace.open_path(
22745 (worktree_id, "main.rs"),
22746 Some(pane_1.downgrade()),
22747 true,
22748 window,
22749 cx,
22750 )
22751 })
22752 .unwrap()
22753 .await
22754 .downcast::<Editor>()
22755 .unwrap();
22756 let _editor_2_reopened = workspace
22757 .update_in(cx, |workspace, window, cx| {
22758 workspace.open_path(
22759 (worktree_id, "main.rs"),
22760 Some(pane_2.downgrade()),
22761 true,
22762 window,
22763 cx,
22764 )
22765 })
22766 .unwrap()
22767 .await
22768 .downcast::<Editor>()
22769 .unwrap();
22770 pane_1.update(cx, |pane, cx| {
22771 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22772 open_editor.update(cx, |editor, cx| {
22773 assert_eq!(
22774 editor.display_text(cx),
22775 main_text,
22776 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
22777 );
22778 assert_eq!(
22779 editor
22780 .selections
22781 .all::<Point>(cx)
22782 .into_iter()
22783 .map(|s| s.range())
22784 .collect::<Vec<_>>(),
22785 expected_ranges,
22786 "Previous editor in the 1st panel had selections and should get them restored on reopen",
22787 );
22788 })
22789 });
22790 pane_2.update(cx, |pane, cx| {
22791 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22792 open_editor.update(cx, |editor, cx| {
22793 assert_eq!(
22794 editor.display_text(cx),
22795 r#"fn main() {
22796⋯rintln!("1");
22797⋯intln!("2");
22798⋯ntln!("3");
22799println!("4");
22800println!("5");
22801}"#,
22802 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
22803 );
22804 assert_eq!(
22805 editor
22806 .selections
22807 .all::<Point>(cx)
22808 .into_iter()
22809 .map(|s| s.range())
22810 .collect::<Vec<_>>(),
22811 vec![Point::zero()..Point::zero()],
22812 "Previous editor in the 2nd pane had no selections changed hence should restore none",
22813 );
22814 })
22815 });
22816}
22817
22818#[gpui::test]
22819async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
22820 init_test(cx, |_| {});
22821
22822 let fs = FakeFs::new(cx.executor());
22823 let main_text = r#"fn main() {
22824println!("1");
22825println!("2");
22826println!("3");
22827println!("4");
22828println!("5");
22829}"#;
22830 let lib_text = "mod foo {}";
22831 fs.insert_tree(
22832 path!("/a"),
22833 json!({
22834 "lib.rs": lib_text,
22835 "main.rs": main_text,
22836 }),
22837 )
22838 .await;
22839
22840 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22841 let (workspace, cx) =
22842 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22843 let worktree_id = workspace.update(cx, |workspace, cx| {
22844 workspace.project().update(cx, |project, cx| {
22845 project.worktrees(cx).next().unwrap().read(cx).id()
22846 })
22847 });
22848
22849 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22850 let editor = workspace
22851 .update_in(cx, |workspace, window, cx| {
22852 workspace.open_path(
22853 (worktree_id, "main.rs"),
22854 Some(pane.downgrade()),
22855 true,
22856 window,
22857 cx,
22858 )
22859 })
22860 .unwrap()
22861 .await
22862 .downcast::<Editor>()
22863 .unwrap();
22864 pane.update(cx, |pane, cx| {
22865 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22866 open_editor.update(cx, |editor, cx| {
22867 assert_eq!(
22868 editor.display_text(cx),
22869 main_text,
22870 "Original main.rs text on initial open",
22871 );
22872 })
22873 });
22874 editor.update_in(cx, |editor, window, cx| {
22875 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22876 });
22877
22878 cx.update_global(|store: &mut SettingsStore, cx| {
22879 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22880 s.restore_on_file_reopen = Some(false);
22881 });
22882 });
22883 editor.update_in(cx, |editor, window, cx| {
22884 editor.fold_ranges(
22885 vec![
22886 Point::new(1, 0)..Point::new(1, 1),
22887 Point::new(2, 0)..Point::new(2, 2),
22888 Point::new(3, 0)..Point::new(3, 3),
22889 ],
22890 false,
22891 window,
22892 cx,
22893 );
22894 });
22895 pane.update_in(cx, |pane, window, cx| {
22896 pane.close_all_items(&CloseAllItems::default(), window, cx)
22897 })
22898 .await
22899 .unwrap();
22900 pane.update(cx, |pane, _| {
22901 assert!(pane.active_item().is_none());
22902 });
22903 cx.update_global(|store: &mut SettingsStore, cx| {
22904 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22905 s.restore_on_file_reopen = Some(true);
22906 });
22907 });
22908
22909 let _editor_reopened = workspace
22910 .update_in(cx, |workspace, window, cx| {
22911 workspace.open_path(
22912 (worktree_id, "main.rs"),
22913 Some(pane.downgrade()),
22914 true,
22915 window,
22916 cx,
22917 )
22918 })
22919 .unwrap()
22920 .await
22921 .downcast::<Editor>()
22922 .unwrap();
22923 pane.update(cx, |pane, cx| {
22924 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22925 open_editor.update(cx, |editor, cx| {
22926 assert_eq!(
22927 editor.display_text(cx),
22928 main_text,
22929 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22930 );
22931 })
22932 });
22933}
22934
22935#[gpui::test]
22936async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22937 struct EmptyModalView {
22938 focus_handle: gpui::FocusHandle,
22939 }
22940 impl EventEmitter<DismissEvent> for EmptyModalView {}
22941 impl Render for EmptyModalView {
22942 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22943 div()
22944 }
22945 }
22946 impl Focusable for EmptyModalView {
22947 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22948 self.focus_handle.clone()
22949 }
22950 }
22951 impl workspace::ModalView for EmptyModalView {}
22952 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22953 EmptyModalView {
22954 focus_handle: cx.focus_handle(),
22955 }
22956 }
22957
22958 init_test(cx, |_| {});
22959
22960 let fs = FakeFs::new(cx.executor());
22961 let project = Project::test(fs, [], cx).await;
22962 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22963 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22964 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22965 let editor = cx.new_window_entity(|window, cx| {
22966 Editor::new(
22967 EditorMode::full(),
22968 buffer,
22969 Some(project.clone()),
22970 window,
22971 cx,
22972 )
22973 });
22974 workspace
22975 .update(cx, |workspace, window, cx| {
22976 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22977 })
22978 .unwrap();
22979 editor.update_in(cx, |editor, window, cx| {
22980 editor.open_context_menu(&OpenContextMenu, window, cx);
22981 assert!(editor.mouse_context_menu.is_some());
22982 });
22983 workspace
22984 .update(cx, |workspace, window, cx| {
22985 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22986 })
22987 .unwrap();
22988 cx.read(|cx| {
22989 assert!(editor.read(cx).mouse_context_menu.is_none());
22990 });
22991}
22992
22993#[gpui::test]
22994async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22995 init_test(cx, |_| {});
22996
22997 let fs = FakeFs::new(cx.executor());
22998 fs.insert_file(path!("/file.html"), Default::default())
22999 .await;
23000
23001 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23002
23003 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23004 let html_language = Arc::new(Language::new(
23005 LanguageConfig {
23006 name: "HTML".into(),
23007 matcher: LanguageMatcher {
23008 path_suffixes: vec!["html".to_string()],
23009 ..LanguageMatcher::default()
23010 },
23011 brackets: BracketPairConfig {
23012 pairs: vec![BracketPair {
23013 start: "<".into(),
23014 end: ">".into(),
23015 close: true,
23016 ..Default::default()
23017 }],
23018 ..Default::default()
23019 },
23020 ..Default::default()
23021 },
23022 Some(tree_sitter_html::LANGUAGE.into()),
23023 ));
23024 language_registry.add(html_language);
23025 let mut fake_servers = language_registry.register_fake_lsp(
23026 "HTML",
23027 FakeLspAdapter {
23028 capabilities: lsp::ServerCapabilities {
23029 completion_provider: Some(lsp::CompletionOptions {
23030 resolve_provider: Some(true),
23031 ..Default::default()
23032 }),
23033 ..Default::default()
23034 },
23035 ..Default::default()
23036 },
23037 );
23038
23039 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23040 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23041
23042 let worktree_id = workspace
23043 .update(cx, |workspace, _window, cx| {
23044 workspace.project().update(cx, |project, cx| {
23045 project.worktrees(cx).next().unwrap().read(cx).id()
23046 })
23047 })
23048 .unwrap();
23049 project
23050 .update(cx, |project, cx| {
23051 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23052 })
23053 .await
23054 .unwrap();
23055 let editor = workspace
23056 .update(cx, |workspace, window, cx| {
23057 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23058 })
23059 .unwrap()
23060 .await
23061 .unwrap()
23062 .downcast::<Editor>()
23063 .unwrap();
23064
23065 let fake_server = fake_servers.next().await.unwrap();
23066 editor.update_in(cx, |editor, window, cx| {
23067 editor.set_text("<ad></ad>", window, cx);
23068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23069 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23070 });
23071 let Some((buffer, _)) = editor
23072 .buffer
23073 .read(cx)
23074 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23075 else {
23076 panic!("Failed to get buffer for selection position");
23077 };
23078 let buffer = buffer.read(cx);
23079 let buffer_id = buffer.remote_id();
23080 let opening_range =
23081 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23082 let closing_range =
23083 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23084 let mut linked_ranges = HashMap::default();
23085 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23086 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23087 });
23088 let mut completion_handle =
23089 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23090 Ok(Some(lsp::CompletionResponse::Array(vec![
23091 lsp::CompletionItem {
23092 label: "head".to_string(),
23093 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23094 lsp::InsertReplaceEdit {
23095 new_text: "head".to_string(),
23096 insert: lsp::Range::new(
23097 lsp::Position::new(0, 1),
23098 lsp::Position::new(0, 3),
23099 ),
23100 replace: lsp::Range::new(
23101 lsp::Position::new(0, 1),
23102 lsp::Position::new(0, 3),
23103 ),
23104 },
23105 )),
23106 ..Default::default()
23107 },
23108 ])))
23109 });
23110 editor.update_in(cx, |editor, window, cx| {
23111 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23112 });
23113 cx.run_until_parked();
23114 completion_handle.next().await.unwrap();
23115 editor.update(cx, |editor, _| {
23116 assert!(
23117 editor.context_menu_visible(),
23118 "Completion menu should be visible"
23119 );
23120 });
23121 editor.update_in(cx, |editor, window, cx| {
23122 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23123 });
23124 cx.executor().run_until_parked();
23125 editor.update(cx, |editor, cx| {
23126 assert_eq!(editor.text(cx), "<head></head>");
23127 });
23128}
23129
23130#[gpui::test]
23131async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23132 init_test(cx, |_| {});
23133
23134 let fs = FakeFs::new(cx.executor());
23135 fs.insert_tree(
23136 path!("/root"),
23137 json!({
23138 "a": {
23139 "main.rs": "fn main() {}",
23140 },
23141 "foo": {
23142 "bar": {
23143 "external_file.rs": "pub mod external {}",
23144 }
23145 }
23146 }),
23147 )
23148 .await;
23149
23150 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23151 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23152 language_registry.add(rust_lang());
23153 let _fake_servers = language_registry.register_fake_lsp(
23154 "Rust",
23155 FakeLspAdapter {
23156 ..FakeLspAdapter::default()
23157 },
23158 );
23159 let (workspace, cx) =
23160 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23161 let worktree_id = workspace.update(cx, |workspace, cx| {
23162 workspace.project().update(cx, |project, cx| {
23163 project.worktrees(cx).next().unwrap().read(cx).id()
23164 })
23165 });
23166
23167 let assert_language_servers_count =
23168 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23169 project.update(cx, |project, cx| {
23170 let current = project
23171 .lsp_store()
23172 .read(cx)
23173 .as_local()
23174 .unwrap()
23175 .language_servers
23176 .len();
23177 assert_eq!(expected, current, "{context}");
23178 });
23179 };
23180
23181 assert_language_servers_count(
23182 0,
23183 "No servers should be running before any file is open",
23184 cx,
23185 );
23186 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23187 let main_editor = workspace
23188 .update_in(cx, |workspace, window, cx| {
23189 workspace.open_path(
23190 (worktree_id, "main.rs"),
23191 Some(pane.downgrade()),
23192 true,
23193 window,
23194 cx,
23195 )
23196 })
23197 .unwrap()
23198 .await
23199 .downcast::<Editor>()
23200 .unwrap();
23201 pane.update(cx, |pane, cx| {
23202 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23203 open_editor.update(cx, |editor, cx| {
23204 assert_eq!(
23205 editor.display_text(cx),
23206 "fn main() {}",
23207 "Original main.rs text on initial open",
23208 );
23209 });
23210 assert_eq!(open_editor, main_editor);
23211 });
23212 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
23213
23214 let external_editor = workspace
23215 .update_in(cx, |workspace, window, cx| {
23216 workspace.open_abs_path(
23217 PathBuf::from("/root/foo/bar/external_file.rs"),
23218 OpenOptions::default(),
23219 window,
23220 cx,
23221 )
23222 })
23223 .await
23224 .expect("opening external file")
23225 .downcast::<Editor>()
23226 .expect("downcasted external file's open element to editor");
23227 pane.update(cx, |pane, cx| {
23228 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23229 open_editor.update(cx, |editor, cx| {
23230 assert_eq!(
23231 editor.display_text(cx),
23232 "pub mod external {}",
23233 "External file is open now",
23234 );
23235 });
23236 assert_eq!(open_editor, external_editor);
23237 });
23238 assert_language_servers_count(
23239 1,
23240 "Second, external, *.rs file should join the existing server",
23241 cx,
23242 );
23243
23244 pane.update_in(cx, |pane, window, cx| {
23245 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23246 })
23247 .await
23248 .unwrap();
23249 pane.update_in(cx, |pane, window, cx| {
23250 pane.navigate_backward(&Default::default(), window, cx);
23251 });
23252 cx.run_until_parked();
23253 pane.update(cx, |pane, cx| {
23254 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23255 open_editor.update(cx, |editor, cx| {
23256 assert_eq!(
23257 editor.display_text(cx),
23258 "pub mod external {}",
23259 "External file is open now",
23260 );
23261 });
23262 });
23263 assert_language_servers_count(
23264 1,
23265 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
23266 cx,
23267 );
23268
23269 cx.update(|_, cx| {
23270 workspace::reload(cx);
23271 });
23272 assert_language_servers_count(
23273 1,
23274 "After reloading the worktree with local and external files opened, only one project should be started",
23275 cx,
23276 );
23277}
23278
23279#[gpui::test]
23280async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
23281 init_test(cx, |_| {});
23282
23283 let mut cx = EditorTestContext::new(cx).await;
23284 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23285 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23286
23287 // test cursor move to start of each line on tab
23288 // for `if`, `elif`, `else`, `while`, `with` and `for`
23289 cx.set_state(indoc! {"
23290 def main():
23291 ˇ for item in items:
23292 ˇ while item.active:
23293 ˇ if item.value > 10:
23294 ˇ continue
23295 ˇ elif item.value < 0:
23296 ˇ break
23297 ˇ else:
23298 ˇ with item.context() as ctx:
23299 ˇ yield count
23300 ˇ else:
23301 ˇ log('while else')
23302 ˇ else:
23303 ˇ log('for else')
23304 "});
23305 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23306 cx.assert_editor_state(indoc! {"
23307 def main():
23308 ˇfor item in items:
23309 ˇwhile item.active:
23310 ˇif item.value > 10:
23311 ˇcontinue
23312 ˇelif item.value < 0:
23313 ˇbreak
23314 ˇelse:
23315 ˇwith item.context() as ctx:
23316 ˇyield count
23317 ˇelse:
23318 ˇlog('while else')
23319 ˇelse:
23320 ˇlog('for else')
23321 "});
23322 // test relative indent is preserved when tab
23323 // for `if`, `elif`, `else`, `while`, `with` and `for`
23324 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23325 cx.assert_editor_state(indoc! {"
23326 def main():
23327 ˇfor item in items:
23328 ˇwhile item.active:
23329 ˇif item.value > 10:
23330 ˇcontinue
23331 ˇelif item.value < 0:
23332 ˇbreak
23333 ˇelse:
23334 ˇwith item.context() as ctx:
23335 ˇyield count
23336 ˇelse:
23337 ˇlog('while else')
23338 ˇelse:
23339 ˇlog('for else')
23340 "});
23341
23342 // test cursor move to start of each line on tab
23343 // for `try`, `except`, `else`, `finally`, `match` and `def`
23344 cx.set_state(indoc! {"
23345 def main():
23346 ˇ try:
23347 ˇ fetch()
23348 ˇ except ValueError:
23349 ˇ handle_error()
23350 ˇ else:
23351 ˇ match value:
23352 ˇ case _:
23353 ˇ finally:
23354 ˇ def status():
23355 ˇ return 0
23356 "});
23357 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23358 cx.assert_editor_state(indoc! {"
23359 def main():
23360 ˇtry:
23361 ˇfetch()
23362 ˇexcept ValueError:
23363 ˇhandle_error()
23364 ˇelse:
23365 ˇmatch value:
23366 ˇcase _:
23367 ˇfinally:
23368 ˇdef status():
23369 ˇreturn 0
23370 "});
23371 // test relative indent is preserved when tab
23372 // for `try`, `except`, `else`, `finally`, `match` and `def`
23373 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23374 cx.assert_editor_state(indoc! {"
23375 def main():
23376 ˇtry:
23377 ˇfetch()
23378 ˇexcept ValueError:
23379 ˇhandle_error()
23380 ˇelse:
23381 ˇmatch value:
23382 ˇcase _:
23383 ˇfinally:
23384 ˇdef status():
23385 ˇreturn 0
23386 "});
23387}
23388
23389#[gpui::test]
23390async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
23391 init_test(cx, |_| {});
23392
23393 let mut cx = EditorTestContext::new(cx).await;
23394 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23395 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23396
23397 // test `else` auto outdents when typed inside `if` block
23398 cx.set_state(indoc! {"
23399 def main():
23400 if i == 2:
23401 return
23402 ˇ
23403 "});
23404 cx.update_editor(|editor, window, cx| {
23405 editor.handle_input("else:", window, cx);
23406 });
23407 cx.assert_editor_state(indoc! {"
23408 def main():
23409 if i == 2:
23410 return
23411 else:ˇ
23412 "});
23413
23414 // test `except` auto outdents when typed inside `try` block
23415 cx.set_state(indoc! {"
23416 def main():
23417 try:
23418 i = 2
23419 ˇ
23420 "});
23421 cx.update_editor(|editor, window, cx| {
23422 editor.handle_input("except:", window, cx);
23423 });
23424 cx.assert_editor_state(indoc! {"
23425 def main():
23426 try:
23427 i = 2
23428 except:ˇ
23429 "});
23430
23431 // test `else` auto outdents when typed inside `except` block
23432 cx.set_state(indoc! {"
23433 def main():
23434 try:
23435 i = 2
23436 except:
23437 j = 2
23438 ˇ
23439 "});
23440 cx.update_editor(|editor, window, cx| {
23441 editor.handle_input("else:", window, cx);
23442 });
23443 cx.assert_editor_state(indoc! {"
23444 def main():
23445 try:
23446 i = 2
23447 except:
23448 j = 2
23449 else:ˇ
23450 "});
23451
23452 // test `finally` auto outdents when typed inside `else` block
23453 cx.set_state(indoc! {"
23454 def main():
23455 try:
23456 i = 2
23457 except:
23458 j = 2
23459 else:
23460 k = 2
23461 ˇ
23462 "});
23463 cx.update_editor(|editor, window, cx| {
23464 editor.handle_input("finally:", window, cx);
23465 });
23466 cx.assert_editor_state(indoc! {"
23467 def main():
23468 try:
23469 i = 2
23470 except:
23471 j = 2
23472 else:
23473 k = 2
23474 finally:ˇ
23475 "});
23476
23477 // test `else` does not outdents when typed inside `except` block right after for block
23478 cx.set_state(indoc! {"
23479 def main():
23480 try:
23481 i = 2
23482 except:
23483 for i in range(n):
23484 pass
23485 ˇ
23486 "});
23487 cx.update_editor(|editor, window, cx| {
23488 editor.handle_input("else:", window, cx);
23489 });
23490 cx.assert_editor_state(indoc! {"
23491 def main():
23492 try:
23493 i = 2
23494 except:
23495 for i in range(n):
23496 pass
23497 else:ˇ
23498 "});
23499
23500 // test `finally` auto outdents when typed inside `else` block right after for block
23501 cx.set_state(indoc! {"
23502 def main():
23503 try:
23504 i = 2
23505 except:
23506 j = 2
23507 else:
23508 for i in range(n):
23509 pass
23510 ˇ
23511 "});
23512 cx.update_editor(|editor, window, cx| {
23513 editor.handle_input("finally:", window, cx);
23514 });
23515 cx.assert_editor_state(indoc! {"
23516 def main():
23517 try:
23518 i = 2
23519 except:
23520 j = 2
23521 else:
23522 for i in range(n):
23523 pass
23524 finally:ˇ
23525 "});
23526
23527 // test `except` outdents to inner "try" block
23528 cx.set_state(indoc! {"
23529 def main():
23530 try:
23531 i = 2
23532 if i == 2:
23533 try:
23534 i = 3
23535 ˇ
23536 "});
23537 cx.update_editor(|editor, window, cx| {
23538 editor.handle_input("except:", window, cx);
23539 });
23540 cx.assert_editor_state(indoc! {"
23541 def main():
23542 try:
23543 i = 2
23544 if i == 2:
23545 try:
23546 i = 3
23547 except:ˇ
23548 "});
23549
23550 // test `except` outdents to outer "try" block
23551 cx.set_state(indoc! {"
23552 def main():
23553 try:
23554 i = 2
23555 if i == 2:
23556 try:
23557 i = 3
23558 ˇ
23559 "});
23560 cx.update_editor(|editor, window, cx| {
23561 editor.handle_input("except:", window, cx);
23562 });
23563 cx.assert_editor_state(indoc! {"
23564 def main():
23565 try:
23566 i = 2
23567 if i == 2:
23568 try:
23569 i = 3
23570 except:ˇ
23571 "});
23572
23573 // test `else` stays at correct indent when typed after `for` block
23574 cx.set_state(indoc! {"
23575 def main():
23576 for i in range(10):
23577 if i == 3:
23578 break
23579 ˇ
23580 "});
23581 cx.update_editor(|editor, window, cx| {
23582 editor.handle_input("else:", window, cx);
23583 });
23584 cx.assert_editor_state(indoc! {"
23585 def main():
23586 for i in range(10):
23587 if i == 3:
23588 break
23589 else:ˇ
23590 "});
23591
23592 // test does not outdent on typing after line with square brackets
23593 cx.set_state(indoc! {"
23594 def f() -> list[str]:
23595 ˇ
23596 "});
23597 cx.update_editor(|editor, window, cx| {
23598 editor.handle_input("a", window, cx);
23599 });
23600 cx.assert_editor_state(indoc! {"
23601 def f() -> list[str]:
23602 aˇ
23603 "});
23604
23605 // test does not outdent on typing : after case keyword
23606 cx.set_state(indoc! {"
23607 match 1:
23608 caseˇ
23609 "});
23610 cx.update_editor(|editor, window, cx| {
23611 editor.handle_input(":", window, cx);
23612 });
23613 cx.assert_editor_state(indoc! {"
23614 match 1:
23615 case:ˇ
23616 "});
23617}
23618
23619#[gpui::test]
23620async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23621 init_test(cx, |_| {});
23622 update_test_language_settings(cx, |settings| {
23623 settings.defaults.extend_comment_on_newline = Some(false);
23624 });
23625 let mut cx = EditorTestContext::new(cx).await;
23626 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23627 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23628
23629 // test correct indent after newline on comment
23630 cx.set_state(indoc! {"
23631 # COMMENT:ˇ
23632 "});
23633 cx.update_editor(|editor, window, cx| {
23634 editor.newline(&Newline, window, cx);
23635 });
23636 cx.assert_editor_state(indoc! {"
23637 # COMMENT:
23638 ˇ
23639 "});
23640
23641 // test correct indent after newline in brackets
23642 cx.set_state(indoc! {"
23643 {ˇ}
23644 "});
23645 cx.update_editor(|editor, window, cx| {
23646 editor.newline(&Newline, window, cx);
23647 });
23648 cx.run_until_parked();
23649 cx.assert_editor_state(indoc! {"
23650 {
23651 ˇ
23652 }
23653 "});
23654
23655 cx.set_state(indoc! {"
23656 (ˇ)
23657 "});
23658 cx.update_editor(|editor, window, cx| {
23659 editor.newline(&Newline, window, cx);
23660 });
23661 cx.run_until_parked();
23662 cx.assert_editor_state(indoc! {"
23663 (
23664 ˇ
23665 )
23666 "});
23667
23668 // do not indent after empty lists or dictionaries
23669 cx.set_state(indoc! {"
23670 a = []ˇ
23671 "});
23672 cx.update_editor(|editor, window, cx| {
23673 editor.newline(&Newline, window, cx);
23674 });
23675 cx.run_until_parked();
23676 cx.assert_editor_state(indoc! {"
23677 a = []
23678 ˇ
23679 "});
23680}
23681
23682#[gpui::test]
23683async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
23684 init_test(cx, |_| {});
23685
23686 let mut cx = EditorTestContext::new(cx).await;
23687 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23688 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23689
23690 // test cursor move to start of each line on tab
23691 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
23692 cx.set_state(indoc! {"
23693 function main() {
23694 ˇ for item in $items; do
23695 ˇ while [ -n \"$item\" ]; do
23696 ˇ if [ \"$value\" -gt 10 ]; then
23697 ˇ continue
23698 ˇ elif [ \"$value\" -lt 0 ]; then
23699 ˇ break
23700 ˇ else
23701 ˇ echo \"$item\"
23702 ˇ fi
23703 ˇ done
23704 ˇ done
23705 ˇ}
23706 "});
23707 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23708 cx.assert_editor_state(indoc! {"
23709 function main() {
23710 ˇfor item in $items; do
23711 ˇwhile [ -n \"$item\" ]; do
23712 ˇif [ \"$value\" -gt 10 ]; then
23713 ˇcontinue
23714 ˇelif [ \"$value\" -lt 0 ]; then
23715 ˇbreak
23716 ˇelse
23717 ˇecho \"$item\"
23718 ˇfi
23719 ˇdone
23720 ˇdone
23721 ˇ}
23722 "});
23723 // test relative indent is preserved when tab
23724 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23725 cx.assert_editor_state(indoc! {"
23726 function main() {
23727 ˇfor item in $items; do
23728 ˇwhile [ -n \"$item\" ]; do
23729 ˇif [ \"$value\" -gt 10 ]; then
23730 ˇcontinue
23731 ˇelif [ \"$value\" -lt 0 ]; then
23732 ˇbreak
23733 ˇelse
23734 ˇecho \"$item\"
23735 ˇfi
23736 ˇdone
23737 ˇdone
23738 ˇ}
23739 "});
23740
23741 // test cursor move to start of each line on tab
23742 // for `case` statement with patterns
23743 cx.set_state(indoc! {"
23744 function handle() {
23745 ˇ case \"$1\" in
23746 ˇ start)
23747 ˇ echo \"a\"
23748 ˇ ;;
23749 ˇ stop)
23750 ˇ echo \"b\"
23751 ˇ ;;
23752 ˇ *)
23753 ˇ echo \"c\"
23754 ˇ ;;
23755 ˇ esac
23756 ˇ}
23757 "});
23758 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23759 cx.assert_editor_state(indoc! {"
23760 function handle() {
23761 ˇcase \"$1\" in
23762 ˇstart)
23763 ˇecho \"a\"
23764 ˇ;;
23765 ˇstop)
23766 ˇecho \"b\"
23767 ˇ;;
23768 ˇ*)
23769 ˇecho \"c\"
23770 ˇ;;
23771 ˇesac
23772 ˇ}
23773 "});
23774}
23775
23776#[gpui::test]
23777async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
23778 init_test(cx, |_| {});
23779
23780 let mut cx = EditorTestContext::new(cx).await;
23781 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23782 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23783
23784 // test indents on comment insert
23785 cx.set_state(indoc! {"
23786 function main() {
23787 ˇ for item in $items; do
23788 ˇ while [ -n \"$item\" ]; do
23789 ˇ if [ \"$value\" -gt 10 ]; then
23790 ˇ continue
23791 ˇ elif [ \"$value\" -lt 0 ]; then
23792 ˇ break
23793 ˇ else
23794 ˇ echo \"$item\"
23795 ˇ fi
23796 ˇ done
23797 ˇ done
23798 ˇ}
23799 "});
23800 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
23801 cx.assert_editor_state(indoc! {"
23802 function main() {
23803 #ˇ for item in $items; do
23804 #ˇ while [ -n \"$item\" ]; do
23805 #ˇ if [ \"$value\" -gt 10 ]; then
23806 #ˇ continue
23807 #ˇ elif [ \"$value\" -lt 0 ]; then
23808 #ˇ break
23809 #ˇ else
23810 #ˇ echo \"$item\"
23811 #ˇ fi
23812 #ˇ done
23813 #ˇ done
23814 #ˇ}
23815 "});
23816}
23817
23818#[gpui::test]
23819async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
23820 init_test(cx, |_| {});
23821
23822 let mut cx = EditorTestContext::new(cx).await;
23823 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23824 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23825
23826 // test `else` auto outdents when typed inside `if` block
23827 cx.set_state(indoc! {"
23828 if [ \"$1\" = \"test\" ]; then
23829 echo \"foo bar\"
23830 ˇ
23831 "});
23832 cx.update_editor(|editor, window, cx| {
23833 editor.handle_input("else", window, cx);
23834 });
23835 cx.assert_editor_state(indoc! {"
23836 if [ \"$1\" = \"test\" ]; then
23837 echo \"foo bar\"
23838 elseˇ
23839 "});
23840
23841 // test `elif` auto outdents when typed inside `if` block
23842 cx.set_state(indoc! {"
23843 if [ \"$1\" = \"test\" ]; then
23844 echo \"foo bar\"
23845 ˇ
23846 "});
23847 cx.update_editor(|editor, window, cx| {
23848 editor.handle_input("elif", window, cx);
23849 });
23850 cx.assert_editor_state(indoc! {"
23851 if [ \"$1\" = \"test\" ]; then
23852 echo \"foo bar\"
23853 elifˇ
23854 "});
23855
23856 // test `fi` auto outdents when typed inside `else` block
23857 cx.set_state(indoc! {"
23858 if [ \"$1\" = \"test\" ]; then
23859 echo \"foo bar\"
23860 else
23861 echo \"bar baz\"
23862 ˇ
23863 "});
23864 cx.update_editor(|editor, window, cx| {
23865 editor.handle_input("fi", window, cx);
23866 });
23867 cx.assert_editor_state(indoc! {"
23868 if [ \"$1\" = \"test\" ]; then
23869 echo \"foo bar\"
23870 else
23871 echo \"bar baz\"
23872 fiˇ
23873 "});
23874
23875 // test `done` auto outdents when typed inside `while` block
23876 cx.set_state(indoc! {"
23877 while read line; do
23878 echo \"$line\"
23879 ˇ
23880 "});
23881 cx.update_editor(|editor, window, cx| {
23882 editor.handle_input("done", window, cx);
23883 });
23884 cx.assert_editor_state(indoc! {"
23885 while read line; do
23886 echo \"$line\"
23887 doneˇ
23888 "});
23889
23890 // test `done` auto outdents when typed inside `for` block
23891 cx.set_state(indoc! {"
23892 for file in *.txt; do
23893 cat \"$file\"
23894 ˇ
23895 "});
23896 cx.update_editor(|editor, window, cx| {
23897 editor.handle_input("done", window, cx);
23898 });
23899 cx.assert_editor_state(indoc! {"
23900 for file in *.txt; do
23901 cat \"$file\"
23902 doneˇ
23903 "});
23904
23905 // test `esac` auto outdents when typed inside `case` block
23906 cx.set_state(indoc! {"
23907 case \"$1\" in
23908 start)
23909 echo \"foo bar\"
23910 ;;
23911 stop)
23912 echo \"bar baz\"
23913 ;;
23914 ˇ
23915 "});
23916 cx.update_editor(|editor, window, cx| {
23917 editor.handle_input("esac", window, cx);
23918 });
23919 cx.assert_editor_state(indoc! {"
23920 case \"$1\" in
23921 start)
23922 echo \"foo bar\"
23923 ;;
23924 stop)
23925 echo \"bar baz\"
23926 ;;
23927 esacˇ
23928 "});
23929
23930 // test `*)` auto outdents when typed inside `case` block
23931 cx.set_state(indoc! {"
23932 case \"$1\" in
23933 start)
23934 echo \"foo bar\"
23935 ;;
23936 ˇ
23937 "});
23938 cx.update_editor(|editor, window, cx| {
23939 editor.handle_input("*)", window, cx);
23940 });
23941 cx.assert_editor_state(indoc! {"
23942 case \"$1\" in
23943 start)
23944 echo \"foo bar\"
23945 ;;
23946 *)ˇ
23947 "});
23948
23949 // test `fi` outdents to correct level with nested if blocks
23950 cx.set_state(indoc! {"
23951 if [ \"$1\" = \"test\" ]; then
23952 echo \"outer if\"
23953 if [ \"$2\" = \"debug\" ]; then
23954 echo \"inner if\"
23955 ˇ
23956 "});
23957 cx.update_editor(|editor, window, cx| {
23958 editor.handle_input("fi", window, cx);
23959 });
23960 cx.assert_editor_state(indoc! {"
23961 if [ \"$1\" = \"test\" ]; then
23962 echo \"outer if\"
23963 if [ \"$2\" = \"debug\" ]; then
23964 echo \"inner if\"
23965 fiˇ
23966 "});
23967}
23968
23969#[gpui::test]
23970async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23971 init_test(cx, |_| {});
23972 update_test_language_settings(cx, |settings| {
23973 settings.defaults.extend_comment_on_newline = Some(false);
23974 });
23975 let mut cx = EditorTestContext::new(cx).await;
23976 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23977 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23978
23979 // test correct indent after newline on comment
23980 cx.set_state(indoc! {"
23981 # COMMENT:ˇ
23982 "});
23983 cx.update_editor(|editor, window, cx| {
23984 editor.newline(&Newline, window, cx);
23985 });
23986 cx.assert_editor_state(indoc! {"
23987 # COMMENT:
23988 ˇ
23989 "});
23990
23991 // test correct indent after newline after `then`
23992 cx.set_state(indoc! {"
23993
23994 if [ \"$1\" = \"test\" ]; thenˇ
23995 "});
23996 cx.update_editor(|editor, window, cx| {
23997 editor.newline(&Newline, window, cx);
23998 });
23999 cx.run_until_parked();
24000 cx.assert_editor_state(indoc! {"
24001
24002 if [ \"$1\" = \"test\" ]; then
24003 ˇ
24004 "});
24005
24006 // test correct indent after newline after `else`
24007 cx.set_state(indoc! {"
24008 if [ \"$1\" = \"test\" ]; then
24009 elseˇ
24010 "});
24011 cx.update_editor(|editor, window, cx| {
24012 editor.newline(&Newline, window, cx);
24013 });
24014 cx.run_until_parked();
24015 cx.assert_editor_state(indoc! {"
24016 if [ \"$1\" = \"test\" ]; then
24017 else
24018 ˇ
24019 "});
24020
24021 // test correct indent after newline after `elif`
24022 cx.set_state(indoc! {"
24023 if [ \"$1\" = \"test\" ]; then
24024 elifˇ
24025 "});
24026 cx.update_editor(|editor, window, cx| {
24027 editor.newline(&Newline, window, cx);
24028 });
24029 cx.run_until_parked();
24030 cx.assert_editor_state(indoc! {"
24031 if [ \"$1\" = \"test\" ]; then
24032 elif
24033 ˇ
24034 "});
24035
24036 // test correct indent after newline after `do`
24037 cx.set_state(indoc! {"
24038 for file in *.txt; doˇ
24039 "});
24040 cx.update_editor(|editor, window, cx| {
24041 editor.newline(&Newline, window, cx);
24042 });
24043 cx.run_until_parked();
24044 cx.assert_editor_state(indoc! {"
24045 for file in *.txt; do
24046 ˇ
24047 "});
24048
24049 // test correct indent after newline after case pattern
24050 cx.set_state(indoc! {"
24051 case \"$1\" in
24052 start)ˇ
24053 "});
24054 cx.update_editor(|editor, window, cx| {
24055 editor.newline(&Newline, window, cx);
24056 });
24057 cx.run_until_parked();
24058 cx.assert_editor_state(indoc! {"
24059 case \"$1\" in
24060 start)
24061 ˇ
24062 "});
24063
24064 // test correct indent after newline after case pattern
24065 cx.set_state(indoc! {"
24066 case \"$1\" in
24067 start)
24068 ;;
24069 *)ˇ
24070 "});
24071 cx.update_editor(|editor, window, cx| {
24072 editor.newline(&Newline, window, cx);
24073 });
24074 cx.run_until_parked();
24075 cx.assert_editor_state(indoc! {"
24076 case \"$1\" in
24077 start)
24078 ;;
24079 *)
24080 ˇ
24081 "});
24082
24083 // test correct indent after newline after function opening brace
24084 cx.set_state(indoc! {"
24085 function test() {ˇ}
24086 "});
24087 cx.update_editor(|editor, window, cx| {
24088 editor.newline(&Newline, window, cx);
24089 });
24090 cx.run_until_parked();
24091 cx.assert_editor_state(indoc! {"
24092 function test() {
24093 ˇ
24094 }
24095 "});
24096
24097 // test no extra indent after semicolon on same line
24098 cx.set_state(indoc! {"
24099 echo \"test\";ˇ
24100 "});
24101 cx.update_editor(|editor, window, cx| {
24102 editor.newline(&Newline, window, cx);
24103 });
24104 cx.run_until_parked();
24105 cx.assert_editor_state(indoc! {"
24106 echo \"test\";
24107 ˇ
24108 "});
24109}
24110
24111fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24112 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24113 point..point
24114}
24115
24116#[track_caller]
24117fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24118 let (text, ranges) = marked_text_ranges(marked_text, true);
24119 assert_eq!(editor.text(cx), text);
24120 assert_eq!(
24121 editor.selections.ranges(cx),
24122 ranges,
24123 "Assert selections are {}",
24124 marked_text
24125 );
24126}
24127
24128pub fn handle_signature_help_request(
24129 cx: &mut EditorLspTestContext,
24130 mocked_response: lsp::SignatureHelp,
24131) -> impl Future<Output = ()> + use<> {
24132 let mut request =
24133 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24134 let mocked_response = mocked_response.clone();
24135 async move { Ok(Some(mocked_response)) }
24136 });
24137
24138 async move {
24139 request.next().await;
24140 }
24141}
24142
24143#[track_caller]
24144pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24145 cx.update_editor(|editor, _, _| {
24146 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24147 let entries = menu.entries.borrow();
24148 let entries = entries
24149 .iter()
24150 .map(|entry| entry.string.as_str())
24151 .collect::<Vec<_>>();
24152 assert_eq!(entries, expected);
24153 } else {
24154 panic!("Expected completions menu");
24155 }
24156 });
24157}
24158
24159/// Handle completion request passing a marked string specifying where the completion
24160/// should be triggered from using '|' character, what range should be replaced, and what completions
24161/// should be returned using '<' and '>' to delimit the range.
24162///
24163/// Also see `handle_completion_request_with_insert_and_replace`.
24164#[track_caller]
24165pub fn handle_completion_request(
24166 marked_string: &str,
24167 completions: Vec<&'static str>,
24168 is_incomplete: bool,
24169 counter: Arc<AtomicUsize>,
24170 cx: &mut EditorLspTestContext,
24171) -> impl Future<Output = ()> {
24172 let complete_from_marker: TextRangeMarker = '|'.into();
24173 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24174 let (_, mut marked_ranges) = marked_text_ranges_by(
24175 marked_string,
24176 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24177 );
24178
24179 let complete_from_position =
24180 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24181 let replace_range =
24182 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24183
24184 let mut request =
24185 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24186 let completions = completions.clone();
24187 counter.fetch_add(1, atomic::Ordering::Release);
24188 async move {
24189 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24190 assert_eq!(
24191 params.text_document_position.position,
24192 complete_from_position
24193 );
24194 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24195 is_incomplete,
24196 item_defaults: None,
24197 items: completions
24198 .iter()
24199 .map(|completion_text| lsp::CompletionItem {
24200 label: completion_text.to_string(),
24201 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24202 range: replace_range,
24203 new_text: completion_text.to_string(),
24204 })),
24205 ..Default::default()
24206 })
24207 .collect(),
24208 })))
24209 }
24210 });
24211
24212 async move {
24213 request.next().await;
24214 }
24215}
24216
24217/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
24218/// given instead, which also contains an `insert` range.
24219///
24220/// This function uses markers to define ranges:
24221/// - `|` marks the cursor position
24222/// - `<>` marks the replace range
24223/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
24224pub fn handle_completion_request_with_insert_and_replace(
24225 cx: &mut EditorLspTestContext,
24226 marked_string: &str,
24227 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
24228 counter: Arc<AtomicUsize>,
24229) -> impl Future<Output = ()> {
24230 let complete_from_marker: TextRangeMarker = '|'.into();
24231 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24232 let insert_range_marker: TextRangeMarker = ('{', '}').into();
24233
24234 let (_, mut marked_ranges) = marked_text_ranges_by(
24235 marked_string,
24236 vec![
24237 complete_from_marker.clone(),
24238 replace_range_marker.clone(),
24239 insert_range_marker.clone(),
24240 ],
24241 );
24242
24243 let complete_from_position =
24244 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24245 let replace_range =
24246 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24247
24248 let insert_range = match marked_ranges.remove(&insert_range_marker) {
24249 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
24250 _ => lsp::Range {
24251 start: replace_range.start,
24252 end: complete_from_position,
24253 },
24254 };
24255
24256 let mut request =
24257 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24258 let completions = completions.clone();
24259 counter.fetch_add(1, atomic::Ordering::Release);
24260 async move {
24261 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24262 assert_eq!(
24263 params.text_document_position.position, complete_from_position,
24264 "marker `|` position doesn't match",
24265 );
24266 Ok(Some(lsp::CompletionResponse::Array(
24267 completions
24268 .iter()
24269 .map(|(label, new_text)| lsp::CompletionItem {
24270 label: label.to_string(),
24271 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24272 lsp::InsertReplaceEdit {
24273 insert: insert_range,
24274 replace: replace_range,
24275 new_text: new_text.to_string(),
24276 },
24277 )),
24278 ..Default::default()
24279 })
24280 .collect(),
24281 )))
24282 }
24283 });
24284
24285 async move {
24286 request.next().await;
24287 }
24288}
24289
24290fn handle_resolve_completion_request(
24291 cx: &mut EditorLspTestContext,
24292 edits: Option<Vec<(&'static str, &'static str)>>,
24293) -> impl Future<Output = ()> {
24294 let edits = edits.map(|edits| {
24295 edits
24296 .iter()
24297 .map(|(marked_string, new_text)| {
24298 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
24299 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
24300 lsp::TextEdit::new(replace_range, new_text.to_string())
24301 })
24302 .collect::<Vec<_>>()
24303 });
24304
24305 let mut request =
24306 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
24307 let edits = edits.clone();
24308 async move {
24309 Ok(lsp::CompletionItem {
24310 additional_text_edits: edits,
24311 ..Default::default()
24312 })
24313 }
24314 });
24315
24316 async move {
24317 request.next().await;
24318 }
24319}
24320
24321pub(crate) fn update_test_language_settings(
24322 cx: &mut TestAppContext,
24323 f: impl Fn(&mut AllLanguageSettingsContent),
24324) {
24325 cx.update(|cx| {
24326 SettingsStore::update_global(cx, |store, cx| {
24327 store.update_user_settings::<AllLanguageSettings>(cx, f);
24328 });
24329 });
24330}
24331
24332pub(crate) fn update_test_project_settings(
24333 cx: &mut TestAppContext,
24334 f: impl Fn(&mut ProjectSettings),
24335) {
24336 cx.update(|cx| {
24337 SettingsStore::update_global(cx, |store, cx| {
24338 store.update_user_settings::<ProjectSettings>(cx, f);
24339 });
24340 });
24341}
24342
24343pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
24344 cx.update(|cx| {
24345 assets::Assets.load_test_fonts(cx);
24346 let store = SettingsStore::test(cx);
24347 cx.set_global(store);
24348 theme::init(theme::LoadThemes::JustBase, cx);
24349 release_channel::init(SemanticVersion::default(), cx);
24350 client::init_settings(cx);
24351 language::init(cx);
24352 Project::init_settings(cx);
24353 workspace::init_settings(cx);
24354 crate::init(cx);
24355 });
24356 zlog::init_test();
24357 update_test_language_settings(cx, f);
24358}
24359
24360#[track_caller]
24361fn assert_hunk_revert(
24362 not_reverted_text_with_selections: &str,
24363 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
24364 expected_reverted_text_with_selections: &str,
24365 base_text: &str,
24366 cx: &mut EditorLspTestContext,
24367) {
24368 cx.set_state(not_reverted_text_with_selections);
24369 cx.set_head_text(base_text);
24370 cx.executor().run_until_parked();
24371
24372 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
24373 let snapshot = editor.snapshot(window, cx);
24374 let reverted_hunk_statuses = snapshot
24375 .buffer_snapshot
24376 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
24377 .map(|hunk| hunk.status().kind)
24378 .collect::<Vec<_>>();
24379
24380 editor.git_restore(&Default::default(), window, cx);
24381 reverted_hunk_statuses
24382 });
24383 cx.executor().run_until_parked();
24384 cx.assert_editor_state(expected_reverted_text_with_selections);
24385 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
24386}
24387
24388#[gpui::test(iterations = 10)]
24389async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
24390 init_test(cx, |_| {});
24391
24392 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
24393 let counter = diagnostic_requests.clone();
24394
24395 let fs = FakeFs::new(cx.executor());
24396 fs.insert_tree(
24397 path!("/a"),
24398 json!({
24399 "first.rs": "fn main() { let a = 5; }",
24400 "second.rs": "// Test file",
24401 }),
24402 )
24403 .await;
24404
24405 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24406 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24407 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24408
24409 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24410 language_registry.add(rust_lang());
24411 let mut fake_servers = language_registry.register_fake_lsp(
24412 "Rust",
24413 FakeLspAdapter {
24414 capabilities: lsp::ServerCapabilities {
24415 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
24416 lsp::DiagnosticOptions {
24417 identifier: None,
24418 inter_file_dependencies: true,
24419 workspace_diagnostics: true,
24420 work_done_progress_options: Default::default(),
24421 },
24422 )),
24423 ..Default::default()
24424 },
24425 ..Default::default()
24426 },
24427 );
24428
24429 let editor = workspace
24430 .update(cx, |workspace, window, cx| {
24431 workspace.open_abs_path(
24432 PathBuf::from(path!("/a/first.rs")),
24433 OpenOptions::default(),
24434 window,
24435 cx,
24436 )
24437 })
24438 .unwrap()
24439 .await
24440 .unwrap()
24441 .downcast::<Editor>()
24442 .unwrap();
24443 let fake_server = fake_servers.next().await.unwrap();
24444 let server_id = fake_server.server.server_id();
24445 let mut first_request = fake_server
24446 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
24447 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
24448 let result_id = Some(new_result_id.to_string());
24449 assert_eq!(
24450 params.text_document.uri,
24451 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24452 );
24453 async move {
24454 Ok(lsp::DocumentDiagnosticReportResult::Report(
24455 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
24456 related_documents: None,
24457 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
24458 items: Vec::new(),
24459 result_id,
24460 },
24461 }),
24462 ))
24463 }
24464 });
24465
24466 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
24467 project.update(cx, |project, cx| {
24468 let buffer_id = editor
24469 .read(cx)
24470 .buffer()
24471 .read(cx)
24472 .as_singleton()
24473 .expect("created a singleton buffer")
24474 .read(cx)
24475 .remote_id();
24476 let buffer_result_id = project
24477 .lsp_store()
24478 .read(cx)
24479 .result_id(server_id, buffer_id, cx);
24480 assert_eq!(expected, buffer_result_id);
24481 });
24482 };
24483
24484 ensure_result_id(None, cx);
24485 cx.executor().advance_clock(Duration::from_millis(60));
24486 cx.executor().run_until_parked();
24487 assert_eq!(
24488 diagnostic_requests.load(atomic::Ordering::Acquire),
24489 1,
24490 "Opening file should trigger diagnostic request"
24491 );
24492 first_request
24493 .next()
24494 .await
24495 .expect("should have sent the first diagnostics pull request");
24496 ensure_result_id(Some("1".to_string()), cx);
24497
24498 // Editing should trigger diagnostics
24499 editor.update_in(cx, |editor, window, cx| {
24500 editor.handle_input("2", window, cx)
24501 });
24502 cx.executor().advance_clock(Duration::from_millis(60));
24503 cx.executor().run_until_parked();
24504 assert_eq!(
24505 diagnostic_requests.load(atomic::Ordering::Acquire),
24506 2,
24507 "Editing should trigger diagnostic request"
24508 );
24509 ensure_result_id(Some("2".to_string()), cx);
24510
24511 // Moving cursor should not trigger diagnostic request
24512 editor.update_in(cx, |editor, window, cx| {
24513 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24514 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
24515 });
24516 });
24517 cx.executor().advance_clock(Duration::from_millis(60));
24518 cx.executor().run_until_parked();
24519 assert_eq!(
24520 diagnostic_requests.load(atomic::Ordering::Acquire),
24521 2,
24522 "Cursor movement should not trigger diagnostic request"
24523 );
24524 ensure_result_id(Some("2".to_string()), cx);
24525 // Multiple rapid edits should be debounced
24526 for _ in 0..5 {
24527 editor.update_in(cx, |editor, window, cx| {
24528 editor.handle_input("x", window, cx)
24529 });
24530 }
24531 cx.executor().advance_clock(Duration::from_millis(60));
24532 cx.executor().run_until_parked();
24533
24534 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
24535 assert!(
24536 final_requests <= 4,
24537 "Multiple rapid edits should be debounced (got {final_requests} requests)",
24538 );
24539 ensure_result_id(Some(final_requests.to_string()), cx);
24540}
24541
24542#[gpui::test]
24543async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
24544 // Regression test for issue #11671
24545 // Previously, adding a cursor after moving multiple cursors would reset
24546 // the cursor count instead of adding to the existing cursors.
24547 init_test(cx, |_| {});
24548 let mut cx = EditorTestContext::new(cx).await;
24549
24550 // Create a simple buffer with cursor at start
24551 cx.set_state(indoc! {"
24552 ˇaaaa
24553 bbbb
24554 cccc
24555 dddd
24556 eeee
24557 ffff
24558 gggg
24559 hhhh"});
24560
24561 // Add 2 cursors below (so we have 3 total)
24562 cx.update_editor(|editor, window, cx| {
24563 editor.add_selection_below(&Default::default(), window, cx);
24564 editor.add_selection_below(&Default::default(), window, cx);
24565 });
24566
24567 // Verify we have 3 cursors
24568 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24569 assert_eq!(
24570 initial_count, 3,
24571 "Should have 3 cursors after adding 2 below"
24572 );
24573
24574 // Move down one line
24575 cx.update_editor(|editor, window, cx| {
24576 editor.move_down(&MoveDown, window, cx);
24577 });
24578
24579 // Add another cursor below
24580 cx.update_editor(|editor, window, cx| {
24581 editor.add_selection_below(&Default::default(), window, cx);
24582 });
24583
24584 // Should now have 4 cursors (3 original + 1 new)
24585 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24586 assert_eq!(
24587 final_count, 4,
24588 "Should have 4 cursors after moving and adding another"
24589 );
24590}
24591
24592#[gpui::test(iterations = 10)]
24593async fn test_document_colors(cx: &mut TestAppContext) {
24594 let expected_color = Rgba {
24595 r: 0.33,
24596 g: 0.33,
24597 b: 0.33,
24598 a: 0.33,
24599 };
24600
24601 init_test(cx, |_| {});
24602
24603 let fs = FakeFs::new(cx.executor());
24604 fs.insert_tree(
24605 path!("/a"),
24606 json!({
24607 "first.rs": "fn main() { let a = 5; }",
24608 }),
24609 )
24610 .await;
24611
24612 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24613 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24614 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24615
24616 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24617 language_registry.add(rust_lang());
24618 let mut fake_servers = language_registry.register_fake_lsp(
24619 "Rust",
24620 FakeLspAdapter {
24621 capabilities: lsp::ServerCapabilities {
24622 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24623 ..lsp::ServerCapabilities::default()
24624 },
24625 name: "rust-analyzer",
24626 ..FakeLspAdapter::default()
24627 },
24628 );
24629 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24630 "Rust",
24631 FakeLspAdapter {
24632 capabilities: lsp::ServerCapabilities {
24633 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24634 ..lsp::ServerCapabilities::default()
24635 },
24636 name: "not-rust-analyzer",
24637 ..FakeLspAdapter::default()
24638 },
24639 );
24640
24641 let editor = workspace
24642 .update(cx, |workspace, window, cx| {
24643 workspace.open_abs_path(
24644 PathBuf::from(path!("/a/first.rs")),
24645 OpenOptions::default(),
24646 window,
24647 cx,
24648 )
24649 })
24650 .unwrap()
24651 .await
24652 .unwrap()
24653 .downcast::<Editor>()
24654 .unwrap();
24655 let fake_language_server = fake_servers.next().await.unwrap();
24656 let fake_language_server_without_capabilities =
24657 fake_servers_without_capabilities.next().await.unwrap();
24658 let requests_made = Arc::new(AtomicUsize::new(0));
24659 let closure_requests_made = Arc::clone(&requests_made);
24660 let mut color_request_handle = fake_language_server
24661 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
24662 let requests_made = Arc::clone(&closure_requests_made);
24663 async move {
24664 assert_eq!(
24665 params.text_document.uri,
24666 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24667 );
24668 requests_made.fetch_add(1, atomic::Ordering::Release);
24669 Ok(vec![
24670 lsp::ColorInformation {
24671 range: lsp::Range {
24672 start: lsp::Position {
24673 line: 0,
24674 character: 0,
24675 },
24676 end: lsp::Position {
24677 line: 0,
24678 character: 1,
24679 },
24680 },
24681 color: lsp::Color {
24682 red: 0.33,
24683 green: 0.33,
24684 blue: 0.33,
24685 alpha: 0.33,
24686 },
24687 },
24688 lsp::ColorInformation {
24689 range: lsp::Range {
24690 start: lsp::Position {
24691 line: 0,
24692 character: 0,
24693 },
24694 end: lsp::Position {
24695 line: 0,
24696 character: 1,
24697 },
24698 },
24699 color: lsp::Color {
24700 red: 0.33,
24701 green: 0.33,
24702 blue: 0.33,
24703 alpha: 0.33,
24704 },
24705 },
24706 ])
24707 }
24708 });
24709
24710 let _handle = fake_language_server_without_capabilities
24711 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
24712 panic!("Should not be called");
24713 });
24714 cx.executor().advance_clock(Duration::from_millis(100));
24715 color_request_handle.next().await.unwrap();
24716 cx.run_until_parked();
24717 assert_eq!(
24718 1,
24719 requests_made.load(atomic::Ordering::Acquire),
24720 "Should query for colors once per editor open"
24721 );
24722 editor.update_in(cx, |editor, _, cx| {
24723 assert_eq!(
24724 vec![expected_color],
24725 extract_color_inlays(editor, cx),
24726 "Should have an initial inlay"
24727 );
24728 });
24729
24730 // opening another file in a split should not influence the LSP query counter
24731 workspace
24732 .update(cx, |workspace, window, cx| {
24733 assert_eq!(
24734 workspace.panes().len(),
24735 1,
24736 "Should have one pane with one editor"
24737 );
24738 workspace.move_item_to_pane_in_direction(
24739 &MoveItemToPaneInDirection {
24740 direction: SplitDirection::Right,
24741 focus: false,
24742 clone: true,
24743 },
24744 window,
24745 cx,
24746 );
24747 })
24748 .unwrap();
24749 cx.run_until_parked();
24750 workspace
24751 .update(cx, |workspace, _, cx| {
24752 let panes = workspace.panes();
24753 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
24754 for pane in panes {
24755 let editor = pane
24756 .read(cx)
24757 .active_item()
24758 .and_then(|item| item.downcast::<Editor>())
24759 .expect("Should have opened an editor in each split");
24760 let editor_file = editor
24761 .read(cx)
24762 .buffer()
24763 .read(cx)
24764 .as_singleton()
24765 .expect("test deals with singleton buffers")
24766 .read(cx)
24767 .file()
24768 .expect("test buffese should have a file")
24769 .path();
24770 assert_eq!(
24771 editor_file.as_ref(),
24772 Path::new("first.rs"),
24773 "Both editors should be opened for the same file"
24774 )
24775 }
24776 })
24777 .unwrap();
24778
24779 cx.executor().advance_clock(Duration::from_millis(500));
24780 let save = editor.update_in(cx, |editor, window, cx| {
24781 editor.move_to_end(&MoveToEnd, window, cx);
24782 editor.handle_input("dirty", window, cx);
24783 editor.save(
24784 SaveOptions {
24785 format: true,
24786 autosave: true,
24787 },
24788 project.clone(),
24789 window,
24790 cx,
24791 )
24792 });
24793 save.await.unwrap();
24794
24795 color_request_handle.next().await.unwrap();
24796 cx.run_until_parked();
24797 assert_eq!(
24798 3,
24799 requests_made.load(atomic::Ordering::Acquire),
24800 "Should query for colors once per save and once per formatting after save"
24801 );
24802
24803 drop(editor);
24804 let close = workspace
24805 .update(cx, |workspace, window, cx| {
24806 workspace.active_pane().update(cx, |pane, cx| {
24807 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24808 })
24809 })
24810 .unwrap();
24811 close.await.unwrap();
24812 let close = workspace
24813 .update(cx, |workspace, window, cx| {
24814 workspace.active_pane().update(cx, |pane, cx| {
24815 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24816 })
24817 })
24818 .unwrap();
24819 close.await.unwrap();
24820 assert_eq!(
24821 3,
24822 requests_made.load(atomic::Ordering::Acquire),
24823 "After saving and closing all editors, no extra requests should be made"
24824 );
24825 workspace
24826 .update(cx, |workspace, _, cx| {
24827 assert!(
24828 workspace.active_item(cx).is_none(),
24829 "Should close all editors"
24830 )
24831 })
24832 .unwrap();
24833
24834 workspace
24835 .update(cx, |workspace, window, cx| {
24836 workspace.active_pane().update(cx, |pane, cx| {
24837 pane.navigate_backward(&Default::default(), window, cx);
24838 })
24839 })
24840 .unwrap();
24841 cx.executor().advance_clock(Duration::from_millis(100));
24842 cx.run_until_parked();
24843 let editor = workspace
24844 .update(cx, |workspace, _, cx| {
24845 workspace
24846 .active_item(cx)
24847 .expect("Should have reopened the editor again after navigating back")
24848 .downcast::<Editor>()
24849 .expect("Should be an editor")
24850 })
24851 .unwrap();
24852 color_request_handle.next().await.unwrap();
24853 assert_eq!(
24854 3,
24855 requests_made.load(atomic::Ordering::Acquire),
24856 "Cache should be reused on buffer close and reopen"
24857 );
24858 editor.update(cx, |editor, cx| {
24859 assert_eq!(
24860 vec![expected_color],
24861 extract_color_inlays(editor, cx),
24862 "Should have an initial inlay"
24863 );
24864 });
24865}
24866
24867#[gpui::test]
24868async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24869 init_test(cx, |_| {});
24870 let (editor, cx) = cx.add_window_view(Editor::single_line);
24871 editor.update_in(cx, |editor, window, cx| {
24872 editor.set_text("oops\n\nwow\n", window, cx)
24873 });
24874 cx.run_until_parked();
24875 editor.update(cx, |editor, cx| {
24876 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24877 });
24878 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24879 cx.run_until_parked();
24880 editor.update(cx, |editor, cx| {
24881 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24882 });
24883}
24884
24885#[gpui::test]
24886async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
24887 init_test(cx, |_| {});
24888
24889 cx.update(|cx| {
24890 register_project_item::<Editor>(cx);
24891 });
24892
24893 let fs = FakeFs::new(cx.executor());
24894 fs.insert_tree("/root1", json!({})).await;
24895 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
24896 .await;
24897
24898 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
24899 let (workspace, cx) =
24900 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24901
24902 let worktree_id = project.update(cx, |project, cx| {
24903 project.worktrees(cx).next().unwrap().read(cx).id()
24904 });
24905
24906 let handle = workspace
24907 .update_in(cx, |workspace, window, cx| {
24908 let project_path = (worktree_id, "one.pdf");
24909 workspace.open_path(project_path, None, true, window, cx)
24910 })
24911 .await
24912 .unwrap();
24913
24914 assert_eq!(
24915 handle.to_any().entity_type(),
24916 TypeId::of::<InvalidBufferView>()
24917 );
24918}
24919
24920#[track_caller]
24921fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24922 editor
24923 .all_inlays(cx)
24924 .into_iter()
24925 .filter_map(|inlay| inlay.get_color())
24926 .map(Rgba::from)
24927 .collect()
24928}