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]
2479async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481
2482 let mut cx = EditorTestContext::new(cx).await;
2483
2484 // For an empty selection, the preceding word fragment is deleted.
2485 // For non-empty selections, only selected characters are deleted.
2486 cx.set_state("onˇe two t«hreˇ»e four");
2487 cx.update_editor(|editor, window, cx| {
2488 editor.delete_to_previous_word_start(
2489 &DeleteToPreviousWordStart {
2490 ignore_newlines: false,
2491 ignore_brackets: false,
2492 },
2493 window,
2494 cx,
2495 );
2496 });
2497 cx.assert_editor_state("ˇe two tˇe four");
2498
2499 cx.set_state("e tˇwo te «fˇ»our");
2500 cx.update_editor(|editor, window, cx| {
2501 editor.delete_to_next_word_end(
2502 &DeleteToNextWordEnd {
2503 ignore_newlines: false,
2504 ignore_brackets: false,
2505 },
2506 window,
2507 cx,
2508 );
2509 });
2510 cx.assert_editor_state("e tˇ te ˇour");
2511}
2512
2513#[gpui::test]
2514async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2515 init_test(cx, |_| {});
2516
2517 let mut cx = EditorTestContext::new(cx).await;
2518
2519 cx.set_state("here is some text ˇwith a space");
2520 cx.update_editor(|editor, window, cx| {
2521 editor.delete_to_previous_word_start(
2522 &DeleteToPreviousWordStart {
2523 ignore_newlines: false,
2524 ignore_brackets: true,
2525 },
2526 window,
2527 cx,
2528 );
2529 });
2530 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2531 cx.assert_editor_state("here is some textˇwith a space");
2532
2533 cx.set_state("here is some text ˇwith a space");
2534 cx.update_editor(|editor, window, cx| {
2535 editor.delete_to_previous_word_start(
2536 &DeleteToPreviousWordStart {
2537 ignore_newlines: false,
2538 ignore_brackets: false,
2539 },
2540 window,
2541 cx,
2542 );
2543 });
2544 cx.assert_editor_state("here is some textˇwith a space");
2545
2546 cx.set_state("here is some textˇ with a space");
2547 cx.update_editor(|editor, window, cx| {
2548 editor.delete_to_next_word_end(
2549 &DeleteToNextWordEnd {
2550 ignore_newlines: false,
2551 ignore_brackets: true,
2552 },
2553 window,
2554 cx,
2555 );
2556 });
2557 // Same happens in the other direction.
2558 cx.assert_editor_state("here is some textˇwith a space");
2559
2560 cx.set_state("here is some textˇ with a space");
2561 cx.update_editor(|editor, window, cx| {
2562 editor.delete_to_next_word_end(
2563 &DeleteToNextWordEnd {
2564 ignore_newlines: false,
2565 ignore_brackets: false,
2566 },
2567 window,
2568 cx,
2569 );
2570 });
2571 cx.assert_editor_state("here is some textˇwith a space");
2572
2573 cx.set_state("here is some textˇ with a space");
2574 cx.update_editor(|editor, window, cx| {
2575 editor.delete_to_next_word_end(
2576 &DeleteToNextWordEnd {
2577 ignore_newlines: true,
2578 ignore_brackets: false,
2579 },
2580 window,
2581 cx,
2582 );
2583 });
2584 cx.assert_editor_state("here is some textˇwith a space");
2585 cx.update_editor(|editor, window, cx| {
2586 editor.delete_to_previous_word_start(
2587 &DeleteToPreviousWordStart {
2588 ignore_newlines: true,
2589 ignore_brackets: false,
2590 },
2591 window,
2592 cx,
2593 );
2594 });
2595 cx.assert_editor_state("here is some ˇwith a space");
2596 cx.update_editor(|editor, window, cx| {
2597 editor.delete_to_previous_word_start(
2598 &DeleteToPreviousWordStart {
2599 ignore_newlines: true,
2600 ignore_brackets: false,
2601 },
2602 window,
2603 cx,
2604 );
2605 });
2606 // Single whitespaces are removed with the word behind them.
2607 cx.assert_editor_state("here is ˇwith a space");
2608 cx.update_editor(|editor, window, cx| {
2609 editor.delete_to_previous_word_start(
2610 &DeleteToPreviousWordStart {
2611 ignore_newlines: true,
2612 ignore_brackets: false,
2613 },
2614 window,
2615 cx,
2616 );
2617 });
2618 cx.assert_editor_state("here ˇwith a space");
2619 cx.update_editor(|editor, window, cx| {
2620 editor.delete_to_previous_word_start(
2621 &DeleteToPreviousWordStart {
2622 ignore_newlines: true,
2623 ignore_brackets: false,
2624 },
2625 window,
2626 cx,
2627 );
2628 });
2629 cx.assert_editor_state("ˇwith a space");
2630 cx.update_editor(|editor, window, cx| {
2631 editor.delete_to_previous_word_start(
2632 &DeleteToPreviousWordStart {
2633 ignore_newlines: true,
2634 ignore_brackets: false,
2635 },
2636 window,
2637 cx,
2638 );
2639 });
2640 cx.assert_editor_state("ˇwith a space");
2641 cx.update_editor(|editor, window, cx| {
2642 editor.delete_to_next_word_end(
2643 &DeleteToNextWordEnd {
2644 ignore_newlines: true,
2645 ignore_brackets: false,
2646 },
2647 window,
2648 cx,
2649 );
2650 });
2651 // Same happens in the other direction.
2652 cx.assert_editor_state("ˇ a space");
2653 cx.update_editor(|editor, window, cx| {
2654 editor.delete_to_next_word_end(
2655 &DeleteToNextWordEnd {
2656 ignore_newlines: true,
2657 ignore_brackets: false,
2658 },
2659 window,
2660 cx,
2661 );
2662 });
2663 cx.assert_editor_state("ˇ space");
2664 cx.update_editor(|editor, window, cx| {
2665 editor.delete_to_next_word_end(
2666 &DeleteToNextWordEnd {
2667 ignore_newlines: true,
2668 ignore_brackets: false,
2669 },
2670 window,
2671 cx,
2672 );
2673 });
2674 cx.assert_editor_state("ˇ");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: true,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("ˇ");
2686 cx.update_editor(|editor, window, cx| {
2687 editor.delete_to_previous_word_start(
2688 &DeleteToPreviousWordStart {
2689 ignore_newlines: true,
2690 ignore_brackets: false,
2691 },
2692 window,
2693 cx,
2694 );
2695 });
2696 cx.assert_editor_state("ˇ");
2697}
2698
2699#[gpui::test]
2700async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2701 init_test(cx, |_| {});
2702
2703 let language = Arc::new(
2704 Language::new(
2705 LanguageConfig {
2706 brackets: BracketPairConfig {
2707 pairs: vec![
2708 BracketPair {
2709 start: "\"".to_string(),
2710 end: "\"".to_string(),
2711 close: true,
2712 surround: true,
2713 newline: false,
2714 },
2715 BracketPair {
2716 start: "(".to_string(),
2717 end: ")".to_string(),
2718 close: true,
2719 surround: true,
2720 newline: true,
2721 },
2722 ],
2723 ..BracketPairConfig::default()
2724 },
2725 ..LanguageConfig::default()
2726 },
2727 Some(tree_sitter_rust::LANGUAGE.into()),
2728 )
2729 .with_brackets_query(
2730 r#"
2731 ("(" @open ")" @close)
2732 ("\"" @open "\"" @close)
2733 "#,
2734 )
2735 .unwrap(),
2736 );
2737
2738 let mut cx = EditorTestContext::new(cx).await;
2739 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2740
2741 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2742 cx.update_editor(|editor, window, cx| {
2743 editor.delete_to_previous_word_start(
2744 &DeleteToPreviousWordStart {
2745 ignore_newlines: true,
2746 ignore_brackets: false,
2747 },
2748 window,
2749 cx,
2750 );
2751 });
2752 // Deletion stops before brackets if asked to not ignore them.
2753 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2754 cx.update_editor(|editor, window, cx| {
2755 editor.delete_to_previous_word_start(
2756 &DeleteToPreviousWordStart {
2757 ignore_newlines: true,
2758 ignore_brackets: false,
2759 },
2760 window,
2761 cx,
2762 );
2763 });
2764 // Deletion has to remove a single bracket and then stop again.
2765 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2766
2767 cx.update_editor(|editor, window, cx| {
2768 editor.delete_to_previous_word_start(
2769 &DeleteToPreviousWordStart {
2770 ignore_newlines: true,
2771 ignore_brackets: false,
2772 },
2773 window,
2774 cx,
2775 );
2776 });
2777 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2778
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_previous_word_start(
2781 &DeleteToPreviousWordStart {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2790
2791 cx.update_editor(|editor, window, cx| {
2792 editor.delete_to_previous_word_start(
2793 &DeleteToPreviousWordStart {
2794 ignore_newlines: true,
2795 ignore_brackets: false,
2796 },
2797 window,
2798 cx,
2799 );
2800 });
2801 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2802
2803 cx.update_editor(|editor, window, cx| {
2804 editor.delete_to_next_word_end(
2805 &DeleteToNextWordEnd {
2806 ignore_newlines: true,
2807 ignore_brackets: false,
2808 },
2809 window,
2810 cx,
2811 );
2812 });
2813 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2814 cx.assert_editor_state(r#"ˇ");"#);
2815
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_next_word_end(
2818 &DeleteToNextWordEnd {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 cx.assert_editor_state(r#"ˇ"#);
2827
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state(r#"ˇ"#);
2839
2840 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2841 cx.update_editor(|editor, window, cx| {
2842 editor.delete_to_previous_word_start(
2843 &DeleteToPreviousWordStart {
2844 ignore_newlines: true,
2845 ignore_brackets: true,
2846 },
2847 window,
2848 cx,
2849 );
2850 });
2851 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2852}
2853
2854#[gpui::test]
2855fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2856 init_test(cx, |_| {});
2857
2858 let editor = cx.add_window(|window, cx| {
2859 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2860 build_editor(buffer, window, cx)
2861 });
2862 let del_to_prev_word_start = DeleteToPreviousWordStart {
2863 ignore_newlines: false,
2864 ignore_brackets: false,
2865 };
2866 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2867 ignore_newlines: true,
2868 ignore_brackets: false,
2869 };
2870
2871 _ = editor.update(cx, |editor, window, cx| {
2872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2873 s.select_display_ranges([
2874 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2875 ])
2876 });
2877 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2878 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2879 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2880 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2881 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2882 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2883 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2884 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2885 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2886 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2887 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2888 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2889 });
2890}
2891
2892#[gpui::test]
2893fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2894 init_test(cx, |_| {});
2895
2896 let editor = cx.add_window(|window, cx| {
2897 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2898 build_editor(buffer, window, cx)
2899 });
2900 let del_to_next_word_end = DeleteToNextWordEnd {
2901 ignore_newlines: false,
2902 ignore_brackets: false,
2903 };
2904 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2905 ignore_newlines: true,
2906 ignore_brackets: false,
2907 };
2908
2909 _ = editor.update(cx, |editor, window, cx| {
2910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2911 s.select_display_ranges([
2912 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2913 ])
2914 });
2915 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2916 assert_eq!(
2917 editor.buffer.read(cx).read(cx).text(),
2918 "one\n two\nthree\n four"
2919 );
2920 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2921 assert_eq!(
2922 editor.buffer.read(cx).read(cx).text(),
2923 "\n two\nthree\n four"
2924 );
2925 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2926 assert_eq!(
2927 editor.buffer.read(cx).read(cx).text(),
2928 "two\nthree\n four"
2929 );
2930 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2931 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2932 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2933 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2934 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2935 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2936 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2937 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2938 });
2939}
2940
2941#[gpui::test]
2942fn test_newline(cx: &mut TestAppContext) {
2943 init_test(cx, |_| {});
2944
2945 let editor = cx.add_window(|window, cx| {
2946 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2947 build_editor(buffer, window, cx)
2948 });
2949
2950 _ = editor.update(cx, |editor, window, cx| {
2951 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2952 s.select_display_ranges([
2953 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2954 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2955 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2956 ])
2957 });
2958
2959 editor.newline(&Newline, window, cx);
2960 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2961 });
2962}
2963
2964#[gpui::test]
2965fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2966 init_test(cx, |_| {});
2967
2968 let editor = cx.add_window(|window, cx| {
2969 let buffer = MultiBuffer::build_simple(
2970 "
2971 a
2972 b(
2973 X
2974 )
2975 c(
2976 X
2977 )
2978 "
2979 .unindent()
2980 .as_str(),
2981 cx,
2982 );
2983 let mut editor = build_editor(buffer, window, cx);
2984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2985 s.select_ranges([
2986 Point::new(2, 4)..Point::new(2, 5),
2987 Point::new(5, 4)..Point::new(5, 5),
2988 ])
2989 });
2990 editor
2991 });
2992
2993 _ = editor.update(cx, |editor, window, cx| {
2994 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2995 editor.buffer.update(cx, |buffer, cx| {
2996 buffer.edit(
2997 [
2998 (Point::new(1, 2)..Point::new(3, 0), ""),
2999 (Point::new(4, 2)..Point::new(6, 0), ""),
3000 ],
3001 None,
3002 cx,
3003 );
3004 assert_eq!(
3005 buffer.read(cx).text(),
3006 "
3007 a
3008 b()
3009 c()
3010 "
3011 .unindent()
3012 );
3013 });
3014 assert_eq!(
3015 editor.selections.ranges(cx),
3016 &[
3017 Point::new(1, 2)..Point::new(1, 2),
3018 Point::new(2, 2)..Point::new(2, 2),
3019 ],
3020 );
3021
3022 editor.newline(&Newline, window, cx);
3023 assert_eq!(
3024 editor.text(cx),
3025 "
3026 a
3027 b(
3028 )
3029 c(
3030 )
3031 "
3032 .unindent()
3033 );
3034
3035 // The selections are moved after the inserted newlines
3036 assert_eq!(
3037 editor.selections.ranges(cx),
3038 &[
3039 Point::new(2, 0)..Point::new(2, 0),
3040 Point::new(4, 0)..Point::new(4, 0),
3041 ],
3042 );
3043 });
3044}
3045
3046#[gpui::test]
3047async fn test_newline_above(cx: &mut TestAppContext) {
3048 init_test(cx, |settings| {
3049 settings.defaults.tab_size = NonZeroU32::new(4)
3050 });
3051
3052 let language = Arc::new(
3053 Language::new(
3054 LanguageConfig::default(),
3055 Some(tree_sitter_rust::LANGUAGE.into()),
3056 )
3057 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3058 .unwrap(),
3059 );
3060
3061 let mut cx = EditorTestContext::new(cx).await;
3062 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3063 cx.set_state(indoc! {"
3064 const a: ˇA = (
3065 (ˇ
3066 «const_functionˇ»(ˇ),
3067 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3068 )ˇ
3069 ˇ);ˇ
3070 "});
3071
3072 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3073 cx.assert_editor_state(indoc! {"
3074 ˇ
3075 const a: A = (
3076 ˇ
3077 (
3078 ˇ
3079 ˇ
3080 const_function(),
3081 ˇ
3082 ˇ
3083 ˇ
3084 ˇ
3085 something_else,
3086 ˇ
3087 )
3088 ˇ
3089 ˇ
3090 );
3091 "});
3092}
3093
3094#[gpui::test]
3095async fn test_newline_below(cx: &mut TestAppContext) {
3096 init_test(cx, |settings| {
3097 settings.defaults.tab_size = NonZeroU32::new(4)
3098 });
3099
3100 let language = Arc::new(
3101 Language::new(
3102 LanguageConfig::default(),
3103 Some(tree_sitter_rust::LANGUAGE.into()),
3104 )
3105 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3106 .unwrap(),
3107 );
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3111 cx.set_state(indoc! {"
3112 const a: ˇA = (
3113 (ˇ
3114 «const_functionˇ»(ˇ),
3115 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3116 )ˇ
3117 ˇ);ˇ
3118 "});
3119
3120 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 const a: A = (
3123 ˇ
3124 (
3125 ˇ
3126 const_function(),
3127 ˇ
3128 ˇ
3129 something_else,
3130 ˇ
3131 ˇ
3132 ˇ
3133 ˇ
3134 )
3135 ˇ
3136 );
3137 ˇ
3138 ˇ
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_newline_comments(cx: &mut TestAppContext) {
3144 init_test(cx, |settings| {
3145 settings.defaults.tab_size = NonZeroU32::new(4)
3146 });
3147
3148 let language = Arc::new(Language::new(
3149 LanguageConfig {
3150 line_comments: vec!["// ".into()],
3151 ..LanguageConfig::default()
3152 },
3153 None,
3154 ));
3155 {
3156 let mut cx = EditorTestContext::new(cx).await;
3157 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3158 cx.set_state(indoc! {"
3159 // Fooˇ
3160 "});
3161
3162 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3163 cx.assert_editor_state(indoc! {"
3164 // Foo
3165 // ˇ
3166 "});
3167 // Ensure that we add comment prefix when existing line contains space
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(
3170 indoc! {"
3171 // Foo
3172 //s
3173 // ˇ
3174 "}
3175 .replace("s", " ") // s is used as space placeholder to prevent format on save
3176 .as_str(),
3177 );
3178 // Ensure that we add comment prefix when existing line does not contain space
3179 cx.set_state(indoc! {"
3180 // Foo
3181 //ˇ
3182 "});
3183 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3184 cx.assert_editor_state(indoc! {"
3185 // Foo
3186 //
3187 // ˇ
3188 "});
3189 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3190 cx.set_state(indoc! {"
3191 ˇ// Foo
3192 "});
3193 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195
3196 ˇ// Foo
3197 "});
3198 }
3199 // Ensure that comment continuations can be disabled.
3200 update_test_language_settings(cx, |settings| {
3201 settings.defaults.extend_comment_on_newline = Some(false);
3202 });
3203 let mut cx = EditorTestContext::new(cx).await;
3204 cx.set_state(indoc! {"
3205 // Fooˇ
3206 "});
3207 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3208 cx.assert_editor_state(indoc! {"
3209 // Foo
3210 ˇ
3211 "});
3212}
3213
3214#[gpui::test]
3215async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3216 init_test(cx, |settings| {
3217 settings.defaults.tab_size = NonZeroU32::new(4)
3218 });
3219
3220 let language = Arc::new(Language::new(
3221 LanguageConfig {
3222 line_comments: vec!["// ".into(), "/// ".into()],
3223 ..LanguageConfig::default()
3224 },
3225 None,
3226 ));
3227 {
3228 let mut cx = EditorTestContext::new(cx).await;
3229 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3230 cx.set_state(indoc! {"
3231 //ˇ
3232 "});
3233 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3234 cx.assert_editor_state(indoc! {"
3235 //
3236 // ˇ
3237 "});
3238
3239 cx.set_state(indoc! {"
3240 ///ˇ
3241 "});
3242 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 ///
3245 /// ˇ
3246 "});
3247 }
3248}
3249
3250#[gpui::test]
3251async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3252 init_test(cx, |settings| {
3253 settings.defaults.tab_size = NonZeroU32::new(4)
3254 });
3255
3256 let language = Arc::new(
3257 Language::new(
3258 LanguageConfig {
3259 documentation_comment: Some(language::BlockCommentConfig {
3260 start: "/**".into(),
3261 end: "*/".into(),
3262 prefix: "* ".into(),
3263 tab_size: 1,
3264 }),
3265
3266 ..LanguageConfig::default()
3267 },
3268 Some(tree_sitter_rust::LANGUAGE.into()),
3269 )
3270 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3271 .unwrap(),
3272 );
3273
3274 {
3275 let mut cx = EditorTestContext::new(cx).await;
3276 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3277 cx.set_state(indoc! {"
3278 /**ˇ
3279 "});
3280
3281 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3282 cx.assert_editor_state(indoc! {"
3283 /**
3284 * ˇ
3285 "});
3286 // Ensure that if cursor is before the comment start,
3287 // we do not actually insert a comment prefix.
3288 cx.set_state(indoc! {"
3289 ˇ/**
3290 "});
3291 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3292 cx.assert_editor_state(indoc! {"
3293
3294 ˇ/**
3295 "});
3296 // Ensure that if cursor is between it doesn't add comment prefix.
3297 cx.set_state(indoc! {"
3298 /*ˇ*
3299 "});
3300 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 /*
3303 ˇ*
3304 "});
3305 // Ensure that if suffix exists on same line after cursor it adds new line.
3306 cx.set_state(indoc! {"
3307 /**ˇ*/
3308 "});
3309 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 /**
3312 * ˇ
3313 */
3314 "});
3315 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3316 cx.set_state(indoc! {"
3317 /**ˇ */
3318 "});
3319 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 /**
3322 * ˇ
3323 */
3324 "});
3325 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3326 cx.set_state(indoc! {"
3327 /** ˇ*/
3328 "});
3329 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3330 cx.assert_editor_state(
3331 indoc! {"
3332 /**s
3333 * ˇ
3334 */
3335 "}
3336 .replace("s", " ") // s is used as space placeholder to prevent format on save
3337 .as_str(),
3338 );
3339 // Ensure that delimiter space is preserved when newline on already
3340 // spaced delimiter.
3341 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3342 cx.assert_editor_state(
3343 indoc! {"
3344 /**s
3345 *s
3346 * ˇ
3347 */
3348 "}
3349 .replace("s", " ") // s is used as space placeholder to prevent format on save
3350 .as_str(),
3351 );
3352 // Ensure that delimiter space is preserved when space is not
3353 // on existing delimiter.
3354 cx.set_state(indoc! {"
3355 /**
3356 *ˇ
3357 */
3358 "});
3359 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3360 cx.assert_editor_state(indoc! {"
3361 /**
3362 *
3363 * ˇ
3364 */
3365 "});
3366 // Ensure that if suffix exists on same line after cursor it
3367 // doesn't add extra new line if prefix is not on same line.
3368 cx.set_state(indoc! {"
3369 /**
3370 ˇ*/
3371 "});
3372 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 /**
3375
3376 ˇ*/
3377 "});
3378 // Ensure that it detects suffix after existing prefix.
3379 cx.set_state(indoc! {"
3380 /**ˇ/
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 /**
3385 ˇ/
3386 "});
3387 // Ensure that if suffix exists on same line before
3388 // cursor it does not add comment prefix.
3389 cx.set_state(indoc! {"
3390 /** */ˇ
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 /** */
3395 ˇ
3396 "});
3397 // Ensure that if suffix exists on same line before
3398 // cursor it does not add comment prefix.
3399 cx.set_state(indoc! {"
3400 /**
3401 *
3402 */ˇ
3403 "});
3404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406 /**
3407 *
3408 */
3409 ˇ
3410 "});
3411
3412 // Ensure that inline comment followed by code
3413 // doesn't add comment prefix on newline
3414 cx.set_state(indoc! {"
3415 /** */ textˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 /** */ text
3420 ˇ
3421 "});
3422
3423 // Ensure that text after comment end tag
3424 // doesn't add comment prefix on newline
3425 cx.set_state(indoc! {"
3426 /**
3427 *
3428 */ˇtext
3429 "});
3430 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 /**
3433 *
3434 */
3435 ˇtext
3436 "});
3437
3438 // Ensure if not comment block it doesn't
3439 // add comment prefix on newline
3440 cx.set_state(indoc! {"
3441 * textˇ
3442 "});
3443 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 * text
3446 ˇ
3447 "});
3448 }
3449 // Ensure that comment continuations can be disabled.
3450 update_test_language_settings(cx, |settings| {
3451 settings.defaults.extend_comment_on_newline = Some(false);
3452 });
3453 let mut cx = EditorTestContext::new(cx).await;
3454 cx.set_state(indoc! {"
3455 /**ˇ
3456 "});
3457 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 /**
3460 ˇ
3461 "});
3462}
3463
3464#[gpui::test]
3465async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3466 init_test(cx, |settings| {
3467 settings.defaults.tab_size = NonZeroU32::new(4)
3468 });
3469
3470 let lua_language = Arc::new(Language::new(
3471 LanguageConfig {
3472 line_comments: vec!["--".into()],
3473 block_comment: Some(language::BlockCommentConfig {
3474 start: "--[[".into(),
3475 prefix: "".into(),
3476 end: "]]".into(),
3477 tab_size: 0,
3478 }),
3479 ..LanguageConfig::default()
3480 },
3481 None,
3482 ));
3483
3484 let mut cx = EditorTestContext::new(cx).await;
3485 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3486
3487 // Line with line comment should extend
3488 cx.set_state(indoc! {"
3489 --ˇ
3490 "});
3491 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3492 cx.assert_editor_state(indoc! {"
3493 --
3494 --ˇ
3495 "});
3496
3497 // Line with block comment that matches line comment should not extend
3498 cx.set_state(indoc! {"
3499 --[[ˇ
3500 "});
3501 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3502 cx.assert_editor_state(indoc! {"
3503 --[[
3504 ˇ
3505 "});
3506}
3507
3508#[gpui::test]
3509fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3510 init_test(cx, |_| {});
3511
3512 let editor = cx.add_window(|window, cx| {
3513 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3514 let mut editor = build_editor(buffer, window, cx);
3515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3516 s.select_ranges([3..4, 11..12, 19..20])
3517 });
3518 editor
3519 });
3520
3521 _ = editor.update(cx, |editor, window, cx| {
3522 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3523 editor.buffer.update(cx, |buffer, cx| {
3524 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3525 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3526 });
3527 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3528
3529 editor.insert("Z", window, cx);
3530 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3531
3532 // The selections are moved after the inserted characters
3533 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3534 });
3535}
3536
3537#[gpui::test]
3538async fn test_tab(cx: &mut TestAppContext) {
3539 init_test(cx, |settings| {
3540 settings.defaults.tab_size = NonZeroU32::new(3)
3541 });
3542
3543 let mut cx = EditorTestContext::new(cx).await;
3544 cx.set_state(indoc! {"
3545 ˇabˇc
3546 ˇ🏀ˇ🏀ˇefg
3547 dˇ
3548 "});
3549 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3550 cx.assert_editor_state(indoc! {"
3551 ˇab ˇc
3552 ˇ🏀 ˇ🏀 ˇefg
3553 d ˇ
3554 "});
3555
3556 cx.set_state(indoc! {"
3557 a
3558 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3559 "});
3560 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3561 cx.assert_editor_state(indoc! {"
3562 a
3563 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3564 "});
3565}
3566
3567#[gpui::test]
3568async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3569 init_test(cx, |_| {});
3570
3571 let mut cx = EditorTestContext::new(cx).await;
3572 let language = Arc::new(
3573 Language::new(
3574 LanguageConfig::default(),
3575 Some(tree_sitter_rust::LANGUAGE.into()),
3576 )
3577 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3578 .unwrap(),
3579 );
3580 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3581
3582 // test when all cursors are not at suggested indent
3583 // then simply move to their suggested indent location
3584 cx.set_state(indoc! {"
3585 const a: B = (
3586 c(
3587 ˇ
3588 ˇ )
3589 );
3590 "});
3591 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3592 cx.assert_editor_state(indoc! {"
3593 const a: B = (
3594 c(
3595 ˇ
3596 ˇ)
3597 );
3598 "});
3599
3600 // test cursor already at suggested indent not moving when
3601 // other cursors are yet to reach their suggested indents
3602 cx.set_state(indoc! {"
3603 ˇ
3604 const a: B = (
3605 c(
3606 d(
3607 ˇ
3608 )
3609 ˇ
3610 ˇ )
3611 );
3612 "});
3613 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3614 cx.assert_editor_state(indoc! {"
3615 ˇ
3616 const a: B = (
3617 c(
3618 d(
3619 ˇ
3620 )
3621 ˇ
3622 ˇ)
3623 );
3624 "});
3625 // test when all cursors are at suggested indent then tab is inserted
3626 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3627 cx.assert_editor_state(indoc! {"
3628 ˇ
3629 const a: B = (
3630 c(
3631 d(
3632 ˇ
3633 )
3634 ˇ
3635 ˇ)
3636 );
3637 "});
3638
3639 // test when current indent is less than suggested indent,
3640 // we adjust line to match suggested indent and move cursor to it
3641 //
3642 // when no other cursor is at word boundary, all of them should move
3643 cx.set_state(indoc! {"
3644 const a: B = (
3645 c(
3646 d(
3647 ˇ
3648 ˇ )
3649 ˇ )
3650 );
3651 "});
3652 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3653 cx.assert_editor_state(indoc! {"
3654 const a: B = (
3655 c(
3656 d(
3657 ˇ
3658 ˇ)
3659 ˇ)
3660 );
3661 "});
3662
3663 // test when current indent is less than suggested indent,
3664 // we adjust line to match suggested indent and move cursor to it
3665 //
3666 // when some other cursor is at word boundary, it should not move
3667 cx.set_state(indoc! {"
3668 const a: B = (
3669 c(
3670 d(
3671 ˇ
3672 ˇ )
3673 ˇ)
3674 );
3675 "});
3676 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 const a: B = (
3679 c(
3680 d(
3681 ˇ
3682 ˇ)
3683 ˇ)
3684 );
3685 "});
3686
3687 // test when current indent is more than suggested indent,
3688 // we just move cursor to current indent instead of suggested indent
3689 //
3690 // when no other cursor is at word boundary, all of them should move
3691 cx.set_state(indoc! {"
3692 const a: B = (
3693 c(
3694 d(
3695 ˇ
3696 ˇ )
3697 ˇ )
3698 );
3699 "});
3700 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 const a: B = (
3703 c(
3704 d(
3705 ˇ
3706 ˇ)
3707 ˇ)
3708 );
3709 "});
3710 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3711 cx.assert_editor_state(indoc! {"
3712 const a: B = (
3713 c(
3714 d(
3715 ˇ
3716 ˇ)
3717 ˇ)
3718 );
3719 "});
3720
3721 // test when current indent is more than suggested indent,
3722 // we just move cursor to current indent instead of suggested indent
3723 //
3724 // when some other cursor is at word boundary, it doesn't move
3725 cx.set_state(indoc! {"
3726 const a: B = (
3727 c(
3728 d(
3729 ˇ
3730 ˇ )
3731 ˇ)
3732 );
3733 "});
3734 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3735 cx.assert_editor_state(indoc! {"
3736 const a: B = (
3737 c(
3738 d(
3739 ˇ
3740 ˇ)
3741 ˇ)
3742 );
3743 "});
3744
3745 // handle auto-indent when there are multiple cursors on the same line
3746 cx.set_state(indoc! {"
3747 const a: B = (
3748 c(
3749 ˇ ˇ
3750 ˇ )
3751 );
3752 "});
3753 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3754 cx.assert_editor_state(indoc! {"
3755 const a: B = (
3756 c(
3757 ˇ
3758 ˇ)
3759 );
3760 "});
3761}
3762
3763#[gpui::test]
3764async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3765 init_test(cx, |settings| {
3766 settings.defaults.tab_size = NonZeroU32::new(3)
3767 });
3768
3769 let mut cx = EditorTestContext::new(cx).await;
3770 cx.set_state(indoc! {"
3771 ˇ
3772 \t ˇ
3773 \t ˇ
3774 \t ˇ
3775 \t \t\t \t \t\t \t\t \t \t ˇ
3776 "});
3777
3778 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3779 cx.assert_editor_state(indoc! {"
3780 ˇ
3781 \t ˇ
3782 \t ˇ
3783 \t ˇ
3784 \t \t\t \t \t\t \t\t \t \t ˇ
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3790 init_test(cx, |settings| {
3791 settings.defaults.tab_size = NonZeroU32::new(4)
3792 });
3793
3794 let language = Arc::new(
3795 Language::new(
3796 LanguageConfig::default(),
3797 Some(tree_sitter_rust::LANGUAGE.into()),
3798 )
3799 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3800 .unwrap(),
3801 );
3802
3803 let mut cx = EditorTestContext::new(cx).await;
3804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3805 cx.set_state(indoc! {"
3806 fn a() {
3807 if b {
3808 \t ˇc
3809 }
3810 }
3811 "});
3812
3813 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3814 cx.assert_editor_state(indoc! {"
3815 fn a() {
3816 if b {
3817 ˇc
3818 }
3819 }
3820 "});
3821}
3822
3823#[gpui::test]
3824async fn test_indent_outdent(cx: &mut TestAppContext) {
3825 init_test(cx, |settings| {
3826 settings.defaults.tab_size = NonZeroU32::new(4);
3827 });
3828
3829 let mut cx = EditorTestContext::new(cx).await;
3830
3831 cx.set_state(indoc! {"
3832 «oneˇ» «twoˇ»
3833 three
3834 four
3835 "});
3836 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3837 cx.assert_editor_state(indoc! {"
3838 «oneˇ» «twoˇ»
3839 three
3840 four
3841 "});
3842
3843 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3844 cx.assert_editor_state(indoc! {"
3845 «oneˇ» «twoˇ»
3846 three
3847 four
3848 "});
3849
3850 // select across line ending
3851 cx.set_state(indoc! {"
3852 one two
3853 t«hree
3854 ˇ» four
3855 "});
3856 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3857 cx.assert_editor_state(indoc! {"
3858 one two
3859 t«hree
3860 ˇ» four
3861 "});
3862
3863 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3864 cx.assert_editor_state(indoc! {"
3865 one two
3866 t«hree
3867 ˇ» four
3868 "});
3869
3870 // Ensure that indenting/outdenting works when the cursor is at column 0.
3871 cx.set_state(indoc! {"
3872 one two
3873 ˇthree
3874 four
3875 "});
3876 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3877 cx.assert_editor_state(indoc! {"
3878 one two
3879 ˇthree
3880 four
3881 "});
3882
3883 cx.set_state(indoc! {"
3884 one two
3885 ˇ three
3886 four
3887 "});
3888 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 one two
3891 ˇthree
3892 four
3893 "});
3894}
3895
3896#[gpui::test]
3897async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3898 // This is a regression test for issue #33761
3899 init_test(cx, |_| {});
3900
3901 let mut cx = EditorTestContext::new(cx).await;
3902 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3903 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3904
3905 cx.set_state(
3906 r#"ˇ# ingress:
3907ˇ# api:
3908ˇ# enabled: false
3909ˇ# pathType: Prefix
3910ˇ# console:
3911ˇ# enabled: false
3912ˇ# pathType: Prefix
3913"#,
3914 );
3915
3916 // Press tab to indent all lines
3917 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3918
3919 cx.assert_editor_state(
3920 r#" ˇ# ingress:
3921 ˇ# api:
3922 ˇ# enabled: false
3923 ˇ# pathType: Prefix
3924 ˇ# console:
3925 ˇ# enabled: false
3926 ˇ# pathType: Prefix
3927"#,
3928 );
3929}
3930
3931#[gpui::test]
3932async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3933 // This is a test to make sure our fix for issue #33761 didn't break anything
3934 init_test(cx, |_| {});
3935
3936 let mut cx = EditorTestContext::new(cx).await;
3937 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3938 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3939
3940 cx.set_state(
3941 r#"ˇingress:
3942ˇ api:
3943ˇ enabled: false
3944ˇ pathType: Prefix
3945"#,
3946 );
3947
3948 // Press tab to indent all lines
3949 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3950
3951 cx.assert_editor_state(
3952 r#"ˇingress:
3953 ˇapi:
3954 ˇenabled: false
3955 ˇpathType: Prefix
3956"#,
3957 );
3958}
3959
3960#[gpui::test]
3961async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3962 init_test(cx, |settings| {
3963 settings.defaults.hard_tabs = Some(true);
3964 });
3965
3966 let mut cx = EditorTestContext::new(cx).await;
3967
3968 // select two ranges on one line
3969 cx.set_state(indoc! {"
3970 «oneˇ» «twoˇ»
3971 three
3972 four
3973 "});
3974 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3975 cx.assert_editor_state(indoc! {"
3976 \t«oneˇ» «twoˇ»
3977 three
3978 four
3979 "});
3980 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3981 cx.assert_editor_state(indoc! {"
3982 \t\t«oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 \t«oneˇ» «twoˇ»
3989 three
3990 four
3991 "});
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 «oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998
3999 // select across a line ending
4000 cx.set_state(indoc! {"
4001 one two
4002 t«hree
4003 ˇ»four
4004 "});
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 one two
4008 \tt«hree
4009 ˇ»four
4010 "});
4011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4012 cx.assert_editor_state(indoc! {"
4013 one two
4014 \t\tt«hree
4015 ˇ»four
4016 "});
4017 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 one two
4020 \tt«hree
4021 ˇ»four
4022 "});
4023 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4024 cx.assert_editor_state(indoc! {"
4025 one two
4026 t«hree
4027 ˇ»four
4028 "});
4029
4030 // Ensure that indenting/outdenting works when the cursor is at column 0.
4031 cx.set_state(indoc! {"
4032 one two
4033 ˇthree
4034 four
4035 "});
4036 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4037 cx.assert_editor_state(indoc! {"
4038 one two
4039 ˇthree
4040 four
4041 "});
4042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4043 cx.assert_editor_state(indoc! {"
4044 one two
4045 \tˇthree
4046 four
4047 "});
4048 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 ˇthree
4052 four
4053 "});
4054}
4055
4056#[gpui::test]
4057fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4058 init_test(cx, |settings| {
4059 settings.languages.0.extend([
4060 (
4061 "TOML".into(),
4062 LanguageSettingsContent {
4063 tab_size: NonZeroU32::new(2),
4064 ..Default::default()
4065 },
4066 ),
4067 (
4068 "Rust".into(),
4069 LanguageSettingsContent {
4070 tab_size: NonZeroU32::new(4),
4071 ..Default::default()
4072 },
4073 ),
4074 ]);
4075 });
4076
4077 let toml_language = Arc::new(Language::new(
4078 LanguageConfig {
4079 name: "TOML".into(),
4080 ..Default::default()
4081 },
4082 None,
4083 ));
4084 let rust_language = Arc::new(Language::new(
4085 LanguageConfig {
4086 name: "Rust".into(),
4087 ..Default::default()
4088 },
4089 None,
4090 ));
4091
4092 let toml_buffer =
4093 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4094 let rust_buffer =
4095 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4096 let multibuffer = cx.new(|cx| {
4097 let mut multibuffer = MultiBuffer::new(ReadWrite);
4098 multibuffer.push_excerpts(
4099 toml_buffer.clone(),
4100 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4101 cx,
4102 );
4103 multibuffer.push_excerpts(
4104 rust_buffer.clone(),
4105 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4106 cx,
4107 );
4108 multibuffer
4109 });
4110
4111 cx.add_window(|window, cx| {
4112 let mut editor = build_editor(multibuffer, window, cx);
4113
4114 assert_eq!(
4115 editor.text(cx),
4116 indoc! {"
4117 a = 1
4118 b = 2
4119
4120 const c: usize = 3;
4121 "}
4122 );
4123
4124 select_ranges(
4125 &mut editor,
4126 indoc! {"
4127 «aˇ» = 1
4128 b = 2
4129
4130 «const c:ˇ» usize = 3;
4131 "},
4132 window,
4133 cx,
4134 );
4135
4136 editor.tab(&Tab, window, cx);
4137 assert_text_with_selections(
4138 &mut editor,
4139 indoc! {"
4140 «aˇ» = 1
4141 b = 2
4142
4143 «const c:ˇ» usize = 3;
4144 "},
4145 cx,
4146 );
4147 editor.backtab(&Backtab, window, cx);
4148 assert_text_with_selections(
4149 &mut editor,
4150 indoc! {"
4151 «aˇ» = 1
4152 b = 2
4153
4154 «const c:ˇ» usize = 3;
4155 "},
4156 cx,
4157 );
4158
4159 editor
4160 });
4161}
4162
4163#[gpui::test]
4164async fn test_backspace(cx: &mut TestAppContext) {
4165 init_test(cx, |_| {});
4166
4167 let mut cx = EditorTestContext::new(cx).await;
4168
4169 // Basic backspace
4170 cx.set_state(indoc! {"
4171 onˇe two three
4172 fou«rˇ» five six
4173 seven «ˇeight nine
4174 »ten
4175 "});
4176 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4177 cx.assert_editor_state(indoc! {"
4178 oˇe two three
4179 fouˇ five six
4180 seven ˇten
4181 "});
4182
4183 // Test backspace inside and around indents
4184 cx.set_state(indoc! {"
4185 zero
4186 ˇone
4187 ˇtwo
4188 ˇ ˇ ˇ three
4189 ˇ ˇ four
4190 "});
4191 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4192 cx.assert_editor_state(indoc! {"
4193 zero
4194 ˇone
4195 ˇtwo
4196 ˇ threeˇ four
4197 "});
4198}
4199
4200#[gpui::test]
4201async fn test_delete(cx: &mut TestAppContext) {
4202 init_test(cx, |_| {});
4203
4204 let mut cx = EditorTestContext::new(cx).await;
4205 cx.set_state(indoc! {"
4206 onˇe two three
4207 fou«rˇ» five six
4208 seven «ˇeight nine
4209 »ten
4210 "});
4211 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4212 cx.assert_editor_state(indoc! {"
4213 onˇ two three
4214 fouˇ five six
4215 seven ˇten
4216 "});
4217}
4218
4219#[gpui::test]
4220fn test_delete_line(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let editor = cx.add_window(|window, cx| {
4224 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4225 build_editor(buffer, window, cx)
4226 });
4227 _ = editor.update(cx, |editor, window, cx| {
4228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4229 s.select_display_ranges([
4230 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4231 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4232 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4233 ])
4234 });
4235 editor.delete_line(&DeleteLine, window, cx);
4236 assert_eq!(editor.display_text(cx), "ghi");
4237 assert_eq!(
4238 editor.selections.display_ranges(cx),
4239 vec![
4240 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4241 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4242 ]
4243 );
4244 });
4245
4246 let editor = cx.add_window(|window, cx| {
4247 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4248 build_editor(buffer, window, cx)
4249 });
4250 _ = editor.update(cx, |editor, window, cx| {
4251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4252 s.select_display_ranges([
4253 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4254 ])
4255 });
4256 editor.delete_line(&DeleteLine, window, cx);
4257 assert_eq!(editor.display_text(cx), "ghi\n");
4258 assert_eq!(
4259 editor.selections.display_ranges(cx),
4260 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4261 );
4262 });
4263}
4264
4265#[gpui::test]
4266fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4267 init_test(cx, |_| {});
4268
4269 cx.add_window(|window, cx| {
4270 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4271 let mut editor = build_editor(buffer.clone(), window, cx);
4272 let buffer = buffer.read(cx).as_singleton().unwrap();
4273
4274 assert_eq!(
4275 editor.selections.ranges::<Point>(cx),
4276 &[Point::new(0, 0)..Point::new(0, 0)]
4277 );
4278
4279 // When on single line, replace newline at end by space
4280 editor.join_lines(&JoinLines, window, cx);
4281 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4282 assert_eq!(
4283 editor.selections.ranges::<Point>(cx),
4284 &[Point::new(0, 3)..Point::new(0, 3)]
4285 );
4286
4287 // When multiple lines are selected, remove newlines that are spanned by the selection
4288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4289 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4290 });
4291 editor.join_lines(&JoinLines, window, cx);
4292 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4293 assert_eq!(
4294 editor.selections.ranges::<Point>(cx),
4295 &[Point::new(0, 11)..Point::new(0, 11)]
4296 );
4297
4298 // Undo should be transactional
4299 editor.undo(&Undo, window, cx);
4300 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4301 assert_eq!(
4302 editor.selections.ranges::<Point>(cx),
4303 &[Point::new(0, 5)..Point::new(2, 2)]
4304 );
4305
4306 // When joining an empty line don't insert a space
4307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4308 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4309 });
4310 editor.join_lines(&JoinLines, window, cx);
4311 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4312 assert_eq!(
4313 editor.selections.ranges::<Point>(cx),
4314 [Point::new(2, 3)..Point::new(2, 3)]
4315 );
4316
4317 // We can remove trailing newlines
4318 editor.join_lines(&JoinLines, window, cx);
4319 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4320 assert_eq!(
4321 editor.selections.ranges::<Point>(cx),
4322 [Point::new(2, 3)..Point::new(2, 3)]
4323 );
4324
4325 // We don't blow up on the last line
4326 editor.join_lines(&JoinLines, window, cx);
4327 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4328 assert_eq!(
4329 editor.selections.ranges::<Point>(cx),
4330 [Point::new(2, 3)..Point::new(2, 3)]
4331 );
4332
4333 // reset to test indentation
4334 editor.buffer.update(cx, |buffer, cx| {
4335 buffer.edit(
4336 [
4337 (Point::new(1, 0)..Point::new(1, 2), " "),
4338 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4339 ],
4340 None,
4341 cx,
4342 )
4343 });
4344
4345 // We remove any leading spaces
4346 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4348 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4349 });
4350 editor.join_lines(&JoinLines, window, cx);
4351 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4352
4353 // We don't insert a space for a line containing only spaces
4354 editor.join_lines(&JoinLines, window, cx);
4355 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4356
4357 // We ignore any leading tabs
4358 editor.join_lines(&JoinLines, window, cx);
4359 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4360
4361 editor
4362 });
4363}
4364
4365#[gpui::test]
4366fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4367 init_test(cx, |_| {});
4368
4369 cx.add_window(|window, cx| {
4370 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4371 let mut editor = build_editor(buffer.clone(), window, cx);
4372 let buffer = buffer.read(cx).as_singleton().unwrap();
4373
4374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4375 s.select_ranges([
4376 Point::new(0, 2)..Point::new(1, 1),
4377 Point::new(1, 2)..Point::new(1, 2),
4378 Point::new(3, 1)..Point::new(3, 2),
4379 ])
4380 });
4381
4382 editor.join_lines(&JoinLines, window, cx);
4383 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4384
4385 assert_eq!(
4386 editor.selections.ranges::<Point>(cx),
4387 [
4388 Point::new(0, 7)..Point::new(0, 7),
4389 Point::new(1, 3)..Point::new(1, 3)
4390 ]
4391 );
4392 editor
4393 });
4394}
4395
4396#[gpui::test]
4397async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4398 init_test(cx, |_| {});
4399
4400 let mut cx = EditorTestContext::new(cx).await;
4401
4402 let diff_base = r#"
4403 Line 0
4404 Line 1
4405 Line 2
4406 Line 3
4407 "#
4408 .unindent();
4409
4410 cx.set_state(
4411 &r#"
4412 ˇLine 0
4413 Line 1
4414 Line 2
4415 Line 3
4416 "#
4417 .unindent(),
4418 );
4419
4420 cx.set_head_text(&diff_base);
4421 executor.run_until_parked();
4422
4423 // Join lines
4424 cx.update_editor(|editor, window, cx| {
4425 editor.join_lines(&JoinLines, window, cx);
4426 });
4427 executor.run_until_parked();
4428
4429 cx.assert_editor_state(
4430 &r#"
4431 Line 0ˇ Line 1
4432 Line 2
4433 Line 3
4434 "#
4435 .unindent(),
4436 );
4437 // Join again
4438 cx.update_editor(|editor, window, cx| {
4439 editor.join_lines(&JoinLines, window, cx);
4440 });
4441 executor.run_until_parked();
4442
4443 cx.assert_editor_state(
4444 &r#"
4445 Line 0 Line 1ˇ Line 2
4446 Line 3
4447 "#
4448 .unindent(),
4449 );
4450}
4451
4452#[gpui::test]
4453async fn test_custom_newlines_cause_no_false_positive_diffs(
4454 executor: BackgroundExecutor,
4455 cx: &mut TestAppContext,
4456) {
4457 init_test(cx, |_| {});
4458 let mut cx = EditorTestContext::new(cx).await;
4459 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4460 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4461 executor.run_until_parked();
4462
4463 cx.update_editor(|editor, window, cx| {
4464 let snapshot = editor.snapshot(window, cx);
4465 assert_eq!(
4466 snapshot
4467 .buffer_snapshot
4468 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4469 .collect::<Vec<_>>(),
4470 Vec::new(),
4471 "Should not have any diffs for files with custom newlines"
4472 );
4473 });
4474}
4475
4476#[gpui::test]
4477async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4478 init_test(cx, |_| {});
4479
4480 let mut cx = EditorTestContext::new(cx).await;
4481
4482 // Test sort_lines_case_insensitive()
4483 cx.set_state(indoc! {"
4484 «z
4485 y
4486 x
4487 Z
4488 Y
4489 Xˇ»
4490 "});
4491 cx.update_editor(|e, window, cx| {
4492 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4493 });
4494 cx.assert_editor_state(indoc! {"
4495 «x
4496 X
4497 y
4498 Y
4499 z
4500 Zˇ»
4501 "});
4502
4503 // Test sort_lines_by_length()
4504 //
4505 // Demonstrates:
4506 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4507 // - sort is stable
4508 cx.set_state(indoc! {"
4509 «123
4510 æ
4511 12
4512 ∞
4513 1
4514 æˇ»
4515 "});
4516 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4517 cx.assert_editor_state(indoc! {"
4518 «æ
4519 ∞
4520 1
4521 æ
4522 12
4523 123ˇ»
4524 "});
4525
4526 // Test reverse_lines()
4527 cx.set_state(indoc! {"
4528 «5
4529 4
4530 3
4531 2
4532 1ˇ»
4533 "});
4534 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4535 cx.assert_editor_state(indoc! {"
4536 «1
4537 2
4538 3
4539 4
4540 5ˇ»
4541 "});
4542
4543 // Skip testing shuffle_line()
4544
4545 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4546 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4547
4548 // Don't manipulate when cursor is on single line, but expand the selection
4549 cx.set_state(indoc! {"
4550 ddˇdd
4551 ccc
4552 bb
4553 a
4554 "});
4555 cx.update_editor(|e, window, cx| {
4556 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4557 });
4558 cx.assert_editor_state(indoc! {"
4559 «ddddˇ»
4560 ccc
4561 bb
4562 a
4563 "});
4564
4565 // Basic manipulate case
4566 // Start selection moves to column 0
4567 // End of selection shrinks to fit shorter line
4568 cx.set_state(indoc! {"
4569 dd«d
4570 ccc
4571 bb
4572 aaaaaˇ»
4573 "});
4574 cx.update_editor(|e, window, cx| {
4575 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4576 });
4577 cx.assert_editor_state(indoc! {"
4578 «aaaaa
4579 bb
4580 ccc
4581 dddˇ»
4582 "});
4583
4584 // Manipulate case with newlines
4585 cx.set_state(indoc! {"
4586 dd«d
4587 ccc
4588
4589 bb
4590 aaaaa
4591
4592 ˇ»
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599
4600 aaaaa
4601 bb
4602 ccc
4603 dddˇ»
4604
4605 "});
4606
4607 // Adding new line
4608 cx.set_state(indoc! {"
4609 aa«a
4610 bbˇ»b
4611 "});
4612 cx.update_editor(|e, window, cx| {
4613 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4614 });
4615 cx.assert_editor_state(indoc! {"
4616 «aaa
4617 bbb
4618 added_lineˇ»
4619 "});
4620
4621 // Removing line
4622 cx.set_state(indoc! {"
4623 aa«a
4624 bbbˇ»
4625 "});
4626 cx.update_editor(|e, window, cx| {
4627 e.manipulate_immutable_lines(window, cx, |lines| {
4628 lines.pop();
4629 })
4630 });
4631 cx.assert_editor_state(indoc! {"
4632 «aaaˇ»
4633 "});
4634
4635 // Removing all lines
4636 cx.set_state(indoc! {"
4637 aa«a
4638 bbbˇ»
4639 "});
4640 cx.update_editor(|e, window, cx| {
4641 e.manipulate_immutable_lines(window, cx, |lines| {
4642 lines.drain(..);
4643 })
4644 });
4645 cx.assert_editor_state(indoc! {"
4646 ˇ
4647 "});
4648}
4649
4650#[gpui::test]
4651async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4652 init_test(cx, |_| {});
4653
4654 let mut cx = EditorTestContext::new(cx).await;
4655
4656 // Consider continuous selection as single selection
4657 cx.set_state(indoc! {"
4658 Aaa«aa
4659 cˇ»c«c
4660 bb
4661 aaaˇ»aa
4662 "});
4663 cx.update_editor(|e, window, cx| {
4664 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4665 });
4666 cx.assert_editor_state(indoc! {"
4667 «Aaaaa
4668 ccc
4669 bb
4670 aaaaaˇ»
4671 "});
4672
4673 cx.set_state(indoc! {"
4674 Aaa«aa
4675 cˇ»c«c
4676 bb
4677 aaaˇ»aa
4678 "});
4679 cx.update_editor(|e, window, cx| {
4680 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4681 });
4682 cx.assert_editor_state(indoc! {"
4683 «Aaaaa
4684 ccc
4685 bbˇ»
4686 "});
4687
4688 // Consider non continuous selection as distinct dedup operations
4689 cx.set_state(indoc! {"
4690 «aaaaa
4691 bb
4692 aaaaa
4693 aaaaaˇ»
4694
4695 aaa«aaˇ»
4696 "});
4697 cx.update_editor(|e, window, cx| {
4698 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4699 });
4700 cx.assert_editor_state(indoc! {"
4701 «aaaaa
4702 bbˇ»
4703
4704 «aaaaaˇ»
4705 "});
4706}
4707
4708#[gpui::test]
4709async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4710 init_test(cx, |_| {});
4711
4712 let mut cx = EditorTestContext::new(cx).await;
4713
4714 cx.set_state(indoc! {"
4715 «Aaa
4716 aAa
4717 Aaaˇ»
4718 "});
4719 cx.update_editor(|e, window, cx| {
4720 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4721 });
4722 cx.assert_editor_state(indoc! {"
4723 «Aaa
4724 aAaˇ»
4725 "});
4726
4727 cx.set_state(indoc! {"
4728 «Aaa
4729 aAa
4730 aaAˇ»
4731 "});
4732 cx.update_editor(|e, window, cx| {
4733 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4734 });
4735 cx.assert_editor_state(indoc! {"
4736 «Aaaˇ»
4737 "});
4738}
4739
4740#[gpui::test]
4741async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4742 init_test(cx, |_| {});
4743
4744 let mut cx = EditorTestContext::new(cx).await;
4745
4746 let js_language = Arc::new(Language::new(
4747 LanguageConfig {
4748 name: "JavaScript".into(),
4749 wrap_characters: Some(language::WrapCharactersConfig {
4750 start_prefix: "<".into(),
4751 start_suffix: ">".into(),
4752 end_prefix: "</".into(),
4753 end_suffix: ">".into(),
4754 }),
4755 ..LanguageConfig::default()
4756 },
4757 None,
4758 ));
4759
4760 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4761
4762 cx.set_state(indoc! {"
4763 «testˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4766 cx.assert_editor_state(indoc! {"
4767 <«ˇ»>test</«ˇ»>
4768 "});
4769
4770 cx.set_state(indoc! {"
4771 «test
4772 testˇ»
4773 "});
4774 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4775 cx.assert_editor_state(indoc! {"
4776 <«ˇ»>test
4777 test</«ˇ»>
4778 "});
4779
4780 cx.set_state(indoc! {"
4781 teˇst
4782 "});
4783 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4784 cx.assert_editor_state(indoc! {"
4785 te<«ˇ»></«ˇ»>st
4786 "});
4787}
4788
4789#[gpui::test]
4790async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4791 init_test(cx, |_| {});
4792
4793 let mut cx = EditorTestContext::new(cx).await;
4794
4795 let js_language = Arc::new(Language::new(
4796 LanguageConfig {
4797 name: "JavaScript".into(),
4798 wrap_characters: Some(language::WrapCharactersConfig {
4799 start_prefix: "<".into(),
4800 start_suffix: ">".into(),
4801 end_prefix: "</".into(),
4802 end_suffix: ">".into(),
4803 }),
4804 ..LanguageConfig::default()
4805 },
4806 None,
4807 ));
4808
4809 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4810
4811 cx.set_state(indoc! {"
4812 «testˇ»
4813 «testˇ» «testˇ»
4814 «testˇ»
4815 "});
4816 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4817 cx.assert_editor_state(indoc! {"
4818 <«ˇ»>test</«ˇ»>
4819 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4820 <«ˇ»>test</«ˇ»>
4821 "});
4822
4823 cx.set_state(indoc! {"
4824 «test
4825 testˇ»
4826 «test
4827 testˇ»
4828 "});
4829 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4830 cx.assert_editor_state(indoc! {"
4831 <«ˇ»>test
4832 test</«ˇ»>
4833 <«ˇ»>test
4834 test</«ˇ»>
4835 "});
4836}
4837
4838#[gpui::test]
4839async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4840 init_test(cx, |_| {});
4841
4842 let mut cx = EditorTestContext::new(cx).await;
4843
4844 let plaintext_language = Arc::new(Language::new(
4845 LanguageConfig {
4846 name: "Plain Text".into(),
4847 ..LanguageConfig::default()
4848 },
4849 None,
4850 ));
4851
4852 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4853
4854 cx.set_state(indoc! {"
4855 «testˇ»
4856 "});
4857 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4858 cx.assert_editor_state(indoc! {"
4859 «testˇ»
4860 "});
4861}
4862
4863#[gpui::test]
4864async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4865 init_test(cx, |_| {});
4866
4867 let mut cx = EditorTestContext::new(cx).await;
4868
4869 // Manipulate with multiple selections on a single line
4870 cx.set_state(indoc! {"
4871 dd«dd
4872 cˇ»c«c
4873 bb
4874 aaaˇ»aa
4875 "});
4876 cx.update_editor(|e, window, cx| {
4877 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4878 });
4879 cx.assert_editor_state(indoc! {"
4880 «aaaaa
4881 bb
4882 ccc
4883 ddddˇ»
4884 "});
4885
4886 // Manipulate with multiple disjoin selections
4887 cx.set_state(indoc! {"
4888 5«
4889 4
4890 3
4891 2
4892 1ˇ»
4893
4894 dd«dd
4895 ccc
4896 bb
4897 aaaˇ»aa
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «1
4904 2
4905 3
4906 4
4907 5ˇ»
4908
4909 «aaaaa
4910 bb
4911 ccc
4912 ddddˇ»
4913 "});
4914
4915 // Adding lines on each selection
4916 cx.set_state(indoc! {"
4917 2«
4918 1ˇ»
4919
4920 bb«bb
4921 aaaˇ»aa
4922 "});
4923 cx.update_editor(|e, window, cx| {
4924 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4925 });
4926 cx.assert_editor_state(indoc! {"
4927 «2
4928 1
4929 added lineˇ»
4930
4931 «bbbb
4932 aaaaa
4933 added lineˇ»
4934 "});
4935
4936 // Removing lines on each selection
4937 cx.set_state(indoc! {"
4938 2«
4939 1ˇ»
4940
4941 bb«bb
4942 aaaˇ»aa
4943 "});
4944 cx.update_editor(|e, window, cx| {
4945 e.manipulate_immutable_lines(window, cx, |lines| {
4946 lines.pop();
4947 })
4948 });
4949 cx.assert_editor_state(indoc! {"
4950 «2ˇ»
4951
4952 «bbbbˇ»
4953 "});
4954}
4955
4956#[gpui::test]
4957async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4958 init_test(cx, |settings| {
4959 settings.defaults.tab_size = NonZeroU32::new(3)
4960 });
4961
4962 let mut cx = EditorTestContext::new(cx).await;
4963
4964 // MULTI SELECTION
4965 // Ln.1 "«" tests empty lines
4966 // Ln.9 tests just leading whitespace
4967 cx.set_state(indoc! {"
4968 «
4969 abc // No indentationˇ»
4970 «\tabc // 1 tabˇ»
4971 \t\tabc « ˇ» // 2 tabs
4972 \t ab«c // Tab followed by space
4973 \tabc // Space followed by tab (3 spaces should be the result)
4974 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4975 abˇ»ˇc ˇ ˇ // Already space indented«
4976 \t
4977 \tabc\tdef // Only the leading tab is manipulatedˇ»
4978 "});
4979 cx.update_editor(|e, window, cx| {
4980 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4981 });
4982 cx.assert_editor_state(
4983 indoc! {"
4984 «
4985 abc // No indentation
4986 abc // 1 tab
4987 abc // 2 tabs
4988 abc // Tab followed by space
4989 abc // Space followed by tab (3 spaces should be the result)
4990 abc // Mixed indentation (tab conversion depends on the column)
4991 abc // Already space indented
4992 ·
4993 abc\tdef // Only the leading tab is manipulatedˇ»
4994 "}
4995 .replace("·", "")
4996 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4997 );
4998
4999 // Test on just a few lines, the others should remain unchanged
5000 // Only lines (3, 5, 10, 11) should change
5001 cx.set_state(
5002 indoc! {"
5003 ·
5004 abc // No indentation
5005 \tabcˇ // 1 tab
5006 \t\tabc // 2 tabs
5007 \t abcˇ // Tab followed by space
5008 \tabc // Space followed by tab (3 spaces should be the result)
5009 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5010 abc // Already space indented
5011 «\t
5012 \tabc\tdef // Only the leading tab is manipulatedˇ»
5013 "}
5014 .replace("·", "")
5015 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5016 );
5017 cx.update_editor(|e, window, cx| {
5018 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5019 });
5020 cx.assert_editor_state(
5021 indoc! {"
5022 ·
5023 abc // No indentation
5024 « abc // 1 tabˇ»
5025 \t\tabc // 2 tabs
5026 « abc // Tab followed by spaceˇ»
5027 \tabc // Space followed by tab (3 spaces should be the result)
5028 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5029 abc // Already space indented
5030 « ·
5031 abc\tdef // Only the leading tab is manipulatedˇ»
5032 "}
5033 .replace("·", "")
5034 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5035 );
5036
5037 // SINGLE SELECTION
5038 // Ln.1 "«" tests empty lines
5039 // Ln.9 tests just leading whitespace
5040 cx.set_state(indoc! {"
5041 «
5042 abc // No indentation
5043 \tabc // 1 tab
5044 \t\tabc // 2 tabs
5045 \t abc // Tab followed by space
5046 \tabc // Space followed by tab (3 spaces should be the result)
5047 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5048 abc // Already space indented
5049 \t
5050 \tabc\tdef // Only the leading tab is manipulatedˇ»
5051 "});
5052 cx.update_editor(|e, window, cx| {
5053 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5054 });
5055 cx.assert_editor_state(
5056 indoc! {"
5057 «
5058 abc // No indentation
5059 abc // 1 tab
5060 abc // 2 tabs
5061 abc // Tab followed by space
5062 abc // Space followed by tab (3 spaces should be the result)
5063 abc // Mixed indentation (tab conversion depends on the column)
5064 abc // Already space indented
5065 ·
5066 abc\tdef // Only the leading tab is manipulatedˇ»
5067 "}
5068 .replace("·", "")
5069 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5070 );
5071}
5072
5073#[gpui::test]
5074async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5075 init_test(cx, |settings| {
5076 settings.defaults.tab_size = NonZeroU32::new(3)
5077 });
5078
5079 let mut cx = EditorTestContext::new(cx).await;
5080
5081 // MULTI SELECTION
5082 // Ln.1 "«" tests empty lines
5083 // Ln.11 tests just leading whitespace
5084 cx.set_state(indoc! {"
5085 «
5086 abˇ»ˇc // No indentation
5087 abc ˇ ˇ // 1 space (< 3 so dont convert)
5088 abc « // 2 spaces (< 3 so dont convert)
5089 abc // 3 spaces (convert)
5090 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5091 «\tˇ»\t«\tˇ»abc // Already tab indented
5092 «\t abc // Tab followed by space
5093 \tabc // Space followed by tab (should be consumed due to tab)
5094 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5095 \tˇ» «\t
5096 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5097 "});
5098 cx.update_editor(|e, window, cx| {
5099 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5100 });
5101 cx.assert_editor_state(indoc! {"
5102 «
5103 abc // No indentation
5104 abc // 1 space (< 3 so dont convert)
5105 abc // 2 spaces (< 3 so dont convert)
5106 \tabc // 3 spaces (convert)
5107 \t abc // 5 spaces (1 tab + 2 spaces)
5108 \t\t\tabc // Already tab indented
5109 \t abc // Tab followed by space
5110 \tabc // Space followed by tab (should be consumed due to tab)
5111 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5112 \t\t\t
5113 \tabc \t // Only the leading spaces should be convertedˇ»
5114 "});
5115
5116 // Test on just a few lines, the other should remain unchanged
5117 // Only lines (4, 8, 11, 12) should change
5118 cx.set_state(
5119 indoc! {"
5120 ·
5121 abc // No indentation
5122 abc // 1 space (< 3 so dont convert)
5123 abc // 2 spaces (< 3 so dont convert)
5124 « abc // 3 spaces (convert)ˇ»
5125 abc // 5 spaces (1 tab + 2 spaces)
5126 \t\t\tabc // Already tab indented
5127 \t abc // Tab followed by space
5128 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5129 \t\t \tabc // Mixed indentation
5130 \t \t \t \tabc // Mixed indentation
5131 \t \tˇ
5132 « abc \t // Only the leading spaces should be convertedˇ»
5133 "}
5134 .replace("·", "")
5135 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5136 );
5137 cx.update_editor(|e, window, cx| {
5138 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5139 });
5140 cx.assert_editor_state(
5141 indoc! {"
5142 ·
5143 abc // No indentation
5144 abc // 1 space (< 3 so dont convert)
5145 abc // 2 spaces (< 3 so dont convert)
5146 «\tabc // 3 spaces (convert)ˇ»
5147 abc // 5 spaces (1 tab + 2 spaces)
5148 \t\t\tabc // Already tab indented
5149 \t abc // Tab followed by space
5150 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5151 \t\t \tabc // Mixed indentation
5152 \t \t \t \tabc // Mixed indentation
5153 «\t\t\t
5154 \tabc \t // Only the leading spaces should be convertedˇ»
5155 "}
5156 .replace("·", "")
5157 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5158 );
5159
5160 // SINGLE SELECTION
5161 // Ln.1 "«" tests empty lines
5162 // Ln.11 tests just leading whitespace
5163 cx.set_state(indoc! {"
5164 «
5165 abc // No indentation
5166 abc // 1 space (< 3 so dont convert)
5167 abc // 2 spaces (< 3 so dont convert)
5168 abc // 3 spaces (convert)
5169 abc // 5 spaces (1 tab + 2 spaces)
5170 \t\t\tabc // Already tab indented
5171 \t abc // Tab followed by space
5172 \tabc // Space followed by tab (should be consumed due to tab)
5173 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5174 \t \t
5175 abc \t // Only the leading spaces should be convertedˇ»
5176 "});
5177 cx.update_editor(|e, window, cx| {
5178 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5179 });
5180 cx.assert_editor_state(indoc! {"
5181 «
5182 abc // No indentation
5183 abc // 1 space (< 3 so dont convert)
5184 abc // 2 spaces (< 3 so dont convert)
5185 \tabc // 3 spaces (convert)
5186 \t abc // 5 spaces (1 tab + 2 spaces)
5187 \t\t\tabc // Already tab indented
5188 \t abc // Tab followed by space
5189 \tabc // Space followed by tab (should be consumed due to tab)
5190 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5191 \t\t\t
5192 \tabc \t // Only the leading spaces should be convertedˇ»
5193 "});
5194}
5195
5196#[gpui::test]
5197async fn test_toggle_case(cx: &mut TestAppContext) {
5198 init_test(cx, |_| {});
5199
5200 let mut cx = EditorTestContext::new(cx).await;
5201
5202 // If all lower case -> upper case
5203 cx.set_state(indoc! {"
5204 «hello worldˇ»
5205 "});
5206 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5207 cx.assert_editor_state(indoc! {"
5208 «HELLO WORLDˇ»
5209 "});
5210
5211 // If all upper case -> lower case
5212 cx.set_state(indoc! {"
5213 «HELLO WORLDˇ»
5214 "});
5215 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5216 cx.assert_editor_state(indoc! {"
5217 «hello worldˇ»
5218 "});
5219
5220 // If any upper case characters are identified -> lower case
5221 // This matches JetBrains IDEs
5222 cx.set_state(indoc! {"
5223 «hEllo worldˇ»
5224 "});
5225 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5226 cx.assert_editor_state(indoc! {"
5227 «hello worldˇ»
5228 "});
5229}
5230
5231#[gpui::test]
5232async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5233 init_test(cx, |_| {});
5234
5235 let mut cx = EditorTestContext::new(cx).await;
5236
5237 cx.set_state(indoc! {"
5238 «implement-windows-supportˇ»
5239 "});
5240 cx.update_editor(|e, window, cx| {
5241 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5242 });
5243 cx.assert_editor_state(indoc! {"
5244 «Implement windows supportˇ»
5245 "});
5246}
5247
5248#[gpui::test]
5249async fn test_manipulate_text(cx: &mut TestAppContext) {
5250 init_test(cx, |_| {});
5251
5252 let mut cx = EditorTestContext::new(cx).await;
5253
5254 // Test convert_to_upper_case()
5255 cx.set_state(indoc! {"
5256 «hello worldˇ»
5257 "});
5258 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5259 cx.assert_editor_state(indoc! {"
5260 «HELLO WORLDˇ»
5261 "});
5262
5263 // Test convert_to_lower_case()
5264 cx.set_state(indoc! {"
5265 «HELLO WORLDˇ»
5266 "});
5267 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5268 cx.assert_editor_state(indoc! {"
5269 «hello worldˇ»
5270 "});
5271
5272 // Test multiple line, single selection case
5273 cx.set_state(indoc! {"
5274 «The quick brown
5275 fox jumps over
5276 the lazy dogˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5279 cx.assert_editor_state(indoc! {"
5280 «The Quick Brown
5281 Fox Jumps Over
5282 The Lazy Dogˇ»
5283 "});
5284
5285 // Test multiple line, single selection case
5286 cx.set_state(indoc! {"
5287 «The quick brown
5288 fox jumps over
5289 the lazy dogˇ»
5290 "});
5291 cx.update_editor(|e, window, cx| {
5292 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5293 });
5294 cx.assert_editor_state(indoc! {"
5295 «TheQuickBrown
5296 FoxJumpsOver
5297 TheLazyDogˇ»
5298 "});
5299
5300 // From here on out, test more complex cases of manipulate_text()
5301
5302 // Test no selection case - should affect words cursors are in
5303 // Cursor at beginning, middle, and end of word
5304 cx.set_state(indoc! {"
5305 ˇhello big beauˇtiful worldˇ
5306 "});
5307 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5308 cx.assert_editor_state(indoc! {"
5309 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5310 "});
5311
5312 // Test multiple selections on a single line and across multiple lines
5313 cx.set_state(indoc! {"
5314 «Theˇ» quick «brown
5315 foxˇ» jumps «overˇ»
5316 the «lazyˇ» dog
5317 "});
5318 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5319 cx.assert_editor_state(indoc! {"
5320 «THEˇ» quick «BROWN
5321 FOXˇ» jumps «OVERˇ»
5322 the «LAZYˇ» dog
5323 "});
5324
5325 // Test case where text length grows
5326 cx.set_state(indoc! {"
5327 «tschüߡ»
5328 "});
5329 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5330 cx.assert_editor_state(indoc! {"
5331 «TSCHÜSSˇ»
5332 "});
5333
5334 // Test to make sure we don't crash when text shrinks
5335 cx.set_state(indoc! {"
5336 aaa_bbbˇ
5337 "});
5338 cx.update_editor(|e, window, cx| {
5339 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5340 });
5341 cx.assert_editor_state(indoc! {"
5342 «aaaBbbˇ»
5343 "});
5344
5345 // Test to make sure we all aware of the fact that each word can grow and shrink
5346 // Final selections should be aware of this fact
5347 cx.set_state(indoc! {"
5348 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5349 "});
5350 cx.update_editor(|e, window, cx| {
5351 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5352 });
5353 cx.assert_editor_state(indoc! {"
5354 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5355 "});
5356
5357 cx.set_state(indoc! {"
5358 «hElLo, WoRld!ˇ»
5359 "});
5360 cx.update_editor(|e, window, cx| {
5361 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5362 });
5363 cx.assert_editor_state(indoc! {"
5364 «HeLlO, wOrLD!ˇ»
5365 "});
5366}
5367
5368#[gpui::test]
5369fn test_duplicate_line(cx: &mut TestAppContext) {
5370 init_test(cx, |_| {});
5371
5372 let editor = cx.add_window(|window, cx| {
5373 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5374 build_editor(buffer, window, cx)
5375 });
5376 _ = editor.update(cx, |editor, window, cx| {
5377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5378 s.select_display_ranges([
5379 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5380 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5381 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5382 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5383 ])
5384 });
5385 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5386 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5387 assert_eq!(
5388 editor.selections.display_ranges(cx),
5389 vec![
5390 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5391 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5392 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5393 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5394 ]
5395 );
5396 });
5397
5398 let editor = cx.add_window(|window, cx| {
5399 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5400 build_editor(buffer, window, cx)
5401 });
5402 _ = editor.update(cx, |editor, window, cx| {
5403 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5404 s.select_display_ranges([
5405 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5406 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5407 ])
5408 });
5409 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5410 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5411 assert_eq!(
5412 editor.selections.display_ranges(cx),
5413 vec![
5414 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5415 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5416 ]
5417 );
5418 });
5419
5420 // With `move_upwards` the selections stay in place, except for
5421 // the lines inserted above them
5422 let editor = cx.add_window(|window, cx| {
5423 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5424 build_editor(buffer, window, cx)
5425 });
5426 _ = editor.update(cx, |editor, window, cx| {
5427 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5428 s.select_display_ranges([
5429 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5430 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5431 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5432 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5433 ])
5434 });
5435 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5436 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5437 assert_eq!(
5438 editor.selections.display_ranges(cx),
5439 vec![
5440 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5441 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5442 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5443 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5444 ]
5445 );
5446 });
5447
5448 let editor = cx.add_window(|window, cx| {
5449 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5450 build_editor(buffer, window, cx)
5451 });
5452 _ = editor.update(cx, |editor, window, cx| {
5453 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5454 s.select_display_ranges([
5455 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5456 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5457 ])
5458 });
5459 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5460 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5461 assert_eq!(
5462 editor.selections.display_ranges(cx),
5463 vec![
5464 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5465 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5466 ]
5467 );
5468 });
5469
5470 let editor = cx.add_window(|window, cx| {
5471 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5472 build_editor(buffer, window, cx)
5473 });
5474 _ = editor.update(cx, |editor, window, cx| {
5475 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5476 s.select_display_ranges([
5477 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5478 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5479 ])
5480 });
5481 editor.duplicate_selection(&DuplicateSelection, window, cx);
5482 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5483 assert_eq!(
5484 editor.selections.display_ranges(cx),
5485 vec![
5486 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5487 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5488 ]
5489 );
5490 });
5491}
5492
5493#[gpui::test]
5494fn test_move_line_up_down(cx: &mut TestAppContext) {
5495 init_test(cx, |_| {});
5496
5497 let editor = cx.add_window(|window, cx| {
5498 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5499 build_editor(buffer, window, cx)
5500 });
5501 _ = editor.update(cx, |editor, window, cx| {
5502 editor.fold_creases(
5503 vec![
5504 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5505 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5506 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5507 ],
5508 true,
5509 window,
5510 cx,
5511 );
5512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5513 s.select_display_ranges([
5514 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5515 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5516 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5517 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5518 ])
5519 });
5520 assert_eq!(
5521 editor.display_text(cx),
5522 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5523 );
5524
5525 editor.move_line_up(&MoveLineUp, window, cx);
5526 assert_eq!(
5527 editor.display_text(cx),
5528 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5529 );
5530 assert_eq!(
5531 editor.selections.display_ranges(cx),
5532 vec![
5533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5534 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5535 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5536 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5537 ]
5538 );
5539 });
5540
5541 _ = editor.update(cx, |editor, window, cx| {
5542 editor.move_line_down(&MoveLineDown, window, cx);
5543 assert_eq!(
5544 editor.display_text(cx),
5545 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5546 );
5547 assert_eq!(
5548 editor.selections.display_ranges(cx),
5549 vec![
5550 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5551 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5552 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5553 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5554 ]
5555 );
5556 });
5557
5558 _ = editor.update(cx, |editor, window, cx| {
5559 editor.move_line_down(&MoveLineDown, window, cx);
5560 assert_eq!(
5561 editor.display_text(cx),
5562 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5563 );
5564 assert_eq!(
5565 editor.selections.display_ranges(cx),
5566 vec![
5567 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5568 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5569 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5570 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5571 ]
5572 );
5573 });
5574
5575 _ = editor.update(cx, |editor, window, cx| {
5576 editor.move_line_up(&MoveLineUp, window, cx);
5577 assert_eq!(
5578 editor.display_text(cx),
5579 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5580 );
5581 assert_eq!(
5582 editor.selections.display_ranges(cx),
5583 vec![
5584 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5585 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5586 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5587 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5588 ]
5589 );
5590 });
5591}
5592
5593#[gpui::test]
5594fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5595 init_test(cx, |_| {});
5596 let editor = cx.add_window(|window, cx| {
5597 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5598 build_editor(buffer, window, cx)
5599 });
5600 _ = editor.update(cx, |editor, window, cx| {
5601 editor.fold_creases(
5602 vec![Crease::simple(
5603 Point::new(6, 4)..Point::new(7, 4),
5604 FoldPlaceholder::test(),
5605 )],
5606 true,
5607 window,
5608 cx,
5609 );
5610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5611 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5612 });
5613 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5614 editor.move_line_up(&MoveLineUp, window, cx);
5615 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5616 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5617 });
5618}
5619
5620#[gpui::test]
5621fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5622 init_test(cx, |_| {});
5623
5624 let editor = cx.add_window(|window, cx| {
5625 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5626 build_editor(buffer, window, cx)
5627 });
5628 _ = editor.update(cx, |editor, window, cx| {
5629 let snapshot = editor.buffer.read(cx).snapshot(cx);
5630 editor.insert_blocks(
5631 [BlockProperties {
5632 style: BlockStyle::Fixed,
5633 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5634 height: Some(1),
5635 render: Arc::new(|_| div().into_any()),
5636 priority: 0,
5637 }],
5638 Some(Autoscroll::fit()),
5639 cx,
5640 );
5641 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5642 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5643 });
5644 editor.move_line_down(&MoveLineDown, window, cx);
5645 });
5646}
5647
5648#[gpui::test]
5649async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5650 init_test(cx, |_| {});
5651
5652 let mut cx = EditorTestContext::new(cx).await;
5653 cx.set_state(
5654 &"
5655 ˇzero
5656 one
5657 two
5658 three
5659 four
5660 five
5661 "
5662 .unindent(),
5663 );
5664
5665 // Create a four-line block that replaces three lines of text.
5666 cx.update_editor(|editor, window, cx| {
5667 let snapshot = editor.snapshot(window, cx);
5668 let snapshot = &snapshot.buffer_snapshot;
5669 let placement = BlockPlacement::Replace(
5670 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5671 );
5672 editor.insert_blocks(
5673 [BlockProperties {
5674 placement,
5675 height: Some(4),
5676 style: BlockStyle::Sticky,
5677 render: Arc::new(|_| gpui::div().into_any_element()),
5678 priority: 0,
5679 }],
5680 None,
5681 cx,
5682 );
5683 });
5684
5685 // Move down so that the cursor touches the block.
5686 cx.update_editor(|editor, window, cx| {
5687 editor.move_down(&Default::default(), window, cx);
5688 });
5689 cx.assert_editor_state(
5690 &"
5691 zero
5692 «one
5693 two
5694 threeˇ»
5695 four
5696 five
5697 "
5698 .unindent(),
5699 );
5700
5701 // Move down past the block.
5702 cx.update_editor(|editor, window, cx| {
5703 editor.move_down(&Default::default(), window, cx);
5704 });
5705 cx.assert_editor_state(
5706 &"
5707 zero
5708 one
5709 two
5710 three
5711 ˇfour
5712 five
5713 "
5714 .unindent(),
5715 );
5716}
5717
5718#[gpui::test]
5719fn test_transpose(cx: &mut TestAppContext) {
5720 init_test(cx, |_| {});
5721
5722 _ = cx.add_window(|window, cx| {
5723 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5724 editor.set_style(EditorStyle::default(), window, cx);
5725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5726 s.select_ranges([1..1])
5727 });
5728 editor.transpose(&Default::default(), window, cx);
5729 assert_eq!(editor.text(cx), "bac");
5730 assert_eq!(editor.selections.ranges(cx), [2..2]);
5731
5732 editor.transpose(&Default::default(), window, cx);
5733 assert_eq!(editor.text(cx), "bca");
5734 assert_eq!(editor.selections.ranges(cx), [3..3]);
5735
5736 editor.transpose(&Default::default(), window, cx);
5737 assert_eq!(editor.text(cx), "bac");
5738 assert_eq!(editor.selections.ranges(cx), [3..3]);
5739
5740 editor
5741 });
5742
5743 _ = cx.add_window(|window, cx| {
5744 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5745 editor.set_style(EditorStyle::default(), window, cx);
5746 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5747 s.select_ranges([3..3])
5748 });
5749 editor.transpose(&Default::default(), window, cx);
5750 assert_eq!(editor.text(cx), "acb\nde");
5751 assert_eq!(editor.selections.ranges(cx), [3..3]);
5752
5753 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5754 s.select_ranges([4..4])
5755 });
5756 editor.transpose(&Default::default(), window, cx);
5757 assert_eq!(editor.text(cx), "acbd\ne");
5758 assert_eq!(editor.selections.ranges(cx), [5..5]);
5759
5760 editor.transpose(&Default::default(), window, cx);
5761 assert_eq!(editor.text(cx), "acbde\n");
5762 assert_eq!(editor.selections.ranges(cx), [6..6]);
5763
5764 editor.transpose(&Default::default(), window, cx);
5765 assert_eq!(editor.text(cx), "acbd\ne");
5766 assert_eq!(editor.selections.ranges(cx), [6..6]);
5767
5768 editor
5769 });
5770
5771 _ = cx.add_window(|window, cx| {
5772 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5773 editor.set_style(EditorStyle::default(), window, cx);
5774 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5775 s.select_ranges([1..1, 2..2, 4..4])
5776 });
5777 editor.transpose(&Default::default(), window, cx);
5778 assert_eq!(editor.text(cx), "bacd\ne");
5779 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5780
5781 editor.transpose(&Default::default(), window, cx);
5782 assert_eq!(editor.text(cx), "bcade\n");
5783 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5784
5785 editor.transpose(&Default::default(), window, cx);
5786 assert_eq!(editor.text(cx), "bcda\ne");
5787 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5788
5789 editor.transpose(&Default::default(), window, cx);
5790 assert_eq!(editor.text(cx), "bcade\n");
5791 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5792
5793 editor.transpose(&Default::default(), window, cx);
5794 assert_eq!(editor.text(cx), "bcaed\n");
5795 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5796
5797 editor
5798 });
5799
5800 _ = cx.add_window(|window, cx| {
5801 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5802 editor.set_style(EditorStyle::default(), window, cx);
5803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5804 s.select_ranges([4..4])
5805 });
5806 editor.transpose(&Default::default(), window, cx);
5807 assert_eq!(editor.text(cx), "🏀🍐✋");
5808 assert_eq!(editor.selections.ranges(cx), [8..8]);
5809
5810 editor.transpose(&Default::default(), window, cx);
5811 assert_eq!(editor.text(cx), "🏀✋🍐");
5812 assert_eq!(editor.selections.ranges(cx), [11..11]);
5813
5814 editor.transpose(&Default::default(), window, cx);
5815 assert_eq!(editor.text(cx), "🏀🍐✋");
5816 assert_eq!(editor.selections.ranges(cx), [11..11]);
5817
5818 editor
5819 });
5820}
5821
5822#[gpui::test]
5823async fn test_rewrap(cx: &mut TestAppContext) {
5824 init_test(cx, |settings| {
5825 settings.languages.0.extend([
5826 (
5827 "Markdown".into(),
5828 LanguageSettingsContent {
5829 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5830 preferred_line_length: Some(40),
5831 ..Default::default()
5832 },
5833 ),
5834 (
5835 "Plain Text".into(),
5836 LanguageSettingsContent {
5837 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5838 preferred_line_length: Some(40),
5839 ..Default::default()
5840 },
5841 ),
5842 (
5843 "C++".into(),
5844 LanguageSettingsContent {
5845 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5846 preferred_line_length: Some(40),
5847 ..Default::default()
5848 },
5849 ),
5850 (
5851 "Python".into(),
5852 LanguageSettingsContent {
5853 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5854 preferred_line_length: Some(40),
5855 ..Default::default()
5856 },
5857 ),
5858 (
5859 "Rust".into(),
5860 LanguageSettingsContent {
5861 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5862 preferred_line_length: Some(40),
5863 ..Default::default()
5864 },
5865 ),
5866 ])
5867 });
5868
5869 let mut cx = EditorTestContext::new(cx).await;
5870
5871 let cpp_language = Arc::new(Language::new(
5872 LanguageConfig {
5873 name: "C++".into(),
5874 line_comments: vec!["// ".into()],
5875 ..LanguageConfig::default()
5876 },
5877 None,
5878 ));
5879 let python_language = Arc::new(Language::new(
5880 LanguageConfig {
5881 name: "Python".into(),
5882 line_comments: vec!["# ".into()],
5883 ..LanguageConfig::default()
5884 },
5885 None,
5886 ));
5887 let markdown_language = Arc::new(Language::new(
5888 LanguageConfig {
5889 name: "Markdown".into(),
5890 rewrap_prefixes: vec![
5891 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5892 regex::Regex::new("[-*+]\\s+").unwrap(),
5893 ],
5894 ..LanguageConfig::default()
5895 },
5896 None,
5897 ));
5898 let rust_language = Arc::new(
5899 Language::new(
5900 LanguageConfig {
5901 name: "Rust".into(),
5902 line_comments: vec!["// ".into(), "/// ".into()],
5903 ..LanguageConfig::default()
5904 },
5905 Some(tree_sitter_rust::LANGUAGE.into()),
5906 )
5907 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5908 .unwrap(),
5909 );
5910
5911 let plaintext_language = Arc::new(Language::new(
5912 LanguageConfig {
5913 name: "Plain Text".into(),
5914 ..LanguageConfig::default()
5915 },
5916 None,
5917 ));
5918
5919 // Test basic rewrapping of a long line with a cursor
5920 assert_rewrap(
5921 indoc! {"
5922 // ˇThis is a long comment that needs to be wrapped.
5923 "},
5924 indoc! {"
5925 // ˇThis is a long comment that needs to
5926 // be wrapped.
5927 "},
5928 cpp_language.clone(),
5929 &mut cx,
5930 );
5931
5932 // Test rewrapping a full selection
5933 assert_rewrap(
5934 indoc! {"
5935 «// This selected long comment needs to be wrapped.ˇ»"
5936 },
5937 indoc! {"
5938 «// This selected long comment needs to
5939 // be wrapped.ˇ»"
5940 },
5941 cpp_language.clone(),
5942 &mut cx,
5943 );
5944
5945 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5946 assert_rewrap(
5947 indoc! {"
5948 // ˇThis is the first line.
5949 // Thisˇ is the second line.
5950 // This is the thirdˇ line, all part of one paragraph.
5951 "},
5952 indoc! {"
5953 // ˇThis is the first line. Thisˇ is the
5954 // second line. This is the thirdˇ line,
5955 // all part of one paragraph.
5956 "},
5957 cpp_language.clone(),
5958 &mut cx,
5959 );
5960
5961 // Test multiple cursors in different paragraphs trigger separate rewraps
5962 assert_rewrap(
5963 indoc! {"
5964 // ˇThis is the first paragraph, first line.
5965 // ˇThis is the first paragraph, second line.
5966
5967 // ˇThis is the second paragraph, first line.
5968 // ˇThis is the second paragraph, second line.
5969 "},
5970 indoc! {"
5971 // ˇThis is the first paragraph, first
5972 // line. ˇThis is the first paragraph,
5973 // second line.
5974
5975 // ˇThis is the second paragraph, first
5976 // line. ˇThis is the second paragraph,
5977 // second line.
5978 "},
5979 cpp_language.clone(),
5980 &mut cx,
5981 );
5982
5983 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5984 assert_rewrap(
5985 indoc! {"
5986 «// A regular long long comment to be wrapped.
5987 /// A documentation long comment to be wrapped.ˇ»
5988 "},
5989 indoc! {"
5990 «// A regular long long comment to be
5991 // wrapped.
5992 /// A documentation long comment to be
5993 /// wrapped.ˇ»
5994 "},
5995 rust_language.clone(),
5996 &mut cx,
5997 );
5998
5999 // Test that change in indentation level trigger seperate rewraps
6000 assert_rewrap(
6001 indoc! {"
6002 fn foo() {
6003 «// This is a long comment at the base indent.
6004 // This is a long comment at the next indent.ˇ»
6005 }
6006 "},
6007 indoc! {"
6008 fn foo() {
6009 «// This is a long comment at the
6010 // base indent.
6011 // This is a long comment at the
6012 // next indent.ˇ»
6013 }
6014 "},
6015 rust_language.clone(),
6016 &mut cx,
6017 );
6018
6019 // Test that different comment prefix characters (e.g., '#') are handled correctly
6020 assert_rewrap(
6021 indoc! {"
6022 # ˇThis is a long comment using a pound sign.
6023 "},
6024 indoc! {"
6025 # ˇThis is a long comment using a pound
6026 # sign.
6027 "},
6028 python_language,
6029 &mut cx,
6030 );
6031
6032 // Test rewrapping only affects comments, not code even when selected
6033 assert_rewrap(
6034 indoc! {"
6035 «/// This doc comment is long and should be wrapped.
6036 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6037 "},
6038 indoc! {"
6039 «/// This doc comment is long and should
6040 /// be wrapped.
6041 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6042 "},
6043 rust_language.clone(),
6044 &mut cx,
6045 );
6046
6047 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6048 assert_rewrap(
6049 indoc! {"
6050 # Header
6051
6052 A long long long line of markdown text to wrap.ˇ
6053 "},
6054 indoc! {"
6055 # Header
6056
6057 A long long long line of markdown text
6058 to wrap.ˇ
6059 "},
6060 markdown_language.clone(),
6061 &mut cx,
6062 );
6063
6064 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6065 assert_rewrap(
6066 indoc! {"
6067 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6068 2. This is a numbered list item that is very long and needs to be wrapped properly.
6069 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6070 "},
6071 indoc! {"
6072 «1. This is a numbered list item that is
6073 very long and needs to be wrapped
6074 properly.
6075 2. This is a numbered list item that is
6076 very long and needs to be wrapped
6077 properly.
6078 - This is an unordered list item that is
6079 also very long and should not merge
6080 with the numbered item.ˇ»
6081 "},
6082 markdown_language.clone(),
6083 &mut cx,
6084 );
6085
6086 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6087 assert_rewrap(
6088 indoc! {"
6089 «1. This is a numbered list item that is
6090 very long and needs to be wrapped
6091 properly.
6092 2. This is a numbered list item that is
6093 very long and needs to be wrapped
6094 properly.
6095 - This is an unordered list item that is
6096 also very long and should not merge with
6097 the numbered item.ˇ»
6098 "},
6099 indoc! {"
6100 «1. This is a numbered list item that is
6101 very long and needs to be wrapped
6102 properly.
6103 2. This is a numbered list item that is
6104 very long and needs to be wrapped
6105 properly.
6106 - This is an unordered list item that is
6107 also very long and should not merge
6108 with the numbered item.ˇ»
6109 "},
6110 markdown_language.clone(),
6111 &mut cx,
6112 );
6113
6114 // Test that rewrapping maintain indents even when they already exists.
6115 assert_rewrap(
6116 indoc! {"
6117 «1. This is a numbered list
6118 item that is very long and needs to be wrapped properly.
6119 2. This is a numbered list
6120 item that is very long and needs to be wrapped properly.
6121 - This is an unordered list item that is also very long and
6122 should not merge with the numbered item.ˇ»
6123 "},
6124 indoc! {"
6125 «1. This is a numbered list item that is
6126 very long and needs to be wrapped
6127 properly.
6128 2. This is a numbered list item that is
6129 very long and needs to be wrapped
6130 properly.
6131 - This is an unordered list item that is
6132 also very long and should not merge
6133 with the numbered item.ˇ»
6134 "},
6135 markdown_language,
6136 &mut cx,
6137 );
6138
6139 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6140 assert_rewrap(
6141 indoc! {"
6142 ˇThis is a very long line of plain text that will be wrapped.
6143 "},
6144 indoc! {"
6145 ˇThis is a very long line of plain text
6146 that will be wrapped.
6147 "},
6148 plaintext_language.clone(),
6149 &mut cx,
6150 );
6151
6152 // Test that non-commented code acts as a paragraph boundary within a selection
6153 assert_rewrap(
6154 indoc! {"
6155 «// This is the first long comment block to be wrapped.
6156 fn my_func(a: u32);
6157 // This is the second long comment block to be wrapped.ˇ»
6158 "},
6159 indoc! {"
6160 «// This is the first long comment block
6161 // to be wrapped.
6162 fn my_func(a: u32);
6163 // This is the second long comment block
6164 // to be wrapped.ˇ»
6165 "},
6166 rust_language,
6167 &mut cx,
6168 );
6169
6170 // Test rewrapping multiple selections, including ones with blank lines or tabs
6171 assert_rewrap(
6172 indoc! {"
6173 «ˇThis is a very long line that will be wrapped.
6174
6175 This is another paragraph in the same selection.»
6176
6177 «\tThis is a very long indented line that will be wrapped.ˇ»
6178 "},
6179 indoc! {"
6180 «ˇThis is a very long line that will be
6181 wrapped.
6182
6183 This is another paragraph in the same
6184 selection.»
6185
6186 «\tThis is a very long indented line
6187 \tthat will be wrapped.ˇ»
6188 "},
6189 plaintext_language,
6190 &mut cx,
6191 );
6192
6193 // Test that an empty comment line acts as a paragraph boundary
6194 assert_rewrap(
6195 indoc! {"
6196 // ˇThis is a long comment that will be wrapped.
6197 //
6198 // And this is another long comment that will also be wrapped.ˇ
6199 "},
6200 indoc! {"
6201 // ˇThis is a long comment that will be
6202 // wrapped.
6203 //
6204 // And this is another long comment that
6205 // will also be wrapped.ˇ
6206 "},
6207 cpp_language,
6208 &mut cx,
6209 );
6210
6211 #[track_caller]
6212 fn assert_rewrap(
6213 unwrapped_text: &str,
6214 wrapped_text: &str,
6215 language: Arc<Language>,
6216 cx: &mut EditorTestContext,
6217 ) {
6218 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6219 cx.set_state(unwrapped_text);
6220 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6221 cx.assert_editor_state(wrapped_text);
6222 }
6223}
6224
6225#[gpui::test]
6226async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6227 init_test(cx, |settings| {
6228 settings.languages.0.extend([(
6229 "Rust".into(),
6230 LanguageSettingsContent {
6231 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6232 preferred_line_length: Some(40),
6233 ..Default::default()
6234 },
6235 )])
6236 });
6237
6238 let mut cx = EditorTestContext::new(cx).await;
6239
6240 let rust_lang = Arc::new(
6241 Language::new(
6242 LanguageConfig {
6243 name: "Rust".into(),
6244 line_comments: vec!["// ".into()],
6245 block_comment: Some(BlockCommentConfig {
6246 start: "/*".into(),
6247 end: "*/".into(),
6248 prefix: "* ".into(),
6249 tab_size: 1,
6250 }),
6251 documentation_comment: Some(BlockCommentConfig {
6252 start: "/**".into(),
6253 end: "*/".into(),
6254 prefix: "* ".into(),
6255 tab_size: 1,
6256 }),
6257
6258 ..LanguageConfig::default()
6259 },
6260 Some(tree_sitter_rust::LANGUAGE.into()),
6261 )
6262 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6263 .unwrap(),
6264 );
6265
6266 // regular block comment
6267 assert_rewrap(
6268 indoc! {"
6269 /*
6270 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6271 */
6272 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6273 "},
6274 indoc! {"
6275 /*
6276 *ˇ Lorem ipsum dolor sit amet,
6277 * consectetur adipiscing elit.
6278 */
6279 /*
6280 *ˇ Lorem ipsum dolor sit amet,
6281 * consectetur adipiscing elit.
6282 */
6283 "},
6284 rust_lang.clone(),
6285 &mut cx,
6286 );
6287
6288 // indent is respected
6289 assert_rewrap(
6290 indoc! {"
6291 {}
6292 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6293 "},
6294 indoc! {"
6295 {}
6296 /*
6297 *ˇ Lorem ipsum dolor sit amet,
6298 * consectetur adipiscing elit.
6299 */
6300 "},
6301 rust_lang.clone(),
6302 &mut cx,
6303 );
6304
6305 // short block comments with inline delimiters
6306 assert_rewrap(
6307 indoc! {"
6308 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6309 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6310 */
6311 /*
6312 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6313 "},
6314 indoc! {"
6315 /*
6316 *ˇ Lorem ipsum dolor sit amet,
6317 * consectetur adipiscing elit.
6318 */
6319 /*
6320 *ˇ Lorem ipsum dolor sit amet,
6321 * consectetur adipiscing elit.
6322 */
6323 /*
6324 *ˇ Lorem ipsum dolor sit amet,
6325 * consectetur adipiscing elit.
6326 */
6327 "},
6328 rust_lang.clone(),
6329 &mut cx,
6330 );
6331
6332 // multiline block comment with inline start/end delimiters
6333 assert_rewrap(
6334 indoc! {"
6335 /*ˇ Lorem ipsum dolor sit amet,
6336 * consectetur adipiscing elit. */
6337 "},
6338 indoc! {"
6339 /*
6340 *ˇ Lorem ipsum dolor sit amet,
6341 * consectetur adipiscing elit.
6342 */
6343 "},
6344 rust_lang.clone(),
6345 &mut cx,
6346 );
6347
6348 // block comment rewrap still respects paragraph bounds
6349 assert_rewrap(
6350 indoc! {"
6351 /*
6352 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6353 *
6354 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6355 */
6356 "},
6357 indoc! {"
6358 /*
6359 *ˇ Lorem ipsum dolor sit amet,
6360 * consectetur adipiscing elit.
6361 *
6362 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6363 */
6364 "},
6365 rust_lang.clone(),
6366 &mut cx,
6367 );
6368
6369 // documentation comments
6370 assert_rewrap(
6371 indoc! {"
6372 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6373 /**
6374 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6375 */
6376 "},
6377 indoc! {"
6378 /**
6379 *ˇ Lorem ipsum dolor sit amet,
6380 * consectetur adipiscing elit.
6381 */
6382 /**
6383 *ˇ Lorem ipsum dolor sit amet,
6384 * consectetur adipiscing elit.
6385 */
6386 "},
6387 rust_lang.clone(),
6388 &mut cx,
6389 );
6390
6391 // different, adjacent comments
6392 assert_rewrap(
6393 indoc! {"
6394 /**
6395 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6396 */
6397 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6398 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6399 "},
6400 indoc! {"
6401 /**
6402 *ˇ Lorem ipsum dolor sit amet,
6403 * consectetur adipiscing elit.
6404 */
6405 /*
6406 *ˇ Lorem ipsum dolor sit amet,
6407 * consectetur adipiscing elit.
6408 */
6409 //ˇ Lorem ipsum dolor sit amet,
6410 // consectetur adipiscing elit.
6411 "},
6412 rust_lang.clone(),
6413 &mut cx,
6414 );
6415
6416 // selection w/ single short block comment
6417 assert_rewrap(
6418 indoc! {"
6419 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6420 "},
6421 indoc! {"
6422 «/*
6423 * Lorem ipsum dolor sit amet,
6424 * consectetur adipiscing elit.
6425 */ˇ»
6426 "},
6427 rust_lang.clone(),
6428 &mut cx,
6429 );
6430
6431 // rewrapping a single comment w/ abutting comments
6432 assert_rewrap(
6433 indoc! {"
6434 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6435 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6436 "},
6437 indoc! {"
6438 /*
6439 * ˇLorem ipsum dolor sit amet,
6440 * consectetur adipiscing elit.
6441 */
6442 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6443 "},
6444 rust_lang.clone(),
6445 &mut cx,
6446 );
6447
6448 // selection w/ non-abutting short block comments
6449 assert_rewrap(
6450 indoc! {"
6451 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6452
6453 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6454 "},
6455 indoc! {"
6456 «/*
6457 * Lorem ipsum dolor sit amet,
6458 * consectetur adipiscing elit.
6459 */
6460
6461 /*
6462 * Lorem ipsum dolor sit amet,
6463 * consectetur adipiscing elit.
6464 */ˇ»
6465 "},
6466 rust_lang.clone(),
6467 &mut cx,
6468 );
6469
6470 // selection of multiline block comments
6471 assert_rewrap(
6472 indoc! {"
6473 «/* Lorem ipsum dolor sit amet,
6474 * consectetur adipiscing elit. */ˇ»
6475 "},
6476 indoc! {"
6477 «/*
6478 * Lorem ipsum dolor sit amet,
6479 * consectetur adipiscing elit.
6480 */ˇ»
6481 "},
6482 rust_lang.clone(),
6483 &mut cx,
6484 );
6485
6486 // partial selection of multiline block comments
6487 assert_rewrap(
6488 indoc! {"
6489 «/* Lorem ipsum dolor sit amet,ˇ»
6490 * consectetur adipiscing elit. */
6491 /* Lorem ipsum dolor sit amet,
6492 «* consectetur adipiscing elit. */ˇ»
6493 "},
6494 indoc! {"
6495 «/*
6496 * Lorem ipsum dolor sit amet,ˇ»
6497 * consectetur adipiscing elit. */
6498 /* Lorem ipsum dolor sit amet,
6499 «* consectetur adipiscing elit.
6500 */ˇ»
6501 "},
6502 rust_lang.clone(),
6503 &mut cx,
6504 );
6505
6506 // selection w/ abutting short block comments
6507 // TODO: should not be combined; should rewrap as 2 comments
6508 assert_rewrap(
6509 indoc! {"
6510 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6511 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6512 "},
6513 // desired behavior:
6514 // indoc! {"
6515 // «/*
6516 // * Lorem ipsum dolor sit amet,
6517 // * consectetur adipiscing elit.
6518 // */
6519 // /*
6520 // * Lorem ipsum dolor sit amet,
6521 // * consectetur adipiscing elit.
6522 // */ˇ»
6523 // "},
6524 // actual behaviour:
6525 indoc! {"
6526 «/*
6527 * Lorem ipsum dolor sit amet,
6528 * consectetur adipiscing elit. Lorem
6529 * ipsum dolor sit amet, consectetur
6530 * adipiscing elit.
6531 */ˇ»
6532 "},
6533 rust_lang.clone(),
6534 &mut cx,
6535 );
6536
6537 // TODO: same as above, but with delimiters on separate line
6538 // assert_rewrap(
6539 // indoc! {"
6540 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6541 // */
6542 // /*
6543 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6544 // "},
6545 // // desired:
6546 // // indoc! {"
6547 // // «/*
6548 // // * Lorem ipsum dolor sit amet,
6549 // // * consectetur adipiscing elit.
6550 // // */
6551 // // /*
6552 // // * Lorem ipsum dolor sit amet,
6553 // // * consectetur adipiscing elit.
6554 // // */ˇ»
6555 // // "},
6556 // // actual: (but with trailing w/s on the empty lines)
6557 // indoc! {"
6558 // «/*
6559 // * Lorem ipsum dolor sit amet,
6560 // * consectetur adipiscing elit.
6561 // *
6562 // */
6563 // /*
6564 // *
6565 // * Lorem ipsum dolor sit amet,
6566 // * consectetur adipiscing elit.
6567 // */ˇ»
6568 // "},
6569 // rust_lang.clone(),
6570 // &mut cx,
6571 // );
6572
6573 // TODO these are unhandled edge cases; not correct, just documenting known issues
6574 assert_rewrap(
6575 indoc! {"
6576 /*
6577 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6578 */
6579 /*
6580 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6581 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6582 "},
6583 // desired:
6584 // indoc! {"
6585 // /*
6586 // *ˇ Lorem ipsum dolor sit amet,
6587 // * consectetur adipiscing elit.
6588 // */
6589 // /*
6590 // *ˇ Lorem ipsum dolor sit amet,
6591 // * consectetur adipiscing elit.
6592 // */
6593 // /*
6594 // *ˇ Lorem ipsum dolor sit amet
6595 // */ /* consectetur adipiscing elit. */
6596 // "},
6597 // actual:
6598 indoc! {"
6599 /*
6600 //ˇ Lorem ipsum dolor sit amet,
6601 // consectetur adipiscing elit.
6602 */
6603 /*
6604 * //ˇ Lorem ipsum dolor sit amet,
6605 * consectetur adipiscing elit.
6606 */
6607 /*
6608 *ˇ Lorem ipsum dolor sit amet */ /*
6609 * consectetur adipiscing elit.
6610 */
6611 "},
6612 rust_lang,
6613 &mut cx,
6614 );
6615
6616 #[track_caller]
6617 fn assert_rewrap(
6618 unwrapped_text: &str,
6619 wrapped_text: &str,
6620 language: Arc<Language>,
6621 cx: &mut EditorTestContext,
6622 ) {
6623 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6624 cx.set_state(unwrapped_text);
6625 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6626 cx.assert_editor_state(wrapped_text);
6627 }
6628}
6629
6630#[gpui::test]
6631async fn test_hard_wrap(cx: &mut TestAppContext) {
6632 init_test(cx, |_| {});
6633 let mut cx = EditorTestContext::new(cx).await;
6634
6635 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6636 cx.update_editor(|editor, _, cx| {
6637 editor.set_hard_wrap(Some(14), cx);
6638 });
6639
6640 cx.set_state(indoc!(
6641 "
6642 one two three ˇ
6643 "
6644 ));
6645 cx.simulate_input("four");
6646 cx.run_until_parked();
6647
6648 cx.assert_editor_state(indoc!(
6649 "
6650 one two three
6651 fourˇ
6652 "
6653 ));
6654
6655 cx.update_editor(|editor, window, cx| {
6656 editor.newline(&Default::default(), window, cx);
6657 });
6658 cx.run_until_parked();
6659 cx.assert_editor_state(indoc!(
6660 "
6661 one two three
6662 four
6663 ˇ
6664 "
6665 ));
6666
6667 cx.simulate_input("five");
6668 cx.run_until_parked();
6669 cx.assert_editor_state(indoc!(
6670 "
6671 one two three
6672 four
6673 fiveˇ
6674 "
6675 ));
6676
6677 cx.update_editor(|editor, window, cx| {
6678 editor.newline(&Default::default(), window, cx);
6679 });
6680 cx.run_until_parked();
6681 cx.simulate_input("# ");
6682 cx.run_until_parked();
6683 cx.assert_editor_state(indoc!(
6684 "
6685 one two three
6686 four
6687 five
6688 # ˇ
6689 "
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.newline(&Default::default(), window, cx);
6694 });
6695 cx.run_until_parked();
6696 cx.assert_editor_state(indoc!(
6697 "
6698 one two three
6699 four
6700 five
6701 #\x20
6702 #ˇ
6703 "
6704 ));
6705
6706 cx.simulate_input(" 6");
6707 cx.run_until_parked();
6708 cx.assert_editor_state(indoc!(
6709 "
6710 one two three
6711 four
6712 five
6713 #
6714 # 6ˇ
6715 "
6716 ));
6717}
6718
6719#[gpui::test]
6720async fn test_clipboard(cx: &mut TestAppContext) {
6721 init_test(cx, |_| {});
6722
6723 let mut cx = EditorTestContext::new(cx).await;
6724
6725 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6726 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6727 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6728
6729 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6730 cx.set_state("two ˇfour ˇsix ˇ");
6731 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6732 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6733
6734 // Paste again but with only two cursors. Since the number of cursors doesn't
6735 // match the number of slices in the clipboard, the entire clipboard text
6736 // is pasted at each cursor.
6737 cx.set_state("ˇtwo one✅ four three six five ˇ");
6738 cx.update_editor(|e, window, cx| {
6739 e.handle_input("( ", window, cx);
6740 e.paste(&Paste, window, cx);
6741 e.handle_input(") ", window, cx);
6742 });
6743 cx.assert_editor_state(
6744 &([
6745 "( one✅ ",
6746 "three ",
6747 "five ) ˇtwo one✅ four three six five ( one✅ ",
6748 "three ",
6749 "five ) ˇ",
6750 ]
6751 .join("\n")),
6752 );
6753
6754 // Cut with three selections, one of which is full-line.
6755 cx.set_state(indoc! {"
6756 1«2ˇ»3
6757 4ˇ567
6758 «8ˇ»9"});
6759 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6760 cx.assert_editor_state(indoc! {"
6761 1ˇ3
6762 ˇ9"});
6763
6764 // Paste with three selections, noticing how the copied selection that was full-line
6765 // gets inserted before the second cursor.
6766 cx.set_state(indoc! {"
6767 1ˇ3
6768 9ˇ
6769 «oˇ»ne"});
6770 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6771 cx.assert_editor_state(indoc! {"
6772 12ˇ3
6773 4567
6774 9ˇ
6775 8ˇne"});
6776
6777 // Copy with a single cursor only, which writes the whole line into the clipboard.
6778 cx.set_state(indoc! {"
6779 The quick brown
6780 fox juˇmps over
6781 the lazy dog"});
6782 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6783 assert_eq!(
6784 cx.read_from_clipboard()
6785 .and_then(|item| item.text().as_deref().map(str::to_string)),
6786 Some("fox jumps over\n".to_string())
6787 );
6788
6789 // Paste with three selections, noticing how the copied full-line selection is inserted
6790 // before the empty selections but replaces the selection that is non-empty.
6791 cx.set_state(indoc! {"
6792 Tˇhe quick brown
6793 «foˇ»x jumps over
6794 tˇhe lazy dog"});
6795 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6796 cx.assert_editor_state(indoc! {"
6797 fox jumps over
6798 Tˇhe quick brown
6799 fox jumps over
6800 ˇx jumps over
6801 fox jumps over
6802 tˇhe lazy dog"});
6803}
6804
6805#[gpui::test]
6806async fn test_copy_trim(cx: &mut TestAppContext) {
6807 init_test(cx, |_| {});
6808
6809 let mut cx = EditorTestContext::new(cx).await;
6810 cx.set_state(
6811 r#" «for selection in selections.iter() {
6812 let mut start = selection.start;
6813 let mut end = selection.end;
6814 let is_entire_line = selection.is_empty();
6815 if is_entire_line {
6816 start = Point::new(start.row, 0);ˇ»
6817 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6818 }
6819 "#,
6820 );
6821 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6822 assert_eq!(
6823 cx.read_from_clipboard()
6824 .and_then(|item| item.text().as_deref().map(str::to_string)),
6825 Some(
6826 "for selection in selections.iter() {
6827 let mut start = selection.start;
6828 let mut end = selection.end;
6829 let is_entire_line = selection.is_empty();
6830 if is_entire_line {
6831 start = Point::new(start.row, 0);"
6832 .to_string()
6833 ),
6834 "Regular copying preserves all indentation selected",
6835 );
6836 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6837 assert_eq!(
6838 cx.read_from_clipboard()
6839 .and_then(|item| item.text().as_deref().map(str::to_string)),
6840 Some(
6841 "for selection in selections.iter() {
6842let mut start = selection.start;
6843let mut end = selection.end;
6844let is_entire_line = selection.is_empty();
6845if is_entire_line {
6846 start = Point::new(start.row, 0);"
6847 .to_string()
6848 ),
6849 "Copying with stripping should strip all leading whitespaces"
6850 );
6851
6852 cx.set_state(
6853 r#" « for selection in selections.iter() {
6854 let mut start = selection.start;
6855 let mut end = selection.end;
6856 let is_entire_line = selection.is_empty();
6857 if is_entire_line {
6858 start = Point::new(start.row, 0);ˇ»
6859 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6860 }
6861 "#,
6862 );
6863 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6864 assert_eq!(
6865 cx.read_from_clipboard()
6866 .and_then(|item| item.text().as_deref().map(str::to_string)),
6867 Some(
6868 " for selection in selections.iter() {
6869 let mut start = selection.start;
6870 let mut end = selection.end;
6871 let is_entire_line = selection.is_empty();
6872 if is_entire_line {
6873 start = Point::new(start.row, 0);"
6874 .to_string()
6875 ),
6876 "Regular copying preserves all indentation selected",
6877 );
6878 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6879 assert_eq!(
6880 cx.read_from_clipboard()
6881 .and_then(|item| item.text().as_deref().map(str::to_string)),
6882 Some(
6883 "for selection in selections.iter() {
6884let mut start = selection.start;
6885let mut end = selection.end;
6886let is_entire_line = selection.is_empty();
6887if is_entire_line {
6888 start = Point::new(start.row, 0);"
6889 .to_string()
6890 ),
6891 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6892 );
6893
6894 cx.set_state(
6895 r#" «ˇ for selection in selections.iter() {
6896 let mut start = selection.start;
6897 let mut end = selection.end;
6898 let is_entire_line = selection.is_empty();
6899 if is_entire_line {
6900 start = Point::new(start.row, 0);»
6901 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6902 }
6903 "#,
6904 );
6905 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6906 assert_eq!(
6907 cx.read_from_clipboard()
6908 .and_then(|item| item.text().as_deref().map(str::to_string)),
6909 Some(
6910 " for selection in selections.iter() {
6911 let mut start = selection.start;
6912 let mut end = selection.end;
6913 let is_entire_line = selection.is_empty();
6914 if is_entire_line {
6915 start = Point::new(start.row, 0);"
6916 .to_string()
6917 ),
6918 "Regular copying for reverse selection works the same",
6919 );
6920 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6921 assert_eq!(
6922 cx.read_from_clipboard()
6923 .and_then(|item| item.text().as_deref().map(str::to_string)),
6924 Some(
6925 "for selection in selections.iter() {
6926let mut start = selection.start;
6927let mut end = selection.end;
6928let is_entire_line = selection.is_empty();
6929if is_entire_line {
6930 start = Point::new(start.row, 0);"
6931 .to_string()
6932 ),
6933 "Copying with stripping for reverse selection works the same"
6934 );
6935
6936 cx.set_state(
6937 r#" for selection «in selections.iter() {
6938 let mut start = selection.start;
6939 let mut end = selection.end;
6940 let is_entire_line = selection.is_empty();
6941 if is_entire_line {
6942 start = Point::new(start.row, 0);ˇ»
6943 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6944 }
6945 "#,
6946 );
6947 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6948 assert_eq!(
6949 cx.read_from_clipboard()
6950 .and_then(|item| item.text().as_deref().map(str::to_string)),
6951 Some(
6952 "in selections.iter() {
6953 let mut start = selection.start;
6954 let mut end = selection.end;
6955 let is_entire_line = selection.is_empty();
6956 if is_entire_line {
6957 start = Point::new(start.row, 0);"
6958 .to_string()
6959 ),
6960 "When selecting past the indent, the copying works as usual",
6961 );
6962 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6963 assert_eq!(
6964 cx.read_from_clipboard()
6965 .and_then(|item| item.text().as_deref().map(str::to_string)),
6966 Some(
6967 "in selections.iter() {
6968 let mut start = selection.start;
6969 let mut end = selection.end;
6970 let is_entire_line = selection.is_empty();
6971 if is_entire_line {
6972 start = Point::new(start.row, 0);"
6973 .to_string()
6974 ),
6975 "When selecting past the indent, nothing is trimmed"
6976 );
6977
6978 cx.set_state(
6979 r#" «for selection in selections.iter() {
6980 let mut start = selection.start;
6981
6982 let mut end = selection.end;
6983 let is_entire_line = selection.is_empty();
6984 if is_entire_line {
6985 start = Point::new(start.row, 0);
6986ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6987 }
6988 "#,
6989 );
6990 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6991 assert_eq!(
6992 cx.read_from_clipboard()
6993 .and_then(|item| item.text().as_deref().map(str::to_string)),
6994 Some(
6995 "for selection in selections.iter() {
6996let mut start = selection.start;
6997
6998let mut end = selection.end;
6999let is_entire_line = selection.is_empty();
7000if is_entire_line {
7001 start = Point::new(start.row, 0);
7002"
7003 .to_string()
7004 ),
7005 "Copying with stripping should ignore empty lines"
7006 );
7007}
7008
7009#[gpui::test]
7010async fn test_paste_multiline(cx: &mut TestAppContext) {
7011 init_test(cx, |_| {});
7012
7013 let mut cx = EditorTestContext::new(cx).await;
7014 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7015
7016 // Cut an indented block, without the leading whitespace.
7017 cx.set_state(indoc! {"
7018 const a: B = (
7019 c(),
7020 «d(
7021 e,
7022 f
7023 )ˇ»
7024 );
7025 "});
7026 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7027 cx.assert_editor_state(indoc! {"
7028 const a: B = (
7029 c(),
7030 ˇ
7031 );
7032 "});
7033
7034 // Paste it at the same position.
7035 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7036 cx.assert_editor_state(indoc! {"
7037 const a: B = (
7038 c(),
7039 d(
7040 e,
7041 f
7042 )ˇ
7043 );
7044 "});
7045
7046 // Paste it at a line with a lower indent level.
7047 cx.set_state(indoc! {"
7048 ˇ
7049 const a: B = (
7050 c(),
7051 );
7052 "});
7053 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7054 cx.assert_editor_state(indoc! {"
7055 d(
7056 e,
7057 f
7058 )ˇ
7059 const a: B = (
7060 c(),
7061 );
7062 "});
7063
7064 // Cut an indented block, with the leading whitespace.
7065 cx.set_state(indoc! {"
7066 const a: B = (
7067 c(),
7068 « d(
7069 e,
7070 f
7071 )
7072 ˇ»);
7073 "});
7074 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7075 cx.assert_editor_state(indoc! {"
7076 const a: B = (
7077 c(),
7078 ˇ);
7079 "});
7080
7081 // Paste it at the same position.
7082 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7083 cx.assert_editor_state(indoc! {"
7084 const a: B = (
7085 c(),
7086 d(
7087 e,
7088 f
7089 )
7090 ˇ);
7091 "});
7092
7093 // Paste it at a line with a higher indent level.
7094 cx.set_state(indoc! {"
7095 const a: B = (
7096 c(),
7097 d(
7098 e,
7099 fˇ
7100 )
7101 );
7102 "});
7103 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7104 cx.assert_editor_state(indoc! {"
7105 const a: B = (
7106 c(),
7107 d(
7108 e,
7109 f d(
7110 e,
7111 f
7112 )
7113 ˇ
7114 )
7115 );
7116 "});
7117
7118 // Copy an indented block, starting mid-line
7119 cx.set_state(indoc! {"
7120 const a: B = (
7121 c(),
7122 somethin«g(
7123 e,
7124 f
7125 )ˇ»
7126 );
7127 "});
7128 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7129
7130 // Paste it on a line with a lower indent level
7131 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7132 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7133 cx.assert_editor_state(indoc! {"
7134 const a: B = (
7135 c(),
7136 something(
7137 e,
7138 f
7139 )
7140 );
7141 g(
7142 e,
7143 f
7144 )ˇ"});
7145}
7146
7147#[gpui::test]
7148async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7149 init_test(cx, |_| {});
7150
7151 cx.write_to_clipboard(ClipboardItem::new_string(
7152 " d(\n e\n );\n".into(),
7153 ));
7154
7155 let mut cx = EditorTestContext::new(cx).await;
7156 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7157
7158 cx.set_state(indoc! {"
7159 fn a() {
7160 b();
7161 if c() {
7162 ˇ
7163 }
7164 }
7165 "});
7166
7167 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7168 cx.assert_editor_state(indoc! {"
7169 fn a() {
7170 b();
7171 if c() {
7172 d(
7173 e
7174 );
7175 ˇ
7176 }
7177 }
7178 "});
7179
7180 cx.set_state(indoc! {"
7181 fn a() {
7182 b();
7183 ˇ
7184 }
7185 "});
7186
7187 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7188 cx.assert_editor_state(indoc! {"
7189 fn a() {
7190 b();
7191 d(
7192 e
7193 );
7194 ˇ
7195 }
7196 "});
7197}
7198
7199#[gpui::test]
7200fn test_select_all(cx: &mut TestAppContext) {
7201 init_test(cx, |_| {});
7202
7203 let editor = cx.add_window(|window, cx| {
7204 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7205 build_editor(buffer, window, cx)
7206 });
7207 _ = editor.update(cx, |editor, window, cx| {
7208 editor.select_all(&SelectAll, window, cx);
7209 assert_eq!(
7210 editor.selections.display_ranges(cx),
7211 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7212 );
7213 });
7214}
7215
7216#[gpui::test]
7217fn test_select_line(cx: &mut TestAppContext) {
7218 init_test(cx, |_| {});
7219
7220 let editor = cx.add_window(|window, cx| {
7221 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7222 build_editor(buffer, window, cx)
7223 });
7224 _ = editor.update(cx, |editor, window, cx| {
7225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7226 s.select_display_ranges([
7227 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7228 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7229 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7230 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7231 ])
7232 });
7233 editor.select_line(&SelectLine, window, cx);
7234 assert_eq!(
7235 editor.selections.display_ranges(cx),
7236 vec![
7237 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7238 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7239 ]
7240 );
7241 });
7242
7243 _ = editor.update(cx, |editor, window, cx| {
7244 editor.select_line(&SelectLine, window, cx);
7245 assert_eq!(
7246 editor.selections.display_ranges(cx),
7247 vec![
7248 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7249 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7250 ]
7251 );
7252 });
7253
7254 _ = editor.update(cx, |editor, window, cx| {
7255 editor.select_line(&SelectLine, window, cx);
7256 assert_eq!(
7257 editor.selections.display_ranges(cx),
7258 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7259 );
7260 });
7261}
7262
7263#[gpui::test]
7264async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7265 init_test(cx, |_| {});
7266 let mut cx = EditorTestContext::new(cx).await;
7267
7268 #[track_caller]
7269 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7270 cx.set_state(initial_state);
7271 cx.update_editor(|e, window, cx| {
7272 e.split_selection_into_lines(&Default::default(), window, cx)
7273 });
7274 cx.assert_editor_state(expected_state);
7275 }
7276
7277 // Selection starts and ends at the middle of lines, left-to-right
7278 test(
7279 &mut cx,
7280 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7281 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7282 );
7283 // Same thing, right-to-left
7284 test(
7285 &mut cx,
7286 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7287 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7288 );
7289
7290 // Whole buffer, left-to-right, last line *doesn't* end with newline
7291 test(
7292 &mut cx,
7293 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7294 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7295 );
7296 // Same thing, right-to-left
7297 test(
7298 &mut cx,
7299 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7300 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7301 );
7302
7303 // Whole buffer, left-to-right, last line ends with newline
7304 test(
7305 &mut cx,
7306 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7307 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7308 );
7309 // Same thing, right-to-left
7310 test(
7311 &mut cx,
7312 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7313 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7314 );
7315
7316 // Starts at the end of a line, ends at the start of another
7317 test(
7318 &mut cx,
7319 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7320 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7321 );
7322}
7323
7324#[gpui::test]
7325async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7326 init_test(cx, |_| {});
7327
7328 let editor = cx.add_window(|window, cx| {
7329 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7330 build_editor(buffer, window, cx)
7331 });
7332
7333 // setup
7334 _ = editor.update(cx, |editor, window, cx| {
7335 editor.fold_creases(
7336 vec![
7337 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7338 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7339 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7340 ],
7341 true,
7342 window,
7343 cx,
7344 );
7345 assert_eq!(
7346 editor.display_text(cx),
7347 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7348 );
7349 });
7350
7351 _ = editor.update(cx, |editor, window, cx| {
7352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7353 s.select_display_ranges([
7354 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7355 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7356 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7357 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7358 ])
7359 });
7360 editor.split_selection_into_lines(&Default::default(), window, cx);
7361 assert_eq!(
7362 editor.display_text(cx),
7363 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7364 );
7365 });
7366 EditorTestContext::for_editor(editor, cx)
7367 .await
7368 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7369
7370 _ = editor.update(cx, |editor, window, cx| {
7371 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7372 s.select_display_ranges([
7373 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7374 ])
7375 });
7376 editor.split_selection_into_lines(&Default::default(), window, cx);
7377 assert_eq!(
7378 editor.display_text(cx),
7379 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7380 );
7381 assert_eq!(
7382 editor.selections.display_ranges(cx),
7383 [
7384 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7385 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7386 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7387 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7388 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7389 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7390 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7391 ]
7392 );
7393 });
7394 EditorTestContext::for_editor(editor, cx)
7395 .await
7396 .assert_editor_state(
7397 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7398 );
7399}
7400
7401#[gpui::test]
7402async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7403 init_test(cx, |_| {});
7404
7405 let mut cx = EditorTestContext::new(cx).await;
7406
7407 cx.set_state(indoc!(
7408 r#"abc
7409 defˇghi
7410
7411 jk
7412 nlmo
7413 "#
7414 ));
7415
7416 cx.update_editor(|editor, window, cx| {
7417 editor.add_selection_above(&Default::default(), window, cx);
7418 });
7419
7420 cx.assert_editor_state(indoc!(
7421 r#"abcˇ
7422 defˇghi
7423
7424 jk
7425 nlmo
7426 "#
7427 ));
7428
7429 cx.update_editor(|editor, window, cx| {
7430 editor.add_selection_above(&Default::default(), window, cx);
7431 });
7432
7433 cx.assert_editor_state(indoc!(
7434 r#"abcˇ
7435 defˇghi
7436
7437 jk
7438 nlmo
7439 "#
7440 ));
7441
7442 cx.update_editor(|editor, window, cx| {
7443 editor.add_selection_below(&Default::default(), window, cx);
7444 });
7445
7446 cx.assert_editor_state(indoc!(
7447 r#"abc
7448 defˇghi
7449
7450 jk
7451 nlmo
7452 "#
7453 ));
7454
7455 cx.update_editor(|editor, window, cx| {
7456 editor.undo_selection(&Default::default(), window, cx);
7457 });
7458
7459 cx.assert_editor_state(indoc!(
7460 r#"abcˇ
7461 defˇghi
7462
7463 jk
7464 nlmo
7465 "#
7466 ));
7467
7468 cx.update_editor(|editor, window, cx| {
7469 editor.redo_selection(&Default::default(), window, cx);
7470 });
7471
7472 cx.assert_editor_state(indoc!(
7473 r#"abc
7474 defˇghi
7475
7476 jk
7477 nlmo
7478 "#
7479 ));
7480
7481 cx.update_editor(|editor, window, cx| {
7482 editor.add_selection_below(&Default::default(), window, cx);
7483 });
7484
7485 cx.assert_editor_state(indoc!(
7486 r#"abc
7487 defˇghi
7488 ˇ
7489 jk
7490 nlmo
7491 "#
7492 ));
7493
7494 cx.update_editor(|editor, window, cx| {
7495 editor.add_selection_below(&Default::default(), window, cx);
7496 });
7497
7498 cx.assert_editor_state(indoc!(
7499 r#"abc
7500 defˇghi
7501 ˇ
7502 jkˇ
7503 nlmo
7504 "#
7505 ));
7506
7507 cx.update_editor(|editor, window, cx| {
7508 editor.add_selection_below(&Default::default(), window, cx);
7509 });
7510
7511 cx.assert_editor_state(indoc!(
7512 r#"abc
7513 defˇghi
7514 ˇ
7515 jkˇ
7516 nlmˇo
7517 "#
7518 ));
7519
7520 cx.update_editor(|editor, window, cx| {
7521 editor.add_selection_below(&Default::default(), window, cx);
7522 });
7523
7524 cx.assert_editor_state(indoc!(
7525 r#"abc
7526 defˇghi
7527 ˇ
7528 jkˇ
7529 nlmˇo
7530 ˇ"#
7531 ));
7532
7533 // change selections
7534 cx.set_state(indoc!(
7535 r#"abc
7536 def«ˇg»hi
7537
7538 jk
7539 nlmo
7540 "#
7541 ));
7542
7543 cx.update_editor(|editor, window, cx| {
7544 editor.add_selection_below(&Default::default(), window, cx);
7545 });
7546
7547 cx.assert_editor_state(indoc!(
7548 r#"abc
7549 def«ˇg»hi
7550
7551 jk
7552 nlm«ˇo»
7553 "#
7554 ));
7555
7556 cx.update_editor(|editor, window, cx| {
7557 editor.add_selection_below(&Default::default(), window, cx);
7558 });
7559
7560 cx.assert_editor_state(indoc!(
7561 r#"abc
7562 def«ˇg»hi
7563
7564 jk
7565 nlm«ˇo»
7566 "#
7567 ));
7568
7569 cx.update_editor(|editor, window, cx| {
7570 editor.add_selection_above(&Default::default(), window, cx);
7571 });
7572
7573 cx.assert_editor_state(indoc!(
7574 r#"abc
7575 def«ˇg»hi
7576
7577 jk
7578 nlmo
7579 "#
7580 ));
7581
7582 cx.update_editor(|editor, window, cx| {
7583 editor.add_selection_above(&Default::default(), window, cx);
7584 });
7585
7586 cx.assert_editor_state(indoc!(
7587 r#"abc
7588 def«ˇg»hi
7589
7590 jk
7591 nlmo
7592 "#
7593 ));
7594
7595 // Change selections again
7596 cx.set_state(indoc!(
7597 r#"a«bc
7598 defgˇ»hi
7599
7600 jk
7601 nlmo
7602 "#
7603 ));
7604
7605 cx.update_editor(|editor, window, cx| {
7606 editor.add_selection_below(&Default::default(), window, cx);
7607 });
7608
7609 cx.assert_editor_state(indoc!(
7610 r#"a«bcˇ»
7611 d«efgˇ»hi
7612
7613 j«kˇ»
7614 nlmo
7615 "#
7616 ));
7617
7618 cx.update_editor(|editor, window, cx| {
7619 editor.add_selection_below(&Default::default(), window, cx);
7620 });
7621 cx.assert_editor_state(indoc!(
7622 r#"a«bcˇ»
7623 d«efgˇ»hi
7624
7625 j«kˇ»
7626 n«lmoˇ»
7627 "#
7628 ));
7629 cx.update_editor(|editor, window, cx| {
7630 editor.add_selection_above(&Default::default(), window, cx);
7631 });
7632
7633 cx.assert_editor_state(indoc!(
7634 r#"a«bcˇ»
7635 d«efgˇ»hi
7636
7637 j«kˇ»
7638 nlmo
7639 "#
7640 ));
7641
7642 // Change selections again
7643 cx.set_state(indoc!(
7644 r#"abc
7645 d«ˇefghi
7646
7647 jk
7648 nlm»o
7649 "#
7650 ));
7651
7652 cx.update_editor(|editor, window, cx| {
7653 editor.add_selection_above(&Default::default(), window, cx);
7654 });
7655
7656 cx.assert_editor_state(indoc!(
7657 r#"a«ˇbc»
7658 d«ˇef»ghi
7659
7660 j«ˇk»
7661 n«ˇlm»o
7662 "#
7663 ));
7664
7665 cx.update_editor(|editor, window, cx| {
7666 editor.add_selection_below(&Default::default(), window, cx);
7667 });
7668
7669 cx.assert_editor_state(indoc!(
7670 r#"abc
7671 d«ˇef»ghi
7672
7673 j«ˇk»
7674 n«ˇlm»o
7675 "#
7676 ));
7677}
7678
7679#[gpui::test]
7680async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7681 init_test(cx, |_| {});
7682 let mut cx = EditorTestContext::new(cx).await;
7683
7684 cx.set_state(indoc!(
7685 r#"line onˇe
7686 liˇne two
7687 line three
7688 line four"#
7689 ));
7690
7691 cx.update_editor(|editor, window, cx| {
7692 editor.add_selection_below(&Default::default(), window, cx);
7693 });
7694
7695 // test multiple cursors expand in the same direction
7696 cx.assert_editor_state(indoc!(
7697 r#"line onˇe
7698 liˇne twˇo
7699 liˇne three
7700 line four"#
7701 ));
7702
7703 cx.update_editor(|editor, window, cx| {
7704 editor.add_selection_below(&Default::default(), window, cx);
7705 });
7706
7707 cx.update_editor(|editor, window, cx| {
7708 editor.add_selection_below(&Default::default(), window, cx);
7709 });
7710
7711 // test multiple cursors expand below overflow
7712 cx.assert_editor_state(indoc!(
7713 r#"line onˇe
7714 liˇne twˇo
7715 liˇne thˇree
7716 liˇne foˇur"#
7717 ));
7718
7719 cx.update_editor(|editor, window, cx| {
7720 editor.add_selection_above(&Default::default(), window, cx);
7721 });
7722
7723 // test multiple cursors retrieves back correctly
7724 cx.assert_editor_state(indoc!(
7725 r#"line onˇe
7726 liˇne twˇo
7727 liˇne thˇree
7728 line four"#
7729 ));
7730
7731 cx.update_editor(|editor, window, cx| {
7732 editor.add_selection_above(&Default::default(), window, cx);
7733 });
7734
7735 cx.update_editor(|editor, window, cx| {
7736 editor.add_selection_above(&Default::default(), window, cx);
7737 });
7738
7739 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7740 cx.assert_editor_state(indoc!(
7741 r#"liˇne onˇe
7742 liˇne two
7743 line three
7744 line four"#
7745 ));
7746
7747 cx.update_editor(|editor, window, cx| {
7748 editor.undo_selection(&Default::default(), window, cx);
7749 });
7750
7751 // test undo
7752 cx.assert_editor_state(indoc!(
7753 r#"line onˇe
7754 liˇne twˇo
7755 line three
7756 line four"#
7757 ));
7758
7759 cx.update_editor(|editor, window, cx| {
7760 editor.redo_selection(&Default::default(), window, cx);
7761 });
7762
7763 // test redo
7764 cx.assert_editor_state(indoc!(
7765 r#"liˇne onˇe
7766 liˇne two
7767 line three
7768 line four"#
7769 ));
7770
7771 cx.set_state(indoc!(
7772 r#"abcd
7773 ef«ghˇ»
7774 ijkl
7775 «mˇ»nop"#
7776 ));
7777
7778 cx.update_editor(|editor, window, cx| {
7779 editor.add_selection_above(&Default::default(), window, cx);
7780 });
7781
7782 // test multiple selections expand in the same direction
7783 cx.assert_editor_state(indoc!(
7784 r#"ab«cdˇ»
7785 ef«ghˇ»
7786 «iˇ»jkl
7787 «mˇ»nop"#
7788 ));
7789
7790 cx.update_editor(|editor, window, cx| {
7791 editor.add_selection_above(&Default::default(), window, cx);
7792 });
7793
7794 // test multiple selection upward overflow
7795 cx.assert_editor_state(indoc!(
7796 r#"ab«cdˇ»
7797 «eˇ»f«ghˇ»
7798 «iˇ»jkl
7799 «mˇ»nop"#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.add_selection_below(&Default::default(), window, cx);
7804 });
7805
7806 // test multiple selection retrieves back correctly
7807 cx.assert_editor_state(indoc!(
7808 r#"abcd
7809 ef«ghˇ»
7810 «iˇ»jkl
7811 «mˇ»nop"#
7812 ));
7813
7814 cx.update_editor(|editor, window, cx| {
7815 editor.add_selection_below(&Default::default(), window, cx);
7816 });
7817
7818 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7819 cx.assert_editor_state(indoc!(
7820 r#"abcd
7821 ef«ghˇ»
7822 ij«klˇ»
7823 «mˇ»nop"#
7824 ));
7825
7826 cx.update_editor(|editor, window, cx| {
7827 editor.undo_selection(&Default::default(), window, cx);
7828 });
7829
7830 // test undo
7831 cx.assert_editor_state(indoc!(
7832 r#"abcd
7833 ef«ghˇ»
7834 «iˇ»jkl
7835 «mˇ»nop"#
7836 ));
7837
7838 cx.update_editor(|editor, window, cx| {
7839 editor.redo_selection(&Default::default(), window, cx);
7840 });
7841
7842 // test redo
7843 cx.assert_editor_state(indoc!(
7844 r#"abcd
7845 ef«ghˇ»
7846 ij«klˇ»
7847 «mˇ»nop"#
7848 ));
7849}
7850
7851#[gpui::test]
7852async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7853 init_test(cx, |_| {});
7854 let mut cx = EditorTestContext::new(cx).await;
7855
7856 cx.set_state(indoc!(
7857 r#"line onˇe
7858 liˇne two
7859 line three
7860 line four"#
7861 ));
7862
7863 cx.update_editor(|editor, window, cx| {
7864 editor.add_selection_below(&Default::default(), window, cx);
7865 editor.add_selection_below(&Default::default(), window, cx);
7866 editor.add_selection_below(&Default::default(), window, cx);
7867 });
7868
7869 // initial state with two multi cursor groups
7870 cx.assert_editor_state(indoc!(
7871 r#"line onˇe
7872 liˇne twˇo
7873 liˇne thˇree
7874 liˇne foˇur"#
7875 ));
7876
7877 // add single cursor in middle - simulate opt click
7878 cx.update_editor(|editor, window, cx| {
7879 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7880 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7881 editor.end_selection(window, cx);
7882 });
7883
7884 cx.assert_editor_state(indoc!(
7885 r#"line onˇe
7886 liˇne twˇo
7887 liˇneˇ thˇree
7888 liˇne foˇur"#
7889 ));
7890
7891 cx.update_editor(|editor, window, cx| {
7892 editor.add_selection_above(&Default::default(), window, cx);
7893 });
7894
7895 // test new added selection expands above and existing selection shrinks
7896 cx.assert_editor_state(indoc!(
7897 r#"line onˇe
7898 liˇneˇ twˇo
7899 liˇneˇ thˇree
7900 line four"#
7901 ));
7902
7903 cx.update_editor(|editor, window, cx| {
7904 editor.add_selection_above(&Default::default(), window, cx);
7905 });
7906
7907 // test new added selection expands above and existing selection shrinks
7908 cx.assert_editor_state(indoc!(
7909 r#"lineˇ onˇe
7910 liˇneˇ twˇo
7911 lineˇ three
7912 line four"#
7913 ));
7914
7915 // intial state with two selection groups
7916 cx.set_state(indoc!(
7917 r#"abcd
7918 ef«ghˇ»
7919 ijkl
7920 «mˇ»nop"#
7921 ));
7922
7923 cx.update_editor(|editor, window, cx| {
7924 editor.add_selection_above(&Default::default(), window, cx);
7925 editor.add_selection_above(&Default::default(), window, cx);
7926 });
7927
7928 cx.assert_editor_state(indoc!(
7929 r#"ab«cdˇ»
7930 «eˇ»f«ghˇ»
7931 «iˇ»jkl
7932 «mˇ»nop"#
7933 ));
7934
7935 // add single selection in middle - simulate opt drag
7936 cx.update_editor(|editor, window, cx| {
7937 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7938 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7939 editor.update_selection(
7940 DisplayPoint::new(DisplayRow(2), 4),
7941 0,
7942 gpui::Point::<f32>::default(),
7943 window,
7944 cx,
7945 );
7946 editor.end_selection(window, cx);
7947 });
7948
7949 cx.assert_editor_state(indoc!(
7950 r#"ab«cdˇ»
7951 «eˇ»f«ghˇ»
7952 «iˇ»jk«lˇ»
7953 «mˇ»nop"#
7954 ));
7955
7956 cx.update_editor(|editor, window, cx| {
7957 editor.add_selection_below(&Default::default(), window, cx);
7958 });
7959
7960 // test new added selection expands below, others shrinks from above
7961 cx.assert_editor_state(indoc!(
7962 r#"abcd
7963 ef«ghˇ»
7964 «iˇ»jk«lˇ»
7965 «mˇ»no«pˇ»"#
7966 ));
7967}
7968
7969#[gpui::test]
7970async fn test_select_next(cx: &mut TestAppContext) {
7971 init_test(cx, |_| {});
7972
7973 let mut cx = EditorTestContext::new(cx).await;
7974 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7975
7976 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7977 .unwrap();
7978 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7979
7980 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7981 .unwrap();
7982 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7983
7984 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7985 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7986
7987 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7988 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7989
7990 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7991 .unwrap();
7992 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7993
7994 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7995 .unwrap();
7996 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7997
7998 // Test selection direction should be preserved
7999 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8000
8001 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8002 .unwrap();
8003 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8004}
8005
8006#[gpui::test]
8007async fn test_select_all_matches(cx: &mut TestAppContext) {
8008 init_test(cx, |_| {});
8009
8010 let mut cx = EditorTestContext::new(cx).await;
8011
8012 // Test caret-only selections
8013 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8014 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8015 .unwrap();
8016 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8017
8018 // Test left-to-right selections
8019 cx.set_state("abc\n«abcˇ»\nabc");
8020 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8021 .unwrap();
8022 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8023
8024 // Test right-to-left selections
8025 cx.set_state("abc\n«ˇabc»\nabc");
8026 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8027 .unwrap();
8028 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8029
8030 // Test selecting whitespace with caret selection
8031 cx.set_state("abc\nˇ abc\nabc");
8032 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8033 .unwrap();
8034 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8035
8036 // Test selecting whitespace with left-to-right selection
8037 cx.set_state("abc\n«ˇ »abc\nabc");
8038 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8039 .unwrap();
8040 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8041
8042 // Test no matches with right-to-left selection
8043 cx.set_state("abc\n« ˇ»abc\nabc");
8044 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8045 .unwrap();
8046 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8047
8048 // Test with a single word and clip_at_line_ends=true (#29823)
8049 cx.set_state("aˇbc");
8050 cx.update_editor(|e, window, cx| {
8051 e.set_clip_at_line_ends(true, cx);
8052 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8053 e.set_clip_at_line_ends(false, cx);
8054 });
8055 cx.assert_editor_state("«abcˇ»");
8056}
8057
8058#[gpui::test]
8059async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8060 init_test(cx, |_| {});
8061
8062 let mut cx = EditorTestContext::new(cx).await;
8063
8064 let large_body_1 = "\nd".repeat(200);
8065 let large_body_2 = "\ne".repeat(200);
8066
8067 cx.set_state(&format!(
8068 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8069 ));
8070 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8071 let scroll_position = editor.scroll_position(cx);
8072 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8073 scroll_position
8074 });
8075
8076 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8077 .unwrap();
8078 cx.assert_editor_state(&format!(
8079 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8080 ));
8081 let scroll_position_after_selection =
8082 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8083 assert_eq!(
8084 initial_scroll_position, scroll_position_after_selection,
8085 "Scroll position should not change after selecting all matches"
8086 );
8087}
8088
8089#[gpui::test]
8090async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8091 init_test(cx, |_| {});
8092
8093 let mut cx = EditorLspTestContext::new_rust(
8094 lsp::ServerCapabilities {
8095 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8096 ..Default::default()
8097 },
8098 cx,
8099 )
8100 .await;
8101
8102 cx.set_state(indoc! {"
8103 line 1
8104 line 2
8105 linˇe 3
8106 line 4
8107 line 5
8108 "});
8109
8110 // Make an edit
8111 cx.update_editor(|editor, window, cx| {
8112 editor.handle_input("X", window, cx);
8113 });
8114
8115 // Move cursor to a different position
8116 cx.update_editor(|editor, window, cx| {
8117 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8118 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8119 });
8120 });
8121
8122 cx.assert_editor_state(indoc! {"
8123 line 1
8124 line 2
8125 linXe 3
8126 line 4
8127 liˇne 5
8128 "});
8129
8130 cx.lsp
8131 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8132 Ok(Some(vec![lsp::TextEdit::new(
8133 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8134 "PREFIX ".to_string(),
8135 )]))
8136 });
8137
8138 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8139 .unwrap()
8140 .await
8141 .unwrap();
8142
8143 cx.assert_editor_state(indoc! {"
8144 PREFIX line 1
8145 line 2
8146 linXe 3
8147 line 4
8148 liˇne 5
8149 "});
8150
8151 // Undo formatting
8152 cx.update_editor(|editor, window, cx| {
8153 editor.undo(&Default::default(), window, cx);
8154 });
8155
8156 // Verify cursor moved back to position after edit
8157 cx.assert_editor_state(indoc! {"
8158 line 1
8159 line 2
8160 linXˇe 3
8161 line 4
8162 line 5
8163 "});
8164}
8165
8166#[gpui::test]
8167async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8168 init_test(cx, |_| {});
8169
8170 let mut cx = EditorTestContext::new(cx).await;
8171
8172 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8173 cx.update_editor(|editor, window, cx| {
8174 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8175 });
8176
8177 cx.set_state(indoc! {"
8178 line 1
8179 line 2
8180 linˇe 3
8181 line 4
8182 line 5
8183 line 6
8184 line 7
8185 line 8
8186 line 9
8187 line 10
8188 "});
8189
8190 let snapshot = cx.buffer_snapshot();
8191 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8192
8193 cx.update(|_, cx| {
8194 provider.update(cx, |provider, _| {
8195 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8196 id: None,
8197 edits: vec![(edit_position..edit_position, "X".into())],
8198 edit_preview: None,
8199 }))
8200 })
8201 });
8202
8203 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8204 cx.update_editor(|editor, window, cx| {
8205 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8206 });
8207
8208 cx.assert_editor_state(indoc! {"
8209 line 1
8210 line 2
8211 lineXˇ 3
8212 line 4
8213 line 5
8214 line 6
8215 line 7
8216 line 8
8217 line 9
8218 line 10
8219 "});
8220
8221 cx.update_editor(|editor, window, cx| {
8222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8223 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8224 });
8225 });
8226
8227 cx.assert_editor_state(indoc! {"
8228 line 1
8229 line 2
8230 lineX 3
8231 line 4
8232 line 5
8233 line 6
8234 line 7
8235 line 8
8236 line 9
8237 liˇne 10
8238 "});
8239
8240 cx.update_editor(|editor, window, cx| {
8241 editor.undo(&Default::default(), window, cx);
8242 });
8243
8244 cx.assert_editor_state(indoc! {"
8245 line 1
8246 line 2
8247 lineˇ 3
8248 line 4
8249 line 5
8250 line 6
8251 line 7
8252 line 8
8253 line 9
8254 line 10
8255 "});
8256}
8257
8258#[gpui::test]
8259async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8260 init_test(cx, |_| {});
8261
8262 let mut cx = EditorTestContext::new(cx).await;
8263 cx.set_state(
8264 r#"let foo = 2;
8265lˇet foo = 2;
8266let fooˇ = 2;
8267let foo = 2;
8268let foo = ˇ2;"#,
8269 );
8270
8271 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8272 .unwrap();
8273 cx.assert_editor_state(
8274 r#"let foo = 2;
8275«letˇ» foo = 2;
8276let «fooˇ» = 2;
8277let foo = 2;
8278let foo = «2ˇ»;"#,
8279 );
8280
8281 // noop for multiple selections with different contents
8282 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8283 .unwrap();
8284 cx.assert_editor_state(
8285 r#"let foo = 2;
8286«letˇ» foo = 2;
8287let «fooˇ» = 2;
8288let foo = 2;
8289let foo = «2ˇ»;"#,
8290 );
8291
8292 // Test last selection direction should be preserved
8293 cx.set_state(
8294 r#"let foo = 2;
8295let foo = 2;
8296let «fooˇ» = 2;
8297let «ˇfoo» = 2;
8298let foo = 2;"#,
8299 );
8300
8301 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8302 .unwrap();
8303 cx.assert_editor_state(
8304 r#"let foo = 2;
8305let foo = 2;
8306let «fooˇ» = 2;
8307let «ˇfoo» = 2;
8308let «ˇfoo» = 2;"#,
8309 );
8310}
8311
8312#[gpui::test]
8313async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8314 init_test(cx, |_| {});
8315
8316 let mut cx =
8317 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8318
8319 cx.assert_editor_state(indoc! {"
8320 ˇbbb
8321 ccc
8322
8323 bbb
8324 ccc
8325 "});
8326 cx.dispatch_action(SelectPrevious::default());
8327 cx.assert_editor_state(indoc! {"
8328 «bbbˇ»
8329 ccc
8330
8331 bbb
8332 ccc
8333 "});
8334 cx.dispatch_action(SelectPrevious::default());
8335 cx.assert_editor_state(indoc! {"
8336 «bbbˇ»
8337 ccc
8338
8339 «bbbˇ»
8340 ccc
8341 "});
8342}
8343
8344#[gpui::test]
8345async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8346 init_test(cx, |_| {});
8347
8348 let mut cx = EditorTestContext::new(cx).await;
8349 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8350
8351 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8352 .unwrap();
8353 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8354
8355 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8356 .unwrap();
8357 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8358
8359 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8360 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8361
8362 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8363 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8364
8365 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8366 .unwrap();
8367 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8368
8369 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8370 .unwrap();
8371 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8372}
8373
8374#[gpui::test]
8375async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8376 init_test(cx, |_| {});
8377
8378 let mut cx = EditorTestContext::new(cx).await;
8379 cx.set_state("aˇ");
8380
8381 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8382 .unwrap();
8383 cx.assert_editor_state("«aˇ»");
8384 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8385 .unwrap();
8386 cx.assert_editor_state("«aˇ»");
8387}
8388
8389#[gpui::test]
8390async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8391 init_test(cx, |_| {});
8392
8393 let mut cx = EditorTestContext::new(cx).await;
8394 cx.set_state(
8395 r#"let foo = 2;
8396lˇet foo = 2;
8397let fooˇ = 2;
8398let foo = 2;
8399let foo = ˇ2;"#,
8400 );
8401
8402 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8403 .unwrap();
8404 cx.assert_editor_state(
8405 r#"let foo = 2;
8406«letˇ» foo = 2;
8407let «fooˇ» = 2;
8408let foo = 2;
8409let foo = «2ˇ»;"#,
8410 );
8411
8412 // noop for multiple selections with different contents
8413 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8414 .unwrap();
8415 cx.assert_editor_state(
8416 r#"let foo = 2;
8417«letˇ» foo = 2;
8418let «fooˇ» = 2;
8419let foo = 2;
8420let foo = «2ˇ»;"#,
8421 );
8422}
8423
8424#[gpui::test]
8425async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8426 init_test(cx, |_| {});
8427
8428 let mut cx = EditorTestContext::new(cx).await;
8429 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8430
8431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8432 .unwrap();
8433 // selection direction is preserved
8434 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8435
8436 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8437 .unwrap();
8438 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8439
8440 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8441 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8442
8443 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8444 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8445
8446 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8447 .unwrap();
8448 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8449
8450 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8451 .unwrap();
8452 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8453}
8454
8455#[gpui::test]
8456async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8457 init_test(cx, |_| {});
8458
8459 let language = Arc::new(Language::new(
8460 LanguageConfig::default(),
8461 Some(tree_sitter_rust::LANGUAGE.into()),
8462 ));
8463
8464 let text = r#"
8465 use mod1::mod2::{mod3, mod4};
8466
8467 fn fn_1(param1: bool, param2: &str) {
8468 let var1 = "text";
8469 }
8470 "#
8471 .unindent();
8472
8473 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8474 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8475 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8476
8477 editor
8478 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8479 .await;
8480
8481 editor.update_in(cx, |editor, window, cx| {
8482 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8483 s.select_display_ranges([
8484 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8485 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8486 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8487 ]);
8488 });
8489 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8490 });
8491 editor.update(cx, |editor, cx| {
8492 assert_text_with_selections(
8493 editor,
8494 indoc! {r#"
8495 use mod1::mod2::{mod3, «mod4ˇ»};
8496
8497 fn fn_1«ˇ(param1: bool, param2: &str)» {
8498 let var1 = "«ˇtext»";
8499 }
8500 "#},
8501 cx,
8502 );
8503 });
8504
8505 editor.update_in(cx, |editor, window, cx| {
8506 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8507 });
8508 editor.update(cx, |editor, cx| {
8509 assert_text_with_selections(
8510 editor,
8511 indoc! {r#"
8512 use mod1::mod2::«{mod3, mod4}ˇ»;
8513
8514 «ˇfn fn_1(param1: bool, param2: &str) {
8515 let var1 = "text";
8516 }»
8517 "#},
8518 cx,
8519 );
8520 });
8521
8522 editor.update_in(cx, |editor, window, cx| {
8523 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8524 });
8525 assert_eq!(
8526 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8527 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8528 );
8529
8530 // Trying to expand the selected syntax node one more time has no effect.
8531 editor.update_in(cx, |editor, window, cx| {
8532 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8533 });
8534 assert_eq!(
8535 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8536 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8537 );
8538
8539 editor.update_in(cx, |editor, window, cx| {
8540 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8541 });
8542 editor.update(cx, |editor, cx| {
8543 assert_text_with_selections(
8544 editor,
8545 indoc! {r#"
8546 use mod1::mod2::«{mod3, mod4}ˇ»;
8547
8548 «ˇfn fn_1(param1: bool, param2: &str) {
8549 let var1 = "text";
8550 }»
8551 "#},
8552 cx,
8553 );
8554 });
8555
8556 editor.update_in(cx, |editor, window, cx| {
8557 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8558 });
8559 editor.update(cx, |editor, cx| {
8560 assert_text_with_selections(
8561 editor,
8562 indoc! {r#"
8563 use mod1::mod2::{mod3, «mod4ˇ»};
8564
8565 fn fn_1«ˇ(param1: bool, param2: &str)» {
8566 let var1 = "«ˇtext»";
8567 }
8568 "#},
8569 cx,
8570 );
8571 });
8572
8573 editor.update_in(cx, |editor, window, cx| {
8574 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8575 });
8576 editor.update(cx, |editor, cx| {
8577 assert_text_with_selections(
8578 editor,
8579 indoc! {r#"
8580 use mod1::mod2::{mod3, mo«ˇ»d4};
8581
8582 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8583 let var1 = "te«ˇ»xt";
8584 }
8585 "#},
8586 cx,
8587 );
8588 });
8589
8590 // Trying to shrink the selected syntax node one more time has no effect.
8591 editor.update_in(cx, |editor, window, cx| {
8592 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8593 });
8594 editor.update_in(cx, |editor, _, cx| {
8595 assert_text_with_selections(
8596 editor,
8597 indoc! {r#"
8598 use mod1::mod2::{mod3, mo«ˇ»d4};
8599
8600 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8601 let var1 = "te«ˇ»xt";
8602 }
8603 "#},
8604 cx,
8605 );
8606 });
8607
8608 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8609 // a fold.
8610 editor.update_in(cx, |editor, window, cx| {
8611 editor.fold_creases(
8612 vec![
8613 Crease::simple(
8614 Point::new(0, 21)..Point::new(0, 24),
8615 FoldPlaceholder::test(),
8616 ),
8617 Crease::simple(
8618 Point::new(3, 20)..Point::new(3, 22),
8619 FoldPlaceholder::test(),
8620 ),
8621 ],
8622 true,
8623 window,
8624 cx,
8625 );
8626 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8627 });
8628 editor.update(cx, |editor, cx| {
8629 assert_text_with_selections(
8630 editor,
8631 indoc! {r#"
8632 use mod1::mod2::«{mod3, mod4}ˇ»;
8633
8634 fn fn_1«ˇ(param1: bool, param2: &str)» {
8635 let var1 = "«ˇtext»";
8636 }
8637 "#},
8638 cx,
8639 );
8640 });
8641}
8642
8643#[gpui::test]
8644async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8645 init_test(cx, |_| {});
8646
8647 let language = Arc::new(Language::new(
8648 LanguageConfig::default(),
8649 Some(tree_sitter_rust::LANGUAGE.into()),
8650 ));
8651
8652 let text = "let a = 2;";
8653
8654 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8655 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8656 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8657
8658 editor
8659 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8660 .await;
8661
8662 // Test case 1: Cursor at end of word
8663 editor.update_in(cx, |editor, window, cx| {
8664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8665 s.select_display_ranges([
8666 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8667 ]);
8668 });
8669 });
8670 editor.update(cx, |editor, cx| {
8671 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8672 });
8673 editor.update_in(cx, |editor, window, cx| {
8674 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8675 });
8676 editor.update(cx, |editor, cx| {
8677 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8678 });
8679 editor.update_in(cx, |editor, window, cx| {
8680 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8681 });
8682 editor.update(cx, |editor, cx| {
8683 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8684 });
8685
8686 // Test case 2: Cursor at end of statement
8687 editor.update_in(cx, |editor, window, cx| {
8688 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8689 s.select_display_ranges([
8690 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8691 ]);
8692 });
8693 });
8694 editor.update(cx, |editor, cx| {
8695 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8696 });
8697 editor.update_in(cx, |editor, window, cx| {
8698 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8699 });
8700 editor.update(cx, |editor, cx| {
8701 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8702 });
8703}
8704
8705#[gpui::test]
8706async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8707 init_test(cx, |_| {});
8708
8709 let language = Arc::new(Language::new(
8710 LanguageConfig::default(),
8711 Some(tree_sitter_rust::LANGUAGE.into()),
8712 ));
8713
8714 let text = r#"
8715 use mod1::mod2::{mod3, mod4};
8716
8717 fn fn_1(param1: bool, param2: &str) {
8718 let var1 = "hello world";
8719 }
8720 "#
8721 .unindent();
8722
8723 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8724 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8725 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8726
8727 editor
8728 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8729 .await;
8730
8731 // Test 1: Cursor on a letter of a string word
8732 editor.update_in(cx, |editor, window, cx| {
8733 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8734 s.select_display_ranges([
8735 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8736 ]);
8737 });
8738 });
8739 editor.update_in(cx, |editor, window, cx| {
8740 assert_text_with_selections(
8741 editor,
8742 indoc! {r#"
8743 use mod1::mod2::{mod3, mod4};
8744
8745 fn fn_1(param1: bool, param2: &str) {
8746 let var1 = "hˇello world";
8747 }
8748 "#},
8749 cx,
8750 );
8751 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8752 assert_text_with_selections(
8753 editor,
8754 indoc! {r#"
8755 use mod1::mod2::{mod3, mod4};
8756
8757 fn fn_1(param1: bool, param2: &str) {
8758 let var1 = "«ˇhello» world";
8759 }
8760 "#},
8761 cx,
8762 );
8763 });
8764
8765 // Test 2: Partial selection within a word
8766 editor.update_in(cx, |editor, window, cx| {
8767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8768 s.select_display_ranges([
8769 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
8770 ]);
8771 });
8772 });
8773 editor.update_in(cx, |editor, window, cx| {
8774 assert_text_with_selections(
8775 editor,
8776 indoc! {r#"
8777 use mod1::mod2::{mod3, mod4};
8778
8779 fn fn_1(param1: bool, param2: &str) {
8780 let var1 = "h«elˇ»lo world";
8781 }
8782 "#},
8783 cx,
8784 );
8785 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8786 assert_text_with_selections(
8787 editor,
8788 indoc! {r#"
8789 use mod1::mod2::{mod3, mod4};
8790
8791 fn fn_1(param1: bool, param2: &str) {
8792 let var1 = "«ˇhello» world";
8793 }
8794 "#},
8795 cx,
8796 );
8797 });
8798
8799 // Test 3: Complete word already selected
8800 editor.update_in(cx, |editor, window, cx| {
8801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8802 s.select_display_ranges([
8803 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
8804 ]);
8805 });
8806 });
8807 editor.update_in(cx, |editor, window, cx| {
8808 assert_text_with_selections(
8809 editor,
8810 indoc! {r#"
8811 use mod1::mod2::{mod3, mod4};
8812
8813 fn fn_1(param1: bool, param2: &str) {
8814 let var1 = "«helloˇ» world";
8815 }
8816 "#},
8817 cx,
8818 );
8819 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8820 assert_text_with_selections(
8821 editor,
8822 indoc! {r#"
8823 use mod1::mod2::{mod3, mod4};
8824
8825 fn fn_1(param1: bool, param2: &str) {
8826 let var1 = "«hello worldˇ»";
8827 }
8828 "#},
8829 cx,
8830 );
8831 });
8832
8833 // Test 4: Selection spanning across words
8834 editor.update_in(cx, |editor, window, cx| {
8835 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8836 s.select_display_ranges([
8837 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
8838 ]);
8839 });
8840 });
8841 editor.update_in(cx, |editor, window, cx| {
8842 assert_text_with_selections(
8843 editor,
8844 indoc! {r#"
8845 use mod1::mod2::{mod3, mod4};
8846
8847 fn fn_1(param1: bool, param2: &str) {
8848 let var1 = "hel«lo woˇ»rld";
8849 }
8850 "#},
8851 cx,
8852 );
8853 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8854 assert_text_with_selections(
8855 editor,
8856 indoc! {r#"
8857 use mod1::mod2::{mod3, mod4};
8858
8859 fn fn_1(param1: bool, param2: &str) {
8860 let var1 = "«ˇhello world»";
8861 }
8862 "#},
8863 cx,
8864 );
8865 });
8866
8867 // Test 5: Expansion beyond string
8868 editor.update_in(cx, |editor, window, cx| {
8869 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8870 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8871 assert_text_with_selections(
8872 editor,
8873 indoc! {r#"
8874 use mod1::mod2::{mod3, mod4};
8875
8876 fn fn_1(param1: bool, param2: &str) {
8877 «ˇlet var1 = "hello world";»
8878 }
8879 "#},
8880 cx,
8881 );
8882 });
8883}
8884
8885#[gpui::test]
8886async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
8887 init_test(cx, |_| {});
8888
8889 let mut cx = EditorTestContext::new(cx).await;
8890
8891 let language = Arc::new(Language::new(
8892 LanguageConfig::default(),
8893 Some(tree_sitter_rust::LANGUAGE.into()),
8894 ));
8895
8896 cx.update_buffer(|buffer, cx| {
8897 buffer.set_language(Some(language), cx);
8898 });
8899
8900 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
8901 cx.update_editor(|editor, window, cx| {
8902 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8903 });
8904
8905 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
8906}
8907
8908#[gpui::test]
8909async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8910 init_test(cx, |_| {});
8911
8912 let base_text = r#"
8913 impl A {
8914 // this is an uncommitted comment
8915
8916 fn b() {
8917 c();
8918 }
8919
8920 // this is another uncommitted comment
8921
8922 fn d() {
8923 // e
8924 // f
8925 }
8926 }
8927
8928 fn g() {
8929 // h
8930 }
8931 "#
8932 .unindent();
8933
8934 let text = r#"
8935 ˇimpl A {
8936
8937 fn b() {
8938 c();
8939 }
8940
8941 fn d() {
8942 // e
8943 // f
8944 }
8945 }
8946
8947 fn g() {
8948 // h
8949 }
8950 "#
8951 .unindent();
8952
8953 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8954 cx.set_state(&text);
8955 cx.set_head_text(&base_text);
8956 cx.update_editor(|editor, window, cx| {
8957 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8958 });
8959
8960 cx.assert_state_with_diff(
8961 "
8962 ˇimpl A {
8963 - // this is an uncommitted comment
8964
8965 fn b() {
8966 c();
8967 }
8968
8969 - // this is another uncommitted comment
8970 -
8971 fn d() {
8972 // e
8973 // f
8974 }
8975 }
8976
8977 fn g() {
8978 // h
8979 }
8980 "
8981 .unindent(),
8982 );
8983
8984 let expected_display_text = "
8985 impl A {
8986 // this is an uncommitted comment
8987
8988 fn b() {
8989 ⋯
8990 }
8991
8992 // this is another uncommitted comment
8993
8994 fn d() {
8995 ⋯
8996 }
8997 }
8998
8999 fn g() {
9000 ⋯
9001 }
9002 "
9003 .unindent();
9004
9005 cx.update_editor(|editor, window, cx| {
9006 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9007 assert_eq!(editor.display_text(cx), expected_display_text);
9008 });
9009}
9010
9011#[gpui::test]
9012async fn test_autoindent(cx: &mut TestAppContext) {
9013 init_test(cx, |_| {});
9014
9015 let language = Arc::new(
9016 Language::new(
9017 LanguageConfig {
9018 brackets: BracketPairConfig {
9019 pairs: vec![
9020 BracketPair {
9021 start: "{".to_string(),
9022 end: "}".to_string(),
9023 close: false,
9024 surround: false,
9025 newline: true,
9026 },
9027 BracketPair {
9028 start: "(".to_string(),
9029 end: ")".to_string(),
9030 close: false,
9031 surround: false,
9032 newline: true,
9033 },
9034 ],
9035 ..Default::default()
9036 },
9037 ..Default::default()
9038 },
9039 Some(tree_sitter_rust::LANGUAGE.into()),
9040 )
9041 .with_indents_query(
9042 r#"
9043 (_ "(" ")" @end) @indent
9044 (_ "{" "}" @end) @indent
9045 "#,
9046 )
9047 .unwrap(),
9048 );
9049
9050 let text = "fn a() {}";
9051
9052 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9053 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9054 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9055 editor
9056 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9057 .await;
9058
9059 editor.update_in(cx, |editor, window, cx| {
9060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9061 s.select_ranges([5..5, 8..8, 9..9])
9062 });
9063 editor.newline(&Newline, window, cx);
9064 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9065 assert_eq!(
9066 editor.selections.ranges(cx),
9067 &[
9068 Point::new(1, 4)..Point::new(1, 4),
9069 Point::new(3, 4)..Point::new(3, 4),
9070 Point::new(5, 0)..Point::new(5, 0)
9071 ]
9072 );
9073 });
9074}
9075
9076#[gpui::test]
9077async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9078 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9079
9080 let language = Arc::new(
9081 Language::new(
9082 LanguageConfig {
9083 brackets: BracketPairConfig {
9084 pairs: vec![
9085 BracketPair {
9086 start: "{".to_string(),
9087 end: "}".to_string(),
9088 close: false,
9089 surround: false,
9090 newline: true,
9091 },
9092 BracketPair {
9093 start: "(".to_string(),
9094 end: ")".to_string(),
9095 close: false,
9096 surround: false,
9097 newline: true,
9098 },
9099 ],
9100 ..Default::default()
9101 },
9102 ..Default::default()
9103 },
9104 Some(tree_sitter_rust::LANGUAGE.into()),
9105 )
9106 .with_indents_query(
9107 r#"
9108 (_ "(" ")" @end) @indent
9109 (_ "{" "}" @end) @indent
9110 "#,
9111 )
9112 .unwrap(),
9113 );
9114
9115 let text = "fn a() {}";
9116
9117 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9118 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9119 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9120 editor
9121 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9122 .await;
9123
9124 editor.update_in(cx, |editor, window, cx| {
9125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9126 s.select_ranges([5..5, 8..8, 9..9])
9127 });
9128 editor.newline(&Newline, window, cx);
9129 assert_eq!(
9130 editor.text(cx),
9131 indoc!(
9132 "
9133 fn a(
9134
9135 ) {
9136
9137 }
9138 "
9139 )
9140 );
9141 assert_eq!(
9142 editor.selections.ranges(cx),
9143 &[
9144 Point::new(1, 0)..Point::new(1, 0),
9145 Point::new(3, 0)..Point::new(3, 0),
9146 Point::new(5, 0)..Point::new(5, 0)
9147 ]
9148 );
9149 });
9150}
9151
9152#[gpui::test]
9153async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9154 init_test(cx, |settings| {
9155 settings.defaults.auto_indent = Some(true);
9156 settings.languages.0.insert(
9157 "python".into(),
9158 LanguageSettingsContent {
9159 auto_indent: Some(false),
9160 ..Default::default()
9161 },
9162 );
9163 });
9164
9165 let mut cx = EditorTestContext::new(cx).await;
9166
9167 let injected_language = Arc::new(
9168 Language::new(
9169 LanguageConfig {
9170 brackets: BracketPairConfig {
9171 pairs: vec![
9172 BracketPair {
9173 start: "{".to_string(),
9174 end: "}".to_string(),
9175 close: false,
9176 surround: false,
9177 newline: true,
9178 },
9179 BracketPair {
9180 start: "(".to_string(),
9181 end: ")".to_string(),
9182 close: true,
9183 surround: false,
9184 newline: true,
9185 },
9186 ],
9187 ..Default::default()
9188 },
9189 name: "python".into(),
9190 ..Default::default()
9191 },
9192 Some(tree_sitter_python::LANGUAGE.into()),
9193 )
9194 .with_indents_query(
9195 r#"
9196 (_ "(" ")" @end) @indent
9197 (_ "{" "}" @end) @indent
9198 "#,
9199 )
9200 .unwrap(),
9201 );
9202
9203 let language = Arc::new(
9204 Language::new(
9205 LanguageConfig {
9206 brackets: BracketPairConfig {
9207 pairs: vec![
9208 BracketPair {
9209 start: "{".to_string(),
9210 end: "}".to_string(),
9211 close: false,
9212 surround: false,
9213 newline: true,
9214 },
9215 BracketPair {
9216 start: "(".to_string(),
9217 end: ")".to_string(),
9218 close: true,
9219 surround: false,
9220 newline: true,
9221 },
9222 ],
9223 ..Default::default()
9224 },
9225 name: LanguageName::new("rust"),
9226 ..Default::default()
9227 },
9228 Some(tree_sitter_rust::LANGUAGE.into()),
9229 )
9230 .with_indents_query(
9231 r#"
9232 (_ "(" ")" @end) @indent
9233 (_ "{" "}" @end) @indent
9234 "#,
9235 )
9236 .unwrap()
9237 .with_injection_query(
9238 r#"
9239 (macro_invocation
9240 macro: (identifier) @_macro_name
9241 (token_tree) @injection.content
9242 (#set! injection.language "python"))
9243 "#,
9244 )
9245 .unwrap(),
9246 );
9247
9248 cx.language_registry().add(injected_language);
9249 cx.language_registry().add(language.clone());
9250
9251 cx.update_buffer(|buffer, cx| {
9252 buffer.set_language(Some(language), cx);
9253 });
9254
9255 cx.set_state(r#"struct A {ˇ}"#);
9256
9257 cx.update_editor(|editor, window, cx| {
9258 editor.newline(&Default::default(), window, cx);
9259 });
9260
9261 cx.assert_editor_state(indoc!(
9262 "struct A {
9263 ˇ
9264 }"
9265 ));
9266
9267 cx.set_state(r#"select_biased!(ˇ)"#);
9268
9269 cx.update_editor(|editor, window, cx| {
9270 editor.newline(&Default::default(), window, cx);
9271 editor.handle_input("def ", window, cx);
9272 editor.handle_input("(", window, cx);
9273 editor.newline(&Default::default(), window, cx);
9274 editor.handle_input("a", window, cx);
9275 });
9276
9277 cx.assert_editor_state(indoc!(
9278 "select_biased!(
9279 def (
9280 aˇ
9281 )
9282 )"
9283 ));
9284}
9285
9286#[gpui::test]
9287async fn test_autoindent_selections(cx: &mut TestAppContext) {
9288 init_test(cx, |_| {});
9289
9290 {
9291 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9292 cx.set_state(indoc! {"
9293 impl A {
9294
9295 fn b() {}
9296
9297 «fn c() {
9298
9299 }ˇ»
9300 }
9301 "});
9302
9303 cx.update_editor(|editor, window, cx| {
9304 editor.autoindent(&Default::default(), window, cx);
9305 });
9306
9307 cx.assert_editor_state(indoc! {"
9308 impl A {
9309
9310 fn b() {}
9311
9312 «fn c() {
9313
9314 }ˇ»
9315 }
9316 "});
9317 }
9318
9319 {
9320 let mut cx = EditorTestContext::new_multibuffer(
9321 cx,
9322 [indoc! { "
9323 impl A {
9324 «
9325 // a
9326 fn b(){}
9327 »
9328 «
9329 }
9330 fn c(){}
9331 »
9332 "}],
9333 );
9334
9335 let buffer = cx.update_editor(|editor, _, cx| {
9336 let buffer = editor.buffer().update(cx, |buffer, _| {
9337 buffer.all_buffers().iter().next().unwrap().clone()
9338 });
9339 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9340 buffer
9341 });
9342
9343 cx.run_until_parked();
9344 cx.update_editor(|editor, window, cx| {
9345 editor.select_all(&Default::default(), window, cx);
9346 editor.autoindent(&Default::default(), window, cx)
9347 });
9348 cx.run_until_parked();
9349
9350 cx.update(|_, cx| {
9351 assert_eq!(
9352 buffer.read(cx).text(),
9353 indoc! { "
9354 impl A {
9355
9356 // a
9357 fn b(){}
9358
9359
9360 }
9361 fn c(){}
9362
9363 " }
9364 )
9365 });
9366 }
9367}
9368
9369#[gpui::test]
9370async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9371 init_test(cx, |_| {});
9372
9373 let mut cx = EditorTestContext::new(cx).await;
9374
9375 let language = Arc::new(Language::new(
9376 LanguageConfig {
9377 brackets: BracketPairConfig {
9378 pairs: vec![
9379 BracketPair {
9380 start: "{".to_string(),
9381 end: "}".to_string(),
9382 close: true,
9383 surround: true,
9384 newline: true,
9385 },
9386 BracketPair {
9387 start: "(".to_string(),
9388 end: ")".to_string(),
9389 close: true,
9390 surround: true,
9391 newline: true,
9392 },
9393 BracketPair {
9394 start: "/*".to_string(),
9395 end: " */".to_string(),
9396 close: true,
9397 surround: true,
9398 newline: true,
9399 },
9400 BracketPair {
9401 start: "[".to_string(),
9402 end: "]".to_string(),
9403 close: false,
9404 surround: false,
9405 newline: true,
9406 },
9407 BracketPair {
9408 start: "\"".to_string(),
9409 end: "\"".to_string(),
9410 close: true,
9411 surround: true,
9412 newline: false,
9413 },
9414 BracketPair {
9415 start: "<".to_string(),
9416 end: ">".to_string(),
9417 close: false,
9418 surround: true,
9419 newline: true,
9420 },
9421 ],
9422 ..Default::default()
9423 },
9424 autoclose_before: "})]".to_string(),
9425 ..Default::default()
9426 },
9427 Some(tree_sitter_rust::LANGUAGE.into()),
9428 ));
9429
9430 cx.language_registry().add(language.clone());
9431 cx.update_buffer(|buffer, cx| {
9432 buffer.set_language(Some(language), cx);
9433 });
9434
9435 cx.set_state(
9436 &r#"
9437 🏀ˇ
9438 εˇ
9439 ❤️ˇ
9440 "#
9441 .unindent(),
9442 );
9443
9444 // autoclose multiple nested brackets at multiple cursors
9445 cx.update_editor(|editor, window, cx| {
9446 editor.handle_input("{", window, cx);
9447 editor.handle_input("{", window, cx);
9448 editor.handle_input("{", window, cx);
9449 });
9450 cx.assert_editor_state(
9451 &"
9452 🏀{{{ˇ}}}
9453 ε{{{ˇ}}}
9454 ❤️{{{ˇ}}}
9455 "
9456 .unindent(),
9457 );
9458
9459 // insert a different closing bracket
9460 cx.update_editor(|editor, window, cx| {
9461 editor.handle_input(")", window, cx);
9462 });
9463 cx.assert_editor_state(
9464 &"
9465 🏀{{{)ˇ}}}
9466 ε{{{)ˇ}}}
9467 ❤️{{{)ˇ}}}
9468 "
9469 .unindent(),
9470 );
9471
9472 // skip over the auto-closed brackets when typing a closing bracket
9473 cx.update_editor(|editor, window, cx| {
9474 editor.move_right(&MoveRight, window, cx);
9475 editor.handle_input("}", window, cx);
9476 editor.handle_input("}", window, cx);
9477 editor.handle_input("}", window, cx);
9478 });
9479 cx.assert_editor_state(
9480 &"
9481 🏀{{{)}}}}ˇ
9482 ε{{{)}}}}ˇ
9483 ❤️{{{)}}}}ˇ
9484 "
9485 .unindent(),
9486 );
9487
9488 // autoclose multi-character pairs
9489 cx.set_state(
9490 &"
9491 ˇ
9492 ˇ
9493 "
9494 .unindent(),
9495 );
9496 cx.update_editor(|editor, window, cx| {
9497 editor.handle_input("/", window, cx);
9498 editor.handle_input("*", window, cx);
9499 });
9500 cx.assert_editor_state(
9501 &"
9502 /*ˇ */
9503 /*ˇ */
9504 "
9505 .unindent(),
9506 );
9507
9508 // one cursor autocloses a multi-character pair, one cursor
9509 // does not autoclose.
9510 cx.set_state(
9511 &"
9512 /ˇ
9513 ˇ
9514 "
9515 .unindent(),
9516 );
9517 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9518 cx.assert_editor_state(
9519 &"
9520 /*ˇ */
9521 *ˇ
9522 "
9523 .unindent(),
9524 );
9525
9526 // Don't autoclose if the next character isn't whitespace and isn't
9527 // listed in the language's "autoclose_before" section.
9528 cx.set_state("ˇa b");
9529 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9530 cx.assert_editor_state("{ˇa b");
9531
9532 // Don't autoclose if `close` is false for the bracket pair
9533 cx.set_state("ˇ");
9534 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9535 cx.assert_editor_state("[ˇ");
9536
9537 // Surround with brackets if text is selected
9538 cx.set_state("«aˇ» b");
9539 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9540 cx.assert_editor_state("{«aˇ»} b");
9541
9542 // Autoclose when not immediately after a word character
9543 cx.set_state("a ˇ");
9544 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9545 cx.assert_editor_state("a \"ˇ\"");
9546
9547 // Autoclose pair where the start and end characters are the same
9548 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9549 cx.assert_editor_state("a \"\"ˇ");
9550
9551 // Don't autoclose when immediately after a word character
9552 cx.set_state("aˇ");
9553 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9554 cx.assert_editor_state("a\"ˇ");
9555
9556 // Do autoclose when after a non-word character
9557 cx.set_state("{ˇ");
9558 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9559 cx.assert_editor_state("{\"ˇ\"");
9560
9561 // Non identical pairs autoclose regardless of preceding character
9562 cx.set_state("aˇ");
9563 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9564 cx.assert_editor_state("a{ˇ}");
9565
9566 // Don't autoclose pair if autoclose is disabled
9567 cx.set_state("ˇ");
9568 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9569 cx.assert_editor_state("<ˇ");
9570
9571 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9572 cx.set_state("«aˇ» b");
9573 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9574 cx.assert_editor_state("<«aˇ»> b");
9575}
9576
9577#[gpui::test]
9578async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9579 init_test(cx, |settings| {
9580 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9581 });
9582
9583 let mut cx = EditorTestContext::new(cx).await;
9584
9585 let language = Arc::new(Language::new(
9586 LanguageConfig {
9587 brackets: BracketPairConfig {
9588 pairs: vec![
9589 BracketPair {
9590 start: "{".to_string(),
9591 end: "}".to_string(),
9592 close: true,
9593 surround: true,
9594 newline: true,
9595 },
9596 BracketPair {
9597 start: "(".to_string(),
9598 end: ")".to_string(),
9599 close: true,
9600 surround: true,
9601 newline: true,
9602 },
9603 BracketPair {
9604 start: "[".to_string(),
9605 end: "]".to_string(),
9606 close: false,
9607 surround: false,
9608 newline: true,
9609 },
9610 ],
9611 ..Default::default()
9612 },
9613 autoclose_before: "})]".to_string(),
9614 ..Default::default()
9615 },
9616 Some(tree_sitter_rust::LANGUAGE.into()),
9617 ));
9618
9619 cx.language_registry().add(language.clone());
9620 cx.update_buffer(|buffer, cx| {
9621 buffer.set_language(Some(language), cx);
9622 });
9623
9624 cx.set_state(
9625 &"
9626 ˇ
9627 ˇ
9628 ˇ
9629 "
9630 .unindent(),
9631 );
9632
9633 // ensure only matching closing brackets are skipped over
9634 cx.update_editor(|editor, window, cx| {
9635 editor.handle_input("}", window, cx);
9636 editor.move_left(&MoveLeft, window, cx);
9637 editor.handle_input(")", window, cx);
9638 editor.move_left(&MoveLeft, window, cx);
9639 });
9640 cx.assert_editor_state(
9641 &"
9642 ˇ)}
9643 ˇ)}
9644 ˇ)}
9645 "
9646 .unindent(),
9647 );
9648
9649 // skip-over closing brackets at multiple cursors
9650 cx.update_editor(|editor, window, cx| {
9651 editor.handle_input(")", window, cx);
9652 editor.handle_input("}", window, cx);
9653 });
9654 cx.assert_editor_state(
9655 &"
9656 )}ˇ
9657 )}ˇ
9658 )}ˇ
9659 "
9660 .unindent(),
9661 );
9662
9663 // ignore non-close brackets
9664 cx.update_editor(|editor, window, cx| {
9665 editor.handle_input("]", window, cx);
9666 editor.move_left(&MoveLeft, window, cx);
9667 editor.handle_input("]", window, cx);
9668 });
9669 cx.assert_editor_state(
9670 &"
9671 )}]ˇ]
9672 )}]ˇ]
9673 )}]ˇ]
9674 "
9675 .unindent(),
9676 );
9677}
9678
9679#[gpui::test]
9680async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9681 init_test(cx, |_| {});
9682
9683 let mut cx = EditorTestContext::new(cx).await;
9684
9685 let html_language = Arc::new(
9686 Language::new(
9687 LanguageConfig {
9688 name: "HTML".into(),
9689 brackets: BracketPairConfig {
9690 pairs: vec![
9691 BracketPair {
9692 start: "<".into(),
9693 end: ">".into(),
9694 close: true,
9695 ..Default::default()
9696 },
9697 BracketPair {
9698 start: "{".into(),
9699 end: "}".into(),
9700 close: true,
9701 ..Default::default()
9702 },
9703 BracketPair {
9704 start: "(".into(),
9705 end: ")".into(),
9706 close: true,
9707 ..Default::default()
9708 },
9709 ],
9710 ..Default::default()
9711 },
9712 autoclose_before: "})]>".into(),
9713 ..Default::default()
9714 },
9715 Some(tree_sitter_html::LANGUAGE.into()),
9716 )
9717 .with_injection_query(
9718 r#"
9719 (script_element
9720 (raw_text) @injection.content
9721 (#set! injection.language "javascript"))
9722 "#,
9723 )
9724 .unwrap(),
9725 );
9726
9727 let javascript_language = Arc::new(Language::new(
9728 LanguageConfig {
9729 name: "JavaScript".into(),
9730 brackets: BracketPairConfig {
9731 pairs: vec![
9732 BracketPair {
9733 start: "/*".into(),
9734 end: " */".into(),
9735 close: true,
9736 ..Default::default()
9737 },
9738 BracketPair {
9739 start: "{".into(),
9740 end: "}".into(),
9741 close: true,
9742 ..Default::default()
9743 },
9744 BracketPair {
9745 start: "(".into(),
9746 end: ")".into(),
9747 close: true,
9748 ..Default::default()
9749 },
9750 ],
9751 ..Default::default()
9752 },
9753 autoclose_before: "})]>".into(),
9754 ..Default::default()
9755 },
9756 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9757 ));
9758
9759 cx.language_registry().add(html_language.clone());
9760 cx.language_registry().add(javascript_language);
9761 cx.executor().run_until_parked();
9762
9763 cx.update_buffer(|buffer, cx| {
9764 buffer.set_language(Some(html_language), cx);
9765 });
9766
9767 cx.set_state(
9768 &r#"
9769 <body>ˇ
9770 <script>
9771 var x = 1;ˇ
9772 </script>
9773 </body>ˇ
9774 "#
9775 .unindent(),
9776 );
9777
9778 // Precondition: different languages are active at different locations.
9779 cx.update_editor(|editor, window, cx| {
9780 let snapshot = editor.snapshot(window, cx);
9781 let cursors = editor.selections.ranges::<usize>(cx);
9782 let languages = cursors
9783 .iter()
9784 .map(|c| snapshot.language_at(c.start).unwrap().name())
9785 .collect::<Vec<_>>();
9786 assert_eq!(
9787 languages,
9788 &["HTML".into(), "JavaScript".into(), "HTML".into()]
9789 );
9790 });
9791
9792 // Angle brackets autoclose in HTML, but not JavaScript.
9793 cx.update_editor(|editor, window, cx| {
9794 editor.handle_input("<", window, cx);
9795 editor.handle_input("a", window, cx);
9796 });
9797 cx.assert_editor_state(
9798 &r#"
9799 <body><aˇ>
9800 <script>
9801 var x = 1;<aˇ
9802 </script>
9803 </body><aˇ>
9804 "#
9805 .unindent(),
9806 );
9807
9808 // Curly braces and parens autoclose in both HTML and JavaScript.
9809 cx.update_editor(|editor, window, cx| {
9810 editor.handle_input(" b=", window, cx);
9811 editor.handle_input("{", window, cx);
9812 editor.handle_input("c", window, cx);
9813 editor.handle_input("(", window, cx);
9814 });
9815 cx.assert_editor_state(
9816 &r#"
9817 <body><a b={c(ˇ)}>
9818 <script>
9819 var x = 1;<a b={c(ˇ)}
9820 </script>
9821 </body><a b={c(ˇ)}>
9822 "#
9823 .unindent(),
9824 );
9825
9826 // Brackets that were already autoclosed are skipped.
9827 cx.update_editor(|editor, window, cx| {
9828 editor.handle_input(")", window, cx);
9829 editor.handle_input("d", window, cx);
9830 editor.handle_input("}", window, cx);
9831 });
9832 cx.assert_editor_state(
9833 &r#"
9834 <body><a b={c()d}ˇ>
9835 <script>
9836 var x = 1;<a b={c()d}ˇ
9837 </script>
9838 </body><a b={c()d}ˇ>
9839 "#
9840 .unindent(),
9841 );
9842 cx.update_editor(|editor, window, cx| {
9843 editor.handle_input(">", window, cx);
9844 });
9845 cx.assert_editor_state(
9846 &r#"
9847 <body><a b={c()d}>ˇ
9848 <script>
9849 var x = 1;<a b={c()d}>ˇ
9850 </script>
9851 </body><a b={c()d}>ˇ
9852 "#
9853 .unindent(),
9854 );
9855
9856 // Reset
9857 cx.set_state(
9858 &r#"
9859 <body>ˇ
9860 <script>
9861 var x = 1;ˇ
9862 </script>
9863 </body>ˇ
9864 "#
9865 .unindent(),
9866 );
9867
9868 cx.update_editor(|editor, window, cx| {
9869 editor.handle_input("<", window, cx);
9870 });
9871 cx.assert_editor_state(
9872 &r#"
9873 <body><ˇ>
9874 <script>
9875 var x = 1;<ˇ
9876 </script>
9877 </body><ˇ>
9878 "#
9879 .unindent(),
9880 );
9881
9882 // When backspacing, the closing angle brackets are removed.
9883 cx.update_editor(|editor, window, cx| {
9884 editor.backspace(&Backspace, window, cx);
9885 });
9886 cx.assert_editor_state(
9887 &r#"
9888 <body>ˇ
9889 <script>
9890 var x = 1;ˇ
9891 </script>
9892 </body>ˇ
9893 "#
9894 .unindent(),
9895 );
9896
9897 // Block comments autoclose in JavaScript, but not HTML.
9898 cx.update_editor(|editor, window, cx| {
9899 editor.handle_input("/", window, cx);
9900 editor.handle_input("*", window, cx);
9901 });
9902 cx.assert_editor_state(
9903 &r#"
9904 <body>/*ˇ
9905 <script>
9906 var x = 1;/*ˇ */
9907 </script>
9908 </body>/*ˇ
9909 "#
9910 .unindent(),
9911 );
9912}
9913
9914#[gpui::test]
9915async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9916 init_test(cx, |_| {});
9917
9918 let mut cx = EditorTestContext::new(cx).await;
9919
9920 let rust_language = Arc::new(
9921 Language::new(
9922 LanguageConfig {
9923 name: "Rust".into(),
9924 brackets: serde_json::from_value(json!([
9925 { "start": "{", "end": "}", "close": true, "newline": true },
9926 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9927 ]))
9928 .unwrap(),
9929 autoclose_before: "})]>".into(),
9930 ..Default::default()
9931 },
9932 Some(tree_sitter_rust::LANGUAGE.into()),
9933 )
9934 .with_override_query("(string_literal) @string")
9935 .unwrap(),
9936 );
9937
9938 cx.language_registry().add(rust_language.clone());
9939 cx.update_buffer(|buffer, cx| {
9940 buffer.set_language(Some(rust_language), cx);
9941 });
9942
9943 cx.set_state(
9944 &r#"
9945 let x = ˇ
9946 "#
9947 .unindent(),
9948 );
9949
9950 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9951 cx.update_editor(|editor, window, cx| {
9952 editor.handle_input("\"", window, cx);
9953 });
9954 cx.assert_editor_state(
9955 &r#"
9956 let x = "ˇ"
9957 "#
9958 .unindent(),
9959 );
9960
9961 // Inserting another quotation mark. The cursor moves across the existing
9962 // automatically-inserted quotation mark.
9963 cx.update_editor(|editor, window, cx| {
9964 editor.handle_input("\"", window, cx);
9965 });
9966 cx.assert_editor_state(
9967 &r#"
9968 let x = ""ˇ
9969 "#
9970 .unindent(),
9971 );
9972
9973 // Reset
9974 cx.set_state(
9975 &r#"
9976 let x = ˇ
9977 "#
9978 .unindent(),
9979 );
9980
9981 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9982 cx.update_editor(|editor, window, cx| {
9983 editor.handle_input("\"", window, cx);
9984 editor.handle_input(" ", window, cx);
9985 editor.move_left(&Default::default(), window, cx);
9986 editor.handle_input("\\", window, cx);
9987 editor.handle_input("\"", window, cx);
9988 });
9989 cx.assert_editor_state(
9990 &r#"
9991 let x = "\"ˇ "
9992 "#
9993 .unindent(),
9994 );
9995
9996 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
9997 // mark. Nothing is inserted.
9998 cx.update_editor(|editor, window, cx| {
9999 editor.move_right(&Default::default(), window, cx);
10000 editor.handle_input("\"", window, cx);
10001 });
10002 cx.assert_editor_state(
10003 &r#"
10004 let x = "\" "ˇ
10005 "#
10006 .unindent(),
10007 );
10008}
10009
10010#[gpui::test]
10011async fn test_surround_with_pair(cx: &mut TestAppContext) {
10012 init_test(cx, |_| {});
10013
10014 let language = Arc::new(Language::new(
10015 LanguageConfig {
10016 brackets: BracketPairConfig {
10017 pairs: vec![
10018 BracketPair {
10019 start: "{".to_string(),
10020 end: "}".to_string(),
10021 close: true,
10022 surround: true,
10023 newline: true,
10024 },
10025 BracketPair {
10026 start: "/* ".to_string(),
10027 end: "*/".to_string(),
10028 close: true,
10029 surround: true,
10030 ..Default::default()
10031 },
10032 ],
10033 ..Default::default()
10034 },
10035 ..Default::default()
10036 },
10037 Some(tree_sitter_rust::LANGUAGE.into()),
10038 ));
10039
10040 let text = r#"
10041 a
10042 b
10043 c
10044 "#
10045 .unindent();
10046
10047 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10048 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10049 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10050 editor
10051 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10052 .await;
10053
10054 editor.update_in(cx, |editor, window, cx| {
10055 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10056 s.select_display_ranges([
10057 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10058 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10059 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10060 ])
10061 });
10062
10063 editor.handle_input("{", window, cx);
10064 editor.handle_input("{", window, cx);
10065 editor.handle_input("{", window, cx);
10066 assert_eq!(
10067 editor.text(cx),
10068 "
10069 {{{a}}}
10070 {{{b}}}
10071 {{{c}}}
10072 "
10073 .unindent()
10074 );
10075 assert_eq!(
10076 editor.selections.display_ranges(cx),
10077 [
10078 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10079 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10080 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10081 ]
10082 );
10083
10084 editor.undo(&Undo, window, cx);
10085 editor.undo(&Undo, window, cx);
10086 editor.undo(&Undo, window, cx);
10087 assert_eq!(
10088 editor.text(cx),
10089 "
10090 a
10091 b
10092 c
10093 "
10094 .unindent()
10095 );
10096 assert_eq!(
10097 editor.selections.display_ranges(cx),
10098 [
10099 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10100 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10101 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10102 ]
10103 );
10104
10105 // Ensure inserting the first character of a multi-byte bracket pair
10106 // doesn't surround the selections with the bracket.
10107 editor.handle_input("/", window, cx);
10108 assert_eq!(
10109 editor.text(cx),
10110 "
10111 /
10112 /
10113 /
10114 "
10115 .unindent()
10116 );
10117 assert_eq!(
10118 editor.selections.display_ranges(cx),
10119 [
10120 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10121 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10122 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10123 ]
10124 );
10125
10126 editor.undo(&Undo, window, cx);
10127 assert_eq!(
10128 editor.text(cx),
10129 "
10130 a
10131 b
10132 c
10133 "
10134 .unindent()
10135 );
10136 assert_eq!(
10137 editor.selections.display_ranges(cx),
10138 [
10139 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10140 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10141 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10142 ]
10143 );
10144
10145 // Ensure inserting the last character of a multi-byte bracket pair
10146 // doesn't surround the selections with the bracket.
10147 editor.handle_input("*", window, cx);
10148 assert_eq!(
10149 editor.text(cx),
10150 "
10151 *
10152 *
10153 *
10154 "
10155 .unindent()
10156 );
10157 assert_eq!(
10158 editor.selections.display_ranges(cx),
10159 [
10160 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10161 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10162 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10163 ]
10164 );
10165 });
10166}
10167
10168#[gpui::test]
10169async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10170 init_test(cx, |_| {});
10171
10172 let language = Arc::new(Language::new(
10173 LanguageConfig {
10174 brackets: BracketPairConfig {
10175 pairs: vec![BracketPair {
10176 start: "{".to_string(),
10177 end: "}".to_string(),
10178 close: true,
10179 surround: true,
10180 newline: true,
10181 }],
10182 ..Default::default()
10183 },
10184 autoclose_before: "}".to_string(),
10185 ..Default::default()
10186 },
10187 Some(tree_sitter_rust::LANGUAGE.into()),
10188 ));
10189
10190 let text = r#"
10191 a
10192 b
10193 c
10194 "#
10195 .unindent();
10196
10197 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10198 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10199 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10200 editor
10201 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10202 .await;
10203
10204 editor.update_in(cx, |editor, window, cx| {
10205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10206 s.select_ranges([
10207 Point::new(0, 1)..Point::new(0, 1),
10208 Point::new(1, 1)..Point::new(1, 1),
10209 Point::new(2, 1)..Point::new(2, 1),
10210 ])
10211 });
10212
10213 editor.handle_input("{", window, cx);
10214 editor.handle_input("{", window, cx);
10215 editor.handle_input("_", window, cx);
10216 assert_eq!(
10217 editor.text(cx),
10218 "
10219 a{{_}}
10220 b{{_}}
10221 c{{_}}
10222 "
10223 .unindent()
10224 );
10225 assert_eq!(
10226 editor.selections.ranges::<Point>(cx),
10227 [
10228 Point::new(0, 4)..Point::new(0, 4),
10229 Point::new(1, 4)..Point::new(1, 4),
10230 Point::new(2, 4)..Point::new(2, 4)
10231 ]
10232 );
10233
10234 editor.backspace(&Default::default(), window, cx);
10235 editor.backspace(&Default::default(), window, cx);
10236 assert_eq!(
10237 editor.text(cx),
10238 "
10239 a{}
10240 b{}
10241 c{}
10242 "
10243 .unindent()
10244 );
10245 assert_eq!(
10246 editor.selections.ranges::<Point>(cx),
10247 [
10248 Point::new(0, 2)..Point::new(0, 2),
10249 Point::new(1, 2)..Point::new(1, 2),
10250 Point::new(2, 2)..Point::new(2, 2)
10251 ]
10252 );
10253
10254 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10255 assert_eq!(
10256 editor.text(cx),
10257 "
10258 a
10259 b
10260 c
10261 "
10262 .unindent()
10263 );
10264 assert_eq!(
10265 editor.selections.ranges::<Point>(cx),
10266 [
10267 Point::new(0, 1)..Point::new(0, 1),
10268 Point::new(1, 1)..Point::new(1, 1),
10269 Point::new(2, 1)..Point::new(2, 1)
10270 ]
10271 );
10272 });
10273}
10274
10275#[gpui::test]
10276async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10277 init_test(cx, |settings| {
10278 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10279 });
10280
10281 let mut cx = EditorTestContext::new(cx).await;
10282
10283 let language = Arc::new(Language::new(
10284 LanguageConfig {
10285 brackets: BracketPairConfig {
10286 pairs: vec![
10287 BracketPair {
10288 start: "{".to_string(),
10289 end: "}".to_string(),
10290 close: true,
10291 surround: true,
10292 newline: true,
10293 },
10294 BracketPair {
10295 start: "(".to_string(),
10296 end: ")".to_string(),
10297 close: true,
10298 surround: true,
10299 newline: true,
10300 },
10301 BracketPair {
10302 start: "[".to_string(),
10303 end: "]".to_string(),
10304 close: false,
10305 surround: true,
10306 newline: true,
10307 },
10308 ],
10309 ..Default::default()
10310 },
10311 autoclose_before: "})]".to_string(),
10312 ..Default::default()
10313 },
10314 Some(tree_sitter_rust::LANGUAGE.into()),
10315 ));
10316
10317 cx.language_registry().add(language.clone());
10318 cx.update_buffer(|buffer, cx| {
10319 buffer.set_language(Some(language), cx);
10320 });
10321
10322 cx.set_state(
10323 &"
10324 {(ˇ)}
10325 [[ˇ]]
10326 {(ˇ)}
10327 "
10328 .unindent(),
10329 );
10330
10331 cx.update_editor(|editor, window, cx| {
10332 editor.backspace(&Default::default(), window, cx);
10333 editor.backspace(&Default::default(), window, cx);
10334 });
10335
10336 cx.assert_editor_state(
10337 &"
10338 ˇ
10339 ˇ]]
10340 ˇ
10341 "
10342 .unindent(),
10343 );
10344
10345 cx.update_editor(|editor, window, cx| {
10346 editor.handle_input("{", window, cx);
10347 editor.handle_input("{", window, cx);
10348 editor.move_right(&MoveRight, window, cx);
10349 editor.move_right(&MoveRight, window, cx);
10350 editor.move_left(&MoveLeft, window, cx);
10351 editor.move_left(&MoveLeft, window, cx);
10352 editor.backspace(&Default::default(), window, cx);
10353 });
10354
10355 cx.assert_editor_state(
10356 &"
10357 {ˇ}
10358 {ˇ}]]
10359 {ˇ}
10360 "
10361 .unindent(),
10362 );
10363
10364 cx.update_editor(|editor, window, cx| {
10365 editor.backspace(&Default::default(), window, cx);
10366 });
10367
10368 cx.assert_editor_state(
10369 &"
10370 ˇ
10371 ˇ]]
10372 ˇ
10373 "
10374 .unindent(),
10375 );
10376}
10377
10378#[gpui::test]
10379async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10380 init_test(cx, |_| {});
10381
10382 let language = Arc::new(Language::new(
10383 LanguageConfig::default(),
10384 Some(tree_sitter_rust::LANGUAGE.into()),
10385 ));
10386
10387 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10388 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10389 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10390 editor
10391 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10392 .await;
10393
10394 editor.update_in(cx, |editor, window, cx| {
10395 editor.set_auto_replace_emoji_shortcode(true);
10396
10397 editor.handle_input("Hello ", window, cx);
10398 editor.handle_input(":wave", window, cx);
10399 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10400
10401 editor.handle_input(":", window, cx);
10402 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10403
10404 editor.handle_input(" :smile", window, cx);
10405 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10406
10407 editor.handle_input(":", window, cx);
10408 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10409
10410 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10411 editor.handle_input(":wave", window, cx);
10412 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10413
10414 editor.handle_input(":", window, cx);
10415 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10416
10417 editor.handle_input(":1", window, cx);
10418 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10419
10420 editor.handle_input(":", window, cx);
10421 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10422
10423 // Ensure shortcode does not get replaced when it is part of a word
10424 editor.handle_input(" Test:wave", window, cx);
10425 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10426
10427 editor.handle_input(":", window, cx);
10428 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10429
10430 editor.set_auto_replace_emoji_shortcode(false);
10431
10432 // Ensure shortcode does not get replaced when auto replace is off
10433 editor.handle_input(" :wave", window, cx);
10434 assert_eq!(
10435 editor.text(cx),
10436 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10437 );
10438
10439 editor.handle_input(":", window, cx);
10440 assert_eq!(
10441 editor.text(cx),
10442 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10443 );
10444 });
10445}
10446
10447#[gpui::test]
10448async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10449 init_test(cx, |_| {});
10450
10451 let (text, insertion_ranges) = marked_text_ranges(
10452 indoc! {"
10453 ˇ
10454 "},
10455 false,
10456 );
10457
10458 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10459 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10460
10461 _ = editor.update_in(cx, |editor, window, cx| {
10462 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10463
10464 editor
10465 .insert_snippet(&insertion_ranges, snippet, window, cx)
10466 .unwrap();
10467
10468 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10469 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10470 assert_eq!(editor.text(cx), expected_text);
10471 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10472 }
10473
10474 assert(
10475 editor,
10476 cx,
10477 indoc! {"
10478 type «» =•
10479 "},
10480 );
10481
10482 assert!(editor.context_menu_visible(), "There should be a matches");
10483 });
10484}
10485
10486#[gpui::test]
10487async fn test_snippets(cx: &mut TestAppContext) {
10488 init_test(cx, |_| {});
10489
10490 let mut cx = EditorTestContext::new(cx).await;
10491
10492 cx.set_state(indoc! {"
10493 a.ˇ b
10494 a.ˇ b
10495 a.ˇ b
10496 "});
10497
10498 cx.update_editor(|editor, window, cx| {
10499 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10500 let insertion_ranges = editor
10501 .selections
10502 .all(cx)
10503 .iter()
10504 .map(|s| s.range())
10505 .collect::<Vec<_>>();
10506 editor
10507 .insert_snippet(&insertion_ranges, snippet, window, cx)
10508 .unwrap();
10509 });
10510
10511 cx.assert_editor_state(indoc! {"
10512 a.f(«oneˇ», two, «threeˇ») b
10513 a.f(«oneˇ», two, «threeˇ») b
10514 a.f(«oneˇ», two, «threeˇ») b
10515 "});
10516
10517 // Can't move earlier than the first tab stop
10518 cx.update_editor(|editor, window, cx| {
10519 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10520 });
10521 cx.assert_editor_state(indoc! {"
10522 a.f(«oneˇ», two, «threeˇ») b
10523 a.f(«oneˇ», two, «threeˇ») b
10524 a.f(«oneˇ», two, «threeˇ») b
10525 "});
10526
10527 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10528 cx.assert_editor_state(indoc! {"
10529 a.f(one, «twoˇ», three) b
10530 a.f(one, «twoˇ», three) b
10531 a.f(one, «twoˇ», three) b
10532 "});
10533
10534 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10535 cx.assert_editor_state(indoc! {"
10536 a.f(«oneˇ», two, «threeˇ») b
10537 a.f(«oneˇ», two, «threeˇ») b
10538 a.f(«oneˇ», two, «threeˇ») b
10539 "});
10540
10541 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10542 cx.assert_editor_state(indoc! {"
10543 a.f(one, «twoˇ», three) b
10544 a.f(one, «twoˇ», three) b
10545 a.f(one, «twoˇ», three) b
10546 "});
10547 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10548 cx.assert_editor_state(indoc! {"
10549 a.f(one, two, three)ˇ b
10550 a.f(one, two, three)ˇ b
10551 a.f(one, two, three)ˇ b
10552 "});
10553
10554 // As soon as the last tab stop is reached, snippet state is gone
10555 cx.update_editor(|editor, window, cx| {
10556 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10557 });
10558 cx.assert_editor_state(indoc! {"
10559 a.f(one, two, three)ˇ b
10560 a.f(one, two, three)ˇ b
10561 a.f(one, two, three)ˇ b
10562 "});
10563}
10564
10565#[gpui::test]
10566async fn test_snippet_indentation(cx: &mut TestAppContext) {
10567 init_test(cx, |_| {});
10568
10569 let mut cx = EditorTestContext::new(cx).await;
10570
10571 cx.update_editor(|editor, window, cx| {
10572 let snippet = Snippet::parse(indoc! {"
10573 /*
10574 * Multiline comment with leading indentation
10575 *
10576 * $1
10577 */
10578 $0"})
10579 .unwrap();
10580 let insertion_ranges = editor
10581 .selections
10582 .all(cx)
10583 .iter()
10584 .map(|s| s.range())
10585 .collect::<Vec<_>>();
10586 editor
10587 .insert_snippet(&insertion_ranges, snippet, window, cx)
10588 .unwrap();
10589 });
10590
10591 cx.assert_editor_state(indoc! {"
10592 /*
10593 * Multiline comment with leading indentation
10594 *
10595 * ˇ
10596 */
10597 "});
10598
10599 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10600 cx.assert_editor_state(indoc! {"
10601 /*
10602 * Multiline comment with leading indentation
10603 *
10604 *•
10605 */
10606 ˇ"});
10607}
10608
10609#[gpui::test]
10610async fn test_document_format_during_save(cx: &mut TestAppContext) {
10611 init_test(cx, |_| {});
10612
10613 let fs = FakeFs::new(cx.executor());
10614 fs.insert_file(path!("/file.rs"), Default::default()).await;
10615
10616 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10617
10618 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10619 language_registry.add(rust_lang());
10620 let mut fake_servers = language_registry.register_fake_lsp(
10621 "Rust",
10622 FakeLspAdapter {
10623 capabilities: lsp::ServerCapabilities {
10624 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10625 ..Default::default()
10626 },
10627 ..Default::default()
10628 },
10629 );
10630
10631 let buffer = project
10632 .update(cx, |project, cx| {
10633 project.open_local_buffer(path!("/file.rs"), cx)
10634 })
10635 .await
10636 .unwrap();
10637
10638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10639 let (editor, cx) = cx.add_window_view(|window, cx| {
10640 build_editor_with_project(project.clone(), buffer, window, cx)
10641 });
10642 editor.update_in(cx, |editor, window, cx| {
10643 editor.set_text("one\ntwo\nthree\n", window, cx)
10644 });
10645 assert!(cx.read(|cx| editor.is_dirty(cx)));
10646
10647 cx.executor().start_waiting();
10648 let fake_server = fake_servers.next().await.unwrap();
10649
10650 {
10651 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10652 move |params, _| async move {
10653 assert_eq!(
10654 params.text_document.uri,
10655 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10656 );
10657 assert_eq!(params.options.tab_size, 4);
10658 Ok(Some(vec![lsp::TextEdit::new(
10659 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10660 ", ".to_string(),
10661 )]))
10662 },
10663 );
10664 let save = editor
10665 .update_in(cx, |editor, window, cx| {
10666 editor.save(
10667 SaveOptions {
10668 format: true,
10669 autosave: false,
10670 },
10671 project.clone(),
10672 window,
10673 cx,
10674 )
10675 })
10676 .unwrap();
10677 cx.executor().start_waiting();
10678 save.await;
10679
10680 assert_eq!(
10681 editor.update(cx, |editor, cx| editor.text(cx)),
10682 "one, two\nthree\n"
10683 );
10684 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10685 }
10686
10687 {
10688 editor.update_in(cx, |editor, window, cx| {
10689 editor.set_text("one\ntwo\nthree\n", window, cx)
10690 });
10691 assert!(cx.read(|cx| editor.is_dirty(cx)));
10692
10693 // Ensure we can still save even if formatting hangs.
10694 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10695 move |params, _| async move {
10696 assert_eq!(
10697 params.text_document.uri,
10698 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10699 );
10700 futures::future::pending::<()>().await;
10701 unreachable!()
10702 },
10703 );
10704 let save = editor
10705 .update_in(cx, |editor, window, cx| {
10706 editor.save(
10707 SaveOptions {
10708 format: true,
10709 autosave: false,
10710 },
10711 project.clone(),
10712 window,
10713 cx,
10714 )
10715 })
10716 .unwrap();
10717 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10718 cx.executor().start_waiting();
10719 save.await;
10720 assert_eq!(
10721 editor.update(cx, |editor, cx| editor.text(cx)),
10722 "one\ntwo\nthree\n"
10723 );
10724 }
10725
10726 // Set rust language override and assert overridden tabsize is sent to language server
10727 update_test_language_settings(cx, |settings| {
10728 settings.languages.0.insert(
10729 "Rust".into(),
10730 LanguageSettingsContent {
10731 tab_size: NonZeroU32::new(8),
10732 ..Default::default()
10733 },
10734 );
10735 });
10736
10737 {
10738 editor.update_in(cx, |editor, window, cx| {
10739 editor.set_text("somehting_new\n", window, cx)
10740 });
10741 assert!(cx.read(|cx| editor.is_dirty(cx)));
10742 let _formatting_request_signal = fake_server
10743 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10744 assert_eq!(
10745 params.text_document.uri,
10746 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10747 );
10748 assert_eq!(params.options.tab_size, 8);
10749 Ok(Some(vec![]))
10750 });
10751 let save = editor
10752 .update_in(cx, |editor, window, cx| {
10753 editor.save(
10754 SaveOptions {
10755 format: true,
10756 autosave: false,
10757 },
10758 project.clone(),
10759 window,
10760 cx,
10761 )
10762 })
10763 .unwrap();
10764 cx.executor().start_waiting();
10765 save.await;
10766 }
10767}
10768
10769#[gpui::test]
10770async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
10771 init_test(cx, |settings| {
10772 settings.defaults.ensure_final_newline_on_save = Some(false);
10773 });
10774
10775 let fs = FakeFs::new(cx.executor());
10776 fs.insert_file(path!("/file.txt"), "foo".into()).await;
10777
10778 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
10779
10780 let buffer = project
10781 .update(cx, |project, cx| {
10782 project.open_local_buffer(path!("/file.txt"), cx)
10783 })
10784 .await
10785 .unwrap();
10786
10787 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10788 let (editor, cx) = cx.add_window_view(|window, cx| {
10789 build_editor_with_project(project.clone(), buffer, window, cx)
10790 });
10791 editor.update_in(cx, |editor, window, cx| {
10792 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10793 s.select_ranges([0..0])
10794 });
10795 });
10796 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10797
10798 editor.update_in(cx, |editor, window, cx| {
10799 editor.handle_input("\n", window, cx)
10800 });
10801 cx.run_until_parked();
10802 save(&editor, &project, cx).await;
10803 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10804
10805 editor.update_in(cx, |editor, window, cx| {
10806 editor.undo(&Default::default(), window, cx);
10807 });
10808 save(&editor, &project, cx).await;
10809 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10810
10811 editor.update_in(cx, |editor, window, cx| {
10812 editor.redo(&Default::default(), window, cx);
10813 });
10814 cx.run_until_parked();
10815 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10816
10817 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
10818 let save = editor
10819 .update_in(cx, |editor, window, cx| {
10820 editor.save(
10821 SaveOptions {
10822 format: true,
10823 autosave: false,
10824 },
10825 project.clone(),
10826 window,
10827 cx,
10828 )
10829 })
10830 .unwrap();
10831 cx.executor().start_waiting();
10832 save.await;
10833 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10834 }
10835}
10836
10837#[gpui::test]
10838async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
10839 init_test(cx, |_| {});
10840
10841 let cols = 4;
10842 let rows = 10;
10843 let sample_text_1 = sample_text(rows, cols, 'a');
10844 assert_eq!(
10845 sample_text_1,
10846 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
10847 );
10848 let sample_text_2 = sample_text(rows, cols, 'l');
10849 assert_eq!(
10850 sample_text_2,
10851 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
10852 );
10853 let sample_text_3 = sample_text(rows, cols, 'v');
10854 assert_eq!(
10855 sample_text_3,
10856 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
10857 );
10858
10859 let fs = FakeFs::new(cx.executor());
10860 fs.insert_tree(
10861 path!("/a"),
10862 json!({
10863 "main.rs": sample_text_1,
10864 "other.rs": sample_text_2,
10865 "lib.rs": sample_text_3,
10866 }),
10867 )
10868 .await;
10869
10870 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10871 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10872 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10873
10874 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10875 language_registry.add(rust_lang());
10876 let mut fake_servers = language_registry.register_fake_lsp(
10877 "Rust",
10878 FakeLspAdapter {
10879 capabilities: lsp::ServerCapabilities {
10880 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10881 ..Default::default()
10882 },
10883 ..Default::default()
10884 },
10885 );
10886
10887 let worktree = project.update(cx, |project, cx| {
10888 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10889 assert_eq!(worktrees.len(), 1);
10890 worktrees.pop().unwrap()
10891 });
10892 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10893
10894 let buffer_1 = project
10895 .update(cx, |project, cx| {
10896 project.open_buffer((worktree_id, "main.rs"), cx)
10897 })
10898 .await
10899 .unwrap();
10900 let buffer_2 = project
10901 .update(cx, |project, cx| {
10902 project.open_buffer((worktree_id, "other.rs"), cx)
10903 })
10904 .await
10905 .unwrap();
10906 let buffer_3 = project
10907 .update(cx, |project, cx| {
10908 project.open_buffer((worktree_id, "lib.rs"), cx)
10909 })
10910 .await
10911 .unwrap();
10912
10913 let multi_buffer = cx.new(|cx| {
10914 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10915 multi_buffer.push_excerpts(
10916 buffer_1.clone(),
10917 [
10918 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10919 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10920 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10921 ],
10922 cx,
10923 );
10924 multi_buffer.push_excerpts(
10925 buffer_2.clone(),
10926 [
10927 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10928 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10929 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10930 ],
10931 cx,
10932 );
10933 multi_buffer.push_excerpts(
10934 buffer_3.clone(),
10935 [
10936 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10937 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10938 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10939 ],
10940 cx,
10941 );
10942 multi_buffer
10943 });
10944 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10945 Editor::new(
10946 EditorMode::full(),
10947 multi_buffer,
10948 Some(project.clone()),
10949 window,
10950 cx,
10951 )
10952 });
10953
10954 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10955 editor.change_selections(
10956 SelectionEffects::scroll(Autoscroll::Next),
10957 window,
10958 cx,
10959 |s| s.select_ranges(Some(1..2)),
10960 );
10961 editor.insert("|one|two|three|", window, cx);
10962 });
10963 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10964 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10965 editor.change_selections(
10966 SelectionEffects::scroll(Autoscroll::Next),
10967 window,
10968 cx,
10969 |s| s.select_ranges(Some(60..70)),
10970 );
10971 editor.insert("|four|five|six|", window, cx);
10972 });
10973 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10974
10975 // First two buffers should be edited, but not the third one.
10976 assert_eq!(
10977 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10978 "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}",
10979 );
10980 buffer_1.update(cx, |buffer, _| {
10981 assert!(buffer.is_dirty());
10982 assert_eq!(
10983 buffer.text(),
10984 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10985 )
10986 });
10987 buffer_2.update(cx, |buffer, _| {
10988 assert!(buffer.is_dirty());
10989 assert_eq!(
10990 buffer.text(),
10991 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
10992 )
10993 });
10994 buffer_3.update(cx, |buffer, _| {
10995 assert!(!buffer.is_dirty());
10996 assert_eq!(buffer.text(), sample_text_3,)
10997 });
10998 cx.executor().run_until_parked();
10999
11000 cx.executor().start_waiting();
11001 let save = multi_buffer_editor
11002 .update_in(cx, |editor, window, cx| {
11003 editor.save(
11004 SaveOptions {
11005 format: true,
11006 autosave: false,
11007 },
11008 project.clone(),
11009 window,
11010 cx,
11011 )
11012 })
11013 .unwrap();
11014
11015 let fake_server = fake_servers.next().await.unwrap();
11016 fake_server
11017 .server
11018 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11019 Ok(Some(vec![lsp::TextEdit::new(
11020 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11021 format!("[{} formatted]", params.text_document.uri),
11022 )]))
11023 })
11024 .detach();
11025 save.await;
11026
11027 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11028 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11029 assert_eq!(
11030 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11031 uri!(
11032 "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}"
11033 ),
11034 );
11035 buffer_1.update(cx, |buffer, _| {
11036 assert!(!buffer.is_dirty());
11037 assert_eq!(
11038 buffer.text(),
11039 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11040 )
11041 });
11042 buffer_2.update(cx, |buffer, _| {
11043 assert!(!buffer.is_dirty());
11044 assert_eq!(
11045 buffer.text(),
11046 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11047 )
11048 });
11049 buffer_3.update(cx, |buffer, _| {
11050 assert!(!buffer.is_dirty());
11051 assert_eq!(buffer.text(), sample_text_3,)
11052 });
11053}
11054
11055#[gpui::test]
11056async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11057 init_test(cx, |_| {});
11058
11059 let fs = FakeFs::new(cx.executor());
11060 fs.insert_tree(
11061 path!("/dir"),
11062 json!({
11063 "file1.rs": "fn main() { println!(\"hello\"); }",
11064 "file2.rs": "fn test() { println!(\"test\"); }",
11065 "file3.rs": "fn other() { println!(\"other\"); }\n",
11066 }),
11067 )
11068 .await;
11069
11070 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11071 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11072 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11073
11074 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11075 language_registry.add(rust_lang());
11076
11077 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11078 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11079
11080 // Open three buffers
11081 let buffer_1 = project
11082 .update(cx, |project, cx| {
11083 project.open_buffer((worktree_id, "file1.rs"), cx)
11084 })
11085 .await
11086 .unwrap();
11087 let buffer_2 = project
11088 .update(cx, |project, cx| {
11089 project.open_buffer((worktree_id, "file2.rs"), cx)
11090 })
11091 .await
11092 .unwrap();
11093 let buffer_3 = project
11094 .update(cx, |project, cx| {
11095 project.open_buffer((worktree_id, "file3.rs"), cx)
11096 })
11097 .await
11098 .unwrap();
11099
11100 // Create a multi-buffer with all three buffers
11101 let multi_buffer = cx.new(|cx| {
11102 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11103 multi_buffer.push_excerpts(
11104 buffer_1.clone(),
11105 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11106 cx,
11107 );
11108 multi_buffer.push_excerpts(
11109 buffer_2.clone(),
11110 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11111 cx,
11112 );
11113 multi_buffer.push_excerpts(
11114 buffer_3.clone(),
11115 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11116 cx,
11117 );
11118 multi_buffer
11119 });
11120
11121 let editor = cx.new_window_entity(|window, cx| {
11122 Editor::new(
11123 EditorMode::full(),
11124 multi_buffer,
11125 Some(project.clone()),
11126 window,
11127 cx,
11128 )
11129 });
11130
11131 // Edit only the first buffer
11132 editor.update_in(cx, |editor, window, cx| {
11133 editor.change_selections(
11134 SelectionEffects::scroll(Autoscroll::Next),
11135 window,
11136 cx,
11137 |s| s.select_ranges(Some(10..10)),
11138 );
11139 editor.insert("// edited", window, cx);
11140 });
11141
11142 // Verify that only buffer 1 is dirty
11143 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11144 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11145 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11146
11147 // Get write counts after file creation (files were created with initial content)
11148 // We expect each file to have been written once during creation
11149 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11150 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11151 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11152
11153 // Perform autosave
11154 let save_task = editor.update_in(cx, |editor, window, cx| {
11155 editor.save(
11156 SaveOptions {
11157 format: true,
11158 autosave: true,
11159 },
11160 project.clone(),
11161 window,
11162 cx,
11163 )
11164 });
11165 save_task.await.unwrap();
11166
11167 // Only the dirty buffer should have been saved
11168 assert_eq!(
11169 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11170 1,
11171 "Buffer 1 was dirty, so it should have been written once during autosave"
11172 );
11173 assert_eq!(
11174 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11175 0,
11176 "Buffer 2 was clean, so it should not have been written during autosave"
11177 );
11178 assert_eq!(
11179 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11180 0,
11181 "Buffer 3 was clean, so it should not have been written during autosave"
11182 );
11183
11184 // Verify buffer states after autosave
11185 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11186 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11187 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11188
11189 // Now perform a manual save (format = true)
11190 let save_task = editor.update_in(cx, |editor, window, cx| {
11191 editor.save(
11192 SaveOptions {
11193 format: true,
11194 autosave: false,
11195 },
11196 project.clone(),
11197 window,
11198 cx,
11199 )
11200 });
11201 save_task.await.unwrap();
11202
11203 // During manual save, clean buffers don't get written to disk
11204 // They just get did_save called for language server notifications
11205 assert_eq!(
11206 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11207 1,
11208 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11209 );
11210 assert_eq!(
11211 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11212 0,
11213 "Buffer 2 should not have been written at all"
11214 );
11215 assert_eq!(
11216 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11217 0,
11218 "Buffer 3 should not have been written at all"
11219 );
11220}
11221
11222async fn setup_range_format_test(
11223 cx: &mut TestAppContext,
11224) -> (
11225 Entity<Project>,
11226 Entity<Editor>,
11227 &mut gpui::VisualTestContext,
11228 lsp::FakeLanguageServer,
11229) {
11230 init_test(cx, |_| {});
11231
11232 let fs = FakeFs::new(cx.executor());
11233 fs.insert_file(path!("/file.rs"), Default::default()).await;
11234
11235 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11236
11237 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11238 language_registry.add(rust_lang());
11239 let mut fake_servers = language_registry.register_fake_lsp(
11240 "Rust",
11241 FakeLspAdapter {
11242 capabilities: lsp::ServerCapabilities {
11243 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11244 ..lsp::ServerCapabilities::default()
11245 },
11246 ..FakeLspAdapter::default()
11247 },
11248 );
11249
11250 let buffer = project
11251 .update(cx, |project, cx| {
11252 project.open_local_buffer(path!("/file.rs"), cx)
11253 })
11254 .await
11255 .unwrap();
11256
11257 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11258 let (editor, cx) = cx.add_window_view(|window, cx| {
11259 build_editor_with_project(project.clone(), buffer, window, cx)
11260 });
11261
11262 cx.executor().start_waiting();
11263 let fake_server = fake_servers.next().await.unwrap();
11264
11265 (project, editor, cx, fake_server)
11266}
11267
11268#[gpui::test]
11269async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11270 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11271
11272 editor.update_in(cx, |editor, window, cx| {
11273 editor.set_text("one\ntwo\nthree\n", window, cx)
11274 });
11275 assert!(cx.read(|cx| editor.is_dirty(cx)));
11276
11277 let save = editor
11278 .update_in(cx, |editor, window, cx| {
11279 editor.save(
11280 SaveOptions {
11281 format: true,
11282 autosave: false,
11283 },
11284 project.clone(),
11285 window,
11286 cx,
11287 )
11288 })
11289 .unwrap();
11290 fake_server
11291 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11292 assert_eq!(
11293 params.text_document.uri,
11294 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11295 );
11296 assert_eq!(params.options.tab_size, 4);
11297 Ok(Some(vec![lsp::TextEdit::new(
11298 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11299 ", ".to_string(),
11300 )]))
11301 })
11302 .next()
11303 .await;
11304 cx.executor().start_waiting();
11305 save.await;
11306 assert_eq!(
11307 editor.update(cx, |editor, cx| editor.text(cx)),
11308 "one, two\nthree\n"
11309 );
11310 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11311}
11312
11313#[gpui::test]
11314async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11315 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11316
11317 editor.update_in(cx, |editor, window, cx| {
11318 editor.set_text("one\ntwo\nthree\n", window, cx)
11319 });
11320 assert!(cx.read(|cx| editor.is_dirty(cx)));
11321
11322 // Test that save still works when formatting hangs
11323 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11324 move |params, _| async move {
11325 assert_eq!(
11326 params.text_document.uri,
11327 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11328 );
11329 futures::future::pending::<()>().await;
11330 unreachable!()
11331 },
11332 );
11333 let save = editor
11334 .update_in(cx, |editor, window, cx| {
11335 editor.save(
11336 SaveOptions {
11337 format: true,
11338 autosave: false,
11339 },
11340 project.clone(),
11341 window,
11342 cx,
11343 )
11344 })
11345 .unwrap();
11346 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11347 cx.executor().start_waiting();
11348 save.await;
11349 assert_eq!(
11350 editor.update(cx, |editor, cx| editor.text(cx)),
11351 "one\ntwo\nthree\n"
11352 );
11353 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11354}
11355
11356#[gpui::test]
11357async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11358 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11359
11360 // Buffer starts clean, no formatting should be requested
11361 let save = editor
11362 .update_in(cx, |editor, window, cx| {
11363 editor.save(
11364 SaveOptions {
11365 format: false,
11366 autosave: false,
11367 },
11368 project.clone(),
11369 window,
11370 cx,
11371 )
11372 })
11373 .unwrap();
11374 let _pending_format_request = fake_server
11375 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11376 panic!("Should not be invoked");
11377 })
11378 .next();
11379 cx.executor().start_waiting();
11380 save.await;
11381 cx.run_until_parked();
11382}
11383
11384#[gpui::test]
11385async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11386 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11387
11388 // Set Rust language override and assert overridden tabsize is sent to language server
11389 update_test_language_settings(cx, |settings| {
11390 settings.languages.0.insert(
11391 "Rust".into(),
11392 LanguageSettingsContent {
11393 tab_size: NonZeroU32::new(8),
11394 ..Default::default()
11395 },
11396 );
11397 });
11398
11399 editor.update_in(cx, |editor, window, cx| {
11400 editor.set_text("something_new\n", window, cx)
11401 });
11402 assert!(cx.read(|cx| editor.is_dirty(cx)));
11403 let save = editor
11404 .update_in(cx, |editor, window, cx| {
11405 editor.save(
11406 SaveOptions {
11407 format: true,
11408 autosave: false,
11409 },
11410 project.clone(),
11411 window,
11412 cx,
11413 )
11414 })
11415 .unwrap();
11416 fake_server
11417 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11418 assert_eq!(
11419 params.text_document.uri,
11420 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11421 );
11422 assert_eq!(params.options.tab_size, 8);
11423 Ok(Some(Vec::new()))
11424 })
11425 .next()
11426 .await;
11427 save.await;
11428}
11429
11430#[gpui::test]
11431async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11432 init_test(cx, |settings| {
11433 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11434 Formatter::LanguageServer { name: None },
11435 )))
11436 });
11437
11438 let fs = FakeFs::new(cx.executor());
11439 fs.insert_file(path!("/file.rs"), Default::default()).await;
11440
11441 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11442
11443 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11444 language_registry.add(Arc::new(Language::new(
11445 LanguageConfig {
11446 name: "Rust".into(),
11447 matcher: LanguageMatcher {
11448 path_suffixes: vec!["rs".to_string()],
11449 ..Default::default()
11450 },
11451 ..LanguageConfig::default()
11452 },
11453 Some(tree_sitter_rust::LANGUAGE.into()),
11454 )));
11455 update_test_language_settings(cx, |settings| {
11456 // Enable Prettier formatting for the same buffer, and ensure
11457 // LSP is called instead of Prettier.
11458 settings.defaults.prettier = Some(PrettierSettings {
11459 allowed: true,
11460 ..PrettierSettings::default()
11461 });
11462 });
11463 let mut fake_servers = language_registry.register_fake_lsp(
11464 "Rust",
11465 FakeLspAdapter {
11466 capabilities: lsp::ServerCapabilities {
11467 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11468 ..Default::default()
11469 },
11470 ..Default::default()
11471 },
11472 );
11473
11474 let buffer = project
11475 .update(cx, |project, cx| {
11476 project.open_local_buffer(path!("/file.rs"), cx)
11477 })
11478 .await
11479 .unwrap();
11480
11481 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11482 let (editor, cx) = cx.add_window_view(|window, cx| {
11483 build_editor_with_project(project.clone(), buffer, window, cx)
11484 });
11485 editor.update_in(cx, |editor, window, cx| {
11486 editor.set_text("one\ntwo\nthree\n", window, cx)
11487 });
11488
11489 cx.executor().start_waiting();
11490 let fake_server = fake_servers.next().await.unwrap();
11491
11492 let format = editor
11493 .update_in(cx, |editor, window, cx| {
11494 editor.perform_format(
11495 project.clone(),
11496 FormatTrigger::Manual,
11497 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11498 window,
11499 cx,
11500 )
11501 })
11502 .unwrap();
11503 fake_server
11504 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11505 assert_eq!(
11506 params.text_document.uri,
11507 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11508 );
11509 assert_eq!(params.options.tab_size, 4);
11510 Ok(Some(vec![lsp::TextEdit::new(
11511 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11512 ", ".to_string(),
11513 )]))
11514 })
11515 .next()
11516 .await;
11517 cx.executor().start_waiting();
11518 format.await;
11519 assert_eq!(
11520 editor.update(cx, |editor, cx| editor.text(cx)),
11521 "one, two\nthree\n"
11522 );
11523
11524 editor.update_in(cx, |editor, window, cx| {
11525 editor.set_text("one\ntwo\nthree\n", window, cx)
11526 });
11527 // Ensure we don't lock if formatting hangs.
11528 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11529 move |params, _| async move {
11530 assert_eq!(
11531 params.text_document.uri,
11532 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11533 );
11534 futures::future::pending::<()>().await;
11535 unreachable!()
11536 },
11537 );
11538 let format = editor
11539 .update_in(cx, |editor, window, cx| {
11540 editor.perform_format(
11541 project,
11542 FormatTrigger::Manual,
11543 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11544 window,
11545 cx,
11546 )
11547 })
11548 .unwrap();
11549 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11550 cx.executor().start_waiting();
11551 format.await;
11552 assert_eq!(
11553 editor.update(cx, |editor, cx| editor.text(cx)),
11554 "one\ntwo\nthree\n"
11555 );
11556}
11557
11558#[gpui::test]
11559async fn test_multiple_formatters(cx: &mut TestAppContext) {
11560 init_test(cx, |settings| {
11561 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11562 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11563 Formatter::LanguageServer { name: None },
11564 Formatter::CodeActions(
11565 [
11566 ("code-action-1".into(), true),
11567 ("code-action-2".into(), true),
11568 ]
11569 .into_iter()
11570 .collect(),
11571 ),
11572 ])))
11573 });
11574
11575 let fs = FakeFs::new(cx.executor());
11576 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11577 .await;
11578
11579 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11580 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11581 language_registry.add(rust_lang());
11582
11583 let mut fake_servers = language_registry.register_fake_lsp(
11584 "Rust",
11585 FakeLspAdapter {
11586 capabilities: lsp::ServerCapabilities {
11587 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11588 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11589 commands: vec!["the-command-for-code-action-1".into()],
11590 ..Default::default()
11591 }),
11592 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11593 ..Default::default()
11594 },
11595 ..Default::default()
11596 },
11597 );
11598
11599 let buffer = project
11600 .update(cx, |project, cx| {
11601 project.open_local_buffer(path!("/file.rs"), cx)
11602 })
11603 .await
11604 .unwrap();
11605
11606 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11607 let (editor, cx) = cx.add_window_view(|window, cx| {
11608 build_editor_with_project(project.clone(), buffer, window, cx)
11609 });
11610
11611 cx.executor().start_waiting();
11612
11613 let fake_server = fake_servers.next().await.unwrap();
11614 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11615 move |_params, _| async move {
11616 Ok(Some(vec![lsp::TextEdit::new(
11617 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11618 "applied-formatting\n".to_string(),
11619 )]))
11620 },
11621 );
11622 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11623 move |params, _| async move {
11624 assert_eq!(
11625 params.context.only,
11626 Some(vec!["code-action-1".into(), "code-action-2".into()])
11627 );
11628 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11629 Ok(Some(vec![
11630 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11631 kind: Some("code-action-1".into()),
11632 edit: Some(lsp::WorkspaceEdit::new(
11633 [(
11634 uri.clone(),
11635 vec![lsp::TextEdit::new(
11636 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11637 "applied-code-action-1-edit\n".to_string(),
11638 )],
11639 )]
11640 .into_iter()
11641 .collect(),
11642 )),
11643 command: Some(lsp::Command {
11644 command: "the-command-for-code-action-1".into(),
11645 ..Default::default()
11646 }),
11647 ..Default::default()
11648 }),
11649 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11650 kind: Some("code-action-2".into()),
11651 edit: Some(lsp::WorkspaceEdit::new(
11652 [(
11653 uri,
11654 vec![lsp::TextEdit::new(
11655 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11656 "applied-code-action-2-edit\n".to_string(),
11657 )],
11658 )]
11659 .into_iter()
11660 .collect(),
11661 )),
11662 ..Default::default()
11663 }),
11664 ]))
11665 },
11666 );
11667
11668 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11669 move |params, _| async move { Ok(params) }
11670 });
11671
11672 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11673 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11674 let fake = fake_server.clone();
11675 let lock = command_lock.clone();
11676 move |params, _| {
11677 assert_eq!(params.command, "the-command-for-code-action-1");
11678 let fake = fake.clone();
11679 let lock = lock.clone();
11680 async move {
11681 lock.lock().await;
11682 fake.server
11683 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11684 label: None,
11685 edit: lsp::WorkspaceEdit {
11686 changes: Some(
11687 [(
11688 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11689 vec![lsp::TextEdit {
11690 range: lsp::Range::new(
11691 lsp::Position::new(0, 0),
11692 lsp::Position::new(0, 0),
11693 ),
11694 new_text: "applied-code-action-1-command\n".into(),
11695 }],
11696 )]
11697 .into_iter()
11698 .collect(),
11699 ),
11700 ..Default::default()
11701 },
11702 })
11703 .await
11704 .into_response()
11705 .unwrap();
11706 Ok(Some(json!(null)))
11707 }
11708 }
11709 });
11710
11711 cx.executor().start_waiting();
11712 editor
11713 .update_in(cx, |editor, window, cx| {
11714 editor.perform_format(
11715 project.clone(),
11716 FormatTrigger::Manual,
11717 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11718 window,
11719 cx,
11720 )
11721 })
11722 .unwrap()
11723 .await;
11724 editor.update(cx, |editor, cx| {
11725 assert_eq!(
11726 editor.text(cx),
11727 r#"
11728 applied-code-action-2-edit
11729 applied-code-action-1-command
11730 applied-code-action-1-edit
11731 applied-formatting
11732 one
11733 two
11734 three
11735 "#
11736 .unindent()
11737 );
11738 });
11739
11740 editor.update_in(cx, |editor, window, cx| {
11741 editor.undo(&Default::default(), window, cx);
11742 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11743 });
11744
11745 // Perform a manual edit while waiting for an LSP command
11746 // that's being run as part of a formatting code action.
11747 let lock_guard = command_lock.lock().await;
11748 let format = editor
11749 .update_in(cx, |editor, window, cx| {
11750 editor.perform_format(
11751 project.clone(),
11752 FormatTrigger::Manual,
11753 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11754 window,
11755 cx,
11756 )
11757 })
11758 .unwrap();
11759 cx.run_until_parked();
11760 editor.update(cx, |editor, cx| {
11761 assert_eq!(
11762 editor.text(cx),
11763 r#"
11764 applied-code-action-1-edit
11765 applied-formatting
11766 one
11767 two
11768 three
11769 "#
11770 .unindent()
11771 );
11772
11773 editor.buffer.update(cx, |buffer, cx| {
11774 let ix = buffer.len(cx);
11775 buffer.edit([(ix..ix, "edited\n")], None, cx);
11776 });
11777 });
11778
11779 // Allow the LSP command to proceed. Because the buffer was edited,
11780 // the second code action will not be run.
11781 drop(lock_guard);
11782 format.await;
11783 editor.update_in(cx, |editor, window, cx| {
11784 assert_eq!(
11785 editor.text(cx),
11786 r#"
11787 applied-code-action-1-command
11788 applied-code-action-1-edit
11789 applied-formatting
11790 one
11791 two
11792 three
11793 edited
11794 "#
11795 .unindent()
11796 );
11797
11798 // The manual edit is undone first, because it is the last thing the user did
11799 // (even though the command completed afterwards).
11800 editor.undo(&Default::default(), window, cx);
11801 assert_eq!(
11802 editor.text(cx),
11803 r#"
11804 applied-code-action-1-command
11805 applied-code-action-1-edit
11806 applied-formatting
11807 one
11808 two
11809 three
11810 "#
11811 .unindent()
11812 );
11813
11814 // All the formatting (including the command, which completed after the manual edit)
11815 // is undone together.
11816 editor.undo(&Default::default(), window, cx);
11817 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11818 });
11819}
11820
11821#[gpui::test]
11822async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
11823 init_test(cx, |settings| {
11824 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11825 Formatter::LanguageServer { name: None },
11826 ])))
11827 });
11828
11829 let fs = FakeFs::new(cx.executor());
11830 fs.insert_file(path!("/file.ts"), Default::default()).await;
11831
11832 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11833
11834 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11835 language_registry.add(Arc::new(Language::new(
11836 LanguageConfig {
11837 name: "TypeScript".into(),
11838 matcher: LanguageMatcher {
11839 path_suffixes: vec!["ts".to_string()],
11840 ..Default::default()
11841 },
11842 ..LanguageConfig::default()
11843 },
11844 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11845 )));
11846 update_test_language_settings(cx, |settings| {
11847 settings.defaults.prettier = Some(PrettierSettings {
11848 allowed: true,
11849 ..PrettierSettings::default()
11850 });
11851 });
11852 let mut fake_servers = language_registry.register_fake_lsp(
11853 "TypeScript",
11854 FakeLspAdapter {
11855 capabilities: lsp::ServerCapabilities {
11856 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11857 ..Default::default()
11858 },
11859 ..Default::default()
11860 },
11861 );
11862
11863 let buffer = project
11864 .update(cx, |project, cx| {
11865 project.open_local_buffer(path!("/file.ts"), cx)
11866 })
11867 .await
11868 .unwrap();
11869
11870 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11871 let (editor, cx) = cx.add_window_view(|window, cx| {
11872 build_editor_with_project(project.clone(), buffer, window, cx)
11873 });
11874 editor.update_in(cx, |editor, window, cx| {
11875 editor.set_text(
11876 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11877 window,
11878 cx,
11879 )
11880 });
11881
11882 cx.executor().start_waiting();
11883 let fake_server = fake_servers.next().await.unwrap();
11884
11885 let format = editor
11886 .update_in(cx, |editor, window, cx| {
11887 editor.perform_code_action_kind(
11888 project.clone(),
11889 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11890 window,
11891 cx,
11892 )
11893 })
11894 .unwrap();
11895 fake_server
11896 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11897 assert_eq!(
11898 params.text_document.uri,
11899 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11900 );
11901 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11902 lsp::CodeAction {
11903 title: "Organize Imports".to_string(),
11904 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11905 edit: Some(lsp::WorkspaceEdit {
11906 changes: Some(
11907 [(
11908 params.text_document.uri.clone(),
11909 vec![lsp::TextEdit::new(
11910 lsp::Range::new(
11911 lsp::Position::new(1, 0),
11912 lsp::Position::new(2, 0),
11913 ),
11914 "".to_string(),
11915 )],
11916 )]
11917 .into_iter()
11918 .collect(),
11919 ),
11920 ..Default::default()
11921 }),
11922 ..Default::default()
11923 },
11924 )]))
11925 })
11926 .next()
11927 .await;
11928 cx.executor().start_waiting();
11929 format.await;
11930 assert_eq!(
11931 editor.update(cx, |editor, cx| editor.text(cx)),
11932 "import { a } from 'module';\n\nconst x = a;\n"
11933 );
11934
11935 editor.update_in(cx, |editor, window, cx| {
11936 editor.set_text(
11937 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11938 window,
11939 cx,
11940 )
11941 });
11942 // Ensure we don't lock if code action hangs.
11943 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11944 move |params, _| async move {
11945 assert_eq!(
11946 params.text_document.uri,
11947 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11948 );
11949 futures::future::pending::<()>().await;
11950 unreachable!()
11951 },
11952 );
11953 let format = editor
11954 .update_in(cx, |editor, window, cx| {
11955 editor.perform_code_action_kind(
11956 project,
11957 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11958 window,
11959 cx,
11960 )
11961 })
11962 .unwrap();
11963 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11964 cx.executor().start_waiting();
11965 format.await;
11966 assert_eq!(
11967 editor.update(cx, |editor, cx| editor.text(cx)),
11968 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11969 );
11970}
11971
11972#[gpui::test]
11973async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11974 init_test(cx, |_| {});
11975
11976 let mut cx = EditorLspTestContext::new_rust(
11977 lsp::ServerCapabilities {
11978 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11979 ..Default::default()
11980 },
11981 cx,
11982 )
11983 .await;
11984
11985 cx.set_state(indoc! {"
11986 one.twoˇ
11987 "});
11988
11989 // The format request takes a long time. When it completes, it inserts
11990 // a newline and an indent before the `.`
11991 cx.lsp
11992 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
11993 let executor = cx.background_executor().clone();
11994 async move {
11995 executor.timer(Duration::from_millis(100)).await;
11996 Ok(Some(vec![lsp::TextEdit {
11997 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
11998 new_text: "\n ".into(),
11999 }]))
12000 }
12001 });
12002
12003 // Submit a format request.
12004 let format_1 = cx
12005 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12006 .unwrap();
12007 cx.executor().run_until_parked();
12008
12009 // Submit a second format request.
12010 let format_2 = cx
12011 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12012 .unwrap();
12013 cx.executor().run_until_parked();
12014
12015 // Wait for both format requests to complete
12016 cx.executor().advance_clock(Duration::from_millis(200));
12017 cx.executor().start_waiting();
12018 format_1.await.unwrap();
12019 cx.executor().start_waiting();
12020 format_2.await.unwrap();
12021
12022 // The formatting edits only happens once.
12023 cx.assert_editor_state(indoc! {"
12024 one
12025 .twoˇ
12026 "});
12027}
12028
12029#[gpui::test]
12030async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12031 init_test(cx, |settings| {
12032 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12033 });
12034
12035 let mut cx = EditorLspTestContext::new_rust(
12036 lsp::ServerCapabilities {
12037 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12038 ..Default::default()
12039 },
12040 cx,
12041 )
12042 .await;
12043
12044 // Set up a buffer white some trailing whitespace and no trailing newline.
12045 cx.set_state(
12046 &[
12047 "one ", //
12048 "twoˇ", //
12049 "three ", //
12050 "four", //
12051 ]
12052 .join("\n"),
12053 );
12054
12055 // Submit a format request.
12056 let format = cx
12057 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12058 .unwrap();
12059
12060 // Record which buffer changes have been sent to the language server
12061 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12062 cx.lsp
12063 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12064 let buffer_changes = buffer_changes.clone();
12065 move |params, _| {
12066 buffer_changes.lock().extend(
12067 params
12068 .content_changes
12069 .into_iter()
12070 .map(|e| (e.range.unwrap(), e.text)),
12071 );
12072 }
12073 });
12074
12075 // Handle formatting requests to the language server.
12076 cx.lsp
12077 .set_request_handler::<lsp::request::Formatting, _, _>({
12078 let buffer_changes = buffer_changes.clone();
12079 move |_, _| {
12080 // When formatting is requested, trailing whitespace has already been stripped,
12081 // and the trailing newline has already been added.
12082 assert_eq!(
12083 &buffer_changes.lock()[1..],
12084 &[
12085 (
12086 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12087 "".into()
12088 ),
12089 (
12090 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12091 "".into()
12092 ),
12093 (
12094 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12095 "\n".into()
12096 ),
12097 ]
12098 );
12099
12100 // Insert blank lines between each line of the buffer.
12101 async move {
12102 Ok(Some(vec![
12103 lsp::TextEdit {
12104 range: lsp::Range::new(
12105 lsp::Position::new(1, 0),
12106 lsp::Position::new(1, 0),
12107 ),
12108 new_text: "\n".into(),
12109 },
12110 lsp::TextEdit {
12111 range: lsp::Range::new(
12112 lsp::Position::new(2, 0),
12113 lsp::Position::new(2, 0),
12114 ),
12115 new_text: "\n".into(),
12116 },
12117 ]))
12118 }
12119 }
12120 });
12121
12122 // After formatting the buffer, the trailing whitespace is stripped,
12123 // a newline is appended, and the edits provided by the language server
12124 // have been applied.
12125 format.await.unwrap();
12126 cx.assert_editor_state(
12127 &[
12128 "one", //
12129 "", //
12130 "twoˇ", //
12131 "", //
12132 "three", //
12133 "four", //
12134 "", //
12135 ]
12136 .join("\n"),
12137 );
12138
12139 // Undoing the formatting undoes the trailing whitespace removal, the
12140 // trailing newline, and the LSP edits.
12141 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12142 cx.assert_editor_state(
12143 &[
12144 "one ", //
12145 "twoˇ", //
12146 "three ", //
12147 "four", //
12148 ]
12149 .join("\n"),
12150 );
12151}
12152
12153#[gpui::test]
12154async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12155 cx: &mut TestAppContext,
12156) {
12157 init_test(cx, |_| {});
12158
12159 cx.update(|cx| {
12160 cx.update_global::<SettingsStore, _>(|settings, cx| {
12161 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12162 settings.auto_signature_help = Some(true);
12163 });
12164 });
12165 });
12166
12167 let mut cx = EditorLspTestContext::new_rust(
12168 lsp::ServerCapabilities {
12169 signature_help_provider: Some(lsp::SignatureHelpOptions {
12170 ..Default::default()
12171 }),
12172 ..Default::default()
12173 },
12174 cx,
12175 )
12176 .await;
12177
12178 let language = Language::new(
12179 LanguageConfig {
12180 name: "Rust".into(),
12181 brackets: BracketPairConfig {
12182 pairs: vec![
12183 BracketPair {
12184 start: "{".to_string(),
12185 end: "}".to_string(),
12186 close: true,
12187 surround: true,
12188 newline: true,
12189 },
12190 BracketPair {
12191 start: "(".to_string(),
12192 end: ")".to_string(),
12193 close: true,
12194 surround: true,
12195 newline: true,
12196 },
12197 BracketPair {
12198 start: "/*".to_string(),
12199 end: " */".to_string(),
12200 close: true,
12201 surround: true,
12202 newline: true,
12203 },
12204 BracketPair {
12205 start: "[".to_string(),
12206 end: "]".to_string(),
12207 close: false,
12208 surround: false,
12209 newline: true,
12210 },
12211 BracketPair {
12212 start: "\"".to_string(),
12213 end: "\"".to_string(),
12214 close: true,
12215 surround: true,
12216 newline: false,
12217 },
12218 BracketPair {
12219 start: "<".to_string(),
12220 end: ">".to_string(),
12221 close: false,
12222 surround: true,
12223 newline: true,
12224 },
12225 ],
12226 ..Default::default()
12227 },
12228 autoclose_before: "})]".to_string(),
12229 ..Default::default()
12230 },
12231 Some(tree_sitter_rust::LANGUAGE.into()),
12232 );
12233 let language = Arc::new(language);
12234
12235 cx.language_registry().add(language.clone());
12236 cx.update_buffer(|buffer, cx| {
12237 buffer.set_language(Some(language), cx);
12238 });
12239
12240 cx.set_state(
12241 &r#"
12242 fn main() {
12243 sampleˇ
12244 }
12245 "#
12246 .unindent(),
12247 );
12248
12249 cx.update_editor(|editor, window, cx| {
12250 editor.handle_input("(", window, cx);
12251 });
12252 cx.assert_editor_state(
12253 &"
12254 fn main() {
12255 sample(ˇ)
12256 }
12257 "
12258 .unindent(),
12259 );
12260
12261 let mocked_response = lsp::SignatureHelp {
12262 signatures: vec![lsp::SignatureInformation {
12263 label: "fn sample(param1: u8, param2: u8)".to_string(),
12264 documentation: None,
12265 parameters: Some(vec![
12266 lsp::ParameterInformation {
12267 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12268 documentation: None,
12269 },
12270 lsp::ParameterInformation {
12271 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12272 documentation: None,
12273 },
12274 ]),
12275 active_parameter: None,
12276 }],
12277 active_signature: Some(0),
12278 active_parameter: Some(0),
12279 };
12280 handle_signature_help_request(&mut cx, mocked_response).await;
12281
12282 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12283 .await;
12284
12285 cx.editor(|editor, _, _| {
12286 let signature_help_state = editor.signature_help_state.popover().cloned();
12287 let signature = signature_help_state.unwrap();
12288 assert_eq!(
12289 signature.signatures[signature.current_signature].label,
12290 "fn sample(param1: u8, param2: u8)"
12291 );
12292 });
12293}
12294
12295#[gpui::test]
12296async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12297 init_test(cx, |_| {});
12298
12299 cx.update(|cx| {
12300 cx.update_global::<SettingsStore, _>(|settings, cx| {
12301 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12302 settings.auto_signature_help = Some(false);
12303 settings.show_signature_help_after_edits = Some(false);
12304 });
12305 });
12306 });
12307
12308 let mut cx = EditorLspTestContext::new_rust(
12309 lsp::ServerCapabilities {
12310 signature_help_provider: Some(lsp::SignatureHelpOptions {
12311 ..Default::default()
12312 }),
12313 ..Default::default()
12314 },
12315 cx,
12316 )
12317 .await;
12318
12319 let language = Language::new(
12320 LanguageConfig {
12321 name: "Rust".into(),
12322 brackets: BracketPairConfig {
12323 pairs: vec![
12324 BracketPair {
12325 start: "{".to_string(),
12326 end: "}".to_string(),
12327 close: true,
12328 surround: true,
12329 newline: true,
12330 },
12331 BracketPair {
12332 start: "(".to_string(),
12333 end: ")".to_string(),
12334 close: true,
12335 surround: true,
12336 newline: true,
12337 },
12338 BracketPair {
12339 start: "/*".to_string(),
12340 end: " */".to_string(),
12341 close: true,
12342 surround: true,
12343 newline: true,
12344 },
12345 BracketPair {
12346 start: "[".to_string(),
12347 end: "]".to_string(),
12348 close: false,
12349 surround: false,
12350 newline: true,
12351 },
12352 BracketPair {
12353 start: "\"".to_string(),
12354 end: "\"".to_string(),
12355 close: true,
12356 surround: true,
12357 newline: false,
12358 },
12359 BracketPair {
12360 start: "<".to_string(),
12361 end: ">".to_string(),
12362 close: false,
12363 surround: true,
12364 newline: true,
12365 },
12366 ],
12367 ..Default::default()
12368 },
12369 autoclose_before: "})]".to_string(),
12370 ..Default::default()
12371 },
12372 Some(tree_sitter_rust::LANGUAGE.into()),
12373 );
12374 let language = Arc::new(language);
12375
12376 cx.language_registry().add(language.clone());
12377 cx.update_buffer(|buffer, cx| {
12378 buffer.set_language(Some(language), cx);
12379 });
12380
12381 // Ensure that signature_help is not called when no signature help is enabled.
12382 cx.set_state(
12383 &r#"
12384 fn main() {
12385 sampleˇ
12386 }
12387 "#
12388 .unindent(),
12389 );
12390 cx.update_editor(|editor, window, cx| {
12391 editor.handle_input("(", window, cx);
12392 });
12393 cx.assert_editor_state(
12394 &"
12395 fn main() {
12396 sample(ˇ)
12397 }
12398 "
12399 .unindent(),
12400 );
12401 cx.editor(|editor, _, _| {
12402 assert!(editor.signature_help_state.task().is_none());
12403 });
12404
12405 let mocked_response = lsp::SignatureHelp {
12406 signatures: vec![lsp::SignatureInformation {
12407 label: "fn sample(param1: u8, param2: u8)".to_string(),
12408 documentation: None,
12409 parameters: Some(vec![
12410 lsp::ParameterInformation {
12411 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12412 documentation: None,
12413 },
12414 lsp::ParameterInformation {
12415 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12416 documentation: None,
12417 },
12418 ]),
12419 active_parameter: None,
12420 }],
12421 active_signature: Some(0),
12422 active_parameter: Some(0),
12423 };
12424
12425 // Ensure that signature_help is called when enabled afte edits
12426 cx.update(|_, cx| {
12427 cx.update_global::<SettingsStore, _>(|settings, cx| {
12428 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12429 settings.auto_signature_help = Some(false);
12430 settings.show_signature_help_after_edits = Some(true);
12431 });
12432 });
12433 });
12434 cx.set_state(
12435 &r#"
12436 fn main() {
12437 sampleˇ
12438 }
12439 "#
12440 .unindent(),
12441 );
12442 cx.update_editor(|editor, window, cx| {
12443 editor.handle_input("(", window, cx);
12444 });
12445 cx.assert_editor_state(
12446 &"
12447 fn main() {
12448 sample(ˇ)
12449 }
12450 "
12451 .unindent(),
12452 );
12453 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12454 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12455 .await;
12456 cx.update_editor(|editor, _, _| {
12457 let signature_help_state = editor.signature_help_state.popover().cloned();
12458 assert!(signature_help_state.is_some());
12459 let signature = signature_help_state.unwrap();
12460 assert_eq!(
12461 signature.signatures[signature.current_signature].label,
12462 "fn sample(param1: u8, param2: u8)"
12463 );
12464 editor.signature_help_state = SignatureHelpState::default();
12465 });
12466
12467 // Ensure that signature_help is called when auto signature help override is enabled
12468 cx.update(|_, cx| {
12469 cx.update_global::<SettingsStore, _>(|settings, cx| {
12470 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12471 settings.auto_signature_help = Some(true);
12472 settings.show_signature_help_after_edits = Some(false);
12473 });
12474 });
12475 });
12476 cx.set_state(
12477 &r#"
12478 fn main() {
12479 sampleˇ
12480 }
12481 "#
12482 .unindent(),
12483 );
12484 cx.update_editor(|editor, window, cx| {
12485 editor.handle_input("(", window, cx);
12486 });
12487 cx.assert_editor_state(
12488 &"
12489 fn main() {
12490 sample(ˇ)
12491 }
12492 "
12493 .unindent(),
12494 );
12495 handle_signature_help_request(&mut cx, mocked_response).await;
12496 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12497 .await;
12498 cx.editor(|editor, _, _| {
12499 let signature_help_state = editor.signature_help_state.popover().cloned();
12500 assert!(signature_help_state.is_some());
12501 let signature = signature_help_state.unwrap();
12502 assert_eq!(
12503 signature.signatures[signature.current_signature].label,
12504 "fn sample(param1: u8, param2: u8)"
12505 );
12506 });
12507}
12508
12509#[gpui::test]
12510async fn test_signature_help(cx: &mut TestAppContext) {
12511 init_test(cx, |_| {});
12512 cx.update(|cx| {
12513 cx.update_global::<SettingsStore, _>(|settings, cx| {
12514 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12515 settings.auto_signature_help = Some(true);
12516 });
12517 });
12518 });
12519
12520 let mut cx = EditorLspTestContext::new_rust(
12521 lsp::ServerCapabilities {
12522 signature_help_provider: Some(lsp::SignatureHelpOptions {
12523 ..Default::default()
12524 }),
12525 ..Default::default()
12526 },
12527 cx,
12528 )
12529 .await;
12530
12531 // A test that directly calls `show_signature_help`
12532 cx.update_editor(|editor, window, cx| {
12533 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12534 });
12535
12536 let mocked_response = lsp::SignatureHelp {
12537 signatures: vec![lsp::SignatureInformation {
12538 label: "fn sample(param1: u8, param2: u8)".to_string(),
12539 documentation: None,
12540 parameters: Some(vec![
12541 lsp::ParameterInformation {
12542 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12543 documentation: None,
12544 },
12545 lsp::ParameterInformation {
12546 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12547 documentation: None,
12548 },
12549 ]),
12550 active_parameter: None,
12551 }],
12552 active_signature: Some(0),
12553 active_parameter: Some(0),
12554 };
12555 handle_signature_help_request(&mut cx, mocked_response).await;
12556
12557 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12558 .await;
12559
12560 cx.editor(|editor, _, _| {
12561 let signature_help_state = editor.signature_help_state.popover().cloned();
12562 assert!(signature_help_state.is_some());
12563 let signature = signature_help_state.unwrap();
12564 assert_eq!(
12565 signature.signatures[signature.current_signature].label,
12566 "fn sample(param1: u8, param2: u8)"
12567 );
12568 });
12569
12570 // When exiting outside from inside the brackets, `signature_help` is closed.
12571 cx.set_state(indoc! {"
12572 fn main() {
12573 sample(ˇ);
12574 }
12575
12576 fn sample(param1: u8, param2: u8) {}
12577 "});
12578
12579 cx.update_editor(|editor, window, cx| {
12580 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12581 s.select_ranges([0..0])
12582 });
12583 });
12584
12585 let mocked_response = lsp::SignatureHelp {
12586 signatures: Vec::new(),
12587 active_signature: None,
12588 active_parameter: None,
12589 };
12590 handle_signature_help_request(&mut cx, mocked_response).await;
12591
12592 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12593 .await;
12594
12595 cx.editor(|editor, _, _| {
12596 assert!(!editor.signature_help_state.is_shown());
12597 });
12598
12599 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12600 cx.set_state(indoc! {"
12601 fn main() {
12602 sample(ˇ);
12603 }
12604
12605 fn sample(param1: u8, param2: u8) {}
12606 "});
12607
12608 let mocked_response = lsp::SignatureHelp {
12609 signatures: vec![lsp::SignatureInformation {
12610 label: "fn sample(param1: u8, param2: u8)".to_string(),
12611 documentation: None,
12612 parameters: Some(vec![
12613 lsp::ParameterInformation {
12614 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12615 documentation: None,
12616 },
12617 lsp::ParameterInformation {
12618 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12619 documentation: None,
12620 },
12621 ]),
12622 active_parameter: None,
12623 }],
12624 active_signature: Some(0),
12625 active_parameter: Some(0),
12626 };
12627 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12628 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12629 .await;
12630 cx.editor(|editor, _, _| {
12631 assert!(editor.signature_help_state.is_shown());
12632 });
12633
12634 // Restore the popover with more parameter input
12635 cx.set_state(indoc! {"
12636 fn main() {
12637 sample(param1, param2ˇ);
12638 }
12639
12640 fn sample(param1: u8, param2: u8) {}
12641 "});
12642
12643 let mocked_response = lsp::SignatureHelp {
12644 signatures: vec![lsp::SignatureInformation {
12645 label: "fn sample(param1: u8, param2: u8)".to_string(),
12646 documentation: None,
12647 parameters: Some(vec![
12648 lsp::ParameterInformation {
12649 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12650 documentation: None,
12651 },
12652 lsp::ParameterInformation {
12653 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12654 documentation: None,
12655 },
12656 ]),
12657 active_parameter: None,
12658 }],
12659 active_signature: Some(0),
12660 active_parameter: Some(1),
12661 };
12662 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12663 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12664 .await;
12665
12666 // When selecting a range, the popover is gone.
12667 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12668 cx.update_editor(|editor, window, cx| {
12669 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12670 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12671 })
12672 });
12673 cx.assert_editor_state(indoc! {"
12674 fn main() {
12675 sample(param1, «ˇparam2»);
12676 }
12677
12678 fn sample(param1: u8, param2: u8) {}
12679 "});
12680 cx.editor(|editor, _, _| {
12681 assert!(!editor.signature_help_state.is_shown());
12682 });
12683
12684 // When unselecting again, the popover is back if within the brackets.
12685 cx.update_editor(|editor, window, cx| {
12686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12687 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12688 })
12689 });
12690 cx.assert_editor_state(indoc! {"
12691 fn main() {
12692 sample(param1, ˇparam2);
12693 }
12694
12695 fn sample(param1: u8, param2: u8) {}
12696 "});
12697 handle_signature_help_request(&mut cx, mocked_response).await;
12698 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12699 .await;
12700 cx.editor(|editor, _, _| {
12701 assert!(editor.signature_help_state.is_shown());
12702 });
12703
12704 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12705 cx.update_editor(|editor, window, cx| {
12706 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12707 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12708 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12709 })
12710 });
12711 cx.assert_editor_state(indoc! {"
12712 fn main() {
12713 sample(param1, ˇparam2);
12714 }
12715
12716 fn sample(param1: u8, param2: u8) {}
12717 "});
12718
12719 let mocked_response = lsp::SignatureHelp {
12720 signatures: vec![lsp::SignatureInformation {
12721 label: "fn sample(param1: u8, param2: u8)".to_string(),
12722 documentation: None,
12723 parameters: Some(vec![
12724 lsp::ParameterInformation {
12725 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12726 documentation: None,
12727 },
12728 lsp::ParameterInformation {
12729 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12730 documentation: None,
12731 },
12732 ]),
12733 active_parameter: None,
12734 }],
12735 active_signature: Some(0),
12736 active_parameter: Some(1),
12737 };
12738 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12739 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12740 .await;
12741 cx.update_editor(|editor, _, cx| {
12742 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12743 });
12744 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12745 .await;
12746 cx.update_editor(|editor, window, cx| {
12747 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12748 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12749 })
12750 });
12751 cx.assert_editor_state(indoc! {"
12752 fn main() {
12753 sample(param1, «ˇparam2»);
12754 }
12755
12756 fn sample(param1: u8, param2: u8) {}
12757 "});
12758 cx.update_editor(|editor, window, cx| {
12759 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12760 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12761 })
12762 });
12763 cx.assert_editor_state(indoc! {"
12764 fn main() {
12765 sample(param1, ˇparam2);
12766 }
12767
12768 fn sample(param1: u8, param2: u8) {}
12769 "});
12770 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
12771 .await;
12772}
12773
12774#[gpui::test]
12775async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
12776 init_test(cx, |_| {});
12777
12778 let mut cx = EditorLspTestContext::new_rust(
12779 lsp::ServerCapabilities {
12780 signature_help_provider: Some(lsp::SignatureHelpOptions {
12781 ..Default::default()
12782 }),
12783 ..Default::default()
12784 },
12785 cx,
12786 )
12787 .await;
12788
12789 cx.set_state(indoc! {"
12790 fn main() {
12791 overloadedˇ
12792 }
12793 "});
12794
12795 cx.update_editor(|editor, window, cx| {
12796 editor.handle_input("(", window, cx);
12797 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12798 });
12799
12800 // Mock response with 3 signatures
12801 let mocked_response = lsp::SignatureHelp {
12802 signatures: vec![
12803 lsp::SignatureInformation {
12804 label: "fn overloaded(x: i32)".to_string(),
12805 documentation: None,
12806 parameters: Some(vec![lsp::ParameterInformation {
12807 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12808 documentation: None,
12809 }]),
12810 active_parameter: None,
12811 },
12812 lsp::SignatureInformation {
12813 label: "fn overloaded(x: i32, y: i32)".to_string(),
12814 documentation: None,
12815 parameters: Some(vec![
12816 lsp::ParameterInformation {
12817 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12818 documentation: None,
12819 },
12820 lsp::ParameterInformation {
12821 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12822 documentation: None,
12823 },
12824 ]),
12825 active_parameter: None,
12826 },
12827 lsp::SignatureInformation {
12828 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
12829 documentation: None,
12830 parameters: Some(vec![
12831 lsp::ParameterInformation {
12832 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12833 documentation: None,
12834 },
12835 lsp::ParameterInformation {
12836 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12837 documentation: None,
12838 },
12839 lsp::ParameterInformation {
12840 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
12841 documentation: None,
12842 },
12843 ]),
12844 active_parameter: None,
12845 },
12846 ],
12847 active_signature: Some(1),
12848 active_parameter: Some(0),
12849 };
12850 handle_signature_help_request(&mut cx, mocked_response).await;
12851
12852 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12853 .await;
12854
12855 // Verify we have multiple signatures and the right one is selected
12856 cx.editor(|editor, _, _| {
12857 let popover = editor.signature_help_state.popover().cloned().unwrap();
12858 assert_eq!(popover.signatures.len(), 3);
12859 // active_signature was 1, so that should be the current
12860 assert_eq!(popover.current_signature, 1);
12861 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
12862 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
12863 assert_eq!(
12864 popover.signatures[2].label,
12865 "fn overloaded(x: i32, y: i32, z: i32)"
12866 );
12867 });
12868
12869 // Test navigation functionality
12870 cx.update_editor(|editor, window, cx| {
12871 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12872 });
12873
12874 cx.editor(|editor, _, _| {
12875 let popover = editor.signature_help_state.popover().cloned().unwrap();
12876 assert_eq!(popover.current_signature, 2);
12877 });
12878
12879 // Test wrap around
12880 cx.update_editor(|editor, window, cx| {
12881 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12882 });
12883
12884 cx.editor(|editor, _, _| {
12885 let popover = editor.signature_help_state.popover().cloned().unwrap();
12886 assert_eq!(popover.current_signature, 0);
12887 });
12888
12889 // Test previous navigation
12890 cx.update_editor(|editor, window, cx| {
12891 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12892 });
12893
12894 cx.editor(|editor, _, _| {
12895 let popover = editor.signature_help_state.popover().cloned().unwrap();
12896 assert_eq!(popover.current_signature, 2);
12897 });
12898}
12899
12900#[gpui::test]
12901async fn test_completion_mode(cx: &mut TestAppContext) {
12902 init_test(cx, |_| {});
12903 let mut cx = EditorLspTestContext::new_rust(
12904 lsp::ServerCapabilities {
12905 completion_provider: Some(lsp::CompletionOptions {
12906 resolve_provider: Some(true),
12907 ..Default::default()
12908 }),
12909 ..Default::default()
12910 },
12911 cx,
12912 )
12913 .await;
12914
12915 struct Run {
12916 run_description: &'static str,
12917 initial_state: String,
12918 buffer_marked_text: String,
12919 completion_label: &'static str,
12920 completion_text: &'static str,
12921 expected_with_insert_mode: String,
12922 expected_with_replace_mode: String,
12923 expected_with_replace_subsequence_mode: String,
12924 expected_with_replace_suffix_mode: String,
12925 }
12926
12927 let runs = [
12928 Run {
12929 run_description: "Start of word matches completion text",
12930 initial_state: "before ediˇ after".into(),
12931 buffer_marked_text: "before <edi|> after".into(),
12932 completion_label: "editor",
12933 completion_text: "editor",
12934 expected_with_insert_mode: "before editorˇ after".into(),
12935 expected_with_replace_mode: "before editorˇ after".into(),
12936 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12937 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12938 },
12939 Run {
12940 run_description: "Accept same text at the middle of the word",
12941 initial_state: "before ediˇtor after".into(),
12942 buffer_marked_text: "before <edi|tor> after".into(),
12943 completion_label: "editor",
12944 completion_text: "editor",
12945 expected_with_insert_mode: "before editorˇtor after".into(),
12946 expected_with_replace_mode: "before editorˇ after".into(),
12947 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12948 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12949 },
12950 Run {
12951 run_description: "End of word matches completion text -- cursor at end",
12952 initial_state: "before torˇ after".into(),
12953 buffer_marked_text: "before <tor|> after".into(),
12954 completion_label: "editor",
12955 completion_text: "editor",
12956 expected_with_insert_mode: "before editorˇ after".into(),
12957 expected_with_replace_mode: "before editorˇ after".into(),
12958 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12959 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12960 },
12961 Run {
12962 run_description: "End of word matches completion text -- cursor at start",
12963 initial_state: "before ˇtor after".into(),
12964 buffer_marked_text: "before <|tor> after".into(),
12965 completion_label: "editor",
12966 completion_text: "editor",
12967 expected_with_insert_mode: "before editorˇtor after".into(),
12968 expected_with_replace_mode: "before editorˇ after".into(),
12969 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12970 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12971 },
12972 Run {
12973 run_description: "Prepend text containing whitespace",
12974 initial_state: "pˇfield: bool".into(),
12975 buffer_marked_text: "<p|field>: bool".into(),
12976 completion_label: "pub ",
12977 completion_text: "pub ",
12978 expected_with_insert_mode: "pub ˇfield: bool".into(),
12979 expected_with_replace_mode: "pub ˇ: bool".into(),
12980 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12981 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12982 },
12983 Run {
12984 run_description: "Add element to start of list",
12985 initial_state: "[element_ˇelement_2]".into(),
12986 buffer_marked_text: "[<element_|element_2>]".into(),
12987 completion_label: "element_1",
12988 completion_text: "element_1",
12989 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
12990 expected_with_replace_mode: "[element_1ˇ]".into(),
12991 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
12992 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
12993 },
12994 Run {
12995 run_description: "Add element to start of list -- first and second elements are equal",
12996 initial_state: "[elˇelement]".into(),
12997 buffer_marked_text: "[<el|element>]".into(),
12998 completion_label: "element",
12999 completion_text: "element",
13000 expected_with_insert_mode: "[elementˇelement]".into(),
13001 expected_with_replace_mode: "[elementˇ]".into(),
13002 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13003 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13004 },
13005 Run {
13006 run_description: "Ends with matching suffix",
13007 initial_state: "SubˇError".into(),
13008 buffer_marked_text: "<Sub|Error>".into(),
13009 completion_label: "SubscriptionError",
13010 completion_text: "SubscriptionError",
13011 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13012 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13013 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13014 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13015 },
13016 Run {
13017 run_description: "Suffix is a subsequence -- contiguous",
13018 initial_state: "SubˇErr".into(),
13019 buffer_marked_text: "<Sub|Err>".into(),
13020 completion_label: "SubscriptionError",
13021 completion_text: "SubscriptionError",
13022 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13023 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13024 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13025 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13026 },
13027 Run {
13028 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13029 initial_state: "Suˇscrirr".into(),
13030 buffer_marked_text: "<Su|scrirr>".into(),
13031 completion_label: "SubscriptionError",
13032 completion_text: "SubscriptionError",
13033 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13034 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13035 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13036 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13037 },
13038 Run {
13039 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13040 initial_state: "foo(indˇix)".into(),
13041 buffer_marked_text: "foo(<ind|ix>)".into(),
13042 completion_label: "node_index",
13043 completion_text: "node_index",
13044 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13045 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13046 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13047 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13048 },
13049 Run {
13050 run_description: "Replace range ends before cursor - should extend to cursor",
13051 initial_state: "before editˇo after".into(),
13052 buffer_marked_text: "before <{ed}>it|o after".into(),
13053 completion_label: "editor",
13054 completion_text: "editor",
13055 expected_with_insert_mode: "before editorˇo after".into(),
13056 expected_with_replace_mode: "before editorˇo after".into(),
13057 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13058 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13059 },
13060 Run {
13061 run_description: "Uses label for suffix matching",
13062 initial_state: "before ediˇtor after".into(),
13063 buffer_marked_text: "before <edi|tor> after".into(),
13064 completion_label: "editor",
13065 completion_text: "editor()",
13066 expected_with_insert_mode: "before editor()ˇtor after".into(),
13067 expected_with_replace_mode: "before editor()ˇ after".into(),
13068 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13069 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13070 },
13071 Run {
13072 run_description: "Case insensitive subsequence and suffix matching",
13073 initial_state: "before EDiˇtoR after".into(),
13074 buffer_marked_text: "before <EDi|toR> after".into(),
13075 completion_label: "editor",
13076 completion_text: "editor",
13077 expected_with_insert_mode: "before editorˇtoR after".into(),
13078 expected_with_replace_mode: "before editorˇ after".into(),
13079 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13080 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13081 },
13082 ];
13083
13084 for run in runs {
13085 let run_variations = [
13086 (LspInsertMode::Insert, run.expected_with_insert_mode),
13087 (LspInsertMode::Replace, run.expected_with_replace_mode),
13088 (
13089 LspInsertMode::ReplaceSubsequence,
13090 run.expected_with_replace_subsequence_mode,
13091 ),
13092 (
13093 LspInsertMode::ReplaceSuffix,
13094 run.expected_with_replace_suffix_mode,
13095 ),
13096 ];
13097
13098 for (lsp_insert_mode, expected_text) in run_variations {
13099 eprintln!(
13100 "run = {:?}, mode = {lsp_insert_mode:.?}",
13101 run.run_description,
13102 );
13103
13104 update_test_language_settings(&mut cx, |settings| {
13105 settings.defaults.completions = Some(CompletionSettings {
13106 lsp_insert_mode,
13107 words: WordsCompletionMode::Disabled,
13108 words_min_length: 0,
13109 lsp: true,
13110 lsp_fetch_timeout_ms: 0,
13111 });
13112 });
13113
13114 cx.set_state(&run.initial_state);
13115 cx.update_editor(|editor, window, cx| {
13116 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13117 });
13118
13119 let counter = Arc::new(AtomicUsize::new(0));
13120 handle_completion_request_with_insert_and_replace(
13121 &mut cx,
13122 &run.buffer_marked_text,
13123 vec![(run.completion_label, run.completion_text)],
13124 counter.clone(),
13125 )
13126 .await;
13127 cx.condition(|editor, _| editor.context_menu_visible())
13128 .await;
13129 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13130
13131 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13132 editor
13133 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13134 .unwrap()
13135 });
13136 cx.assert_editor_state(&expected_text);
13137 handle_resolve_completion_request(&mut cx, None).await;
13138 apply_additional_edits.await.unwrap();
13139 }
13140 }
13141}
13142
13143#[gpui::test]
13144async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13145 init_test(cx, |_| {});
13146 let mut cx = EditorLspTestContext::new_rust(
13147 lsp::ServerCapabilities {
13148 completion_provider: Some(lsp::CompletionOptions {
13149 resolve_provider: Some(true),
13150 ..Default::default()
13151 }),
13152 ..Default::default()
13153 },
13154 cx,
13155 )
13156 .await;
13157
13158 let initial_state = "SubˇError";
13159 let buffer_marked_text = "<Sub|Error>";
13160 let completion_text = "SubscriptionError";
13161 let expected_with_insert_mode = "SubscriptionErrorˇError";
13162 let expected_with_replace_mode = "SubscriptionErrorˇ";
13163
13164 update_test_language_settings(&mut cx, |settings| {
13165 settings.defaults.completions = Some(CompletionSettings {
13166 words: WordsCompletionMode::Disabled,
13167 words_min_length: 0,
13168 // set the opposite here to ensure that the action is overriding the default behavior
13169 lsp_insert_mode: LspInsertMode::Insert,
13170 lsp: true,
13171 lsp_fetch_timeout_ms: 0,
13172 });
13173 });
13174
13175 cx.set_state(initial_state);
13176 cx.update_editor(|editor, window, cx| {
13177 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13178 });
13179
13180 let counter = Arc::new(AtomicUsize::new(0));
13181 handle_completion_request_with_insert_and_replace(
13182 &mut cx,
13183 buffer_marked_text,
13184 vec![(completion_text, completion_text)],
13185 counter.clone(),
13186 )
13187 .await;
13188 cx.condition(|editor, _| editor.context_menu_visible())
13189 .await;
13190 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13191
13192 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13193 editor
13194 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13195 .unwrap()
13196 });
13197 cx.assert_editor_state(expected_with_replace_mode);
13198 handle_resolve_completion_request(&mut cx, None).await;
13199 apply_additional_edits.await.unwrap();
13200
13201 update_test_language_settings(&mut cx, |settings| {
13202 settings.defaults.completions = Some(CompletionSettings {
13203 words: WordsCompletionMode::Disabled,
13204 words_min_length: 0,
13205 // set the opposite here to ensure that the action is overriding the default behavior
13206 lsp_insert_mode: LspInsertMode::Replace,
13207 lsp: true,
13208 lsp_fetch_timeout_ms: 0,
13209 });
13210 });
13211
13212 cx.set_state(initial_state);
13213 cx.update_editor(|editor, window, cx| {
13214 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13215 });
13216 handle_completion_request_with_insert_and_replace(
13217 &mut cx,
13218 buffer_marked_text,
13219 vec![(completion_text, completion_text)],
13220 counter.clone(),
13221 )
13222 .await;
13223 cx.condition(|editor, _| editor.context_menu_visible())
13224 .await;
13225 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13226
13227 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13228 editor
13229 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13230 .unwrap()
13231 });
13232 cx.assert_editor_state(expected_with_insert_mode);
13233 handle_resolve_completion_request(&mut cx, None).await;
13234 apply_additional_edits.await.unwrap();
13235}
13236
13237#[gpui::test]
13238async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13239 init_test(cx, |_| {});
13240 let mut cx = EditorLspTestContext::new_rust(
13241 lsp::ServerCapabilities {
13242 completion_provider: Some(lsp::CompletionOptions {
13243 resolve_provider: Some(true),
13244 ..Default::default()
13245 }),
13246 ..Default::default()
13247 },
13248 cx,
13249 )
13250 .await;
13251
13252 // scenario: surrounding text matches completion text
13253 let completion_text = "to_offset";
13254 let initial_state = indoc! {"
13255 1. buf.to_offˇsuffix
13256 2. buf.to_offˇsuf
13257 3. buf.to_offˇfix
13258 4. buf.to_offˇ
13259 5. into_offˇensive
13260 6. ˇsuffix
13261 7. let ˇ //
13262 8. aaˇzz
13263 9. buf.to_off«zzzzzˇ»suffix
13264 10. buf.«ˇzzzzz»suffix
13265 11. to_off«ˇzzzzz»
13266
13267 buf.to_offˇsuffix // newest cursor
13268 "};
13269 let completion_marked_buffer = indoc! {"
13270 1. buf.to_offsuffix
13271 2. buf.to_offsuf
13272 3. buf.to_offfix
13273 4. buf.to_off
13274 5. into_offensive
13275 6. suffix
13276 7. let //
13277 8. aazz
13278 9. buf.to_offzzzzzsuffix
13279 10. buf.zzzzzsuffix
13280 11. to_offzzzzz
13281
13282 buf.<to_off|suffix> // newest cursor
13283 "};
13284 let expected = indoc! {"
13285 1. buf.to_offsetˇ
13286 2. buf.to_offsetˇsuf
13287 3. buf.to_offsetˇfix
13288 4. buf.to_offsetˇ
13289 5. into_offsetˇensive
13290 6. to_offsetˇsuffix
13291 7. let to_offsetˇ //
13292 8. aato_offsetˇzz
13293 9. buf.to_offsetˇ
13294 10. buf.to_offsetˇsuffix
13295 11. to_offsetˇ
13296
13297 buf.to_offsetˇ // newest cursor
13298 "};
13299 cx.set_state(initial_state);
13300 cx.update_editor(|editor, window, cx| {
13301 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13302 });
13303 handle_completion_request_with_insert_and_replace(
13304 &mut cx,
13305 completion_marked_buffer,
13306 vec![(completion_text, completion_text)],
13307 Arc::new(AtomicUsize::new(0)),
13308 )
13309 .await;
13310 cx.condition(|editor, _| editor.context_menu_visible())
13311 .await;
13312 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13313 editor
13314 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13315 .unwrap()
13316 });
13317 cx.assert_editor_state(expected);
13318 handle_resolve_completion_request(&mut cx, None).await;
13319 apply_additional_edits.await.unwrap();
13320
13321 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13322 let completion_text = "foo_and_bar";
13323 let initial_state = indoc! {"
13324 1. ooanbˇ
13325 2. zooanbˇ
13326 3. ooanbˇz
13327 4. zooanbˇz
13328 5. ooanˇ
13329 6. oanbˇ
13330
13331 ooanbˇ
13332 "};
13333 let completion_marked_buffer = indoc! {"
13334 1. ooanb
13335 2. zooanb
13336 3. ooanbz
13337 4. zooanbz
13338 5. ooan
13339 6. oanb
13340
13341 <ooanb|>
13342 "};
13343 let expected = indoc! {"
13344 1. foo_and_barˇ
13345 2. zfoo_and_barˇ
13346 3. foo_and_barˇz
13347 4. zfoo_and_barˇz
13348 5. ooanfoo_and_barˇ
13349 6. oanbfoo_and_barˇ
13350
13351 foo_and_barˇ
13352 "};
13353 cx.set_state(initial_state);
13354 cx.update_editor(|editor, window, cx| {
13355 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13356 });
13357 handle_completion_request_with_insert_and_replace(
13358 &mut cx,
13359 completion_marked_buffer,
13360 vec![(completion_text, completion_text)],
13361 Arc::new(AtomicUsize::new(0)),
13362 )
13363 .await;
13364 cx.condition(|editor, _| editor.context_menu_visible())
13365 .await;
13366 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13367 editor
13368 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13369 .unwrap()
13370 });
13371 cx.assert_editor_state(expected);
13372 handle_resolve_completion_request(&mut cx, None).await;
13373 apply_additional_edits.await.unwrap();
13374
13375 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13376 // (expects the same as if it was inserted at the end)
13377 let completion_text = "foo_and_bar";
13378 let initial_state = indoc! {"
13379 1. ooˇanb
13380 2. zooˇanb
13381 3. ooˇanbz
13382 4. zooˇanbz
13383
13384 ooˇanb
13385 "};
13386 let completion_marked_buffer = indoc! {"
13387 1. ooanb
13388 2. zooanb
13389 3. ooanbz
13390 4. zooanbz
13391
13392 <oo|anb>
13393 "};
13394 let expected = indoc! {"
13395 1. foo_and_barˇ
13396 2. zfoo_and_barˇ
13397 3. foo_and_barˇz
13398 4. zfoo_and_barˇz
13399
13400 foo_and_barˇ
13401 "};
13402 cx.set_state(initial_state);
13403 cx.update_editor(|editor, window, cx| {
13404 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13405 });
13406 handle_completion_request_with_insert_and_replace(
13407 &mut cx,
13408 completion_marked_buffer,
13409 vec![(completion_text, completion_text)],
13410 Arc::new(AtomicUsize::new(0)),
13411 )
13412 .await;
13413 cx.condition(|editor, _| editor.context_menu_visible())
13414 .await;
13415 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13416 editor
13417 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13418 .unwrap()
13419 });
13420 cx.assert_editor_state(expected);
13421 handle_resolve_completion_request(&mut cx, None).await;
13422 apply_additional_edits.await.unwrap();
13423}
13424
13425// This used to crash
13426#[gpui::test]
13427async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13428 init_test(cx, |_| {});
13429
13430 let buffer_text = indoc! {"
13431 fn main() {
13432 10.satu;
13433
13434 //
13435 // separate cursors so they open in different excerpts (manually reproducible)
13436 //
13437
13438 10.satu20;
13439 }
13440 "};
13441 let multibuffer_text_with_selections = indoc! {"
13442 fn main() {
13443 10.satuˇ;
13444
13445 //
13446
13447 //
13448
13449 10.satuˇ20;
13450 }
13451 "};
13452 let expected_multibuffer = indoc! {"
13453 fn main() {
13454 10.saturating_sub()ˇ;
13455
13456 //
13457
13458 //
13459
13460 10.saturating_sub()ˇ;
13461 }
13462 "};
13463
13464 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13465 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13466
13467 let fs = FakeFs::new(cx.executor());
13468 fs.insert_tree(
13469 path!("/a"),
13470 json!({
13471 "main.rs": buffer_text,
13472 }),
13473 )
13474 .await;
13475
13476 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13477 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13478 language_registry.add(rust_lang());
13479 let mut fake_servers = language_registry.register_fake_lsp(
13480 "Rust",
13481 FakeLspAdapter {
13482 capabilities: lsp::ServerCapabilities {
13483 completion_provider: Some(lsp::CompletionOptions {
13484 resolve_provider: None,
13485 ..lsp::CompletionOptions::default()
13486 }),
13487 ..lsp::ServerCapabilities::default()
13488 },
13489 ..FakeLspAdapter::default()
13490 },
13491 );
13492 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13493 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13494 let buffer = project
13495 .update(cx, |project, cx| {
13496 project.open_local_buffer(path!("/a/main.rs"), cx)
13497 })
13498 .await
13499 .unwrap();
13500
13501 let multi_buffer = cx.new(|cx| {
13502 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13503 multi_buffer.push_excerpts(
13504 buffer.clone(),
13505 [ExcerptRange::new(0..first_excerpt_end)],
13506 cx,
13507 );
13508 multi_buffer.push_excerpts(
13509 buffer.clone(),
13510 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13511 cx,
13512 );
13513 multi_buffer
13514 });
13515
13516 let editor = workspace
13517 .update(cx, |_, window, cx| {
13518 cx.new(|cx| {
13519 Editor::new(
13520 EditorMode::Full {
13521 scale_ui_elements_with_buffer_font_size: false,
13522 show_active_line_background: false,
13523 sized_by_content: false,
13524 },
13525 multi_buffer.clone(),
13526 Some(project.clone()),
13527 window,
13528 cx,
13529 )
13530 })
13531 })
13532 .unwrap();
13533
13534 let pane = workspace
13535 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13536 .unwrap();
13537 pane.update_in(cx, |pane, window, cx| {
13538 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13539 });
13540
13541 let fake_server = fake_servers.next().await.unwrap();
13542
13543 editor.update_in(cx, |editor, window, cx| {
13544 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13545 s.select_ranges([
13546 Point::new(1, 11)..Point::new(1, 11),
13547 Point::new(7, 11)..Point::new(7, 11),
13548 ])
13549 });
13550
13551 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13552 });
13553
13554 editor.update_in(cx, |editor, window, cx| {
13555 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13556 });
13557
13558 fake_server
13559 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13560 let completion_item = lsp::CompletionItem {
13561 label: "saturating_sub()".into(),
13562 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13563 lsp::InsertReplaceEdit {
13564 new_text: "saturating_sub()".to_owned(),
13565 insert: lsp::Range::new(
13566 lsp::Position::new(7, 7),
13567 lsp::Position::new(7, 11),
13568 ),
13569 replace: lsp::Range::new(
13570 lsp::Position::new(7, 7),
13571 lsp::Position::new(7, 13),
13572 ),
13573 },
13574 )),
13575 ..lsp::CompletionItem::default()
13576 };
13577
13578 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13579 })
13580 .next()
13581 .await
13582 .unwrap();
13583
13584 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13585 .await;
13586
13587 editor
13588 .update_in(cx, |editor, window, cx| {
13589 editor
13590 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13591 .unwrap()
13592 })
13593 .await
13594 .unwrap();
13595
13596 editor.update(cx, |editor, cx| {
13597 assert_text_with_selections(editor, expected_multibuffer, cx);
13598 })
13599}
13600
13601#[gpui::test]
13602async fn test_completion(cx: &mut TestAppContext) {
13603 init_test(cx, |_| {});
13604
13605 let mut cx = EditorLspTestContext::new_rust(
13606 lsp::ServerCapabilities {
13607 completion_provider: Some(lsp::CompletionOptions {
13608 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13609 resolve_provider: Some(true),
13610 ..Default::default()
13611 }),
13612 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13613 ..Default::default()
13614 },
13615 cx,
13616 )
13617 .await;
13618 let counter = Arc::new(AtomicUsize::new(0));
13619
13620 cx.set_state(indoc! {"
13621 oneˇ
13622 two
13623 three
13624 "});
13625 cx.simulate_keystroke(".");
13626 handle_completion_request(
13627 indoc! {"
13628 one.|<>
13629 two
13630 three
13631 "},
13632 vec!["first_completion", "second_completion"],
13633 true,
13634 counter.clone(),
13635 &mut cx,
13636 )
13637 .await;
13638 cx.condition(|editor, _| editor.context_menu_visible())
13639 .await;
13640 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13641
13642 let _handler = handle_signature_help_request(
13643 &mut cx,
13644 lsp::SignatureHelp {
13645 signatures: vec![lsp::SignatureInformation {
13646 label: "test signature".to_string(),
13647 documentation: None,
13648 parameters: Some(vec![lsp::ParameterInformation {
13649 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13650 documentation: None,
13651 }]),
13652 active_parameter: None,
13653 }],
13654 active_signature: None,
13655 active_parameter: None,
13656 },
13657 );
13658 cx.update_editor(|editor, window, cx| {
13659 assert!(
13660 !editor.signature_help_state.is_shown(),
13661 "No signature help was called for"
13662 );
13663 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13664 });
13665 cx.run_until_parked();
13666 cx.update_editor(|editor, _, _| {
13667 assert!(
13668 !editor.signature_help_state.is_shown(),
13669 "No signature help should be shown when completions menu is open"
13670 );
13671 });
13672
13673 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13674 editor.context_menu_next(&Default::default(), window, cx);
13675 editor
13676 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13677 .unwrap()
13678 });
13679 cx.assert_editor_state(indoc! {"
13680 one.second_completionˇ
13681 two
13682 three
13683 "});
13684
13685 handle_resolve_completion_request(
13686 &mut cx,
13687 Some(vec![
13688 (
13689 //This overlaps with the primary completion edit which is
13690 //misbehavior from the LSP spec, test that we filter it out
13691 indoc! {"
13692 one.second_ˇcompletion
13693 two
13694 threeˇ
13695 "},
13696 "overlapping additional edit",
13697 ),
13698 (
13699 indoc! {"
13700 one.second_completion
13701 two
13702 threeˇ
13703 "},
13704 "\nadditional edit",
13705 ),
13706 ]),
13707 )
13708 .await;
13709 apply_additional_edits.await.unwrap();
13710 cx.assert_editor_state(indoc! {"
13711 one.second_completionˇ
13712 two
13713 three
13714 additional edit
13715 "});
13716
13717 cx.set_state(indoc! {"
13718 one.second_completion
13719 twoˇ
13720 threeˇ
13721 additional edit
13722 "});
13723 cx.simulate_keystroke(" ");
13724 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13725 cx.simulate_keystroke("s");
13726 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13727
13728 cx.assert_editor_state(indoc! {"
13729 one.second_completion
13730 two sˇ
13731 three sˇ
13732 additional edit
13733 "});
13734 handle_completion_request(
13735 indoc! {"
13736 one.second_completion
13737 two s
13738 three <s|>
13739 additional edit
13740 "},
13741 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13742 true,
13743 counter.clone(),
13744 &mut cx,
13745 )
13746 .await;
13747 cx.condition(|editor, _| editor.context_menu_visible())
13748 .await;
13749 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13750
13751 cx.simulate_keystroke("i");
13752
13753 handle_completion_request(
13754 indoc! {"
13755 one.second_completion
13756 two si
13757 three <si|>
13758 additional edit
13759 "},
13760 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13761 true,
13762 counter.clone(),
13763 &mut cx,
13764 )
13765 .await;
13766 cx.condition(|editor, _| editor.context_menu_visible())
13767 .await;
13768 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13769
13770 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13771 editor
13772 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13773 .unwrap()
13774 });
13775 cx.assert_editor_state(indoc! {"
13776 one.second_completion
13777 two sixth_completionˇ
13778 three sixth_completionˇ
13779 additional edit
13780 "});
13781
13782 apply_additional_edits.await.unwrap();
13783
13784 update_test_language_settings(&mut cx, |settings| {
13785 settings.defaults.show_completions_on_input = Some(false);
13786 });
13787 cx.set_state("editorˇ");
13788 cx.simulate_keystroke(".");
13789 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13790 cx.simulate_keystrokes("c l o");
13791 cx.assert_editor_state("editor.cloˇ");
13792 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13793 cx.update_editor(|editor, window, cx| {
13794 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13795 });
13796 handle_completion_request(
13797 "editor.<clo|>",
13798 vec!["close", "clobber"],
13799 true,
13800 counter.clone(),
13801 &mut cx,
13802 )
13803 .await;
13804 cx.condition(|editor, _| editor.context_menu_visible())
13805 .await;
13806 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
13807
13808 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13809 editor
13810 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13811 .unwrap()
13812 });
13813 cx.assert_editor_state("editor.clobberˇ");
13814 handle_resolve_completion_request(&mut cx, None).await;
13815 apply_additional_edits.await.unwrap();
13816}
13817
13818#[gpui::test]
13819async fn test_completion_reuse(cx: &mut TestAppContext) {
13820 init_test(cx, |_| {});
13821
13822 let mut cx = EditorLspTestContext::new_rust(
13823 lsp::ServerCapabilities {
13824 completion_provider: Some(lsp::CompletionOptions {
13825 trigger_characters: Some(vec![".".to_string()]),
13826 ..Default::default()
13827 }),
13828 ..Default::default()
13829 },
13830 cx,
13831 )
13832 .await;
13833
13834 let counter = Arc::new(AtomicUsize::new(0));
13835 cx.set_state("objˇ");
13836 cx.simulate_keystroke(".");
13837
13838 // Initial completion request returns complete results
13839 let is_incomplete = false;
13840 handle_completion_request(
13841 "obj.|<>",
13842 vec!["a", "ab", "abc"],
13843 is_incomplete,
13844 counter.clone(),
13845 &mut cx,
13846 )
13847 .await;
13848 cx.run_until_parked();
13849 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13850 cx.assert_editor_state("obj.ˇ");
13851 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13852
13853 // Type "a" - filters existing completions
13854 cx.simulate_keystroke("a");
13855 cx.run_until_parked();
13856 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13857 cx.assert_editor_state("obj.aˇ");
13858 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13859
13860 // Type "b" - filters existing completions
13861 cx.simulate_keystroke("b");
13862 cx.run_until_parked();
13863 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13864 cx.assert_editor_state("obj.abˇ");
13865 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13866
13867 // Type "c" - filters existing completions
13868 cx.simulate_keystroke("c");
13869 cx.run_until_parked();
13870 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13871 cx.assert_editor_state("obj.abcˇ");
13872 check_displayed_completions(vec!["abc"], &mut cx);
13873
13874 // Backspace to delete "c" - filters existing completions
13875 cx.update_editor(|editor, window, cx| {
13876 editor.backspace(&Backspace, window, cx);
13877 });
13878 cx.run_until_parked();
13879 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13880 cx.assert_editor_state("obj.abˇ");
13881 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13882
13883 // Moving cursor to the left dismisses menu.
13884 cx.update_editor(|editor, window, cx| {
13885 editor.move_left(&MoveLeft, window, cx);
13886 });
13887 cx.run_until_parked();
13888 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13889 cx.assert_editor_state("obj.aˇb");
13890 cx.update_editor(|editor, _, _| {
13891 assert_eq!(editor.context_menu_visible(), false);
13892 });
13893
13894 // Type "b" - new request
13895 cx.simulate_keystroke("b");
13896 let is_incomplete = false;
13897 handle_completion_request(
13898 "obj.<ab|>a",
13899 vec!["ab", "abc"],
13900 is_incomplete,
13901 counter.clone(),
13902 &mut cx,
13903 )
13904 .await;
13905 cx.run_until_parked();
13906 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13907 cx.assert_editor_state("obj.abˇb");
13908 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13909
13910 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13911 cx.update_editor(|editor, window, cx| {
13912 editor.backspace(&Backspace, window, cx);
13913 });
13914 let is_incomplete = false;
13915 handle_completion_request(
13916 "obj.<a|>b",
13917 vec!["a", "ab", "abc"],
13918 is_incomplete,
13919 counter.clone(),
13920 &mut cx,
13921 )
13922 .await;
13923 cx.run_until_parked();
13924 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13925 cx.assert_editor_state("obj.aˇb");
13926 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13927
13928 // Backspace to delete "a" - dismisses menu.
13929 cx.update_editor(|editor, window, cx| {
13930 editor.backspace(&Backspace, window, cx);
13931 });
13932 cx.run_until_parked();
13933 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13934 cx.assert_editor_state("obj.ˇb");
13935 cx.update_editor(|editor, _, _| {
13936 assert_eq!(editor.context_menu_visible(), false);
13937 });
13938}
13939
13940#[gpui::test]
13941async fn test_word_completion(cx: &mut TestAppContext) {
13942 let lsp_fetch_timeout_ms = 10;
13943 init_test(cx, |language_settings| {
13944 language_settings.defaults.completions = Some(CompletionSettings {
13945 words: WordsCompletionMode::Fallback,
13946 words_min_length: 0,
13947 lsp: true,
13948 lsp_fetch_timeout_ms: 10,
13949 lsp_insert_mode: LspInsertMode::Insert,
13950 });
13951 });
13952
13953 let mut cx = EditorLspTestContext::new_rust(
13954 lsp::ServerCapabilities {
13955 completion_provider: Some(lsp::CompletionOptions {
13956 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13957 ..lsp::CompletionOptions::default()
13958 }),
13959 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13960 ..lsp::ServerCapabilities::default()
13961 },
13962 cx,
13963 )
13964 .await;
13965
13966 let throttle_completions = Arc::new(AtomicBool::new(false));
13967
13968 let lsp_throttle_completions = throttle_completions.clone();
13969 let _completion_requests_handler =
13970 cx.lsp
13971 .server
13972 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13973 let lsp_throttle_completions = lsp_throttle_completions.clone();
13974 let cx = cx.clone();
13975 async move {
13976 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13977 cx.background_executor()
13978 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13979 .await;
13980 }
13981 Ok(Some(lsp::CompletionResponse::Array(vec![
13982 lsp::CompletionItem {
13983 label: "first".into(),
13984 ..lsp::CompletionItem::default()
13985 },
13986 lsp::CompletionItem {
13987 label: "last".into(),
13988 ..lsp::CompletionItem::default()
13989 },
13990 ])))
13991 }
13992 });
13993
13994 cx.set_state(indoc! {"
13995 oneˇ
13996 two
13997 three
13998 "});
13999 cx.simulate_keystroke(".");
14000 cx.executor().run_until_parked();
14001 cx.condition(|editor, _| editor.context_menu_visible())
14002 .await;
14003 cx.update_editor(|editor, window, cx| {
14004 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14005 {
14006 assert_eq!(
14007 completion_menu_entries(menu),
14008 &["first", "last"],
14009 "When LSP server is fast to reply, no fallback word completions are used"
14010 );
14011 } else {
14012 panic!("expected completion menu to be open");
14013 }
14014 editor.cancel(&Cancel, window, cx);
14015 });
14016 cx.executor().run_until_parked();
14017 cx.condition(|editor, _| !editor.context_menu_visible())
14018 .await;
14019
14020 throttle_completions.store(true, atomic::Ordering::Release);
14021 cx.simulate_keystroke(".");
14022 cx.executor()
14023 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14024 cx.executor().run_until_parked();
14025 cx.condition(|editor, _| editor.context_menu_visible())
14026 .await;
14027 cx.update_editor(|editor, _, _| {
14028 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14029 {
14030 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14031 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14032 } else {
14033 panic!("expected completion menu to be open");
14034 }
14035 });
14036}
14037
14038#[gpui::test]
14039async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14040 init_test(cx, |language_settings| {
14041 language_settings.defaults.completions = Some(CompletionSettings {
14042 words: WordsCompletionMode::Enabled,
14043 words_min_length: 0,
14044 lsp: true,
14045 lsp_fetch_timeout_ms: 0,
14046 lsp_insert_mode: LspInsertMode::Insert,
14047 });
14048 });
14049
14050 let mut cx = EditorLspTestContext::new_rust(
14051 lsp::ServerCapabilities {
14052 completion_provider: Some(lsp::CompletionOptions {
14053 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14054 ..lsp::CompletionOptions::default()
14055 }),
14056 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14057 ..lsp::ServerCapabilities::default()
14058 },
14059 cx,
14060 )
14061 .await;
14062
14063 let _completion_requests_handler =
14064 cx.lsp
14065 .server
14066 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14067 Ok(Some(lsp::CompletionResponse::Array(vec![
14068 lsp::CompletionItem {
14069 label: "first".into(),
14070 ..lsp::CompletionItem::default()
14071 },
14072 lsp::CompletionItem {
14073 label: "last".into(),
14074 ..lsp::CompletionItem::default()
14075 },
14076 ])))
14077 });
14078
14079 cx.set_state(indoc! {"ˇ
14080 first
14081 last
14082 second
14083 "});
14084 cx.simulate_keystroke(".");
14085 cx.executor().run_until_parked();
14086 cx.condition(|editor, _| editor.context_menu_visible())
14087 .await;
14088 cx.update_editor(|editor, _, _| {
14089 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14090 {
14091 assert_eq!(
14092 completion_menu_entries(menu),
14093 &["first", "last", "second"],
14094 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14095 );
14096 } else {
14097 panic!("expected completion menu to be open");
14098 }
14099 });
14100}
14101
14102#[gpui::test]
14103async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14104 init_test(cx, |language_settings| {
14105 language_settings.defaults.completions = Some(CompletionSettings {
14106 words: WordsCompletionMode::Disabled,
14107 words_min_length: 0,
14108 lsp: true,
14109 lsp_fetch_timeout_ms: 0,
14110 lsp_insert_mode: LspInsertMode::Insert,
14111 });
14112 });
14113
14114 let mut cx = EditorLspTestContext::new_rust(
14115 lsp::ServerCapabilities {
14116 completion_provider: Some(lsp::CompletionOptions {
14117 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14118 ..lsp::CompletionOptions::default()
14119 }),
14120 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14121 ..lsp::ServerCapabilities::default()
14122 },
14123 cx,
14124 )
14125 .await;
14126
14127 let _completion_requests_handler =
14128 cx.lsp
14129 .server
14130 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14131 panic!("LSP completions should not be queried when dealing with word completions")
14132 });
14133
14134 cx.set_state(indoc! {"ˇ
14135 first
14136 last
14137 second
14138 "});
14139 cx.update_editor(|editor, window, cx| {
14140 editor.show_word_completions(&ShowWordCompletions, window, cx);
14141 });
14142 cx.executor().run_until_parked();
14143 cx.condition(|editor, _| editor.context_menu_visible())
14144 .await;
14145 cx.update_editor(|editor, _, _| {
14146 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14147 {
14148 assert_eq!(
14149 completion_menu_entries(menu),
14150 &["first", "last", "second"],
14151 "`ShowWordCompletions` action should show word completions"
14152 );
14153 } else {
14154 panic!("expected completion menu to be open");
14155 }
14156 });
14157
14158 cx.simulate_keystroke("l");
14159 cx.executor().run_until_parked();
14160 cx.condition(|editor, _| editor.context_menu_visible())
14161 .await;
14162 cx.update_editor(|editor, _, _| {
14163 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14164 {
14165 assert_eq!(
14166 completion_menu_entries(menu),
14167 &["last"],
14168 "After showing word completions, further editing should filter them and not query the LSP"
14169 );
14170 } else {
14171 panic!("expected completion menu to be open");
14172 }
14173 });
14174}
14175
14176#[gpui::test]
14177async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14178 init_test(cx, |language_settings| {
14179 language_settings.defaults.completions = Some(CompletionSettings {
14180 words: WordsCompletionMode::Fallback,
14181 words_min_length: 0,
14182 lsp: false,
14183 lsp_fetch_timeout_ms: 0,
14184 lsp_insert_mode: LspInsertMode::Insert,
14185 });
14186 });
14187
14188 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14189
14190 cx.set_state(indoc! {"ˇ
14191 0_usize
14192 let
14193 33
14194 4.5f32
14195 "});
14196 cx.update_editor(|editor, window, cx| {
14197 editor.show_completions(&ShowCompletions::default(), window, cx);
14198 });
14199 cx.executor().run_until_parked();
14200 cx.condition(|editor, _| editor.context_menu_visible())
14201 .await;
14202 cx.update_editor(|editor, window, cx| {
14203 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14204 {
14205 assert_eq!(
14206 completion_menu_entries(menu),
14207 &["let"],
14208 "With no digits in the completion query, no digits should be in the word completions"
14209 );
14210 } else {
14211 panic!("expected completion menu to be open");
14212 }
14213 editor.cancel(&Cancel, window, cx);
14214 });
14215
14216 cx.set_state(indoc! {"3ˇ
14217 0_usize
14218 let
14219 3
14220 33.35f32
14221 "});
14222 cx.update_editor(|editor, window, cx| {
14223 editor.show_completions(&ShowCompletions::default(), window, cx);
14224 });
14225 cx.executor().run_until_parked();
14226 cx.condition(|editor, _| editor.context_menu_visible())
14227 .await;
14228 cx.update_editor(|editor, _, _| {
14229 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14230 {
14231 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14232 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14233 } else {
14234 panic!("expected completion menu to be open");
14235 }
14236 });
14237}
14238
14239#[gpui::test]
14240async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14241 init_test(cx, |language_settings| {
14242 language_settings.defaults.completions = Some(CompletionSettings {
14243 words: WordsCompletionMode::Enabled,
14244 words_min_length: 3,
14245 lsp: true,
14246 lsp_fetch_timeout_ms: 0,
14247 lsp_insert_mode: LspInsertMode::Insert,
14248 });
14249 });
14250
14251 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14252 cx.set_state(indoc! {"ˇ
14253 wow
14254 wowen
14255 wowser
14256 "});
14257 cx.simulate_keystroke("w");
14258 cx.executor().run_until_parked();
14259 cx.update_editor(|editor, _, _| {
14260 if editor.context_menu.borrow_mut().is_some() {
14261 panic!(
14262 "expected completion menu to be hidden, as words completion threshold is not met"
14263 );
14264 }
14265 });
14266
14267 cx.simulate_keystroke("o");
14268 cx.executor().run_until_parked();
14269 cx.update_editor(|editor, _, _| {
14270 if editor.context_menu.borrow_mut().is_some() {
14271 panic!(
14272 "expected completion menu to be hidden, as words completion threshold is not met still"
14273 );
14274 }
14275 });
14276
14277 cx.simulate_keystroke("w");
14278 cx.executor().run_until_parked();
14279 cx.update_editor(|editor, _, _| {
14280 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14281 {
14282 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14283 } else {
14284 panic!("expected completion menu to be open after the word completions threshold is met");
14285 }
14286 });
14287}
14288
14289fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14290 let position = || lsp::Position {
14291 line: params.text_document_position.position.line,
14292 character: params.text_document_position.position.character,
14293 };
14294 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14295 range: lsp::Range {
14296 start: position(),
14297 end: position(),
14298 },
14299 new_text: text.to_string(),
14300 }))
14301}
14302
14303#[gpui::test]
14304async fn test_multiline_completion(cx: &mut TestAppContext) {
14305 init_test(cx, |_| {});
14306
14307 let fs = FakeFs::new(cx.executor());
14308 fs.insert_tree(
14309 path!("/a"),
14310 json!({
14311 "main.ts": "a",
14312 }),
14313 )
14314 .await;
14315
14316 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14317 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14318 let typescript_language = Arc::new(Language::new(
14319 LanguageConfig {
14320 name: "TypeScript".into(),
14321 matcher: LanguageMatcher {
14322 path_suffixes: vec!["ts".to_string()],
14323 ..LanguageMatcher::default()
14324 },
14325 line_comments: vec!["// ".into()],
14326 ..LanguageConfig::default()
14327 },
14328 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14329 ));
14330 language_registry.add(typescript_language.clone());
14331 let mut fake_servers = language_registry.register_fake_lsp(
14332 "TypeScript",
14333 FakeLspAdapter {
14334 capabilities: lsp::ServerCapabilities {
14335 completion_provider: Some(lsp::CompletionOptions {
14336 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14337 ..lsp::CompletionOptions::default()
14338 }),
14339 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14340 ..lsp::ServerCapabilities::default()
14341 },
14342 // Emulate vtsls label generation
14343 label_for_completion: Some(Box::new(|item, _| {
14344 let text = if let Some(description) = item
14345 .label_details
14346 .as_ref()
14347 .and_then(|label_details| label_details.description.as_ref())
14348 {
14349 format!("{} {}", item.label, description)
14350 } else if let Some(detail) = &item.detail {
14351 format!("{} {}", item.label, detail)
14352 } else {
14353 item.label.clone()
14354 };
14355 let len = text.len();
14356 Some(language::CodeLabel {
14357 text,
14358 runs: Vec::new(),
14359 filter_range: 0..len,
14360 })
14361 })),
14362 ..FakeLspAdapter::default()
14363 },
14364 );
14365 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14366 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14367 let worktree_id = workspace
14368 .update(cx, |workspace, _window, cx| {
14369 workspace.project().update(cx, |project, cx| {
14370 project.worktrees(cx).next().unwrap().read(cx).id()
14371 })
14372 })
14373 .unwrap();
14374 let _buffer = project
14375 .update(cx, |project, cx| {
14376 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14377 })
14378 .await
14379 .unwrap();
14380 let editor = workspace
14381 .update(cx, |workspace, window, cx| {
14382 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14383 })
14384 .unwrap()
14385 .await
14386 .unwrap()
14387 .downcast::<Editor>()
14388 .unwrap();
14389 let fake_server = fake_servers.next().await.unwrap();
14390
14391 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14392 let multiline_label_2 = "a\nb\nc\n";
14393 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14394 let multiline_description = "d\ne\nf\n";
14395 let multiline_detail_2 = "g\nh\ni\n";
14396
14397 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14398 move |params, _| async move {
14399 Ok(Some(lsp::CompletionResponse::Array(vec![
14400 lsp::CompletionItem {
14401 label: multiline_label.to_string(),
14402 text_edit: gen_text_edit(¶ms, "new_text_1"),
14403 ..lsp::CompletionItem::default()
14404 },
14405 lsp::CompletionItem {
14406 label: "single line label 1".to_string(),
14407 detail: Some(multiline_detail.to_string()),
14408 text_edit: gen_text_edit(¶ms, "new_text_2"),
14409 ..lsp::CompletionItem::default()
14410 },
14411 lsp::CompletionItem {
14412 label: "single line label 2".to_string(),
14413 label_details: Some(lsp::CompletionItemLabelDetails {
14414 description: Some(multiline_description.to_string()),
14415 detail: None,
14416 }),
14417 text_edit: gen_text_edit(¶ms, "new_text_2"),
14418 ..lsp::CompletionItem::default()
14419 },
14420 lsp::CompletionItem {
14421 label: multiline_label_2.to_string(),
14422 detail: Some(multiline_detail_2.to_string()),
14423 text_edit: gen_text_edit(¶ms, "new_text_3"),
14424 ..lsp::CompletionItem::default()
14425 },
14426 lsp::CompletionItem {
14427 label: "Label with many spaces and \t but without newlines".to_string(),
14428 detail: Some(
14429 "Details with many spaces and \t but without newlines".to_string(),
14430 ),
14431 text_edit: gen_text_edit(¶ms, "new_text_4"),
14432 ..lsp::CompletionItem::default()
14433 },
14434 ])))
14435 },
14436 );
14437
14438 editor.update_in(cx, |editor, window, cx| {
14439 cx.focus_self(window);
14440 editor.move_to_end(&MoveToEnd, window, cx);
14441 editor.handle_input(".", window, cx);
14442 });
14443 cx.run_until_parked();
14444 completion_handle.next().await.unwrap();
14445
14446 editor.update(cx, |editor, _| {
14447 assert!(editor.context_menu_visible());
14448 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14449 {
14450 let completion_labels = menu
14451 .completions
14452 .borrow()
14453 .iter()
14454 .map(|c| c.label.text.clone())
14455 .collect::<Vec<_>>();
14456 assert_eq!(
14457 completion_labels,
14458 &[
14459 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14460 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14461 "single line label 2 d e f ",
14462 "a b c g h i ",
14463 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14464 ],
14465 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14466 );
14467
14468 for completion in menu
14469 .completions
14470 .borrow()
14471 .iter() {
14472 assert_eq!(
14473 completion.label.filter_range,
14474 0..completion.label.text.len(),
14475 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14476 );
14477 }
14478 } else {
14479 panic!("expected completion menu to be open");
14480 }
14481 });
14482}
14483
14484#[gpui::test]
14485async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14486 init_test(cx, |_| {});
14487 let mut cx = EditorLspTestContext::new_rust(
14488 lsp::ServerCapabilities {
14489 completion_provider: Some(lsp::CompletionOptions {
14490 trigger_characters: Some(vec![".".to_string()]),
14491 ..Default::default()
14492 }),
14493 ..Default::default()
14494 },
14495 cx,
14496 )
14497 .await;
14498 cx.lsp
14499 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14500 Ok(Some(lsp::CompletionResponse::Array(vec![
14501 lsp::CompletionItem {
14502 label: "first".into(),
14503 ..Default::default()
14504 },
14505 lsp::CompletionItem {
14506 label: "last".into(),
14507 ..Default::default()
14508 },
14509 ])))
14510 });
14511 cx.set_state("variableˇ");
14512 cx.simulate_keystroke(".");
14513 cx.executor().run_until_parked();
14514
14515 cx.update_editor(|editor, _, _| {
14516 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14517 {
14518 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14519 } else {
14520 panic!("expected completion menu to be open");
14521 }
14522 });
14523
14524 cx.update_editor(|editor, window, cx| {
14525 editor.move_page_down(&MovePageDown::default(), window, cx);
14526 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14527 {
14528 assert!(
14529 menu.selected_item == 1,
14530 "expected PageDown to select the last item from the context menu"
14531 );
14532 } else {
14533 panic!("expected completion menu to stay open after PageDown");
14534 }
14535 });
14536
14537 cx.update_editor(|editor, window, cx| {
14538 editor.move_page_up(&MovePageUp::default(), window, cx);
14539 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14540 {
14541 assert!(
14542 menu.selected_item == 0,
14543 "expected PageUp to select the first item from the context menu"
14544 );
14545 } else {
14546 panic!("expected completion menu to stay open after PageUp");
14547 }
14548 });
14549}
14550
14551#[gpui::test]
14552async fn test_as_is_completions(cx: &mut TestAppContext) {
14553 init_test(cx, |_| {});
14554 let mut cx = EditorLspTestContext::new_rust(
14555 lsp::ServerCapabilities {
14556 completion_provider: Some(lsp::CompletionOptions {
14557 ..Default::default()
14558 }),
14559 ..Default::default()
14560 },
14561 cx,
14562 )
14563 .await;
14564 cx.lsp
14565 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14566 Ok(Some(lsp::CompletionResponse::Array(vec![
14567 lsp::CompletionItem {
14568 label: "unsafe".into(),
14569 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14570 range: lsp::Range {
14571 start: lsp::Position {
14572 line: 1,
14573 character: 2,
14574 },
14575 end: lsp::Position {
14576 line: 1,
14577 character: 3,
14578 },
14579 },
14580 new_text: "unsafe".to_string(),
14581 })),
14582 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14583 ..Default::default()
14584 },
14585 ])))
14586 });
14587 cx.set_state("fn a() {}\n nˇ");
14588 cx.executor().run_until_parked();
14589 cx.update_editor(|editor, window, cx| {
14590 editor.show_completions(
14591 &ShowCompletions {
14592 trigger: Some("\n".into()),
14593 },
14594 window,
14595 cx,
14596 );
14597 });
14598 cx.executor().run_until_parked();
14599
14600 cx.update_editor(|editor, window, cx| {
14601 editor.confirm_completion(&Default::default(), window, cx)
14602 });
14603 cx.executor().run_until_parked();
14604 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14605}
14606
14607#[gpui::test]
14608async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14609 init_test(cx, |_| {});
14610 let language =
14611 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14612 let mut cx = EditorLspTestContext::new(
14613 language,
14614 lsp::ServerCapabilities {
14615 completion_provider: Some(lsp::CompletionOptions {
14616 ..lsp::CompletionOptions::default()
14617 }),
14618 ..lsp::ServerCapabilities::default()
14619 },
14620 cx,
14621 )
14622 .await;
14623
14624 cx.set_state(
14625 "#ifndef BAR_H
14626#define BAR_H
14627
14628#include <stdbool.h>
14629
14630int fn_branch(bool do_branch1, bool do_branch2);
14631
14632#endif // BAR_H
14633ˇ",
14634 );
14635 cx.executor().run_until_parked();
14636 cx.update_editor(|editor, window, cx| {
14637 editor.handle_input("#", window, cx);
14638 });
14639 cx.executor().run_until_parked();
14640 cx.update_editor(|editor, window, cx| {
14641 editor.handle_input("i", window, cx);
14642 });
14643 cx.executor().run_until_parked();
14644 cx.update_editor(|editor, window, cx| {
14645 editor.handle_input("n", window, cx);
14646 });
14647 cx.executor().run_until_parked();
14648 cx.assert_editor_state(
14649 "#ifndef BAR_H
14650#define BAR_H
14651
14652#include <stdbool.h>
14653
14654int fn_branch(bool do_branch1, bool do_branch2);
14655
14656#endif // BAR_H
14657#inˇ",
14658 );
14659
14660 cx.lsp
14661 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14662 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14663 is_incomplete: false,
14664 item_defaults: None,
14665 items: vec![lsp::CompletionItem {
14666 kind: Some(lsp::CompletionItemKind::SNIPPET),
14667 label_details: Some(lsp::CompletionItemLabelDetails {
14668 detail: Some("header".to_string()),
14669 description: None,
14670 }),
14671 label: " include".to_string(),
14672 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14673 range: lsp::Range {
14674 start: lsp::Position {
14675 line: 8,
14676 character: 1,
14677 },
14678 end: lsp::Position {
14679 line: 8,
14680 character: 1,
14681 },
14682 },
14683 new_text: "include \"$0\"".to_string(),
14684 })),
14685 sort_text: Some("40b67681include".to_string()),
14686 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14687 filter_text: Some("include".to_string()),
14688 insert_text: Some("include \"$0\"".to_string()),
14689 ..lsp::CompletionItem::default()
14690 }],
14691 })))
14692 });
14693 cx.update_editor(|editor, window, cx| {
14694 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14695 });
14696 cx.executor().run_until_parked();
14697 cx.update_editor(|editor, window, cx| {
14698 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14699 });
14700 cx.executor().run_until_parked();
14701 cx.assert_editor_state(
14702 "#ifndef BAR_H
14703#define BAR_H
14704
14705#include <stdbool.h>
14706
14707int fn_branch(bool do_branch1, bool do_branch2);
14708
14709#endif // BAR_H
14710#include \"ˇ\"",
14711 );
14712
14713 cx.lsp
14714 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14715 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14716 is_incomplete: true,
14717 item_defaults: None,
14718 items: vec![lsp::CompletionItem {
14719 kind: Some(lsp::CompletionItemKind::FILE),
14720 label: "AGL/".to_string(),
14721 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14722 range: lsp::Range {
14723 start: lsp::Position {
14724 line: 8,
14725 character: 10,
14726 },
14727 end: lsp::Position {
14728 line: 8,
14729 character: 11,
14730 },
14731 },
14732 new_text: "AGL/".to_string(),
14733 })),
14734 sort_text: Some("40b67681AGL/".to_string()),
14735 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14736 filter_text: Some("AGL/".to_string()),
14737 insert_text: Some("AGL/".to_string()),
14738 ..lsp::CompletionItem::default()
14739 }],
14740 })))
14741 });
14742 cx.update_editor(|editor, window, cx| {
14743 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14744 });
14745 cx.executor().run_until_parked();
14746 cx.update_editor(|editor, window, cx| {
14747 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14748 });
14749 cx.executor().run_until_parked();
14750 cx.assert_editor_state(
14751 r##"#ifndef BAR_H
14752#define BAR_H
14753
14754#include <stdbool.h>
14755
14756int fn_branch(bool do_branch1, bool do_branch2);
14757
14758#endif // BAR_H
14759#include "AGL/ˇ"##,
14760 );
14761
14762 cx.update_editor(|editor, window, cx| {
14763 editor.handle_input("\"", window, cx);
14764 });
14765 cx.executor().run_until_parked();
14766 cx.assert_editor_state(
14767 r##"#ifndef BAR_H
14768#define BAR_H
14769
14770#include <stdbool.h>
14771
14772int fn_branch(bool do_branch1, bool do_branch2);
14773
14774#endif // BAR_H
14775#include "AGL/"ˇ"##,
14776 );
14777}
14778
14779#[gpui::test]
14780async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
14781 init_test(cx, |_| {});
14782
14783 let mut cx = EditorLspTestContext::new_rust(
14784 lsp::ServerCapabilities {
14785 completion_provider: Some(lsp::CompletionOptions {
14786 trigger_characters: Some(vec![".".to_string()]),
14787 resolve_provider: Some(true),
14788 ..Default::default()
14789 }),
14790 ..Default::default()
14791 },
14792 cx,
14793 )
14794 .await;
14795
14796 cx.set_state("fn main() { let a = 2ˇ; }");
14797 cx.simulate_keystroke(".");
14798 let completion_item = lsp::CompletionItem {
14799 label: "Some".into(),
14800 kind: Some(lsp::CompletionItemKind::SNIPPET),
14801 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14802 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14803 kind: lsp::MarkupKind::Markdown,
14804 value: "```rust\nSome(2)\n```".to_string(),
14805 })),
14806 deprecated: Some(false),
14807 sort_text: Some("Some".to_string()),
14808 filter_text: Some("Some".to_string()),
14809 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14810 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14811 range: lsp::Range {
14812 start: lsp::Position {
14813 line: 0,
14814 character: 22,
14815 },
14816 end: lsp::Position {
14817 line: 0,
14818 character: 22,
14819 },
14820 },
14821 new_text: "Some(2)".to_string(),
14822 })),
14823 additional_text_edits: Some(vec![lsp::TextEdit {
14824 range: lsp::Range {
14825 start: lsp::Position {
14826 line: 0,
14827 character: 20,
14828 },
14829 end: lsp::Position {
14830 line: 0,
14831 character: 22,
14832 },
14833 },
14834 new_text: "".to_string(),
14835 }]),
14836 ..Default::default()
14837 };
14838
14839 let closure_completion_item = completion_item.clone();
14840 let counter = Arc::new(AtomicUsize::new(0));
14841 let counter_clone = counter.clone();
14842 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14843 let task_completion_item = closure_completion_item.clone();
14844 counter_clone.fetch_add(1, atomic::Ordering::Release);
14845 async move {
14846 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14847 is_incomplete: true,
14848 item_defaults: None,
14849 items: vec![task_completion_item],
14850 })))
14851 }
14852 });
14853
14854 cx.condition(|editor, _| editor.context_menu_visible())
14855 .await;
14856 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
14857 assert!(request.next().await.is_some());
14858 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14859
14860 cx.simulate_keystrokes("S o m");
14861 cx.condition(|editor, _| editor.context_menu_visible())
14862 .await;
14863 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
14864 assert!(request.next().await.is_some());
14865 assert!(request.next().await.is_some());
14866 assert!(request.next().await.is_some());
14867 request.close();
14868 assert!(request.next().await.is_none());
14869 assert_eq!(
14870 counter.load(atomic::Ordering::Acquire),
14871 4,
14872 "With the completions menu open, only one LSP request should happen per input"
14873 );
14874}
14875
14876#[gpui::test]
14877async fn test_toggle_comment(cx: &mut TestAppContext) {
14878 init_test(cx, |_| {});
14879 let mut cx = EditorTestContext::new(cx).await;
14880 let language = Arc::new(Language::new(
14881 LanguageConfig {
14882 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14883 ..Default::default()
14884 },
14885 Some(tree_sitter_rust::LANGUAGE.into()),
14886 ));
14887 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14888
14889 // If multiple selections intersect a line, the line is only toggled once.
14890 cx.set_state(indoc! {"
14891 fn a() {
14892 «//b();
14893 ˇ»// «c();
14894 //ˇ» d();
14895 }
14896 "});
14897
14898 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14899
14900 cx.assert_editor_state(indoc! {"
14901 fn a() {
14902 «b();
14903 c();
14904 ˇ» d();
14905 }
14906 "});
14907
14908 // The comment prefix is inserted at the same column for every line in a
14909 // selection.
14910 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14911
14912 cx.assert_editor_state(indoc! {"
14913 fn a() {
14914 // «b();
14915 // c();
14916 ˇ»// d();
14917 }
14918 "});
14919
14920 // If a selection ends at the beginning of a line, that line is not toggled.
14921 cx.set_selections_state(indoc! {"
14922 fn a() {
14923 // b();
14924 «// c();
14925 ˇ» // d();
14926 }
14927 "});
14928
14929 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14930
14931 cx.assert_editor_state(indoc! {"
14932 fn a() {
14933 // b();
14934 «c();
14935 ˇ» // d();
14936 }
14937 "});
14938
14939 // If a selection span a single line and is empty, the line is toggled.
14940 cx.set_state(indoc! {"
14941 fn a() {
14942 a();
14943 b();
14944 ˇ
14945 }
14946 "});
14947
14948 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14949
14950 cx.assert_editor_state(indoc! {"
14951 fn a() {
14952 a();
14953 b();
14954 //•ˇ
14955 }
14956 "});
14957
14958 // If a selection span multiple lines, empty lines are not toggled.
14959 cx.set_state(indoc! {"
14960 fn a() {
14961 «a();
14962
14963 c();ˇ»
14964 }
14965 "});
14966
14967 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14968
14969 cx.assert_editor_state(indoc! {"
14970 fn a() {
14971 // «a();
14972
14973 // c();ˇ»
14974 }
14975 "});
14976
14977 // If a selection includes multiple comment prefixes, all lines are uncommented.
14978 cx.set_state(indoc! {"
14979 fn a() {
14980 «// a();
14981 /// b();
14982 //! c();ˇ»
14983 }
14984 "});
14985
14986 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14987
14988 cx.assert_editor_state(indoc! {"
14989 fn a() {
14990 «a();
14991 b();
14992 c();ˇ»
14993 }
14994 "});
14995}
14996
14997#[gpui::test]
14998async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
14999 init_test(cx, |_| {});
15000 let mut cx = EditorTestContext::new(cx).await;
15001 let language = Arc::new(Language::new(
15002 LanguageConfig {
15003 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15004 ..Default::default()
15005 },
15006 Some(tree_sitter_rust::LANGUAGE.into()),
15007 ));
15008 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15009
15010 let toggle_comments = &ToggleComments {
15011 advance_downwards: false,
15012 ignore_indent: true,
15013 };
15014
15015 // If multiple selections intersect a line, the line is only toggled once.
15016 cx.set_state(indoc! {"
15017 fn a() {
15018 // «b();
15019 // c();
15020 // ˇ» d();
15021 }
15022 "});
15023
15024 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15025
15026 cx.assert_editor_state(indoc! {"
15027 fn a() {
15028 «b();
15029 c();
15030 ˇ» d();
15031 }
15032 "});
15033
15034 // The comment prefix is inserted at the beginning of each line
15035 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15036
15037 cx.assert_editor_state(indoc! {"
15038 fn a() {
15039 // «b();
15040 // c();
15041 // ˇ» d();
15042 }
15043 "});
15044
15045 // If a selection ends at the beginning of a line, that line is not toggled.
15046 cx.set_selections_state(indoc! {"
15047 fn a() {
15048 // b();
15049 // «c();
15050 ˇ»// d();
15051 }
15052 "});
15053
15054 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15055
15056 cx.assert_editor_state(indoc! {"
15057 fn a() {
15058 // b();
15059 «c();
15060 ˇ»// d();
15061 }
15062 "});
15063
15064 // If a selection span a single line and is empty, the line is toggled.
15065 cx.set_state(indoc! {"
15066 fn a() {
15067 a();
15068 b();
15069 ˇ
15070 }
15071 "});
15072
15073 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15074
15075 cx.assert_editor_state(indoc! {"
15076 fn a() {
15077 a();
15078 b();
15079 //ˇ
15080 }
15081 "});
15082
15083 // If a selection span multiple lines, empty lines are not toggled.
15084 cx.set_state(indoc! {"
15085 fn a() {
15086 «a();
15087
15088 c();ˇ»
15089 }
15090 "});
15091
15092 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15093
15094 cx.assert_editor_state(indoc! {"
15095 fn a() {
15096 // «a();
15097
15098 // c();ˇ»
15099 }
15100 "});
15101
15102 // If a selection includes multiple comment prefixes, all lines are uncommented.
15103 cx.set_state(indoc! {"
15104 fn a() {
15105 // «a();
15106 /// b();
15107 //! c();ˇ»
15108 }
15109 "});
15110
15111 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15112
15113 cx.assert_editor_state(indoc! {"
15114 fn a() {
15115 «a();
15116 b();
15117 c();ˇ»
15118 }
15119 "});
15120}
15121
15122#[gpui::test]
15123async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15124 init_test(cx, |_| {});
15125
15126 let language = Arc::new(Language::new(
15127 LanguageConfig {
15128 line_comments: vec!["// ".into()],
15129 ..Default::default()
15130 },
15131 Some(tree_sitter_rust::LANGUAGE.into()),
15132 ));
15133
15134 let mut cx = EditorTestContext::new(cx).await;
15135
15136 cx.language_registry().add(language.clone());
15137 cx.update_buffer(|buffer, cx| {
15138 buffer.set_language(Some(language), cx);
15139 });
15140
15141 let toggle_comments = &ToggleComments {
15142 advance_downwards: true,
15143 ignore_indent: false,
15144 };
15145
15146 // Single cursor on one line -> advance
15147 // Cursor moves horizontally 3 characters as well on non-blank line
15148 cx.set_state(indoc!(
15149 "fn a() {
15150 ˇdog();
15151 cat();
15152 }"
15153 ));
15154 cx.update_editor(|editor, window, cx| {
15155 editor.toggle_comments(toggle_comments, window, cx);
15156 });
15157 cx.assert_editor_state(indoc!(
15158 "fn a() {
15159 // dog();
15160 catˇ();
15161 }"
15162 ));
15163
15164 // Single selection on one line -> don't advance
15165 cx.set_state(indoc!(
15166 "fn a() {
15167 «dog()ˇ»;
15168 cat();
15169 }"
15170 ));
15171 cx.update_editor(|editor, window, cx| {
15172 editor.toggle_comments(toggle_comments, window, cx);
15173 });
15174 cx.assert_editor_state(indoc!(
15175 "fn a() {
15176 // «dog()ˇ»;
15177 cat();
15178 }"
15179 ));
15180
15181 // Multiple cursors on one line -> advance
15182 cx.set_state(indoc!(
15183 "fn a() {
15184 ˇdˇog();
15185 cat();
15186 }"
15187 ));
15188 cx.update_editor(|editor, window, cx| {
15189 editor.toggle_comments(toggle_comments, window, cx);
15190 });
15191 cx.assert_editor_state(indoc!(
15192 "fn a() {
15193 // dog();
15194 catˇ(ˇ);
15195 }"
15196 ));
15197
15198 // Multiple cursors on one line, with selection -> don't advance
15199 cx.set_state(indoc!(
15200 "fn a() {
15201 ˇdˇog«()ˇ»;
15202 cat();
15203 }"
15204 ));
15205 cx.update_editor(|editor, window, cx| {
15206 editor.toggle_comments(toggle_comments, window, cx);
15207 });
15208 cx.assert_editor_state(indoc!(
15209 "fn a() {
15210 // ˇdˇog«()ˇ»;
15211 cat();
15212 }"
15213 ));
15214
15215 // Single cursor on one line -> advance
15216 // Cursor moves to column 0 on blank line
15217 cx.set_state(indoc!(
15218 "fn a() {
15219 ˇdog();
15220
15221 cat();
15222 }"
15223 ));
15224 cx.update_editor(|editor, window, cx| {
15225 editor.toggle_comments(toggle_comments, window, cx);
15226 });
15227 cx.assert_editor_state(indoc!(
15228 "fn a() {
15229 // dog();
15230 ˇ
15231 cat();
15232 }"
15233 ));
15234
15235 // Single cursor on one line -> advance
15236 // Cursor starts and ends at column 0
15237 cx.set_state(indoc!(
15238 "fn a() {
15239 ˇ dog();
15240 cat();
15241 }"
15242 ));
15243 cx.update_editor(|editor, window, cx| {
15244 editor.toggle_comments(toggle_comments, window, cx);
15245 });
15246 cx.assert_editor_state(indoc!(
15247 "fn a() {
15248 // dog();
15249 ˇ cat();
15250 }"
15251 ));
15252}
15253
15254#[gpui::test]
15255async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15256 init_test(cx, |_| {});
15257
15258 let mut cx = EditorTestContext::new(cx).await;
15259
15260 let html_language = Arc::new(
15261 Language::new(
15262 LanguageConfig {
15263 name: "HTML".into(),
15264 block_comment: Some(BlockCommentConfig {
15265 start: "<!-- ".into(),
15266 prefix: "".into(),
15267 end: " -->".into(),
15268 tab_size: 0,
15269 }),
15270 ..Default::default()
15271 },
15272 Some(tree_sitter_html::LANGUAGE.into()),
15273 )
15274 .with_injection_query(
15275 r#"
15276 (script_element
15277 (raw_text) @injection.content
15278 (#set! injection.language "javascript"))
15279 "#,
15280 )
15281 .unwrap(),
15282 );
15283
15284 let javascript_language = Arc::new(Language::new(
15285 LanguageConfig {
15286 name: "JavaScript".into(),
15287 line_comments: vec!["// ".into()],
15288 ..Default::default()
15289 },
15290 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15291 ));
15292
15293 cx.language_registry().add(html_language.clone());
15294 cx.language_registry().add(javascript_language);
15295 cx.update_buffer(|buffer, cx| {
15296 buffer.set_language(Some(html_language), cx);
15297 });
15298
15299 // Toggle comments for empty selections
15300 cx.set_state(
15301 &r#"
15302 <p>A</p>ˇ
15303 <p>B</p>ˇ
15304 <p>C</p>ˇ
15305 "#
15306 .unindent(),
15307 );
15308 cx.update_editor(|editor, window, cx| {
15309 editor.toggle_comments(&ToggleComments::default(), window, cx)
15310 });
15311 cx.assert_editor_state(
15312 &r#"
15313 <!-- <p>A</p>ˇ -->
15314 <!-- <p>B</p>ˇ -->
15315 <!-- <p>C</p>ˇ -->
15316 "#
15317 .unindent(),
15318 );
15319 cx.update_editor(|editor, window, cx| {
15320 editor.toggle_comments(&ToggleComments::default(), window, cx)
15321 });
15322 cx.assert_editor_state(
15323 &r#"
15324 <p>A</p>ˇ
15325 <p>B</p>ˇ
15326 <p>C</p>ˇ
15327 "#
15328 .unindent(),
15329 );
15330
15331 // Toggle comments for mixture of empty and non-empty selections, where
15332 // multiple selections occupy a given line.
15333 cx.set_state(
15334 &r#"
15335 <p>A«</p>
15336 <p>ˇ»B</p>ˇ
15337 <p>C«</p>
15338 <p>ˇ»D</p>ˇ
15339 "#
15340 .unindent(),
15341 );
15342
15343 cx.update_editor(|editor, window, cx| {
15344 editor.toggle_comments(&ToggleComments::default(), window, cx)
15345 });
15346 cx.assert_editor_state(
15347 &r#"
15348 <!-- <p>A«</p>
15349 <p>ˇ»B</p>ˇ -->
15350 <!-- <p>C«</p>
15351 <p>ˇ»D</p>ˇ -->
15352 "#
15353 .unindent(),
15354 );
15355 cx.update_editor(|editor, window, cx| {
15356 editor.toggle_comments(&ToggleComments::default(), window, cx)
15357 });
15358 cx.assert_editor_state(
15359 &r#"
15360 <p>A«</p>
15361 <p>ˇ»B</p>ˇ
15362 <p>C«</p>
15363 <p>ˇ»D</p>ˇ
15364 "#
15365 .unindent(),
15366 );
15367
15368 // Toggle comments when different languages are active for different
15369 // selections.
15370 cx.set_state(
15371 &r#"
15372 ˇ<script>
15373 ˇvar x = new Y();
15374 ˇ</script>
15375 "#
15376 .unindent(),
15377 );
15378 cx.executor().run_until_parked();
15379 cx.update_editor(|editor, window, cx| {
15380 editor.toggle_comments(&ToggleComments::default(), window, cx)
15381 });
15382 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15383 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15384 cx.assert_editor_state(
15385 &r#"
15386 <!-- ˇ<script> -->
15387 // ˇvar x = new Y();
15388 <!-- ˇ</script> -->
15389 "#
15390 .unindent(),
15391 );
15392}
15393
15394#[gpui::test]
15395fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15396 init_test(cx, |_| {});
15397
15398 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15399 let multibuffer = cx.new(|cx| {
15400 let mut multibuffer = MultiBuffer::new(ReadWrite);
15401 multibuffer.push_excerpts(
15402 buffer.clone(),
15403 [
15404 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15405 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15406 ],
15407 cx,
15408 );
15409 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15410 multibuffer
15411 });
15412
15413 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15414 editor.update_in(cx, |editor, window, cx| {
15415 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15417 s.select_ranges([
15418 Point::new(0, 0)..Point::new(0, 0),
15419 Point::new(1, 0)..Point::new(1, 0),
15420 ])
15421 });
15422
15423 editor.handle_input("X", window, cx);
15424 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15425 assert_eq!(
15426 editor.selections.ranges(cx),
15427 [
15428 Point::new(0, 1)..Point::new(0, 1),
15429 Point::new(1, 1)..Point::new(1, 1),
15430 ]
15431 );
15432
15433 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15434 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15435 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15436 });
15437 editor.backspace(&Default::default(), window, cx);
15438 assert_eq!(editor.text(cx), "Xa\nbbb");
15439 assert_eq!(
15440 editor.selections.ranges(cx),
15441 [Point::new(1, 0)..Point::new(1, 0)]
15442 );
15443
15444 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15445 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15446 });
15447 editor.backspace(&Default::default(), window, cx);
15448 assert_eq!(editor.text(cx), "X\nbb");
15449 assert_eq!(
15450 editor.selections.ranges(cx),
15451 [Point::new(0, 1)..Point::new(0, 1)]
15452 );
15453 });
15454}
15455
15456#[gpui::test]
15457fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15458 init_test(cx, |_| {});
15459
15460 let markers = vec![('[', ']').into(), ('(', ')').into()];
15461 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15462 indoc! {"
15463 [aaaa
15464 (bbbb]
15465 cccc)",
15466 },
15467 markers.clone(),
15468 );
15469 let excerpt_ranges = markers.into_iter().map(|marker| {
15470 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15471 ExcerptRange::new(context)
15472 });
15473 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15474 let multibuffer = cx.new(|cx| {
15475 let mut multibuffer = MultiBuffer::new(ReadWrite);
15476 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15477 multibuffer
15478 });
15479
15480 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15481 editor.update_in(cx, |editor, window, cx| {
15482 let (expected_text, selection_ranges) = marked_text_ranges(
15483 indoc! {"
15484 aaaa
15485 bˇbbb
15486 bˇbbˇb
15487 cccc"
15488 },
15489 true,
15490 );
15491 assert_eq!(editor.text(cx), expected_text);
15492 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15493 s.select_ranges(selection_ranges)
15494 });
15495
15496 editor.handle_input("X", window, cx);
15497
15498 let (expected_text, expected_selections) = marked_text_ranges(
15499 indoc! {"
15500 aaaa
15501 bXˇbbXb
15502 bXˇbbXˇb
15503 cccc"
15504 },
15505 false,
15506 );
15507 assert_eq!(editor.text(cx), expected_text);
15508 assert_eq!(editor.selections.ranges(cx), expected_selections);
15509
15510 editor.newline(&Newline, window, cx);
15511 let (expected_text, expected_selections) = marked_text_ranges(
15512 indoc! {"
15513 aaaa
15514 bX
15515 ˇbbX
15516 b
15517 bX
15518 ˇbbX
15519 ˇb
15520 cccc"
15521 },
15522 false,
15523 );
15524 assert_eq!(editor.text(cx), expected_text);
15525 assert_eq!(editor.selections.ranges(cx), expected_selections);
15526 });
15527}
15528
15529#[gpui::test]
15530fn test_refresh_selections(cx: &mut TestAppContext) {
15531 init_test(cx, |_| {});
15532
15533 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15534 let mut excerpt1_id = None;
15535 let multibuffer = cx.new(|cx| {
15536 let mut multibuffer = MultiBuffer::new(ReadWrite);
15537 excerpt1_id = multibuffer
15538 .push_excerpts(
15539 buffer.clone(),
15540 [
15541 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15542 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15543 ],
15544 cx,
15545 )
15546 .into_iter()
15547 .next();
15548 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15549 multibuffer
15550 });
15551
15552 let editor = cx.add_window(|window, cx| {
15553 let mut editor = build_editor(multibuffer.clone(), window, cx);
15554 let snapshot = editor.snapshot(window, cx);
15555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15556 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15557 });
15558 editor.begin_selection(
15559 Point::new(2, 1).to_display_point(&snapshot),
15560 true,
15561 1,
15562 window,
15563 cx,
15564 );
15565 assert_eq!(
15566 editor.selections.ranges(cx),
15567 [
15568 Point::new(1, 3)..Point::new(1, 3),
15569 Point::new(2, 1)..Point::new(2, 1),
15570 ]
15571 );
15572 editor
15573 });
15574
15575 // Refreshing selections is a no-op when excerpts haven't changed.
15576 _ = editor.update(cx, |editor, window, cx| {
15577 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15578 assert_eq!(
15579 editor.selections.ranges(cx),
15580 [
15581 Point::new(1, 3)..Point::new(1, 3),
15582 Point::new(2, 1)..Point::new(2, 1),
15583 ]
15584 );
15585 });
15586
15587 multibuffer.update(cx, |multibuffer, cx| {
15588 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15589 });
15590 _ = editor.update(cx, |editor, window, cx| {
15591 // Removing an excerpt causes the first selection to become degenerate.
15592 assert_eq!(
15593 editor.selections.ranges(cx),
15594 [
15595 Point::new(0, 0)..Point::new(0, 0),
15596 Point::new(0, 1)..Point::new(0, 1)
15597 ]
15598 );
15599
15600 // Refreshing selections will relocate the first selection to the original buffer
15601 // location.
15602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15603 assert_eq!(
15604 editor.selections.ranges(cx),
15605 [
15606 Point::new(0, 1)..Point::new(0, 1),
15607 Point::new(0, 3)..Point::new(0, 3)
15608 ]
15609 );
15610 assert!(editor.selections.pending_anchor().is_some());
15611 });
15612}
15613
15614#[gpui::test]
15615fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15616 init_test(cx, |_| {});
15617
15618 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15619 let mut excerpt1_id = None;
15620 let multibuffer = cx.new(|cx| {
15621 let mut multibuffer = MultiBuffer::new(ReadWrite);
15622 excerpt1_id = multibuffer
15623 .push_excerpts(
15624 buffer.clone(),
15625 [
15626 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15627 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15628 ],
15629 cx,
15630 )
15631 .into_iter()
15632 .next();
15633 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15634 multibuffer
15635 });
15636
15637 let editor = cx.add_window(|window, cx| {
15638 let mut editor = build_editor(multibuffer.clone(), window, cx);
15639 let snapshot = editor.snapshot(window, cx);
15640 editor.begin_selection(
15641 Point::new(1, 3).to_display_point(&snapshot),
15642 false,
15643 1,
15644 window,
15645 cx,
15646 );
15647 assert_eq!(
15648 editor.selections.ranges(cx),
15649 [Point::new(1, 3)..Point::new(1, 3)]
15650 );
15651 editor
15652 });
15653
15654 multibuffer.update(cx, |multibuffer, cx| {
15655 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15656 });
15657 _ = editor.update(cx, |editor, window, cx| {
15658 assert_eq!(
15659 editor.selections.ranges(cx),
15660 [Point::new(0, 0)..Point::new(0, 0)]
15661 );
15662
15663 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15665 assert_eq!(
15666 editor.selections.ranges(cx),
15667 [Point::new(0, 3)..Point::new(0, 3)]
15668 );
15669 assert!(editor.selections.pending_anchor().is_some());
15670 });
15671}
15672
15673#[gpui::test]
15674async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15675 init_test(cx, |_| {});
15676
15677 let language = Arc::new(
15678 Language::new(
15679 LanguageConfig {
15680 brackets: BracketPairConfig {
15681 pairs: vec![
15682 BracketPair {
15683 start: "{".to_string(),
15684 end: "}".to_string(),
15685 close: true,
15686 surround: true,
15687 newline: true,
15688 },
15689 BracketPair {
15690 start: "/* ".to_string(),
15691 end: " */".to_string(),
15692 close: true,
15693 surround: true,
15694 newline: true,
15695 },
15696 ],
15697 ..Default::default()
15698 },
15699 ..Default::default()
15700 },
15701 Some(tree_sitter_rust::LANGUAGE.into()),
15702 )
15703 .with_indents_query("")
15704 .unwrap(),
15705 );
15706
15707 let text = concat!(
15708 "{ }\n", //
15709 " x\n", //
15710 " /* */\n", //
15711 "x\n", //
15712 "{{} }\n", //
15713 );
15714
15715 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15716 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15717 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15718 editor
15719 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
15720 .await;
15721
15722 editor.update_in(cx, |editor, window, cx| {
15723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15724 s.select_display_ranges([
15725 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
15726 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
15727 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
15728 ])
15729 });
15730 editor.newline(&Newline, window, cx);
15731
15732 assert_eq!(
15733 editor.buffer().read(cx).read(cx).text(),
15734 concat!(
15735 "{ \n", // Suppress rustfmt
15736 "\n", //
15737 "}\n", //
15738 " x\n", //
15739 " /* \n", //
15740 " \n", //
15741 " */\n", //
15742 "x\n", //
15743 "{{} \n", //
15744 "}\n", //
15745 )
15746 );
15747 });
15748}
15749
15750#[gpui::test]
15751fn test_highlighted_ranges(cx: &mut TestAppContext) {
15752 init_test(cx, |_| {});
15753
15754 let editor = cx.add_window(|window, cx| {
15755 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
15756 build_editor(buffer, window, cx)
15757 });
15758
15759 _ = editor.update(cx, |editor, window, cx| {
15760 struct Type1;
15761 struct Type2;
15762
15763 let buffer = editor.buffer.read(cx).snapshot(cx);
15764
15765 let anchor_range =
15766 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
15767
15768 editor.highlight_background::<Type1>(
15769 &[
15770 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
15771 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
15772 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
15773 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
15774 ],
15775 |_| Hsla::red(),
15776 cx,
15777 );
15778 editor.highlight_background::<Type2>(
15779 &[
15780 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
15781 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
15782 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
15783 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
15784 ],
15785 |_| Hsla::green(),
15786 cx,
15787 );
15788
15789 let snapshot = editor.snapshot(window, cx);
15790 let highlighted_ranges = editor.sorted_background_highlights_in_range(
15791 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
15792 &snapshot,
15793 cx.theme(),
15794 );
15795 assert_eq!(
15796 highlighted_ranges,
15797 &[
15798 (
15799 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
15800 Hsla::green(),
15801 ),
15802 (
15803 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
15804 Hsla::red(),
15805 ),
15806 (
15807 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
15808 Hsla::green(),
15809 ),
15810 (
15811 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15812 Hsla::red(),
15813 ),
15814 ]
15815 );
15816 assert_eq!(
15817 editor.sorted_background_highlights_in_range(
15818 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
15819 &snapshot,
15820 cx.theme(),
15821 ),
15822 &[(
15823 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15824 Hsla::red(),
15825 )]
15826 );
15827 });
15828}
15829
15830#[gpui::test]
15831async fn test_following(cx: &mut TestAppContext) {
15832 init_test(cx, |_| {});
15833
15834 let fs = FakeFs::new(cx.executor());
15835 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15836
15837 let buffer = project.update(cx, |project, cx| {
15838 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
15839 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
15840 });
15841 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
15842 let follower = cx.update(|cx| {
15843 cx.open_window(
15844 WindowOptions {
15845 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
15846 gpui::Point::new(px(0.), px(0.)),
15847 gpui::Point::new(px(10.), px(80.)),
15848 ))),
15849 ..Default::default()
15850 },
15851 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
15852 )
15853 .unwrap()
15854 });
15855
15856 let is_still_following = Rc::new(RefCell::new(true));
15857 let follower_edit_event_count = Rc::new(RefCell::new(0));
15858 let pending_update = Rc::new(RefCell::new(None));
15859 let leader_entity = leader.root(cx).unwrap();
15860 let follower_entity = follower.root(cx).unwrap();
15861 _ = follower.update(cx, {
15862 let update = pending_update.clone();
15863 let is_still_following = is_still_following.clone();
15864 let follower_edit_event_count = follower_edit_event_count.clone();
15865 |_, window, cx| {
15866 cx.subscribe_in(
15867 &leader_entity,
15868 window,
15869 move |_, leader, event, window, cx| {
15870 leader.read(cx).add_event_to_update_proto(
15871 event,
15872 &mut update.borrow_mut(),
15873 window,
15874 cx,
15875 );
15876 },
15877 )
15878 .detach();
15879
15880 cx.subscribe_in(
15881 &follower_entity,
15882 window,
15883 move |_, _, event: &EditorEvent, _window, _cx| {
15884 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
15885 *is_still_following.borrow_mut() = false;
15886 }
15887
15888 if let EditorEvent::BufferEdited = event {
15889 *follower_edit_event_count.borrow_mut() += 1;
15890 }
15891 },
15892 )
15893 .detach();
15894 }
15895 });
15896
15897 // Update the selections only
15898 _ = leader.update(cx, |leader, window, cx| {
15899 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15900 s.select_ranges([1..1])
15901 });
15902 });
15903 follower
15904 .update(cx, |follower, window, cx| {
15905 follower.apply_update_proto(
15906 &project,
15907 pending_update.borrow_mut().take().unwrap(),
15908 window,
15909 cx,
15910 )
15911 })
15912 .unwrap()
15913 .await
15914 .unwrap();
15915 _ = follower.update(cx, |follower, _, cx| {
15916 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15917 });
15918 assert!(*is_still_following.borrow());
15919 assert_eq!(*follower_edit_event_count.borrow(), 0);
15920
15921 // Update the scroll position only
15922 _ = leader.update(cx, |leader, window, cx| {
15923 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15924 });
15925 follower
15926 .update(cx, |follower, window, cx| {
15927 follower.apply_update_proto(
15928 &project,
15929 pending_update.borrow_mut().take().unwrap(),
15930 window,
15931 cx,
15932 )
15933 })
15934 .unwrap()
15935 .await
15936 .unwrap();
15937 assert_eq!(
15938 follower
15939 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15940 .unwrap(),
15941 gpui::Point::new(1.5, 3.5)
15942 );
15943 assert!(*is_still_following.borrow());
15944 assert_eq!(*follower_edit_event_count.borrow(), 0);
15945
15946 // Update the selections and scroll position. The follower's scroll position is updated
15947 // via autoscroll, not via the leader's exact scroll position.
15948 _ = leader.update(cx, |leader, window, cx| {
15949 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15950 s.select_ranges([0..0])
15951 });
15952 leader.request_autoscroll(Autoscroll::newest(), cx);
15953 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15954 });
15955 follower
15956 .update(cx, |follower, window, cx| {
15957 follower.apply_update_proto(
15958 &project,
15959 pending_update.borrow_mut().take().unwrap(),
15960 window,
15961 cx,
15962 )
15963 })
15964 .unwrap()
15965 .await
15966 .unwrap();
15967 _ = follower.update(cx, |follower, _, cx| {
15968 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15969 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15970 });
15971 assert!(*is_still_following.borrow());
15972
15973 // Creating a pending selection that precedes another selection
15974 _ = leader.update(cx, |leader, window, cx| {
15975 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15976 s.select_ranges([1..1])
15977 });
15978 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15979 });
15980 follower
15981 .update(cx, |follower, window, cx| {
15982 follower.apply_update_proto(
15983 &project,
15984 pending_update.borrow_mut().take().unwrap(),
15985 window,
15986 cx,
15987 )
15988 })
15989 .unwrap()
15990 .await
15991 .unwrap();
15992 _ = follower.update(cx, |follower, _, cx| {
15993 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
15994 });
15995 assert!(*is_still_following.borrow());
15996
15997 // Extend the pending selection so that it surrounds another selection
15998 _ = leader.update(cx, |leader, window, cx| {
15999 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16000 });
16001 follower
16002 .update(cx, |follower, window, cx| {
16003 follower.apply_update_proto(
16004 &project,
16005 pending_update.borrow_mut().take().unwrap(),
16006 window,
16007 cx,
16008 )
16009 })
16010 .unwrap()
16011 .await
16012 .unwrap();
16013 _ = follower.update(cx, |follower, _, cx| {
16014 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16015 });
16016
16017 // Scrolling locally breaks the follow
16018 _ = follower.update(cx, |follower, window, cx| {
16019 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16020 follower.set_scroll_anchor(
16021 ScrollAnchor {
16022 anchor: top_anchor,
16023 offset: gpui::Point::new(0.0, 0.5),
16024 },
16025 window,
16026 cx,
16027 );
16028 });
16029 assert!(!(*is_still_following.borrow()));
16030}
16031
16032#[gpui::test]
16033async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16034 init_test(cx, |_| {});
16035
16036 let fs = FakeFs::new(cx.executor());
16037 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16038 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16039 let pane = workspace
16040 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16041 .unwrap();
16042
16043 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16044
16045 let leader = pane.update_in(cx, |_, window, cx| {
16046 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16047 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16048 });
16049
16050 // Start following the editor when it has no excerpts.
16051 let mut state_message =
16052 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16053 let workspace_entity = workspace.root(cx).unwrap();
16054 let follower_1 = cx
16055 .update_window(*workspace.deref(), |_, window, cx| {
16056 Editor::from_state_proto(
16057 workspace_entity,
16058 ViewId {
16059 creator: CollaboratorId::PeerId(PeerId::default()),
16060 id: 0,
16061 },
16062 &mut state_message,
16063 window,
16064 cx,
16065 )
16066 })
16067 .unwrap()
16068 .unwrap()
16069 .await
16070 .unwrap();
16071
16072 let update_message = Rc::new(RefCell::new(None));
16073 follower_1.update_in(cx, {
16074 let update = update_message.clone();
16075 |_, window, cx| {
16076 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16077 leader.read(cx).add_event_to_update_proto(
16078 event,
16079 &mut update.borrow_mut(),
16080 window,
16081 cx,
16082 );
16083 })
16084 .detach();
16085 }
16086 });
16087
16088 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16089 (
16090 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
16091 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
16092 )
16093 });
16094
16095 // Insert some excerpts.
16096 leader.update(cx, |leader, cx| {
16097 leader.buffer.update(cx, |multibuffer, cx| {
16098 multibuffer.set_excerpts_for_path(
16099 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16100 buffer_1.clone(),
16101 vec![
16102 Point::row_range(0..3),
16103 Point::row_range(1..6),
16104 Point::row_range(12..15),
16105 ],
16106 0,
16107 cx,
16108 );
16109 multibuffer.set_excerpts_for_path(
16110 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16111 buffer_2.clone(),
16112 vec![Point::row_range(0..6), Point::row_range(8..12)],
16113 0,
16114 cx,
16115 );
16116 });
16117 });
16118
16119 // Apply the update of adding the excerpts.
16120 follower_1
16121 .update_in(cx, |follower, window, cx| {
16122 follower.apply_update_proto(
16123 &project,
16124 update_message.borrow().clone().unwrap(),
16125 window,
16126 cx,
16127 )
16128 })
16129 .await
16130 .unwrap();
16131 assert_eq!(
16132 follower_1.update(cx, |editor, cx| editor.text(cx)),
16133 leader.update(cx, |editor, cx| editor.text(cx))
16134 );
16135 update_message.borrow_mut().take();
16136
16137 // Start following separately after it already has excerpts.
16138 let mut state_message =
16139 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16140 let workspace_entity = workspace.root(cx).unwrap();
16141 let follower_2 = cx
16142 .update_window(*workspace.deref(), |_, window, cx| {
16143 Editor::from_state_proto(
16144 workspace_entity,
16145 ViewId {
16146 creator: CollaboratorId::PeerId(PeerId::default()),
16147 id: 0,
16148 },
16149 &mut state_message,
16150 window,
16151 cx,
16152 )
16153 })
16154 .unwrap()
16155 .unwrap()
16156 .await
16157 .unwrap();
16158 assert_eq!(
16159 follower_2.update(cx, |editor, cx| editor.text(cx)),
16160 leader.update(cx, |editor, cx| editor.text(cx))
16161 );
16162
16163 // Remove some excerpts.
16164 leader.update(cx, |leader, cx| {
16165 leader.buffer.update(cx, |multibuffer, cx| {
16166 let excerpt_ids = multibuffer.excerpt_ids();
16167 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16168 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16169 });
16170 });
16171
16172 // Apply the update of removing the excerpts.
16173 follower_1
16174 .update_in(cx, |follower, window, cx| {
16175 follower.apply_update_proto(
16176 &project,
16177 update_message.borrow().clone().unwrap(),
16178 window,
16179 cx,
16180 )
16181 })
16182 .await
16183 .unwrap();
16184 follower_2
16185 .update_in(cx, |follower, window, cx| {
16186 follower.apply_update_proto(
16187 &project,
16188 update_message.borrow().clone().unwrap(),
16189 window,
16190 cx,
16191 )
16192 })
16193 .await
16194 .unwrap();
16195 update_message.borrow_mut().take();
16196 assert_eq!(
16197 follower_1.update(cx, |editor, cx| editor.text(cx)),
16198 leader.update(cx, |editor, cx| editor.text(cx))
16199 );
16200}
16201
16202#[gpui::test]
16203async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16204 init_test(cx, |_| {});
16205
16206 let mut cx = EditorTestContext::new(cx).await;
16207 let lsp_store =
16208 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16209
16210 cx.set_state(indoc! {"
16211 ˇfn func(abc def: i32) -> u32 {
16212 }
16213 "});
16214
16215 cx.update(|_, cx| {
16216 lsp_store.update(cx, |lsp_store, cx| {
16217 lsp_store
16218 .update_diagnostics(
16219 LanguageServerId(0),
16220 lsp::PublishDiagnosticsParams {
16221 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16222 version: None,
16223 diagnostics: vec![
16224 lsp::Diagnostic {
16225 range: lsp::Range::new(
16226 lsp::Position::new(0, 11),
16227 lsp::Position::new(0, 12),
16228 ),
16229 severity: Some(lsp::DiagnosticSeverity::ERROR),
16230 ..Default::default()
16231 },
16232 lsp::Diagnostic {
16233 range: lsp::Range::new(
16234 lsp::Position::new(0, 12),
16235 lsp::Position::new(0, 15),
16236 ),
16237 severity: Some(lsp::DiagnosticSeverity::ERROR),
16238 ..Default::default()
16239 },
16240 lsp::Diagnostic {
16241 range: lsp::Range::new(
16242 lsp::Position::new(0, 25),
16243 lsp::Position::new(0, 28),
16244 ),
16245 severity: Some(lsp::DiagnosticSeverity::ERROR),
16246 ..Default::default()
16247 },
16248 ],
16249 },
16250 None,
16251 DiagnosticSourceKind::Pushed,
16252 &[],
16253 cx,
16254 )
16255 .unwrap()
16256 });
16257 });
16258
16259 executor.run_until_parked();
16260
16261 cx.update_editor(|editor, window, cx| {
16262 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16263 });
16264
16265 cx.assert_editor_state(indoc! {"
16266 fn func(abc def: i32) -> ˇu32 {
16267 }
16268 "});
16269
16270 cx.update_editor(|editor, window, cx| {
16271 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16272 });
16273
16274 cx.assert_editor_state(indoc! {"
16275 fn func(abc ˇdef: i32) -> u32 {
16276 }
16277 "});
16278
16279 cx.update_editor(|editor, window, cx| {
16280 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16281 });
16282
16283 cx.assert_editor_state(indoc! {"
16284 fn func(abcˇ def: i32) -> u32 {
16285 }
16286 "});
16287
16288 cx.update_editor(|editor, window, cx| {
16289 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16290 });
16291
16292 cx.assert_editor_state(indoc! {"
16293 fn func(abc def: i32) -> ˇu32 {
16294 }
16295 "});
16296}
16297
16298#[gpui::test]
16299async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16300 init_test(cx, |_| {});
16301
16302 let mut cx = EditorTestContext::new(cx).await;
16303
16304 let diff_base = r#"
16305 use some::mod;
16306
16307 const A: u32 = 42;
16308
16309 fn main() {
16310 println!("hello");
16311
16312 println!("world");
16313 }
16314 "#
16315 .unindent();
16316
16317 // Edits are modified, removed, modified, added
16318 cx.set_state(
16319 &r#"
16320 use some::modified;
16321
16322 ˇ
16323 fn main() {
16324 println!("hello there");
16325
16326 println!("around the");
16327 println!("world");
16328 }
16329 "#
16330 .unindent(),
16331 );
16332
16333 cx.set_head_text(&diff_base);
16334 executor.run_until_parked();
16335
16336 cx.update_editor(|editor, window, cx| {
16337 //Wrap around the bottom of the buffer
16338 for _ in 0..3 {
16339 editor.go_to_next_hunk(&GoToHunk, window, cx);
16340 }
16341 });
16342
16343 cx.assert_editor_state(
16344 &r#"
16345 ˇuse some::modified;
16346
16347
16348 fn main() {
16349 println!("hello there");
16350
16351 println!("around the");
16352 println!("world");
16353 }
16354 "#
16355 .unindent(),
16356 );
16357
16358 cx.update_editor(|editor, window, cx| {
16359 //Wrap around the top of the buffer
16360 for _ in 0..2 {
16361 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16362 }
16363 });
16364
16365 cx.assert_editor_state(
16366 &r#"
16367 use some::modified;
16368
16369
16370 fn main() {
16371 ˇ println!("hello there");
16372
16373 println!("around the");
16374 println!("world");
16375 }
16376 "#
16377 .unindent(),
16378 );
16379
16380 cx.update_editor(|editor, window, cx| {
16381 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16382 });
16383
16384 cx.assert_editor_state(
16385 &r#"
16386 use some::modified;
16387
16388 ˇ
16389 fn main() {
16390 println!("hello there");
16391
16392 println!("around the");
16393 println!("world");
16394 }
16395 "#
16396 .unindent(),
16397 );
16398
16399 cx.update_editor(|editor, window, cx| {
16400 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16401 });
16402
16403 cx.assert_editor_state(
16404 &r#"
16405 ˇuse some::modified;
16406
16407
16408 fn main() {
16409 println!("hello there");
16410
16411 println!("around the");
16412 println!("world");
16413 }
16414 "#
16415 .unindent(),
16416 );
16417
16418 cx.update_editor(|editor, window, cx| {
16419 for _ in 0..2 {
16420 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16421 }
16422 });
16423
16424 cx.assert_editor_state(
16425 &r#"
16426 use some::modified;
16427
16428
16429 fn main() {
16430 ˇ println!("hello there");
16431
16432 println!("around the");
16433 println!("world");
16434 }
16435 "#
16436 .unindent(),
16437 );
16438
16439 cx.update_editor(|editor, window, cx| {
16440 editor.fold(&Fold, window, cx);
16441 });
16442
16443 cx.update_editor(|editor, window, cx| {
16444 editor.go_to_next_hunk(&GoToHunk, window, cx);
16445 });
16446
16447 cx.assert_editor_state(
16448 &r#"
16449 ˇuse some::modified;
16450
16451
16452 fn main() {
16453 println!("hello there");
16454
16455 println!("around the");
16456 println!("world");
16457 }
16458 "#
16459 .unindent(),
16460 );
16461}
16462
16463#[test]
16464fn test_split_words() {
16465 fn split(text: &str) -> Vec<&str> {
16466 split_words(text).collect()
16467 }
16468
16469 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16470 assert_eq!(split("hello_world"), &["hello_", "world"]);
16471 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16472 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16473 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16474 assert_eq!(split("helloworld"), &["helloworld"]);
16475
16476 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16477}
16478
16479#[gpui::test]
16480async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16481 init_test(cx, |_| {});
16482
16483 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16484 let mut assert = |before, after| {
16485 let _state_context = cx.set_state(before);
16486 cx.run_until_parked();
16487 cx.update_editor(|editor, window, cx| {
16488 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16489 });
16490 cx.run_until_parked();
16491 cx.assert_editor_state(after);
16492 };
16493
16494 // Outside bracket jumps to outside of matching bracket
16495 assert("console.logˇ(var);", "console.log(var)ˇ;");
16496 assert("console.log(var)ˇ;", "console.logˇ(var);");
16497
16498 // Inside bracket jumps to inside of matching bracket
16499 assert("console.log(ˇvar);", "console.log(varˇ);");
16500 assert("console.log(varˇ);", "console.log(ˇvar);");
16501
16502 // When outside a bracket and inside, favor jumping to the inside bracket
16503 assert(
16504 "console.log('foo', [1, 2, 3]ˇ);",
16505 "console.log(ˇ'foo', [1, 2, 3]);",
16506 );
16507 assert(
16508 "console.log(ˇ'foo', [1, 2, 3]);",
16509 "console.log('foo', [1, 2, 3]ˇ);",
16510 );
16511
16512 // Bias forward if two options are equally likely
16513 assert(
16514 "let result = curried_fun()ˇ();",
16515 "let result = curried_fun()()ˇ;",
16516 );
16517
16518 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16519 assert(
16520 indoc! {"
16521 function test() {
16522 console.log('test')ˇ
16523 }"},
16524 indoc! {"
16525 function test() {
16526 console.logˇ('test')
16527 }"},
16528 );
16529}
16530
16531#[gpui::test]
16532async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16533 init_test(cx, |_| {});
16534
16535 let fs = FakeFs::new(cx.executor());
16536 fs.insert_tree(
16537 path!("/a"),
16538 json!({
16539 "main.rs": "fn main() { let a = 5; }",
16540 "other.rs": "// Test file",
16541 }),
16542 )
16543 .await;
16544 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16545
16546 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16547 language_registry.add(Arc::new(Language::new(
16548 LanguageConfig {
16549 name: "Rust".into(),
16550 matcher: LanguageMatcher {
16551 path_suffixes: vec!["rs".to_string()],
16552 ..Default::default()
16553 },
16554 brackets: BracketPairConfig {
16555 pairs: vec![BracketPair {
16556 start: "{".to_string(),
16557 end: "}".to_string(),
16558 close: true,
16559 surround: true,
16560 newline: true,
16561 }],
16562 disabled_scopes_by_bracket_ix: Vec::new(),
16563 },
16564 ..Default::default()
16565 },
16566 Some(tree_sitter_rust::LANGUAGE.into()),
16567 )));
16568 let mut fake_servers = language_registry.register_fake_lsp(
16569 "Rust",
16570 FakeLspAdapter {
16571 capabilities: lsp::ServerCapabilities {
16572 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16573 first_trigger_character: "{".to_string(),
16574 more_trigger_character: None,
16575 }),
16576 ..Default::default()
16577 },
16578 ..Default::default()
16579 },
16580 );
16581
16582 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16583
16584 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16585
16586 let worktree_id = workspace
16587 .update(cx, |workspace, _, cx| {
16588 workspace.project().update(cx, |project, cx| {
16589 project.worktrees(cx).next().unwrap().read(cx).id()
16590 })
16591 })
16592 .unwrap();
16593
16594 let buffer = project
16595 .update(cx, |project, cx| {
16596 project.open_local_buffer(path!("/a/main.rs"), cx)
16597 })
16598 .await
16599 .unwrap();
16600 let editor_handle = workspace
16601 .update(cx, |workspace, window, cx| {
16602 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16603 })
16604 .unwrap()
16605 .await
16606 .unwrap()
16607 .downcast::<Editor>()
16608 .unwrap();
16609
16610 cx.executor().start_waiting();
16611 let fake_server = fake_servers.next().await.unwrap();
16612
16613 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16614 |params, _| async move {
16615 assert_eq!(
16616 params.text_document_position.text_document.uri,
16617 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16618 );
16619 assert_eq!(
16620 params.text_document_position.position,
16621 lsp::Position::new(0, 21),
16622 );
16623
16624 Ok(Some(vec![lsp::TextEdit {
16625 new_text: "]".to_string(),
16626 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16627 }]))
16628 },
16629 );
16630
16631 editor_handle.update_in(cx, |editor, window, cx| {
16632 window.focus(&editor.focus_handle(cx));
16633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16634 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16635 });
16636 editor.handle_input("{", window, cx);
16637 });
16638
16639 cx.executor().run_until_parked();
16640
16641 buffer.update(cx, |buffer, _| {
16642 assert_eq!(
16643 buffer.text(),
16644 "fn main() { let a = {5}; }",
16645 "No extra braces from on type formatting should appear in the buffer"
16646 )
16647 });
16648}
16649
16650#[gpui::test(iterations = 20, seeds(31))]
16651async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16652 init_test(cx, |_| {});
16653
16654 let mut cx = EditorLspTestContext::new_rust(
16655 lsp::ServerCapabilities {
16656 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16657 first_trigger_character: ".".to_string(),
16658 more_trigger_character: None,
16659 }),
16660 ..Default::default()
16661 },
16662 cx,
16663 )
16664 .await;
16665
16666 cx.update_buffer(|buffer, _| {
16667 // This causes autoindent to be async.
16668 buffer.set_sync_parse_timeout(Duration::ZERO)
16669 });
16670
16671 cx.set_state("fn c() {\n d()ˇ\n}\n");
16672 cx.simulate_keystroke("\n");
16673 cx.run_until_parked();
16674
16675 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16676 let mut request =
16677 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16678 let buffer_cloned = buffer_cloned.clone();
16679 async move {
16680 buffer_cloned.update(&mut cx, |buffer, _| {
16681 assert_eq!(
16682 buffer.text(),
16683 "fn c() {\n d()\n .\n}\n",
16684 "OnTypeFormatting should triggered after autoindent applied"
16685 )
16686 })?;
16687
16688 Ok(Some(vec![]))
16689 }
16690 });
16691
16692 cx.simulate_keystroke(".");
16693 cx.run_until_parked();
16694
16695 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16696 assert!(request.next().await.is_some());
16697 request.close();
16698 assert!(request.next().await.is_none());
16699}
16700
16701#[gpui::test]
16702async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
16703 init_test(cx, |_| {});
16704
16705 let fs = FakeFs::new(cx.executor());
16706 fs.insert_tree(
16707 path!("/a"),
16708 json!({
16709 "main.rs": "fn main() { let a = 5; }",
16710 "other.rs": "// Test file",
16711 }),
16712 )
16713 .await;
16714
16715 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16716
16717 let server_restarts = Arc::new(AtomicUsize::new(0));
16718 let closure_restarts = Arc::clone(&server_restarts);
16719 let language_server_name = "test language server";
16720 let language_name: LanguageName = "Rust".into();
16721
16722 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16723 language_registry.add(Arc::new(Language::new(
16724 LanguageConfig {
16725 name: language_name.clone(),
16726 matcher: LanguageMatcher {
16727 path_suffixes: vec!["rs".to_string()],
16728 ..Default::default()
16729 },
16730 ..Default::default()
16731 },
16732 Some(tree_sitter_rust::LANGUAGE.into()),
16733 )));
16734 let mut fake_servers = language_registry.register_fake_lsp(
16735 "Rust",
16736 FakeLspAdapter {
16737 name: language_server_name,
16738 initialization_options: Some(json!({
16739 "testOptionValue": true
16740 })),
16741 initializer: Some(Box::new(move |fake_server| {
16742 let task_restarts = Arc::clone(&closure_restarts);
16743 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
16744 task_restarts.fetch_add(1, atomic::Ordering::Release);
16745 futures::future::ready(Ok(()))
16746 });
16747 })),
16748 ..Default::default()
16749 },
16750 );
16751
16752 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16753 let _buffer = project
16754 .update(cx, |project, cx| {
16755 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
16756 })
16757 .await
16758 .unwrap();
16759 let _fake_server = fake_servers.next().await.unwrap();
16760 update_test_language_settings(cx, |language_settings| {
16761 language_settings.languages.0.insert(
16762 language_name.clone(),
16763 LanguageSettingsContent {
16764 tab_size: NonZeroU32::new(8),
16765 ..Default::default()
16766 },
16767 );
16768 });
16769 cx.executor().run_until_parked();
16770 assert_eq!(
16771 server_restarts.load(atomic::Ordering::Acquire),
16772 0,
16773 "Should not restart LSP server on an unrelated change"
16774 );
16775
16776 update_test_project_settings(cx, |project_settings| {
16777 project_settings.lsp.insert(
16778 "Some other server name".into(),
16779 LspSettings {
16780 binary: None,
16781 settings: None,
16782 initialization_options: Some(json!({
16783 "some other init value": false
16784 })),
16785 enable_lsp_tasks: false,
16786 },
16787 );
16788 });
16789 cx.executor().run_until_parked();
16790 assert_eq!(
16791 server_restarts.load(atomic::Ordering::Acquire),
16792 0,
16793 "Should not restart LSP server on an unrelated LSP settings change"
16794 );
16795
16796 update_test_project_settings(cx, |project_settings| {
16797 project_settings.lsp.insert(
16798 language_server_name.into(),
16799 LspSettings {
16800 binary: None,
16801 settings: None,
16802 initialization_options: Some(json!({
16803 "anotherInitValue": false
16804 })),
16805 enable_lsp_tasks: false,
16806 },
16807 );
16808 });
16809 cx.executor().run_until_parked();
16810 assert_eq!(
16811 server_restarts.load(atomic::Ordering::Acquire),
16812 1,
16813 "Should restart LSP server on a related LSP settings change"
16814 );
16815
16816 update_test_project_settings(cx, |project_settings| {
16817 project_settings.lsp.insert(
16818 language_server_name.into(),
16819 LspSettings {
16820 binary: None,
16821 settings: None,
16822 initialization_options: Some(json!({
16823 "anotherInitValue": false
16824 })),
16825 enable_lsp_tasks: false,
16826 },
16827 );
16828 });
16829 cx.executor().run_until_parked();
16830 assert_eq!(
16831 server_restarts.load(atomic::Ordering::Acquire),
16832 1,
16833 "Should not restart LSP server on a related LSP settings change that is the same"
16834 );
16835
16836 update_test_project_settings(cx, |project_settings| {
16837 project_settings.lsp.insert(
16838 language_server_name.into(),
16839 LspSettings {
16840 binary: None,
16841 settings: None,
16842 initialization_options: None,
16843 enable_lsp_tasks: false,
16844 },
16845 );
16846 });
16847 cx.executor().run_until_parked();
16848 assert_eq!(
16849 server_restarts.load(atomic::Ordering::Acquire),
16850 2,
16851 "Should restart LSP server on another related LSP settings change"
16852 );
16853}
16854
16855#[gpui::test]
16856async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
16857 init_test(cx, |_| {});
16858
16859 let mut cx = EditorLspTestContext::new_rust(
16860 lsp::ServerCapabilities {
16861 completion_provider: Some(lsp::CompletionOptions {
16862 trigger_characters: Some(vec![".".to_string()]),
16863 resolve_provider: Some(true),
16864 ..Default::default()
16865 }),
16866 ..Default::default()
16867 },
16868 cx,
16869 )
16870 .await;
16871
16872 cx.set_state("fn main() { let a = 2ˇ; }");
16873 cx.simulate_keystroke(".");
16874 let completion_item = lsp::CompletionItem {
16875 label: "some".into(),
16876 kind: Some(lsp::CompletionItemKind::SNIPPET),
16877 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16878 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16879 kind: lsp::MarkupKind::Markdown,
16880 value: "```rust\nSome(2)\n```".to_string(),
16881 })),
16882 deprecated: Some(false),
16883 sort_text: Some("fffffff2".to_string()),
16884 filter_text: Some("some".to_string()),
16885 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16886 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16887 range: lsp::Range {
16888 start: lsp::Position {
16889 line: 0,
16890 character: 22,
16891 },
16892 end: lsp::Position {
16893 line: 0,
16894 character: 22,
16895 },
16896 },
16897 new_text: "Some(2)".to_string(),
16898 })),
16899 additional_text_edits: Some(vec![lsp::TextEdit {
16900 range: lsp::Range {
16901 start: lsp::Position {
16902 line: 0,
16903 character: 20,
16904 },
16905 end: lsp::Position {
16906 line: 0,
16907 character: 22,
16908 },
16909 },
16910 new_text: "".to_string(),
16911 }]),
16912 ..Default::default()
16913 };
16914
16915 let closure_completion_item = completion_item.clone();
16916 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16917 let task_completion_item = closure_completion_item.clone();
16918 async move {
16919 Ok(Some(lsp::CompletionResponse::Array(vec![
16920 task_completion_item,
16921 ])))
16922 }
16923 });
16924
16925 request.next().await;
16926
16927 cx.condition(|editor, _| editor.context_menu_visible())
16928 .await;
16929 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16930 editor
16931 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16932 .unwrap()
16933 });
16934 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16935
16936 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16937 let task_completion_item = completion_item.clone();
16938 async move { Ok(task_completion_item) }
16939 })
16940 .next()
16941 .await
16942 .unwrap();
16943 apply_additional_edits.await.unwrap();
16944 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16945}
16946
16947#[gpui::test]
16948async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16949 init_test(cx, |_| {});
16950
16951 let mut cx = EditorLspTestContext::new_rust(
16952 lsp::ServerCapabilities {
16953 completion_provider: Some(lsp::CompletionOptions {
16954 trigger_characters: Some(vec![".".to_string()]),
16955 resolve_provider: Some(true),
16956 ..Default::default()
16957 }),
16958 ..Default::default()
16959 },
16960 cx,
16961 )
16962 .await;
16963
16964 cx.set_state("fn main() { let a = 2ˇ; }");
16965 cx.simulate_keystroke(".");
16966
16967 let item1 = lsp::CompletionItem {
16968 label: "method id()".to_string(),
16969 filter_text: Some("id".to_string()),
16970 detail: None,
16971 documentation: None,
16972 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16973 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16974 new_text: ".id".to_string(),
16975 })),
16976 ..lsp::CompletionItem::default()
16977 };
16978
16979 let item2 = lsp::CompletionItem {
16980 label: "other".to_string(),
16981 filter_text: Some("other".to_string()),
16982 detail: None,
16983 documentation: None,
16984 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16985 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16986 new_text: ".other".to_string(),
16987 })),
16988 ..lsp::CompletionItem::default()
16989 };
16990
16991 let item1 = item1.clone();
16992 cx.set_request_handler::<lsp::request::Completion, _, _>({
16993 let item1 = item1.clone();
16994 move |_, _, _| {
16995 let item1 = item1.clone();
16996 let item2 = item2.clone();
16997 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
16998 }
16999 })
17000 .next()
17001 .await;
17002
17003 cx.condition(|editor, _| editor.context_menu_visible())
17004 .await;
17005 cx.update_editor(|editor, _, _| {
17006 let context_menu = editor.context_menu.borrow_mut();
17007 let context_menu = context_menu
17008 .as_ref()
17009 .expect("Should have the context menu deployed");
17010 match context_menu {
17011 CodeContextMenu::Completions(completions_menu) => {
17012 let completions = completions_menu.completions.borrow_mut();
17013 assert_eq!(
17014 completions
17015 .iter()
17016 .map(|completion| &completion.label.text)
17017 .collect::<Vec<_>>(),
17018 vec!["method id()", "other"]
17019 )
17020 }
17021 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17022 }
17023 });
17024
17025 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17026 let item1 = item1.clone();
17027 move |_, item_to_resolve, _| {
17028 let item1 = item1.clone();
17029 async move {
17030 if item1 == item_to_resolve {
17031 Ok(lsp::CompletionItem {
17032 label: "method id()".to_string(),
17033 filter_text: Some("id".to_string()),
17034 detail: Some("Now resolved!".to_string()),
17035 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17036 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17037 range: lsp::Range::new(
17038 lsp::Position::new(0, 22),
17039 lsp::Position::new(0, 22),
17040 ),
17041 new_text: ".id".to_string(),
17042 })),
17043 ..lsp::CompletionItem::default()
17044 })
17045 } else {
17046 Ok(item_to_resolve)
17047 }
17048 }
17049 }
17050 })
17051 .next()
17052 .await
17053 .unwrap();
17054 cx.run_until_parked();
17055
17056 cx.update_editor(|editor, window, cx| {
17057 editor.context_menu_next(&Default::default(), window, cx);
17058 });
17059
17060 cx.update_editor(|editor, _, _| {
17061 let context_menu = editor.context_menu.borrow_mut();
17062 let context_menu = context_menu
17063 .as_ref()
17064 .expect("Should have the context menu deployed");
17065 match context_menu {
17066 CodeContextMenu::Completions(completions_menu) => {
17067 let completions = completions_menu.completions.borrow_mut();
17068 assert_eq!(
17069 completions
17070 .iter()
17071 .map(|completion| &completion.label.text)
17072 .collect::<Vec<_>>(),
17073 vec!["method id() Now resolved!", "other"],
17074 "Should update first completion label, but not second as the filter text did not match."
17075 );
17076 }
17077 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17078 }
17079 });
17080}
17081
17082#[gpui::test]
17083async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17084 init_test(cx, |_| {});
17085 let mut cx = EditorLspTestContext::new_rust(
17086 lsp::ServerCapabilities {
17087 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17088 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17089 completion_provider: Some(lsp::CompletionOptions {
17090 resolve_provider: Some(true),
17091 ..Default::default()
17092 }),
17093 ..Default::default()
17094 },
17095 cx,
17096 )
17097 .await;
17098 cx.set_state(indoc! {"
17099 struct TestStruct {
17100 field: i32
17101 }
17102
17103 fn mainˇ() {
17104 let unused_var = 42;
17105 let test_struct = TestStruct { field: 42 };
17106 }
17107 "});
17108 let symbol_range = cx.lsp_range(indoc! {"
17109 struct TestStruct {
17110 field: i32
17111 }
17112
17113 «fn main»() {
17114 let unused_var = 42;
17115 let test_struct = TestStruct { field: 42 };
17116 }
17117 "});
17118 let mut hover_requests =
17119 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17120 Ok(Some(lsp::Hover {
17121 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17122 kind: lsp::MarkupKind::Markdown,
17123 value: "Function documentation".to_string(),
17124 }),
17125 range: Some(symbol_range),
17126 }))
17127 });
17128
17129 // Case 1: Test that code action menu hide hover popover
17130 cx.dispatch_action(Hover);
17131 hover_requests.next().await;
17132 cx.condition(|editor, _| editor.hover_state.visible()).await;
17133 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17134 move |_, _, _| async move {
17135 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17136 lsp::CodeAction {
17137 title: "Remove unused variable".to_string(),
17138 kind: Some(CodeActionKind::QUICKFIX),
17139 edit: Some(lsp::WorkspaceEdit {
17140 changes: Some(
17141 [(
17142 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17143 vec![lsp::TextEdit {
17144 range: lsp::Range::new(
17145 lsp::Position::new(5, 4),
17146 lsp::Position::new(5, 27),
17147 ),
17148 new_text: "".to_string(),
17149 }],
17150 )]
17151 .into_iter()
17152 .collect(),
17153 ),
17154 ..Default::default()
17155 }),
17156 ..Default::default()
17157 },
17158 )]))
17159 },
17160 );
17161 cx.update_editor(|editor, window, cx| {
17162 editor.toggle_code_actions(
17163 &ToggleCodeActions {
17164 deployed_from: None,
17165 quick_launch: false,
17166 },
17167 window,
17168 cx,
17169 );
17170 });
17171 code_action_requests.next().await;
17172 cx.run_until_parked();
17173 cx.condition(|editor, _| editor.context_menu_visible())
17174 .await;
17175 cx.update_editor(|editor, _, _| {
17176 assert!(
17177 !editor.hover_state.visible(),
17178 "Hover popover should be hidden when code action menu is shown"
17179 );
17180 // Hide code actions
17181 editor.context_menu.take();
17182 });
17183
17184 // Case 2: Test that code completions hide hover popover
17185 cx.dispatch_action(Hover);
17186 hover_requests.next().await;
17187 cx.condition(|editor, _| editor.hover_state.visible()).await;
17188 let counter = Arc::new(AtomicUsize::new(0));
17189 let mut completion_requests =
17190 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17191 let counter = counter.clone();
17192 async move {
17193 counter.fetch_add(1, atomic::Ordering::Release);
17194 Ok(Some(lsp::CompletionResponse::Array(vec![
17195 lsp::CompletionItem {
17196 label: "main".into(),
17197 kind: Some(lsp::CompletionItemKind::FUNCTION),
17198 detail: Some("() -> ()".to_string()),
17199 ..Default::default()
17200 },
17201 lsp::CompletionItem {
17202 label: "TestStruct".into(),
17203 kind: Some(lsp::CompletionItemKind::STRUCT),
17204 detail: Some("struct TestStruct".to_string()),
17205 ..Default::default()
17206 },
17207 ])))
17208 }
17209 });
17210 cx.update_editor(|editor, window, cx| {
17211 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17212 });
17213 completion_requests.next().await;
17214 cx.condition(|editor, _| editor.context_menu_visible())
17215 .await;
17216 cx.update_editor(|editor, _, _| {
17217 assert!(
17218 !editor.hover_state.visible(),
17219 "Hover popover should be hidden when completion menu is shown"
17220 );
17221 });
17222}
17223
17224#[gpui::test]
17225async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17226 init_test(cx, |_| {});
17227
17228 let mut cx = EditorLspTestContext::new_rust(
17229 lsp::ServerCapabilities {
17230 completion_provider: Some(lsp::CompletionOptions {
17231 trigger_characters: Some(vec![".".to_string()]),
17232 resolve_provider: Some(true),
17233 ..Default::default()
17234 }),
17235 ..Default::default()
17236 },
17237 cx,
17238 )
17239 .await;
17240
17241 cx.set_state("fn main() { let a = 2ˇ; }");
17242 cx.simulate_keystroke(".");
17243
17244 let unresolved_item_1 = lsp::CompletionItem {
17245 label: "id".to_string(),
17246 filter_text: Some("id".to_string()),
17247 detail: None,
17248 documentation: None,
17249 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17250 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17251 new_text: ".id".to_string(),
17252 })),
17253 ..lsp::CompletionItem::default()
17254 };
17255 let resolved_item_1 = lsp::CompletionItem {
17256 additional_text_edits: Some(vec![lsp::TextEdit {
17257 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17258 new_text: "!!".to_string(),
17259 }]),
17260 ..unresolved_item_1.clone()
17261 };
17262 let unresolved_item_2 = lsp::CompletionItem {
17263 label: "other".to_string(),
17264 filter_text: Some("other".to_string()),
17265 detail: None,
17266 documentation: None,
17267 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17268 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17269 new_text: ".other".to_string(),
17270 })),
17271 ..lsp::CompletionItem::default()
17272 };
17273 let resolved_item_2 = lsp::CompletionItem {
17274 additional_text_edits: Some(vec![lsp::TextEdit {
17275 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17276 new_text: "??".to_string(),
17277 }]),
17278 ..unresolved_item_2.clone()
17279 };
17280
17281 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17282 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17283 cx.lsp
17284 .server
17285 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17286 let unresolved_item_1 = unresolved_item_1.clone();
17287 let resolved_item_1 = resolved_item_1.clone();
17288 let unresolved_item_2 = unresolved_item_2.clone();
17289 let resolved_item_2 = resolved_item_2.clone();
17290 let resolve_requests_1 = resolve_requests_1.clone();
17291 let resolve_requests_2 = resolve_requests_2.clone();
17292 move |unresolved_request, _| {
17293 let unresolved_item_1 = unresolved_item_1.clone();
17294 let resolved_item_1 = resolved_item_1.clone();
17295 let unresolved_item_2 = unresolved_item_2.clone();
17296 let resolved_item_2 = resolved_item_2.clone();
17297 let resolve_requests_1 = resolve_requests_1.clone();
17298 let resolve_requests_2 = resolve_requests_2.clone();
17299 async move {
17300 if unresolved_request == unresolved_item_1 {
17301 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17302 Ok(resolved_item_1.clone())
17303 } else if unresolved_request == unresolved_item_2 {
17304 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17305 Ok(resolved_item_2.clone())
17306 } else {
17307 panic!("Unexpected completion item {unresolved_request:?}")
17308 }
17309 }
17310 }
17311 })
17312 .detach();
17313
17314 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17315 let unresolved_item_1 = unresolved_item_1.clone();
17316 let unresolved_item_2 = unresolved_item_2.clone();
17317 async move {
17318 Ok(Some(lsp::CompletionResponse::Array(vec![
17319 unresolved_item_1,
17320 unresolved_item_2,
17321 ])))
17322 }
17323 })
17324 .next()
17325 .await;
17326
17327 cx.condition(|editor, _| editor.context_menu_visible())
17328 .await;
17329 cx.update_editor(|editor, _, _| {
17330 let context_menu = editor.context_menu.borrow_mut();
17331 let context_menu = context_menu
17332 .as_ref()
17333 .expect("Should have the context menu deployed");
17334 match context_menu {
17335 CodeContextMenu::Completions(completions_menu) => {
17336 let completions = completions_menu.completions.borrow_mut();
17337 assert_eq!(
17338 completions
17339 .iter()
17340 .map(|completion| &completion.label.text)
17341 .collect::<Vec<_>>(),
17342 vec!["id", "other"]
17343 )
17344 }
17345 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17346 }
17347 });
17348 cx.run_until_parked();
17349
17350 cx.update_editor(|editor, window, cx| {
17351 editor.context_menu_next(&ContextMenuNext, window, cx);
17352 });
17353 cx.run_until_parked();
17354 cx.update_editor(|editor, window, cx| {
17355 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17356 });
17357 cx.run_until_parked();
17358 cx.update_editor(|editor, window, cx| {
17359 editor.context_menu_next(&ContextMenuNext, window, cx);
17360 });
17361 cx.run_until_parked();
17362 cx.update_editor(|editor, window, cx| {
17363 editor
17364 .compose_completion(&ComposeCompletion::default(), window, cx)
17365 .expect("No task returned")
17366 })
17367 .await
17368 .expect("Completion failed");
17369 cx.run_until_parked();
17370
17371 cx.update_editor(|editor, _, cx| {
17372 assert_eq!(
17373 resolve_requests_1.load(atomic::Ordering::Acquire),
17374 1,
17375 "Should always resolve once despite multiple selections"
17376 );
17377 assert_eq!(
17378 resolve_requests_2.load(atomic::Ordering::Acquire),
17379 1,
17380 "Should always resolve once after multiple selections and applying the completion"
17381 );
17382 assert_eq!(
17383 editor.text(cx),
17384 "fn main() { let a = ??.other; }",
17385 "Should use resolved data when applying the completion"
17386 );
17387 });
17388}
17389
17390#[gpui::test]
17391async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17392 init_test(cx, |_| {});
17393
17394 let item_0 = lsp::CompletionItem {
17395 label: "abs".into(),
17396 insert_text: Some("abs".into()),
17397 data: Some(json!({ "very": "special"})),
17398 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17399 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17400 lsp::InsertReplaceEdit {
17401 new_text: "abs".to_string(),
17402 insert: lsp::Range::default(),
17403 replace: lsp::Range::default(),
17404 },
17405 )),
17406 ..lsp::CompletionItem::default()
17407 };
17408 let items = iter::once(item_0.clone())
17409 .chain((11..51).map(|i| lsp::CompletionItem {
17410 label: format!("item_{}", i),
17411 insert_text: Some(format!("item_{}", i)),
17412 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17413 ..lsp::CompletionItem::default()
17414 }))
17415 .collect::<Vec<_>>();
17416
17417 let default_commit_characters = vec!["?".to_string()];
17418 let default_data = json!({ "default": "data"});
17419 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17420 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17421 let default_edit_range = lsp::Range {
17422 start: lsp::Position {
17423 line: 0,
17424 character: 5,
17425 },
17426 end: lsp::Position {
17427 line: 0,
17428 character: 5,
17429 },
17430 };
17431
17432 let mut cx = EditorLspTestContext::new_rust(
17433 lsp::ServerCapabilities {
17434 completion_provider: Some(lsp::CompletionOptions {
17435 trigger_characters: Some(vec![".".to_string()]),
17436 resolve_provider: Some(true),
17437 ..Default::default()
17438 }),
17439 ..Default::default()
17440 },
17441 cx,
17442 )
17443 .await;
17444
17445 cx.set_state("fn main() { let a = 2ˇ; }");
17446 cx.simulate_keystroke(".");
17447
17448 let completion_data = default_data.clone();
17449 let completion_characters = default_commit_characters.clone();
17450 let completion_items = items.clone();
17451 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17452 let default_data = completion_data.clone();
17453 let default_commit_characters = completion_characters.clone();
17454 let items = completion_items.clone();
17455 async move {
17456 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17457 items,
17458 item_defaults: Some(lsp::CompletionListItemDefaults {
17459 data: Some(default_data.clone()),
17460 commit_characters: Some(default_commit_characters.clone()),
17461 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17462 default_edit_range,
17463 )),
17464 insert_text_format: Some(default_insert_text_format),
17465 insert_text_mode: Some(default_insert_text_mode),
17466 }),
17467 ..lsp::CompletionList::default()
17468 })))
17469 }
17470 })
17471 .next()
17472 .await;
17473
17474 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17475 cx.lsp
17476 .server
17477 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17478 let closure_resolved_items = resolved_items.clone();
17479 move |item_to_resolve, _| {
17480 let closure_resolved_items = closure_resolved_items.clone();
17481 async move {
17482 closure_resolved_items.lock().push(item_to_resolve.clone());
17483 Ok(item_to_resolve)
17484 }
17485 }
17486 })
17487 .detach();
17488
17489 cx.condition(|editor, _| editor.context_menu_visible())
17490 .await;
17491 cx.run_until_parked();
17492 cx.update_editor(|editor, _, _| {
17493 let menu = editor.context_menu.borrow_mut();
17494 match menu.as_ref().expect("should have the completions menu") {
17495 CodeContextMenu::Completions(completions_menu) => {
17496 assert_eq!(
17497 completions_menu
17498 .entries
17499 .borrow()
17500 .iter()
17501 .map(|mat| mat.string.clone())
17502 .collect::<Vec<String>>(),
17503 items
17504 .iter()
17505 .map(|completion| completion.label.clone())
17506 .collect::<Vec<String>>()
17507 );
17508 }
17509 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17510 }
17511 });
17512 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17513 // with 4 from the end.
17514 assert_eq!(
17515 *resolved_items.lock(),
17516 [&items[0..16], &items[items.len() - 4..items.len()]]
17517 .concat()
17518 .iter()
17519 .cloned()
17520 .map(|mut item| {
17521 if item.data.is_none() {
17522 item.data = Some(default_data.clone());
17523 }
17524 item
17525 })
17526 .collect::<Vec<lsp::CompletionItem>>(),
17527 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17528 );
17529 resolved_items.lock().clear();
17530
17531 cx.update_editor(|editor, window, cx| {
17532 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17533 });
17534 cx.run_until_parked();
17535 // Completions that have already been resolved are skipped.
17536 assert_eq!(
17537 *resolved_items.lock(),
17538 items[items.len() - 17..items.len() - 4]
17539 .iter()
17540 .cloned()
17541 .map(|mut item| {
17542 if item.data.is_none() {
17543 item.data = Some(default_data.clone());
17544 }
17545 item
17546 })
17547 .collect::<Vec<lsp::CompletionItem>>()
17548 );
17549 resolved_items.lock().clear();
17550}
17551
17552#[gpui::test]
17553async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17554 init_test(cx, |_| {});
17555
17556 let mut cx = EditorLspTestContext::new(
17557 Language::new(
17558 LanguageConfig {
17559 matcher: LanguageMatcher {
17560 path_suffixes: vec!["jsx".into()],
17561 ..Default::default()
17562 },
17563 overrides: [(
17564 "element".into(),
17565 LanguageConfigOverride {
17566 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17567 ..Default::default()
17568 },
17569 )]
17570 .into_iter()
17571 .collect(),
17572 ..Default::default()
17573 },
17574 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17575 )
17576 .with_override_query("(jsx_self_closing_element) @element")
17577 .unwrap(),
17578 lsp::ServerCapabilities {
17579 completion_provider: Some(lsp::CompletionOptions {
17580 trigger_characters: Some(vec![":".to_string()]),
17581 ..Default::default()
17582 }),
17583 ..Default::default()
17584 },
17585 cx,
17586 )
17587 .await;
17588
17589 cx.lsp
17590 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17591 Ok(Some(lsp::CompletionResponse::Array(vec![
17592 lsp::CompletionItem {
17593 label: "bg-blue".into(),
17594 ..Default::default()
17595 },
17596 lsp::CompletionItem {
17597 label: "bg-red".into(),
17598 ..Default::default()
17599 },
17600 lsp::CompletionItem {
17601 label: "bg-yellow".into(),
17602 ..Default::default()
17603 },
17604 ])))
17605 });
17606
17607 cx.set_state(r#"<p class="bgˇ" />"#);
17608
17609 // Trigger completion when typing a dash, because the dash is an extra
17610 // word character in the 'element' scope, which contains the cursor.
17611 cx.simulate_keystroke("-");
17612 cx.executor().run_until_parked();
17613 cx.update_editor(|editor, _, _| {
17614 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17615 {
17616 assert_eq!(
17617 completion_menu_entries(menu),
17618 &["bg-blue", "bg-red", "bg-yellow"]
17619 );
17620 } else {
17621 panic!("expected completion menu to be open");
17622 }
17623 });
17624
17625 cx.simulate_keystroke("l");
17626 cx.executor().run_until_parked();
17627 cx.update_editor(|editor, _, _| {
17628 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17629 {
17630 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17631 } else {
17632 panic!("expected completion menu to be open");
17633 }
17634 });
17635
17636 // When filtering completions, consider the character after the '-' to
17637 // be the start of a subword.
17638 cx.set_state(r#"<p class="yelˇ" />"#);
17639 cx.simulate_keystroke("l");
17640 cx.executor().run_until_parked();
17641 cx.update_editor(|editor, _, _| {
17642 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17643 {
17644 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17645 } else {
17646 panic!("expected completion menu to be open");
17647 }
17648 });
17649}
17650
17651fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17652 let entries = menu.entries.borrow();
17653 entries.iter().map(|mat| mat.string.clone()).collect()
17654}
17655
17656#[gpui::test]
17657async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17658 init_test(cx, |settings| {
17659 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17660 Formatter::Prettier,
17661 )))
17662 });
17663
17664 let fs = FakeFs::new(cx.executor());
17665 fs.insert_file(path!("/file.ts"), Default::default()).await;
17666
17667 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17668 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17669
17670 language_registry.add(Arc::new(Language::new(
17671 LanguageConfig {
17672 name: "TypeScript".into(),
17673 matcher: LanguageMatcher {
17674 path_suffixes: vec!["ts".to_string()],
17675 ..Default::default()
17676 },
17677 ..Default::default()
17678 },
17679 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17680 )));
17681 update_test_language_settings(cx, |settings| {
17682 settings.defaults.prettier = Some(PrettierSettings {
17683 allowed: true,
17684 ..PrettierSettings::default()
17685 });
17686 });
17687
17688 let test_plugin = "test_plugin";
17689 let _ = language_registry.register_fake_lsp(
17690 "TypeScript",
17691 FakeLspAdapter {
17692 prettier_plugins: vec![test_plugin],
17693 ..Default::default()
17694 },
17695 );
17696
17697 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17698 let buffer = project
17699 .update(cx, |project, cx| {
17700 project.open_local_buffer(path!("/file.ts"), cx)
17701 })
17702 .await
17703 .unwrap();
17704
17705 let buffer_text = "one\ntwo\nthree\n";
17706 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17707 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17708 editor.update_in(cx, |editor, window, cx| {
17709 editor.set_text(buffer_text, window, cx)
17710 });
17711
17712 editor
17713 .update_in(cx, |editor, window, cx| {
17714 editor.perform_format(
17715 project.clone(),
17716 FormatTrigger::Manual,
17717 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17718 window,
17719 cx,
17720 )
17721 })
17722 .unwrap()
17723 .await;
17724 assert_eq!(
17725 editor.update(cx, |editor, cx| editor.text(cx)),
17726 buffer_text.to_string() + prettier_format_suffix,
17727 "Test prettier formatting was not applied to the original buffer text",
17728 );
17729
17730 update_test_language_settings(cx, |settings| {
17731 settings.defaults.formatter = Some(SelectedFormatter::Auto)
17732 });
17733 let format = editor.update_in(cx, |editor, window, cx| {
17734 editor.perform_format(
17735 project.clone(),
17736 FormatTrigger::Manual,
17737 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17738 window,
17739 cx,
17740 )
17741 });
17742 format.await.unwrap();
17743 assert_eq!(
17744 editor.update(cx, |editor, cx| editor.text(cx)),
17745 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
17746 "Autoformatting (via test prettier) was not applied to the original buffer text",
17747 );
17748}
17749
17750#[gpui::test]
17751async fn test_addition_reverts(cx: &mut TestAppContext) {
17752 init_test(cx, |_| {});
17753 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17754 let base_text = indoc! {r#"
17755 struct Row;
17756 struct Row1;
17757 struct Row2;
17758
17759 struct Row4;
17760 struct Row5;
17761 struct Row6;
17762
17763 struct Row8;
17764 struct Row9;
17765 struct Row10;"#};
17766
17767 // When addition hunks are not adjacent to carets, no hunk revert is performed
17768 assert_hunk_revert(
17769 indoc! {r#"struct Row;
17770 struct Row1;
17771 struct Row1.1;
17772 struct Row1.2;
17773 struct Row2;ˇ
17774
17775 struct Row4;
17776 struct Row5;
17777 struct Row6;
17778
17779 struct Row8;
17780 ˇstruct Row9;
17781 struct Row9.1;
17782 struct Row9.2;
17783 struct Row9.3;
17784 struct Row10;"#},
17785 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17786 indoc! {r#"struct Row;
17787 struct Row1;
17788 struct Row1.1;
17789 struct Row1.2;
17790 struct Row2;ˇ
17791
17792 struct Row4;
17793 struct Row5;
17794 struct Row6;
17795
17796 struct Row8;
17797 ˇstruct Row9;
17798 struct Row9.1;
17799 struct Row9.2;
17800 struct Row9.3;
17801 struct Row10;"#},
17802 base_text,
17803 &mut cx,
17804 );
17805 // Same for selections
17806 assert_hunk_revert(
17807 indoc! {r#"struct Row;
17808 struct Row1;
17809 struct Row2;
17810 struct Row2.1;
17811 struct Row2.2;
17812 «ˇ
17813 struct Row4;
17814 struct» Row5;
17815 «struct Row6;
17816 ˇ»
17817 struct Row9.1;
17818 struct Row9.2;
17819 struct Row9.3;
17820 struct Row8;
17821 struct Row9;
17822 struct Row10;"#},
17823 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17824 indoc! {r#"struct Row;
17825 struct Row1;
17826 struct Row2;
17827 struct Row2.1;
17828 struct Row2.2;
17829 «ˇ
17830 struct Row4;
17831 struct» Row5;
17832 «struct Row6;
17833 ˇ»
17834 struct Row9.1;
17835 struct Row9.2;
17836 struct Row9.3;
17837 struct Row8;
17838 struct Row9;
17839 struct Row10;"#},
17840 base_text,
17841 &mut cx,
17842 );
17843
17844 // When carets and selections intersect the addition hunks, those are reverted.
17845 // Adjacent carets got merged.
17846 assert_hunk_revert(
17847 indoc! {r#"struct Row;
17848 ˇ// something on the top
17849 struct Row1;
17850 struct Row2;
17851 struct Roˇw3.1;
17852 struct Row2.2;
17853 struct Row2.3;ˇ
17854
17855 struct Row4;
17856 struct ˇRow5.1;
17857 struct Row5.2;
17858 struct «Rowˇ»5.3;
17859 struct Row5;
17860 struct Row6;
17861 ˇ
17862 struct Row9.1;
17863 struct «Rowˇ»9.2;
17864 struct «ˇRow»9.3;
17865 struct Row8;
17866 struct Row9;
17867 «ˇ// something on bottom»
17868 struct Row10;"#},
17869 vec![
17870 DiffHunkStatusKind::Added,
17871 DiffHunkStatusKind::Added,
17872 DiffHunkStatusKind::Added,
17873 DiffHunkStatusKind::Added,
17874 DiffHunkStatusKind::Added,
17875 ],
17876 indoc! {r#"struct Row;
17877 ˇstruct Row1;
17878 struct Row2;
17879 ˇ
17880 struct Row4;
17881 ˇstruct Row5;
17882 struct Row6;
17883 ˇ
17884 ˇstruct Row8;
17885 struct Row9;
17886 ˇstruct Row10;"#},
17887 base_text,
17888 &mut cx,
17889 );
17890}
17891
17892#[gpui::test]
17893async fn test_modification_reverts(cx: &mut TestAppContext) {
17894 init_test(cx, |_| {});
17895 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17896 let base_text = indoc! {r#"
17897 struct Row;
17898 struct Row1;
17899 struct Row2;
17900
17901 struct Row4;
17902 struct Row5;
17903 struct Row6;
17904
17905 struct Row8;
17906 struct Row9;
17907 struct Row10;"#};
17908
17909 // Modification hunks behave the same as the addition ones.
17910 assert_hunk_revert(
17911 indoc! {r#"struct Row;
17912 struct Row1;
17913 struct Row33;
17914 ˇ
17915 struct Row4;
17916 struct Row5;
17917 struct Row6;
17918 ˇ
17919 struct Row99;
17920 struct Row9;
17921 struct Row10;"#},
17922 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17923 indoc! {r#"struct Row;
17924 struct Row1;
17925 struct Row33;
17926 ˇ
17927 struct Row4;
17928 struct Row5;
17929 struct Row6;
17930 ˇ
17931 struct Row99;
17932 struct Row9;
17933 struct Row10;"#},
17934 base_text,
17935 &mut cx,
17936 );
17937 assert_hunk_revert(
17938 indoc! {r#"struct Row;
17939 struct Row1;
17940 struct Row33;
17941 «ˇ
17942 struct Row4;
17943 struct» Row5;
17944 «struct Row6;
17945 ˇ»
17946 struct Row99;
17947 struct Row9;
17948 struct Row10;"#},
17949 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17950 indoc! {r#"struct Row;
17951 struct Row1;
17952 struct Row33;
17953 «ˇ
17954 struct Row4;
17955 struct» Row5;
17956 «struct Row6;
17957 ˇ»
17958 struct Row99;
17959 struct Row9;
17960 struct Row10;"#},
17961 base_text,
17962 &mut cx,
17963 );
17964
17965 assert_hunk_revert(
17966 indoc! {r#"ˇstruct Row1.1;
17967 struct Row1;
17968 «ˇstr»uct Row22;
17969
17970 struct ˇRow44;
17971 struct Row5;
17972 struct «Rˇ»ow66;ˇ
17973
17974 «struˇ»ct Row88;
17975 struct Row9;
17976 struct Row1011;ˇ"#},
17977 vec![
17978 DiffHunkStatusKind::Modified,
17979 DiffHunkStatusKind::Modified,
17980 DiffHunkStatusKind::Modified,
17981 DiffHunkStatusKind::Modified,
17982 DiffHunkStatusKind::Modified,
17983 DiffHunkStatusKind::Modified,
17984 ],
17985 indoc! {r#"struct Row;
17986 ˇstruct Row1;
17987 struct Row2;
17988 ˇ
17989 struct Row4;
17990 ˇstruct Row5;
17991 struct Row6;
17992 ˇ
17993 struct Row8;
17994 ˇstruct Row9;
17995 struct Row10;ˇ"#},
17996 base_text,
17997 &mut cx,
17998 );
17999}
18000
18001#[gpui::test]
18002async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18003 init_test(cx, |_| {});
18004 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18005 let base_text = indoc! {r#"
18006 one
18007
18008 two
18009 three
18010 "#};
18011
18012 cx.set_head_text(base_text);
18013 cx.set_state("\nˇ\n");
18014 cx.executor().run_until_parked();
18015 cx.update_editor(|editor, _window, cx| {
18016 editor.expand_selected_diff_hunks(cx);
18017 });
18018 cx.executor().run_until_parked();
18019 cx.update_editor(|editor, window, cx| {
18020 editor.backspace(&Default::default(), window, cx);
18021 });
18022 cx.run_until_parked();
18023 cx.assert_state_with_diff(
18024 indoc! {r#"
18025
18026 - two
18027 - threeˇ
18028 +
18029 "#}
18030 .to_string(),
18031 );
18032}
18033
18034#[gpui::test]
18035async fn test_deletion_reverts(cx: &mut TestAppContext) {
18036 init_test(cx, |_| {});
18037 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18038 let base_text = indoc! {r#"struct Row;
18039struct Row1;
18040struct Row2;
18041
18042struct Row4;
18043struct Row5;
18044struct Row6;
18045
18046struct Row8;
18047struct Row9;
18048struct Row10;"#};
18049
18050 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18051 assert_hunk_revert(
18052 indoc! {r#"struct Row;
18053 struct Row2;
18054
18055 ˇstruct Row4;
18056 struct Row5;
18057 struct Row6;
18058 ˇ
18059 struct Row8;
18060 struct Row10;"#},
18061 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18062 indoc! {r#"struct Row;
18063 struct Row2;
18064
18065 ˇstruct Row4;
18066 struct Row5;
18067 struct Row6;
18068 ˇ
18069 struct Row8;
18070 struct Row10;"#},
18071 base_text,
18072 &mut cx,
18073 );
18074 assert_hunk_revert(
18075 indoc! {r#"struct Row;
18076 struct Row2;
18077
18078 «ˇstruct Row4;
18079 struct» Row5;
18080 «struct Row6;
18081 ˇ»
18082 struct Row8;
18083 struct Row10;"#},
18084 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18085 indoc! {r#"struct Row;
18086 struct Row2;
18087
18088 «ˇstruct Row4;
18089 struct» Row5;
18090 «struct Row6;
18091 ˇ»
18092 struct Row8;
18093 struct Row10;"#},
18094 base_text,
18095 &mut cx,
18096 );
18097
18098 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18099 assert_hunk_revert(
18100 indoc! {r#"struct Row;
18101 ˇstruct Row2;
18102
18103 struct Row4;
18104 struct Row5;
18105 struct Row6;
18106
18107 struct Row8;ˇ
18108 struct Row10;"#},
18109 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18110 indoc! {r#"struct Row;
18111 struct Row1;
18112 ˇstruct Row2;
18113
18114 struct Row4;
18115 struct Row5;
18116 struct Row6;
18117
18118 struct Row8;ˇ
18119 struct Row9;
18120 struct Row10;"#},
18121 base_text,
18122 &mut cx,
18123 );
18124 assert_hunk_revert(
18125 indoc! {r#"struct Row;
18126 struct Row2«ˇ;
18127 struct Row4;
18128 struct» Row5;
18129 «struct Row6;
18130
18131 struct Row8;ˇ»
18132 struct Row10;"#},
18133 vec![
18134 DiffHunkStatusKind::Deleted,
18135 DiffHunkStatusKind::Deleted,
18136 DiffHunkStatusKind::Deleted,
18137 ],
18138 indoc! {r#"struct Row;
18139 struct Row1;
18140 struct Row2«ˇ;
18141
18142 struct Row4;
18143 struct» Row5;
18144 «struct Row6;
18145
18146 struct Row8;ˇ»
18147 struct Row9;
18148 struct Row10;"#},
18149 base_text,
18150 &mut cx,
18151 );
18152}
18153
18154#[gpui::test]
18155async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18156 init_test(cx, |_| {});
18157
18158 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18159 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18160 let base_text_3 =
18161 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18162
18163 let text_1 = edit_first_char_of_every_line(base_text_1);
18164 let text_2 = edit_first_char_of_every_line(base_text_2);
18165 let text_3 = edit_first_char_of_every_line(base_text_3);
18166
18167 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18168 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18169 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18170
18171 let multibuffer = cx.new(|cx| {
18172 let mut multibuffer = MultiBuffer::new(ReadWrite);
18173 multibuffer.push_excerpts(
18174 buffer_1.clone(),
18175 [
18176 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18177 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18178 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18179 ],
18180 cx,
18181 );
18182 multibuffer.push_excerpts(
18183 buffer_2.clone(),
18184 [
18185 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18186 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18187 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18188 ],
18189 cx,
18190 );
18191 multibuffer.push_excerpts(
18192 buffer_3.clone(),
18193 [
18194 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18195 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18196 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18197 ],
18198 cx,
18199 );
18200 multibuffer
18201 });
18202
18203 let fs = FakeFs::new(cx.executor());
18204 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18205 let (editor, cx) = cx
18206 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18207 editor.update_in(cx, |editor, _window, cx| {
18208 for (buffer, diff_base) in [
18209 (buffer_1.clone(), base_text_1),
18210 (buffer_2.clone(), base_text_2),
18211 (buffer_3.clone(), base_text_3),
18212 ] {
18213 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18214 editor
18215 .buffer
18216 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18217 }
18218 });
18219 cx.executor().run_until_parked();
18220
18221 editor.update_in(cx, |editor, window, cx| {
18222 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}");
18223 editor.select_all(&SelectAll, window, cx);
18224 editor.git_restore(&Default::default(), window, cx);
18225 });
18226 cx.executor().run_until_parked();
18227
18228 // When all ranges are selected, all buffer hunks are reverted.
18229 editor.update(cx, |editor, cx| {
18230 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");
18231 });
18232 buffer_1.update(cx, |buffer, _| {
18233 assert_eq!(buffer.text(), base_text_1);
18234 });
18235 buffer_2.update(cx, |buffer, _| {
18236 assert_eq!(buffer.text(), base_text_2);
18237 });
18238 buffer_3.update(cx, |buffer, _| {
18239 assert_eq!(buffer.text(), base_text_3);
18240 });
18241
18242 editor.update_in(cx, |editor, window, cx| {
18243 editor.undo(&Default::default(), window, cx);
18244 });
18245
18246 editor.update_in(cx, |editor, window, cx| {
18247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18248 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18249 });
18250 editor.git_restore(&Default::default(), window, cx);
18251 });
18252
18253 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18254 // but not affect buffer_2 and its related excerpts.
18255 editor.update(cx, |editor, cx| {
18256 assert_eq!(
18257 editor.text(cx),
18258 "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}"
18259 );
18260 });
18261 buffer_1.update(cx, |buffer, _| {
18262 assert_eq!(buffer.text(), base_text_1);
18263 });
18264 buffer_2.update(cx, |buffer, _| {
18265 assert_eq!(
18266 buffer.text(),
18267 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18268 );
18269 });
18270 buffer_3.update(cx, |buffer, _| {
18271 assert_eq!(
18272 buffer.text(),
18273 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18274 );
18275 });
18276
18277 fn edit_first_char_of_every_line(text: &str) -> String {
18278 text.split('\n')
18279 .map(|line| format!("X{}", &line[1..]))
18280 .collect::<Vec<_>>()
18281 .join("\n")
18282 }
18283}
18284
18285#[gpui::test]
18286async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18287 init_test(cx, |_| {});
18288
18289 let cols = 4;
18290 let rows = 10;
18291 let sample_text_1 = sample_text(rows, cols, 'a');
18292 assert_eq!(
18293 sample_text_1,
18294 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18295 );
18296 let sample_text_2 = sample_text(rows, cols, 'l');
18297 assert_eq!(
18298 sample_text_2,
18299 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18300 );
18301 let sample_text_3 = sample_text(rows, cols, 'v');
18302 assert_eq!(
18303 sample_text_3,
18304 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18305 );
18306
18307 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18308 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18309 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18310
18311 let multi_buffer = cx.new(|cx| {
18312 let mut multibuffer = MultiBuffer::new(ReadWrite);
18313 multibuffer.push_excerpts(
18314 buffer_1.clone(),
18315 [
18316 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18317 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18318 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18319 ],
18320 cx,
18321 );
18322 multibuffer.push_excerpts(
18323 buffer_2.clone(),
18324 [
18325 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18326 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18327 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18328 ],
18329 cx,
18330 );
18331 multibuffer.push_excerpts(
18332 buffer_3.clone(),
18333 [
18334 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18335 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18336 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18337 ],
18338 cx,
18339 );
18340 multibuffer
18341 });
18342
18343 let fs = FakeFs::new(cx.executor());
18344 fs.insert_tree(
18345 "/a",
18346 json!({
18347 "main.rs": sample_text_1,
18348 "other.rs": sample_text_2,
18349 "lib.rs": sample_text_3,
18350 }),
18351 )
18352 .await;
18353 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18354 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18355 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18356 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18357 Editor::new(
18358 EditorMode::full(),
18359 multi_buffer,
18360 Some(project.clone()),
18361 window,
18362 cx,
18363 )
18364 });
18365 let multibuffer_item_id = workspace
18366 .update(cx, |workspace, window, cx| {
18367 assert!(
18368 workspace.active_item(cx).is_none(),
18369 "active item should be None before the first item is added"
18370 );
18371 workspace.add_item_to_active_pane(
18372 Box::new(multi_buffer_editor.clone()),
18373 None,
18374 true,
18375 window,
18376 cx,
18377 );
18378 let active_item = workspace
18379 .active_item(cx)
18380 .expect("should have an active item after adding the multi buffer");
18381 assert!(
18382 !active_item.is_singleton(cx),
18383 "A multi buffer was expected to active after adding"
18384 );
18385 active_item.item_id()
18386 })
18387 .unwrap();
18388 cx.executor().run_until_parked();
18389
18390 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18391 editor.change_selections(
18392 SelectionEffects::scroll(Autoscroll::Next),
18393 window,
18394 cx,
18395 |s| s.select_ranges(Some(1..2)),
18396 );
18397 editor.open_excerpts(&OpenExcerpts, window, cx);
18398 });
18399 cx.executor().run_until_parked();
18400 let first_item_id = workspace
18401 .update(cx, |workspace, window, cx| {
18402 let active_item = workspace
18403 .active_item(cx)
18404 .expect("should have an active item after navigating into the 1st buffer");
18405 let first_item_id = active_item.item_id();
18406 assert_ne!(
18407 first_item_id, multibuffer_item_id,
18408 "Should navigate into the 1st buffer and activate it"
18409 );
18410 assert!(
18411 active_item.is_singleton(cx),
18412 "New active item should be a singleton buffer"
18413 );
18414 assert_eq!(
18415 active_item
18416 .act_as::<Editor>(cx)
18417 .expect("should have navigated into an editor for the 1st buffer")
18418 .read(cx)
18419 .text(cx),
18420 sample_text_1
18421 );
18422
18423 workspace
18424 .go_back(workspace.active_pane().downgrade(), window, cx)
18425 .detach_and_log_err(cx);
18426
18427 first_item_id
18428 })
18429 .unwrap();
18430 cx.executor().run_until_parked();
18431 workspace
18432 .update(cx, |workspace, _, cx| {
18433 let active_item = workspace
18434 .active_item(cx)
18435 .expect("should have an active item after navigating back");
18436 assert_eq!(
18437 active_item.item_id(),
18438 multibuffer_item_id,
18439 "Should navigate back to the multi buffer"
18440 );
18441 assert!(!active_item.is_singleton(cx));
18442 })
18443 .unwrap();
18444
18445 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18446 editor.change_selections(
18447 SelectionEffects::scroll(Autoscroll::Next),
18448 window,
18449 cx,
18450 |s| s.select_ranges(Some(39..40)),
18451 );
18452 editor.open_excerpts(&OpenExcerpts, window, cx);
18453 });
18454 cx.executor().run_until_parked();
18455 let second_item_id = workspace
18456 .update(cx, |workspace, window, cx| {
18457 let active_item = workspace
18458 .active_item(cx)
18459 .expect("should have an active item after navigating into the 2nd buffer");
18460 let second_item_id = active_item.item_id();
18461 assert_ne!(
18462 second_item_id, multibuffer_item_id,
18463 "Should navigate away from the multibuffer"
18464 );
18465 assert_ne!(
18466 second_item_id, first_item_id,
18467 "Should navigate into the 2nd buffer and activate it"
18468 );
18469 assert!(
18470 active_item.is_singleton(cx),
18471 "New active item should be a singleton buffer"
18472 );
18473 assert_eq!(
18474 active_item
18475 .act_as::<Editor>(cx)
18476 .expect("should have navigated into an editor")
18477 .read(cx)
18478 .text(cx),
18479 sample_text_2
18480 );
18481
18482 workspace
18483 .go_back(workspace.active_pane().downgrade(), window, cx)
18484 .detach_and_log_err(cx);
18485
18486 second_item_id
18487 })
18488 .unwrap();
18489 cx.executor().run_until_parked();
18490 workspace
18491 .update(cx, |workspace, _, cx| {
18492 let active_item = workspace
18493 .active_item(cx)
18494 .expect("should have an active item after navigating back from the 2nd buffer");
18495 assert_eq!(
18496 active_item.item_id(),
18497 multibuffer_item_id,
18498 "Should navigate back from the 2nd buffer to the multi buffer"
18499 );
18500 assert!(!active_item.is_singleton(cx));
18501 })
18502 .unwrap();
18503
18504 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18505 editor.change_selections(
18506 SelectionEffects::scroll(Autoscroll::Next),
18507 window,
18508 cx,
18509 |s| s.select_ranges(Some(70..70)),
18510 );
18511 editor.open_excerpts(&OpenExcerpts, window, cx);
18512 });
18513 cx.executor().run_until_parked();
18514 workspace
18515 .update(cx, |workspace, window, cx| {
18516 let active_item = workspace
18517 .active_item(cx)
18518 .expect("should have an active item after navigating into the 3rd buffer");
18519 let third_item_id = active_item.item_id();
18520 assert_ne!(
18521 third_item_id, multibuffer_item_id,
18522 "Should navigate into the 3rd buffer and activate it"
18523 );
18524 assert_ne!(third_item_id, first_item_id);
18525 assert_ne!(third_item_id, second_item_id);
18526 assert!(
18527 active_item.is_singleton(cx),
18528 "New active item should be a singleton buffer"
18529 );
18530 assert_eq!(
18531 active_item
18532 .act_as::<Editor>(cx)
18533 .expect("should have navigated into an editor")
18534 .read(cx)
18535 .text(cx),
18536 sample_text_3
18537 );
18538
18539 workspace
18540 .go_back(workspace.active_pane().downgrade(), window, cx)
18541 .detach_and_log_err(cx);
18542 })
18543 .unwrap();
18544 cx.executor().run_until_parked();
18545 workspace
18546 .update(cx, |workspace, _, cx| {
18547 let active_item = workspace
18548 .active_item(cx)
18549 .expect("should have an active item after navigating back from the 3rd buffer");
18550 assert_eq!(
18551 active_item.item_id(),
18552 multibuffer_item_id,
18553 "Should navigate back from the 3rd buffer to the multi buffer"
18554 );
18555 assert!(!active_item.is_singleton(cx));
18556 })
18557 .unwrap();
18558}
18559
18560#[gpui::test]
18561async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18562 init_test(cx, |_| {});
18563
18564 let mut cx = EditorTestContext::new(cx).await;
18565
18566 let diff_base = r#"
18567 use some::mod;
18568
18569 const A: u32 = 42;
18570
18571 fn main() {
18572 println!("hello");
18573
18574 println!("world");
18575 }
18576 "#
18577 .unindent();
18578
18579 cx.set_state(
18580 &r#"
18581 use some::modified;
18582
18583 ˇ
18584 fn main() {
18585 println!("hello there");
18586
18587 println!("around the");
18588 println!("world");
18589 }
18590 "#
18591 .unindent(),
18592 );
18593
18594 cx.set_head_text(&diff_base);
18595 executor.run_until_parked();
18596
18597 cx.update_editor(|editor, window, cx| {
18598 editor.go_to_next_hunk(&GoToHunk, window, cx);
18599 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18600 });
18601 executor.run_until_parked();
18602 cx.assert_state_with_diff(
18603 r#"
18604 use some::modified;
18605
18606
18607 fn main() {
18608 - println!("hello");
18609 + ˇ println!("hello there");
18610
18611 println!("around the");
18612 println!("world");
18613 }
18614 "#
18615 .unindent(),
18616 );
18617
18618 cx.update_editor(|editor, window, cx| {
18619 for _ in 0..2 {
18620 editor.go_to_next_hunk(&GoToHunk, window, cx);
18621 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18622 }
18623 });
18624 executor.run_until_parked();
18625 cx.assert_state_with_diff(
18626 r#"
18627 - use some::mod;
18628 + ˇuse some::modified;
18629
18630
18631 fn main() {
18632 - println!("hello");
18633 + println!("hello there");
18634
18635 + println!("around the");
18636 println!("world");
18637 }
18638 "#
18639 .unindent(),
18640 );
18641
18642 cx.update_editor(|editor, window, cx| {
18643 editor.go_to_next_hunk(&GoToHunk, window, cx);
18644 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18645 });
18646 executor.run_until_parked();
18647 cx.assert_state_with_diff(
18648 r#"
18649 - use some::mod;
18650 + use some::modified;
18651
18652 - const A: u32 = 42;
18653 ˇ
18654 fn main() {
18655 - println!("hello");
18656 + println!("hello there");
18657
18658 + println!("around the");
18659 println!("world");
18660 }
18661 "#
18662 .unindent(),
18663 );
18664
18665 cx.update_editor(|editor, window, cx| {
18666 editor.cancel(&Cancel, window, cx);
18667 });
18668
18669 cx.assert_state_with_diff(
18670 r#"
18671 use some::modified;
18672
18673 ˇ
18674 fn main() {
18675 println!("hello there");
18676
18677 println!("around the");
18678 println!("world");
18679 }
18680 "#
18681 .unindent(),
18682 );
18683}
18684
18685#[gpui::test]
18686async fn test_diff_base_change_with_expanded_diff_hunks(
18687 executor: BackgroundExecutor,
18688 cx: &mut TestAppContext,
18689) {
18690 init_test(cx, |_| {});
18691
18692 let mut cx = EditorTestContext::new(cx).await;
18693
18694 let diff_base = r#"
18695 use some::mod1;
18696 use some::mod2;
18697
18698 const A: u32 = 42;
18699 const B: u32 = 42;
18700 const C: u32 = 42;
18701
18702 fn main() {
18703 println!("hello");
18704
18705 println!("world");
18706 }
18707 "#
18708 .unindent();
18709
18710 cx.set_state(
18711 &r#"
18712 use some::mod2;
18713
18714 const A: u32 = 42;
18715 const C: u32 = 42;
18716
18717 fn main(ˇ) {
18718 //println!("hello");
18719
18720 println!("world");
18721 //
18722 //
18723 }
18724 "#
18725 .unindent(),
18726 );
18727
18728 cx.set_head_text(&diff_base);
18729 executor.run_until_parked();
18730
18731 cx.update_editor(|editor, window, cx| {
18732 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18733 });
18734 executor.run_until_parked();
18735 cx.assert_state_with_diff(
18736 r#"
18737 - use some::mod1;
18738 use some::mod2;
18739
18740 const A: u32 = 42;
18741 - const B: u32 = 42;
18742 const C: u32 = 42;
18743
18744 fn main(ˇ) {
18745 - println!("hello");
18746 + //println!("hello");
18747
18748 println!("world");
18749 + //
18750 + //
18751 }
18752 "#
18753 .unindent(),
18754 );
18755
18756 cx.set_head_text("new diff base!");
18757 executor.run_until_parked();
18758 cx.assert_state_with_diff(
18759 r#"
18760 - new diff base!
18761 + use some::mod2;
18762 +
18763 + const A: u32 = 42;
18764 + const C: u32 = 42;
18765 +
18766 + fn main(ˇ) {
18767 + //println!("hello");
18768 +
18769 + println!("world");
18770 + //
18771 + //
18772 + }
18773 "#
18774 .unindent(),
18775 );
18776}
18777
18778#[gpui::test]
18779async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
18780 init_test(cx, |_| {});
18781
18782 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18783 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18784 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18785 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18786 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
18787 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
18788
18789 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
18790 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
18791 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
18792
18793 let multi_buffer = cx.new(|cx| {
18794 let mut multibuffer = MultiBuffer::new(ReadWrite);
18795 multibuffer.push_excerpts(
18796 buffer_1.clone(),
18797 [
18798 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18799 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18800 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18801 ],
18802 cx,
18803 );
18804 multibuffer.push_excerpts(
18805 buffer_2.clone(),
18806 [
18807 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18808 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18809 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18810 ],
18811 cx,
18812 );
18813 multibuffer.push_excerpts(
18814 buffer_3.clone(),
18815 [
18816 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18817 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18818 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18819 ],
18820 cx,
18821 );
18822 multibuffer
18823 });
18824
18825 let editor =
18826 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18827 editor
18828 .update(cx, |editor, _window, cx| {
18829 for (buffer, diff_base) in [
18830 (buffer_1.clone(), file_1_old),
18831 (buffer_2.clone(), file_2_old),
18832 (buffer_3.clone(), file_3_old),
18833 ] {
18834 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18835 editor
18836 .buffer
18837 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18838 }
18839 })
18840 .unwrap();
18841
18842 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18843 cx.run_until_parked();
18844
18845 cx.assert_editor_state(
18846 &"
18847 ˇaaa
18848 ccc
18849 ddd
18850
18851 ggg
18852 hhh
18853
18854
18855 lll
18856 mmm
18857 NNN
18858
18859 qqq
18860 rrr
18861
18862 uuu
18863 111
18864 222
18865 333
18866
18867 666
18868 777
18869
18870 000
18871 !!!"
18872 .unindent(),
18873 );
18874
18875 cx.update_editor(|editor, window, cx| {
18876 editor.select_all(&SelectAll, window, cx);
18877 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18878 });
18879 cx.executor().run_until_parked();
18880
18881 cx.assert_state_with_diff(
18882 "
18883 «aaa
18884 - bbb
18885 ccc
18886 ddd
18887
18888 ggg
18889 hhh
18890
18891
18892 lll
18893 mmm
18894 - nnn
18895 + NNN
18896
18897 qqq
18898 rrr
18899
18900 uuu
18901 111
18902 222
18903 333
18904
18905 + 666
18906 777
18907
18908 000
18909 !!!ˇ»"
18910 .unindent(),
18911 );
18912}
18913
18914#[gpui::test]
18915async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18916 init_test(cx, |_| {});
18917
18918 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
18919 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
18920
18921 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18922 let multi_buffer = cx.new(|cx| {
18923 let mut multibuffer = MultiBuffer::new(ReadWrite);
18924 multibuffer.push_excerpts(
18925 buffer.clone(),
18926 [
18927 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18928 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18929 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18930 ],
18931 cx,
18932 );
18933 multibuffer
18934 });
18935
18936 let editor =
18937 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18938 editor
18939 .update(cx, |editor, _window, cx| {
18940 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18941 editor
18942 .buffer
18943 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18944 })
18945 .unwrap();
18946
18947 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18948 cx.run_until_parked();
18949
18950 cx.update_editor(|editor, window, cx| {
18951 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18952 });
18953 cx.executor().run_until_parked();
18954
18955 // When the start of a hunk coincides with the start of its excerpt,
18956 // the hunk is expanded. When the start of a a hunk is earlier than
18957 // the start of its excerpt, the hunk is not expanded.
18958 cx.assert_state_with_diff(
18959 "
18960 ˇaaa
18961 - bbb
18962 + BBB
18963
18964 - ddd
18965 - eee
18966 + DDD
18967 + EEE
18968 fff
18969
18970 iii
18971 "
18972 .unindent(),
18973 );
18974}
18975
18976#[gpui::test]
18977async fn test_edits_around_expanded_insertion_hunks(
18978 executor: BackgroundExecutor,
18979 cx: &mut TestAppContext,
18980) {
18981 init_test(cx, |_| {});
18982
18983 let mut cx = EditorTestContext::new(cx).await;
18984
18985 let diff_base = r#"
18986 use some::mod1;
18987 use some::mod2;
18988
18989 const A: u32 = 42;
18990
18991 fn main() {
18992 println!("hello");
18993
18994 println!("world");
18995 }
18996 "#
18997 .unindent();
18998 executor.run_until_parked();
18999 cx.set_state(
19000 &r#"
19001 use some::mod1;
19002 use some::mod2;
19003
19004 const A: u32 = 42;
19005 const B: u32 = 42;
19006 const C: u32 = 42;
19007 ˇ
19008
19009 fn main() {
19010 println!("hello");
19011
19012 println!("world");
19013 }
19014 "#
19015 .unindent(),
19016 );
19017
19018 cx.set_head_text(&diff_base);
19019 executor.run_until_parked();
19020
19021 cx.update_editor(|editor, window, cx| {
19022 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19023 });
19024 executor.run_until_parked();
19025
19026 cx.assert_state_with_diff(
19027 r#"
19028 use some::mod1;
19029 use some::mod2;
19030
19031 const A: u32 = 42;
19032 + const B: u32 = 42;
19033 + const C: u32 = 42;
19034 + ˇ
19035
19036 fn main() {
19037 println!("hello");
19038
19039 println!("world");
19040 }
19041 "#
19042 .unindent(),
19043 );
19044
19045 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19046 executor.run_until_parked();
19047
19048 cx.assert_state_with_diff(
19049 r#"
19050 use some::mod1;
19051 use some::mod2;
19052
19053 const A: u32 = 42;
19054 + const B: u32 = 42;
19055 + const C: u32 = 42;
19056 + const D: u32 = 42;
19057 + ˇ
19058
19059 fn main() {
19060 println!("hello");
19061
19062 println!("world");
19063 }
19064 "#
19065 .unindent(),
19066 );
19067
19068 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19069 executor.run_until_parked();
19070
19071 cx.assert_state_with_diff(
19072 r#"
19073 use some::mod1;
19074 use some::mod2;
19075
19076 const A: u32 = 42;
19077 + const B: u32 = 42;
19078 + const C: u32 = 42;
19079 + const D: u32 = 42;
19080 + const E: u32 = 42;
19081 + ˇ
19082
19083 fn main() {
19084 println!("hello");
19085
19086 println!("world");
19087 }
19088 "#
19089 .unindent(),
19090 );
19091
19092 cx.update_editor(|editor, window, cx| {
19093 editor.delete_line(&DeleteLine, window, cx);
19094 });
19095 executor.run_until_parked();
19096
19097 cx.assert_state_with_diff(
19098 r#"
19099 use some::mod1;
19100 use some::mod2;
19101
19102 const A: u32 = 42;
19103 + const B: u32 = 42;
19104 + const C: u32 = 42;
19105 + const D: u32 = 42;
19106 + const E: u32 = 42;
19107 ˇ
19108 fn main() {
19109 println!("hello");
19110
19111 println!("world");
19112 }
19113 "#
19114 .unindent(),
19115 );
19116
19117 cx.update_editor(|editor, window, cx| {
19118 editor.move_up(&MoveUp, window, cx);
19119 editor.delete_line(&DeleteLine, window, cx);
19120 editor.move_up(&MoveUp, window, cx);
19121 editor.delete_line(&DeleteLine, window, cx);
19122 editor.move_up(&MoveUp, window, cx);
19123 editor.delete_line(&DeleteLine, window, cx);
19124 });
19125 executor.run_until_parked();
19126 cx.assert_state_with_diff(
19127 r#"
19128 use some::mod1;
19129 use some::mod2;
19130
19131 const A: u32 = 42;
19132 + const B: u32 = 42;
19133 ˇ
19134 fn main() {
19135 println!("hello");
19136
19137 println!("world");
19138 }
19139 "#
19140 .unindent(),
19141 );
19142
19143 cx.update_editor(|editor, window, cx| {
19144 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19145 editor.delete_line(&DeleteLine, window, cx);
19146 });
19147 executor.run_until_parked();
19148 cx.assert_state_with_diff(
19149 r#"
19150 ˇ
19151 fn main() {
19152 println!("hello");
19153
19154 println!("world");
19155 }
19156 "#
19157 .unindent(),
19158 );
19159}
19160
19161#[gpui::test]
19162async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19163 init_test(cx, |_| {});
19164
19165 let mut cx = EditorTestContext::new(cx).await;
19166 cx.set_head_text(indoc! { "
19167 one
19168 two
19169 three
19170 four
19171 five
19172 "
19173 });
19174 cx.set_state(indoc! { "
19175 one
19176 ˇthree
19177 five
19178 "});
19179 cx.run_until_parked();
19180 cx.update_editor(|editor, window, cx| {
19181 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19182 });
19183 cx.assert_state_with_diff(
19184 indoc! { "
19185 one
19186 - two
19187 ˇthree
19188 - four
19189 five
19190 "}
19191 .to_string(),
19192 );
19193 cx.update_editor(|editor, window, cx| {
19194 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19195 });
19196
19197 cx.assert_state_with_diff(
19198 indoc! { "
19199 one
19200 ˇthree
19201 five
19202 "}
19203 .to_string(),
19204 );
19205
19206 cx.set_state(indoc! { "
19207 one
19208 ˇTWO
19209 three
19210 four
19211 five
19212 "});
19213 cx.run_until_parked();
19214 cx.update_editor(|editor, window, cx| {
19215 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19216 });
19217
19218 cx.assert_state_with_diff(
19219 indoc! { "
19220 one
19221 - two
19222 + ˇTWO
19223 three
19224 four
19225 five
19226 "}
19227 .to_string(),
19228 );
19229 cx.update_editor(|editor, window, cx| {
19230 editor.move_up(&Default::default(), window, cx);
19231 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19232 });
19233 cx.assert_state_with_diff(
19234 indoc! { "
19235 one
19236 ˇTWO
19237 three
19238 four
19239 five
19240 "}
19241 .to_string(),
19242 );
19243}
19244
19245#[gpui::test]
19246async fn test_edits_around_expanded_deletion_hunks(
19247 executor: BackgroundExecutor,
19248 cx: &mut TestAppContext,
19249) {
19250 init_test(cx, |_| {});
19251
19252 let mut cx = EditorTestContext::new(cx).await;
19253
19254 let diff_base = r#"
19255 use some::mod1;
19256 use some::mod2;
19257
19258 const A: u32 = 42;
19259 const B: u32 = 42;
19260 const C: u32 = 42;
19261
19262
19263 fn main() {
19264 println!("hello");
19265
19266 println!("world");
19267 }
19268 "#
19269 .unindent();
19270 executor.run_until_parked();
19271 cx.set_state(
19272 &r#"
19273 use some::mod1;
19274 use some::mod2;
19275
19276 ˇconst B: u32 = 42;
19277 const C: u32 = 42;
19278
19279
19280 fn main() {
19281 println!("hello");
19282
19283 println!("world");
19284 }
19285 "#
19286 .unindent(),
19287 );
19288
19289 cx.set_head_text(&diff_base);
19290 executor.run_until_parked();
19291
19292 cx.update_editor(|editor, window, cx| {
19293 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19294 });
19295 executor.run_until_parked();
19296
19297 cx.assert_state_with_diff(
19298 r#"
19299 use some::mod1;
19300 use some::mod2;
19301
19302 - const A: u32 = 42;
19303 ˇconst B: u32 = 42;
19304 const C: u32 = 42;
19305
19306
19307 fn main() {
19308 println!("hello");
19309
19310 println!("world");
19311 }
19312 "#
19313 .unindent(),
19314 );
19315
19316 cx.update_editor(|editor, window, cx| {
19317 editor.delete_line(&DeleteLine, window, cx);
19318 });
19319 executor.run_until_parked();
19320 cx.assert_state_with_diff(
19321 r#"
19322 use some::mod1;
19323 use some::mod2;
19324
19325 - const A: u32 = 42;
19326 - const B: u32 = 42;
19327 ˇconst C: u32 = 42;
19328
19329
19330 fn main() {
19331 println!("hello");
19332
19333 println!("world");
19334 }
19335 "#
19336 .unindent(),
19337 );
19338
19339 cx.update_editor(|editor, window, cx| {
19340 editor.delete_line(&DeleteLine, window, cx);
19341 });
19342 executor.run_until_parked();
19343 cx.assert_state_with_diff(
19344 r#"
19345 use some::mod1;
19346 use some::mod2;
19347
19348 - const A: u32 = 42;
19349 - const B: u32 = 42;
19350 - const C: u32 = 42;
19351 ˇ
19352
19353 fn main() {
19354 println!("hello");
19355
19356 println!("world");
19357 }
19358 "#
19359 .unindent(),
19360 );
19361
19362 cx.update_editor(|editor, window, cx| {
19363 editor.handle_input("replacement", window, cx);
19364 });
19365 executor.run_until_parked();
19366 cx.assert_state_with_diff(
19367 r#"
19368 use some::mod1;
19369 use some::mod2;
19370
19371 - const A: u32 = 42;
19372 - const B: u32 = 42;
19373 - const C: u32 = 42;
19374 -
19375 + replacementˇ
19376
19377 fn main() {
19378 println!("hello");
19379
19380 println!("world");
19381 }
19382 "#
19383 .unindent(),
19384 );
19385}
19386
19387#[gpui::test]
19388async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19389 init_test(cx, |_| {});
19390
19391 let mut cx = EditorTestContext::new(cx).await;
19392
19393 let base_text = r#"
19394 one
19395 two
19396 three
19397 four
19398 five
19399 "#
19400 .unindent();
19401 executor.run_until_parked();
19402 cx.set_state(
19403 &r#"
19404 one
19405 two
19406 fˇour
19407 five
19408 "#
19409 .unindent(),
19410 );
19411
19412 cx.set_head_text(&base_text);
19413 executor.run_until_parked();
19414
19415 cx.update_editor(|editor, window, cx| {
19416 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19417 });
19418 executor.run_until_parked();
19419
19420 cx.assert_state_with_diff(
19421 r#"
19422 one
19423 two
19424 - three
19425 fˇour
19426 five
19427 "#
19428 .unindent(),
19429 );
19430
19431 cx.update_editor(|editor, window, cx| {
19432 editor.backspace(&Backspace, window, cx);
19433 editor.backspace(&Backspace, window, cx);
19434 });
19435 executor.run_until_parked();
19436 cx.assert_state_with_diff(
19437 r#"
19438 one
19439 two
19440 - threeˇ
19441 - four
19442 + our
19443 five
19444 "#
19445 .unindent(),
19446 );
19447}
19448
19449#[gpui::test]
19450async fn test_edit_after_expanded_modification_hunk(
19451 executor: BackgroundExecutor,
19452 cx: &mut TestAppContext,
19453) {
19454 init_test(cx, |_| {});
19455
19456 let mut cx = EditorTestContext::new(cx).await;
19457
19458 let diff_base = r#"
19459 use some::mod1;
19460 use some::mod2;
19461
19462 const A: u32 = 42;
19463 const B: u32 = 42;
19464 const C: u32 = 42;
19465 const D: u32 = 42;
19466
19467
19468 fn main() {
19469 println!("hello");
19470
19471 println!("world");
19472 }"#
19473 .unindent();
19474
19475 cx.set_state(
19476 &r#"
19477 use some::mod1;
19478 use some::mod2;
19479
19480 const A: u32 = 42;
19481 const B: u32 = 42;
19482 const C: u32 = 43ˇ
19483 const D: u32 = 42;
19484
19485
19486 fn main() {
19487 println!("hello");
19488
19489 println!("world");
19490 }"#
19491 .unindent(),
19492 );
19493
19494 cx.set_head_text(&diff_base);
19495 executor.run_until_parked();
19496 cx.update_editor(|editor, window, cx| {
19497 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19498 });
19499 executor.run_until_parked();
19500
19501 cx.assert_state_with_diff(
19502 r#"
19503 use some::mod1;
19504 use some::mod2;
19505
19506 const A: u32 = 42;
19507 const B: u32 = 42;
19508 - const C: u32 = 42;
19509 + const C: u32 = 43ˇ
19510 const D: u32 = 42;
19511
19512
19513 fn main() {
19514 println!("hello");
19515
19516 println!("world");
19517 }"#
19518 .unindent(),
19519 );
19520
19521 cx.update_editor(|editor, window, cx| {
19522 editor.handle_input("\nnew_line\n", window, cx);
19523 });
19524 executor.run_until_parked();
19525
19526 cx.assert_state_with_diff(
19527 r#"
19528 use some::mod1;
19529 use some::mod2;
19530
19531 const A: u32 = 42;
19532 const B: u32 = 42;
19533 - const C: u32 = 42;
19534 + const C: u32 = 43
19535 + new_line
19536 + ˇ
19537 const D: u32 = 42;
19538
19539
19540 fn main() {
19541 println!("hello");
19542
19543 println!("world");
19544 }"#
19545 .unindent(),
19546 );
19547}
19548
19549#[gpui::test]
19550async fn test_stage_and_unstage_added_file_hunk(
19551 executor: BackgroundExecutor,
19552 cx: &mut TestAppContext,
19553) {
19554 init_test(cx, |_| {});
19555
19556 let mut cx = EditorTestContext::new(cx).await;
19557 cx.update_editor(|editor, _, cx| {
19558 editor.set_expand_all_diff_hunks(cx);
19559 });
19560
19561 let working_copy = r#"
19562 ˇfn main() {
19563 println!("hello, world!");
19564 }
19565 "#
19566 .unindent();
19567
19568 cx.set_state(&working_copy);
19569 executor.run_until_parked();
19570
19571 cx.assert_state_with_diff(
19572 r#"
19573 + ˇfn main() {
19574 + println!("hello, world!");
19575 + }
19576 "#
19577 .unindent(),
19578 );
19579 cx.assert_index_text(None);
19580
19581 cx.update_editor(|editor, window, cx| {
19582 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19583 });
19584 executor.run_until_parked();
19585 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19586 cx.assert_state_with_diff(
19587 r#"
19588 + ˇfn main() {
19589 + println!("hello, world!");
19590 + }
19591 "#
19592 .unindent(),
19593 );
19594
19595 cx.update_editor(|editor, window, cx| {
19596 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19597 });
19598 executor.run_until_parked();
19599 cx.assert_index_text(None);
19600}
19601
19602async fn setup_indent_guides_editor(
19603 text: &str,
19604 cx: &mut TestAppContext,
19605) -> (BufferId, EditorTestContext) {
19606 init_test(cx, |_| {});
19607
19608 let mut cx = EditorTestContext::new(cx).await;
19609
19610 let buffer_id = cx.update_editor(|editor, window, cx| {
19611 editor.set_text(text, window, cx);
19612 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19613
19614 buffer_ids[0]
19615 });
19616
19617 (buffer_id, cx)
19618}
19619
19620fn assert_indent_guides(
19621 range: Range<u32>,
19622 expected: Vec<IndentGuide>,
19623 active_indices: Option<Vec<usize>>,
19624 cx: &mut EditorTestContext,
19625) {
19626 let indent_guides = cx.update_editor(|editor, window, cx| {
19627 let snapshot = editor.snapshot(window, cx).display_snapshot;
19628 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19629 editor,
19630 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19631 true,
19632 &snapshot,
19633 cx,
19634 );
19635
19636 indent_guides.sort_by(|a, b| {
19637 a.depth.cmp(&b.depth).then(
19638 a.start_row
19639 .cmp(&b.start_row)
19640 .then(a.end_row.cmp(&b.end_row)),
19641 )
19642 });
19643 indent_guides
19644 });
19645
19646 if let Some(expected) = active_indices {
19647 let active_indices = cx.update_editor(|editor, window, cx| {
19648 let snapshot = editor.snapshot(window, cx).display_snapshot;
19649 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19650 });
19651
19652 assert_eq!(
19653 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19654 expected,
19655 "Active indent guide indices do not match"
19656 );
19657 }
19658
19659 assert_eq!(indent_guides, expected, "Indent guides do not match");
19660}
19661
19662fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19663 IndentGuide {
19664 buffer_id,
19665 start_row: MultiBufferRow(start_row),
19666 end_row: MultiBufferRow(end_row),
19667 depth,
19668 tab_size: 4,
19669 settings: IndentGuideSettings {
19670 enabled: true,
19671 line_width: 1,
19672 active_line_width: 1,
19673 ..Default::default()
19674 },
19675 }
19676}
19677
19678#[gpui::test]
19679async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19680 let (buffer_id, mut cx) = setup_indent_guides_editor(
19681 &"
19682 fn main() {
19683 let a = 1;
19684 }"
19685 .unindent(),
19686 cx,
19687 )
19688 .await;
19689
19690 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19691}
19692
19693#[gpui::test]
19694async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19695 let (buffer_id, mut cx) = setup_indent_guides_editor(
19696 &"
19697 fn main() {
19698 let a = 1;
19699 let b = 2;
19700 }"
19701 .unindent(),
19702 cx,
19703 )
19704 .await;
19705
19706 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
19707}
19708
19709#[gpui::test]
19710async fn test_indent_guide_nested(cx: &mut TestAppContext) {
19711 let (buffer_id, mut cx) = setup_indent_guides_editor(
19712 &"
19713 fn main() {
19714 let a = 1;
19715 if a == 3 {
19716 let b = 2;
19717 } else {
19718 let c = 3;
19719 }
19720 }"
19721 .unindent(),
19722 cx,
19723 )
19724 .await;
19725
19726 assert_indent_guides(
19727 0..8,
19728 vec![
19729 indent_guide(buffer_id, 1, 6, 0),
19730 indent_guide(buffer_id, 3, 3, 1),
19731 indent_guide(buffer_id, 5, 5, 1),
19732 ],
19733 None,
19734 &mut cx,
19735 );
19736}
19737
19738#[gpui::test]
19739async fn test_indent_guide_tab(cx: &mut TestAppContext) {
19740 let (buffer_id, mut cx) = setup_indent_guides_editor(
19741 &"
19742 fn main() {
19743 let a = 1;
19744 let b = 2;
19745 let c = 3;
19746 }"
19747 .unindent(),
19748 cx,
19749 )
19750 .await;
19751
19752 assert_indent_guides(
19753 0..5,
19754 vec![
19755 indent_guide(buffer_id, 1, 3, 0),
19756 indent_guide(buffer_id, 2, 2, 1),
19757 ],
19758 None,
19759 &mut cx,
19760 );
19761}
19762
19763#[gpui::test]
19764async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
19765 let (buffer_id, mut cx) = setup_indent_guides_editor(
19766 &"
19767 fn main() {
19768 let a = 1;
19769
19770 let c = 3;
19771 }"
19772 .unindent(),
19773 cx,
19774 )
19775 .await;
19776
19777 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
19778}
19779
19780#[gpui::test]
19781async fn test_indent_guide_complex(cx: &mut TestAppContext) {
19782 let (buffer_id, mut cx) = setup_indent_guides_editor(
19783 &"
19784 fn main() {
19785 let a = 1;
19786
19787 let c = 3;
19788
19789 if a == 3 {
19790 let b = 2;
19791 } else {
19792 let c = 3;
19793 }
19794 }"
19795 .unindent(),
19796 cx,
19797 )
19798 .await;
19799
19800 assert_indent_guides(
19801 0..11,
19802 vec![
19803 indent_guide(buffer_id, 1, 9, 0),
19804 indent_guide(buffer_id, 6, 6, 1),
19805 indent_guide(buffer_id, 8, 8, 1),
19806 ],
19807 None,
19808 &mut cx,
19809 );
19810}
19811
19812#[gpui::test]
19813async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
19814 let (buffer_id, mut cx) = setup_indent_guides_editor(
19815 &"
19816 fn main() {
19817 let a = 1;
19818
19819 let c = 3;
19820
19821 if a == 3 {
19822 let b = 2;
19823 } else {
19824 let c = 3;
19825 }
19826 }"
19827 .unindent(),
19828 cx,
19829 )
19830 .await;
19831
19832 assert_indent_guides(
19833 1..11,
19834 vec![
19835 indent_guide(buffer_id, 1, 9, 0),
19836 indent_guide(buffer_id, 6, 6, 1),
19837 indent_guide(buffer_id, 8, 8, 1),
19838 ],
19839 None,
19840 &mut cx,
19841 );
19842}
19843
19844#[gpui::test]
19845async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
19846 let (buffer_id, mut cx) = setup_indent_guides_editor(
19847 &"
19848 fn main() {
19849 let a = 1;
19850
19851 let c = 3;
19852
19853 if a == 3 {
19854 let b = 2;
19855 } else {
19856 let c = 3;
19857 }
19858 }"
19859 .unindent(),
19860 cx,
19861 )
19862 .await;
19863
19864 assert_indent_guides(
19865 1..10,
19866 vec![
19867 indent_guide(buffer_id, 1, 9, 0),
19868 indent_guide(buffer_id, 6, 6, 1),
19869 indent_guide(buffer_id, 8, 8, 1),
19870 ],
19871 None,
19872 &mut cx,
19873 );
19874}
19875
19876#[gpui::test]
19877async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
19878 let (buffer_id, mut cx) = setup_indent_guides_editor(
19879 &"
19880 fn main() {
19881 if a {
19882 b(
19883 c,
19884 d,
19885 )
19886 } else {
19887 e(
19888 f
19889 )
19890 }
19891 }"
19892 .unindent(),
19893 cx,
19894 )
19895 .await;
19896
19897 assert_indent_guides(
19898 0..11,
19899 vec![
19900 indent_guide(buffer_id, 1, 10, 0),
19901 indent_guide(buffer_id, 2, 5, 1),
19902 indent_guide(buffer_id, 7, 9, 1),
19903 indent_guide(buffer_id, 3, 4, 2),
19904 indent_guide(buffer_id, 8, 8, 2),
19905 ],
19906 None,
19907 &mut cx,
19908 );
19909
19910 cx.update_editor(|editor, window, cx| {
19911 editor.fold_at(MultiBufferRow(2), window, cx);
19912 assert_eq!(
19913 editor.display_text(cx),
19914 "
19915 fn main() {
19916 if a {
19917 b(⋯
19918 )
19919 } else {
19920 e(
19921 f
19922 )
19923 }
19924 }"
19925 .unindent()
19926 );
19927 });
19928
19929 assert_indent_guides(
19930 0..11,
19931 vec![
19932 indent_guide(buffer_id, 1, 10, 0),
19933 indent_guide(buffer_id, 2, 5, 1),
19934 indent_guide(buffer_id, 7, 9, 1),
19935 indent_guide(buffer_id, 8, 8, 2),
19936 ],
19937 None,
19938 &mut cx,
19939 );
19940}
19941
19942#[gpui::test]
19943async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19944 let (buffer_id, mut cx) = setup_indent_guides_editor(
19945 &"
19946 block1
19947 block2
19948 block3
19949 block4
19950 block2
19951 block1
19952 block1"
19953 .unindent(),
19954 cx,
19955 )
19956 .await;
19957
19958 assert_indent_guides(
19959 1..10,
19960 vec![
19961 indent_guide(buffer_id, 1, 4, 0),
19962 indent_guide(buffer_id, 2, 3, 1),
19963 indent_guide(buffer_id, 3, 3, 2),
19964 ],
19965 None,
19966 &mut cx,
19967 );
19968}
19969
19970#[gpui::test]
19971async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19972 let (buffer_id, mut cx) = setup_indent_guides_editor(
19973 &"
19974 block1
19975 block2
19976 block3
19977
19978 block1
19979 block1"
19980 .unindent(),
19981 cx,
19982 )
19983 .await;
19984
19985 assert_indent_guides(
19986 0..6,
19987 vec![
19988 indent_guide(buffer_id, 1, 2, 0),
19989 indent_guide(buffer_id, 2, 2, 1),
19990 ],
19991 None,
19992 &mut cx,
19993 );
19994}
19995
19996#[gpui::test]
19997async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
19998 let (buffer_id, mut cx) = setup_indent_guides_editor(
19999 &"
20000 function component() {
20001 \treturn (
20002 \t\t\t
20003 \t\t<div>
20004 \t\t\t<abc></abc>
20005 \t\t</div>
20006 \t)
20007 }"
20008 .unindent(),
20009 cx,
20010 )
20011 .await;
20012
20013 assert_indent_guides(
20014 0..8,
20015 vec![
20016 indent_guide(buffer_id, 1, 6, 0),
20017 indent_guide(buffer_id, 2, 5, 1),
20018 indent_guide(buffer_id, 4, 4, 2),
20019 ],
20020 None,
20021 &mut cx,
20022 );
20023}
20024
20025#[gpui::test]
20026async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20027 let (buffer_id, mut cx) = setup_indent_guides_editor(
20028 &"
20029 function component() {
20030 \treturn (
20031 \t
20032 \t\t<div>
20033 \t\t\t<abc></abc>
20034 \t\t</div>
20035 \t)
20036 }"
20037 .unindent(),
20038 cx,
20039 )
20040 .await;
20041
20042 assert_indent_guides(
20043 0..8,
20044 vec![
20045 indent_guide(buffer_id, 1, 6, 0),
20046 indent_guide(buffer_id, 2, 5, 1),
20047 indent_guide(buffer_id, 4, 4, 2),
20048 ],
20049 None,
20050 &mut cx,
20051 );
20052}
20053
20054#[gpui::test]
20055async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20056 let (buffer_id, mut cx) = setup_indent_guides_editor(
20057 &"
20058 block1
20059
20060
20061
20062 block2
20063 "
20064 .unindent(),
20065 cx,
20066 )
20067 .await;
20068
20069 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20070}
20071
20072#[gpui::test]
20073async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20074 let (buffer_id, mut cx) = setup_indent_guides_editor(
20075 &"
20076 def a:
20077 \tb = 3
20078 \tif True:
20079 \t\tc = 4
20080 \t\td = 5
20081 \tprint(b)
20082 "
20083 .unindent(),
20084 cx,
20085 )
20086 .await;
20087
20088 assert_indent_guides(
20089 0..6,
20090 vec![
20091 indent_guide(buffer_id, 1, 5, 0),
20092 indent_guide(buffer_id, 3, 4, 1),
20093 ],
20094 None,
20095 &mut cx,
20096 );
20097}
20098
20099#[gpui::test]
20100async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20101 let (buffer_id, mut cx) = setup_indent_guides_editor(
20102 &"
20103 fn main() {
20104 let a = 1;
20105 }"
20106 .unindent(),
20107 cx,
20108 )
20109 .await;
20110
20111 cx.update_editor(|editor, window, cx| {
20112 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20113 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20114 });
20115 });
20116
20117 assert_indent_guides(
20118 0..3,
20119 vec![indent_guide(buffer_id, 1, 1, 0)],
20120 Some(vec![0]),
20121 &mut cx,
20122 );
20123}
20124
20125#[gpui::test]
20126async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20127 let (buffer_id, mut cx) = setup_indent_guides_editor(
20128 &"
20129 fn main() {
20130 if 1 == 2 {
20131 let a = 1;
20132 }
20133 }"
20134 .unindent(),
20135 cx,
20136 )
20137 .await;
20138
20139 cx.update_editor(|editor, window, cx| {
20140 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20141 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20142 });
20143 });
20144
20145 assert_indent_guides(
20146 0..4,
20147 vec![
20148 indent_guide(buffer_id, 1, 3, 0),
20149 indent_guide(buffer_id, 2, 2, 1),
20150 ],
20151 Some(vec![1]),
20152 &mut cx,
20153 );
20154
20155 cx.update_editor(|editor, window, cx| {
20156 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20157 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20158 });
20159 });
20160
20161 assert_indent_guides(
20162 0..4,
20163 vec![
20164 indent_guide(buffer_id, 1, 3, 0),
20165 indent_guide(buffer_id, 2, 2, 1),
20166 ],
20167 Some(vec![1]),
20168 &mut cx,
20169 );
20170
20171 cx.update_editor(|editor, window, cx| {
20172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20173 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20174 });
20175 });
20176
20177 assert_indent_guides(
20178 0..4,
20179 vec![
20180 indent_guide(buffer_id, 1, 3, 0),
20181 indent_guide(buffer_id, 2, 2, 1),
20182 ],
20183 Some(vec![0]),
20184 &mut cx,
20185 );
20186}
20187
20188#[gpui::test]
20189async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20190 let (buffer_id, mut cx) = setup_indent_guides_editor(
20191 &"
20192 fn main() {
20193 let a = 1;
20194
20195 let b = 2;
20196 }"
20197 .unindent(),
20198 cx,
20199 )
20200 .await;
20201
20202 cx.update_editor(|editor, window, cx| {
20203 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20204 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20205 });
20206 });
20207
20208 assert_indent_guides(
20209 0..5,
20210 vec![indent_guide(buffer_id, 1, 3, 0)],
20211 Some(vec![0]),
20212 &mut cx,
20213 );
20214}
20215
20216#[gpui::test]
20217async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20218 let (buffer_id, mut cx) = setup_indent_guides_editor(
20219 &"
20220 def m:
20221 a = 1
20222 pass"
20223 .unindent(),
20224 cx,
20225 )
20226 .await;
20227
20228 cx.update_editor(|editor, window, cx| {
20229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20230 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20231 });
20232 });
20233
20234 assert_indent_guides(
20235 0..3,
20236 vec![indent_guide(buffer_id, 1, 2, 0)],
20237 Some(vec![0]),
20238 &mut cx,
20239 );
20240}
20241
20242#[gpui::test]
20243async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20244 init_test(cx, |_| {});
20245 let mut cx = EditorTestContext::new(cx).await;
20246 let text = indoc! {
20247 "
20248 impl A {
20249 fn b() {
20250 0;
20251 3;
20252 5;
20253 6;
20254 7;
20255 }
20256 }
20257 "
20258 };
20259 let base_text = indoc! {
20260 "
20261 impl A {
20262 fn b() {
20263 0;
20264 1;
20265 2;
20266 3;
20267 4;
20268 }
20269 fn c() {
20270 5;
20271 6;
20272 7;
20273 }
20274 }
20275 "
20276 };
20277
20278 cx.update_editor(|editor, window, cx| {
20279 editor.set_text(text, window, cx);
20280
20281 editor.buffer().update(cx, |multibuffer, cx| {
20282 let buffer = multibuffer.as_singleton().unwrap();
20283 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20284
20285 multibuffer.set_all_diff_hunks_expanded(cx);
20286 multibuffer.add_diff(diff, cx);
20287
20288 buffer.read(cx).remote_id()
20289 })
20290 });
20291 cx.run_until_parked();
20292
20293 cx.assert_state_with_diff(
20294 indoc! { "
20295 impl A {
20296 fn b() {
20297 0;
20298 - 1;
20299 - 2;
20300 3;
20301 - 4;
20302 - }
20303 - fn c() {
20304 5;
20305 6;
20306 7;
20307 }
20308 }
20309 ˇ"
20310 }
20311 .to_string(),
20312 );
20313
20314 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20315 editor
20316 .snapshot(window, cx)
20317 .buffer_snapshot
20318 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20319 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20320 .collect::<Vec<_>>()
20321 });
20322 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20323 assert_eq!(
20324 actual_guides,
20325 vec![
20326 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20327 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20328 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20329 ]
20330 );
20331}
20332
20333#[gpui::test]
20334async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20335 init_test(cx, |_| {});
20336 let mut cx = EditorTestContext::new(cx).await;
20337
20338 let diff_base = r#"
20339 a
20340 b
20341 c
20342 "#
20343 .unindent();
20344
20345 cx.set_state(
20346 &r#"
20347 ˇA
20348 b
20349 C
20350 "#
20351 .unindent(),
20352 );
20353 cx.set_head_text(&diff_base);
20354 cx.update_editor(|editor, window, cx| {
20355 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20356 });
20357 executor.run_until_parked();
20358
20359 let both_hunks_expanded = r#"
20360 - a
20361 + ˇA
20362 b
20363 - c
20364 + C
20365 "#
20366 .unindent();
20367
20368 cx.assert_state_with_diff(both_hunks_expanded.clone());
20369
20370 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20371 let snapshot = editor.snapshot(window, cx);
20372 let hunks = editor
20373 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20374 .collect::<Vec<_>>();
20375 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20376 let buffer_id = hunks[0].buffer_id;
20377 hunks
20378 .into_iter()
20379 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20380 .collect::<Vec<_>>()
20381 });
20382 assert_eq!(hunk_ranges.len(), 2);
20383
20384 cx.update_editor(|editor, _, cx| {
20385 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20386 });
20387 executor.run_until_parked();
20388
20389 let second_hunk_expanded = r#"
20390 ˇA
20391 b
20392 - c
20393 + C
20394 "#
20395 .unindent();
20396
20397 cx.assert_state_with_diff(second_hunk_expanded);
20398
20399 cx.update_editor(|editor, _, cx| {
20400 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20401 });
20402 executor.run_until_parked();
20403
20404 cx.assert_state_with_diff(both_hunks_expanded.clone());
20405
20406 cx.update_editor(|editor, _, cx| {
20407 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20408 });
20409 executor.run_until_parked();
20410
20411 let first_hunk_expanded = r#"
20412 - a
20413 + ˇA
20414 b
20415 C
20416 "#
20417 .unindent();
20418
20419 cx.assert_state_with_diff(first_hunk_expanded);
20420
20421 cx.update_editor(|editor, _, cx| {
20422 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20423 });
20424 executor.run_until_parked();
20425
20426 cx.assert_state_with_diff(both_hunks_expanded);
20427
20428 cx.set_state(
20429 &r#"
20430 ˇA
20431 b
20432 "#
20433 .unindent(),
20434 );
20435 cx.run_until_parked();
20436
20437 // TODO this cursor position seems bad
20438 cx.assert_state_with_diff(
20439 r#"
20440 - ˇa
20441 + A
20442 b
20443 "#
20444 .unindent(),
20445 );
20446
20447 cx.update_editor(|editor, window, cx| {
20448 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20449 });
20450
20451 cx.assert_state_with_diff(
20452 r#"
20453 - ˇa
20454 + A
20455 b
20456 - c
20457 "#
20458 .unindent(),
20459 );
20460
20461 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20462 let snapshot = editor.snapshot(window, cx);
20463 let hunks = editor
20464 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20465 .collect::<Vec<_>>();
20466 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20467 let buffer_id = hunks[0].buffer_id;
20468 hunks
20469 .into_iter()
20470 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20471 .collect::<Vec<_>>()
20472 });
20473 assert_eq!(hunk_ranges.len(), 2);
20474
20475 cx.update_editor(|editor, _, cx| {
20476 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20477 });
20478 executor.run_until_parked();
20479
20480 cx.assert_state_with_diff(
20481 r#"
20482 - ˇa
20483 + A
20484 b
20485 "#
20486 .unindent(),
20487 );
20488}
20489
20490#[gpui::test]
20491async fn test_toggle_deletion_hunk_at_start_of_file(
20492 executor: BackgroundExecutor,
20493 cx: &mut TestAppContext,
20494) {
20495 init_test(cx, |_| {});
20496 let mut cx = EditorTestContext::new(cx).await;
20497
20498 let diff_base = r#"
20499 a
20500 b
20501 c
20502 "#
20503 .unindent();
20504
20505 cx.set_state(
20506 &r#"
20507 ˇb
20508 c
20509 "#
20510 .unindent(),
20511 );
20512 cx.set_head_text(&diff_base);
20513 cx.update_editor(|editor, window, cx| {
20514 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20515 });
20516 executor.run_until_parked();
20517
20518 let hunk_expanded = r#"
20519 - a
20520 ˇb
20521 c
20522 "#
20523 .unindent();
20524
20525 cx.assert_state_with_diff(hunk_expanded.clone());
20526
20527 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20528 let snapshot = editor.snapshot(window, cx);
20529 let hunks = editor
20530 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20531 .collect::<Vec<_>>();
20532 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20533 let buffer_id = hunks[0].buffer_id;
20534 hunks
20535 .into_iter()
20536 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20537 .collect::<Vec<_>>()
20538 });
20539 assert_eq!(hunk_ranges.len(), 1);
20540
20541 cx.update_editor(|editor, _, cx| {
20542 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20543 });
20544 executor.run_until_parked();
20545
20546 let hunk_collapsed = r#"
20547 ˇb
20548 c
20549 "#
20550 .unindent();
20551
20552 cx.assert_state_with_diff(hunk_collapsed);
20553
20554 cx.update_editor(|editor, _, cx| {
20555 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20556 });
20557 executor.run_until_parked();
20558
20559 cx.assert_state_with_diff(hunk_expanded);
20560}
20561
20562#[gpui::test]
20563async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20564 init_test(cx, |_| {});
20565
20566 let fs = FakeFs::new(cx.executor());
20567 fs.insert_tree(
20568 path!("/test"),
20569 json!({
20570 ".git": {},
20571 "file-1": "ONE\n",
20572 "file-2": "TWO\n",
20573 "file-3": "THREE\n",
20574 }),
20575 )
20576 .await;
20577
20578 fs.set_head_for_repo(
20579 path!("/test/.git").as_ref(),
20580 &[
20581 ("file-1".into(), "one\n".into()),
20582 ("file-2".into(), "two\n".into()),
20583 ("file-3".into(), "three\n".into()),
20584 ],
20585 "deadbeef",
20586 );
20587
20588 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20589 let mut buffers = vec![];
20590 for i in 1..=3 {
20591 let buffer = project
20592 .update(cx, |project, cx| {
20593 let path = format!(path!("/test/file-{}"), i);
20594 project.open_local_buffer(path, cx)
20595 })
20596 .await
20597 .unwrap();
20598 buffers.push(buffer);
20599 }
20600
20601 let multibuffer = cx.new(|cx| {
20602 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20603 multibuffer.set_all_diff_hunks_expanded(cx);
20604 for buffer in &buffers {
20605 let snapshot = buffer.read(cx).snapshot();
20606 multibuffer.set_excerpts_for_path(
20607 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20608 buffer.clone(),
20609 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20610 2,
20611 cx,
20612 );
20613 }
20614 multibuffer
20615 });
20616
20617 let editor = cx.add_window(|window, cx| {
20618 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20619 });
20620 cx.run_until_parked();
20621
20622 let snapshot = editor
20623 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20624 .unwrap();
20625 let hunks = snapshot
20626 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20627 .map(|hunk| match hunk {
20628 DisplayDiffHunk::Unfolded {
20629 display_row_range, ..
20630 } => display_row_range,
20631 DisplayDiffHunk::Folded { .. } => unreachable!(),
20632 })
20633 .collect::<Vec<_>>();
20634 assert_eq!(
20635 hunks,
20636 [
20637 DisplayRow(2)..DisplayRow(4),
20638 DisplayRow(7)..DisplayRow(9),
20639 DisplayRow(12)..DisplayRow(14),
20640 ]
20641 );
20642}
20643
20644#[gpui::test]
20645async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20646 init_test(cx, |_| {});
20647
20648 let mut cx = EditorTestContext::new(cx).await;
20649 cx.set_head_text(indoc! { "
20650 one
20651 two
20652 three
20653 four
20654 five
20655 "
20656 });
20657 cx.set_index_text(indoc! { "
20658 one
20659 two
20660 three
20661 four
20662 five
20663 "
20664 });
20665 cx.set_state(indoc! {"
20666 one
20667 TWO
20668 ˇTHREE
20669 FOUR
20670 five
20671 "});
20672 cx.run_until_parked();
20673 cx.update_editor(|editor, window, cx| {
20674 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20675 });
20676 cx.run_until_parked();
20677 cx.assert_index_text(Some(indoc! {"
20678 one
20679 TWO
20680 THREE
20681 FOUR
20682 five
20683 "}));
20684 cx.set_state(indoc! { "
20685 one
20686 TWO
20687 ˇTHREE-HUNDRED
20688 FOUR
20689 five
20690 "});
20691 cx.run_until_parked();
20692 cx.update_editor(|editor, window, cx| {
20693 let snapshot = editor.snapshot(window, cx);
20694 let hunks = editor
20695 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20696 .collect::<Vec<_>>();
20697 assert_eq!(hunks.len(), 1);
20698 assert_eq!(
20699 hunks[0].status(),
20700 DiffHunkStatus {
20701 kind: DiffHunkStatusKind::Modified,
20702 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
20703 }
20704 );
20705
20706 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20707 });
20708 cx.run_until_parked();
20709 cx.assert_index_text(Some(indoc! {"
20710 one
20711 TWO
20712 THREE-HUNDRED
20713 FOUR
20714 five
20715 "}));
20716}
20717
20718#[gpui::test]
20719fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
20720 init_test(cx, |_| {});
20721
20722 let editor = cx.add_window(|window, cx| {
20723 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
20724 build_editor(buffer, window, cx)
20725 });
20726
20727 let render_args = Arc::new(Mutex::new(None));
20728 let snapshot = editor
20729 .update(cx, |editor, window, cx| {
20730 let snapshot = editor.buffer().read(cx).snapshot(cx);
20731 let range =
20732 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
20733
20734 struct RenderArgs {
20735 row: MultiBufferRow,
20736 folded: bool,
20737 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
20738 }
20739
20740 let crease = Crease::inline(
20741 range,
20742 FoldPlaceholder::test(),
20743 {
20744 let toggle_callback = render_args.clone();
20745 move |row, folded, callback, _window, _cx| {
20746 *toggle_callback.lock() = Some(RenderArgs {
20747 row,
20748 folded,
20749 callback,
20750 });
20751 div()
20752 }
20753 },
20754 |_row, _folded, _window, _cx| div(),
20755 );
20756
20757 editor.insert_creases(Some(crease), cx);
20758 let snapshot = editor.snapshot(window, cx);
20759 let _div =
20760 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
20761 snapshot
20762 })
20763 .unwrap();
20764
20765 let render_args = render_args.lock().take().unwrap();
20766 assert_eq!(render_args.row, MultiBufferRow(1));
20767 assert!(!render_args.folded);
20768 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20769
20770 cx.update_window(*editor, |_, window, cx| {
20771 (render_args.callback)(true, window, cx)
20772 })
20773 .unwrap();
20774 let snapshot = editor
20775 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20776 .unwrap();
20777 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
20778
20779 cx.update_window(*editor, |_, window, cx| {
20780 (render_args.callback)(false, window, cx)
20781 })
20782 .unwrap();
20783 let snapshot = editor
20784 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20785 .unwrap();
20786 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20787}
20788
20789#[gpui::test]
20790async fn test_input_text(cx: &mut TestAppContext) {
20791 init_test(cx, |_| {});
20792 let mut cx = EditorTestContext::new(cx).await;
20793
20794 cx.set_state(
20795 &r#"ˇone
20796 two
20797
20798 three
20799 fourˇ
20800 five
20801
20802 siˇx"#
20803 .unindent(),
20804 );
20805
20806 cx.dispatch_action(HandleInput(String::new()));
20807 cx.assert_editor_state(
20808 &r#"ˇone
20809 two
20810
20811 three
20812 fourˇ
20813 five
20814
20815 siˇx"#
20816 .unindent(),
20817 );
20818
20819 cx.dispatch_action(HandleInput("AAAA".to_string()));
20820 cx.assert_editor_state(
20821 &r#"AAAAˇone
20822 two
20823
20824 three
20825 fourAAAAˇ
20826 five
20827
20828 siAAAAˇx"#
20829 .unindent(),
20830 );
20831}
20832
20833#[gpui::test]
20834async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
20835 init_test(cx, |_| {});
20836
20837 let mut cx = EditorTestContext::new(cx).await;
20838 cx.set_state(
20839 r#"let foo = 1;
20840let foo = 2;
20841let foo = 3;
20842let fooˇ = 4;
20843let foo = 5;
20844let foo = 6;
20845let foo = 7;
20846let foo = 8;
20847let foo = 9;
20848let foo = 10;
20849let foo = 11;
20850let foo = 12;
20851let foo = 13;
20852let foo = 14;
20853let foo = 15;"#,
20854 );
20855
20856 cx.update_editor(|e, window, cx| {
20857 assert_eq!(
20858 e.next_scroll_position,
20859 NextScrollCursorCenterTopBottom::Center,
20860 "Default next scroll direction is center",
20861 );
20862
20863 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20864 assert_eq!(
20865 e.next_scroll_position,
20866 NextScrollCursorCenterTopBottom::Top,
20867 "After center, next scroll direction should be top",
20868 );
20869
20870 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20871 assert_eq!(
20872 e.next_scroll_position,
20873 NextScrollCursorCenterTopBottom::Bottom,
20874 "After top, next scroll direction should be bottom",
20875 );
20876
20877 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20878 assert_eq!(
20879 e.next_scroll_position,
20880 NextScrollCursorCenterTopBottom::Center,
20881 "After bottom, scrolling should start over",
20882 );
20883
20884 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20885 assert_eq!(
20886 e.next_scroll_position,
20887 NextScrollCursorCenterTopBottom::Top,
20888 "Scrolling continues if retriggered fast enough"
20889 );
20890 });
20891
20892 cx.executor()
20893 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
20894 cx.executor().run_until_parked();
20895 cx.update_editor(|e, _, _| {
20896 assert_eq!(
20897 e.next_scroll_position,
20898 NextScrollCursorCenterTopBottom::Center,
20899 "If scrolling is not triggered fast enough, it should reset"
20900 );
20901 });
20902}
20903
20904#[gpui::test]
20905async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
20906 init_test(cx, |_| {});
20907 let mut cx = EditorLspTestContext::new_rust(
20908 lsp::ServerCapabilities {
20909 definition_provider: Some(lsp::OneOf::Left(true)),
20910 references_provider: Some(lsp::OneOf::Left(true)),
20911 ..lsp::ServerCapabilities::default()
20912 },
20913 cx,
20914 )
20915 .await;
20916
20917 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
20918 let go_to_definition = cx
20919 .lsp
20920 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20921 move |params, _| async move {
20922 if empty_go_to_definition {
20923 Ok(None)
20924 } else {
20925 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20926 uri: params.text_document_position_params.text_document.uri,
20927 range: lsp::Range::new(
20928 lsp::Position::new(4, 3),
20929 lsp::Position::new(4, 6),
20930 ),
20931 })))
20932 }
20933 },
20934 );
20935 let references = cx
20936 .lsp
20937 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20938 Ok(Some(vec![lsp::Location {
20939 uri: params.text_document_position.text_document.uri,
20940 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20941 }]))
20942 });
20943 (go_to_definition, references)
20944 };
20945
20946 cx.set_state(
20947 &r#"fn one() {
20948 let mut a = ˇtwo();
20949 }
20950
20951 fn two() {}"#
20952 .unindent(),
20953 );
20954 set_up_lsp_handlers(false, &mut cx);
20955 let navigated = cx
20956 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20957 .await
20958 .expect("Failed to navigate to definition");
20959 assert_eq!(
20960 navigated,
20961 Navigated::Yes,
20962 "Should have navigated to definition from the GetDefinition response"
20963 );
20964 cx.assert_editor_state(
20965 &r#"fn one() {
20966 let mut a = two();
20967 }
20968
20969 fn «twoˇ»() {}"#
20970 .unindent(),
20971 );
20972
20973 let editors = cx.update_workspace(|workspace, _, cx| {
20974 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20975 });
20976 cx.update_editor(|_, _, test_editor_cx| {
20977 assert_eq!(
20978 editors.len(),
20979 1,
20980 "Initially, only one, test, editor should be open in the workspace"
20981 );
20982 assert_eq!(
20983 test_editor_cx.entity(),
20984 editors.last().expect("Asserted len is 1").clone()
20985 );
20986 });
20987
20988 set_up_lsp_handlers(true, &mut cx);
20989 let navigated = cx
20990 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20991 .await
20992 .expect("Failed to navigate to lookup references");
20993 assert_eq!(
20994 navigated,
20995 Navigated::Yes,
20996 "Should have navigated to references as a fallback after empty GoToDefinition response"
20997 );
20998 // We should not change the selections in the existing file,
20999 // if opening another milti buffer with the references
21000 cx.assert_editor_state(
21001 &r#"fn one() {
21002 let mut a = two();
21003 }
21004
21005 fn «twoˇ»() {}"#
21006 .unindent(),
21007 );
21008 let editors = cx.update_workspace(|workspace, _, cx| {
21009 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21010 });
21011 cx.update_editor(|_, _, test_editor_cx| {
21012 assert_eq!(
21013 editors.len(),
21014 2,
21015 "After falling back to references search, we open a new editor with the results"
21016 );
21017 let references_fallback_text = editors
21018 .into_iter()
21019 .find(|new_editor| *new_editor != test_editor_cx.entity())
21020 .expect("Should have one non-test editor now")
21021 .read(test_editor_cx)
21022 .text(test_editor_cx);
21023 assert_eq!(
21024 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21025 "Should use the range from the references response and not the GoToDefinition one"
21026 );
21027 });
21028}
21029
21030#[gpui::test]
21031async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21032 init_test(cx, |_| {});
21033 cx.update(|cx| {
21034 let mut editor_settings = EditorSettings::get_global(cx).clone();
21035 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21036 EditorSettings::override_global(editor_settings, cx);
21037 });
21038 let mut cx = EditorLspTestContext::new_rust(
21039 lsp::ServerCapabilities {
21040 definition_provider: Some(lsp::OneOf::Left(true)),
21041 references_provider: Some(lsp::OneOf::Left(true)),
21042 ..lsp::ServerCapabilities::default()
21043 },
21044 cx,
21045 )
21046 .await;
21047 let original_state = r#"fn one() {
21048 let mut a = ˇtwo();
21049 }
21050
21051 fn two() {}"#
21052 .unindent();
21053 cx.set_state(&original_state);
21054
21055 let mut go_to_definition = cx
21056 .lsp
21057 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21058 move |_, _| async move { Ok(None) },
21059 );
21060 let _references = cx
21061 .lsp
21062 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21063 panic!("Should not call for references with no go to definition fallback")
21064 });
21065
21066 let navigated = cx
21067 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21068 .await
21069 .expect("Failed to navigate to lookup references");
21070 go_to_definition
21071 .next()
21072 .await
21073 .expect("Should have called the go_to_definition handler");
21074
21075 assert_eq!(
21076 navigated,
21077 Navigated::No,
21078 "Should have navigated to references as a fallback after empty GoToDefinition response"
21079 );
21080 cx.assert_editor_state(&original_state);
21081 let editors = cx.update_workspace(|workspace, _, cx| {
21082 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21083 });
21084 cx.update_editor(|_, _, _| {
21085 assert_eq!(
21086 editors.len(),
21087 1,
21088 "After unsuccessful fallback, no other editor should have been opened"
21089 );
21090 });
21091}
21092
21093#[gpui::test]
21094async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21095 init_test(cx, |_| {});
21096
21097 let language = Arc::new(Language::new(
21098 LanguageConfig::default(),
21099 Some(tree_sitter_rust::LANGUAGE.into()),
21100 ));
21101
21102 let text = r#"
21103 #[cfg(test)]
21104 mod tests() {
21105 #[test]
21106 fn runnable_1() {
21107 let a = 1;
21108 }
21109
21110 #[test]
21111 fn runnable_2() {
21112 let a = 1;
21113 let b = 2;
21114 }
21115 }
21116 "#
21117 .unindent();
21118
21119 let fs = FakeFs::new(cx.executor());
21120 fs.insert_file("/file.rs", Default::default()).await;
21121
21122 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21123 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21124 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21125 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21126 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21127
21128 let editor = cx.new_window_entity(|window, cx| {
21129 Editor::new(
21130 EditorMode::full(),
21131 multi_buffer,
21132 Some(project.clone()),
21133 window,
21134 cx,
21135 )
21136 });
21137
21138 editor.update_in(cx, |editor, window, cx| {
21139 let snapshot = editor.buffer().read(cx).snapshot(cx);
21140 editor.tasks.insert(
21141 (buffer.read(cx).remote_id(), 3),
21142 RunnableTasks {
21143 templates: vec![],
21144 offset: snapshot.anchor_before(43),
21145 column: 0,
21146 extra_variables: HashMap::default(),
21147 context_range: BufferOffset(43)..BufferOffset(85),
21148 },
21149 );
21150 editor.tasks.insert(
21151 (buffer.read(cx).remote_id(), 8),
21152 RunnableTasks {
21153 templates: vec![],
21154 offset: snapshot.anchor_before(86),
21155 column: 0,
21156 extra_variables: HashMap::default(),
21157 context_range: BufferOffset(86)..BufferOffset(191),
21158 },
21159 );
21160
21161 // Test finding task when cursor is inside function body
21162 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21163 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21164 });
21165 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21166 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21167
21168 // Test finding task when cursor is on function name
21169 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21170 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21171 });
21172 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21173 assert_eq!(row, 8, "Should find task when cursor is on function name");
21174 });
21175}
21176
21177#[gpui::test]
21178async fn test_folding_buffers(cx: &mut TestAppContext) {
21179 init_test(cx, |_| {});
21180
21181 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21182 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21183 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21184
21185 let fs = FakeFs::new(cx.executor());
21186 fs.insert_tree(
21187 path!("/a"),
21188 json!({
21189 "first.rs": sample_text_1,
21190 "second.rs": sample_text_2,
21191 "third.rs": sample_text_3,
21192 }),
21193 )
21194 .await;
21195 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21196 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21197 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21198 let worktree = project.update(cx, |project, cx| {
21199 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21200 assert_eq!(worktrees.len(), 1);
21201 worktrees.pop().unwrap()
21202 });
21203 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21204
21205 let buffer_1 = project
21206 .update(cx, |project, cx| {
21207 project.open_buffer((worktree_id, "first.rs"), cx)
21208 })
21209 .await
21210 .unwrap();
21211 let buffer_2 = project
21212 .update(cx, |project, cx| {
21213 project.open_buffer((worktree_id, "second.rs"), cx)
21214 })
21215 .await
21216 .unwrap();
21217 let buffer_3 = project
21218 .update(cx, |project, cx| {
21219 project.open_buffer((worktree_id, "third.rs"), cx)
21220 })
21221 .await
21222 .unwrap();
21223
21224 let multi_buffer = cx.new(|cx| {
21225 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21226 multi_buffer.push_excerpts(
21227 buffer_1.clone(),
21228 [
21229 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21230 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21231 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21232 ],
21233 cx,
21234 );
21235 multi_buffer.push_excerpts(
21236 buffer_2.clone(),
21237 [
21238 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21239 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21240 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21241 ],
21242 cx,
21243 );
21244 multi_buffer.push_excerpts(
21245 buffer_3.clone(),
21246 [
21247 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21248 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21249 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21250 ],
21251 cx,
21252 );
21253 multi_buffer
21254 });
21255 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21256 Editor::new(
21257 EditorMode::full(),
21258 multi_buffer.clone(),
21259 Some(project.clone()),
21260 window,
21261 cx,
21262 )
21263 });
21264
21265 assert_eq!(
21266 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21267 "\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",
21268 );
21269
21270 multi_buffer_editor.update(cx, |editor, cx| {
21271 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21272 });
21273 assert_eq!(
21274 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21275 "\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",
21276 "After folding the first buffer, its text should not be displayed"
21277 );
21278
21279 multi_buffer_editor.update(cx, |editor, cx| {
21280 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21281 });
21282 assert_eq!(
21283 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21284 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21285 "After folding the second buffer, its text should not be displayed"
21286 );
21287
21288 multi_buffer_editor.update(cx, |editor, cx| {
21289 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21290 });
21291 assert_eq!(
21292 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21293 "\n\n\n\n\n",
21294 "After folding the third buffer, its text should not be displayed"
21295 );
21296
21297 // Emulate selection inside the fold logic, that should work
21298 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21299 editor
21300 .snapshot(window, cx)
21301 .next_line_boundary(Point::new(0, 4));
21302 });
21303
21304 multi_buffer_editor.update(cx, |editor, cx| {
21305 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21306 });
21307 assert_eq!(
21308 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21309 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21310 "After unfolding the second buffer, its text should be displayed"
21311 );
21312
21313 // Typing inside of buffer 1 causes that buffer to be unfolded.
21314 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21315 assert_eq!(
21316 multi_buffer
21317 .read(cx)
21318 .snapshot(cx)
21319 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21320 .collect::<String>(),
21321 "bbbb"
21322 );
21323 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21324 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21325 });
21326 editor.handle_input("B", window, cx);
21327 });
21328
21329 assert_eq!(
21330 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21331 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21332 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21333 );
21334
21335 multi_buffer_editor.update(cx, |editor, cx| {
21336 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21337 });
21338 assert_eq!(
21339 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21340 "\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",
21341 "After unfolding the all buffers, all original text should be displayed"
21342 );
21343}
21344
21345#[gpui::test]
21346async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21347 init_test(cx, |_| {});
21348
21349 let sample_text_1 = "1111\n2222\n3333".to_string();
21350 let sample_text_2 = "4444\n5555\n6666".to_string();
21351 let sample_text_3 = "7777\n8888\n9999".to_string();
21352
21353 let fs = FakeFs::new(cx.executor());
21354 fs.insert_tree(
21355 path!("/a"),
21356 json!({
21357 "first.rs": sample_text_1,
21358 "second.rs": sample_text_2,
21359 "third.rs": sample_text_3,
21360 }),
21361 )
21362 .await;
21363 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21364 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21365 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21366 let worktree = project.update(cx, |project, cx| {
21367 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21368 assert_eq!(worktrees.len(), 1);
21369 worktrees.pop().unwrap()
21370 });
21371 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21372
21373 let buffer_1 = project
21374 .update(cx, |project, cx| {
21375 project.open_buffer((worktree_id, "first.rs"), cx)
21376 })
21377 .await
21378 .unwrap();
21379 let buffer_2 = project
21380 .update(cx, |project, cx| {
21381 project.open_buffer((worktree_id, "second.rs"), cx)
21382 })
21383 .await
21384 .unwrap();
21385 let buffer_3 = project
21386 .update(cx, |project, cx| {
21387 project.open_buffer((worktree_id, "third.rs"), cx)
21388 })
21389 .await
21390 .unwrap();
21391
21392 let multi_buffer = cx.new(|cx| {
21393 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21394 multi_buffer.push_excerpts(
21395 buffer_1.clone(),
21396 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21397 cx,
21398 );
21399 multi_buffer.push_excerpts(
21400 buffer_2.clone(),
21401 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21402 cx,
21403 );
21404 multi_buffer.push_excerpts(
21405 buffer_3.clone(),
21406 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21407 cx,
21408 );
21409 multi_buffer
21410 });
21411
21412 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21413 Editor::new(
21414 EditorMode::full(),
21415 multi_buffer,
21416 Some(project.clone()),
21417 window,
21418 cx,
21419 )
21420 });
21421
21422 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21423 assert_eq!(
21424 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21425 full_text,
21426 );
21427
21428 multi_buffer_editor.update(cx, |editor, cx| {
21429 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21430 });
21431 assert_eq!(
21432 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21433 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21434 "After folding the first buffer, its text should not be displayed"
21435 );
21436
21437 multi_buffer_editor.update(cx, |editor, cx| {
21438 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21439 });
21440
21441 assert_eq!(
21442 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21443 "\n\n\n\n\n\n7777\n8888\n9999",
21444 "After folding the second buffer, its text should not be displayed"
21445 );
21446
21447 multi_buffer_editor.update(cx, |editor, cx| {
21448 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21449 });
21450 assert_eq!(
21451 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21452 "\n\n\n\n\n",
21453 "After folding the third buffer, its text should not be displayed"
21454 );
21455
21456 multi_buffer_editor.update(cx, |editor, cx| {
21457 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21458 });
21459 assert_eq!(
21460 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21461 "\n\n\n\n4444\n5555\n6666\n\n",
21462 "After unfolding the second buffer, its text should be displayed"
21463 );
21464
21465 multi_buffer_editor.update(cx, |editor, cx| {
21466 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21467 });
21468 assert_eq!(
21469 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21470 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21471 "After unfolding the first buffer, its text should be displayed"
21472 );
21473
21474 multi_buffer_editor.update(cx, |editor, cx| {
21475 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21476 });
21477 assert_eq!(
21478 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21479 full_text,
21480 "After unfolding all buffers, all original text should be displayed"
21481 );
21482}
21483
21484#[gpui::test]
21485async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21486 init_test(cx, |_| {});
21487
21488 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21489
21490 let fs = FakeFs::new(cx.executor());
21491 fs.insert_tree(
21492 path!("/a"),
21493 json!({
21494 "main.rs": sample_text,
21495 }),
21496 )
21497 .await;
21498 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21499 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21500 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21501 let worktree = project.update(cx, |project, cx| {
21502 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21503 assert_eq!(worktrees.len(), 1);
21504 worktrees.pop().unwrap()
21505 });
21506 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21507
21508 let buffer_1 = project
21509 .update(cx, |project, cx| {
21510 project.open_buffer((worktree_id, "main.rs"), cx)
21511 })
21512 .await
21513 .unwrap();
21514
21515 let multi_buffer = cx.new(|cx| {
21516 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21517 multi_buffer.push_excerpts(
21518 buffer_1.clone(),
21519 [ExcerptRange::new(
21520 Point::new(0, 0)
21521 ..Point::new(
21522 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21523 0,
21524 ),
21525 )],
21526 cx,
21527 );
21528 multi_buffer
21529 });
21530 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21531 Editor::new(
21532 EditorMode::full(),
21533 multi_buffer,
21534 Some(project.clone()),
21535 window,
21536 cx,
21537 )
21538 });
21539
21540 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21541 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21542 enum TestHighlight {}
21543 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21544 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21545 editor.highlight_text::<TestHighlight>(
21546 vec![highlight_range.clone()],
21547 HighlightStyle::color(Hsla::green()),
21548 cx,
21549 );
21550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21551 s.select_ranges(Some(highlight_range))
21552 });
21553 });
21554
21555 let full_text = format!("\n\n{sample_text}");
21556 assert_eq!(
21557 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21558 full_text,
21559 );
21560}
21561
21562#[gpui::test]
21563async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
21564 init_test(cx, |_| {});
21565 cx.update(|cx| {
21566 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
21567 "keymaps/default-linux.json",
21568 cx,
21569 )
21570 .unwrap();
21571 cx.bind_keys(default_key_bindings);
21572 });
21573
21574 let (editor, cx) = cx.add_window_view(|window, cx| {
21575 let multi_buffer = MultiBuffer::build_multi(
21576 [
21577 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
21578 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
21579 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
21580 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
21581 ],
21582 cx,
21583 );
21584 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
21585
21586 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
21587 // fold all but the second buffer, so that we test navigating between two
21588 // adjacent folded buffers, as well as folded buffers at the start and
21589 // end the multibuffer
21590 editor.fold_buffer(buffer_ids[0], cx);
21591 editor.fold_buffer(buffer_ids[2], cx);
21592 editor.fold_buffer(buffer_ids[3], cx);
21593
21594 editor
21595 });
21596 cx.simulate_resize(size(px(1000.), px(1000.)));
21597
21598 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
21599 cx.assert_excerpts_with_selections(indoc! {"
21600 [EXCERPT]
21601 ˇ[FOLDED]
21602 [EXCERPT]
21603 a1
21604 b1
21605 [EXCERPT]
21606 [FOLDED]
21607 [EXCERPT]
21608 [FOLDED]
21609 "
21610 });
21611 cx.simulate_keystroke("down");
21612 cx.assert_excerpts_with_selections(indoc! {"
21613 [EXCERPT]
21614 [FOLDED]
21615 [EXCERPT]
21616 ˇa1
21617 b1
21618 [EXCERPT]
21619 [FOLDED]
21620 [EXCERPT]
21621 [FOLDED]
21622 "
21623 });
21624 cx.simulate_keystroke("down");
21625 cx.assert_excerpts_with_selections(indoc! {"
21626 [EXCERPT]
21627 [FOLDED]
21628 [EXCERPT]
21629 a1
21630 ˇb1
21631 [EXCERPT]
21632 [FOLDED]
21633 [EXCERPT]
21634 [FOLDED]
21635 "
21636 });
21637 cx.simulate_keystroke("down");
21638 cx.assert_excerpts_with_selections(indoc! {"
21639 [EXCERPT]
21640 [FOLDED]
21641 [EXCERPT]
21642 a1
21643 b1
21644 ˇ[EXCERPT]
21645 [FOLDED]
21646 [EXCERPT]
21647 [FOLDED]
21648 "
21649 });
21650 cx.simulate_keystroke("down");
21651 cx.assert_excerpts_with_selections(indoc! {"
21652 [EXCERPT]
21653 [FOLDED]
21654 [EXCERPT]
21655 a1
21656 b1
21657 [EXCERPT]
21658 ˇ[FOLDED]
21659 [EXCERPT]
21660 [FOLDED]
21661 "
21662 });
21663 for _ in 0..5 {
21664 cx.simulate_keystroke("down");
21665 cx.assert_excerpts_with_selections(indoc! {"
21666 [EXCERPT]
21667 [FOLDED]
21668 [EXCERPT]
21669 a1
21670 b1
21671 [EXCERPT]
21672 [FOLDED]
21673 [EXCERPT]
21674 ˇ[FOLDED]
21675 "
21676 });
21677 }
21678
21679 cx.simulate_keystroke("up");
21680 cx.assert_excerpts_with_selections(indoc! {"
21681 [EXCERPT]
21682 [FOLDED]
21683 [EXCERPT]
21684 a1
21685 b1
21686 [EXCERPT]
21687 ˇ[FOLDED]
21688 [EXCERPT]
21689 [FOLDED]
21690 "
21691 });
21692 cx.simulate_keystroke("up");
21693 cx.assert_excerpts_with_selections(indoc! {"
21694 [EXCERPT]
21695 [FOLDED]
21696 [EXCERPT]
21697 a1
21698 b1
21699 ˇ[EXCERPT]
21700 [FOLDED]
21701 [EXCERPT]
21702 [FOLDED]
21703 "
21704 });
21705 cx.simulate_keystroke("up");
21706 cx.assert_excerpts_with_selections(indoc! {"
21707 [EXCERPT]
21708 [FOLDED]
21709 [EXCERPT]
21710 a1
21711 ˇb1
21712 [EXCERPT]
21713 [FOLDED]
21714 [EXCERPT]
21715 [FOLDED]
21716 "
21717 });
21718 cx.simulate_keystroke("up");
21719 cx.assert_excerpts_with_selections(indoc! {"
21720 [EXCERPT]
21721 [FOLDED]
21722 [EXCERPT]
21723 ˇa1
21724 b1
21725 [EXCERPT]
21726 [FOLDED]
21727 [EXCERPT]
21728 [FOLDED]
21729 "
21730 });
21731 for _ in 0..5 {
21732 cx.simulate_keystroke("up");
21733 cx.assert_excerpts_with_selections(indoc! {"
21734 [EXCERPT]
21735 ˇ[FOLDED]
21736 [EXCERPT]
21737 a1
21738 b1
21739 [EXCERPT]
21740 [FOLDED]
21741 [EXCERPT]
21742 [FOLDED]
21743 "
21744 });
21745 }
21746}
21747
21748#[gpui::test]
21749async fn test_edit_prediction_text(cx: &mut TestAppContext) {
21750 init_test(cx, |_| {});
21751
21752 // Simple insertion
21753 assert_highlighted_edits(
21754 "Hello, world!",
21755 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
21756 true,
21757 cx,
21758 |highlighted_edits, cx| {
21759 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
21760 assert_eq!(highlighted_edits.highlights.len(), 1);
21761 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
21762 assert_eq!(
21763 highlighted_edits.highlights[0].1.background_color,
21764 Some(cx.theme().status().created_background)
21765 );
21766 },
21767 )
21768 .await;
21769
21770 // Replacement
21771 assert_highlighted_edits(
21772 "This is a test.",
21773 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
21774 false,
21775 cx,
21776 |highlighted_edits, cx| {
21777 assert_eq!(highlighted_edits.text, "That is a test.");
21778 assert_eq!(highlighted_edits.highlights.len(), 1);
21779 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
21780 assert_eq!(
21781 highlighted_edits.highlights[0].1.background_color,
21782 Some(cx.theme().status().created_background)
21783 );
21784 },
21785 )
21786 .await;
21787
21788 // Multiple edits
21789 assert_highlighted_edits(
21790 "Hello, world!",
21791 vec![
21792 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
21793 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
21794 ],
21795 false,
21796 cx,
21797 |highlighted_edits, cx| {
21798 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
21799 assert_eq!(highlighted_edits.highlights.len(), 2);
21800 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
21801 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
21802 assert_eq!(
21803 highlighted_edits.highlights[0].1.background_color,
21804 Some(cx.theme().status().created_background)
21805 );
21806 assert_eq!(
21807 highlighted_edits.highlights[1].1.background_color,
21808 Some(cx.theme().status().created_background)
21809 );
21810 },
21811 )
21812 .await;
21813
21814 // Multiple lines with edits
21815 assert_highlighted_edits(
21816 "First line\nSecond line\nThird line\nFourth line",
21817 vec![
21818 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
21819 (
21820 Point::new(2, 0)..Point::new(2, 10),
21821 "New third line".to_string(),
21822 ),
21823 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
21824 ],
21825 false,
21826 cx,
21827 |highlighted_edits, cx| {
21828 assert_eq!(
21829 highlighted_edits.text,
21830 "Second modified\nNew third line\nFourth updated line"
21831 );
21832 assert_eq!(highlighted_edits.highlights.len(), 3);
21833 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
21834 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
21835 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
21836 for highlight in &highlighted_edits.highlights {
21837 assert_eq!(
21838 highlight.1.background_color,
21839 Some(cx.theme().status().created_background)
21840 );
21841 }
21842 },
21843 )
21844 .await;
21845}
21846
21847#[gpui::test]
21848async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
21849 init_test(cx, |_| {});
21850
21851 // Deletion
21852 assert_highlighted_edits(
21853 "Hello, world!",
21854 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
21855 true,
21856 cx,
21857 |highlighted_edits, cx| {
21858 assert_eq!(highlighted_edits.text, "Hello, world!");
21859 assert_eq!(highlighted_edits.highlights.len(), 1);
21860 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
21861 assert_eq!(
21862 highlighted_edits.highlights[0].1.background_color,
21863 Some(cx.theme().status().deleted_background)
21864 );
21865 },
21866 )
21867 .await;
21868
21869 // Insertion
21870 assert_highlighted_edits(
21871 "Hello, world!",
21872 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
21873 true,
21874 cx,
21875 |highlighted_edits, cx| {
21876 assert_eq!(highlighted_edits.highlights.len(), 1);
21877 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
21878 assert_eq!(
21879 highlighted_edits.highlights[0].1.background_color,
21880 Some(cx.theme().status().created_background)
21881 );
21882 },
21883 )
21884 .await;
21885}
21886
21887async fn assert_highlighted_edits(
21888 text: &str,
21889 edits: Vec<(Range<Point>, String)>,
21890 include_deletions: bool,
21891 cx: &mut TestAppContext,
21892 assertion_fn: impl Fn(HighlightedText, &App),
21893) {
21894 let window = cx.add_window(|window, cx| {
21895 let buffer = MultiBuffer::build_simple(text, cx);
21896 Editor::new(EditorMode::full(), buffer, None, window, cx)
21897 });
21898 let cx = &mut VisualTestContext::from_window(*window, cx);
21899
21900 let (buffer, snapshot) = window
21901 .update(cx, |editor, _window, cx| {
21902 (
21903 editor.buffer().clone(),
21904 editor.buffer().read(cx).snapshot(cx),
21905 )
21906 })
21907 .unwrap();
21908
21909 let edits = edits
21910 .into_iter()
21911 .map(|(range, edit)| {
21912 (
21913 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21914 edit,
21915 )
21916 })
21917 .collect::<Vec<_>>();
21918
21919 let text_anchor_edits = edits
21920 .clone()
21921 .into_iter()
21922 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21923 .collect::<Vec<_>>();
21924
21925 let edit_preview = window
21926 .update(cx, |_, _window, cx| {
21927 buffer
21928 .read(cx)
21929 .as_singleton()
21930 .unwrap()
21931 .read(cx)
21932 .preview_edits(text_anchor_edits.into(), cx)
21933 })
21934 .unwrap()
21935 .await;
21936
21937 cx.update(|_window, cx| {
21938 let highlighted_edits = edit_prediction_edit_text(
21939 snapshot.as_singleton().unwrap().2,
21940 &edits,
21941 &edit_preview,
21942 include_deletions,
21943 cx,
21944 );
21945 assertion_fn(highlighted_edits, cx)
21946 });
21947}
21948
21949#[track_caller]
21950fn assert_breakpoint(
21951 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21952 path: &Arc<Path>,
21953 expected: Vec<(u32, Breakpoint)>,
21954) {
21955 if expected.is_empty() {
21956 assert!(!breakpoints.contains_key(path), "{}", path.display());
21957 } else {
21958 let mut breakpoint = breakpoints
21959 .get(path)
21960 .unwrap()
21961 .iter()
21962 .map(|breakpoint| {
21963 (
21964 breakpoint.row,
21965 Breakpoint {
21966 message: breakpoint.message.clone(),
21967 state: breakpoint.state,
21968 condition: breakpoint.condition.clone(),
21969 hit_condition: breakpoint.hit_condition.clone(),
21970 },
21971 )
21972 })
21973 .collect::<Vec<_>>();
21974
21975 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21976
21977 assert_eq!(expected, breakpoint);
21978 }
21979}
21980
21981fn add_log_breakpoint_at_cursor(
21982 editor: &mut Editor,
21983 log_message: &str,
21984 window: &mut Window,
21985 cx: &mut Context<Editor>,
21986) {
21987 let (anchor, bp) = editor
21988 .breakpoints_at_cursors(window, cx)
21989 .first()
21990 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
21991 .unwrap_or_else(|| {
21992 let cursor_position: Point = editor.selections.newest(cx).head();
21993
21994 let breakpoint_position = editor
21995 .snapshot(window, cx)
21996 .display_snapshot
21997 .buffer_snapshot
21998 .anchor_before(Point::new(cursor_position.row, 0));
21999
22000 (breakpoint_position, Breakpoint::new_log(log_message))
22001 });
22002
22003 editor.edit_breakpoint_at_anchor(
22004 anchor,
22005 bp,
22006 BreakpointEditAction::EditLogMessage(log_message.into()),
22007 cx,
22008 );
22009}
22010
22011#[gpui::test]
22012async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22013 init_test(cx, |_| {});
22014
22015 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22016 let fs = FakeFs::new(cx.executor());
22017 fs.insert_tree(
22018 path!("/a"),
22019 json!({
22020 "main.rs": sample_text,
22021 }),
22022 )
22023 .await;
22024 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22025 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22026 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22027
22028 let fs = FakeFs::new(cx.executor());
22029 fs.insert_tree(
22030 path!("/a"),
22031 json!({
22032 "main.rs": sample_text,
22033 }),
22034 )
22035 .await;
22036 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22037 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22038 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22039 let worktree_id = workspace
22040 .update(cx, |workspace, _window, cx| {
22041 workspace.project().update(cx, |project, cx| {
22042 project.worktrees(cx).next().unwrap().read(cx).id()
22043 })
22044 })
22045 .unwrap();
22046
22047 let buffer = project
22048 .update(cx, |project, cx| {
22049 project.open_buffer((worktree_id, "main.rs"), cx)
22050 })
22051 .await
22052 .unwrap();
22053
22054 let (editor, cx) = cx.add_window_view(|window, cx| {
22055 Editor::new(
22056 EditorMode::full(),
22057 MultiBuffer::build_from_buffer(buffer, cx),
22058 Some(project.clone()),
22059 window,
22060 cx,
22061 )
22062 });
22063
22064 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22065 let abs_path = project.read_with(cx, |project, cx| {
22066 project
22067 .absolute_path(&project_path, cx)
22068 .map(Arc::from)
22069 .unwrap()
22070 });
22071
22072 // assert we can add breakpoint on the first line
22073 editor.update_in(cx, |editor, window, cx| {
22074 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22075 editor.move_to_end(&MoveToEnd, window, cx);
22076 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22077 });
22078
22079 let breakpoints = editor.update(cx, |editor, cx| {
22080 editor
22081 .breakpoint_store()
22082 .as_ref()
22083 .unwrap()
22084 .read(cx)
22085 .all_source_breakpoints(cx)
22086 });
22087
22088 assert_eq!(1, breakpoints.len());
22089 assert_breakpoint(
22090 &breakpoints,
22091 &abs_path,
22092 vec![
22093 (0, Breakpoint::new_standard()),
22094 (3, Breakpoint::new_standard()),
22095 ],
22096 );
22097
22098 editor.update_in(cx, |editor, window, cx| {
22099 editor.move_to_beginning(&MoveToBeginning, window, cx);
22100 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22101 });
22102
22103 let breakpoints = editor.update(cx, |editor, cx| {
22104 editor
22105 .breakpoint_store()
22106 .as_ref()
22107 .unwrap()
22108 .read(cx)
22109 .all_source_breakpoints(cx)
22110 });
22111
22112 assert_eq!(1, breakpoints.len());
22113 assert_breakpoint(
22114 &breakpoints,
22115 &abs_path,
22116 vec![(3, Breakpoint::new_standard())],
22117 );
22118
22119 editor.update_in(cx, |editor, window, cx| {
22120 editor.move_to_end(&MoveToEnd, window, cx);
22121 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22122 });
22123
22124 let breakpoints = editor.update(cx, |editor, cx| {
22125 editor
22126 .breakpoint_store()
22127 .as_ref()
22128 .unwrap()
22129 .read(cx)
22130 .all_source_breakpoints(cx)
22131 });
22132
22133 assert_eq!(0, breakpoints.len());
22134 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22135}
22136
22137#[gpui::test]
22138async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22139 init_test(cx, |_| {});
22140
22141 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22142
22143 let fs = FakeFs::new(cx.executor());
22144 fs.insert_tree(
22145 path!("/a"),
22146 json!({
22147 "main.rs": sample_text,
22148 }),
22149 )
22150 .await;
22151 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22152 let (workspace, cx) =
22153 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22154
22155 let worktree_id = workspace.update(cx, |workspace, cx| {
22156 workspace.project().update(cx, |project, cx| {
22157 project.worktrees(cx).next().unwrap().read(cx).id()
22158 })
22159 });
22160
22161 let buffer = project
22162 .update(cx, |project, cx| {
22163 project.open_buffer((worktree_id, "main.rs"), cx)
22164 })
22165 .await
22166 .unwrap();
22167
22168 let (editor, cx) = cx.add_window_view(|window, cx| {
22169 Editor::new(
22170 EditorMode::full(),
22171 MultiBuffer::build_from_buffer(buffer, cx),
22172 Some(project.clone()),
22173 window,
22174 cx,
22175 )
22176 });
22177
22178 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22179 let abs_path = project.read_with(cx, |project, cx| {
22180 project
22181 .absolute_path(&project_path, cx)
22182 .map(Arc::from)
22183 .unwrap()
22184 });
22185
22186 editor.update_in(cx, |editor, window, cx| {
22187 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22188 });
22189
22190 let breakpoints = editor.update(cx, |editor, cx| {
22191 editor
22192 .breakpoint_store()
22193 .as_ref()
22194 .unwrap()
22195 .read(cx)
22196 .all_source_breakpoints(cx)
22197 });
22198
22199 assert_breakpoint(
22200 &breakpoints,
22201 &abs_path,
22202 vec![(0, Breakpoint::new_log("hello world"))],
22203 );
22204
22205 // Removing a log message from a log breakpoint should remove it
22206 editor.update_in(cx, |editor, window, cx| {
22207 add_log_breakpoint_at_cursor(editor, "", window, cx);
22208 });
22209
22210 let breakpoints = editor.update(cx, |editor, cx| {
22211 editor
22212 .breakpoint_store()
22213 .as_ref()
22214 .unwrap()
22215 .read(cx)
22216 .all_source_breakpoints(cx)
22217 });
22218
22219 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22220
22221 editor.update_in(cx, |editor, window, cx| {
22222 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22223 editor.move_to_end(&MoveToEnd, window, cx);
22224 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22225 // Not adding a log message to a standard breakpoint shouldn't remove it
22226 add_log_breakpoint_at_cursor(editor, "", window, cx);
22227 });
22228
22229 let breakpoints = editor.update(cx, |editor, cx| {
22230 editor
22231 .breakpoint_store()
22232 .as_ref()
22233 .unwrap()
22234 .read(cx)
22235 .all_source_breakpoints(cx)
22236 });
22237
22238 assert_breakpoint(
22239 &breakpoints,
22240 &abs_path,
22241 vec![
22242 (0, Breakpoint::new_standard()),
22243 (3, Breakpoint::new_standard()),
22244 ],
22245 );
22246
22247 editor.update_in(cx, |editor, window, cx| {
22248 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22249 });
22250
22251 let breakpoints = editor.update(cx, |editor, cx| {
22252 editor
22253 .breakpoint_store()
22254 .as_ref()
22255 .unwrap()
22256 .read(cx)
22257 .all_source_breakpoints(cx)
22258 });
22259
22260 assert_breakpoint(
22261 &breakpoints,
22262 &abs_path,
22263 vec![
22264 (0, Breakpoint::new_standard()),
22265 (3, Breakpoint::new_log("hello world")),
22266 ],
22267 );
22268
22269 editor.update_in(cx, |editor, window, cx| {
22270 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22271 });
22272
22273 let breakpoints = editor.update(cx, |editor, cx| {
22274 editor
22275 .breakpoint_store()
22276 .as_ref()
22277 .unwrap()
22278 .read(cx)
22279 .all_source_breakpoints(cx)
22280 });
22281
22282 assert_breakpoint(
22283 &breakpoints,
22284 &abs_path,
22285 vec![
22286 (0, Breakpoint::new_standard()),
22287 (3, Breakpoint::new_log("hello Earth!!")),
22288 ],
22289 );
22290}
22291
22292/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22293/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22294/// or when breakpoints were placed out of order. This tests for a regression too
22295#[gpui::test]
22296async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22297 init_test(cx, |_| {});
22298
22299 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22300 let fs = FakeFs::new(cx.executor());
22301 fs.insert_tree(
22302 path!("/a"),
22303 json!({
22304 "main.rs": sample_text,
22305 }),
22306 )
22307 .await;
22308 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22309 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22310 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22311
22312 let fs = FakeFs::new(cx.executor());
22313 fs.insert_tree(
22314 path!("/a"),
22315 json!({
22316 "main.rs": sample_text,
22317 }),
22318 )
22319 .await;
22320 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22321 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22322 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22323 let worktree_id = workspace
22324 .update(cx, |workspace, _window, cx| {
22325 workspace.project().update(cx, |project, cx| {
22326 project.worktrees(cx).next().unwrap().read(cx).id()
22327 })
22328 })
22329 .unwrap();
22330
22331 let buffer = project
22332 .update(cx, |project, cx| {
22333 project.open_buffer((worktree_id, "main.rs"), cx)
22334 })
22335 .await
22336 .unwrap();
22337
22338 let (editor, cx) = cx.add_window_view(|window, cx| {
22339 Editor::new(
22340 EditorMode::full(),
22341 MultiBuffer::build_from_buffer(buffer, cx),
22342 Some(project.clone()),
22343 window,
22344 cx,
22345 )
22346 });
22347
22348 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22349 let abs_path = project.read_with(cx, |project, cx| {
22350 project
22351 .absolute_path(&project_path, cx)
22352 .map(Arc::from)
22353 .unwrap()
22354 });
22355
22356 // assert we can add breakpoint on the first line
22357 editor.update_in(cx, |editor, window, cx| {
22358 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22359 editor.move_to_end(&MoveToEnd, window, cx);
22360 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22361 editor.move_up(&MoveUp, window, cx);
22362 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22363 });
22364
22365 let breakpoints = editor.update(cx, |editor, cx| {
22366 editor
22367 .breakpoint_store()
22368 .as_ref()
22369 .unwrap()
22370 .read(cx)
22371 .all_source_breakpoints(cx)
22372 });
22373
22374 assert_eq!(1, breakpoints.len());
22375 assert_breakpoint(
22376 &breakpoints,
22377 &abs_path,
22378 vec![
22379 (0, Breakpoint::new_standard()),
22380 (2, Breakpoint::new_standard()),
22381 (3, Breakpoint::new_standard()),
22382 ],
22383 );
22384
22385 editor.update_in(cx, |editor, window, cx| {
22386 editor.move_to_beginning(&MoveToBeginning, window, cx);
22387 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22388 editor.move_to_end(&MoveToEnd, window, cx);
22389 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22390 // Disabling a breakpoint that doesn't exist should do nothing
22391 editor.move_up(&MoveUp, window, cx);
22392 editor.move_up(&MoveUp, window, cx);
22393 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22394 });
22395
22396 let breakpoints = editor.update(cx, |editor, cx| {
22397 editor
22398 .breakpoint_store()
22399 .as_ref()
22400 .unwrap()
22401 .read(cx)
22402 .all_source_breakpoints(cx)
22403 });
22404
22405 let disable_breakpoint = {
22406 let mut bp = Breakpoint::new_standard();
22407 bp.state = BreakpointState::Disabled;
22408 bp
22409 };
22410
22411 assert_eq!(1, breakpoints.len());
22412 assert_breakpoint(
22413 &breakpoints,
22414 &abs_path,
22415 vec![
22416 (0, disable_breakpoint.clone()),
22417 (2, Breakpoint::new_standard()),
22418 (3, disable_breakpoint.clone()),
22419 ],
22420 );
22421
22422 editor.update_in(cx, |editor, window, cx| {
22423 editor.move_to_beginning(&MoveToBeginning, window, cx);
22424 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22425 editor.move_to_end(&MoveToEnd, window, cx);
22426 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22427 editor.move_up(&MoveUp, window, cx);
22428 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22429 });
22430
22431 let breakpoints = editor.update(cx, |editor, cx| {
22432 editor
22433 .breakpoint_store()
22434 .as_ref()
22435 .unwrap()
22436 .read(cx)
22437 .all_source_breakpoints(cx)
22438 });
22439
22440 assert_eq!(1, breakpoints.len());
22441 assert_breakpoint(
22442 &breakpoints,
22443 &abs_path,
22444 vec![
22445 (0, Breakpoint::new_standard()),
22446 (2, disable_breakpoint),
22447 (3, Breakpoint::new_standard()),
22448 ],
22449 );
22450}
22451
22452#[gpui::test]
22453async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22454 init_test(cx, |_| {});
22455 let capabilities = lsp::ServerCapabilities {
22456 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22457 prepare_provider: Some(true),
22458 work_done_progress_options: Default::default(),
22459 })),
22460 ..Default::default()
22461 };
22462 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22463
22464 cx.set_state(indoc! {"
22465 struct Fˇoo {}
22466 "});
22467
22468 cx.update_editor(|editor, _, cx| {
22469 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22470 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22471 editor.highlight_background::<DocumentHighlightRead>(
22472 &[highlight_range],
22473 |theme| theme.colors().editor_document_highlight_read_background,
22474 cx,
22475 );
22476 });
22477
22478 let mut prepare_rename_handler = cx
22479 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22480 move |_, _, _| async move {
22481 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22482 start: lsp::Position {
22483 line: 0,
22484 character: 7,
22485 },
22486 end: lsp::Position {
22487 line: 0,
22488 character: 10,
22489 },
22490 })))
22491 },
22492 );
22493 let prepare_rename_task = cx
22494 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22495 .expect("Prepare rename was not started");
22496 prepare_rename_handler.next().await.unwrap();
22497 prepare_rename_task.await.expect("Prepare rename failed");
22498
22499 let mut rename_handler =
22500 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22501 let edit = lsp::TextEdit {
22502 range: lsp::Range {
22503 start: lsp::Position {
22504 line: 0,
22505 character: 7,
22506 },
22507 end: lsp::Position {
22508 line: 0,
22509 character: 10,
22510 },
22511 },
22512 new_text: "FooRenamed".to_string(),
22513 };
22514 Ok(Some(lsp::WorkspaceEdit::new(
22515 // Specify the same edit twice
22516 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22517 )))
22518 });
22519 let rename_task = cx
22520 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22521 .expect("Confirm rename was not started");
22522 rename_handler.next().await.unwrap();
22523 rename_task.await.expect("Confirm rename failed");
22524 cx.run_until_parked();
22525
22526 // Despite two edits, only one is actually applied as those are identical
22527 cx.assert_editor_state(indoc! {"
22528 struct FooRenamedˇ {}
22529 "});
22530}
22531
22532#[gpui::test]
22533async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22534 init_test(cx, |_| {});
22535 // These capabilities indicate that the server does not support prepare rename.
22536 let capabilities = lsp::ServerCapabilities {
22537 rename_provider: Some(lsp::OneOf::Left(true)),
22538 ..Default::default()
22539 };
22540 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22541
22542 cx.set_state(indoc! {"
22543 struct Fˇoo {}
22544 "});
22545
22546 cx.update_editor(|editor, _window, cx| {
22547 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22548 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22549 editor.highlight_background::<DocumentHighlightRead>(
22550 &[highlight_range],
22551 |theme| theme.colors().editor_document_highlight_read_background,
22552 cx,
22553 );
22554 });
22555
22556 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22557 .expect("Prepare rename was not started")
22558 .await
22559 .expect("Prepare rename failed");
22560
22561 let mut rename_handler =
22562 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22563 let edit = lsp::TextEdit {
22564 range: lsp::Range {
22565 start: lsp::Position {
22566 line: 0,
22567 character: 7,
22568 },
22569 end: lsp::Position {
22570 line: 0,
22571 character: 10,
22572 },
22573 },
22574 new_text: "FooRenamed".to_string(),
22575 };
22576 Ok(Some(lsp::WorkspaceEdit::new(
22577 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
22578 )))
22579 });
22580 let rename_task = cx
22581 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22582 .expect("Confirm rename was not started");
22583 rename_handler.next().await.unwrap();
22584 rename_task.await.expect("Confirm rename failed");
22585 cx.run_until_parked();
22586
22587 // Correct range is renamed, as `surrounding_word` is used to find it.
22588 cx.assert_editor_state(indoc! {"
22589 struct FooRenamedˇ {}
22590 "});
22591}
22592
22593#[gpui::test]
22594async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
22595 init_test(cx, |_| {});
22596 let mut cx = EditorTestContext::new(cx).await;
22597
22598 let language = Arc::new(
22599 Language::new(
22600 LanguageConfig::default(),
22601 Some(tree_sitter_html::LANGUAGE.into()),
22602 )
22603 .with_brackets_query(
22604 r#"
22605 ("<" @open "/>" @close)
22606 ("</" @open ">" @close)
22607 ("<" @open ">" @close)
22608 ("\"" @open "\"" @close)
22609 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
22610 "#,
22611 )
22612 .unwrap(),
22613 );
22614 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22615
22616 cx.set_state(indoc! {"
22617 <span>ˇ</span>
22618 "});
22619 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22620 cx.assert_editor_state(indoc! {"
22621 <span>
22622 ˇ
22623 </span>
22624 "});
22625
22626 cx.set_state(indoc! {"
22627 <span><span></span>ˇ</span>
22628 "});
22629 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22630 cx.assert_editor_state(indoc! {"
22631 <span><span></span>
22632 ˇ</span>
22633 "});
22634
22635 cx.set_state(indoc! {"
22636 <span>ˇ
22637 </span>
22638 "});
22639 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22640 cx.assert_editor_state(indoc! {"
22641 <span>
22642 ˇ
22643 </span>
22644 "});
22645}
22646
22647#[gpui::test(iterations = 10)]
22648async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
22649 init_test(cx, |_| {});
22650
22651 let fs = FakeFs::new(cx.executor());
22652 fs.insert_tree(
22653 path!("/dir"),
22654 json!({
22655 "a.ts": "a",
22656 }),
22657 )
22658 .await;
22659
22660 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
22661 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22662 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22663
22664 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22665 language_registry.add(Arc::new(Language::new(
22666 LanguageConfig {
22667 name: "TypeScript".into(),
22668 matcher: LanguageMatcher {
22669 path_suffixes: vec!["ts".to_string()],
22670 ..Default::default()
22671 },
22672 ..Default::default()
22673 },
22674 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
22675 )));
22676 let mut fake_language_servers = language_registry.register_fake_lsp(
22677 "TypeScript",
22678 FakeLspAdapter {
22679 capabilities: lsp::ServerCapabilities {
22680 code_lens_provider: Some(lsp::CodeLensOptions {
22681 resolve_provider: Some(true),
22682 }),
22683 execute_command_provider: Some(lsp::ExecuteCommandOptions {
22684 commands: vec!["_the/command".to_string()],
22685 ..lsp::ExecuteCommandOptions::default()
22686 }),
22687 ..lsp::ServerCapabilities::default()
22688 },
22689 ..FakeLspAdapter::default()
22690 },
22691 );
22692
22693 let editor = workspace
22694 .update(cx, |workspace, window, cx| {
22695 workspace.open_abs_path(
22696 PathBuf::from(path!("/dir/a.ts")),
22697 OpenOptions::default(),
22698 window,
22699 cx,
22700 )
22701 })
22702 .unwrap()
22703 .await
22704 .unwrap()
22705 .downcast::<Editor>()
22706 .unwrap();
22707 cx.executor().run_until_parked();
22708
22709 let fake_server = fake_language_servers.next().await.unwrap();
22710
22711 let buffer = editor.update(cx, |editor, cx| {
22712 editor
22713 .buffer()
22714 .read(cx)
22715 .as_singleton()
22716 .expect("have opened a single file by path")
22717 });
22718
22719 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
22720 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
22721 drop(buffer_snapshot);
22722 let actions = cx
22723 .update_window(*workspace, |_, window, cx| {
22724 project.code_actions(&buffer, anchor..anchor, window, cx)
22725 })
22726 .unwrap();
22727
22728 fake_server
22729 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22730 Ok(Some(vec![
22731 lsp::CodeLens {
22732 range: lsp::Range::default(),
22733 command: Some(lsp::Command {
22734 title: "Code lens command".to_owned(),
22735 command: "_the/command".to_owned(),
22736 arguments: None,
22737 }),
22738 data: None,
22739 },
22740 lsp::CodeLens {
22741 range: lsp::Range::default(),
22742 command: Some(lsp::Command {
22743 title: "Command not in capabilities".to_owned(),
22744 command: "not in capabilities".to_owned(),
22745 arguments: None,
22746 }),
22747 data: None,
22748 },
22749 lsp::CodeLens {
22750 range: lsp::Range {
22751 start: lsp::Position {
22752 line: 1,
22753 character: 1,
22754 },
22755 end: lsp::Position {
22756 line: 1,
22757 character: 1,
22758 },
22759 },
22760 command: Some(lsp::Command {
22761 title: "Command not in range".to_owned(),
22762 command: "_the/command".to_owned(),
22763 arguments: None,
22764 }),
22765 data: None,
22766 },
22767 ]))
22768 })
22769 .next()
22770 .await;
22771
22772 let actions = actions.await.unwrap();
22773 assert_eq!(
22774 actions.len(),
22775 1,
22776 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
22777 );
22778 let action = actions[0].clone();
22779 let apply = project.update(cx, |project, cx| {
22780 project.apply_code_action(buffer.clone(), action, true, cx)
22781 });
22782
22783 // Resolving the code action does not populate its edits. In absence of
22784 // edits, we must execute the given command.
22785 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
22786 |mut lens, _| async move {
22787 let lens_command = lens.command.as_mut().expect("should have a command");
22788 assert_eq!(lens_command.title, "Code lens command");
22789 lens_command.arguments = Some(vec![json!("the-argument")]);
22790 Ok(lens)
22791 },
22792 );
22793
22794 // While executing the command, the language server sends the editor
22795 // a `workspaceEdit` request.
22796 fake_server
22797 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
22798 let fake = fake_server.clone();
22799 move |params, _| {
22800 assert_eq!(params.command, "_the/command");
22801 let fake = fake.clone();
22802 async move {
22803 fake.server
22804 .request::<lsp::request::ApplyWorkspaceEdit>(
22805 lsp::ApplyWorkspaceEditParams {
22806 label: None,
22807 edit: lsp::WorkspaceEdit {
22808 changes: Some(
22809 [(
22810 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
22811 vec![lsp::TextEdit {
22812 range: lsp::Range::new(
22813 lsp::Position::new(0, 0),
22814 lsp::Position::new(0, 0),
22815 ),
22816 new_text: "X".into(),
22817 }],
22818 )]
22819 .into_iter()
22820 .collect(),
22821 ),
22822 ..lsp::WorkspaceEdit::default()
22823 },
22824 },
22825 )
22826 .await
22827 .into_response()
22828 .unwrap();
22829 Ok(Some(json!(null)))
22830 }
22831 }
22832 })
22833 .next()
22834 .await;
22835
22836 // Applying the code lens command returns a project transaction containing the edits
22837 // sent by the language server in its `workspaceEdit` request.
22838 let transaction = apply.await.unwrap();
22839 assert!(transaction.0.contains_key(&buffer));
22840 buffer.update(cx, |buffer, cx| {
22841 assert_eq!(buffer.text(), "Xa");
22842 buffer.undo(cx);
22843 assert_eq!(buffer.text(), "a");
22844 });
22845
22846 let actions_after_edits = cx
22847 .update_window(*workspace, |_, window, cx| {
22848 project.code_actions(&buffer, anchor..anchor, window, cx)
22849 })
22850 .unwrap()
22851 .await
22852 .unwrap();
22853 assert_eq!(
22854 actions, actions_after_edits,
22855 "For the same selection, same code lens actions should be returned"
22856 );
22857
22858 let _responses =
22859 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22860 panic!("No more code lens requests are expected");
22861 });
22862 editor.update_in(cx, |editor, window, cx| {
22863 editor.select_all(&SelectAll, window, cx);
22864 });
22865 cx.executor().run_until_parked();
22866 let new_actions = cx
22867 .update_window(*workspace, |_, window, cx| {
22868 project.code_actions(&buffer, anchor..anchor, window, cx)
22869 })
22870 .unwrap()
22871 .await
22872 .unwrap();
22873 assert_eq!(
22874 actions, new_actions,
22875 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
22876 );
22877}
22878
22879#[gpui::test]
22880async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
22881 init_test(cx, |_| {});
22882
22883 let fs = FakeFs::new(cx.executor());
22884 let main_text = r#"fn main() {
22885println!("1");
22886println!("2");
22887println!("3");
22888println!("4");
22889println!("5");
22890}"#;
22891 let lib_text = "mod foo {}";
22892 fs.insert_tree(
22893 path!("/a"),
22894 json!({
22895 "lib.rs": lib_text,
22896 "main.rs": main_text,
22897 }),
22898 )
22899 .await;
22900
22901 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22902 let (workspace, cx) =
22903 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22904 let worktree_id = workspace.update(cx, |workspace, cx| {
22905 workspace.project().update(cx, |project, cx| {
22906 project.worktrees(cx).next().unwrap().read(cx).id()
22907 })
22908 });
22909
22910 let expected_ranges = vec![
22911 Point::new(0, 0)..Point::new(0, 0),
22912 Point::new(1, 0)..Point::new(1, 1),
22913 Point::new(2, 0)..Point::new(2, 2),
22914 Point::new(3, 0)..Point::new(3, 3),
22915 ];
22916
22917 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22918 let editor_1 = workspace
22919 .update_in(cx, |workspace, window, cx| {
22920 workspace.open_path(
22921 (worktree_id, "main.rs"),
22922 Some(pane_1.downgrade()),
22923 true,
22924 window,
22925 cx,
22926 )
22927 })
22928 .unwrap()
22929 .await
22930 .downcast::<Editor>()
22931 .unwrap();
22932 pane_1.update(cx, |pane, cx| {
22933 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22934 open_editor.update(cx, |editor, cx| {
22935 assert_eq!(
22936 editor.display_text(cx),
22937 main_text,
22938 "Original main.rs text on initial open",
22939 );
22940 assert_eq!(
22941 editor
22942 .selections
22943 .all::<Point>(cx)
22944 .into_iter()
22945 .map(|s| s.range())
22946 .collect::<Vec<_>>(),
22947 vec![Point::zero()..Point::zero()],
22948 "Default selections on initial open",
22949 );
22950 })
22951 });
22952 editor_1.update_in(cx, |editor, window, cx| {
22953 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22954 s.select_ranges(expected_ranges.clone());
22955 });
22956 });
22957
22958 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22959 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22960 });
22961 let editor_2 = workspace
22962 .update_in(cx, |workspace, window, cx| {
22963 workspace.open_path(
22964 (worktree_id, "main.rs"),
22965 Some(pane_2.downgrade()),
22966 true,
22967 window,
22968 cx,
22969 )
22970 })
22971 .unwrap()
22972 .await
22973 .downcast::<Editor>()
22974 .unwrap();
22975 pane_2.update(cx, |pane, cx| {
22976 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22977 open_editor.update(cx, |editor, cx| {
22978 assert_eq!(
22979 editor.display_text(cx),
22980 main_text,
22981 "Original main.rs text on initial open in another panel",
22982 );
22983 assert_eq!(
22984 editor
22985 .selections
22986 .all::<Point>(cx)
22987 .into_iter()
22988 .map(|s| s.range())
22989 .collect::<Vec<_>>(),
22990 vec![Point::zero()..Point::zero()],
22991 "Default selections on initial open in another panel",
22992 );
22993 })
22994 });
22995
22996 editor_2.update_in(cx, |editor, window, cx| {
22997 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
22998 });
22999
23000 let _other_editor_1 = workspace
23001 .update_in(cx, |workspace, window, cx| {
23002 workspace.open_path(
23003 (worktree_id, "lib.rs"),
23004 Some(pane_1.downgrade()),
23005 true,
23006 window,
23007 cx,
23008 )
23009 })
23010 .unwrap()
23011 .await
23012 .downcast::<Editor>()
23013 .unwrap();
23014 pane_1
23015 .update_in(cx, |pane, window, cx| {
23016 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23017 })
23018 .await
23019 .unwrap();
23020 drop(editor_1);
23021 pane_1.update(cx, |pane, cx| {
23022 pane.active_item()
23023 .unwrap()
23024 .downcast::<Editor>()
23025 .unwrap()
23026 .update(cx, |editor, cx| {
23027 assert_eq!(
23028 editor.display_text(cx),
23029 lib_text,
23030 "Other file should be open and active",
23031 );
23032 });
23033 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23034 });
23035
23036 let _other_editor_2 = workspace
23037 .update_in(cx, |workspace, window, cx| {
23038 workspace.open_path(
23039 (worktree_id, "lib.rs"),
23040 Some(pane_2.downgrade()),
23041 true,
23042 window,
23043 cx,
23044 )
23045 })
23046 .unwrap()
23047 .await
23048 .downcast::<Editor>()
23049 .unwrap();
23050 pane_2
23051 .update_in(cx, |pane, window, cx| {
23052 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23053 })
23054 .await
23055 .unwrap();
23056 drop(editor_2);
23057 pane_2.update(cx, |pane, cx| {
23058 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23059 open_editor.update(cx, |editor, cx| {
23060 assert_eq!(
23061 editor.display_text(cx),
23062 lib_text,
23063 "Other file should be open and active in another panel too",
23064 );
23065 });
23066 assert_eq!(
23067 pane.items().count(),
23068 1,
23069 "No other editors should be open in another pane",
23070 );
23071 });
23072
23073 let _editor_1_reopened = workspace
23074 .update_in(cx, |workspace, window, cx| {
23075 workspace.open_path(
23076 (worktree_id, "main.rs"),
23077 Some(pane_1.downgrade()),
23078 true,
23079 window,
23080 cx,
23081 )
23082 })
23083 .unwrap()
23084 .await
23085 .downcast::<Editor>()
23086 .unwrap();
23087 let _editor_2_reopened = workspace
23088 .update_in(cx, |workspace, window, cx| {
23089 workspace.open_path(
23090 (worktree_id, "main.rs"),
23091 Some(pane_2.downgrade()),
23092 true,
23093 window,
23094 cx,
23095 )
23096 })
23097 .unwrap()
23098 .await
23099 .downcast::<Editor>()
23100 .unwrap();
23101 pane_1.update(cx, |pane, cx| {
23102 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23103 open_editor.update(cx, |editor, cx| {
23104 assert_eq!(
23105 editor.display_text(cx),
23106 main_text,
23107 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23108 );
23109 assert_eq!(
23110 editor
23111 .selections
23112 .all::<Point>(cx)
23113 .into_iter()
23114 .map(|s| s.range())
23115 .collect::<Vec<_>>(),
23116 expected_ranges,
23117 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23118 );
23119 })
23120 });
23121 pane_2.update(cx, |pane, cx| {
23122 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23123 open_editor.update(cx, |editor, cx| {
23124 assert_eq!(
23125 editor.display_text(cx),
23126 r#"fn main() {
23127⋯rintln!("1");
23128⋯intln!("2");
23129⋯ntln!("3");
23130println!("4");
23131println!("5");
23132}"#,
23133 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23134 );
23135 assert_eq!(
23136 editor
23137 .selections
23138 .all::<Point>(cx)
23139 .into_iter()
23140 .map(|s| s.range())
23141 .collect::<Vec<_>>(),
23142 vec![Point::zero()..Point::zero()],
23143 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23144 );
23145 })
23146 });
23147}
23148
23149#[gpui::test]
23150async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23151 init_test(cx, |_| {});
23152
23153 let fs = FakeFs::new(cx.executor());
23154 let main_text = r#"fn main() {
23155println!("1");
23156println!("2");
23157println!("3");
23158println!("4");
23159println!("5");
23160}"#;
23161 let lib_text = "mod foo {}";
23162 fs.insert_tree(
23163 path!("/a"),
23164 json!({
23165 "lib.rs": lib_text,
23166 "main.rs": main_text,
23167 }),
23168 )
23169 .await;
23170
23171 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23172 let (workspace, cx) =
23173 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23174 let worktree_id = workspace.update(cx, |workspace, cx| {
23175 workspace.project().update(cx, |project, cx| {
23176 project.worktrees(cx).next().unwrap().read(cx).id()
23177 })
23178 });
23179
23180 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23181 let editor = workspace
23182 .update_in(cx, |workspace, window, cx| {
23183 workspace.open_path(
23184 (worktree_id, "main.rs"),
23185 Some(pane.downgrade()),
23186 true,
23187 window,
23188 cx,
23189 )
23190 })
23191 .unwrap()
23192 .await
23193 .downcast::<Editor>()
23194 .unwrap();
23195 pane.update(cx, |pane, cx| {
23196 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23197 open_editor.update(cx, |editor, cx| {
23198 assert_eq!(
23199 editor.display_text(cx),
23200 main_text,
23201 "Original main.rs text on initial open",
23202 );
23203 })
23204 });
23205 editor.update_in(cx, |editor, window, cx| {
23206 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23207 });
23208
23209 cx.update_global(|store: &mut SettingsStore, cx| {
23210 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23211 s.restore_on_file_reopen = Some(false);
23212 });
23213 });
23214 editor.update_in(cx, |editor, window, cx| {
23215 editor.fold_ranges(
23216 vec![
23217 Point::new(1, 0)..Point::new(1, 1),
23218 Point::new(2, 0)..Point::new(2, 2),
23219 Point::new(3, 0)..Point::new(3, 3),
23220 ],
23221 false,
23222 window,
23223 cx,
23224 );
23225 });
23226 pane.update_in(cx, |pane, window, cx| {
23227 pane.close_all_items(&CloseAllItems::default(), window, cx)
23228 })
23229 .await
23230 .unwrap();
23231 pane.update(cx, |pane, _| {
23232 assert!(pane.active_item().is_none());
23233 });
23234 cx.update_global(|store: &mut SettingsStore, cx| {
23235 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23236 s.restore_on_file_reopen = Some(true);
23237 });
23238 });
23239
23240 let _editor_reopened = workspace
23241 .update_in(cx, |workspace, window, cx| {
23242 workspace.open_path(
23243 (worktree_id, "main.rs"),
23244 Some(pane.downgrade()),
23245 true,
23246 window,
23247 cx,
23248 )
23249 })
23250 .unwrap()
23251 .await
23252 .downcast::<Editor>()
23253 .unwrap();
23254 pane.update(cx, |pane, cx| {
23255 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23256 open_editor.update(cx, |editor, cx| {
23257 assert_eq!(
23258 editor.display_text(cx),
23259 main_text,
23260 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23261 );
23262 })
23263 });
23264}
23265
23266#[gpui::test]
23267async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23268 struct EmptyModalView {
23269 focus_handle: gpui::FocusHandle,
23270 }
23271 impl EventEmitter<DismissEvent> for EmptyModalView {}
23272 impl Render for EmptyModalView {
23273 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23274 div()
23275 }
23276 }
23277 impl Focusable for EmptyModalView {
23278 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23279 self.focus_handle.clone()
23280 }
23281 }
23282 impl workspace::ModalView for EmptyModalView {}
23283 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23284 EmptyModalView {
23285 focus_handle: cx.focus_handle(),
23286 }
23287 }
23288
23289 init_test(cx, |_| {});
23290
23291 let fs = FakeFs::new(cx.executor());
23292 let project = Project::test(fs, [], cx).await;
23293 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23294 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23295 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23296 let editor = cx.new_window_entity(|window, cx| {
23297 Editor::new(
23298 EditorMode::full(),
23299 buffer,
23300 Some(project.clone()),
23301 window,
23302 cx,
23303 )
23304 });
23305 workspace
23306 .update(cx, |workspace, window, cx| {
23307 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23308 })
23309 .unwrap();
23310 editor.update_in(cx, |editor, window, cx| {
23311 editor.open_context_menu(&OpenContextMenu, window, cx);
23312 assert!(editor.mouse_context_menu.is_some());
23313 });
23314 workspace
23315 .update(cx, |workspace, window, cx| {
23316 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23317 })
23318 .unwrap();
23319 cx.read(|cx| {
23320 assert!(editor.read(cx).mouse_context_menu.is_none());
23321 });
23322}
23323
23324#[gpui::test]
23325async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23326 init_test(cx, |_| {});
23327
23328 let fs = FakeFs::new(cx.executor());
23329 fs.insert_file(path!("/file.html"), Default::default())
23330 .await;
23331
23332 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23333
23334 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23335 let html_language = Arc::new(Language::new(
23336 LanguageConfig {
23337 name: "HTML".into(),
23338 matcher: LanguageMatcher {
23339 path_suffixes: vec!["html".to_string()],
23340 ..LanguageMatcher::default()
23341 },
23342 brackets: BracketPairConfig {
23343 pairs: vec![BracketPair {
23344 start: "<".into(),
23345 end: ">".into(),
23346 close: true,
23347 ..Default::default()
23348 }],
23349 ..Default::default()
23350 },
23351 ..Default::default()
23352 },
23353 Some(tree_sitter_html::LANGUAGE.into()),
23354 ));
23355 language_registry.add(html_language);
23356 let mut fake_servers = language_registry.register_fake_lsp(
23357 "HTML",
23358 FakeLspAdapter {
23359 capabilities: lsp::ServerCapabilities {
23360 completion_provider: Some(lsp::CompletionOptions {
23361 resolve_provider: Some(true),
23362 ..Default::default()
23363 }),
23364 ..Default::default()
23365 },
23366 ..Default::default()
23367 },
23368 );
23369
23370 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23371 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23372
23373 let worktree_id = workspace
23374 .update(cx, |workspace, _window, cx| {
23375 workspace.project().update(cx, |project, cx| {
23376 project.worktrees(cx).next().unwrap().read(cx).id()
23377 })
23378 })
23379 .unwrap();
23380 project
23381 .update(cx, |project, cx| {
23382 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23383 })
23384 .await
23385 .unwrap();
23386 let editor = workspace
23387 .update(cx, |workspace, window, cx| {
23388 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23389 })
23390 .unwrap()
23391 .await
23392 .unwrap()
23393 .downcast::<Editor>()
23394 .unwrap();
23395
23396 let fake_server = fake_servers.next().await.unwrap();
23397 editor.update_in(cx, |editor, window, cx| {
23398 editor.set_text("<ad></ad>", window, cx);
23399 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23400 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23401 });
23402 let Some((buffer, _)) = editor
23403 .buffer
23404 .read(cx)
23405 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23406 else {
23407 panic!("Failed to get buffer for selection position");
23408 };
23409 let buffer = buffer.read(cx);
23410 let buffer_id = buffer.remote_id();
23411 let opening_range =
23412 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23413 let closing_range =
23414 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23415 let mut linked_ranges = HashMap::default();
23416 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23417 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23418 });
23419 let mut completion_handle =
23420 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23421 Ok(Some(lsp::CompletionResponse::Array(vec![
23422 lsp::CompletionItem {
23423 label: "head".to_string(),
23424 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23425 lsp::InsertReplaceEdit {
23426 new_text: "head".to_string(),
23427 insert: lsp::Range::new(
23428 lsp::Position::new(0, 1),
23429 lsp::Position::new(0, 3),
23430 ),
23431 replace: lsp::Range::new(
23432 lsp::Position::new(0, 1),
23433 lsp::Position::new(0, 3),
23434 ),
23435 },
23436 )),
23437 ..Default::default()
23438 },
23439 ])))
23440 });
23441 editor.update_in(cx, |editor, window, cx| {
23442 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23443 });
23444 cx.run_until_parked();
23445 completion_handle.next().await.unwrap();
23446 editor.update(cx, |editor, _| {
23447 assert!(
23448 editor.context_menu_visible(),
23449 "Completion menu should be visible"
23450 );
23451 });
23452 editor.update_in(cx, |editor, window, cx| {
23453 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23454 });
23455 cx.executor().run_until_parked();
23456 editor.update(cx, |editor, cx| {
23457 assert_eq!(editor.text(cx), "<head></head>");
23458 });
23459}
23460
23461#[gpui::test]
23462async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23463 init_test(cx, |_| {});
23464
23465 let fs = FakeFs::new(cx.executor());
23466 fs.insert_tree(
23467 path!("/root"),
23468 json!({
23469 "a": {
23470 "main.rs": "fn main() {}",
23471 },
23472 "foo": {
23473 "bar": {
23474 "external_file.rs": "pub mod external {}",
23475 }
23476 }
23477 }),
23478 )
23479 .await;
23480
23481 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23482 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23483 language_registry.add(rust_lang());
23484 let _fake_servers = language_registry.register_fake_lsp(
23485 "Rust",
23486 FakeLspAdapter {
23487 ..FakeLspAdapter::default()
23488 },
23489 );
23490 let (workspace, cx) =
23491 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23492 let worktree_id = workspace.update(cx, |workspace, cx| {
23493 workspace.project().update(cx, |project, cx| {
23494 project.worktrees(cx).next().unwrap().read(cx).id()
23495 })
23496 });
23497
23498 let assert_language_servers_count =
23499 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23500 project.update(cx, |project, cx| {
23501 let current = project
23502 .lsp_store()
23503 .read(cx)
23504 .as_local()
23505 .unwrap()
23506 .language_servers
23507 .len();
23508 assert_eq!(expected, current, "{context}");
23509 });
23510 };
23511
23512 assert_language_servers_count(
23513 0,
23514 "No servers should be running before any file is open",
23515 cx,
23516 );
23517 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23518 let main_editor = workspace
23519 .update_in(cx, |workspace, window, cx| {
23520 workspace.open_path(
23521 (worktree_id, "main.rs"),
23522 Some(pane.downgrade()),
23523 true,
23524 window,
23525 cx,
23526 )
23527 })
23528 .unwrap()
23529 .await
23530 .downcast::<Editor>()
23531 .unwrap();
23532 pane.update(cx, |pane, cx| {
23533 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23534 open_editor.update(cx, |editor, cx| {
23535 assert_eq!(
23536 editor.display_text(cx),
23537 "fn main() {}",
23538 "Original main.rs text on initial open",
23539 );
23540 });
23541 assert_eq!(open_editor, main_editor);
23542 });
23543 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
23544
23545 let external_editor = workspace
23546 .update_in(cx, |workspace, window, cx| {
23547 workspace.open_abs_path(
23548 PathBuf::from("/root/foo/bar/external_file.rs"),
23549 OpenOptions::default(),
23550 window,
23551 cx,
23552 )
23553 })
23554 .await
23555 .expect("opening external file")
23556 .downcast::<Editor>()
23557 .expect("downcasted external file's open element to editor");
23558 pane.update(cx, |pane, cx| {
23559 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23560 open_editor.update(cx, |editor, cx| {
23561 assert_eq!(
23562 editor.display_text(cx),
23563 "pub mod external {}",
23564 "External file is open now",
23565 );
23566 });
23567 assert_eq!(open_editor, external_editor);
23568 });
23569 assert_language_servers_count(
23570 1,
23571 "Second, external, *.rs file should join the existing server",
23572 cx,
23573 );
23574
23575 pane.update_in(cx, |pane, window, cx| {
23576 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23577 })
23578 .await
23579 .unwrap();
23580 pane.update_in(cx, |pane, window, cx| {
23581 pane.navigate_backward(&Default::default(), window, cx);
23582 });
23583 cx.run_until_parked();
23584 pane.update(cx, |pane, cx| {
23585 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23586 open_editor.update(cx, |editor, cx| {
23587 assert_eq!(
23588 editor.display_text(cx),
23589 "pub mod external {}",
23590 "External file is open now",
23591 );
23592 });
23593 });
23594 assert_language_servers_count(
23595 1,
23596 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
23597 cx,
23598 );
23599
23600 cx.update(|_, cx| {
23601 workspace::reload(cx);
23602 });
23603 assert_language_servers_count(
23604 1,
23605 "After reloading the worktree with local and external files opened, only one project should be started",
23606 cx,
23607 );
23608}
23609
23610#[gpui::test]
23611async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
23612 init_test(cx, |_| {});
23613
23614 let mut cx = EditorTestContext::new(cx).await;
23615 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23616 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23617
23618 // test cursor move to start of each line on tab
23619 // for `if`, `elif`, `else`, `while`, `with` and `for`
23620 cx.set_state(indoc! {"
23621 def main():
23622 ˇ for item in items:
23623 ˇ while item.active:
23624 ˇ if item.value > 10:
23625 ˇ continue
23626 ˇ elif item.value < 0:
23627 ˇ break
23628 ˇ else:
23629 ˇ with item.context() as ctx:
23630 ˇ yield count
23631 ˇ else:
23632 ˇ log('while else')
23633 ˇ else:
23634 ˇ log('for else')
23635 "});
23636 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23637 cx.assert_editor_state(indoc! {"
23638 def main():
23639 ˇfor item in items:
23640 ˇwhile item.active:
23641 ˇif item.value > 10:
23642 ˇcontinue
23643 ˇelif item.value < 0:
23644 ˇbreak
23645 ˇelse:
23646 ˇwith item.context() as ctx:
23647 ˇyield count
23648 ˇelse:
23649 ˇlog('while else')
23650 ˇelse:
23651 ˇlog('for else')
23652 "});
23653 // test relative indent is preserved when tab
23654 // for `if`, `elif`, `else`, `while`, `with` and `for`
23655 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23656 cx.assert_editor_state(indoc! {"
23657 def main():
23658 ˇfor item in items:
23659 ˇwhile item.active:
23660 ˇif item.value > 10:
23661 ˇcontinue
23662 ˇelif item.value < 0:
23663 ˇbreak
23664 ˇelse:
23665 ˇwith item.context() as ctx:
23666 ˇyield count
23667 ˇelse:
23668 ˇlog('while else')
23669 ˇelse:
23670 ˇlog('for else')
23671 "});
23672
23673 // test cursor move to start of each line on tab
23674 // for `try`, `except`, `else`, `finally`, `match` and `def`
23675 cx.set_state(indoc! {"
23676 def main():
23677 ˇ try:
23678 ˇ fetch()
23679 ˇ except ValueError:
23680 ˇ handle_error()
23681 ˇ else:
23682 ˇ match value:
23683 ˇ case _:
23684 ˇ finally:
23685 ˇ def status():
23686 ˇ return 0
23687 "});
23688 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23689 cx.assert_editor_state(indoc! {"
23690 def main():
23691 ˇtry:
23692 ˇfetch()
23693 ˇexcept ValueError:
23694 ˇhandle_error()
23695 ˇelse:
23696 ˇmatch value:
23697 ˇcase _:
23698 ˇfinally:
23699 ˇdef status():
23700 ˇreturn 0
23701 "});
23702 // test relative indent is preserved when tab
23703 // for `try`, `except`, `else`, `finally`, `match` and `def`
23704 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23705 cx.assert_editor_state(indoc! {"
23706 def main():
23707 ˇtry:
23708 ˇfetch()
23709 ˇexcept ValueError:
23710 ˇhandle_error()
23711 ˇelse:
23712 ˇmatch value:
23713 ˇcase _:
23714 ˇfinally:
23715 ˇdef status():
23716 ˇreturn 0
23717 "});
23718}
23719
23720#[gpui::test]
23721async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
23722 init_test(cx, |_| {});
23723
23724 let mut cx = EditorTestContext::new(cx).await;
23725 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23726 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23727
23728 // test `else` auto outdents when typed inside `if` block
23729 cx.set_state(indoc! {"
23730 def main():
23731 if i == 2:
23732 return
23733 ˇ
23734 "});
23735 cx.update_editor(|editor, window, cx| {
23736 editor.handle_input("else:", window, cx);
23737 });
23738 cx.assert_editor_state(indoc! {"
23739 def main():
23740 if i == 2:
23741 return
23742 else:ˇ
23743 "});
23744
23745 // test `except` auto outdents when typed inside `try` block
23746 cx.set_state(indoc! {"
23747 def main():
23748 try:
23749 i = 2
23750 ˇ
23751 "});
23752 cx.update_editor(|editor, window, cx| {
23753 editor.handle_input("except:", window, cx);
23754 });
23755 cx.assert_editor_state(indoc! {"
23756 def main():
23757 try:
23758 i = 2
23759 except:ˇ
23760 "});
23761
23762 // test `else` auto outdents when typed inside `except` block
23763 cx.set_state(indoc! {"
23764 def main():
23765 try:
23766 i = 2
23767 except:
23768 j = 2
23769 ˇ
23770 "});
23771 cx.update_editor(|editor, window, cx| {
23772 editor.handle_input("else:", window, cx);
23773 });
23774 cx.assert_editor_state(indoc! {"
23775 def main():
23776 try:
23777 i = 2
23778 except:
23779 j = 2
23780 else:ˇ
23781 "});
23782
23783 // test `finally` auto outdents when typed inside `else` block
23784 cx.set_state(indoc! {"
23785 def main():
23786 try:
23787 i = 2
23788 except:
23789 j = 2
23790 else:
23791 k = 2
23792 ˇ
23793 "});
23794 cx.update_editor(|editor, window, cx| {
23795 editor.handle_input("finally:", window, cx);
23796 });
23797 cx.assert_editor_state(indoc! {"
23798 def main():
23799 try:
23800 i = 2
23801 except:
23802 j = 2
23803 else:
23804 k = 2
23805 finally:ˇ
23806 "});
23807
23808 // test `else` does not outdents when typed inside `except` block right after for block
23809 cx.set_state(indoc! {"
23810 def main():
23811 try:
23812 i = 2
23813 except:
23814 for i in range(n):
23815 pass
23816 ˇ
23817 "});
23818 cx.update_editor(|editor, window, cx| {
23819 editor.handle_input("else:", window, cx);
23820 });
23821 cx.assert_editor_state(indoc! {"
23822 def main():
23823 try:
23824 i = 2
23825 except:
23826 for i in range(n):
23827 pass
23828 else:ˇ
23829 "});
23830
23831 // test `finally` auto outdents when typed inside `else` block right after for block
23832 cx.set_state(indoc! {"
23833 def main():
23834 try:
23835 i = 2
23836 except:
23837 j = 2
23838 else:
23839 for i in range(n):
23840 pass
23841 ˇ
23842 "});
23843 cx.update_editor(|editor, window, cx| {
23844 editor.handle_input("finally:", window, cx);
23845 });
23846 cx.assert_editor_state(indoc! {"
23847 def main():
23848 try:
23849 i = 2
23850 except:
23851 j = 2
23852 else:
23853 for i in range(n):
23854 pass
23855 finally:ˇ
23856 "});
23857
23858 // test `except` outdents to inner "try" block
23859 cx.set_state(indoc! {"
23860 def main():
23861 try:
23862 i = 2
23863 if i == 2:
23864 try:
23865 i = 3
23866 ˇ
23867 "});
23868 cx.update_editor(|editor, window, cx| {
23869 editor.handle_input("except:", window, cx);
23870 });
23871 cx.assert_editor_state(indoc! {"
23872 def main():
23873 try:
23874 i = 2
23875 if i == 2:
23876 try:
23877 i = 3
23878 except:ˇ
23879 "});
23880
23881 // test `except` outdents to outer "try" block
23882 cx.set_state(indoc! {"
23883 def main():
23884 try:
23885 i = 2
23886 if i == 2:
23887 try:
23888 i = 3
23889 ˇ
23890 "});
23891 cx.update_editor(|editor, window, cx| {
23892 editor.handle_input("except:", window, cx);
23893 });
23894 cx.assert_editor_state(indoc! {"
23895 def main():
23896 try:
23897 i = 2
23898 if i == 2:
23899 try:
23900 i = 3
23901 except:ˇ
23902 "});
23903
23904 // test `else` stays at correct indent when typed after `for` block
23905 cx.set_state(indoc! {"
23906 def main():
23907 for i in range(10):
23908 if i == 3:
23909 break
23910 ˇ
23911 "});
23912 cx.update_editor(|editor, window, cx| {
23913 editor.handle_input("else:", window, cx);
23914 });
23915 cx.assert_editor_state(indoc! {"
23916 def main():
23917 for i in range(10):
23918 if i == 3:
23919 break
23920 else:ˇ
23921 "});
23922
23923 // test does not outdent on typing after line with square brackets
23924 cx.set_state(indoc! {"
23925 def f() -> list[str]:
23926 ˇ
23927 "});
23928 cx.update_editor(|editor, window, cx| {
23929 editor.handle_input("a", window, cx);
23930 });
23931 cx.assert_editor_state(indoc! {"
23932 def f() -> list[str]:
23933 aˇ
23934 "});
23935
23936 // test does not outdent on typing : after case keyword
23937 cx.set_state(indoc! {"
23938 match 1:
23939 caseˇ
23940 "});
23941 cx.update_editor(|editor, window, cx| {
23942 editor.handle_input(":", window, cx);
23943 });
23944 cx.assert_editor_state(indoc! {"
23945 match 1:
23946 case:ˇ
23947 "});
23948}
23949
23950#[gpui::test]
23951async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23952 init_test(cx, |_| {});
23953 update_test_language_settings(cx, |settings| {
23954 settings.defaults.extend_comment_on_newline = Some(false);
23955 });
23956 let mut cx = EditorTestContext::new(cx).await;
23957 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23958 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23959
23960 // test correct indent after newline on comment
23961 cx.set_state(indoc! {"
23962 # COMMENT:ˇ
23963 "});
23964 cx.update_editor(|editor, window, cx| {
23965 editor.newline(&Newline, window, cx);
23966 });
23967 cx.assert_editor_state(indoc! {"
23968 # COMMENT:
23969 ˇ
23970 "});
23971
23972 // test correct indent after newline in brackets
23973 cx.set_state(indoc! {"
23974 {ˇ}
23975 "});
23976 cx.update_editor(|editor, window, cx| {
23977 editor.newline(&Newline, window, cx);
23978 });
23979 cx.run_until_parked();
23980 cx.assert_editor_state(indoc! {"
23981 {
23982 ˇ
23983 }
23984 "});
23985
23986 cx.set_state(indoc! {"
23987 (ˇ)
23988 "});
23989 cx.update_editor(|editor, window, cx| {
23990 editor.newline(&Newline, window, cx);
23991 });
23992 cx.run_until_parked();
23993 cx.assert_editor_state(indoc! {"
23994 (
23995 ˇ
23996 )
23997 "});
23998
23999 // do not indent after empty lists or dictionaries
24000 cx.set_state(indoc! {"
24001 a = []ˇ
24002 "});
24003 cx.update_editor(|editor, window, cx| {
24004 editor.newline(&Newline, window, cx);
24005 });
24006 cx.run_until_parked();
24007 cx.assert_editor_state(indoc! {"
24008 a = []
24009 ˇ
24010 "});
24011}
24012
24013#[gpui::test]
24014async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24015 init_test(cx, |_| {});
24016
24017 let mut cx = EditorTestContext::new(cx).await;
24018 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24019 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24020
24021 // test cursor move to start of each line on tab
24022 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24023 cx.set_state(indoc! {"
24024 function main() {
24025 ˇ for item in $items; do
24026 ˇ while [ -n \"$item\" ]; do
24027 ˇ if [ \"$value\" -gt 10 ]; then
24028 ˇ continue
24029 ˇ elif [ \"$value\" -lt 0 ]; then
24030 ˇ break
24031 ˇ else
24032 ˇ echo \"$item\"
24033 ˇ fi
24034 ˇ done
24035 ˇ done
24036 ˇ}
24037 "});
24038 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24039 cx.assert_editor_state(indoc! {"
24040 function main() {
24041 ˇfor item in $items; do
24042 ˇwhile [ -n \"$item\" ]; do
24043 ˇif [ \"$value\" -gt 10 ]; then
24044 ˇcontinue
24045 ˇelif [ \"$value\" -lt 0 ]; then
24046 ˇbreak
24047 ˇelse
24048 ˇecho \"$item\"
24049 ˇfi
24050 ˇdone
24051 ˇdone
24052 ˇ}
24053 "});
24054 // test relative indent is preserved when tab
24055 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24056 cx.assert_editor_state(indoc! {"
24057 function main() {
24058 ˇfor item in $items; do
24059 ˇwhile [ -n \"$item\" ]; do
24060 ˇif [ \"$value\" -gt 10 ]; then
24061 ˇcontinue
24062 ˇelif [ \"$value\" -lt 0 ]; then
24063 ˇbreak
24064 ˇelse
24065 ˇecho \"$item\"
24066 ˇfi
24067 ˇdone
24068 ˇdone
24069 ˇ}
24070 "});
24071
24072 // test cursor move to start of each line on tab
24073 // for `case` statement with patterns
24074 cx.set_state(indoc! {"
24075 function handle() {
24076 ˇ case \"$1\" in
24077 ˇ start)
24078 ˇ echo \"a\"
24079 ˇ ;;
24080 ˇ stop)
24081 ˇ echo \"b\"
24082 ˇ ;;
24083 ˇ *)
24084 ˇ echo \"c\"
24085 ˇ ;;
24086 ˇ esac
24087 ˇ}
24088 "});
24089 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24090 cx.assert_editor_state(indoc! {"
24091 function handle() {
24092 ˇcase \"$1\" in
24093 ˇstart)
24094 ˇecho \"a\"
24095 ˇ;;
24096 ˇstop)
24097 ˇecho \"b\"
24098 ˇ;;
24099 ˇ*)
24100 ˇecho \"c\"
24101 ˇ;;
24102 ˇesac
24103 ˇ}
24104 "});
24105}
24106
24107#[gpui::test]
24108async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24109 init_test(cx, |_| {});
24110
24111 let mut cx = EditorTestContext::new(cx).await;
24112 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24113 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24114
24115 // test indents on comment insert
24116 cx.set_state(indoc! {"
24117 function main() {
24118 ˇ for item in $items; do
24119 ˇ while [ -n \"$item\" ]; do
24120 ˇ if [ \"$value\" -gt 10 ]; then
24121 ˇ continue
24122 ˇ elif [ \"$value\" -lt 0 ]; then
24123 ˇ break
24124 ˇ else
24125 ˇ echo \"$item\"
24126 ˇ fi
24127 ˇ done
24128 ˇ done
24129 ˇ}
24130 "});
24131 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24132 cx.assert_editor_state(indoc! {"
24133 function main() {
24134 #ˇ for item in $items; do
24135 #ˇ while [ -n \"$item\" ]; do
24136 #ˇ if [ \"$value\" -gt 10 ]; then
24137 #ˇ continue
24138 #ˇ elif [ \"$value\" -lt 0 ]; then
24139 #ˇ break
24140 #ˇ else
24141 #ˇ echo \"$item\"
24142 #ˇ fi
24143 #ˇ done
24144 #ˇ done
24145 #ˇ}
24146 "});
24147}
24148
24149#[gpui::test]
24150async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24151 init_test(cx, |_| {});
24152
24153 let mut cx = EditorTestContext::new(cx).await;
24154 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24155 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24156
24157 // test `else` auto outdents when typed inside `if` block
24158 cx.set_state(indoc! {"
24159 if [ \"$1\" = \"test\" ]; then
24160 echo \"foo bar\"
24161 ˇ
24162 "});
24163 cx.update_editor(|editor, window, cx| {
24164 editor.handle_input("else", window, cx);
24165 });
24166 cx.assert_editor_state(indoc! {"
24167 if [ \"$1\" = \"test\" ]; then
24168 echo \"foo bar\"
24169 elseˇ
24170 "});
24171
24172 // test `elif` auto outdents when typed inside `if` block
24173 cx.set_state(indoc! {"
24174 if [ \"$1\" = \"test\" ]; then
24175 echo \"foo bar\"
24176 ˇ
24177 "});
24178 cx.update_editor(|editor, window, cx| {
24179 editor.handle_input("elif", window, cx);
24180 });
24181 cx.assert_editor_state(indoc! {"
24182 if [ \"$1\" = \"test\" ]; then
24183 echo \"foo bar\"
24184 elifˇ
24185 "});
24186
24187 // test `fi` auto outdents when typed inside `else` block
24188 cx.set_state(indoc! {"
24189 if [ \"$1\" = \"test\" ]; then
24190 echo \"foo bar\"
24191 else
24192 echo \"bar baz\"
24193 ˇ
24194 "});
24195 cx.update_editor(|editor, window, cx| {
24196 editor.handle_input("fi", window, cx);
24197 });
24198 cx.assert_editor_state(indoc! {"
24199 if [ \"$1\" = \"test\" ]; then
24200 echo \"foo bar\"
24201 else
24202 echo \"bar baz\"
24203 fiˇ
24204 "});
24205
24206 // test `done` auto outdents when typed inside `while` block
24207 cx.set_state(indoc! {"
24208 while read line; do
24209 echo \"$line\"
24210 ˇ
24211 "});
24212 cx.update_editor(|editor, window, cx| {
24213 editor.handle_input("done", window, cx);
24214 });
24215 cx.assert_editor_state(indoc! {"
24216 while read line; do
24217 echo \"$line\"
24218 doneˇ
24219 "});
24220
24221 // test `done` auto outdents when typed inside `for` block
24222 cx.set_state(indoc! {"
24223 for file in *.txt; do
24224 cat \"$file\"
24225 ˇ
24226 "});
24227 cx.update_editor(|editor, window, cx| {
24228 editor.handle_input("done", window, cx);
24229 });
24230 cx.assert_editor_state(indoc! {"
24231 for file in *.txt; do
24232 cat \"$file\"
24233 doneˇ
24234 "});
24235
24236 // test `esac` auto outdents when typed inside `case` block
24237 cx.set_state(indoc! {"
24238 case \"$1\" in
24239 start)
24240 echo \"foo bar\"
24241 ;;
24242 stop)
24243 echo \"bar baz\"
24244 ;;
24245 ˇ
24246 "});
24247 cx.update_editor(|editor, window, cx| {
24248 editor.handle_input("esac", window, cx);
24249 });
24250 cx.assert_editor_state(indoc! {"
24251 case \"$1\" in
24252 start)
24253 echo \"foo bar\"
24254 ;;
24255 stop)
24256 echo \"bar baz\"
24257 ;;
24258 esacˇ
24259 "});
24260
24261 // test `*)` auto outdents when typed inside `case` block
24262 cx.set_state(indoc! {"
24263 case \"$1\" in
24264 start)
24265 echo \"foo bar\"
24266 ;;
24267 ˇ
24268 "});
24269 cx.update_editor(|editor, window, cx| {
24270 editor.handle_input("*)", window, cx);
24271 });
24272 cx.assert_editor_state(indoc! {"
24273 case \"$1\" in
24274 start)
24275 echo \"foo bar\"
24276 ;;
24277 *)ˇ
24278 "});
24279
24280 // test `fi` outdents to correct level with nested if blocks
24281 cx.set_state(indoc! {"
24282 if [ \"$1\" = \"test\" ]; then
24283 echo \"outer if\"
24284 if [ \"$2\" = \"debug\" ]; then
24285 echo \"inner if\"
24286 ˇ
24287 "});
24288 cx.update_editor(|editor, window, cx| {
24289 editor.handle_input("fi", window, cx);
24290 });
24291 cx.assert_editor_state(indoc! {"
24292 if [ \"$1\" = \"test\" ]; then
24293 echo \"outer if\"
24294 if [ \"$2\" = \"debug\" ]; then
24295 echo \"inner if\"
24296 fiˇ
24297 "});
24298}
24299
24300#[gpui::test]
24301async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24302 init_test(cx, |_| {});
24303 update_test_language_settings(cx, |settings| {
24304 settings.defaults.extend_comment_on_newline = Some(false);
24305 });
24306 let mut cx = EditorTestContext::new(cx).await;
24307 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24308 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24309
24310 // test correct indent after newline on comment
24311 cx.set_state(indoc! {"
24312 # COMMENT:ˇ
24313 "});
24314 cx.update_editor(|editor, window, cx| {
24315 editor.newline(&Newline, window, cx);
24316 });
24317 cx.assert_editor_state(indoc! {"
24318 # COMMENT:
24319 ˇ
24320 "});
24321
24322 // test correct indent after newline after `then`
24323 cx.set_state(indoc! {"
24324
24325 if [ \"$1\" = \"test\" ]; thenˇ
24326 "});
24327 cx.update_editor(|editor, window, cx| {
24328 editor.newline(&Newline, window, cx);
24329 });
24330 cx.run_until_parked();
24331 cx.assert_editor_state(indoc! {"
24332
24333 if [ \"$1\" = \"test\" ]; then
24334 ˇ
24335 "});
24336
24337 // test correct indent after newline after `else`
24338 cx.set_state(indoc! {"
24339 if [ \"$1\" = \"test\" ]; then
24340 elseˇ
24341 "});
24342 cx.update_editor(|editor, window, cx| {
24343 editor.newline(&Newline, window, cx);
24344 });
24345 cx.run_until_parked();
24346 cx.assert_editor_state(indoc! {"
24347 if [ \"$1\" = \"test\" ]; then
24348 else
24349 ˇ
24350 "});
24351
24352 // test correct indent after newline after `elif`
24353 cx.set_state(indoc! {"
24354 if [ \"$1\" = \"test\" ]; then
24355 elifˇ
24356 "});
24357 cx.update_editor(|editor, window, cx| {
24358 editor.newline(&Newline, window, cx);
24359 });
24360 cx.run_until_parked();
24361 cx.assert_editor_state(indoc! {"
24362 if [ \"$1\" = \"test\" ]; then
24363 elif
24364 ˇ
24365 "});
24366
24367 // test correct indent after newline after `do`
24368 cx.set_state(indoc! {"
24369 for file in *.txt; doˇ
24370 "});
24371 cx.update_editor(|editor, window, cx| {
24372 editor.newline(&Newline, window, cx);
24373 });
24374 cx.run_until_parked();
24375 cx.assert_editor_state(indoc! {"
24376 for file in *.txt; do
24377 ˇ
24378 "});
24379
24380 // test correct indent after newline after case pattern
24381 cx.set_state(indoc! {"
24382 case \"$1\" in
24383 start)ˇ
24384 "});
24385 cx.update_editor(|editor, window, cx| {
24386 editor.newline(&Newline, window, cx);
24387 });
24388 cx.run_until_parked();
24389 cx.assert_editor_state(indoc! {"
24390 case \"$1\" in
24391 start)
24392 ˇ
24393 "});
24394
24395 // test correct indent after newline after case pattern
24396 cx.set_state(indoc! {"
24397 case \"$1\" in
24398 start)
24399 ;;
24400 *)ˇ
24401 "});
24402 cx.update_editor(|editor, window, cx| {
24403 editor.newline(&Newline, window, cx);
24404 });
24405 cx.run_until_parked();
24406 cx.assert_editor_state(indoc! {"
24407 case \"$1\" in
24408 start)
24409 ;;
24410 *)
24411 ˇ
24412 "});
24413
24414 // test correct indent after newline after function opening brace
24415 cx.set_state(indoc! {"
24416 function test() {ˇ}
24417 "});
24418 cx.update_editor(|editor, window, cx| {
24419 editor.newline(&Newline, window, cx);
24420 });
24421 cx.run_until_parked();
24422 cx.assert_editor_state(indoc! {"
24423 function test() {
24424 ˇ
24425 }
24426 "});
24427
24428 // test no extra indent after semicolon on same line
24429 cx.set_state(indoc! {"
24430 echo \"test\";ˇ
24431 "});
24432 cx.update_editor(|editor, window, cx| {
24433 editor.newline(&Newline, window, cx);
24434 });
24435 cx.run_until_parked();
24436 cx.assert_editor_state(indoc! {"
24437 echo \"test\";
24438 ˇ
24439 "});
24440}
24441
24442fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24443 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24444 point..point
24445}
24446
24447#[track_caller]
24448fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24449 let (text, ranges) = marked_text_ranges(marked_text, true);
24450 assert_eq!(editor.text(cx), text);
24451 assert_eq!(
24452 editor.selections.ranges(cx),
24453 ranges,
24454 "Assert selections are {}",
24455 marked_text
24456 );
24457}
24458
24459pub fn handle_signature_help_request(
24460 cx: &mut EditorLspTestContext,
24461 mocked_response: lsp::SignatureHelp,
24462) -> impl Future<Output = ()> + use<> {
24463 let mut request =
24464 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24465 let mocked_response = mocked_response.clone();
24466 async move { Ok(Some(mocked_response)) }
24467 });
24468
24469 async move {
24470 request.next().await;
24471 }
24472}
24473
24474#[track_caller]
24475pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24476 cx.update_editor(|editor, _, _| {
24477 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24478 let entries = menu.entries.borrow();
24479 let entries = entries
24480 .iter()
24481 .map(|entry| entry.string.as_str())
24482 .collect::<Vec<_>>();
24483 assert_eq!(entries, expected);
24484 } else {
24485 panic!("Expected completions menu");
24486 }
24487 });
24488}
24489
24490/// Handle completion request passing a marked string specifying where the completion
24491/// should be triggered from using '|' character, what range should be replaced, and what completions
24492/// should be returned using '<' and '>' to delimit the range.
24493///
24494/// Also see `handle_completion_request_with_insert_and_replace`.
24495#[track_caller]
24496pub fn handle_completion_request(
24497 marked_string: &str,
24498 completions: Vec<&'static str>,
24499 is_incomplete: bool,
24500 counter: Arc<AtomicUsize>,
24501 cx: &mut EditorLspTestContext,
24502) -> impl Future<Output = ()> {
24503 let complete_from_marker: TextRangeMarker = '|'.into();
24504 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24505 let (_, mut marked_ranges) = marked_text_ranges_by(
24506 marked_string,
24507 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24508 );
24509
24510 let complete_from_position =
24511 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24512 let replace_range =
24513 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24514
24515 let mut request =
24516 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24517 let completions = completions.clone();
24518 counter.fetch_add(1, atomic::Ordering::Release);
24519 async move {
24520 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24521 assert_eq!(
24522 params.text_document_position.position,
24523 complete_from_position
24524 );
24525 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24526 is_incomplete,
24527 item_defaults: None,
24528 items: completions
24529 .iter()
24530 .map(|completion_text| lsp::CompletionItem {
24531 label: completion_text.to_string(),
24532 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24533 range: replace_range,
24534 new_text: completion_text.to_string(),
24535 })),
24536 ..Default::default()
24537 })
24538 .collect(),
24539 })))
24540 }
24541 });
24542
24543 async move {
24544 request.next().await;
24545 }
24546}
24547
24548/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
24549/// given instead, which also contains an `insert` range.
24550///
24551/// This function uses markers to define ranges:
24552/// - `|` marks the cursor position
24553/// - `<>` marks the replace range
24554/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
24555pub fn handle_completion_request_with_insert_and_replace(
24556 cx: &mut EditorLspTestContext,
24557 marked_string: &str,
24558 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
24559 counter: Arc<AtomicUsize>,
24560) -> impl Future<Output = ()> {
24561 let complete_from_marker: TextRangeMarker = '|'.into();
24562 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24563 let insert_range_marker: TextRangeMarker = ('{', '}').into();
24564
24565 let (_, mut marked_ranges) = marked_text_ranges_by(
24566 marked_string,
24567 vec![
24568 complete_from_marker.clone(),
24569 replace_range_marker.clone(),
24570 insert_range_marker.clone(),
24571 ],
24572 );
24573
24574 let complete_from_position =
24575 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24576 let replace_range =
24577 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24578
24579 let insert_range = match marked_ranges.remove(&insert_range_marker) {
24580 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
24581 _ => lsp::Range {
24582 start: replace_range.start,
24583 end: complete_from_position,
24584 },
24585 };
24586
24587 let mut request =
24588 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24589 let completions = completions.clone();
24590 counter.fetch_add(1, atomic::Ordering::Release);
24591 async move {
24592 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24593 assert_eq!(
24594 params.text_document_position.position, complete_from_position,
24595 "marker `|` position doesn't match",
24596 );
24597 Ok(Some(lsp::CompletionResponse::Array(
24598 completions
24599 .iter()
24600 .map(|(label, new_text)| lsp::CompletionItem {
24601 label: label.to_string(),
24602 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24603 lsp::InsertReplaceEdit {
24604 insert: insert_range,
24605 replace: replace_range,
24606 new_text: new_text.to_string(),
24607 },
24608 )),
24609 ..Default::default()
24610 })
24611 .collect(),
24612 )))
24613 }
24614 });
24615
24616 async move {
24617 request.next().await;
24618 }
24619}
24620
24621fn handle_resolve_completion_request(
24622 cx: &mut EditorLspTestContext,
24623 edits: Option<Vec<(&'static str, &'static str)>>,
24624) -> impl Future<Output = ()> {
24625 let edits = edits.map(|edits| {
24626 edits
24627 .iter()
24628 .map(|(marked_string, new_text)| {
24629 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
24630 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
24631 lsp::TextEdit::new(replace_range, new_text.to_string())
24632 })
24633 .collect::<Vec<_>>()
24634 });
24635
24636 let mut request =
24637 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
24638 let edits = edits.clone();
24639 async move {
24640 Ok(lsp::CompletionItem {
24641 additional_text_edits: edits,
24642 ..Default::default()
24643 })
24644 }
24645 });
24646
24647 async move {
24648 request.next().await;
24649 }
24650}
24651
24652pub(crate) fn update_test_language_settings(
24653 cx: &mut TestAppContext,
24654 f: impl Fn(&mut AllLanguageSettingsContent),
24655) {
24656 cx.update(|cx| {
24657 SettingsStore::update_global(cx, |store, cx| {
24658 store.update_user_settings::<AllLanguageSettings>(cx, f);
24659 });
24660 });
24661}
24662
24663pub(crate) fn update_test_project_settings(
24664 cx: &mut TestAppContext,
24665 f: impl Fn(&mut ProjectSettings),
24666) {
24667 cx.update(|cx| {
24668 SettingsStore::update_global(cx, |store, cx| {
24669 store.update_user_settings::<ProjectSettings>(cx, f);
24670 });
24671 });
24672}
24673
24674pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
24675 cx.update(|cx| {
24676 assets::Assets.load_test_fonts(cx);
24677 let store = SettingsStore::test(cx);
24678 cx.set_global(store);
24679 theme::init(theme::LoadThemes::JustBase, cx);
24680 release_channel::init(SemanticVersion::default(), cx);
24681 client::init_settings(cx);
24682 language::init(cx);
24683 Project::init_settings(cx);
24684 workspace::init_settings(cx);
24685 crate::init(cx);
24686 });
24687 zlog::init_test();
24688 update_test_language_settings(cx, f);
24689}
24690
24691#[track_caller]
24692fn assert_hunk_revert(
24693 not_reverted_text_with_selections: &str,
24694 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
24695 expected_reverted_text_with_selections: &str,
24696 base_text: &str,
24697 cx: &mut EditorLspTestContext,
24698) {
24699 cx.set_state(not_reverted_text_with_selections);
24700 cx.set_head_text(base_text);
24701 cx.executor().run_until_parked();
24702
24703 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
24704 let snapshot = editor.snapshot(window, cx);
24705 let reverted_hunk_statuses = snapshot
24706 .buffer_snapshot
24707 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
24708 .map(|hunk| hunk.status().kind)
24709 .collect::<Vec<_>>();
24710
24711 editor.git_restore(&Default::default(), window, cx);
24712 reverted_hunk_statuses
24713 });
24714 cx.executor().run_until_parked();
24715 cx.assert_editor_state(expected_reverted_text_with_selections);
24716 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
24717}
24718
24719#[gpui::test(iterations = 10)]
24720async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
24721 init_test(cx, |_| {});
24722
24723 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
24724 let counter = diagnostic_requests.clone();
24725
24726 let fs = FakeFs::new(cx.executor());
24727 fs.insert_tree(
24728 path!("/a"),
24729 json!({
24730 "first.rs": "fn main() { let a = 5; }",
24731 "second.rs": "// Test file",
24732 }),
24733 )
24734 .await;
24735
24736 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24737 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24738 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24739
24740 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24741 language_registry.add(rust_lang());
24742 let mut fake_servers = language_registry.register_fake_lsp(
24743 "Rust",
24744 FakeLspAdapter {
24745 capabilities: lsp::ServerCapabilities {
24746 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
24747 lsp::DiagnosticOptions {
24748 identifier: None,
24749 inter_file_dependencies: true,
24750 workspace_diagnostics: true,
24751 work_done_progress_options: Default::default(),
24752 },
24753 )),
24754 ..Default::default()
24755 },
24756 ..Default::default()
24757 },
24758 );
24759
24760 let editor = workspace
24761 .update(cx, |workspace, window, cx| {
24762 workspace.open_abs_path(
24763 PathBuf::from(path!("/a/first.rs")),
24764 OpenOptions::default(),
24765 window,
24766 cx,
24767 )
24768 })
24769 .unwrap()
24770 .await
24771 .unwrap()
24772 .downcast::<Editor>()
24773 .unwrap();
24774 let fake_server = fake_servers.next().await.unwrap();
24775 let server_id = fake_server.server.server_id();
24776 let mut first_request = fake_server
24777 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
24778 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
24779 let result_id = Some(new_result_id.to_string());
24780 assert_eq!(
24781 params.text_document.uri,
24782 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24783 );
24784 async move {
24785 Ok(lsp::DocumentDiagnosticReportResult::Report(
24786 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
24787 related_documents: None,
24788 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
24789 items: Vec::new(),
24790 result_id,
24791 },
24792 }),
24793 ))
24794 }
24795 });
24796
24797 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
24798 project.update(cx, |project, cx| {
24799 let buffer_id = editor
24800 .read(cx)
24801 .buffer()
24802 .read(cx)
24803 .as_singleton()
24804 .expect("created a singleton buffer")
24805 .read(cx)
24806 .remote_id();
24807 let buffer_result_id = project
24808 .lsp_store()
24809 .read(cx)
24810 .result_id(server_id, buffer_id, cx);
24811 assert_eq!(expected, buffer_result_id);
24812 });
24813 };
24814
24815 ensure_result_id(None, cx);
24816 cx.executor().advance_clock(Duration::from_millis(60));
24817 cx.executor().run_until_parked();
24818 assert_eq!(
24819 diagnostic_requests.load(atomic::Ordering::Acquire),
24820 1,
24821 "Opening file should trigger diagnostic request"
24822 );
24823 first_request
24824 .next()
24825 .await
24826 .expect("should have sent the first diagnostics pull request");
24827 ensure_result_id(Some("1".to_string()), cx);
24828
24829 // Editing should trigger diagnostics
24830 editor.update_in(cx, |editor, window, cx| {
24831 editor.handle_input("2", window, cx)
24832 });
24833 cx.executor().advance_clock(Duration::from_millis(60));
24834 cx.executor().run_until_parked();
24835 assert_eq!(
24836 diagnostic_requests.load(atomic::Ordering::Acquire),
24837 2,
24838 "Editing should trigger diagnostic request"
24839 );
24840 ensure_result_id(Some("2".to_string()), cx);
24841
24842 // Moving cursor should not trigger diagnostic request
24843 editor.update_in(cx, |editor, window, cx| {
24844 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24845 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
24846 });
24847 });
24848 cx.executor().advance_clock(Duration::from_millis(60));
24849 cx.executor().run_until_parked();
24850 assert_eq!(
24851 diagnostic_requests.load(atomic::Ordering::Acquire),
24852 2,
24853 "Cursor movement should not trigger diagnostic request"
24854 );
24855 ensure_result_id(Some("2".to_string()), cx);
24856 // Multiple rapid edits should be debounced
24857 for _ in 0..5 {
24858 editor.update_in(cx, |editor, window, cx| {
24859 editor.handle_input("x", window, cx)
24860 });
24861 }
24862 cx.executor().advance_clock(Duration::from_millis(60));
24863 cx.executor().run_until_parked();
24864
24865 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
24866 assert!(
24867 final_requests <= 4,
24868 "Multiple rapid edits should be debounced (got {final_requests} requests)",
24869 );
24870 ensure_result_id(Some(final_requests.to_string()), cx);
24871}
24872
24873#[gpui::test]
24874async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
24875 // Regression test for issue #11671
24876 // Previously, adding a cursor after moving multiple cursors would reset
24877 // the cursor count instead of adding to the existing cursors.
24878 init_test(cx, |_| {});
24879 let mut cx = EditorTestContext::new(cx).await;
24880
24881 // Create a simple buffer with cursor at start
24882 cx.set_state(indoc! {"
24883 ˇaaaa
24884 bbbb
24885 cccc
24886 dddd
24887 eeee
24888 ffff
24889 gggg
24890 hhhh"});
24891
24892 // Add 2 cursors below (so we have 3 total)
24893 cx.update_editor(|editor, window, cx| {
24894 editor.add_selection_below(&Default::default(), window, cx);
24895 editor.add_selection_below(&Default::default(), window, cx);
24896 });
24897
24898 // Verify we have 3 cursors
24899 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24900 assert_eq!(
24901 initial_count, 3,
24902 "Should have 3 cursors after adding 2 below"
24903 );
24904
24905 // Move down one line
24906 cx.update_editor(|editor, window, cx| {
24907 editor.move_down(&MoveDown, window, cx);
24908 });
24909
24910 // Add another cursor below
24911 cx.update_editor(|editor, window, cx| {
24912 editor.add_selection_below(&Default::default(), window, cx);
24913 });
24914
24915 // Should now have 4 cursors (3 original + 1 new)
24916 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24917 assert_eq!(
24918 final_count, 4,
24919 "Should have 4 cursors after moving and adding another"
24920 );
24921}
24922
24923#[gpui::test(iterations = 10)]
24924async fn test_document_colors(cx: &mut TestAppContext) {
24925 let expected_color = Rgba {
24926 r: 0.33,
24927 g: 0.33,
24928 b: 0.33,
24929 a: 0.33,
24930 };
24931
24932 init_test(cx, |_| {});
24933
24934 let fs = FakeFs::new(cx.executor());
24935 fs.insert_tree(
24936 path!("/a"),
24937 json!({
24938 "first.rs": "fn main() { let a = 5; }",
24939 }),
24940 )
24941 .await;
24942
24943 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24944 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24945 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24946
24947 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24948 language_registry.add(rust_lang());
24949 let mut fake_servers = language_registry.register_fake_lsp(
24950 "Rust",
24951 FakeLspAdapter {
24952 capabilities: lsp::ServerCapabilities {
24953 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24954 ..lsp::ServerCapabilities::default()
24955 },
24956 name: "rust-analyzer",
24957 ..FakeLspAdapter::default()
24958 },
24959 );
24960 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24961 "Rust",
24962 FakeLspAdapter {
24963 capabilities: lsp::ServerCapabilities {
24964 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24965 ..lsp::ServerCapabilities::default()
24966 },
24967 name: "not-rust-analyzer",
24968 ..FakeLspAdapter::default()
24969 },
24970 );
24971
24972 let editor = workspace
24973 .update(cx, |workspace, window, cx| {
24974 workspace.open_abs_path(
24975 PathBuf::from(path!("/a/first.rs")),
24976 OpenOptions::default(),
24977 window,
24978 cx,
24979 )
24980 })
24981 .unwrap()
24982 .await
24983 .unwrap()
24984 .downcast::<Editor>()
24985 .unwrap();
24986 let fake_language_server = fake_servers.next().await.unwrap();
24987 let fake_language_server_without_capabilities =
24988 fake_servers_without_capabilities.next().await.unwrap();
24989 let requests_made = Arc::new(AtomicUsize::new(0));
24990 let closure_requests_made = Arc::clone(&requests_made);
24991 let mut color_request_handle = fake_language_server
24992 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
24993 let requests_made = Arc::clone(&closure_requests_made);
24994 async move {
24995 assert_eq!(
24996 params.text_document.uri,
24997 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24998 );
24999 requests_made.fetch_add(1, atomic::Ordering::Release);
25000 Ok(vec![
25001 lsp::ColorInformation {
25002 range: lsp::Range {
25003 start: lsp::Position {
25004 line: 0,
25005 character: 0,
25006 },
25007 end: lsp::Position {
25008 line: 0,
25009 character: 1,
25010 },
25011 },
25012 color: lsp::Color {
25013 red: 0.33,
25014 green: 0.33,
25015 blue: 0.33,
25016 alpha: 0.33,
25017 },
25018 },
25019 lsp::ColorInformation {
25020 range: lsp::Range {
25021 start: lsp::Position {
25022 line: 0,
25023 character: 0,
25024 },
25025 end: lsp::Position {
25026 line: 0,
25027 character: 1,
25028 },
25029 },
25030 color: lsp::Color {
25031 red: 0.33,
25032 green: 0.33,
25033 blue: 0.33,
25034 alpha: 0.33,
25035 },
25036 },
25037 ])
25038 }
25039 });
25040
25041 let _handle = fake_language_server_without_capabilities
25042 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25043 panic!("Should not be called");
25044 });
25045 cx.executor().advance_clock(Duration::from_millis(100));
25046 color_request_handle.next().await.unwrap();
25047 cx.run_until_parked();
25048 assert_eq!(
25049 1,
25050 requests_made.load(atomic::Ordering::Acquire),
25051 "Should query for colors once per editor open"
25052 );
25053 editor.update_in(cx, |editor, _, cx| {
25054 assert_eq!(
25055 vec![expected_color],
25056 extract_color_inlays(editor, cx),
25057 "Should have an initial inlay"
25058 );
25059 });
25060
25061 // opening another file in a split should not influence the LSP query counter
25062 workspace
25063 .update(cx, |workspace, window, cx| {
25064 assert_eq!(
25065 workspace.panes().len(),
25066 1,
25067 "Should have one pane with one editor"
25068 );
25069 workspace.move_item_to_pane_in_direction(
25070 &MoveItemToPaneInDirection {
25071 direction: SplitDirection::Right,
25072 focus: false,
25073 clone: true,
25074 },
25075 window,
25076 cx,
25077 );
25078 })
25079 .unwrap();
25080 cx.run_until_parked();
25081 workspace
25082 .update(cx, |workspace, _, cx| {
25083 let panes = workspace.panes();
25084 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25085 for pane in panes {
25086 let editor = pane
25087 .read(cx)
25088 .active_item()
25089 .and_then(|item| item.downcast::<Editor>())
25090 .expect("Should have opened an editor in each split");
25091 let editor_file = editor
25092 .read(cx)
25093 .buffer()
25094 .read(cx)
25095 .as_singleton()
25096 .expect("test deals with singleton buffers")
25097 .read(cx)
25098 .file()
25099 .expect("test buffese should have a file")
25100 .path();
25101 assert_eq!(
25102 editor_file.as_ref(),
25103 Path::new("first.rs"),
25104 "Both editors should be opened for the same file"
25105 )
25106 }
25107 })
25108 .unwrap();
25109
25110 cx.executor().advance_clock(Duration::from_millis(500));
25111 let save = editor.update_in(cx, |editor, window, cx| {
25112 editor.move_to_end(&MoveToEnd, window, cx);
25113 editor.handle_input("dirty", window, cx);
25114 editor.save(
25115 SaveOptions {
25116 format: true,
25117 autosave: true,
25118 },
25119 project.clone(),
25120 window,
25121 cx,
25122 )
25123 });
25124 save.await.unwrap();
25125
25126 color_request_handle.next().await.unwrap();
25127 cx.run_until_parked();
25128 assert_eq!(
25129 3,
25130 requests_made.load(atomic::Ordering::Acquire),
25131 "Should query for colors once per save and once per formatting after save"
25132 );
25133
25134 drop(editor);
25135 let close = workspace
25136 .update(cx, |workspace, window, cx| {
25137 workspace.active_pane().update(cx, |pane, cx| {
25138 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25139 })
25140 })
25141 .unwrap();
25142 close.await.unwrap();
25143 let close = workspace
25144 .update(cx, |workspace, window, cx| {
25145 workspace.active_pane().update(cx, |pane, cx| {
25146 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25147 })
25148 })
25149 .unwrap();
25150 close.await.unwrap();
25151 assert_eq!(
25152 3,
25153 requests_made.load(atomic::Ordering::Acquire),
25154 "After saving and closing all editors, no extra requests should be made"
25155 );
25156 workspace
25157 .update(cx, |workspace, _, cx| {
25158 assert!(
25159 workspace.active_item(cx).is_none(),
25160 "Should close all editors"
25161 )
25162 })
25163 .unwrap();
25164
25165 workspace
25166 .update(cx, |workspace, window, cx| {
25167 workspace.active_pane().update(cx, |pane, cx| {
25168 pane.navigate_backward(&Default::default(), window, cx);
25169 })
25170 })
25171 .unwrap();
25172 cx.executor().advance_clock(Duration::from_millis(100));
25173 cx.run_until_parked();
25174 let editor = workspace
25175 .update(cx, |workspace, _, cx| {
25176 workspace
25177 .active_item(cx)
25178 .expect("Should have reopened the editor again after navigating back")
25179 .downcast::<Editor>()
25180 .expect("Should be an editor")
25181 })
25182 .unwrap();
25183 color_request_handle.next().await.unwrap();
25184 assert_eq!(
25185 3,
25186 requests_made.load(atomic::Ordering::Acquire),
25187 "Cache should be reused on buffer close and reopen"
25188 );
25189 editor.update(cx, |editor, cx| {
25190 assert_eq!(
25191 vec![expected_color],
25192 extract_color_inlays(editor, cx),
25193 "Should have an initial inlay"
25194 );
25195 });
25196}
25197
25198#[gpui::test]
25199async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25200 init_test(cx, |_| {});
25201 let (editor, cx) = cx.add_window_view(Editor::single_line);
25202 editor.update_in(cx, |editor, window, cx| {
25203 editor.set_text("oops\n\nwow\n", window, cx)
25204 });
25205 cx.run_until_parked();
25206 editor.update(cx, |editor, cx| {
25207 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25208 });
25209 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25210 cx.run_until_parked();
25211 editor.update(cx, |editor, cx| {
25212 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25213 });
25214}
25215
25216#[gpui::test]
25217async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25218 init_test(cx, |_| {});
25219
25220 cx.update(|cx| {
25221 register_project_item::<Editor>(cx);
25222 });
25223
25224 let fs = FakeFs::new(cx.executor());
25225 fs.insert_tree("/root1", json!({})).await;
25226 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25227 .await;
25228
25229 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25230 let (workspace, cx) =
25231 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25232
25233 let worktree_id = project.update(cx, |project, cx| {
25234 project.worktrees(cx).next().unwrap().read(cx).id()
25235 });
25236
25237 let handle = workspace
25238 .update_in(cx, |workspace, window, cx| {
25239 let project_path = (worktree_id, "one.pdf");
25240 workspace.open_path(project_path, None, true, window, cx)
25241 })
25242 .await
25243 .unwrap();
25244
25245 assert_eq!(
25246 handle.to_any().entity_type(),
25247 TypeId::of::<InvalidBufferView>()
25248 );
25249}
25250
25251#[track_caller]
25252fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25253 editor
25254 .all_inlays(cx)
25255 .into_iter()
25256 .filter_map(|inlay| inlay.get_color())
25257 .map(Rgba::from)
25258 .collect()
25259}