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 // Test selections with `line_mode = true`.
5368 cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
5369 cx.set_state(indoc! {"
5370 «The quick brown
5371 fox jumps over
5372 tˇ»he lazy dog
5373 "});
5374 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5375 cx.assert_editor_state(indoc! {"
5376 «THE QUICK BROWN
5377 FOX JUMPS OVER
5378 THE LAZY DOGˇ»
5379 "});
5380}
5381
5382#[gpui::test]
5383fn test_duplicate_line(cx: &mut TestAppContext) {
5384 init_test(cx, |_| {});
5385
5386 let editor = cx.add_window(|window, cx| {
5387 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5388 build_editor(buffer, window, cx)
5389 });
5390 _ = editor.update(cx, |editor, window, cx| {
5391 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5392 s.select_display_ranges([
5393 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5394 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5395 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5396 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5397 ])
5398 });
5399 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5400 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5401 assert_eq!(
5402 editor.selections.display_ranges(cx),
5403 vec![
5404 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5405 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5406 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5407 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5408 ]
5409 );
5410 });
5411
5412 let editor = cx.add_window(|window, cx| {
5413 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5414 build_editor(buffer, window, cx)
5415 });
5416 _ = editor.update(cx, |editor, window, cx| {
5417 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5418 s.select_display_ranges([
5419 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5420 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5421 ])
5422 });
5423 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5424 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5425 assert_eq!(
5426 editor.selections.display_ranges(cx),
5427 vec![
5428 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5429 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5430 ]
5431 );
5432 });
5433
5434 // With `move_upwards` the selections stay in place, except for
5435 // the lines inserted above them
5436 let editor = cx.add_window(|window, cx| {
5437 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5438 build_editor(buffer, window, cx)
5439 });
5440 _ = editor.update(cx, |editor, window, cx| {
5441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5442 s.select_display_ranges([
5443 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5445 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5446 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5447 ])
5448 });
5449 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5450 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5451 assert_eq!(
5452 editor.selections.display_ranges(cx),
5453 vec![
5454 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5455 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5456 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5457 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5458 ]
5459 );
5460 });
5461
5462 let editor = cx.add_window(|window, cx| {
5463 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5464 build_editor(buffer, window, cx)
5465 });
5466 _ = editor.update(cx, |editor, window, cx| {
5467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5468 s.select_display_ranges([
5469 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5470 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5471 ])
5472 });
5473 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5474 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5475 assert_eq!(
5476 editor.selections.display_ranges(cx),
5477 vec![
5478 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5479 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5480 ]
5481 );
5482 });
5483
5484 let editor = cx.add_window(|window, cx| {
5485 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5486 build_editor(buffer, window, cx)
5487 });
5488 _ = editor.update(cx, |editor, window, cx| {
5489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5490 s.select_display_ranges([
5491 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5492 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5493 ])
5494 });
5495 editor.duplicate_selection(&DuplicateSelection, window, cx);
5496 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5497 assert_eq!(
5498 editor.selections.display_ranges(cx),
5499 vec![
5500 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5501 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5502 ]
5503 );
5504 });
5505}
5506
5507#[gpui::test]
5508fn test_move_line_up_down(cx: &mut TestAppContext) {
5509 init_test(cx, |_| {});
5510
5511 let editor = cx.add_window(|window, cx| {
5512 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5513 build_editor(buffer, window, cx)
5514 });
5515 _ = editor.update(cx, |editor, window, cx| {
5516 editor.fold_creases(
5517 vec![
5518 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5519 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5520 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5521 ],
5522 true,
5523 window,
5524 cx,
5525 );
5526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5527 s.select_display_ranges([
5528 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5529 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5530 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5531 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5532 ])
5533 });
5534 assert_eq!(
5535 editor.display_text(cx),
5536 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5537 );
5538
5539 editor.move_line_up(&MoveLineUp, window, cx);
5540 assert_eq!(
5541 editor.display_text(cx),
5542 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5543 );
5544 assert_eq!(
5545 editor.selections.display_ranges(cx),
5546 vec![
5547 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5548 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5549 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5550 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5551 ]
5552 );
5553 });
5554
5555 _ = editor.update(cx, |editor, window, cx| {
5556 editor.move_line_down(&MoveLineDown, window, cx);
5557 assert_eq!(
5558 editor.display_text(cx),
5559 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5560 );
5561 assert_eq!(
5562 editor.selections.display_ranges(cx),
5563 vec![
5564 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5565 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5566 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5567 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5568 ]
5569 );
5570 });
5571
5572 _ = editor.update(cx, |editor, window, cx| {
5573 editor.move_line_down(&MoveLineDown, window, cx);
5574 assert_eq!(
5575 editor.display_text(cx),
5576 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5577 );
5578 assert_eq!(
5579 editor.selections.display_ranges(cx),
5580 vec![
5581 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5582 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5583 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5584 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5585 ]
5586 );
5587 });
5588
5589 _ = editor.update(cx, |editor, window, cx| {
5590 editor.move_line_up(&MoveLineUp, window, cx);
5591 assert_eq!(
5592 editor.display_text(cx),
5593 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5594 );
5595 assert_eq!(
5596 editor.selections.display_ranges(cx),
5597 vec![
5598 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5599 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5600 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5601 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5602 ]
5603 );
5604 });
5605}
5606
5607#[gpui::test]
5608fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5609 init_test(cx, |_| {});
5610 let editor = cx.add_window(|window, cx| {
5611 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5612 build_editor(buffer, window, cx)
5613 });
5614 _ = editor.update(cx, |editor, window, cx| {
5615 editor.fold_creases(
5616 vec![Crease::simple(
5617 Point::new(6, 4)..Point::new(7, 4),
5618 FoldPlaceholder::test(),
5619 )],
5620 true,
5621 window,
5622 cx,
5623 );
5624 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5625 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5626 });
5627 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5628 editor.move_line_up(&MoveLineUp, window, cx);
5629 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5630 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5631 });
5632}
5633
5634#[gpui::test]
5635fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5636 init_test(cx, |_| {});
5637
5638 let editor = cx.add_window(|window, cx| {
5639 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5640 build_editor(buffer, window, cx)
5641 });
5642 _ = editor.update(cx, |editor, window, cx| {
5643 let snapshot = editor.buffer.read(cx).snapshot(cx);
5644 editor.insert_blocks(
5645 [BlockProperties {
5646 style: BlockStyle::Fixed,
5647 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5648 height: Some(1),
5649 render: Arc::new(|_| div().into_any()),
5650 priority: 0,
5651 }],
5652 Some(Autoscroll::fit()),
5653 cx,
5654 );
5655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5656 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5657 });
5658 editor.move_line_down(&MoveLineDown, window, cx);
5659 });
5660}
5661
5662#[gpui::test]
5663async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5664 init_test(cx, |_| {});
5665
5666 let mut cx = EditorTestContext::new(cx).await;
5667 cx.set_state(
5668 &"
5669 ˇzero
5670 one
5671 two
5672 three
5673 four
5674 five
5675 "
5676 .unindent(),
5677 );
5678
5679 // Create a four-line block that replaces three lines of text.
5680 cx.update_editor(|editor, window, cx| {
5681 let snapshot = editor.snapshot(window, cx);
5682 let snapshot = &snapshot.buffer_snapshot;
5683 let placement = BlockPlacement::Replace(
5684 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5685 );
5686 editor.insert_blocks(
5687 [BlockProperties {
5688 placement,
5689 height: Some(4),
5690 style: BlockStyle::Sticky,
5691 render: Arc::new(|_| gpui::div().into_any_element()),
5692 priority: 0,
5693 }],
5694 None,
5695 cx,
5696 );
5697 });
5698
5699 // Move down so that the cursor touches the block.
5700 cx.update_editor(|editor, window, cx| {
5701 editor.move_down(&Default::default(), window, cx);
5702 });
5703 cx.assert_editor_state(
5704 &"
5705 zero
5706 «one
5707 two
5708 threeˇ»
5709 four
5710 five
5711 "
5712 .unindent(),
5713 );
5714
5715 // Move down past the block.
5716 cx.update_editor(|editor, window, cx| {
5717 editor.move_down(&Default::default(), window, cx);
5718 });
5719 cx.assert_editor_state(
5720 &"
5721 zero
5722 one
5723 two
5724 three
5725 ˇfour
5726 five
5727 "
5728 .unindent(),
5729 );
5730}
5731
5732#[gpui::test]
5733fn test_transpose(cx: &mut TestAppContext) {
5734 init_test(cx, |_| {});
5735
5736 _ = cx.add_window(|window, cx| {
5737 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5738 editor.set_style(EditorStyle::default(), window, cx);
5739 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5740 s.select_ranges([1..1])
5741 });
5742 editor.transpose(&Default::default(), window, cx);
5743 assert_eq!(editor.text(cx), "bac");
5744 assert_eq!(editor.selections.ranges(cx), [2..2]);
5745
5746 editor.transpose(&Default::default(), window, cx);
5747 assert_eq!(editor.text(cx), "bca");
5748 assert_eq!(editor.selections.ranges(cx), [3..3]);
5749
5750 editor.transpose(&Default::default(), window, cx);
5751 assert_eq!(editor.text(cx), "bac");
5752 assert_eq!(editor.selections.ranges(cx), [3..3]);
5753
5754 editor
5755 });
5756
5757 _ = cx.add_window(|window, cx| {
5758 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5759 editor.set_style(EditorStyle::default(), window, cx);
5760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5761 s.select_ranges([3..3])
5762 });
5763 editor.transpose(&Default::default(), window, cx);
5764 assert_eq!(editor.text(cx), "acb\nde");
5765 assert_eq!(editor.selections.ranges(cx), [3..3]);
5766
5767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5768 s.select_ranges([4..4])
5769 });
5770 editor.transpose(&Default::default(), window, cx);
5771 assert_eq!(editor.text(cx), "acbd\ne");
5772 assert_eq!(editor.selections.ranges(cx), [5..5]);
5773
5774 editor.transpose(&Default::default(), window, cx);
5775 assert_eq!(editor.text(cx), "acbde\n");
5776 assert_eq!(editor.selections.ranges(cx), [6..6]);
5777
5778 editor.transpose(&Default::default(), window, cx);
5779 assert_eq!(editor.text(cx), "acbd\ne");
5780 assert_eq!(editor.selections.ranges(cx), [6..6]);
5781
5782 editor
5783 });
5784
5785 _ = cx.add_window(|window, cx| {
5786 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5787 editor.set_style(EditorStyle::default(), window, cx);
5788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5789 s.select_ranges([1..1, 2..2, 4..4])
5790 });
5791 editor.transpose(&Default::default(), window, cx);
5792 assert_eq!(editor.text(cx), "bacd\ne");
5793 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5794
5795 editor.transpose(&Default::default(), window, cx);
5796 assert_eq!(editor.text(cx), "bcade\n");
5797 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5798
5799 editor.transpose(&Default::default(), window, cx);
5800 assert_eq!(editor.text(cx), "bcda\ne");
5801 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5802
5803 editor.transpose(&Default::default(), window, cx);
5804 assert_eq!(editor.text(cx), "bcade\n");
5805 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5806
5807 editor.transpose(&Default::default(), window, cx);
5808 assert_eq!(editor.text(cx), "bcaed\n");
5809 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5810
5811 editor
5812 });
5813
5814 _ = cx.add_window(|window, cx| {
5815 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5816 editor.set_style(EditorStyle::default(), window, cx);
5817 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5818 s.select_ranges([4..4])
5819 });
5820 editor.transpose(&Default::default(), window, cx);
5821 assert_eq!(editor.text(cx), "🏀🍐✋");
5822 assert_eq!(editor.selections.ranges(cx), [8..8]);
5823
5824 editor.transpose(&Default::default(), window, cx);
5825 assert_eq!(editor.text(cx), "🏀✋🍐");
5826 assert_eq!(editor.selections.ranges(cx), [11..11]);
5827
5828 editor.transpose(&Default::default(), window, cx);
5829 assert_eq!(editor.text(cx), "🏀🍐✋");
5830 assert_eq!(editor.selections.ranges(cx), [11..11]);
5831
5832 editor
5833 });
5834}
5835
5836#[gpui::test]
5837async fn test_rewrap(cx: &mut TestAppContext) {
5838 init_test(cx, |settings| {
5839 settings.languages.0.extend([
5840 (
5841 "Markdown".into(),
5842 LanguageSettingsContent {
5843 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5844 preferred_line_length: Some(40),
5845 ..Default::default()
5846 },
5847 ),
5848 (
5849 "Plain Text".into(),
5850 LanguageSettingsContent {
5851 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5852 preferred_line_length: Some(40),
5853 ..Default::default()
5854 },
5855 ),
5856 (
5857 "C++".into(),
5858 LanguageSettingsContent {
5859 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5860 preferred_line_length: Some(40),
5861 ..Default::default()
5862 },
5863 ),
5864 (
5865 "Python".into(),
5866 LanguageSettingsContent {
5867 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5868 preferred_line_length: Some(40),
5869 ..Default::default()
5870 },
5871 ),
5872 (
5873 "Rust".into(),
5874 LanguageSettingsContent {
5875 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5876 preferred_line_length: Some(40),
5877 ..Default::default()
5878 },
5879 ),
5880 ])
5881 });
5882
5883 let mut cx = EditorTestContext::new(cx).await;
5884
5885 let cpp_language = Arc::new(Language::new(
5886 LanguageConfig {
5887 name: "C++".into(),
5888 line_comments: vec!["// ".into()],
5889 ..LanguageConfig::default()
5890 },
5891 None,
5892 ));
5893 let python_language = Arc::new(Language::new(
5894 LanguageConfig {
5895 name: "Python".into(),
5896 line_comments: vec!["# ".into()],
5897 ..LanguageConfig::default()
5898 },
5899 None,
5900 ));
5901 let markdown_language = Arc::new(Language::new(
5902 LanguageConfig {
5903 name: "Markdown".into(),
5904 rewrap_prefixes: vec![
5905 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5906 regex::Regex::new("[-*+]\\s+").unwrap(),
5907 ],
5908 ..LanguageConfig::default()
5909 },
5910 None,
5911 ));
5912 let rust_language = Arc::new(
5913 Language::new(
5914 LanguageConfig {
5915 name: "Rust".into(),
5916 line_comments: vec!["// ".into(), "/// ".into()],
5917 ..LanguageConfig::default()
5918 },
5919 Some(tree_sitter_rust::LANGUAGE.into()),
5920 )
5921 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5922 .unwrap(),
5923 );
5924
5925 let plaintext_language = Arc::new(Language::new(
5926 LanguageConfig {
5927 name: "Plain Text".into(),
5928 ..LanguageConfig::default()
5929 },
5930 None,
5931 ));
5932
5933 // Test basic rewrapping of a long line with a cursor
5934 assert_rewrap(
5935 indoc! {"
5936 // ˇThis is a long comment that needs to be wrapped.
5937 "},
5938 indoc! {"
5939 // ˇThis is a long comment that needs to
5940 // be wrapped.
5941 "},
5942 cpp_language.clone(),
5943 &mut cx,
5944 );
5945
5946 // Test rewrapping a full selection
5947 assert_rewrap(
5948 indoc! {"
5949 «// This selected long comment needs to be wrapped.ˇ»"
5950 },
5951 indoc! {"
5952 «// This selected long comment needs to
5953 // be wrapped.ˇ»"
5954 },
5955 cpp_language.clone(),
5956 &mut cx,
5957 );
5958
5959 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5960 assert_rewrap(
5961 indoc! {"
5962 // ˇThis is the first line.
5963 // Thisˇ is the second line.
5964 // This is the thirdˇ line, all part of one paragraph.
5965 "},
5966 indoc! {"
5967 // ˇThis is the first line. Thisˇ is the
5968 // second line. This is the thirdˇ line,
5969 // all part of one paragraph.
5970 "},
5971 cpp_language.clone(),
5972 &mut cx,
5973 );
5974
5975 // Test multiple cursors in different paragraphs trigger separate rewraps
5976 assert_rewrap(
5977 indoc! {"
5978 // ˇThis is the first paragraph, first line.
5979 // ˇThis is the first paragraph, second line.
5980
5981 // ˇThis is the second paragraph, first line.
5982 // ˇThis is the second paragraph, second line.
5983 "},
5984 indoc! {"
5985 // ˇThis is the first paragraph, first
5986 // line. ˇThis is the first paragraph,
5987 // second line.
5988
5989 // ˇThis is the second paragraph, first
5990 // line. ˇThis is the second paragraph,
5991 // second line.
5992 "},
5993 cpp_language.clone(),
5994 &mut cx,
5995 );
5996
5997 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5998 assert_rewrap(
5999 indoc! {"
6000 «// A regular long long comment to be wrapped.
6001 /// A documentation long comment to be wrapped.ˇ»
6002 "},
6003 indoc! {"
6004 «// A regular long long comment to be
6005 // wrapped.
6006 /// A documentation long comment to be
6007 /// wrapped.ˇ»
6008 "},
6009 rust_language.clone(),
6010 &mut cx,
6011 );
6012
6013 // Test that change in indentation level trigger seperate rewraps
6014 assert_rewrap(
6015 indoc! {"
6016 fn foo() {
6017 «// This is a long comment at the base indent.
6018 // This is a long comment at the next indent.ˇ»
6019 }
6020 "},
6021 indoc! {"
6022 fn foo() {
6023 «// This is a long comment at the
6024 // base indent.
6025 // This is a long comment at the
6026 // next indent.ˇ»
6027 }
6028 "},
6029 rust_language.clone(),
6030 &mut cx,
6031 );
6032
6033 // Test that different comment prefix characters (e.g., '#') are handled correctly
6034 assert_rewrap(
6035 indoc! {"
6036 # ˇThis is a long comment using a pound sign.
6037 "},
6038 indoc! {"
6039 # ˇThis is a long comment using a pound
6040 # sign.
6041 "},
6042 python_language,
6043 &mut cx,
6044 );
6045
6046 // Test rewrapping only affects comments, not code even when selected
6047 assert_rewrap(
6048 indoc! {"
6049 «/// This doc comment is long and should be wrapped.
6050 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6051 "},
6052 indoc! {"
6053 «/// This doc comment is long and should
6054 /// be wrapped.
6055 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6056 "},
6057 rust_language.clone(),
6058 &mut cx,
6059 );
6060
6061 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6062 assert_rewrap(
6063 indoc! {"
6064 # Header
6065
6066 A long long long line of markdown text to wrap.ˇ
6067 "},
6068 indoc! {"
6069 # Header
6070
6071 A long long long line of markdown text
6072 to wrap.ˇ
6073 "},
6074 markdown_language.clone(),
6075 &mut cx,
6076 );
6077
6078 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6079 assert_rewrap(
6080 indoc! {"
6081 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6082 2. This is a numbered list item that is very long and needs to be wrapped properly.
6083 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6084 "},
6085 indoc! {"
6086 «1. This is a numbered list item that is
6087 very long and needs to be wrapped
6088 properly.
6089 2. This is a numbered list item that is
6090 very long and needs to be wrapped
6091 properly.
6092 - This is an unordered list item that is
6093 also very long and should not merge
6094 with the numbered item.ˇ»
6095 "},
6096 markdown_language.clone(),
6097 &mut cx,
6098 );
6099
6100 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6101 assert_rewrap(
6102 indoc! {"
6103 «1. This is a numbered list item that is
6104 very long and needs to be wrapped
6105 properly.
6106 2. This is a numbered list item that is
6107 very long and needs to be wrapped
6108 properly.
6109 - This is an unordered list item that is
6110 also very long and should not merge with
6111 the numbered item.ˇ»
6112 "},
6113 indoc! {"
6114 «1. This is a numbered list item that is
6115 very long and needs to be wrapped
6116 properly.
6117 2. This is a numbered list item that is
6118 very long and needs to be wrapped
6119 properly.
6120 - This is an unordered list item that is
6121 also very long and should not merge
6122 with the numbered item.ˇ»
6123 "},
6124 markdown_language.clone(),
6125 &mut cx,
6126 );
6127
6128 // Test that rewrapping maintain indents even when they already exists.
6129 assert_rewrap(
6130 indoc! {"
6131 «1. This is a numbered list
6132 item that is very long and needs to be wrapped properly.
6133 2. This is a numbered list
6134 item that is very long and needs to be wrapped properly.
6135 - This is an unordered list item that is also very long and
6136 should not merge with the numbered item.ˇ»
6137 "},
6138 indoc! {"
6139 «1. This is a numbered list item that is
6140 very long and needs to be wrapped
6141 properly.
6142 2. This is a numbered list item that is
6143 very long and needs to be wrapped
6144 properly.
6145 - This is an unordered list item that is
6146 also very long and should not merge
6147 with the numbered item.ˇ»
6148 "},
6149 markdown_language,
6150 &mut cx,
6151 );
6152
6153 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6154 assert_rewrap(
6155 indoc! {"
6156 ˇThis is a very long line of plain text that will be wrapped.
6157 "},
6158 indoc! {"
6159 ˇThis is a very long line of plain text
6160 that will be wrapped.
6161 "},
6162 plaintext_language.clone(),
6163 &mut cx,
6164 );
6165
6166 // Test that non-commented code acts as a paragraph boundary within a selection
6167 assert_rewrap(
6168 indoc! {"
6169 «// This is the first long comment block to be wrapped.
6170 fn my_func(a: u32);
6171 // This is the second long comment block to be wrapped.ˇ»
6172 "},
6173 indoc! {"
6174 «// This is the first long comment block
6175 // to be wrapped.
6176 fn my_func(a: u32);
6177 // This is the second long comment block
6178 // to be wrapped.ˇ»
6179 "},
6180 rust_language,
6181 &mut cx,
6182 );
6183
6184 // Test rewrapping multiple selections, including ones with blank lines or tabs
6185 assert_rewrap(
6186 indoc! {"
6187 «ˇThis is a very long line that will be wrapped.
6188
6189 This is another paragraph in the same selection.»
6190
6191 «\tThis is a very long indented line that will be wrapped.ˇ»
6192 "},
6193 indoc! {"
6194 «ˇThis is a very long line that will be
6195 wrapped.
6196
6197 This is another paragraph in the same
6198 selection.»
6199
6200 «\tThis is a very long indented line
6201 \tthat will be wrapped.ˇ»
6202 "},
6203 plaintext_language,
6204 &mut cx,
6205 );
6206
6207 // Test that an empty comment line acts as a paragraph boundary
6208 assert_rewrap(
6209 indoc! {"
6210 // ˇThis is a long comment that will be wrapped.
6211 //
6212 // And this is another long comment that will also be wrapped.ˇ
6213 "},
6214 indoc! {"
6215 // ˇThis is a long comment that will be
6216 // wrapped.
6217 //
6218 // And this is another long comment that
6219 // will also be wrapped.ˇ
6220 "},
6221 cpp_language,
6222 &mut cx,
6223 );
6224
6225 #[track_caller]
6226 fn assert_rewrap(
6227 unwrapped_text: &str,
6228 wrapped_text: &str,
6229 language: Arc<Language>,
6230 cx: &mut EditorTestContext,
6231 ) {
6232 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6233 cx.set_state(unwrapped_text);
6234 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6235 cx.assert_editor_state(wrapped_text);
6236 }
6237}
6238
6239#[gpui::test]
6240async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6241 init_test(cx, |settings| {
6242 settings.languages.0.extend([(
6243 "Rust".into(),
6244 LanguageSettingsContent {
6245 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6246 preferred_line_length: Some(40),
6247 ..Default::default()
6248 },
6249 )])
6250 });
6251
6252 let mut cx = EditorTestContext::new(cx).await;
6253
6254 let rust_lang = Arc::new(
6255 Language::new(
6256 LanguageConfig {
6257 name: "Rust".into(),
6258 line_comments: vec!["// ".into()],
6259 block_comment: Some(BlockCommentConfig {
6260 start: "/*".into(),
6261 end: "*/".into(),
6262 prefix: "* ".into(),
6263 tab_size: 1,
6264 }),
6265 documentation_comment: Some(BlockCommentConfig {
6266 start: "/**".into(),
6267 end: "*/".into(),
6268 prefix: "* ".into(),
6269 tab_size: 1,
6270 }),
6271
6272 ..LanguageConfig::default()
6273 },
6274 Some(tree_sitter_rust::LANGUAGE.into()),
6275 )
6276 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6277 .unwrap(),
6278 );
6279
6280 // regular block comment
6281 assert_rewrap(
6282 indoc! {"
6283 /*
6284 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6285 */
6286 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6287 "},
6288 indoc! {"
6289 /*
6290 *ˇ Lorem ipsum dolor sit amet,
6291 * consectetur adipiscing elit.
6292 */
6293 /*
6294 *ˇ Lorem ipsum dolor sit amet,
6295 * consectetur adipiscing elit.
6296 */
6297 "},
6298 rust_lang.clone(),
6299 &mut cx,
6300 );
6301
6302 // indent is respected
6303 assert_rewrap(
6304 indoc! {"
6305 {}
6306 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6307 "},
6308 indoc! {"
6309 {}
6310 /*
6311 *ˇ Lorem ipsum dolor sit amet,
6312 * consectetur adipiscing elit.
6313 */
6314 "},
6315 rust_lang.clone(),
6316 &mut cx,
6317 );
6318
6319 // short block comments with inline delimiters
6320 assert_rewrap(
6321 indoc! {"
6322 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6323 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6324 */
6325 /*
6326 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6327 "},
6328 indoc! {"
6329 /*
6330 *ˇ Lorem ipsum dolor sit amet,
6331 * consectetur adipiscing elit.
6332 */
6333 /*
6334 *ˇ Lorem ipsum dolor sit amet,
6335 * consectetur adipiscing elit.
6336 */
6337 /*
6338 *ˇ Lorem ipsum dolor sit amet,
6339 * consectetur adipiscing elit.
6340 */
6341 "},
6342 rust_lang.clone(),
6343 &mut cx,
6344 );
6345
6346 // multiline block comment with inline start/end delimiters
6347 assert_rewrap(
6348 indoc! {"
6349 /*ˇ Lorem ipsum dolor sit amet,
6350 * consectetur adipiscing elit. */
6351 "},
6352 indoc! {"
6353 /*
6354 *ˇ Lorem ipsum dolor sit amet,
6355 * consectetur adipiscing elit.
6356 */
6357 "},
6358 rust_lang.clone(),
6359 &mut cx,
6360 );
6361
6362 // block comment rewrap still respects paragraph bounds
6363 assert_rewrap(
6364 indoc! {"
6365 /*
6366 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6367 *
6368 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6369 */
6370 "},
6371 indoc! {"
6372 /*
6373 *ˇ Lorem ipsum dolor sit amet,
6374 * consectetur adipiscing elit.
6375 *
6376 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6377 */
6378 "},
6379 rust_lang.clone(),
6380 &mut cx,
6381 );
6382
6383 // documentation comments
6384 assert_rewrap(
6385 indoc! {"
6386 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6387 /**
6388 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6389 */
6390 "},
6391 indoc! {"
6392 /**
6393 *ˇ Lorem ipsum dolor sit amet,
6394 * consectetur adipiscing elit.
6395 */
6396 /**
6397 *ˇ Lorem ipsum dolor sit amet,
6398 * consectetur adipiscing elit.
6399 */
6400 "},
6401 rust_lang.clone(),
6402 &mut cx,
6403 );
6404
6405 // different, adjacent comments
6406 assert_rewrap(
6407 indoc! {"
6408 /**
6409 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6410 */
6411 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6412 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6413 "},
6414 indoc! {"
6415 /**
6416 *ˇ Lorem ipsum dolor sit amet,
6417 * consectetur adipiscing elit.
6418 */
6419 /*
6420 *ˇ Lorem ipsum dolor sit amet,
6421 * consectetur adipiscing elit.
6422 */
6423 //ˇ Lorem ipsum dolor sit amet,
6424 // consectetur adipiscing elit.
6425 "},
6426 rust_lang.clone(),
6427 &mut cx,
6428 );
6429
6430 // selection w/ single short block comment
6431 assert_rewrap(
6432 indoc! {"
6433 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6434 "},
6435 indoc! {"
6436 «/*
6437 * Lorem ipsum dolor sit amet,
6438 * consectetur adipiscing elit.
6439 */ˇ»
6440 "},
6441 rust_lang.clone(),
6442 &mut cx,
6443 );
6444
6445 // rewrapping a single comment w/ abutting comments
6446 assert_rewrap(
6447 indoc! {"
6448 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6449 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6450 "},
6451 indoc! {"
6452 /*
6453 * ˇLorem ipsum dolor sit amet,
6454 * consectetur adipiscing elit.
6455 */
6456 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6457 "},
6458 rust_lang.clone(),
6459 &mut cx,
6460 );
6461
6462 // selection w/ non-abutting short block comments
6463 assert_rewrap(
6464 indoc! {"
6465 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6466
6467 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6468 "},
6469 indoc! {"
6470 «/*
6471 * Lorem ipsum dolor sit amet,
6472 * consectetur adipiscing elit.
6473 */
6474
6475 /*
6476 * Lorem ipsum dolor sit amet,
6477 * consectetur adipiscing elit.
6478 */ˇ»
6479 "},
6480 rust_lang.clone(),
6481 &mut cx,
6482 );
6483
6484 // selection of multiline block comments
6485 assert_rewrap(
6486 indoc! {"
6487 «/* Lorem ipsum dolor sit amet,
6488 * consectetur adipiscing elit. */ˇ»
6489 "},
6490 indoc! {"
6491 «/*
6492 * Lorem ipsum dolor sit amet,
6493 * consectetur adipiscing elit.
6494 */ˇ»
6495 "},
6496 rust_lang.clone(),
6497 &mut cx,
6498 );
6499
6500 // partial selection of multiline block comments
6501 assert_rewrap(
6502 indoc! {"
6503 «/* Lorem ipsum dolor sit amet,ˇ»
6504 * consectetur adipiscing elit. */
6505 /* Lorem ipsum dolor sit amet,
6506 «* consectetur adipiscing elit. */ˇ»
6507 "},
6508 indoc! {"
6509 «/*
6510 * Lorem ipsum dolor sit amet,ˇ»
6511 * consectetur adipiscing elit. */
6512 /* Lorem ipsum dolor sit amet,
6513 «* consectetur adipiscing elit.
6514 */ˇ»
6515 "},
6516 rust_lang.clone(),
6517 &mut cx,
6518 );
6519
6520 // selection w/ abutting short block comments
6521 // TODO: should not be combined; should rewrap as 2 comments
6522 assert_rewrap(
6523 indoc! {"
6524 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6525 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6526 "},
6527 // desired behavior:
6528 // indoc! {"
6529 // «/*
6530 // * Lorem ipsum dolor sit amet,
6531 // * consectetur adipiscing elit.
6532 // */
6533 // /*
6534 // * Lorem ipsum dolor sit amet,
6535 // * consectetur adipiscing elit.
6536 // */ˇ»
6537 // "},
6538 // actual behaviour:
6539 indoc! {"
6540 «/*
6541 * Lorem ipsum dolor sit amet,
6542 * consectetur adipiscing elit. Lorem
6543 * ipsum dolor sit amet, consectetur
6544 * adipiscing elit.
6545 */ˇ»
6546 "},
6547 rust_lang.clone(),
6548 &mut cx,
6549 );
6550
6551 // TODO: same as above, but with delimiters on separate line
6552 // assert_rewrap(
6553 // indoc! {"
6554 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6555 // */
6556 // /*
6557 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6558 // "},
6559 // // desired:
6560 // // indoc! {"
6561 // // «/*
6562 // // * Lorem ipsum dolor sit amet,
6563 // // * consectetur adipiscing elit.
6564 // // */
6565 // // /*
6566 // // * Lorem ipsum dolor sit amet,
6567 // // * consectetur adipiscing elit.
6568 // // */ˇ»
6569 // // "},
6570 // // actual: (but with trailing w/s on the empty lines)
6571 // indoc! {"
6572 // «/*
6573 // * Lorem ipsum dolor sit amet,
6574 // * consectetur adipiscing elit.
6575 // *
6576 // */
6577 // /*
6578 // *
6579 // * Lorem ipsum dolor sit amet,
6580 // * consectetur adipiscing elit.
6581 // */ˇ»
6582 // "},
6583 // rust_lang.clone(),
6584 // &mut cx,
6585 // );
6586
6587 // TODO these are unhandled edge cases; not correct, just documenting known issues
6588 assert_rewrap(
6589 indoc! {"
6590 /*
6591 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6592 */
6593 /*
6594 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6595 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6596 "},
6597 // desired:
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 // actual:
6612 indoc! {"
6613 /*
6614 //ˇ Lorem ipsum dolor sit amet,
6615 // consectetur adipiscing elit.
6616 */
6617 /*
6618 * //ˇ Lorem ipsum dolor sit amet,
6619 * consectetur adipiscing elit.
6620 */
6621 /*
6622 *ˇ Lorem ipsum dolor sit amet */ /*
6623 * consectetur adipiscing elit.
6624 */
6625 "},
6626 rust_lang,
6627 &mut cx,
6628 );
6629
6630 #[track_caller]
6631 fn assert_rewrap(
6632 unwrapped_text: &str,
6633 wrapped_text: &str,
6634 language: Arc<Language>,
6635 cx: &mut EditorTestContext,
6636 ) {
6637 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6638 cx.set_state(unwrapped_text);
6639 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6640 cx.assert_editor_state(wrapped_text);
6641 }
6642}
6643
6644#[gpui::test]
6645async fn test_hard_wrap(cx: &mut TestAppContext) {
6646 init_test(cx, |_| {});
6647 let mut cx = EditorTestContext::new(cx).await;
6648
6649 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6650 cx.update_editor(|editor, _, cx| {
6651 editor.set_hard_wrap(Some(14), cx);
6652 });
6653
6654 cx.set_state(indoc!(
6655 "
6656 one two three ˇ
6657 "
6658 ));
6659 cx.simulate_input("four");
6660 cx.run_until_parked();
6661
6662 cx.assert_editor_state(indoc!(
6663 "
6664 one two three
6665 fourˇ
6666 "
6667 ));
6668
6669 cx.update_editor(|editor, window, cx| {
6670 editor.newline(&Default::default(), window, cx);
6671 });
6672 cx.run_until_parked();
6673 cx.assert_editor_state(indoc!(
6674 "
6675 one two three
6676 four
6677 ˇ
6678 "
6679 ));
6680
6681 cx.simulate_input("five");
6682 cx.run_until_parked();
6683 cx.assert_editor_state(indoc!(
6684 "
6685 one two three
6686 four
6687 fiveˇ
6688 "
6689 ));
6690
6691 cx.update_editor(|editor, window, cx| {
6692 editor.newline(&Default::default(), window, cx);
6693 });
6694 cx.run_until_parked();
6695 cx.simulate_input("# ");
6696 cx.run_until_parked();
6697 cx.assert_editor_state(indoc!(
6698 "
6699 one two three
6700 four
6701 five
6702 # ˇ
6703 "
6704 ));
6705
6706 cx.update_editor(|editor, window, cx| {
6707 editor.newline(&Default::default(), window, cx);
6708 });
6709 cx.run_until_parked();
6710 cx.assert_editor_state(indoc!(
6711 "
6712 one two three
6713 four
6714 five
6715 #\x20
6716 #ˇ
6717 "
6718 ));
6719
6720 cx.simulate_input(" 6");
6721 cx.run_until_parked();
6722 cx.assert_editor_state(indoc!(
6723 "
6724 one two three
6725 four
6726 five
6727 #
6728 # 6ˇ
6729 "
6730 ));
6731}
6732
6733#[gpui::test]
6734async fn test_clipboard(cx: &mut TestAppContext) {
6735 init_test(cx, |_| {});
6736
6737 let mut cx = EditorTestContext::new(cx).await;
6738
6739 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6740 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6741 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6742
6743 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6744 cx.set_state("two ˇfour ˇsix ˇ");
6745 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6746 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6747
6748 // Paste again but with only two cursors. Since the number of cursors doesn't
6749 // match the number of slices in the clipboard, the entire clipboard text
6750 // is pasted at each cursor.
6751 cx.set_state("ˇtwo one✅ four three six five ˇ");
6752 cx.update_editor(|e, window, cx| {
6753 e.handle_input("( ", window, cx);
6754 e.paste(&Paste, window, cx);
6755 e.handle_input(") ", window, cx);
6756 });
6757 cx.assert_editor_state(
6758 &([
6759 "( one✅ ",
6760 "three ",
6761 "five ) ˇtwo one✅ four three six five ( one✅ ",
6762 "three ",
6763 "five ) ˇ",
6764 ]
6765 .join("\n")),
6766 );
6767
6768 // Cut with three selections, one of which is full-line.
6769 cx.set_state(indoc! {"
6770 1«2ˇ»3
6771 4ˇ567
6772 «8ˇ»9"});
6773 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6774 cx.assert_editor_state(indoc! {"
6775 1ˇ3
6776 ˇ9"});
6777
6778 // Paste with three selections, noticing how the copied selection that was full-line
6779 // gets inserted before the second cursor.
6780 cx.set_state(indoc! {"
6781 1ˇ3
6782 9ˇ
6783 «oˇ»ne"});
6784 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6785 cx.assert_editor_state(indoc! {"
6786 12ˇ3
6787 4567
6788 9ˇ
6789 8ˇne"});
6790
6791 // Copy with a single cursor only, which writes the whole line into the clipboard.
6792 cx.set_state(indoc! {"
6793 The quick brown
6794 fox juˇmps over
6795 the lazy dog"});
6796 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6797 assert_eq!(
6798 cx.read_from_clipboard()
6799 .and_then(|item| item.text().as_deref().map(str::to_string)),
6800 Some("fox jumps over\n".to_string())
6801 );
6802
6803 // Paste with three selections, noticing how the copied full-line selection is inserted
6804 // before the empty selections but replaces the selection that is non-empty.
6805 cx.set_state(indoc! {"
6806 Tˇhe quick brown
6807 «foˇ»x jumps over
6808 tˇhe lazy dog"});
6809 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6810 cx.assert_editor_state(indoc! {"
6811 fox jumps over
6812 Tˇhe quick brown
6813 fox jumps over
6814 ˇx jumps over
6815 fox jumps over
6816 tˇhe lazy dog"});
6817}
6818
6819#[gpui::test]
6820async fn test_copy_trim(cx: &mut TestAppContext) {
6821 init_test(cx, |_| {});
6822
6823 let mut cx = EditorTestContext::new(cx).await;
6824 cx.set_state(
6825 r#" «for selection in selections.iter() {
6826 let mut start = selection.start;
6827 let mut end = selection.end;
6828 let is_entire_line = selection.is_empty();
6829 if is_entire_line {
6830 start = Point::new(start.row, 0);ˇ»
6831 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6832 }
6833 "#,
6834 );
6835 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6836 assert_eq!(
6837 cx.read_from_clipboard()
6838 .and_then(|item| item.text().as_deref().map(str::to_string)),
6839 Some(
6840 "for selection in selections.iter() {
6841 let mut start = selection.start;
6842 let mut end = selection.end;
6843 let is_entire_line = selection.is_empty();
6844 if is_entire_line {
6845 start = Point::new(start.row, 0);"
6846 .to_string()
6847 ),
6848 "Regular copying preserves all indentation selected",
6849 );
6850 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6851 assert_eq!(
6852 cx.read_from_clipboard()
6853 .and_then(|item| item.text().as_deref().map(str::to_string)),
6854 Some(
6855 "for selection in selections.iter() {
6856let mut start = selection.start;
6857let mut end = selection.end;
6858let is_entire_line = selection.is_empty();
6859if is_entire_line {
6860 start = Point::new(start.row, 0);"
6861 .to_string()
6862 ),
6863 "Copying with stripping should strip all leading whitespaces"
6864 );
6865
6866 cx.set_state(
6867 r#" « for selection in selections.iter() {
6868 let mut start = selection.start;
6869 let mut end = selection.end;
6870 let is_entire_line = selection.is_empty();
6871 if is_entire_line {
6872 start = Point::new(start.row, 0);ˇ»
6873 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6874 }
6875 "#,
6876 );
6877 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6878 assert_eq!(
6879 cx.read_from_clipboard()
6880 .and_then(|item| item.text().as_deref().map(str::to_string)),
6881 Some(
6882 " for selection in selections.iter() {
6883 let mut start = selection.start;
6884 let mut end = selection.end;
6885 let is_entire_line = selection.is_empty();
6886 if is_entire_line {
6887 start = Point::new(start.row, 0);"
6888 .to_string()
6889 ),
6890 "Regular copying preserves all indentation selected",
6891 );
6892 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6893 assert_eq!(
6894 cx.read_from_clipboard()
6895 .and_then(|item| item.text().as_deref().map(str::to_string)),
6896 Some(
6897 "for selection in selections.iter() {
6898let mut start = selection.start;
6899let mut end = selection.end;
6900let is_entire_line = selection.is_empty();
6901if is_entire_line {
6902 start = Point::new(start.row, 0);"
6903 .to_string()
6904 ),
6905 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6906 );
6907
6908 cx.set_state(
6909 r#" «ˇ for selection in selections.iter() {
6910 let mut start = selection.start;
6911 let mut end = selection.end;
6912 let is_entire_line = selection.is_empty();
6913 if is_entire_line {
6914 start = Point::new(start.row, 0);»
6915 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6916 }
6917 "#,
6918 );
6919 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6920 assert_eq!(
6921 cx.read_from_clipboard()
6922 .and_then(|item| item.text().as_deref().map(str::to_string)),
6923 Some(
6924 " for selection in selections.iter() {
6925 let mut start = selection.start;
6926 let mut end = selection.end;
6927 let is_entire_line = selection.is_empty();
6928 if is_entire_line {
6929 start = Point::new(start.row, 0);"
6930 .to_string()
6931 ),
6932 "Regular copying for reverse selection works the same",
6933 );
6934 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6935 assert_eq!(
6936 cx.read_from_clipboard()
6937 .and_then(|item| item.text().as_deref().map(str::to_string)),
6938 Some(
6939 "for selection in selections.iter() {
6940let mut start = selection.start;
6941let mut end = selection.end;
6942let is_entire_line = selection.is_empty();
6943if is_entire_line {
6944 start = Point::new(start.row, 0);"
6945 .to_string()
6946 ),
6947 "Copying with stripping for reverse selection works the same"
6948 );
6949
6950 cx.set_state(
6951 r#" for selection «in selections.iter() {
6952 let mut start = selection.start;
6953 let mut end = selection.end;
6954 let is_entire_line = selection.is_empty();
6955 if is_entire_line {
6956 start = Point::new(start.row, 0);ˇ»
6957 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6958 }
6959 "#,
6960 );
6961 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6962 assert_eq!(
6963 cx.read_from_clipboard()
6964 .and_then(|item| item.text().as_deref().map(str::to_string)),
6965 Some(
6966 "in selections.iter() {
6967 let mut start = selection.start;
6968 let mut end = selection.end;
6969 let is_entire_line = selection.is_empty();
6970 if is_entire_line {
6971 start = Point::new(start.row, 0);"
6972 .to_string()
6973 ),
6974 "When selecting past the indent, the copying works as usual",
6975 );
6976 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6977 assert_eq!(
6978 cx.read_from_clipboard()
6979 .and_then(|item| item.text().as_deref().map(str::to_string)),
6980 Some(
6981 "in selections.iter() {
6982 let mut start = selection.start;
6983 let mut end = selection.end;
6984 let is_entire_line = selection.is_empty();
6985 if is_entire_line {
6986 start = Point::new(start.row, 0);"
6987 .to_string()
6988 ),
6989 "When selecting past the indent, nothing is trimmed"
6990 );
6991
6992 cx.set_state(
6993 r#" «for selection in selections.iter() {
6994 let mut start = selection.start;
6995
6996 let mut end = selection.end;
6997 let is_entire_line = selection.is_empty();
6998 if is_entire_line {
6999 start = Point::new(start.row, 0);
7000ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7001 }
7002 "#,
7003 );
7004 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7005 assert_eq!(
7006 cx.read_from_clipboard()
7007 .and_then(|item| item.text().as_deref().map(str::to_string)),
7008 Some(
7009 "for selection in selections.iter() {
7010let mut start = selection.start;
7011
7012let mut end = selection.end;
7013let is_entire_line = selection.is_empty();
7014if is_entire_line {
7015 start = Point::new(start.row, 0);
7016"
7017 .to_string()
7018 ),
7019 "Copying with stripping should ignore empty lines"
7020 );
7021}
7022
7023#[gpui::test]
7024async fn test_paste_multiline(cx: &mut TestAppContext) {
7025 init_test(cx, |_| {});
7026
7027 let mut cx = EditorTestContext::new(cx).await;
7028 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7029
7030 // Cut an indented block, without the leading whitespace.
7031 cx.set_state(indoc! {"
7032 const a: B = (
7033 c(),
7034 «d(
7035 e,
7036 f
7037 )ˇ»
7038 );
7039 "});
7040 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7041 cx.assert_editor_state(indoc! {"
7042 const a: B = (
7043 c(),
7044 ˇ
7045 );
7046 "});
7047
7048 // Paste it at the same position.
7049 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7050 cx.assert_editor_state(indoc! {"
7051 const a: B = (
7052 c(),
7053 d(
7054 e,
7055 f
7056 )ˇ
7057 );
7058 "});
7059
7060 // Paste it at a line with a lower indent level.
7061 cx.set_state(indoc! {"
7062 ˇ
7063 const a: B = (
7064 c(),
7065 );
7066 "});
7067 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7068 cx.assert_editor_state(indoc! {"
7069 d(
7070 e,
7071 f
7072 )ˇ
7073 const a: B = (
7074 c(),
7075 );
7076 "});
7077
7078 // Cut an indented block, with the leading whitespace.
7079 cx.set_state(indoc! {"
7080 const a: B = (
7081 c(),
7082 « d(
7083 e,
7084 f
7085 )
7086 ˇ»);
7087 "});
7088 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7089 cx.assert_editor_state(indoc! {"
7090 const a: B = (
7091 c(),
7092 ˇ);
7093 "});
7094
7095 // Paste it at the same position.
7096 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7097 cx.assert_editor_state(indoc! {"
7098 const a: B = (
7099 c(),
7100 d(
7101 e,
7102 f
7103 )
7104 ˇ);
7105 "});
7106
7107 // Paste it at a line with a higher indent level.
7108 cx.set_state(indoc! {"
7109 const a: B = (
7110 c(),
7111 d(
7112 e,
7113 fˇ
7114 )
7115 );
7116 "});
7117 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7118 cx.assert_editor_state(indoc! {"
7119 const a: B = (
7120 c(),
7121 d(
7122 e,
7123 f d(
7124 e,
7125 f
7126 )
7127 ˇ
7128 )
7129 );
7130 "});
7131
7132 // Copy an indented block, starting mid-line
7133 cx.set_state(indoc! {"
7134 const a: B = (
7135 c(),
7136 somethin«g(
7137 e,
7138 f
7139 )ˇ»
7140 );
7141 "});
7142 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7143
7144 // Paste it on a line with a lower indent level
7145 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7146 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7147 cx.assert_editor_state(indoc! {"
7148 const a: B = (
7149 c(),
7150 something(
7151 e,
7152 f
7153 )
7154 );
7155 g(
7156 e,
7157 f
7158 )ˇ"});
7159}
7160
7161#[gpui::test]
7162async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7163 init_test(cx, |_| {});
7164
7165 cx.write_to_clipboard(ClipboardItem::new_string(
7166 " d(\n e\n );\n".into(),
7167 ));
7168
7169 let mut cx = EditorTestContext::new(cx).await;
7170 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7171
7172 cx.set_state(indoc! {"
7173 fn a() {
7174 b();
7175 if c() {
7176 ˇ
7177 }
7178 }
7179 "});
7180
7181 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7182 cx.assert_editor_state(indoc! {"
7183 fn a() {
7184 b();
7185 if c() {
7186 d(
7187 e
7188 );
7189 ˇ
7190 }
7191 }
7192 "});
7193
7194 cx.set_state(indoc! {"
7195 fn a() {
7196 b();
7197 ˇ
7198 }
7199 "});
7200
7201 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7202 cx.assert_editor_state(indoc! {"
7203 fn a() {
7204 b();
7205 d(
7206 e
7207 );
7208 ˇ
7209 }
7210 "});
7211}
7212
7213#[gpui::test]
7214fn test_select_all(cx: &mut TestAppContext) {
7215 init_test(cx, |_| {});
7216
7217 let editor = cx.add_window(|window, cx| {
7218 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7219 build_editor(buffer, window, cx)
7220 });
7221 _ = editor.update(cx, |editor, window, cx| {
7222 editor.select_all(&SelectAll, window, cx);
7223 assert_eq!(
7224 editor.selections.display_ranges(cx),
7225 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7226 );
7227 });
7228}
7229
7230#[gpui::test]
7231fn test_select_line(cx: &mut TestAppContext) {
7232 init_test(cx, |_| {});
7233
7234 let editor = cx.add_window(|window, cx| {
7235 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7236 build_editor(buffer, window, cx)
7237 });
7238 _ = editor.update(cx, |editor, window, cx| {
7239 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7240 s.select_display_ranges([
7241 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7242 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7243 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7244 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7245 ])
7246 });
7247 editor.select_line(&SelectLine, window, cx);
7248 assert_eq!(
7249 editor.selections.display_ranges(cx),
7250 vec![
7251 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7252 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7253 ]
7254 );
7255 });
7256
7257 _ = editor.update(cx, |editor, window, cx| {
7258 editor.select_line(&SelectLine, window, cx);
7259 assert_eq!(
7260 editor.selections.display_ranges(cx),
7261 vec![
7262 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7263 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7264 ]
7265 );
7266 });
7267
7268 _ = editor.update(cx, |editor, window, cx| {
7269 editor.select_line(&SelectLine, window, cx);
7270 assert_eq!(
7271 editor.selections.display_ranges(cx),
7272 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7273 );
7274 });
7275}
7276
7277#[gpui::test]
7278async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7279 init_test(cx, |_| {});
7280 let mut cx = EditorTestContext::new(cx).await;
7281
7282 #[track_caller]
7283 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7284 cx.set_state(initial_state);
7285 cx.update_editor(|e, window, cx| {
7286 e.split_selection_into_lines(&Default::default(), window, cx)
7287 });
7288 cx.assert_editor_state(expected_state);
7289 }
7290
7291 // Selection starts and ends at the middle of lines, left-to-right
7292 test(
7293 &mut cx,
7294 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7295 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7296 );
7297 // Same thing, right-to-left
7298 test(
7299 &mut cx,
7300 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7301 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7302 );
7303
7304 // Whole buffer, left-to-right, last line *doesn't* end with newline
7305 test(
7306 &mut cx,
7307 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7308 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7309 );
7310 // Same thing, right-to-left
7311 test(
7312 &mut cx,
7313 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7314 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7315 );
7316
7317 // Whole buffer, left-to-right, last line ends with newline
7318 test(
7319 &mut cx,
7320 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7321 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7322 );
7323 // Same thing, right-to-left
7324 test(
7325 &mut cx,
7326 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7327 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7328 );
7329
7330 // Starts at the end of a line, ends at the start of another
7331 test(
7332 &mut cx,
7333 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7334 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7335 );
7336}
7337
7338#[gpui::test]
7339async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7340 init_test(cx, |_| {});
7341
7342 let editor = cx.add_window(|window, cx| {
7343 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7344 build_editor(buffer, window, cx)
7345 });
7346
7347 // setup
7348 _ = editor.update(cx, |editor, window, cx| {
7349 editor.fold_creases(
7350 vec![
7351 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7352 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7353 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7354 ],
7355 true,
7356 window,
7357 cx,
7358 );
7359 assert_eq!(
7360 editor.display_text(cx),
7361 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7362 );
7363 });
7364
7365 _ = editor.update(cx, |editor, window, cx| {
7366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7367 s.select_display_ranges([
7368 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7369 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7370 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7371 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7372 ])
7373 });
7374 editor.split_selection_into_lines(&Default::default(), window, cx);
7375 assert_eq!(
7376 editor.display_text(cx),
7377 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7378 );
7379 });
7380 EditorTestContext::for_editor(editor, cx)
7381 .await
7382 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7383
7384 _ = editor.update(cx, |editor, window, cx| {
7385 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7386 s.select_display_ranges([
7387 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7388 ])
7389 });
7390 editor.split_selection_into_lines(&Default::default(), window, cx);
7391 assert_eq!(
7392 editor.display_text(cx),
7393 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7394 );
7395 assert_eq!(
7396 editor.selections.display_ranges(cx),
7397 [
7398 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7399 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7400 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7401 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7402 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7403 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7404 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7405 ]
7406 );
7407 });
7408 EditorTestContext::for_editor(editor, cx)
7409 .await
7410 .assert_editor_state(
7411 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7412 );
7413}
7414
7415#[gpui::test]
7416async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7417 init_test(cx, |_| {});
7418
7419 let mut cx = EditorTestContext::new(cx).await;
7420
7421 cx.set_state(indoc!(
7422 r#"abc
7423 defˇghi
7424
7425 jk
7426 nlmo
7427 "#
7428 ));
7429
7430 cx.update_editor(|editor, window, cx| {
7431 editor.add_selection_above(&Default::default(), window, cx);
7432 });
7433
7434 cx.assert_editor_state(indoc!(
7435 r#"abcˇ
7436 defˇghi
7437
7438 jk
7439 nlmo
7440 "#
7441 ));
7442
7443 cx.update_editor(|editor, window, cx| {
7444 editor.add_selection_above(&Default::default(), window, cx);
7445 });
7446
7447 cx.assert_editor_state(indoc!(
7448 r#"abcˇ
7449 defˇghi
7450
7451 jk
7452 nlmo
7453 "#
7454 ));
7455
7456 cx.update_editor(|editor, window, cx| {
7457 editor.add_selection_below(&Default::default(), window, cx);
7458 });
7459
7460 cx.assert_editor_state(indoc!(
7461 r#"abc
7462 defˇghi
7463
7464 jk
7465 nlmo
7466 "#
7467 ));
7468
7469 cx.update_editor(|editor, window, cx| {
7470 editor.undo_selection(&Default::default(), window, cx);
7471 });
7472
7473 cx.assert_editor_state(indoc!(
7474 r#"abcˇ
7475 defˇghi
7476
7477 jk
7478 nlmo
7479 "#
7480 ));
7481
7482 cx.update_editor(|editor, window, cx| {
7483 editor.redo_selection(&Default::default(), window, cx);
7484 });
7485
7486 cx.assert_editor_state(indoc!(
7487 r#"abc
7488 defˇghi
7489
7490 jk
7491 nlmo
7492 "#
7493 ));
7494
7495 cx.update_editor(|editor, window, cx| {
7496 editor.add_selection_below(&Default::default(), window, cx);
7497 });
7498
7499 cx.assert_editor_state(indoc!(
7500 r#"abc
7501 defˇghi
7502 ˇ
7503 jk
7504 nlmo
7505 "#
7506 ));
7507
7508 cx.update_editor(|editor, window, cx| {
7509 editor.add_selection_below(&Default::default(), window, cx);
7510 });
7511
7512 cx.assert_editor_state(indoc!(
7513 r#"abc
7514 defˇghi
7515 ˇ
7516 jkˇ
7517 nlmo
7518 "#
7519 ));
7520
7521 cx.update_editor(|editor, window, cx| {
7522 editor.add_selection_below(&Default::default(), window, cx);
7523 });
7524
7525 cx.assert_editor_state(indoc!(
7526 r#"abc
7527 defˇghi
7528 ˇ
7529 jkˇ
7530 nlmˇo
7531 "#
7532 ));
7533
7534 cx.update_editor(|editor, window, cx| {
7535 editor.add_selection_below(&Default::default(), window, cx);
7536 });
7537
7538 cx.assert_editor_state(indoc!(
7539 r#"abc
7540 defˇghi
7541 ˇ
7542 jkˇ
7543 nlmˇo
7544 ˇ"#
7545 ));
7546
7547 // change selections
7548 cx.set_state(indoc!(
7549 r#"abc
7550 def«ˇg»hi
7551
7552 jk
7553 nlmo
7554 "#
7555 ));
7556
7557 cx.update_editor(|editor, window, cx| {
7558 editor.add_selection_below(&Default::default(), window, cx);
7559 });
7560
7561 cx.assert_editor_state(indoc!(
7562 r#"abc
7563 def«ˇg»hi
7564
7565 jk
7566 nlm«ˇo»
7567 "#
7568 ));
7569
7570 cx.update_editor(|editor, window, cx| {
7571 editor.add_selection_below(&Default::default(), window, cx);
7572 });
7573
7574 cx.assert_editor_state(indoc!(
7575 r#"abc
7576 def«ˇg»hi
7577
7578 jk
7579 nlm«ˇo»
7580 "#
7581 ));
7582
7583 cx.update_editor(|editor, window, cx| {
7584 editor.add_selection_above(&Default::default(), window, cx);
7585 });
7586
7587 cx.assert_editor_state(indoc!(
7588 r#"abc
7589 def«ˇg»hi
7590
7591 jk
7592 nlmo
7593 "#
7594 ));
7595
7596 cx.update_editor(|editor, window, cx| {
7597 editor.add_selection_above(&Default::default(), window, cx);
7598 });
7599
7600 cx.assert_editor_state(indoc!(
7601 r#"abc
7602 def«ˇg»hi
7603
7604 jk
7605 nlmo
7606 "#
7607 ));
7608
7609 // Change selections again
7610 cx.set_state(indoc!(
7611 r#"a«bc
7612 defgˇ»hi
7613
7614 jk
7615 nlmo
7616 "#
7617 ));
7618
7619 cx.update_editor(|editor, window, cx| {
7620 editor.add_selection_below(&Default::default(), window, cx);
7621 });
7622
7623 cx.assert_editor_state(indoc!(
7624 r#"a«bcˇ»
7625 d«efgˇ»hi
7626
7627 j«kˇ»
7628 nlmo
7629 "#
7630 ));
7631
7632 cx.update_editor(|editor, window, cx| {
7633 editor.add_selection_below(&Default::default(), window, cx);
7634 });
7635 cx.assert_editor_state(indoc!(
7636 r#"a«bcˇ»
7637 d«efgˇ»hi
7638
7639 j«kˇ»
7640 n«lmoˇ»
7641 "#
7642 ));
7643 cx.update_editor(|editor, window, cx| {
7644 editor.add_selection_above(&Default::default(), window, cx);
7645 });
7646
7647 cx.assert_editor_state(indoc!(
7648 r#"a«bcˇ»
7649 d«efgˇ»hi
7650
7651 j«kˇ»
7652 nlmo
7653 "#
7654 ));
7655
7656 // Change selections again
7657 cx.set_state(indoc!(
7658 r#"abc
7659 d«ˇefghi
7660
7661 jk
7662 nlm»o
7663 "#
7664 ));
7665
7666 cx.update_editor(|editor, window, cx| {
7667 editor.add_selection_above(&Default::default(), window, cx);
7668 });
7669
7670 cx.assert_editor_state(indoc!(
7671 r#"a«ˇbc»
7672 d«ˇef»ghi
7673
7674 j«ˇk»
7675 n«ˇlm»o
7676 "#
7677 ));
7678
7679 cx.update_editor(|editor, window, cx| {
7680 editor.add_selection_below(&Default::default(), window, cx);
7681 });
7682
7683 cx.assert_editor_state(indoc!(
7684 r#"abc
7685 d«ˇef»ghi
7686
7687 j«ˇk»
7688 n«ˇlm»o
7689 "#
7690 ));
7691}
7692
7693#[gpui::test]
7694async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7695 init_test(cx, |_| {});
7696 let mut cx = EditorTestContext::new(cx).await;
7697
7698 cx.set_state(indoc!(
7699 r#"line onˇe
7700 liˇne two
7701 line three
7702 line four"#
7703 ));
7704
7705 cx.update_editor(|editor, window, cx| {
7706 editor.add_selection_below(&Default::default(), window, cx);
7707 });
7708
7709 // test multiple cursors expand in the same direction
7710 cx.assert_editor_state(indoc!(
7711 r#"line onˇe
7712 liˇne twˇo
7713 liˇne three
7714 line four"#
7715 ));
7716
7717 cx.update_editor(|editor, window, cx| {
7718 editor.add_selection_below(&Default::default(), window, cx);
7719 });
7720
7721 cx.update_editor(|editor, window, cx| {
7722 editor.add_selection_below(&Default::default(), window, cx);
7723 });
7724
7725 // test multiple cursors expand below overflow
7726 cx.assert_editor_state(indoc!(
7727 r#"line onˇe
7728 liˇne twˇo
7729 liˇne thˇree
7730 liˇne foˇur"#
7731 ));
7732
7733 cx.update_editor(|editor, window, cx| {
7734 editor.add_selection_above(&Default::default(), window, cx);
7735 });
7736
7737 // test multiple cursors retrieves back correctly
7738 cx.assert_editor_state(indoc!(
7739 r#"line onˇe
7740 liˇne twˇo
7741 liˇne thˇree
7742 line four"#
7743 ));
7744
7745 cx.update_editor(|editor, window, cx| {
7746 editor.add_selection_above(&Default::default(), window, cx);
7747 });
7748
7749 cx.update_editor(|editor, window, cx| {
7750 editor.add_selection_above(&Default::default(), window, cx);
7751 });
7752
7753 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7754 cx.assert_editor_state(indoc!(
7755 r#"liˇne onˇe
7756 liˇne two
7757 line three
7758 line four"#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.undo_selection(&Default::default(), window, cx);
7763 });
7764
7765 // test undo
7766 cx.assert_editor_state(indoc!(
7767 r#"line onˇe
7768 liˇne twˇo
7769 line three
7770 line four"#
7771 ));
7772
7773 cx.update_editor(|editor, window, cx| {
7774 editor.redo_selection(&Default::default(), window, cx);
7775 });
7776
7777 // test redo
7778 cx.assert_editor_state(indoc!(
7779 r#"liˇne onˇe
7780 liˇne two
7781 line three
7782 line four"#
7783 ));
7784
7785 cx.set_state(indoc!(
7786 r#"abcd
7787 ef«ghˇ»
7788 ijkl
7789 «mˇ»nop"#
7790 ));
7791
7792 cx.update_editor(|editor, window, cx| {
7793 editor.add_selection_above(&Default::default(), window, cx);
7794 });
7795
7796 // test multiple selections expand in the same direction
7797 cx.assert_editor_state(indoc!(
7798 r#"ab«cdˇ»
7799 ef«ghˇ»
7800 «iˇ»jkl
7801 «mˇ»nop"#
7802 ));
7803
7804 cx.update_editor(|editor, window, cx| {
7805 editor.add_selection_above(&Default::default(), window, cx);
7806 });
7807
7808 // test multiple selection upward overflow
7809 cx.assert_editor_state(indoc!(
7810 r#"ab«cdˇ»
7811 «eˇ»f«ghˇ»
7812 «iˇ»jkl
7813 «mˇ»nop"#
7814 ));
7815
7816 cx.update_editor(|editor, window, cx| {
7817 editor.add_selection_below(&Default::default(), window, cx);
7818 });
7819
7820 // test multiple selection retrieves back correctly
7821 cx.assert_editor_state(indoc!(
7822 r#"abcd
7823 ef«ghˇ»
7824 «iˇ»jkl
7825 «mˇ»nop"#
7826 ));
7827
7828 cx.update_editor(|editor, window, cx| {
7829 editor.add_selection_below(&Default::default(), window, cx);
7830 });
7831
7832 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7833 cx.assert_editor_state(indoc!(
7834 r#"abcd
7835 ef«ghˇ»
7836 ij«klˇ»
7837 «mˇ»nop"#
7838 ));
7839
7840 cx.update_editor(|editor, window, cx| {
7841 editor.undo_selection(&Default::default(), window, cx);
7842 });
7843
7844 // test undo
7845 cx.assert_editor_state(indoc!(
7846 r#"abcd
7847 ef«ghˇ»
7848 «iˇ»jkl
7849 «mˇ»nop"#
7850 ));
7851
7852 cx.update_editor(|editor, window, cx| {
7853 editor.redo_selection(&Default::default(), window, cx);
7854 });
7855
7856 // test redo
7857 cx.assert_editor_state(indoc!(
7858 r#"abcd
7859 ef«ghˇ»
7860 ij«klˇ»
7861 «mˇ»nop"#
7862 ));
7863}
7864
7865#[gpui::test]
7866async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7867 init_test(cx, |_| {});
7868 let mut cx = EditorTestContext::new(cx).await;
7869
7870 cx.set_state(indoc!(
7871 r#"line onˇe
7872 liˇne two
7873 line three
7874 line four"#
7875 ));
7876
7877 cx.update_editor(|editor, window, cx| {
7878 editor.add_selection_below(&Default::default(), window, cx);
7879 editor.add_selection_below(&Default::default(), window, cx);
7880 editor.add_selection_below(&Default::default(), window, cx);
7881 });
7882
7883 // initial state with two multi cursor groups
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 // add single cursor in middle - simulate opt click
7892 cx.update_editor(|editor, window, cx| {
7893 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7894 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7895 editor.end_selection(window, cx);
7896 });
7897
7898 cx.assert_editor_state(indoc!(
7899 r#"line onˇe
7900 liˇne twˇo
7901 liˇneˇ thˇree
7902 liˇne foˇur"#
7903 ));
7904
7905 cx.update_editor(|editor, window, cx| {
7906 editor.add_selection_above(&Default::default(), window, cx);
7907 });
7908
7909 // test new added selection expands above and existing selection shrinks
7910 cx.assert_editor_state(indoc!(
7911 r#"line onˇe
7912 liˇneˇ twˇo
7913 liˇneˇ thˇree
7914 line four"#
7915 ));
7916
7917 cx.update_editor(|editor, window, cx| {
7918 editor.add_selection_above(&Default::default(), window, cx);
7919 });
7920
7921 // test new added selection expands above and existing selection shrinks
7922 cx.assert_editor_state(indoc!(
7923 r#"lineˇ onˇe
7924 liˇneˇ twˇo
7925 lineˇ three
7926 line four"#
7927 ));
7928
7929 // intial state with two selection groups
7930 cx.set_state(indoc!(
7931 r#"abcd
7932 ef«ghˇ»
7933 ijkl
7934 «mˇ»nop"#
7935 ));
7936
7937 cx.update_editor(|editor, window, cx| {
7938 editor.add_selection_above(&Default::default(), window, cx);
7939 editor.add_selection_above(&Default::default(), window, cx);
7940 });
7941
7942 cx.assert_editor_state(indoc!(
7943 r#"ab«cdˇ»
7944 «eˇ»f«ghˇ»
7945 «iˇ»jkl
7946 «mˇ»nop"#
7947 ));
7948
7949 // add single selection in middle - simulate opt drag
7950 cx.update_editor(|editor, window, cx| {
7951 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7952 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7953 editor.update_selection(
7954 DisplayPoint::new(DisplayRow(2), 4),
7955 0,
7956 gpui::Point::<f32>::default(),
7957 window,
7958 cx,
7959 );
7960 editor.end_selection(window, cx);
7961 });
7962
7963 cx.assert_editor_state(indoc!(
7964 r#"ab«cdˇ»
7965 «eˇ»f«ghˇ»
7966 «iˇ»jk«lˇ»
7967 «mˇ»nop"#
7968 ));
7969
7970 cx.update_editor(|editor, window, cx| {
7971 editor.add_selection_below(&Default::default(), window, cx);
7972 });
7973
7974 // test new added selection expands below, others shrinks from above
7975 cx.assert_editor_state(indoc!(
7976 r#"abcd
7977 ef«ghˇ»
7978 «iˇ»jk«lˇ»
7979 «mˇ»no«pˇ»"#
7980 ));
7981}
7982
7983#[gpui::test]
7984async fn test_select_next(cx: &mut TestAppContext) {
7985 init_test(cx, |_| {});
7986
7987 let mut cx = EditorTestContext::new(cx).await;
7988 cx.set_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\nabc");
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\nabc");
7997
7998 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7999 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8000
8001 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8002 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8003
8004 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8005 .unwrap();
8006 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8007
8008 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8009 .unwrap();
8010 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8011
8012 // Test selection direction should be preserved
8013 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8014
8015 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8016 .unwrap();
8017 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8018}
8019
8020#[gpui::test]
8021async fn test_select_all_matches(cx: &mut TestAppContext) {
8022 init_test(cx, |_| {});
8023
8024 let mut cx = EditorTestContext::new(cx).await;
8025
8026 // Test caret-only selections
8027 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8028 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8029 .unwrap();
8030 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8031
8032 // Test left-to-right selections
8033 cx.set_state("abc\n«abcˇ»\nabc");
8034 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8035 .unwrap();
8036 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8037
8038 // Test right-to-left selections
8039 cx.set_state("abc\n«ˇabc»\nabc");
8040 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8041 .unwrap();
8042 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8043
8044 // Test selecting whitespace with caret selection
8045 cx.set_state("abc\nˇ abc\nabc");
8046 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8047 .unwrap();
8048 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8049
8050 // Test selecting whitespace with left-to-right selection
8051 cx.set_state("abc\n«ˇ »abc\nabc");
8052 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8053 .unwrap();
8054 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8055
8056 // Test no matches with right-to-left selection
8057 cx.set_state("abc\n« ˇ»abc\nabc");
8058 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8059 .unwrap();
8060 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8061
8062 // Test with a single word and clip_at_line_ends=true (#29823)
8063 cx.set_state("aˇbc");
8064 cx.update_editor(|e, window, cx| {
8065 e.set_clip_at_line_ends(true, cx);
8066 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8067 e.set_clip_at_line_ends(false, cx);
8068 });
8069 cx.assert_editor_state("«abcˇ»");
8070}
8071
8072#[gpui::test]
8073async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8074 init_test(cx, |_| {});
8075
8076 let mut cx = EditorTestContext::new(cx).await;
8077
8078 let large_body_1 = "\nd".repeat(200);
8079 let large_body_2 = "\ne".repeat(200);
8080
8081 cx.set_state(&format!(
8082 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8083 ));
8084 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8085 let scroll_position = editor.scroll_position(cx);
8086 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8087 scroll_position
8088 });
8089
8090 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8091 .unwrap();
8092 cx.assert_editor_state(&format!(
8093 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8094 ));
8095 let scroll_position_after_selection =
8096 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8097 assert_eq!(
8098 initial_scroll_position, scroll_position_after_selection,
8099 "Scroll position should not change after selecting all matches"
8100 );
8101}
8102
8103#[gpui::test]
8104async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8105 init_test(cx, |_| {});
8106
8107 let mut cx = EditorLspTestContext::new_rust(
8108 lsp::ServerCapabilities {
8109 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8110 ..Default::default()
8111 },
8112 cx,
8113 )
8114 .await;
8115
8116 cx.set_state(indoc! {"
8117 line 1
8118 line 2
8119 linˇe 3
8120 line 4
8121 line 5
8122 "});
8123
8124 // Make an edit
8125 cx.update_editor(|editor, window, cx| {
8126 editor.handle_input("X", window, cx);
8127 });
8128
8129 // Move cursor to a different position
8130 cx.update_editor(|editor, window, cx| {
8131 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8132 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8133 });
8134 });
8135
8136 cx.assert_editor_state(indoc! {"
8137 line 1
8138 line 2
8139 linXe 3
8140 line 4
8141 liˇne 5
8142 "});
8143
8144 cx.lsp
8145 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8146 Ok(Some(vec![lsp::TextEdit::new(
8147 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8148 "PREFIX ".to_string(),
8149 )]))
8150 });
8151
8152 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8153 .unwrap()
8154 .await
8155 .unwrap();
8156
8157 cx.assert_editor_state(indoc! {"
8158 PREFIX line 1
8159 line 2
8160 linXe 3
8161 line 4
8162 liˇne 5
8163 "});
8164
8165 // Undo formatting
8166 cx.update_editor(|editor, window, cx| {
8167 editor.undo(&Default::default(), window, cx);
8168 });
8169
8170 // Verify cursor moved back to position after edit
8171 cx.assert_editor_state(indoc! {"
8172 line 1
8173 line 2
8174 linXˇe 3
8175 line 4
8176 line 5
8177 "});
8178}
8179
8180#[gpui::test]
8181async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8182 init_test(cx, |_| {});
8183
8184 let mut cx = EditorTestContext::new(cx).await;
8185
8186 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8187 cx.update_editor(|editor, window, cx| {
8188 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8189 });
8190
8191 cx.set_state(indoc! {"
8192 line 1
8193 line 2
8194 linˇe 3
8195 line 4
8196 line 5
8197 line 6
8198 line 7
8199 line 8
8200 line 9
8201 line 10
8202 "});
8203
8204 let snapshot = cx.buffer_snapshot();
8205 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8206
8207 cx.update(|_, cx| {
8208 provider.update(cx, |provider, _| {
8209 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8210 id: None,
8211 edits: vec![(edit_position..edit_position, "X".into())],
8212 edit_preview: None,
8213 }))
8214 })
8215 });
8216
8217 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8218 cx.update_editor(|editor, window, cx| {
8219 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8220 });
8221
8222 cx.assert_editor_state(indoc! {"
8223 line 1
8224 line 2
8225 lineXˇ 3
8226 line 4
8227 line 5
8228 line 6
8229 line 7
8230 line 8
8231 line 9
8232 line 10
8233 "});
8234
8235 cx.update_editor(|editor, window, cx| {
8236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8237 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8238 });
8239 });
8240
8241 cx.assert_editor_state(indoc! {"
8242 line 1
8243 line 2
8244 lineX 3
8245 line 4
8246 line 5
8247 line 6
8248 line 7
8249 line 8
8250 line 9
8251 liˇne 10
8252 "});
8253
8254 cx.update_editor(|editor, window, cx| {
8255 editor.undo(&Default::default(), window, cx);
8256 });
8257
8258 cx.assert_editor_state(indoc! {"
8259 line 1
8260 line 2
8261 lineˇ 3
8262 line 4
8263 line 5
8264 line 6
8265 line 7
8266 line 8
8267 line 9
8268 line 10
8269 "});
8270}
8271
8272#[gpui::test]
8273async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8274 init_test(cx, |_| {});
8275
8276 let mut cx = EditorTestContext::new(cx).await;
8277 cx.set_state(
8278 r#"let foo = 2;
8279lˇet foo = 2;
8280let fooˇ = 2;
8281let foo = 2;
8282let foo = ˇ2;"#,
8283 );
8284
8285 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8286 .unwrap();
8287 cx.assert_editor_state(
8288 r#"let foo = 2;
8289«letˇ» foo = 2;
8290let «fooˇ» = 2;
8291let foo = 2;
8292let foo = «2ˇ»;"#,
8293 );
8294
8295 // noop for multiple selections with different contents
8296 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8297 .unwrap();
8298 cx.assert_editor_state(
8299 r#"let foo = 2;
8300«letˇ» foo = 2;
8301let «fooˇ» = 2;
8302let foo = 2;
8303let foo = «2ˇ»;"#,
8304 );
8305
8306 // Test last selection direction should be preserved
8307 cx.set_state(
8308 r#"let foo = 2;
8309let foo = 2;
8310let «fooˇ» = 2;
8311let «ˇfoo» = 2;
8312let foo = 2;"#,
8313 );
8314
8315 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8316 .unwrap();
8317 cx.assert_editor_state(
8318 r#"let foo = 2;
8319let foo = 2;
8320let «fooˇ» = 2;
8321let «ˇfoo» = 2;
8322let «ˇfoo» = 2;"#,
8323 );
8324}
8325
8326#[gpui::test]
8327async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8328 init_test(cx, |_| {});
8329
8330 let mut cx =
8331 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8332
8333 cx.assert_editor_state(indoc! {"
8334 ˇbbb
8335 ccc
8336
8337 bbb
8338 ccc
8339 "});
8340 cx.dispatch_action(SelectPrevious::default());
8341 cx.assert_editor_state(indoc! {"
8342 «bbbˇ»
8343 ccc
8344
8345 bbb
8346 ccc
8347 "});
8348 cx.dispatch_action(SelectPrevious::default());
8349 cx.assert_editor_state(indoc! {"
8350 «bbbˇ»
8351 ccc
8352
8353 «bbbˇ»
8354 ccc
8355 "});
8356}
8357
8358#[gpui::test]
8359async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8360 init_test(cx, |_| {});
8361
8362 let mut cx = EditorTestContext::new(cx).await;
8363 cx.set_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\nabc");
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\nabc");
8372
8373 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8374 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8375
8376 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8377 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8378
8379 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8380 .unwrap();
8381 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8382
8383 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8384 .unwrap();
8385 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8386}
8387
8388#[gpui::test]
8389async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8390 init_test(cx, |_| {});
8391
8392 let mut cx = EditorTestContext::new(cx).await;
8393 cx.set_state("aˇ");
8394
8395 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8396 .unwrap();
8397 cx.assert_editor_state("«aˇ»");
8398 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8399 .unwrap();
8400 cx.assert_editor_state("«aˇ»");
8401}
8402
8403#[gpui::test]
8404async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8405 init_test(cx, |_| {});
8406
8407 let mut cx = EditorTestContext::new(cx).await;
8408 cx.set_state(
8409 r#"let foo = 2;
8410lˇet foo = 2;
8411let fooˇ = 2;
8412let foo = 2;
8413let foo = ˇ2;"#,
8414 );
8415
8416 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8417 .unwrap();
8418 cx.assert_editor_state(
8419 r#"let foo = 2;
8420«letˇ» foo = 2;
8421let «fooˇ» = 2;
8422let foo = 2;
8423let foo = «2ˇ»;"#,
8424 );
8425
8426 // noop for multiple selections with different contents
8427 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8428 .unwrap();
8429 cx.assert_editor_state(
8430 r#"let foo = 2;
8431«letˇ» foo = 2;
8432let «fooˇ» = 2;
8433let foo = 2;
8434let foo = «2ˇ»;"#,
8435 );
8436}
8437
8438#[gpui::test]
8439async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8440 init_test(cx, |_| {});
8441
8442 let mut cx = EditorTestContext::new(cx).await;
8443 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8444
8445 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8446 .unwrap();
8447 // selection direction is preserved
8448 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
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\ndefabc\n«ˇabc»");
8453
8454 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8455 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8456
8457 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8458 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8459
8460 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8461 .unwrap();
8462 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8463
8464 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8465 .unwrap();
8466 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8467}
8468
8469#[gpui::test]
8470async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8471 init_test(cx, |_| {});
8472
8473 let language = Arc::new(Language::new(
8474 LanguageConfig::default(),
8475 Some(tree_sitter_rust::LANGUAGE.into()),
8476 ));
8477
8478 let text = r#"
8479 use mod1::mod2::{mod3, mod4};
8480
8481 fn fn_1(param1: bool, param2: &str) {
8482 let var1 = "text";
8483 }
8484 "#
8485 .unindent();
8486
8487 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8488 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8489 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8490
8491 editor
8492 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8493 .await;
8494
8495 editor.update_in(cx, |editor, window, cx| {
8496 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8497 s.select_display_ranges([
8498 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8499 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8500 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8501 ]);
8502 });
8503 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8504 });
8505 editor.update(cx, |editor, cx| {
8506 assert_text_with_selections(
8507 editor,
8508 indoc! {r#"
8509 use mod1::mod2::{mod3, «mod4ˇ»};
8510
8511 fn fn_1«ˇ(param1: bool, param2: &str)» {
8512 let var1 = "«ˇtext»";
8513 }
8514 "#},
8515 cx,
8516 );
8517 });
8518
8519 editor.update_in(cx, |editor, window, cx| {
8520 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8521 });
8522 editor.update(cx, |editor, cx| {
8523 assert_text_with_selections(
8524 editor,
8525 indoc! {r#"
8526 use mod1::mod2::«{mod3, mod4}ˇ»;
8527
8528 «ˇfn fn_1(param1: bool, param2: &str) {
8529 let var1 = "text";
8530 }»
8531 "#},
8532 cx,
8533 );
8534 });
8535
8536 editor.update_in(cx, |editor, window, cx| {
8537 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8538 });
8539 assert_eq!(
8540 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8541 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8542 );
8543
8544 // Trying to expand the selected syntax node one more time has no effect.
8545 editor.update_in(cx, |editor, window, cx| {
8546 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8547 });
8548 assert_eq!(
8549 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8550 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8551 );
8552
8553 editor.update_in(cx, |editor, window, cx| {
8554 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8555 });
8556 editor.update(cx, |editor, cx| {
8557 assert_text_with_selections(
8558 editor,
8559 indoc! {r#"
8560 use mod1::mod2::«{mod3, mod4}ˇ»;
8561
8562 «ˇfn fn_1(param1: bool, param2: &str) {
8563 let var1 = "text";
8564 }»
8565 "#},
8566 cx,
8567 );
8568 });
8569
8570 editor.update_in(cx, |editor, window, cx| {
8571 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8572 });
8573 editor.update(cx, |editor, cx| {
8574 assert_text_with_selections(
8575 editor,
8576 indoc! {r#"
8577 use mod1::mod2::{mod3, «mod4ˇ»};
8578
8579 fn fn_1«ˇ(param1: bool, param2: &str)» {
8580 let var1 = "«ˇtext»";
8581 }
8582 "#},
8583 cx,
8584 );
8585 });
8586
8587 editor.update_in(cx, |editor, window, cx| {
8588 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8589 });
8590 editor.update(cx, |editor, cx| {
8591 assert_text_with_selections(
8592 editor,
8593 indoc! {r#"
8594 use mod1::mod2::{mod3, mo«ˇ»d4};
8595
8596 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8597 let var1 = "te«ˇ»xt";
8598 }
8599 "#},
8600 cx,
8601 );
8602 });
8603
8604 // Trying to shrink the selected syntax node one more time has no effect.
8605 editor.update_in(cx, |editor, window, cx| {
8606 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8607 });
8608 editor.update_in(cx, |editor, _, cx| {
8609 assert_text_with_selections(
8610 editor,
8611 indoc! {r#"
8612 use mod1::mod2::{mod3, mo«ˇ»d4};
8613
8614 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8615 let var1 = "te«ˇ»xt";
8616 }
8617 "#},
8618 cx,
8619 );
8620 });
8621
8622 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8623 // a fold.
8624 editor.update_in(cx, |editor, window, cx| {
8625 editor.fold_creases(
8626 vec![
8627 Crease::simple(
8628 Point::new(0, 21)..Point::new(0, 24),
8629 FoldPlaceholder::test(),
8630 ),
8631 Crease::simple(
8632 Point::new(3, 20)..Point::new(3, 22),
8633 FoldPlaceholder::test(),
8634 ),
8635 ],
8636 true,
8637 window,
8638 cx,
8639 );
8640 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8641 });
8642 editor.update(cx, |editor, cx| {
8643 assert_text_with_selections(
8644 editor,
8645 indoc! {r#"
8646 use mod1::mod2::«{mod3, mod4}ˇ»;
8647
8648 fn fn_1«ˇ(param1: bool, param2: &str)» {
8649 let var1 = "«ˇtext»";
8650 }
8651 "#},
8652 cx,
8653 );
8654 });
8655}
8656
8657#[gpui::test]
8658async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8659 init_test(cx, |_| {});
8660
8661 let language = Arc::new(Language::new(
8662 LanguageConfig::default(),
8663 Some(tree_sitter_rust::LANGUAGE.into()),
8664 ));
8665
8666 let text = "let a = 2;";
8667
8668 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8669 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8670 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8671
8672 editor
8673 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8674 .await;
8675
8676 // Test case 1: Cursor at end of word
8677 editor.update_in(cx, |editor, window, cx| {
8678 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8679 s.select_display_ranges([
8680 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8681 ]);
8682 });
8683 });
8684 editor.update(cx, |editor, cx| {
8685 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8686 });
8687 editor.update_in(cx, |editor, window, cx| {
8688 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8689 });
8690 editor.update(cx, |editor, cx| {
8691 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8692 });
8693 editor.update_in(cx, |editor, window, cx| {
8694 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8695 });
8696 editor.update(cx, |editor, cx| {
8697 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8698 });
8699
8700 // Test case 2: Cursor at end of statement
8701 editor.update_in(cx, |editor, window, cx| {
8702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8703 s.select_display_ranges([
8704 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8705 ]);
8706 });
8707 });
8708 editor.update(cx, |editor, cx| {
8709 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8710 });
8711 editor.update_in(cx, |editor, window, cx| {
8712 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8713 });
8714 editor.update(cx, |editor, cx| {
8715 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8716 });
8717}
8718
8719#[gpui::test]
8720async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8721 init_test(cx, |_| {});
8722
8723 let language = Arc::new(Language::new(
8724 LanguageConfig::default(),
8725 Some(tree_sitter_rust::LANGUAGE.into()),
8726 ));
8727
8728 let text = r#"
8729 use mod1::mod2::{mod3, mod4};
8730
8731 fn fn_1(param1: bool, param2: &str) {
8732 let var1 = "hello world";
8733 }
8734 "#
8735 .unindent();
8736
8737 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8738 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8739 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8740
8741 editor
8742 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8743 .await;
8744
8745 // Test 1: Cursor on a letter of a string word
8746 editor.update_in(cx, |editor, window, cx| {
8747 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8748 s.select_display_ranges([
8749 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8750 ]);
8751 });
8752 });
8753 editor.update_in(cx, |editor, window, cx| {
8754 assert_text_with_selections(
8755 editor,
8756 indoc! {r#"
8757 use mod1::mod2::{mod3, mod4};
8758
8759 fn fn_1(param1: bool, param2: &str) {
8760 let var1 = "hˇello world";
8761 }
8762 "#},
8763 cx,
8764 );
8765 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8766 assert_text_with_selections(
8767 editor,
8768 indoc! {r#"
8769 use mod1::mod2::{mod3, mod4};
8770
8771 fn fn_1(param1: bool, param2: &str) {
8772 let var1 = "«ˇhello» world";
8773 }
8774 "#},
8775 cx,
8776 );
8777 });
8778
8779 // Test 2: Partial selection within a word
8780 editor.update_in(cx, |editor, window, cx| {
8781 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8782 s.select_display_ranges([
8783 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
8784 ]);
8785 });
8786 });
8787 editor.update_in(cx, |editor, window, cx| {
8788 assert_text_with_selections(
8789 editor,
8790 indoc! {r#"
8791 use mod1::mod2::{mod3, mod4};
8792
8793 fn fn_1(param1: bool, param2: &str) {
8794 let var1 = "h«elˇ»lo world";
8795 }
8796 "#},
8797 cx,
8798 );
8799 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8800 assert_text_with_selections(
8801 editor,
8802 indoc! {r#"
8803 use mod1::mod2::{mod3, mod4};
8804
8805 fn fn_1(param1: bool, param2: &str) {
8806 let var1 = "«ˇhello» world";
8807 }
8808 "#},
8809 cx,
8810 );
8811 });
8812
8813 // Test 3: Complete word already selected
8814 editor.update_in(cx, |editor, window, cx| {
8815 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8816 s.select_display_ranges([
8817 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
8818 ]);
8819 });
8820 });
8821 editor.update_in(cx, |editor, window, cx| {
8822 assert_text_with_selections(
8823 editor,
8824 indoc! {r#"
8825 use mod1::mod2::{mod3, mod4};
8826
8827 fn fn_1(param1: bool, param2: &str) {
8828 let var1 = "«helloˇ» world";
8829 }
8830 "#},
8831 cx,
8832 );
8833 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8834 assert_text_with_selections(
8835 editor,
8836 indoc! {r#"
8837 use mod1::mod2::{mod3, mod4};
8838
8839 fn fn_1(param1: bool, param2: &str) {
8840 let var1 = "«hello worldˇ»";
8841 }
8842 "#},
8843 cx,
8844 );
8845 });
8846
8847 // Test 4: Selection spanning across words
8848 editor.update_in(cx, |editor, window, cx| {
8849 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8850 s.select_display_ranges([
8851 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
8852 ]);
8853 });
8854 });
8855 editor.update_in(cx, |editor, window, cx| {
8856 assert_text_with_selections(
8857 editor,
8858 indoc! {r#"
8859 use mod1::mod2::{mod3, mod4};
8860
8861 fn fn_1(param1: bool, param2: &str) {
8862 let var1 = "hel«lo woˇ»rld";
8863 }
8864 "#},
8865 cx,
8866 );
8867 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8868 assert_text_with_selections(
8869 editor,
8870 indoc! {r#"
8871 use mod1::mod2::{mod3, mod4};
8872
8873 fn fn_1(param1: bool, param2: &str) {
8874 let var1 = "«ˇhello world»";
8875 }
8876 "#},
8877 cx,
8878 );
8879 });
8880
8881 // Test 5: Expansion beyond string
8882 editor.update_in(cx, |editor, window, cx| {
8883 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8884 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8885 assert_text_with_selections(
8886 editor,
8887 indoc! {r#"
8888 use mod1::mod2::{mod3, mod4};
8889
8890 fn fn_1(param1: bool, param2: &str) {
8891 «ˇlet var1 = "hello world";»
8892 }
8893 "#},
8894 cx,
8895 );
8896 });
8897}
8898
8899#[gpui::test]
8900async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
8901 init_test(cx, |_| {});
8902
8903 let mut cx = EditorTestContext::new(cx).await;
8904
8905 let language = Arc::new(Language::new(
8906 LanguageConfig::default(),
8907 Some(tree_sitter_rust::LANGUAGE.into()),
8908 ));
8909
8910 cx.update_buffer(|buffer, cx| {
8911 buffer.set_language(Some(language), cx);
8912 });
8913
8914 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
8915 cx.update_editor(|editor, window, cx| {
8916 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8917 });
8918
8919 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
8920}
8921
8922#[gpui::test]
8923async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8924 init_test(cx, |_| {});
8925
8926 let base_text = r#"
8927 impl A {
8928 // this is an uncommitted comment
8929
8930 fn b() {
8931 c();
8932 }
8933
8934 // this is another uncommitted comment
8935
8936 fn d() {
8937 // e
8938 // f
8939 }
8940 }
8941
8942 fn g() {
8943 // h
8944 }
8945 "#
8946 .unindent();
8947
8948 let text = r#"
8949 ˇimpl A {
8950
8951 fn b() {
8952 c();
8953 }
8954
8955 fn d() {
8956 // e
8957 // f
8958 }
8959 }
8960
8961 fn g() {
8962 // h
8963 }
8964 "#
8965 .unindent();
8966
8967 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8968 cx.set_state(&text);
8969 cx.set_head_text(&base_text);
8970 cx.update_editor(|editor, window, cx| {
8971 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8972 });
8973
8974 cx.assert_state_with_diff(
8975 "
8976 ˇimpl A {
8977 - // this is an uncommitted comment
8978
8979 fn b() {
8980 c();
8981 }
8982
8983 - // this is another uncommitted comment
8984 -
8985 fn d() {
8986 // e
8987 // f
8988 }
8989 }
8990
8991 fn g() {
8992 // h
8993 }
8994 "
8995 .unindent(),
8996 );
8997
8998 let expected_display_text = "
8999 impl A {
9000 // this is an uncommitted comment
9001
9002 fn b() {
9003 ⋯
9004 }
9005
9006 // this is another uncommitted comment
9007
9008 fn d() {
9009 ⋯
9010 }
9011 }
9012
9013 fn g() {
9014 ⋯
9015 }
9016 "
9017 .unindent();
9018
9019 cx.update_editor(|editor, window, cx| {
9020 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9021 assert_eq!(editor.display_text(cx), expected_display_text);
9022 });
9023}
9024
9025#[gpui::test]
9026async fn test_autoindent(cx: &mut TestAppContext) {
9027 init_test(cx, |_| {});
9028
9029 let language = Arc::new(
9030 Language::new(
9031 LanguageConfig {
9032 brackets: BracketPairConfig {
9033 pairs: vec![
9034 BracketPair {
9035 start: "{".to_string(),
9036 end: "}".to_string(),
9037 close: false,
9038 surround: false,
9039 newline: true,
9040 },
9041 BracketPair {
9042 start: "(".to_string(),
9043 end: ")".to_string(),
9044 close: false,
9045 surround: false,
9046 newline: true,
9047 },
9048 ],
9049 ..Default::default()
9050 },
9051 ..Default::default()
9052 },
9053 Some(tree_sitter_rust::LANGUAGE.into()),
9054 )
9055 .with_indents_query(
9056 r#"
9057 (_ "(" ")" @end) @indent
9058 (_ "{" "}" @end) @indent
9059 "#,
9060 )
9061 .unwrap(),
9062 );
9063
9064 let text = "fn a() {}";
9065
9066 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9067 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9068 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9069 editor
9070 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9071 .await;
9072
9073 editor.update_in(cx, |editor, window, cx| {
9074 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9075 s.select_ranges([5..5, 8..8, 9..9])
9076 });
9077 editor.newline(&Newline, window, cx);
9078 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9079 assert_eq!(
9080 editor.selections.ranges(cx),
9081 &[
9082 Point::new(1, 4)..Point::new(1, 4),
9083 Point::new(3, 4)..Point::new(3, 4),
9084 Point::new(5, 0)..Point::new(5, 0)
9085 ]
9086 );
9087 });
9088}
9089
9090#[gpui::test]
9091async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9092 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9093
9094 let language = Arc::new(
9095 Language::new(
9096 LanguageConfig {
9097 brackets: BracketPairConfig {
9098 pairs: vec![
9099 BracketPair {
9100 start: "{".to_string(),
9101 end: "}".to_string(),
9102 close: false,
9103 surround: false,
9104 newline: true,
9105 },
9106 BracketPair {
9107 start: "(".to_string(),
9108 end: ")".to_string(),
9109 close: false,
9110 surround: false,
9111 newline: true,
9112 },
9113 ],
9114 ..Default::default()
9115 },
9116 ..Default::default()
9117 },
9118 Some(tree_sitter_rust::LANGUAGE.into()),
9119 )
9120 .with_indents_query(
9121 r#"
9122 (_ "(" ")" @end) @indent
9123 (_ "{" "}" @end) @indent
9124 "#,
9125 )
9126 .unwrap(),
9127 );
9128
9129 let text = "fn a() {}";
9130
9131 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9132 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9133 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9134 editor
9135 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9136 .await;
9137
9138 editor.update_in(cx, |editor, window, cx| {
9139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9140 s.select_ranges([5..5, 8..8, 9..9])
9141 });
9142 editor.newline(&Newline, window, cx);
9143 assert_eq!(
9144 editor.text(cx),
9145 indoc!(
9146 "
9147 fn a(
9148
9149 ) {
9150
9151 }
9152 "
9153 )
9154 );
9155 assert_eq!(
9156 editor.selections.ranges(cx),
9157 &[
9158 Point::new(1, 0)..Point::new(1, 0),
9159 Point::new(3, 0)..Point::new(3, 0),
9160 Point::new(5, 0)..Point::new(5, 0)
9161 ]
9162 );
9163 });
9164}
9165
9166#[gpui::test]
9167async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9168 init_test(cx, |settings| {
9169 settings.defaults.auto_indent = Some(true);
9170 settings.languages.0.insert(
9171 "python".into(),
9172 LanguageSettingsContent {
9173 auto_indent: Some(false),
9174 ..Default::default()
9175 },
9176 );
9177 });
9178
9179 let mut cx = EditorTestContext::new(cx).await;
9180
9181 let injected_language = Arc::new(
9182 Language::new(
9183 LanguageConfig {
9184 brackets: BracketPairConfig {
9185 pairs: vec![
9186 BracketPair {
9187 start: "{".to_string(),
9188 end: "}".to_string(),
9189 close: false,
9190 surround: false,
9191 newline: true,
9192 },
9193 BracketPair {
9194 start: "(".to_string(),
9195 end: ")".to_string(),
9196 close: true,
9197 surround: false,
9198 newline: true,
9199 },
9200 ],
9201 ..Default::default()
9202 },
9203 name: "python".into(),
9204 ..Default::default()
9205 },
9206 Some(tree_sitter_python::LANGUAGE.into()),
9207 )
9208 .with_indents_query(
9209 r#"
9210 (_ "(" ")" @end) @indent
9211 (_ "{" "}" @end) @indent
9212 "#,
9213 )
9214 .unwrap(),
9215 );
9216
9217 let language = Arc::new(
9218 Language::new(
9219 LanguageConfig {
9220 brackets: BracketPairConfig {
9221 pairs: vec![
9222 BracketPair {
9223 start: "{".to_string(),
9224 end: "}".to_string(),
9225 close: false,
9226 surround: false,
9227 newline: true,
9228 },
9229 BracketPair {
9230 start: "(".to_string(),
9231 end: ")".to_string(),
9232 close: true,
9233 surround: false,
9234 newline: true,
9235 },
9236 ],
9237 ..Default::default()
9238 },
9239 name: LanguageName::new("rust"),
9240 ..Default::default()
9241 },
9242 Some(tree_sitter_rust::LANGUAGE.into()),
9243 )
9244 .with_indents_query(
9245 r#"
9246 (_ "(" ")" @end) @indent
9247 (_ "{" "}" @end) @indent
9248 "#,
9249 )
9250 .unwrap()
9251 .with_injection_query(
9252 r#"
9253 (macro_invocation
9254 macro: (identifier) @_macro_name
9255 (token_tree) @injection.content
9256 (#set! injection.language "python"))
9257 "#,
9258 )
9259 .unwrap(),
9260 );
9261
9262 cx.language_registry().add(injected_language);
9263 cx.language_registry().add(language.clone());
9264
9265 cx.update_buffer(|buffer, cx| {
9266 buffer.set_language(Some(language), cx);
9267 });
9268
9269 cx.set_state(r#"struct A {ˇ}"#);
9270
9271 cx.update_editor(|editor, window, cx| {
9272 editor.newline(&Default::default(), window, cx);
9273 });
9274
9275 cx.assert_editor_state(indoc!(
9276 "struct A {
9277 ˇ
9278 }"
9279 ));
9280
9281 cx.set_state(r#"select_biased!(ˇ)"#);
9282
9283 cx.update_editor(|editor, window, cx| {
9284 editor.newline(&Default::default(), window, cx);
9285 editor.handle_input("def ", window, cx);
9286 editor.handle_input("(", window, cx);
9287 editor.newline(&Default::default(), window, cx);
9288 editor.handle_input("a", window, cx);
9289 });
9290
9291 cx.assert_editor_state(indoc!(
9292 "select_biased!(
9293 def (
9294 aˇ
9295 )
9296 )"
9297 ));
9298}
9299
9300#[gpui::test]
9301async fn test_autoindent_selections(cx: &mut TestAppContext) {
9302 init_test(cx, |_| {});
9303
9304 {
9305 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9306 cx.set_state(indoc! {"
9307 impl A {
9308
9309 fn b() {}
9310
9311 «fn c() {
9312
9313 }ˇ»
9314 }
9315 "});
9316
9317 cx.update_editor(|editor, window, cx| {
9318 editor.autoindent(&Default::default(), window, cx);
9319 });
9320
9321 cx.assert_editor_state(indoc! {"
9322 impl A {
9323
9324 fn b() {}
9325
9326 «fn c() {
9327
9328 }ˇ»
9329 }
9330 "});
9331 }
9332
9333 {
9334 let mut cx = EditorTestContext::new_multibuffer(
9335 cx,
9336 [indoc! { "
9337 impl A {
9338 «
9339 // a
9340 fn b(){}
9341 »
9342 «
9343 }
9344 fn c(){}
9345 »
9346 "}],
9347 );
9348
9349 let buffer = cx.update_editor(|editor, _, cx| {
9350 let buffer = editor.buffer().update(cx, |buffer, _| {
9351 buffer.all_buffers().iter().next().unwrap().clone()
9352 });
9353 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9354 buffer
9355 });
9356
9357 cx.run_until_parked();
9358 cx.update_editor(|editor, window, cx| {
9359 editor.select_all(&Default::default(), window, cx);
9360 editor.autoindent(&Default::default(), window, cx)
9361 });
9362 cx.run_until_parked();
9363
9364 cx.update(|_, cx| {
9365 assert_eq!(
9366 buffer.read(cx).text(),
9367 indoc! { "
9368 impl A {
9369
9370 // a
9371 fn b(){}
9372
9373
9374 }
9375 fn c(){}
9376
9377 " }
9378 )
9379 });
9380 }
9381}
9382
9383#[gpui::test]
9384async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9385 init_test(cx, |_| {});
9386
9387 let mut cx = EditorTestContext::new(cx).await;
9388
9389 let language = Arc::new(Language::new(
9390 LanguageConfig {
9391 brackets: BracketPairConfig {
9392 pairs: vec![
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: true,
9404 surround: true,
9405 newline: true,
9406 },
9407 BracketPair {
9408 start: "/*".to_string(),
9409 end: " */".to_string(),
9410 close: true,
9411 surround: true,
9412 newline: true,
9413 },
9414 BracketPair {
9415 start: "[".to_string(),
9416 end: "]".to_string(),
9417 close: false,
9418 surround: false,
9419 newline: true,
9420 },
9421 BracketPair {
9422 start: "\"".to_string(),
9423 end: "\"".to_string(),
9424 close: true,
9425 surround: true,
9426 newline: false,
9427 },
9428 BracketPair {
9429 start: "<".to_string(),
9430 end: ">".to_string(),
9431 close: false,
9432 surround: true,
9433 newline: true,
9434 },
9435 ],
9436 ..Default::default()
9437 },
9438 autoclose_before: "})]".to_string(),
9439 ..Default::default()
9440 },
9441 Some(tree_sitter_rust::LANGUAGE.into()),
9442 ));
9443
9444 cx.language_registry().add(language.clone());
9445 cx.update_buffer(|buffer, cx| {
9446 buffer.set_language(Some(language), cx);
9447 });
9448
9449 cx.set_state(
9450 &r#"
9451 🏀ˇ
9452 εˇ
9453 ❤️ˇ
9454 "#
9455 .unindent(),
9456 );
9457
9458 // autoclose multiple nested brackets at multiple cursors
9459 cx.update_editor(|editor, window, cx| {
9460 editor.handle_input("{", window, cx);
9461 editor.handle_input("{", window, cx);
9462 editor.handle_input("{", window, cx);
9463 });
9464 cx.assert_editor_state(
9465 &"
9466 🏀{{{ˇ}}}
9467 ε{{{ˇ}}}
9468 ❤️{{{ˇ}}}
9469 "
9470 .unindent(),
9471 );
9472
9473 // insert a different closing bracket
9474 cx.update_editor(|editor, window, cx| {
9475 editor.handle_input(")", window, cx);
9476 });
9477 cx.assert_editor_state(
9478 &"
9479 🏀{{{)ˇ}}}
9480 ε{{{)ˇ}}}
9481 ❤️{{{)ˇ}}}
9482 "
9483 .unindent(),
9484 );
9485
9486 // skip over the auto-closed brackets when typing a closing bracket
9487 cx.update_editor(|editor, window, cx| {
9488 editor.move_right(&MoveRight, window, cx);
9489 editor.handle_input("}", window, cx);
9490 editor.handle_input("}", window, cx);
9491 editor.handle_input("}", window, cx);
9492 });
9493 cx.assert_editor_state(
9494 &"
9495 🏀{{{)}}}}ˇ
9496 ε{{{)}}}}ˇ
9497 ❤️{{{)}}}}ˇ
9498 "
9499 .unindent(),
9500 );
9501
9502 // autoclose multi-character pairs
9503 cx.set_state(
9504 &"
9505 ˇ
9506 ˇ
9507 "
9508 .unindent(),
9509 );
9510 cx.update_editor(|editor, window, cx| {
9511 editor.handle_input("/", window, cx);
9512 editor.handle_input("*", window, cx);
9513 });
9514 cx.assert_editor_state(
9515 &"
9516 /*ˇ */
9517 /*ˇ */
9518 "
9519 .unindent(),
9520 );
9521
9522 // one cursor autocloses a multi-character pair, one cursor
9523 // does not autoclose.
9524 cx.set_state(
9525 &"
9526 /ˇ
9527 ˇ
9528 "
9529 .unindent(),
9530 );
9531 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9532 cx.assert_editor_state(
9533 &"
9534 /*ˇ */
9535 *ˇ
9536 "
9537 .unindent(),
9538 );
9539
9540 // Don't autoclose if the next character isn't whitespace and isn't
9541 // listed in the language's "autoclose_before" section.
9542 cx.set_state("ˇa b");
9543 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9544 cx.assert_editor_state("{ˇa b");
9545
9546 // Don't autoclose if `close` is false for the bracket pair
9547 cx.set_state("ˇ");
9548 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9549 cx.assert_editor_state("[ˇ");
9550
9551 // Surround with brackets if text is selected
9552 cx.set_state("«aˇ» b");
9553 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9554 cx.assert_editor_state("{«aˇ»} b");
9555
9556 // Autoclose when not immediately after a word character
9557 cx.set_state("a ˇ");
9558 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9559 cx.assert_editor_state("a \"ˇ\"");
9560
9561 // Autoclose pair where the start and end characters are the same
9562 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9563 cx.assert_editor_state("a \"\"ˇ");
9564
9565 // Don't autoclose when immediately after a word character
9566 cx.set_state("aˇ");
9567 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9568 cx.assert_editor_state("a\"ˇ");
9569
9570 // Do autoclose when after a non-word character
9571 cx.set_state("{ˇ");
9572 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9573 cx.assert_editor_state("{\"ˇ\"");
9574
9575 // Non identical pairs autoclose regardless of preceding character
9576 cx.set_state("aˇ");
9577 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9578 cx.assert_editor_state("a{ˇ}");
9579
9580 // Don't autoclose pair if autoclose is disabled
9581 cx.set_state("ˇ");
9582 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9583 cx.assert_editor_state("<ˇ");
9584
9585 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9586 cx.set_state("«aˇ» b");
9587 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9588 cx.assert_editor_state("<«aˇ»> b");
9589}
9590
9591#[gpui::test]
9592async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9593 init_test(cx, |settings| {
9594 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9595 });
9596
9597 let mut cx = EditorTestContext::new(cx).await;
9598
9599 let language = Arc::new(Language::new(
9600 LanguageConfig {
9601 brackets: BracketPairConfig {
9602 pairs: vec![
9603 BracketPair {
9604 start: "{".to_string(),
9605 end: "}".to_string(),
9606 close: true,
9607 surround: true,
9608 newline: true,
9609 },
9610 BracketPair {
9611 start: "(".to_string(),
9612 end: ")".to_string(),
9613 close: true,
9614 surround: true,
9615 newline: true,
9616 },
9617 BracketPair {
9618 start: "[".to_string(),
9619 end: "]".to_string(),
9620 close: false,
9621 surround: false,
9622 newline: true,
9623 },
9624 ],
9625 ..Default::default()
9626 },
9627 autoclose_before: "})]".to_string(),
9628 ..Default::default()
9629 },
9630 Some(tree_sitter_rust::LANGUAGE.into()),
9631 ));
9632
9633 cx.language_registry().add(language.clone());
9634 cx.update_buffer(|buffer, cx| {
9635 buffer.set_language(Some(language), cx);
9636 });
9637
9638 cx.set_state(
9639 &"
9640 ˇ
9641 ˇ
9642 ˇ
9643 "
9644 .unindent(),
9645 );
9646
9647 // ensure only matching closing brackets are skipped over
9648 cx.update_editor(|editor, window, cx| {
9649 editor.handle_input("}", window, cx);
9650 editor.move_left(&MoveLeft, window, cx);
9651 editor.handle_input(")", window, cx);
9652 editor.move_left(&MoveLeft, window, cx);
9653 });
9654 cx.assert_editor_state(
9655 &"
9656 ˇ)}
9657 ˇ)}
9658 ˇ)}
9659 "
9660 .unindent(),
9661 );
9662
9663 // skip-over closing brackets at multiple cursors
9664 cx.update_editor(|editor, window, cx| {
9665 editor.handle_input(")", window, cx);
9666 editor.handle_input("}", window, cx);
9667 });
9668 cx.assert_editor_state(
9669 &"
9670 )}ˇ
9671 )}ˇ
9672 )}ˇ
9673 "
9674 .unindent(),
9675 );
9676
9677 // ignore non-close brackets
9678 cx.update_editor(|editor, window, cx| {
9679 editor.handle_input("]", window, cx);
9680 editor.move_left(&MoveLeft, window, cx);
9681 editor.handle_input("]", window, cx);
9682 });
9683 cx.assert_editor_state(
9684 &"
9685 )}]ˇ]
9686 )}]ˇ]
9687 )}]ˇ]
9688 "
9689 .unindent(),
9690 );
9691}
9692
9693#[gpui::test]
9694async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9695 init_test(cx, |_| {});
9696
9697 let mut cx = EditorTestContext::new(cx).await;
9698
9699 let html_language = Arc::new(
9700 Language::new(
9701 LanguageConfig {
9702 name: "HTML".into(),
9703 brackets: BracketPairConfig {
9704 pairs: vec![
9705 BracketPair {
9706 start: "<".into(),
9707 end: ">".into(),
9708 close: true,
9709 ..Default::default()
9710 },
9711 BracketPair {
9712 start: "{".into(),
9713 end: "}".into(),
9714 close: true,
9715 ..Default::default()
9716 },
9717 BracketPair {
9718 start: "(".into(),
9719 end: ")".into(),
9720 close: true,
9721 ..Default::default()
9722 },
9723 ],
9724 ..Default::default()
9725 },
9726 autoclose_before: "})]>".into(),
9727 ..Default::default()
9728 },
9729 Some(tree_sitter_html::LANGUAGE.into()),
9730 )
9731 .with_injection_query(
9732 r#"
9733 (script_element
9734 (raw_text) @injection.content
9735 (#set! injection.language "javascript"))
9736 "#,
9737 )
9738 .unwrap(),
9739 );
9740
9741 let javascript_language = Arc::new(Language::new(
9742 LanguageConfig {
9743 name: "JavaScript".into(),
9744 brackets: BracketPairConfig {
9745 pairs: vec![
9746 BracketPair {
9747 start: "/*".into(),
9748 end: " */".into(),
9749 close: true,
9750 ..Default::default()
9751 },
9752 BracketPair {
9753 start: "{".into(),
9754 end: "}".into(),
9755 close: true,
9756 ..Default::default()
9757 },
9758 BracketPair {
9759 start: "(".into(),
9760 end: ")".into(),
9761 close: true,
9762 ..Default::default()
9763 },
9764 ],
9765 ..Default::default()
9766 },
9767 autoclose_before: "})]>".into(),
9768 ..Default::default()
9769 },
9770 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9771 ));
9772
9773 cx.language_registry().add(html_language.clone());
9774 cx.language_registry().add(javascript_language);
9775 cx.executor().run_until_parked();
9776
9777 cx.update_buffer(|buffer, cx| {
9778 buffer.set_language(Some(html_language), cx);
9779 });
9780
9781 cx.set_state(
9782 &r#"
9783 <body>ˇ
9784 <script>
9785 var x = 1;ˇ
9786 </script>
9787 </body>ˇ
9788 "#
9789 .unindent(),
9790 );
9791
9792 // Precondition: different languages are active at different locations.
9793 cx.update_editor(|editor, window, cx| {
9794 let snapshot = editor.snapshot(window, cx);
9795 let cursors = editor.selections.ranges::<usize>(cx);
9796 let languages = cursors
9797 .iter()
9798 .map(|c| snapshot.language_at(c.start).unwrap().name())
9799 .collect::<Vec<_>>();
9800 assert_eq!(
9801 languages,
9802 &["HTML".into(), "JavaScript".into(), "HTML".into()]
9803 );
9804 });
9805
9806 // Angle brackets autoclose in HTML, but not JavaScript.
9807 cx.update_editor(|editor, window, cx| {
9808 editor.handle_input("<", window, cx);
9809 editor.handle_input("a", window, cx);
9810 });
9811 cx.assert_editor_state(
9812 &r#"
9813 <body><aˇ>
9814 <script>
9815 var x = 1;<aˇ
9816 </script>
9817 </body><aˇ>
9818 "#
9819 .unindent(),
9820 );
9821
9822 // Curly braces and parens autoclose in both HTML and JavaScript.
9823 cx.update_editor(|editor, window, cx| {
9824 editor.handle_input(" b=", window, cx);
9825 editor.handle_input("{", window, cx);
9826 editor.handle_input("c", window, cx);
9827 editor.handle_input("(", window, cx);
9828 });
9829 cx.assert_editor_state(
9830 &r#"
9831 <body><a b={c(ˇ)}>
9832 <script>
9833 var x = 1;<a b={c(ˇ)}
9834 </script>
9835 </body><a b={c(ˇ)}>
9836 "#
9837 .unindent(),
9838 );
9839
9840 // Brackets that were already autoclosed are skipped.
9841 cx.update_editor(|editor, window, cx| {
9842 editor.handle_input(")", window, cx);
9843 editor.handle_input("d", window, cx);
9844 editor.handle_input("}", window, cx);
9845 });
9846 cx.assert_editor_state(
9847 &r#"
9848 <body><a b={c()d}ˇ>
9849 <script>
9850 var x = 1;<a b={c()d}ˇ
9851 </script>
9852 </body><a b={c()d}ˇ>
9853 "#
9854 .unindent(),
9855 );
9856 cx.update_editor(|editor, window, cx| {
9857 editor.handle_input(">", window, cx);
9858 });
9859 cx.assert_editor_state(
9860 &r#"
9861 <body><a b={c()d}>ˇ
9862 <script>
9863 var x = 1;<a b={c()d}>ˇ
9864 </script>
9865 </body><a b={c()d}>ˇ
9866 "#
9867 .unindent(),
9868 );
9869
9870 // Reset
9871 cx.set_state(
9872 &r#"
9873 <body>ˇ
9874 <script>
9875 var x = 1;ˇ
9876 </script>
9877 </body>ˇ
9878 "#
9879 .unindent(),
9880 );
9881
9882 cx.update_editor(|editor, window, cx| {
9883 editor.handle_input("<", window, cx);
9884 });
9885 cx.assert_editor_state(
9886 &r#"
9887 <body><ˇ>
9888 <script>
9889 var x = 1;<ˇ
9890 </script>
9891 </body><ˇ>
9892 "#
9893 .unindent(),
9894 );
9895
9896 // When backspacing, the closing angle brackets are removed.
9897 cx.update_editor(|editor, window, cx| {
9898 editor.backspace(&Backspace, window, cx);
9899 });
9900 cx.assert_editor_state(
9901 &r#"
9902 <body>ˇ
9903 <script>
9904 var x = 1;ˇ
9905 </script>
9906 </body>ˇ
9907 "#
9908 .unindent(),
9909 );
9910
9911 // Block comments autoclose in JavaScript, but not HTML.
9912 cx.update_editor(|editor, window, cx| {
9913 editor.handle_input("/", window, cx);
9914 editor.handle_input("*", window, cx);
9915 });
9916 cx.assert_editor_state(
9917 &r#"
9918 <body>/*ˇ
9919 <script>
9920 var x = 1;/*ˇ */
9921 </script>
9922 </body>/*ˇ
9923 "#
9924 .unindent(),
9925 );
9926}
9927
9928#[gpui::test]
9929async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9930 init_test(cx, |_| {});
9931
9932 let mut cx = EditorTestContext::new(cx).await;
9933
9934 let rust_language = Arc::new(
9935 Language::new(
9936 LanguageConfig {
9937 name: "Rust".into(),
9938 brackets: serde_json::from_value(json!([
9939 { "start": "{", "end": "}", "close": true, "newline": true },
9940 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9941 ]))
9942 .unwrap(),
9943 autoclose_before: "})]>".into(),
9944 ..Default::default()
9945 },
9946 Some(tree_sitter_rust::LANGUAGE.into()),
9947 )
9948 .with_override_query("(string_literal) @string")
9949 .unwrap(),
9950 );
9951
9952 cx.language_registry().add(rust_language.clone());
9953 cx.update_buffer(|buffer, cx| {
9954 buffer.set_language(Some(rust_language), cx);
9955 });
9956
9957 cx.set_state(
9958 &r#"
9959 let x = ˇ
9960 "#
9961 .unindent(),
9962 );
9963
9964 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9965 cx.update_editor(|editor, window, cx| {
9966 editor.handle_input("\"", window, cx);
9967 });
9968 cx.assert_editor_state(
9969 &r#"
9970 let x = "ˇ"
9971 "#
9972 .unindent(),
9973 );
9974
9975 // Inserting another quotation mark. The cursor moves across the existing
9976 // automatically-inserted quotation mark.
9977 cx.update_editor(|editor, window, cx| {
9978 editor.handle_input("\"", window, cx);
9979 });
9980 cx.assert_editor_state(
9981 &r#"
9982 let x = ""ˇ
9983 "#
9984 .unindent(),
9985 );
9986
9987 // Reset
9988 cx.set_state(
9989 &r#"
9990 let x = ˇ
9991 "#
9992 .unindent(),
9993 );
9994
9995 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9996 cx.update_editor(|editor, window, cx| {
9997 editor.handle_input("\"", window, cx);
9998 editor.handle_input(" ", window, cx);
9999 editor.move_left(&Default::default(), window, cx);
10000 editor.handle_input("\\", window, cx);
10001 editor.handle_input("\"", window, cx);
10002 });
10003 cx.assert_editor_state(
10004 &r#"
10005 let x = "\"ˇ "
10006 "#
10007 .unindent(),
10008 );
10009
10010 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10011 // mark. Nothing is inserted.
10012 cx.update_editor(|editor, window, cx| {
10013 editor.move_right(&Default::default(), window, cx);
10014 editor.handle_input("\"", window, cx);
10015 });
10016 cx.assert_editor_state(
10017 &r#"
10018 let x = "\" "ˇ
10019 "#
10020 .unindent(),
10021 );
10022}
10023
10024#[gpui::test]
10025async fn test_surround_with_pair(cx: &mut TestAppContext) {
10026 init_test(cx, |_| {});
10027
10028 let language = Arc::new(Language::new(
10029 LanguageConfig {
10030 brackets: BracketPairConfig {
10031 pairs: vec![
10032 BracketPair {
10033 start: "{".to_string(),
10034 end: "}".to_string(),
10035 close: true,
10036 surround: true,
10037 newline: true,
10038 },
10039 BracketPair {
10040 start: "/* ".to_string(),
10041 end: "*/".to_string(),
10042 close: true,
10043 surround: true,
10044 ..Default::default()
10045 },
10046 ],
10047 ..Default::default()
10048 },
10049 ..Default::default()
10050 },
10051 Some(tree_sitter_rust::LANGUAGE.into()),
10052 ));
10053
10054 let text = r#"
10055 a
10056 b
10057 c
10058 "#
10059 .unindent();
10060
10061 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10062 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10063 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10064 editor
10065 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10066 .await;
10067
10068 editor.update_in(cx, |editor, window, cx| {
10069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10070 s.select_display_ranges([
10071 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10072 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10073 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10074 ])
10075 });
10076
10077 editor.handle_input("{", window, cx);
10078 editor.handle_input("{", window, cx);
10079 editor.handle_input("{", window, cx);
10080 assert_eq!(
10081 editor.text(cx),
10082 "
10083 {{{a}}}
10084 {{{b}}}
10085 {{{c}}}
10086 "
10087 .unindent()
10088 );
10089 assert_eq!(
10090 editor.selections.display_ranges(cx),
10091 [
10092 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10093 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10094 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10095 ]
10096 );
10097
10098 editor.undo(&Undo, window, cx);
10099 editor.undo(&Undo, window, cx);
10100 editor.undo(&Undo, window, cx);
10101 assert_eq!(
10102 editor.text(cx),
10103 "
10104 a
10105 b
10106 c
10107 "
10108 .unindent()
10109 );
10110 assert_eq!(
10111 editor.selections.display_ranges(cx),
10112 [
10113 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10114 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10115 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10116 ]
10117 );
10118
10119 // Ensure inserting the first character of a multi-byte bracket pair
10120 // doesn't surround the selections with the bracket.
10121 editor.handle_input("/", window, cx);
10122 assert_eq!(
10123 editor.text(cx),
10124 "
10125 /
10126 /
10127 /
10128 "
10129 .unindent()
10130 );
10131 assert_eq!(
10132 editor.selections.display_ranges(cx),
10133 [
10134 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10135 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10136 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10137 ]
10138 );
10139
10140 editor.undo(&Undo, window, cx);
10141 assert_eq!(
10142 editor.text(cx),
10143 "
10144 a
10145 b
10146 c
10147 "
10148 .unindent()
10149 );
10150 assert_eq!(
10151 editor.selections.display_ranges(cx),
10152 [
10153 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10154 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10155 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10156 ]
10157 );
10158
10159 // Ensure inserting the last character of a multi-byte bracket pair
10160 // doesn't surround the selections with the bracket.
10161 editor.handle_input("*", window, cx);
10162 assert_eq!(
10163 editor.text(cx),
10164 "
10165 *
10166 *
10167 *
10168 "
10169 .unindent()
10170 );
10171 assert_eq!(
10172 editor.selections.display_ranges(cx),
10173 [
10174 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10175 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10176 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10177 ]
10178 );
10179 });
10180}
10181
10182#[gpui::test]
10183async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10184 init_test(cx, |_| {});
10185
10186 let language = Arc::new(Language::new(
10187 LanguageConfig {
10188 brackets: BracketPairConfig {
10189 pairs: vec![BracketPair {
10190 start: "{".to_string(),
10191 end: "}".to_string(),
10192 close: true,
10193 surround: true,
10194 newline: true,
10195 }],
10196 ..Default::default()
10197 },
10198 autoclose_before: "}".to_string(),
10199 ..Default::default()
10200 },
10201 Some(tree_sitter_rust::LANGUAGE.into()),
10202 ));
10203
10204 let text = r#"
10205 a
10206 b
10207 c
10208 "#
10209 .unindent();
10210
10211 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10212 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10213 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10214 editor
10215 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10216 .await;
10217
10218 editor.update_in(cx, |editor, window, cx| {
10219 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10220 s.select_ranges([
10221 Point::new(0, 1)..Point::new(0, 1),
10222 Point::new(1, 1)..Point::new(1, 1),
10223 Point::new(2, 1)..Point::new(2, 1),
10224 ])
10225 });
10226
10227 editor.handle_input("{", window, cx);
10228 editor.handle_input("{", window, cx);
10229 editor.handle_input("_", window, cx);
10230 assert_eq!(
10231 editor.text(cx),
10232 "
10233 a{{_}}
10234 b{{_}}
10235 c{{_}}
10236 "
10237 .unindent()
10238 );
10239 assert_eq!(
10240 editor.selections.ranges::<Point>(cx),
10241 [
10242 Point::new(0, 4)..Point::new(0, 4),
10243 Point::new(1, 4)..Point::new(1, 4),
10244 Point::new(2, 4)..Point::new(2, 4)
10245 ]
10246 );
10247
10248 editor.backspace(&Default::default(), window, cx);
10249 editor.backspace(&Default::default(), window, cx);
10250 assert_eq!(
10251 editor.text(cx),
10252 "
10253 a{}
10254 b{}
10255 c{}
10256 "
10257 .unindent()
10258 );
10259 assert_eq!(
10260 editor.selections.ranges::<Point>(cx),
10261 [
10262 Point::new(0, 2)..Point::new(0, 2),
10263 Point::new(1, 2)..Point::new(1, 2),
10264 Point::new(2, 2)..Point::new(2, 2)
10265 ]
10266 );
10267
10268 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10269 assert_eq!(
10270 editor.text(cx),
10271 "
10272 a
10273 b
10274 c
10275 "
10276 .unindent()
10277 );
10278 assert_eq!(
10279 editor.selections.ranges::<Point>(cx),
10280 [
10281 Point::new(0, 1)..Point::new(0, 1),
10282 Point::new(1, 1)..Point::new(1, 1),
10283 Point::new(2, 1)..Point::new(2, 1)
10284 ]
10285 );
10286 });
10287}
10288
10289#[gpui::test]
10290async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10291 init_test(cx, |settings| {
10292 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10293 });
10294
10295 let mut cx = EditorTestContext::new(cx).await;
10296
10297 let language = Arc::new(Language::new(
10298 LanguageConfig {
10299 brackets: BracketPairConfig {
10300 pairs: vec![
10301 BracketPair {
10302 start: "{".to_string(),
10303 end: "}".to_string(),
10304 close: true,
10305 surround: true,
10306 newline: true,
10307 },
10308 BracketPair {
10309 start: "(".to_string(),
10310 end: ")".to_string(),
10311 close: true,
10312 surround: true,
10313 newline: true,
10314 },
10315 BracketPair {
10316 start: "[".to_string(),
10317 end: "]".to_string(),
10318 close: false,
10319 surround: true,
10320 newline: true,
10321 },
10322 ],
10323 ..Default::default()
10324 },
10325 autoclose_before: "})]".to_string(),
10326 ..Default::default()
10327 },
10328 Some(tree_sitter_rust::LANGUAGE.into()),
10329 ));
10330
10331 cx.language_registry().add(language.clone());
10332 cx.update_buffer(|buffer, cx| {
10333 buffer.set_language(Some(language), cx);
10334 });
10335
10336 cx.set_state(
10337 &"
10338 {(ˇ)}
10339 [[ˇ]]
10340 {(ˇ)}
10341 "
10342 .unindent(),
10343 );
10344
10345 cx.update_editor(|editor, window, cx| {
10346 editor.backspace(&Default::default(), window, cx);
10347 editor.backspace(&Default::default(), window, cx);
10348 });
10349
10350 cx.assert_editor_state(
10351 &"
10352 ˇ
10353 ˇ]]
10354 ˇ
10355 "
10356 .unindent(),
10357 );
10358
10359 cx.update_editor(|editor, window, cx| {
10360 editor.handle_input("{", window, cx);
10361 editor.handle_input("{", window, cx);
10362 editor.move_right(&MoveRight, window, cx);
10363 editor.move_right(&MoveRight, window, cx);
10364 editor.move_left(&MoveLeft, window, cx);
10365 editor.move_left(&MoveLeft, window, cx);
10366 editor.backspace(&Default::default(), window, cx);
10367 });
10368
10369 cx.assert_editor_state(
10370 &"
10371 {ˇ}
10372 {ˇ}]]
10373 {ˇ}
10374 "
10375 .unindent(),
10376 );
10377
10378 cx.update_editor(|editor, window, cx| {
10379 editor.backspace(&Default::default(), window, cx);
10380 });
10381
10382 cx.assert_editor_state(
10383 &"
10384 ˇ
10385 ˇ]]
10386 ˇ
10387 "
10388 .unindent(),
10389 );
10390}
10391
10392#[gpui::test]
10393async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10394 init_test(cx, |_| {});
10395
10396 let language = Arc::new(Language::new(
10397 LanguageConfig::default(),
10398 Some(tree_sitter_rust::LANGUAGE.into()),
10399 ));
10400
10401 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10402 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10403 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10404 editor
10405 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10406 .await;
10407
10408 editor.update_in(cx, |editor, window, cx| {
10409 editor.set_auto_replace_emoji_shortcode(true);
10410
10411 editor.handle_input("Hello ", window, cx);
10412 editor.handle_input(":wave", window, cx);
10413 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10414
10415 editor.handle_input(":", window, cx);
10416 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10417
10418 editor.handle_input(" :smile", window, cx);
10419 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10420
10421 editor.handle_input(":", window, cx);
10422 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10423
10424 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10425 editor.handle_input(":wave", window, cx);
10426 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10427
10428 editor.handle_input(":", window, cx);
10429 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10430
10431 editor.handle_input(":1", window, cx);
10432 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10433
10434 editor.handle_input(":", window, cx);
10435 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10436
10437 // Ensure shortcode does not get replaced when it is part of a word
10438 editor.handle_input(" Test:wave", window, cx);
10439 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10440
10441 editor.handle_input(":", window, cx);
10442 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10443
10444 editor.set_auto_replace_emoji_shortcode(false);
10445
10446 // Ensure shortcode does not get replaced when auto replace is off
10447 editor.handle_input(" :wave", window, cx);
10448 assert_eq!(
10449 editor.text(cx),
10450 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10451 );
10452
10453 editor.handle_input(":", window, cx);
10454 assert_eq!(
10455 editor.text(cx),
10456 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10457 );
10458 });
10459}
10460
10461#[gpui::test]
10462async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10463 init_test(cx, |_| {});
10464
10465 let (text, insertion_ranges) = marked_text_ranges(
10466 indoc! {"
10467 ˇ
10468 "},
10469 false,
10470 );
10471
10472 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10473 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10474
10475 _ = editor.update_in(cx, |editor, window, cx| {
10476 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10477
10478 editor
10479 .insert_snippet(&insertion_ranges, snippet, window, cx)
10480 .unwrap();
10481
10482 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10483 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10484 assert_eq!(editor.text(cx), expected_text);
10485 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10486 }
10487
10488 assert(
10489 editor,
10490 cx,
10491 indoc! {"
10492 type «» =•
10493 "},
10494 );
10495
10496 assert!(editor.context_menu_visible(), "There should be a matches");
10497 });
10498}
10499
10500#[gpui::test]
10501async fn test_snippets(cx: &mut TestAppContext) {
10502 init_test(cx, |_| {});
10503
10504 let mut cx = EditorTestContext::new(cx).await;
10505
10506 cx.set_state(indoc! {"
10507 a.ˇ b
10508 a.ˇ b
10509 a.ˇ b
10510 "});
10511
10512 cx.update_editor(|editor, window, cx| {
10513 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10514 let insertion_ranges = editor
10515 .selections
10516 .all(cx)
10517 .iter()
10518 .map(|s| s.range())
10519 .collect::<Vec<_>>();
10520 editor
10521 .insert_snippet(&insertion_ranges, snippet, window, cx)
10522 .unwrap();
10523 });
10524
10525 cx.assert_editor_state(indoc! {"
10526 a.f(«oneˇ», two, «threeˇ») b
10527 a.f(«oneˇ», two, «threeˇ») b
10528 a.f(«oneˇ», two, «threeˇ») b
10529 "});
10530
10531 // Can't move earlier than the first tab stop
10532 cx.update_editor(|editor, window, cx| {
10533 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10534 });
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
10548 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10549 cx.assert_editor_state(indoc! {"
10550 a.f(«oneˇ», two, «threeˇ») b
10551 a.f(«oneˇ», two, «threeˇ») b
10552 a.f(«oneˇ», two, «threeˇ») b
10553 "});
10554
10555 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10556 cx.assert_editor_state(indoc! {"
10557 a.f(one, «twoˇ», three) b
10558 a.f(one, «twoˇ», three) b
10559 a.f(one, «twoˇ», three) b
10560 "});
10561 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10562 cx.assert_editor_state(indoc! {"
10563 a.f(one, two, three)ˇ b
10564 a.f(one, two, three)ˇ b
10565 a.f(one, two, three)ˇ b
10566 "});
10567
10568 // As soon as the last tab stop is reached, snippet state is gone
10569 cx.update_editor(|editor, window, cx| {
10570 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10571 });
10572 cx.assert_editor_state(indoc! {"
10573 a.f(one, two, three)ˇ b
10574 a.f(one, two, three)ˇ b
10575 a.f(one, two, three)ˇ b
10576 "});
10577}
10578
10579#[gpui::test]
10580async fn test_snippet_indentation(cx: &mut TestAppContext) {
10581 init_test(cx, |_| {});
10582
10583 let mut cx = EditorTestContext::new(cx).await;
10584
10585 cx.update_editor(|editor, window, cx| {
10586 let snippet = Snippet::parse(indoc! {"
10587 /*
10588 * Multiline comment with leading indentation
10589 *
10590 * $1
10591 */
10592 $0"})
10593 .unwrap();
10594 let insertion_ranges = editor
10595 .selections
10596 .all(cx)
10597 .iter()
10598 .map(|s| s.range())
10599 .collect::<Vec<_>>();
10600 editor
10601 .insert_snippet(&insertion_ranges, snippet, window, cx)
10602 .unwrap();
10603 });
10604
10605 cx.assert_editor_state(indoc! {"
10606 /*
10607 * Multiline comment with leading indentation
10608 *
10609 * ˇ
10610 */
10611 "});
10612
10613 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10614 cx.assert_editor_state(indoc! {"
10615 /*
10616 * Multiline comment with leading indentation
10617 *
10618 *•
10619 */
10620 ˇ"});
10621}
10622
10623#[gpui::test]
10624async fn test_document_format_during_save(cx: &mut TestAppContext) {
10625 init_test(cx, |_| {});
10626
10627 let fs = FakeFs::new(cx.executor());
10628 fs.insert_file(path!("/file.rs"), Default::default()).await;
10629
10630 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10631
10632 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10633 language_registry.add(rust_lang());
10634 let mut fake_servers = language_registry.register_fake_lsp(
10635 "Rust",
10636 FakeLspAdapter {
10637 capabilities: lsp::ServerCapabilities {
10638 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10639 ..Default::default()
10640 },
10641 ..Default::default()
10642 },
10643 );
10644
10645 let buffer = project
10646 .update(cx, |project, cx| {
10647 project.open_local_buffer(path!("/file.rs"), cx)
10648 })
10649 .await
10650 .unwrap();
10651
10652 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10653 let (editor, cx) = cx.add_window_view(|window, cx| {
10654 build_editor_with_project(project.clone(), buffer, window, cx)
10655 });
10656 editor.update_in(cx, |editor, window, cx| {
10657 editor.set_text("one\ntwo\nthree\n", window, cx)
10658 });
10659 assert!(cx.read(|cx| editor.is_dirty(cx)));
10660
10661 cx.executor().start_waiting();
10662 let fake_server = fake_servers.next().await.unwrap();
10663
10664 {
10665 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10666 move |params, _| async move {
10667 assert_eq!(
10668 params.text_document.uri,
10669 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10670 );
10671 assert_eq!(params.options.tab_size, 4);
10672 Ok(Some(vec![lsp::TextEdit::new(
10673 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10674 ", ".to_string(),
10675 )]))
10676 },
10677 );
10678 let save = editor
10679 .update_in(cx, |editor, window, cx| {
10680 editor.save(
10681 SaveOptions {
10682 format: true,
10683 autosave: false,
10684 },
10685 project.clone(),
10686 window,
10687 cx,
10688 )
10689 })
10690 .unwrap();
10691 cx.executor().start_waiting();
10692 save.await;
10693
10694 assert_eq!(
10695 editor.update(cx, |editor, cx| editor.text(cx)),
10696 "one, two\nthree\n"
10697 );
10698 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10699 }
10700
10701 {
10702 editor.update_in(cx, |editor, window, cx| {
10703 editor.set_text("one\ntwo\nthree\n", window, cx)
10704 });
10705 assert!(cx.read(|cx| editor.is_dirty(cx)));
10706
10707 // Ensure we can still save even if formatting hangs.
10708 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10709 move |params, _| async move {
10710 assert_eq!(
10711 params.text_document.uri,
10712 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10713 );
10714 futures::future::pending::<()>().await;
10715 unreachable!()
10716 },
10717 );
10718 let save = editor
10719 .update_in(cx, |editor, window, cx| {
10720 editor.save(
10721 SaveOptions {
10722 format: true,
10723 autosave: false,
10724 },
10725 project.clone(),
10726 window,
10727 cx,
10728 )
10729 })
10730 .unwrap();
10731 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10732 cx.executor().start_waiting();
10733 save.await;
10734 assert_eq!(
10735 editor.update(cx, |editor, cx| editor.text(cx)),
10736 "one\ntwo\nthree\n"
10737 );
10738 }
10739
10740 // Set rust language override and assert overridden tabsize is sent to language server
10741 update_test_language_settings(cx, |settings| {
10742 settings.languages.0.insert(
10743 "Rust".into(),
10744 LanguageSettingsContent {
10745 tab_size: NonZeroU32::new(8),
10746 ..Default::default()
10747 },
10748 );
10749 });
10750
10751 {
10752 editor.update_in(cx, |editor, window, cx| {
10753 editor.set_text("somehting_new\n", window, cx)
10754 });
10755 assert!(cx.read(|cx| editor.is_dirty(cx)));
10756 let _formatting_request_signal = fake_server
10757 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10758 assert_eq!(
10759 params.text_document.uri,
10760 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10761 );
10762 assert_eq!(params.options.tab_size, 8);
10763 Ok(Some(vec![]))
10764 });
10765 let save = editor
10766 .update_in(cx, |editor, window, cx| {
10767 editor.save(
10768 SaveOptions {
10769 format: true,
10770 autosave: false,
10771 },
10772 project.clone(),
10773 window,
10774 cx,
10775 )
10776 })
10777 .unwrap();
10778 cx.executor().start_waiting();
10779 save.await;
10780 }
10781}
10782
10783#[gpui::test]
10784async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
10785 init_test(cx, |settings| {
10786 settings.defaults.ensure_final_newline_on_save = Some(false);
10787 });
10788
10789 let fs = FakeFs::new(cx.executor());
10790 fs.insert_file(path!("/file.txt"), "foo".into()).await;
10791
10792 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
10793
10794 let buffer = project
10795 .update(cx, |project, cx| {
10796 project.open_local_buffer(path!("/file.txt"), cx)
10797 })
10798 .await
10799 .unwrap();
10800
10801 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10802 let (editor, cx) = cx.add_window_view(|window, cx| {
10803 build_editor_with_project(project.clone(), buffer, window, cx)
10804 });
10805 editor.update_in(cx, |editor, window, cx| {
10806 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10807 s.select_ranges([0..0])
10808 });
10809 });
10810 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10811
10812 editor.update_in(cx, |editor, window, cx| {
10813 editor.handle_input("\n", window, cx)
10814 });
10815 cx.run_until_parked();
10816 save(&editor, &project, cx).await;
10817 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10818
10819 editor.update_in(cx, |editor, window, cx| {
10820 editor.undo(&Default::default(), window, cx);
10821 });
10822 save(&editor, &project, cx).await;
10823 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10824
10825 editor.update_in(cx, |editor, window, cx| {
10826 editor.redo(&Default::default(), window, cx);
10827 });
10828 cx.run_until_parked();
10829 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10830
10831 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
10832 let save = editor
10833 .update_in(cx, |editor, window, cx| {
10834 editor.save(
10835 SaveOptions {
10836 format: true,
10837 autosave: false,
10838 },
10839 project.clone(),
10840 window,
10841 cx,
10842 )
10843 })
10844 .unwrap();
10845 cx.executor().start_waiting();
10846 save.await;
10847 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10848 }
10849}
10850
10851#[gpui::test]
10852async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
10853 init_test(cx, |_| {});
10854
10855 let cols = 4;
10856 let rows = 10;
10857 let sample_text_1 = sample_text(rows, cols, 'a');
10858 assert_eq!(
10859 sample_text_1,
10860 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
10861 );
10862 let sample_text_2 = sample_text(rows, cols, 'l');
10863 assert_eq!(
10864 sample_text_2,
10865 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
10866 );
10867 let sample_text_3 = sample_text(rows, cols, 'v');
10868 assert_eq!(
10869 sample_text_3,
10870 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
10871 );
10872
10873 let fs = FakeFs::new(cx.executor());
10874 fs.insert_tree(
10875 path!("/a"),
10876 json!({
10877 "main.rs": sample_text_1,
10878 "other.rs": sample_text_2,
10879 "lib.rs": sample_text_3,
10880 }),
10881 )
10882 .await;
10883
10884 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10885 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10886 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10887
10888 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10889 language_registry.add(rust_lang());
10890 let mut fake_servers = language_registry.register_fake_lsp(
10891 "Rust",
10892 FakeLspAdapter {
10893 capabilities: lsp::ServerCapabilities {
10894 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10895 ..Default::default()
10896 },
10897 ..Default::default()
10898 },
10899 );
10900
10901 let worktree = project.update(cx, |project, cx| {
10902 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10903 assert_eq!(worktrees.len(), 1);
10904 worktrees.pop().unwrap()
10905 });
10906 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10907
10908 let buffer_1 = project
10909 .update(cx, |project, cx| {
10910 project.open_buffer((worktree_id, "main.rs"), cx)
10911 })
10912 .await
10913 .unwrap();
10914 let buffer_2 = project
10915 .update(cx, |project, cx| {
10916 project.open_buffer((worktree_id, "other.rs"), cx)
10917 })
10918 .await
10919 .unwrap();
10920 let buffer_3 = project
10921 .update(cx, |project, cx| {
10922 project.open_buffer((worktree_id, "lib.rs"), cx)
10923 })
10924 .await
10925 .unwrap();
10926
10927 let multi_buffer = cx.new(|cx| {
10928 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10929 multi_buffer.push_excerpts(
10930 buffer_1.clone(),
10931 [
10932 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10933 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10934 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10935 ],
10936 cx,
10937 );
10938 multi_buffer.push_excerpts(
10939 buffer_2.clone(),
10940 [
10941 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10942 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10943 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10944 ],
10945 cx,
10946 );
10947 multi_buffer.push_excerpts(
10948 buffer_3.clone(),
10949 [
10950 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10951 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10952 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10953 ],
10954 cx,
10955 );
10956 multi_buffer
10957 });
10958 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10959 Editor::new(
10960 EditorMode::full(),
10961 multi_buffer,
10962 Some(project.clone()),
10963 window,
10964 cx,
10965 )
10966 });
10967
10968 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10969 editor.change_selections(
10970 SelectionEffects::scroll(Autoscroll::Next),
10971 window,
10972 cx,
10973 |s| s.select_ranges(Some(1..2)),
10974 );
10975 editor.insert("|one|two|three|", window, cx);
10976 });
10977 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10978 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10979 editor.change_selections(
10980 SelectionEffects::scroll(Autoscroll::Next),
10981 window,
10982 cx,
10983 |s| s.select_ranges(Some(60..70)),
10984 );
10985 editor.insert("|four|five|six|", window, cx);
10986 });
10987 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10988
10989 // First two buffers should be edited, but not the third one.
10990 assert_eq!(
10991 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10992 "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}",
10993 );
10994 buffer_1.update(cx, |buffer, _| {
10995 assert!(buffer.is_dirty());
10996 assert_eq!(
10997 buffer.text(),
10998 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10999 )
11000 });
11001 buffer_2.update(cx, |buffer, _| {
11002 assert!(buffer.is_dirty());
11003 assert_eq!(
11004 buffer.text(),
11005 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11006 )
11007 });
11008 buffer_3.update(cx, |buffer, _| {
11009 assert!(!buffer.is_dirty());
11010 assert_eq!(buffer.text(), sample_text_3,)
11011 });
11012 cx.executor().run_until_parked();
11013
11014 cx.executor().start_waiting();
11015 let save = multi_buffer_editor
11016 .update_in(cx, |editor, window, cx| {
11017 editor.save(
11018 SaveOptions {
11019 format: true,
11020 autosave: false,
11021 },
11022 project.clone(),
11023 window,
11024 cx,
11025 )
11026 })
11027 .unwrap();
11028
11029 let fake_server = fake_servers.next().await.unwrap();
11030 fake_server
11031 .server
11032 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11033 Ok(Some(vec![lsp::TextEdit::new(
11034 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11035 format!("[{} formatted]", params.text_document.uri),
11036 )]))
11037 })
11038 .detach();
11039 save.await;
11040
11041 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11042 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11043 assert_eq!(
11044 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11045 uri!(
11046 "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}"
11047 ),
11048 );
11049 buffer_1.update(cx, |buffer, _| {
11050 assert!(!buffer.is_dirty());
11051 assert_eq!(
11052 buffer.text(),
11053 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11054 )
11055 });
11056 buffer_2.update(cx, |buffer, _| {
11057 assert!(!buffer.is_dirty());
11058 assert_eq!(
11059 buffer.text(),
11060 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11061 )
11062 });
11063 buffer_3.update(cx, |buffer, _| {
11064 assert!(!buffer.is_dirty());
11065 assert_eq!(buffer.text(), sample_text_3,)
11066 });
11067}
11068
11069#[gpui::test]
11070async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11071 init_test(cx, |_| {});
11072
11073 let fs = FakeFs::new(cx.executor());
11074 fs.insert_tree(
11075 path!("/dir"),
11076 json!({
11077 "file1.rs": "fn main() { println!(\"hello\"); }",
11078 "file2.rs": "fn test() { println!(\"test\"); }",
11079 "file3.rs": "fn other() { println!(\"other\"); }\n",
11080 }),
11081 )
11082 .await;
11083
11084 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11085 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11086 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11087
11088 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11089 language_registry.add(rust_lang());
11090
11091 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11092 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11093
11094 // Open three buffers
11095 let buffer_1 = project
11096 .update(cx, |project, cx| {
11097 project.open_buffer((worktree_id, "file1.rs"), cx)
11098 })
11099 .await
11100 .unwrap();
11101 let buffer_2 = project
11102 .update(cx, |project, cx| {
11103 project.open_buffer((worktree_id, "file2.rs"), cx)
11104 })
11105 .await
11106 .unwrap();
11107 let buffer_3 = project
11108 .update(cx, |project, cx| {
11109 project.open_buffer((worktree_id, "file3.rs"), cx)
11110 })
11111 .await
11112 .unwrap();
11113
11114 // Create a multi-buffer with all three buffers
11115 let multi_buffer = cx.new(|cx| {
11116 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11117 multi_buffer.push_excerpts(
11118 buffer_1.clone(),
11119 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11120 cx,
11121 );
11122 multi_buffer.push_excerpts(
11123 buffer_2.clone(),
11124 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11125 cx,
11126 );
11127 multi_buffer.push_excerpts(
11128 buffer_3.clone(),
11129 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11130 cx,
11131 );
11132 multi_buffer
11133 });
11134
11135 let editor = cx.new_window_entity(|window, cx| {
11136 Editor::new(
11137 EditorMode::full(),
11138 multi_buffer,
11139 Some(project.clone()),
11140 window,
11141 cx,
11142 )
11143 });
11144
11145 // Edit only the first buffer
11146 editor.update_in(cx, |editor, window, cx| {
11147 editor.change_selections(
11148 SelectionEffects::scroll(Autoscroll::Next),
11149 window,
11150 cx,
11151 |s| s.select_ranges(Some(10..10)),
11152 );
11153 editor.insert("// edited", window, cx);
11154 });
11155
11156 // Verify that only buffer 1 is dirty
11157 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11158 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11159 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11160
11161 // Get write counts after file creation (files were created with initial content)
11162 // We expect each file to have been written once during creation
11163 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11164 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11165 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11166
11167 // Perform autosave
11168 let save_task = editor.update_in(cx, |editor, window, cx| {
11169 editor.save(
11170 SaveOptions {
11171 format: true,
11172 autosave: true,
11173 },
11174 project.clone(),
11175 window,
11176 cx,
11177 )
11178 });
11179 save_task.await.unwrap();
11180
11181 // Only the dirty buffer should have been saved
11182 assert_eq!(
11183 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11184 1,
11185 "Buffer 1 was dirty, so it should have been written once during autosave"
11186 );
11187 assert_eq!(
11188 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11189 0,
11190 "Buffer 2 was clean, so it should not have been written during autosave"
11191 );
11192 assert_eq!(
11193 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11194 0,
11195 "Buffer 3 was clean, so it should not have been written during autosave"
11196 );
11197
11198 // Verify buffer states after autosave
11199 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11200 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11201 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11202
11203 // Now perform a manual save (format = true)
11204 let save_task = editor.update_in(cx, |editor, window, cx| {
11205 editor.save(
11206 SaveOptions {
11207 format: true,
11208 autosave: false,
11209 },
11210 project.clone(),
11211 window,
11212 cx,
11213 )
11214 });
11215 save_task.await.unwrap();
11216
11217 // During manual save, clean buffers don't get written to disk
11218 // They just get did_save called for language server notifications
11219 assert_eq!(
11220 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11221 1,
11222 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11223 );
11224 assert_eq!(
11225 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11226 0,
11227 "Buffer 2 should not have been written at all"
11228 );
11229 assert_eq!(
11230 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11231 0,
11232 "Buffer 3 should not have been written at all"
11233 );
11234}
11235
11236async fn setup_range_format_test(
11237 cx: &mut TestAppContext,
11238) -> (
11239 Entity<Project>,
11240 Entity<Editor>,
11241 &mut gpui::VisualTestContext,
11242 lsp::FakeLanguageServer,
11243) {
11244 init_test(cx, |_| {});
11245
11246 let fs = FakeFs::new(cx.executor());
11247 fs.insert_file(path!("/file.rs"), Default::default()).await;
11248
11249 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11250
11251 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11252 language_registry.add(rust_lang());
11253 let mut fake_servers = language_registry.register_fake_lsp(
11254 "Rust",
11255 FakeLspAdapter {
11256 capabilities: lsp::ServerCapabilities {
11257 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11258 ..lsp::ServerCapabilities::default()
11259 },
11260 ..FakeLspAdapter::default()
11261 },
11262 );
11263
11264 let buffer = project
11265 .update(cx, |project, cx| {
11266 project.open_local_buffer(path!("/file.rs"), cx)
11267 })
11268 .await
11269 .unwrap();
11270
11271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11272 let (editor, cx) = cx.add_window_view(|window, cx| {
11273 build_editor_with_project(project.clone(), buffer, window, cx)
11274 });
11275
11276 cx.executor().start_waiting();
11277 let fake_server = fake_servers.next().await.unwrap();
11278
11279 (project, editor, cx, fake_server)
11280}
11281
11282#[gpui::test]
11283async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11284 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11285
11286 editor.update_in(cx, |editor, window, cx| {
11287 editor.set_text("one\ntwo\nthree\n", window, cx)
11288 });
11289 assert!(cx.read(|cx| editor.is_dirty(cx)));
11290
11291 let save = editor
11292 .update_in(cx, |editor, window, cx| {
11293 editor.save(
11294 SaveOptions {
11295 format: true,
11296 autosave: false,
11297 },
11298 project.clone(),
11299 window,
11300 cx,
11301 )
11302 })
11303 .unwrap();
11304 fake_server
11305 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11306 assert_eq!(
11307 params.text_document.uri,
11308 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11309 );
11310 assert_eq!(params.options.tab_size, 4);
11311 Ok(Some(vec![lsp::TextEdit::new(
11312 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11313 ", ".to_string(),
11314 )]))
11315 })
11316 .next()
11317 .await;
11318 cx.executor().start_waiting();
11319 save.await;
11320 assert_eq!(
11321 editor.update(cx, |editor, cx| editor.text(cx)),
11322 "one, two\nthree\n"
11323 );
11324 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11325}
11326
11327#[gpui::test]
11328async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11329 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11330
11331 editor.update_in(cx, |editor, window, cx| {
11332 editor.set_text("one\ntwo\nthree\n", window, cx)
11333 });
11334 assert!(cx.read(|cx| editor.is_dirty(cx)));
11335
11336 // Test that save still works when formatting hangs
11337 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11338 move |params, _| async move {
11339 assert_eq!(
11340 params.text_document.uri,
11341 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11342 );
11343 futures::future::pending::<()>().await;
11344 unreachable!()
11345 },
11346 );
11347 let save = editor
11348 .update_in(cx, |editor, window, cx| {
11349 editor.save(
11350 SaveOptions {
11351 format: true,
11352 autosave: false,
11353 },
11354 project.clone(),
11355 window,
11356 cx,
11357 )
11358 })
11359 .unwrap();
11360 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11361 cx.executor().start_waiting();
11362 save.await;
11363 assert_eq!(
11364 editor.update(cx, |editor, cx| editor.text(cx)),
11365 "one\ntwo\nthree\n"
11366 );
11367 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11368}
11369
11370#[gpui::test]
11371async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11372 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11373
11374 // Buffer starts clean, no formatting should be requested
11375 let save = editor
11376 .update_in(cx, |editor, window, cx| {
11377 editor.save(
11378 SaveOptions {
11379 format: false,
11380 autosave: false,
11381 },
11382 project.clone(),
11383 window,
11384 cx,
11385 )
11386 })
11387 .unwrap();
11388 let _pending_format_request = fake_server
11389 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11390 panic!("Should not be invoked");
11391 })
11392 .next();
11393 cx.executor().start_waiting();
11394 save.await;
11395 cx.run_until_parked();
11396}
11397
11398#[gpui::test]
11399async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11400 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11401
11402 // Set Rust language override and assert overridden tabsize is sent to language server
11403 update_test_language_settings(cx, |settings| {
11404 settings.languages.0.insert(
11405 "Rust".into(),
11406 LanguageSettingsContent {
11407 tab_size: NonZeroU32::new(8),
11408 ..Default::default()
11409 },
11410 );
11411 });
11412
11413 editor.update_in(cx, |editor, window, cx| {
11414 editor.set_text("something_new\n", window, cx)
11415 });
11416 assert!(cx.read(|cx| editor.is_dirty(cx)));
11417 let save = editor
11418 .update_in(cx, |editor, window, cx| {
11419 editor.save(
11420 SaveOptions {
11421 format: true,
11422 autosave: false,
11423 },
11424 project.clone(),
11425 window,
11426 cx,
11427 )
11428 })
11429 .unwrap();
11430 fake_server
11431 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11432 assert_eq!(
11433 params.text_document.uri,
11434 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11435 );
11436 assert_eq!(params.options.tab_size, 8);
11437 Ok(Some(Vec::new()))
11438 })
11439 .next()
11440 .await;
11441 save.await;
11442}
11443
11444#[gpui::test]
11445async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11446 init_test(cx, |settings| {
11447 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11448 Formatter::LanguageServer { name: None },
11449 )))
11450 });
11451
11452 let fs = FakeFs::new(cx.executor());
11453 fs.insert_file(path!("/file.rs"), Default::default()).await;
11454
11455 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11456
11457 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11458 language_registry.add(Arc::new(Language::new(
11459 LanguageConfig {
11460 name: "Rust".into(),
11461 matcher: LanguageMatcher {
11462 path_suffixes: vec!["rs".to_string()],
11463 ..Default::default()
11464 },
11465 ..LanguageConfig::default()
11466 },
11467 Some(tree_sitter_rust::LANGUAGE.into()),
11468 )));
11469 update_test_language_settings(cx, |settings| {
11470 // Enable Prettier formatting for the same buffer, and ensure
11471 // LSP is called instead of Prettier.
11472 settings.defaults.prettier = Some(PrettierSettings {
11473 allowed: true,
11474 ..PrettierSettings::default()
11475 });
11476 });
11477 let mut fake_servers = language_registry.register_fake_lsp(
11478 "Rust",
11479 FakeLspAdapter {
11480 capabilities: lsp::ServerCapabilities {
11481 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11482 ..Default::default()
11483 },
11484 ..Default::default()
11485 },
11486 );
11487
11488 let buffer = project
11489 .update(cx, |project, cx| {
11490 project.open_local_buffer(path!("/file.rs"), cx)
11491 })
11492 .await
11493 .unwrap();
11494
11495 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11496 let (editor, cx) = cx.add_window_view(|window, cx| {
11497 build_editor_with_project(project.clone(), buffer, window, cx)
11498 });
11499 editor.update_in(cx, |editor, window, cx| {
11500 editor.set_text("one\ntwo\nthree\n", window, cx)
11501 });
11502
11503 cx.executor().start_waiting();
11504 let fake_server = fake_servers.next().await.unwrap();
11505
11506 let format = editor
11507 .update_in(cx, |editor, window, cx| {
11508 editor.perform_format(
11509 project.clone(),
11510 FormatTrigger::Manual,
11511 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11512 window,
11513 cx,
11514 )
11515 })
11516 .unwrap();
11517 fake_server
11518 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11519 assert_eq!(
11520 params.text_document.uri,
11521 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11522 );
11523 assert_eq!(params.options.tab_size, 4);
11524 Ok(Some(vec![lsp::TextEdit::new(
11525 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11526 ", ".to_string(),
11527 )]))
11528 })
11529 .next()
11530 .await;
11531 cx.executor().start_waiting();
11532 format.await;
11533 assert_eq!(
11534 editor.update(cx, |editor, cx| editor.text(cx)),
11535 "one, two\nthree\n"
11536 );
11537
11538 editor.update_in(cx, |editor, window, cx| {
11539 editor.set_text("one\ntwo\nthree\n", window, cx)
11540 });
11541 // Ensure we don't lock if formatting hangs.
11542 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11543 move |params, _| async move {
11544 assert_eq!(
11545 params.text_document.uri,
11546 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11547 );
11548 futures::future::pending::<()>().await;
11549 unreachable!()
11550 },
11551 );
11552 let format = editor
11553 .update_in(cx, |editor, window, cx| {
11554 editor.perform_format(
11555 project,
11556 FormatTrigger::Manual,
11557 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11558 window,
11559 cx,
11560 )
11561 })
11562 .unwrap();
11563 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11564 cx.executor().start_waiting();
11565 format.await;
11566 assert_eq!(
11567 editor.update(cx, |editor, cx| editor.text(cx)),
11568 "one\ntwo\nthree\n"
11569 );
11570}
11571
11572#[gpui::test]
11573async fn test_multiple_formatters(cx: &mut TestAppContext) {
11574 init_test(cx, |settings| {
11575 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11576 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11577 Formatter::LanguageServer { name: None },
11578 Formatter::CodeActions(
11579 [
11580 ("code-action-1".into(), true),
11581 ("code-action-2".into(), true),
11582 ]
11583 .into_iter()
11584 .collect(),
11585 ),
11586 ])))
11587 });
11588
11589 let fs = FakeFs::new(cx.executor());
11590 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11591 .await;
11592
11593 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11594 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11595 language_registry.add(rust_lang());
11596
11597 let mut fake_servers = language_registry.register_fake_lsp(
11598 "Rust",
11599 FakeLspAdapter {
11600 capabilities: lsp::ServerCapabilities {
11601 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11602 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11603 commands: vec!["the-command-for-code-action-1".into()],
11604 ..Default::default()
11605 }),
11606 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11607 ..Default::default()
11608 },
11609 ..Default::default()
11610 },
11611 );
11612
11613 let buffer = project
11614 .update(cx, |project, cx| {
11615 project.open_local_buffer(path!("/file.rs"), cx)
11616 })
11617 .await
11618 .unwrap();
11619
11620 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11621 let (editor, cx) = cx.add_window_view(|window, cx| {
11622 build_editor_with_project(project.clone(), buffer, window, cx)
11623 });
11624
11625 cx.executor().start_waiting();
11626
11627 let fake_server = fake_servers.next().await.unwrap();
11628 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11629 move |_params, _| async move {
11630 Ok(Some(vec![lsp::TextEdit::new(
11631 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11632 "applied-formatting\n".to_string(),
11633 )]))
11634 },
11635 );
11636 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11637 move |params, _| async move {
11638 assert_eq!(
11639 params.context.only,
11640 Some(vec!["code-action-1".into(), "code-action-2".into()])
11641 );
11642 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11643 Ok(Some(vec![
11644 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11645 kind: Some("code-action-1".into()),
11646 edit: Some(lsp::WorkspaceEdit::new(
11647 [(
11648 uri.clone(),
11649 vec![lsp::TextEdit::new(
11650 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11651 "applied-code-action-1-edit\n".to_string(),
11652 )],
11653 )]
11654 .into_iter()
11655 .collect(),
11656 )),
11657 command: Some(lsp::Command {
11658 command: "the-command-for-code-action-1".into(),
11659 ..Default::default()
11660 }),
11661 ..Default::default()
11662 }),
11663 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11664 kind: Some("code-action-2".into()),
11665 edit: Some(lsp::WorkspaceEdit::new(
11666 [(
11667 uri,
11668 vec![lsp::TextEdit::new(
11669 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11670 "applied-code-action-2-edit\n".to_string(),
11671 )],
11672 )]
11673 .into_iter()
11674 .collect(),
11675 )),
11676 ..Default::default()
11677 }),
11678 ]))
11679 },
11680 );
11681
11682 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11683 move |params, _| async move { Ok(params) }
11684 });
11685
11686 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11687 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11688 let fake = fake_server.clone();
11689 let lock = command_lock.clone();
11690 move |params, _| {
11691 assert_eq!(params.command, "the-command-for-code-action-1");
11692 let fake = fake.clone();
11693 let lock = lock.clone();
11694 async move {
11695 lock.lock().await;
11696 fake.server
11697 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11698 label: None,
11699 edit: lsp::WorkspaceEdit {
11700 changes: Some(
11701 [(
11702 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11703 vec![lsp::TextEdit {
11704 range: lsp::Range::new(
11705 lsp::Position::new(0, 0),
11706 lsp::Position::new(0, 0),
11707 ),
11708 new_text: "applied-code-action-1-command\n".into(),
11709 }],
11710 )]
11711 .into_iter()
11712 .collect(),
11713 ),
11714 ..Default::default()
11715 },
11716 })
11717 .await
11718 .into_response()
11719 .unwrap();
11720 Ok(Some(json!(null)))
11721 }
11722 }
11723 });
11724
11725 cx.executor().start_waiting();
11726 editor
11727 .update_in(cx, |editor, window, cx| {
11728 editor.perform_format(
11729 project.clone(),
11730 FormatTrigger::Manual,
11731 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11732 window,
11733 cx,
11734 )
11735 })
11736 .unwrap()
11737 .await;
11738 editor.update(cx, |editor, cx| {
11739 assert_eq!(
11740 editor.text(cx),
11741 r#"
11742 applied-code-action-2-edit
11743 applied-code-action-1-command
11744 applied-code-action-1-edit
11745 applied-formatting
11746 one
11747 two
11748 three
11749 "#
11750 .unindent()
11751 );
11752 });
11753
11754 editor.update_in(cx, |editor, window, cx| {
11755 editor.undo(&Default::default(), window, cx);
11756 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11757 });
11758
11759 // Perform a manual edit while waiting for an LSP command
11760 // that's being run as part of a formatting code action.
11761 let lock_guard = command_lock.lock().await;
11762 let format = editor
11763 .update_in(cx, |editor, window, cx| {
11764 editor.perform_format(
11765 project.clone(),
11766 FormatTrigger::Manual,
11767 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11768 window,
11769 cx,
11770 )
11771 })
11772 .unwrap();
11773 cx.run_until_parked();
11774 editor.update(cx, |editor, cx| {
11775 assert_eq!(
11776 editor.text(cx),
11777 r#"
11778 applied-code-action-1-edit
11779 applied-formatting
11780 one
11781 two
11782 three
11783 "#
11784 .unindent()
11785 );
11786
11787 editor.buffer.update(cx, |buffer, cx| {
11788 let ix = buffer.len(cx);
11789 buffer.edit([(ix..ix, "edited\n")], None, cx);
11790 });
11791 });
11792
11793 // Allow the LSP command to proceed. Because the buffer was edited,
11794 // the second code action will not be run.
11795 drop(lock_guard);
11796 format.await;
11797 editor.update_in(cx, |editor, window, cx| {
11798 assert_eq!(
11799 editor.text(cx),
11800 r#"
11801 applied-code-action-1-command
11802 applied-code-action-1-edit
11803 applied-formatting
11804 one
11805 two
11806 three
11807 edited
11808 "#
11809 .unindent()
11810 );
11811
11812 // The manual edit is undone first, because it is the last thing the user did
11813 // (even though the command completed afterwards).
11814 editor.undo(&Default::default(), window, cx);
11815 assert_eq!(
11816 editor.text(cx),
11817 r#"
11818 applied-code-action-1-command
11819 applied-code-action-1-edit
11820 applied-formatting
11821 one
11822 two
11823 three
11824 "#
11825 .unindent()
11826 );
11827
11828 // All the formatting (including the command, which completed after the manual edit)
11829 // is undone together.
11830 editor.undo(&Default::default(), window, cx);
11831 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11832 });
11833}
11834
11835#[gpui::test]
11836async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
11837 init_test(cx, |settings| {
11838 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11839 Formatter::LanguageServer { name: None },
11840 ])))
11841 });
11842
11843 let fs = FakeFs::new(cx.executor());
11844 fs.insert_file(path!("/file.ts"), Default::default()).await;
11845
11846 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11847
11848 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11849 language_registry.add(Arc::new(Language::new(
11850 LanguageConfig {
11851 name: "TypeScript".into(),
11852 matcher: LanguageMatcher {
11853 path_suffixes: vec!["ts".to_string()],
11854 ..Default::default()
11855 },
11856 ..LanguageConfig::default()
11857 },
11858 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11859 )));
11860 update_test_language_settings(cx, |settings| {
11861 settings.defaults.prettier = Some(PrettierSettings {
11862 allowed: true,
11863 ..PrettierSettings::default()
11864 });
11865 });
11866 let mut fake_servers = language_registry.register_fake_lsp(
11867 "TypeScript",
11868 FakeLspAdapter {
11869 capabilities: lsp::ServerCapabilities {
11870 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11871 ..Default::default()
11872 },
11873 ..Default::default()
11874 },
11875 );
11876
11877 let buffer = project
11878 .update(cx, |project, cx| {
11879 project.open_local_buffer(path!("/file.ts"), cx)
11880 })
11881 .await
11882 .unwrap();
11883
11884 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11885 let (editor, cx) = cx.add_window_view(|window, cx| {
11886 build_editor_with_project(project.clone(), buffer, window, cx)
11887 });
11888 editor.update_in(cx, |editor, window, cx| {
11889 editor.set_text(
11890 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11891 window,
11892 cx,
11893 )
11894 });
11895
11896 cx.executor().start_waiting();
11897 let fake_server = fake_servers.next().await.unwrap();
11898
11899 let format = editor
11900 .update_in(cx, |editor, window, cx| {
11901 editor.perform_code_action_kind(
11902 project.clone(),
11903 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11904 window,
11905 cx,
11906 )
11907 })
11908 .unwrap();
11909 fake_server
11910 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11911 assert_eq!(
11912 params.text_document.uri,
11913 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11914 );
11915 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11916 lsp::CodeAction {
11917 title: "Organize Imports".to_string(),
11918 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11919 edit: Some(lsp::WorkspaceEdit {
11920 changes: Some(
11921 [(
11922 params.text_document.uri.clone(),
11923 vec![lsp::TextEdit::new(
11924 lsp::Range::new(
11925 lsp::Position::new(1, 0),
11926 lsp::Position::new(2, 0),
11927 ),
11928 "".to_string(),
11929 )],
11930 )]
11931 .into_iter()
11932 .collect(),
11933 ),
11934 ..Default::default()
11935 }),
11936 ..Default::default()
11937 },
11938 )]))
11939 })
11940 .next()
11941 .await;
11942 cx.executor().start_waiting();
11943 format.await;
11944 assert_eq!(
11945 editor.update(cx, |editor, cx| editor.text(cx)),
11946 "import { a } from 'module';\n\nconst x = a;\n"
11947 );
11948
11949 editor.update_in(cx, |editor, window, cx| {
11950 editor.set_text(
11951 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11952 window,
11953 cx,
11954 )
11955 });
11956 // Ensure we don't lock if code action hangs.
11957 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11958 move |params, _| async move {
11959 assert_eq!(
11960 params.text_document.uri,
11961 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11962 );
11963 futures::future::pending::<()>().await;
11964 unreachable!()
11965 },
11966 );
11967 let format = editor
11968 .update_in(cx, |editor, window, cx| {
11969 editor.perform_code_action_kind(
11970 project,
11971 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11972 window,
11973 cx,
11974 )
11975 })
11976 .unwrap();
11977 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11978 cx.executor().start_waiting();
11979 format.await;
11980 assert_eq!(
11981 editor.update(cx, |editor, cx| editor.text(cx)),
11982 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11983 );
11984}
11985
11986#[gpui::test]
11987async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11988 init_test(cx, |_| {});
11989
11990 let mut cx = EditorLspTestContext::new_rust(
11991 lsp::ServerCapabilities {
11992 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11993 ..Default::default()
11994 },
11995 cx,
11996 )
11997 .await;
11998
11999 cx.set_state(indoc! {"
12000 one.twoˇ
12001 "});
12002
12003 // The format request takes a long time. When it completes, it inserts
12004 // a newline and an indent before the `.`
12005 cx.lsp
12006 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12007 let executor = cx.background_executor().clone();
12008 async move {
12009 executor.timer(Duration::from_millis(100)).await;
12010 Ok(Some(vec![lsp::TextEdit {
12011 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12012 new_text: "\n ".into(),
12013 }]))
12014 }
12015 });
12016
12017 // Submit a format request.
12018 let format_1 = cx
12019 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12020 .unwrap();
12021 cx.executor().run_until_parked();
12022
12023 // Submit a second format request.
12024 let format_2 = cx
12025 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12026 .unwrap();
12027 cx.executor().run_until_parked();
12028
12029 // Wait for both format requests to complete
12030 cx.executor().advance_clock(Duration::from_millis(200));
12031 cx.executor().start_waiting();
12032 format_1.await.unwrap();
12033 cx.executor().start_waiting();
12034 format_2.await.unwrap();
12035
12036 // The formatting edits only happens once.
12037 cx.assert_editor_state(indoc! {"
12038 one
12039 .twoˇ
12040 "});
12041}
12042
12043#[gpui::test]
12044async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12045 init_test(cx, |settings| {
12046 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12047 });
12048
12049 let mut cx = EditorLspTestContext::new_rust(
12050 lsp::ServerCapabilities {
12051 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12052 ..Default::default()
12053 },
12054 cx,
12055 )
12056 .await;
12057
12058 // Set up a buffer white some trailing whitespace and no trailing newline.
12059 cx.set_state(
12060 &[
12061 "one ", //
12062 "twoˇ", //
12063 "three ", //
12064 "four", //
12065 ]
12066 .join("\n"),
12067 );
12068
12069 // Submit a format request.
12070 let format = cx
12071 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12072 .unwrap();
12073
12074 // Record which buffer changes have been sent to the language server
12075 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12076 cx.lsp
12077 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12078 let buffer_changes = buffer_changes.clone();
12079 move |params, _| {
12080 buffer_changes.lock().extend(
12081 params
12082 .content_changes
12083 .into_iter()
12084 .map(|e| (e.range.unwrap(), e.text)),
12085 );
12086 }
12087 });
12088
12089 // Handle formatting requests to the language server.
12090 cx.lsp
12091 .set_request_handler::<lsp::request::Formatting, _, _>({
12092 let buffer_changes = buffer_changes.clone();
12093 move |_, _| {
12094 // When formatting is requested, trailing whitespace has already been stripped,
12095 // and the trailing newline has already been added.
12096 assert_eq!(
12097 &buffer_changes.lock()[1..],
12098 &[
12099 (
12100 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12101 "".into()
12102 ),
12103 (
12104 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12105 "".into()
12106 ),
12107 (
12108 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12109 "\n".into()
12110 ),
12111 ]
12112 );
12113
12114 // Insert blank lines between each line of the buffer.
12115 async move {
12116 Ok(Some(vec![
12117 lsp::TextEdit {
12118 range: lsp::Range::new(
12119 lsp::Position::new(1, 0),
12120 lsp::Position::new(1, 0),
12121 ),
12122 new_text: "\n".into(),
12123 },
12124 lsp::TextEdit {
12125 range: lsp::Range::new(
12126 lsp::Position::new(2, 0),
12127 lsp::Position::new(2, 0),
12128 ),
12129 new_text: "\n".into(),
12130 },
12131 ]))
12132 }
12133 }
12134 });
12135
12136 // After formatting the buffer, the trailing whitespace is stripped,
12137 // a newline is appended, and the edits provided by the language server
12138 // have been applied.
12139 format.await.unwrap();
12140 cx.assert_editor_state(
12141 &[
12142 "one", //
12143 "", //
12144 "twoˇ", //
12145 "", //
12146 "three", //
12147 "four", //
12148 "", //
12149 ]
12150 .join("\n"),
12151 );
12152
12153 // Undoing the formatting undoes the trailing whitespace removal, the
12154 // trailing newline, and the LSP edits.
12155 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12156 cx.assert_editor_state(
12157 &[
12158 "one ", //
12159 "twoˇ", //
12160 "three ", //
12161 "four", //
12162 ]
12163 .join("\n"),
12164 );
12165}
12166
12167#[gpui::test]
12168async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12169 cx: &mut TestAppContext,
12170) {
12171 init_test(cx, |_| {});
12172
12173 cx.update(|cx| {
12174 cx.update_global::<SettingsStore, _>(|settings, cx| {
12175 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12176 settings.auto_signature_help = Some(true);
12177 });
12178 });
12179 });
12180
12181 let mut cx = EditorLspTestContext::new_rust(
12182 lsp::ServerCapabilities {
12183 signature_help_provider: Some(lsp::SignatureHelpOptions {
12184 ..Default::default()
12185 }),
12186 ..Default::default()
12187 },
12188 cx,
12189 )
12190 .await;
12191
12192 let language = Language::new(
12193 LanguageConfig {
12194 name: "Rust".into(),
12195 brackets: BracketPairConfig {
12196 pairs: vec![
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: true,
12208 surround: true,
12209 newline: true,
12210 },
12211 BracketPair {
12212 start: "/*".to_string(),
12213 end: " */".to_string(),
12214 close: true,
12215 surround: true,
12216 newline: true,
12217 },
12218 BracketPair {
12219 start: "[".to_string(),
12220 end: "]".to_string(),
12221 close: false,
12222 surround: false,
12223 newline: true,
12224 },
12225 BracketPair {
12226 start: "\"".to_string(),
12227 end: "\"".to_string(),
12228 close: true,
12229 surround: true,
12230 newline: false,
12231 },
12232 BracketPair {
12233 start: "<".to_string(),
12234 end: ">".to_string(),
12235 close: false,
12236 surround: true,
12237 newline: true,
12238 },
12239 ],
12240 ..Default::default()
12241 },
12242 autoclose_before: "})]".to_string(),
12243 ..Default::default()
12244 },
12245 Some(tree_sitter_rust::LANGUAGE.into()),
12246 );
12247 let language = Arc::new(language);
12248
12249 cx.language_registry().add(language.clone());
12250 cx.update_buffer(|buffer, cx| {
12251 buffer.set_language(Some(language), cx);
12252 });
12253
12254 cx.set_state(
12255 &r#"
12256 fn main() {
12257 sampleˇ
12258 }
12259 "#
12260 .unindent(),
12261 );
12262
12263 cx.update_editor(|editor, window, cx| {
12264 editor.handle_input("(", window, cx);
12265 });
12266 cx.assert_editor_state(
12267 &"
12268 fn main() {
12269 sample(ˇ)
12270 }
12271 "
12272 .unindent(),
12273 );
12274
12275 let mocked_response = lsp::SignatureHelp {
12276 signatures: vec![lsp::SignatureInformation {
12277 label: "fn sample(param1: u8, param2: u8)".to_string(),
12278 documentation: None,
12279 parameters: Some(vec![
12280 lsp::ParameterInformation {
12281 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12282 documentation: None,
12283 },
12284 lsp::ParameterInformation {
12285 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12286 documentation: None,
12287 },
12288 ]),
12289 active_parameter: None,
12290 }],
12291 active_signature: Some(0),
12292 active_parameter: Some(0),
12293 };
12294 handle_signature_help_request(&mut cx, mocked_response).await;
12295
12296 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12297 .await;
12298
12299 cx.editor(|editor, _, _| {
12300 let signature_help_state = editor.signature_help_state.popover().cloned();
12301 let signature = signature_help_state.unwrap();
12302 assert_eq!(
12303 signature.signatures[signature.current_signature].label,
12304 "fn sample(param1: u8, param2: u8)"
12305 );
12306 });
12307}
12308
12309#[gpui::test]
12310async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12311 init_test(cx, |_| {});
12312
12313 cx.update(|cx| {
12314 cx.update_global::<SettingsStore, _>(|settings, cx| {
12315 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12316 settings.auto_signature_help = Some(false);
12317 settings.show_signature_help_after_edits = Some(false);
12318 });
12319 });
12320 });
12321
12322 let mut cx = EditorLspTestContext::new_rust(
12323 lsp::ServerCapabilities {
12324 signature_help_provider: Some(lsp::SignatureHelpOptions {
12325 ..Default::default()
12326 }),
12327 ..Default::default()
12328 },
12329 cx,
12330 )
12331 .await;
12332
12333 let language = Language::new(
12334 LanguageConfig {
12335 name: "Rust".into(),
12336 brackets: BracketPairConfig {
12337 pairs: vec![
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: true,
12349 surround: true,
12350 newline: true,
12351 },
12352 BracketPair {
12353 start: "/*".to_string(),
12354 end: " */".to_string(),
12355 close: true,
12356 surround: true,
12357 newline: true,
12358 },
12359 BracketPair {
12360 start: "[".to_string(),
12361 end: "]".to_string(),
12362 close: false,
12363 surround: false,
12364 newline: true,
12365 },
12366 BracketPair {
12367 start: "\"".to_string(),
12368 end: "\"".to_string(),
12369 close: true,
12370 surround: true,
12371 newline: false,
12372 },
12373 BracketPair {
12374 start: "<".to_string(),
12375 end: ">".to_string(),
12376 close: false,
12377 surround: true,
12378 newline: true,
12379 },
12380 ],
12381 ..Default::default()
12382 },
12383 autoclose_before: "})]".to_string(),
12384 ..Default::default()
12385 },
12386 Some(tree_sitter_rust::LANGUAGE.into()),
12387 );
12388 let language = Arc::new(language);
12389
12390 cx.language_registry().add(language.clone());
12391 cx.update_buffer(|buffer, cx| {
12392 buffer.set_language(Some(language), cx);
12393 });
12394
12395 // Ensure that signature_help is not called when no signature help is enabled.
12396 cx.set_state(
12397 &r#"
12398 fn main() {
12399 sampleˇ
12400 }
12401 "#
12402 .unindent(),
12403 );
12404 cx.update_editor(|editor, window, cx| {
12405 editor.handle_input("(", window, cx);
12406 });
12407 cx.assert_editor_state(
12408 &"
12409 fn main() {
12410 sample(ˇ)
12411 }
12412 "
12413 .unindent(),
12414 );
12415 cx.editor(|editor, _, _| {
12416 assert!(editor.signature_help_state.task().is_none());
12417 });
12418
12419 let mocked_response = lsp::SignatureHelp {
12420 signatures: vec![lsp::SignatureInformation {
12421 label: "fn sample(param1: u8, param2: u8)".to_string(),
12422 documentation: None,
12423 parameters: Some(vec![
12424 lsp::ParameterInformation {
12425 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12426 documentation: None,
12427 },
12428 lsp::ParameterInformation {
12429 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12430 documentation: None,
12431 },
12432 ]),
12433 active_parameter: None,
12434 }],
12435 active_signature: Some(0),
12436 active_parameter: Some(0),
12437 };
12438
12439 // Ensure that signature_help is called when enabled afte edits
12440 cx.update(|_, cx| {
12441 cx.update_global::<SettingsStore, _>(|settings, cx| {
12442 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12443 settings.auto_signature_help = Some(false);
12444 settings.show_signature_help_after_edits = Some(true);
12445 });
12446 });
12447 });
12448 cx.set_state(
12449 &r#"
12450 fn main() {
12451 sampleˇ
12452 }
12453 "#
12454 .unindent(),
12455 );
12456 cx.update_editor(|editor, window, cx| {
12457 editor.handle_input("(", window, cx);
12458 });
12459 cx.assert_editor_state(
12460 &"
12461 fn main() {
12462 sample(ˇ)
12463 }
12464 "
12465 .unindent(),
12466 );
12467 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12468 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12469 .await;
12470 cx.update_editor(|editor, _, _| {
12471 let signature_help_state = editor.signature_help_state.popover().cloned();
12472 assert!(signature_help_state.is_some());
12473 let signature = signature_help_state.unwrap();
12474 assert_eq!(
12475 signature.signatures[signature.current_signature].label,
12476 "fn sample(param1: u8, param2: u8)"
12477 );
12478 editor.signature_help_state = SignatureHelpState::default();
12479 });
12480
12481 // Ensure that signature_help is called when auto signature help override is enabled
12482 cx.update(|_, cx| {
12483 cx.update_global::<SettingsStore, _>(|settings, cx| {
12484 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12485 settings.auto_signature_help = Some(true);
12486 settings.show_signature_help_after_edits = Some(false);
12487 });
12488 });
12489 });
12490 cx.set_state(
12491 &r#"
12492 fn main() {
12493 sampleˇ
12494 }
12495 "#
12496 .unindent(),
12497 );
12498 cx.update_editor(|editor, window, cx| {
12499 editor.handle_input("(", window, cx);
12500 });
12501 cx.assert_editor_state(
12502 &"
12503 fn main() {
12504 sample(ˇ)
12505 }
12506 "
12507 .unindent(),
12508 );
12509 handle_signature_help_request(&mut cx, mocked_response).await;
12510 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12511 .await;
12512 cx.editor(|editor, _, _| {
12513 let signature_help_state = editor.signature_help_state.popover().cloned();
12514 assert!(signature_help_state.is_some());
12515 let signature = signature_help_state.unwrap();
12516 assert_eq!(
12517 signature.signatures[signature.current_signature].label,
12518 "fn sample(param1: u8, param2: u8)"
12519 );
12520 });
12521}
12522
12523#[gpui::test]
12524async fn test_signature_help(cx: &mut TestAppContext) {
12525 init_test(cx, |_| {});
12526 cx.update(|cx| {
12527 cx.update_global::<SettingsStore, _>(|settings, cx| {
12528 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12529 settings.auto_signature_help = Some(true);
12530 });
12531 });
12532 });
12533
12534 let mut cx = EditorLspTestContext::new_rust(
12535 lsp::ServerCapabilities {
12536 signature_help_provider: Some(lsp::SignatureHelpOptions {
12537 ..Default::default()
12538 }),
12539 ..Default::default()
12540 },
12541 cx,
12542 )
12543 .await;
12544
12545 // A test that directly calls `show_signature_help`
12546 cx.update_editor(|editor, window, cx| {
12547 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12548 });
12549
12550 let mocked_response = lsp::SignatureHelp {
12551 signatures: vec![lsp::SignatureInformation {
12552 label: "fn sample(param1: u8, param2: u8)".to_string(),
12553 documentation: None,
12554 parameters: Some(vec![
12555 lsp::ParameterInformation {
12556 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12557 documentation: None,
12558 },
12559 lsp::ParameterInformation {
12560 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12561 documentation: None,
12562 },
12563 ]),
12564 active_parameter: None,
12565 }],
12566 active_signature: Some(0),
12567 active_parameter: Some(0),
12568 };
12569 handle_signature_help_request(&mut cx, mocked_response).await;
12570
12571 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12572 .await;
12573
12574 cx.editor(|editor, _, _| {
12575 let signature_help_state = editor.signature_help_state.popover().cloned();
12576 assert!(signature_help_state.is_some());
12577 let signature = signature_help_state.unwrap();
12578 assert_eq!(
12579 signature.signatures[signature.current_signature].label,
12580 "fn sample(param1: u8, param2: u8)"
12581 );
12582 });
12583
12584 // When exiting outside from inside the brackets, `signature_help` is closed.
12585 cx.set_state(indoc! {"
12586 fn main() {
12587 sample(ˇ);
12588 }
12589
12590 fn sample(param1: u8, param2: u8) {}
12591 "});
12592
12593 cx.update_editor(|editor, window, cx| {
12594 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12595 s.select_ranges([0..0])
12596 });
12597 });
12598
12599 let mocked_response = lsp::SignatureHelp {
12600 signatures: Vec::new(),
12601 active_signature: None,
12602 active_parameter: None,
12603 };
12604 handle_signature_help_request(&mut cx, mocked_response).await;
12605
12606 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12607 .await;
12608
12609 cx.editor(|editor, _, _| {
12610 assert!(!editor.signature_help_state.is_shown());
12611 });
12612
12613 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12614 cx.set_state(indoc! {"
12615 fn main() {
12616 sample(ˇ);
12617 }
12618
12619 fn sample(param1: u8, param2: u8) {}
12620 "});
12621
12622 let mocked_response = lsp::SignatureHelp {
12623 signatures: vec![lsp::SignatureInformation {
12624 label: "fn sample(param1: u8, param2: u8)".to_string(),
12625 documentation: None,
12626 parameters: Some(vec![
12627 lsp::ParameterInformation {
12628 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12629 documentation: None,
12630 },
12631 lsp::ParameterInformation {
12632 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12633 documentation: None,
12634 },
12635 ]),
12636 active_parameter: None,
12637 }],
12638 active_signature: Some(0),
12639 active_parameter: Some(0),
12640 };
12641 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12642 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12643 .await;
12644 cx.editor(|editor, _, _| {
12645 assert!(editor.signature_help_state.is_shown());
12646 });
12647
12648 // Restore the popover with more parameter input
12649 cx.set_state(indoc! {"
12650 fn main() {
12651 sample(param1, param2ˇ);
12652 }
12653
12654 fn sample(param1: u8, param2: u8) {}
12655 "});
12656
12657 let mocked_response = lsp::SignatureHelp {
12658 signatures: vec![lsp::SignatureInformation {
12659 label: "fn sample(param1: u8, param2: u8)".to_string(),
12660 documentation: None,
12661 parameters: Some(vec![
12662 lsp::ParameterInformation {
12663 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12664 documentation: None,
12665 },
12666 lsp::ParameterInformation {
12667 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12668 documentation: None,
12669 },
12670 ]),
12671 active_parameter: None,
12672 }],
12673 active_signature: Some(0),
12674 active_parameter: Some(1),
12675 };
12676 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12677 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12678 .await;
12679
12680 // When selecting a range, the popover is gone.
12681 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12682 cx.update_editor(|editor, window, cx| {
12683 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12684 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12685 })
12686 });
12687 cx.assert_editor_state(indoc! {"
12688 fn main() {
12689 sample(param1, «ˇparam2»);
12690 }
12691
12692 fn sample(param1: u8, param2: u8) {}
12693 "});
12694 cx.editor(|editor, _, _| {
12695 assert!(!editor.signature_help_state.is_shown());
12696 });
12697
12698 // When unselecting again, the popover is back if within the brackets.
12699 cx.update_editor(|editor, window, cx| {
12700 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12701 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12702 })
12703 });
12704 cx.assert_editor_state(indoc! {"
12705 fn main() {
12706 sample(param1, ˇparam2);
12707 }
12708
12709 fn sample(param1: u8, param2: u8) {}
12710 "});
12711 handle_signature_help_request(&mut cx, mocked_response).await;
12712 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12713 .await;
12714 cx.editor(|editor, _, _| {
12715 assert!(editor.signature_help_state.is_shown());
12716 });
12717
12718 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12719 cx.update_editor(|editor, window, cx| {
12720 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12721 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12722 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12723 })
12724 });
12725 cx.assert_editor_state(indoc! {"
12726 fn main() {
12727 sample(param1, ˇparam2);
12728 }
12729
12730 fn sample(param1: u8, param2: u8) {}
12731 "});
12732
12733 let mocked_response = lsp::SignatureHelp {
12734 signatures: vec![lsp::SignatureInformation {
12735 label: "fn sample(param1: u8, param2: u8)".to_string(),
12736 documentation: None,
12737 parameters: Some(vec![
12738 lsp::ParameterInformation {
12739 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12740 documentation: None,
12741 },
12742 lsp::ParameterInformation {
12743 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12744 documentation: None,
12745 },
12746 ]),
12747 active_parameter: None,
12748 }],
12749 active_signature: Some(0),
12750 active_parameter: Some(1),
12751 };
12752 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12753 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12754 .await;
12755 cx.update_editor(|editor, _, cx| {
12756 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12757 });
12758 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12759 .await;
12760 cx.update_editor(|editor, window, cx| {
12761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12762 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12763 })
12764 });
12765 cx.assert_editor_state(indoc! {"
12766 fn main() {
12767 sample(param1, «ˇparam2»);
12768 }
12769
12770 fn sample(param1: u8, param2: u8) {}
12771 "});
12772 cx.update_editor(|editor, window, cx| {
12773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12774 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12775 })
12776 });
12777 cx.assert_editor_state(indoc! {"
12778 fn main() {
12779 sample(param1, ˇparam2);
12780 }
12781
12782 fn sample(param1: u8, param2: u8) {}
12783 "});
12784 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
12785 .await;
12786}
12787
12788#[gpui::test]
12789async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
12790 init_test(cx, |_| {});
12791
12792 let mut cx = EditorLspTestContext::new_rust(
12793 lsp::ServerCapabilities {
12794 signature_help_provider: Some(lsp::SignatureHelpOptions {
12795 ..Default::default()
12796 }),
12797 ..Default::default()
12798 },
12799 cx,
12800 )
12801 .await;
12802
12803 cx.set_state(indoc! {"
12804 fn main() {
12805 overloadedˇ
12806 }
12807 "});
12808
12809 cx.update_editor(|editor, window, cx| {
12810 editor.handle_input("(", window, cx);
12811 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12812 });
12813
12814 // Mock response with 3 signatures
12815 let mocked_response = lsp::SignatureHelp {
12816 signatures: vec![
12817 lsp::SignatureInformation {
12818 label: "fn overloaded(x: i32)".to_string(),
12819 documentation: None,
12820 parameters: Some(vec![lsp::ParameterInformation {
12821 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12822 documentation: None,
12823 }]),
12824 active_parameter: None,
12825 },
12826 lsp::SignatureInformation {
12827 label: "fn overloaded(x: i32, y: i32)".to_string(),
12828 documentation: None,
12829 parameters: Some(vec![
12830 lsp::ParameterInformation {
12831 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12832 documentation: None,
12833 },
12834 lsp::ParameterInformation {
12835 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12836 documentation: None,
12837 },
12838 ]),
12839 active_parameter: None,
12840 },
12841 lsp::SignatureInformation {
12842 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
12843 documentation: None,
12844 parameters: Some(vec![
12845 lsp::ParameterInformation {
12846 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12847 documentation: None,
12848 },
12849 lsp::ParameterInformation {
12850 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12851 documentation: None,
12852 },
12853 lsp::ParameterInformation {
12854 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
12855 documentation: None,
12856 },
12857 ]),
12858 active_parameter: None,
12859 },
12860 ],
12861 active_signature: Some(1),
12862 active_parameter: Some(0),
12863 };
12864 handle_signature_help_request(&mut cx, mocked_response).await;
12865
12866 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12867 .await;
12868
12869 // Verify we have multiple signatures and the right one is selected
12870 cx.editor(|editor, _, _| {
12871 let popover = editor.signature_help_state.popover().cloned().unwrap();
12872 assert_eq!(popover.signatures.len(), 3);
12873 // active_signature was 1, so that should be the current
12874 assert_eq!(popover.current_signature, 1);
12875 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
12876 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
12877 assert_eq!(
12878 popover.signatures[2].label,
12879 "fn overloaded(x: i32, y: i32, z: i32)"
12880 );
12881 });
12882
12883 // Test navigation functionality
12884 cx.update_editor(|editor, window, cx| {
12885 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12886 });
12887
12888 cx.editor(|editor, _, _| {
12889 let popover = editor.signature_help_state.popover().cloned().unwrap();
12890 assert_eq!(popover.current_signature, 2);
12891 });
12892
12893 // Test wrap around
12894 cx.update_editor(|editor, window, cx| {
12895 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12896 });
12897
12898 cx.editor(|editor, _, _| {
12899 let popover = editor.signature_help_state.popover().cloned().unwrap();
12900 assert_eq!(popover.current_signature, 0);
12901 });
12902
12903 // Test previous navigation
12904 cx.update_editor(|editor, window, cx| {
12905 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12906 });
12907
12908 cx.editor(|editor, _, _| {
12909 let popover = editor.signature_help_state.popover().cloned().unwrap();
12910 assert_eq!(popover.current_signature, 2);
12911 });
12912}
12913
12914#[gpui::test]
12915async fn test_completion_mode(cx: &mut TestAppContext) {
12916 init_test(cx, |_| {});
12917 let mut cx = EditorLspTestContext::new_rust(
12918 lsp::ServerCapabilities {
12919 completion_provider: Some(lsp::CompletionOptions {
12920 resolve_provider: Some(true),
12921 ..Default::default()
12922 }),
12923 ..Default::default()
12924 },
12925 cx,
12926 )
12927 .await;
12928
12929 struct Run {
12930 run_description: &'static str,
12931 initial_state: String,
12932 buffer_marked_text: String,
12933 completion_label: &'static str,
12934 completion_text: &'static str,
12935 expected_with_insert_mode: String,
12936 expected_with_replace_mode: String,
12937 expected_with_replace_subsequence_mode: String,
12938 expected_with_replace_suffix_mode: String,
12939 }
12940
12941 let runs = [
12942 Run {
12943 run_description: "Start of word matches completion text",
12944 initial_state: "before ediˇ after".into(),
12945 buffer_marked_text: "before <edi|> after".into(),
12946 completion_label: "editor",
12947 completion_text: "editor",
12948 expected_with_insert_mode: "before editorˇ after".into(),
12949 expected_with_replace_mode: "before editorˇ after".into(),
12950 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12951 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12952 },
12953 Run {
12954 run_description: "Accept same text at the middle of the word",
12955 initial_state: "before ediˇtor after".into(),
12956 buffer_marked_text: "before <edi|tor> after".into(),
12957 completion_label: "editor",
12958 completion_text: "editor",
12959 expected_with_insert_mode: "before editorˇtor after".into(),
12960 expected_with_replace_mode: "before editorˇ after".into(),
12961 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12962 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12963 },
12964 Run {
12965 run_description: "End of word matches completion text -- cursor at end",
12966 initial_state: "before torˇ after".into(),
12967 buffer_marked_text: "before <tor|> after".into(),
12968 completion_label: "editor",
12969 completion_text: "editor",
12970 expected_with_insert_mode: "before editorˇ after".into(),
12971 expected_with_replace_mode: "before editorˇ after".into(),
12972 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12973 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12974 },
12975 Run {
12976 run_description: "End of word matches completion text -- cursor at start",
12977 initial_state: "before ˇtor after".into(),
12978 buffer_marked_text: "before <|tor> after".into(),
12979 completion_label: "editor",
12980 completion_text: "editor",
12981 expected_with_insert_mode: "before editorˇtor after".into(),
12982 expected_with_replace_mode: "before editorˇ after".into(),
12983 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12984 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12985 },
12986 Run {
12987 run_description: "Prepend text containing whitespace",
12988 initial_state: "pˇfield: bool".into(),
12989 buffer_marked_text: "<p|field>: bool".into(),
12990 completion_label: "pub ",
12991 completion_text: "pub ",
12992 expected_with_insert_mode: "pub ˇfield: bool".into(),
12993 expected_with_replace_mode: "pub ˇ: bool".into(),
12994 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12995 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12996 },
12997 Run {
12998 run_description: "Add element to start of list",
12999 initial_state: "[element_ˇelement_2]".into(),
13000 buffer_marked_text: "[<element_|element_2>]".into(),
13001 completion_label: "element_1",
13002 completion_text: "element_1",
13003 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13004 expected_with_replace_mode: "[element_1ˇ]".into(),
13005 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13006 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13007 },
13008 Run {
13009 run_description: "Add element to start of list -- first and second elements are equal",
13010 initial_state: "[elˇelement]".into(),
13011 buffer_marked_text: "[<el|element>]".into(),
13012 completion_label: "element",
13013 completion_text: "element",
13014 expected_with_insert_mode: "[elementˇelement]".into(),
13015 expected_with_replace_mode: "[elementˇ]".into(),
13016 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13017 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13018 },
13019 Run {
13020 run_description: "Ends with matching suffix",
13021 initial_state: "SubˇError".into(),
13022 buffer_marked_text: "<Sub|Error>".into(),
13023 completion_label: "SubscriptionError",
13024 completion_text: "SubscriptionError",
13025 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13026 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13027 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13028 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13029 },
13030 Run {
13031 run_description: "Suffix is a subsequence -- contiguous",
13032 initial_state: "SubˇErr".into(),
13033 buffer_marked_text: "<Sub|Err>".into(),
13034 completion_label: "SubscriptionError",
13035 completion_text: "SubscriptionError",
13036 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13037 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13038 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13039 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13040 },
13041 Run {
13042 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13043 initial_state: "Suˇscrirr".into(),
13044 buffer_marked_text: "<Su|scrirr>".into(),
13045 completion_label: "SubscriptionError",
13046 completion_text: "SubscriptionError",
13047 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13048 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13049 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13050 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13051 },
13052 Run {
13053 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13054 initial_state: "foo(indˇix)".into(),
13055 buffer_marked_text: "foo(<ind|ix>)".into(),
13056 completion_label: "node_index",
13057 completion_text: "node_index",
13058 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13059 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13060 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13061 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13062 },
13063 Run {
13064 run_description: "Replace range ends before cursor - should extend to cursor",
13065 initial_state: "before editˇo after".into(),
13066 buffer_marked_text: "before <{ed}>it|o after".into(),
13067 completion_label: "editor",
13068 completion_text: "editor",
13069 expected_with_insert_mode: "before editorˇo after".into(),
13070 expected_with_replace_mode: "before editorˇo after".into(),
13071 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13072 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13073 },
13074 Run {
13075 run_description: "Uses label for suffix matching",
13076 initial_state: "before ediˇtor after".into(),
13077 buffer_marked_text: "before <edi|tor> after".into(),
13078 completion_label: "editor",
13079 completion_text: "editor()",
13080 expected_with_insert_mode: "before editor()ˇtor after".into(),
13081 expected_with_replace_mode: "before editor()ˇ after".into(),
13082 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13083 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13084 },
13085 Run {
13086 run_description: "Case insensitive subsequence and suffix matching",
13087 initial_state: "before EDiˇtoR after".into(),
13088 buffer_marked_text: "before <EDi|toR> after".into(),
13089 completion_label: "editor",
13090 completion_text: "editor",
13091 expected_with_insert_mode: "before editorˇtoR after".into(),
13092 expected_with_replace_mode: "before editorˇ after".into(),
13093 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13094 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13095 },
13096 ];
13097
13098 for run in runs {
13099 let run_variations = [
13100 (LspInsertMode::Insert, run.expected_with_insert_mode),
13101 (LspInsertMode::Replace, run.expected_with_replace_mode),
13102 (
13103 LspInsertMode::ReplaceSubsequence,
13104 run.expected_with_replace_subsequence_mode,
13105 ),
13106 (
13107 LspInsertMode::ReplaceSuffix,
13108 run.expected_with_replace_suffix_mode,
13109 ),
13110 ];
13111
13112 for (lsp_insert_mode, expected_text) in run_variations {
13113 eprintln!(
13114 "run = {:?}, mode = {lsp_insert_mode:.?}",
13115 run.run_description,
13116 );
13117
13118 update_test_language_settings(&mut cx, |settings| {
13119 settings.defaults.completions = Some(CompletionSettings {
13120 lsp_insert_mode,
13121 words: WordsCompletionMode::Disabled,
13122 words_min_length: 0,
13123 lsp: true,
13124 lsp_fetch_timeout_ms: 0,
13125 });
13126 });
13127
13128 cx.set_state(&run.initial_state);
13129 cx.update_editor(|editor, window, cx| {
13130 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13131 });
13132
13133 let counter = Arc::new(AtomicUsize::new(0));
13134 handle_completion_request_with_insert_and_replace(
13135 &mut cx,
13136 &run.buffer_marked_text,
13137 vec![(run.completion_label, run.completion_text)],
13138 counter.clone(),
13139 )
13140 .await;
13141 cx.condition(|editor, _| editor.context_menu_visible())
13142 .await;
13143 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13144
13145 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13146 editor
13147 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13148 .unwrap()
13149 });
13150 cx.assert_editor_state(&expected_text);
13151 handle_resolve_completion_request(&mut cx, None).await;
13152 apply_additional_edits.await.unwrap();
13153 }
13154 }
13155}
13156
13157#[gpui::test]
13158async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13159 init_test(cx, |_| {});
13160 let mut cx = EditorLspTestContext::new_rust(
13161 lsp::ServerCapabilities {
13162 completion_provider: Some(lsp::CompletionOptions {
13163 resolve_provider: Some(true),
13164 ..Default::default()
13165 }),
13166 ..Default::default()
13167 },
13168 cx,
13169 )
13170 .await;
13171
13172 let initial_state = "SubˇError";
13173 let buffer_marked_text = "<Sub|Error>";
13174 let completion_text = "SubscriptionError";
13175 let expected_with_insert_mode = "SubscriptionErrorˇError";
13176 let expected_with_replace_mode = "SubscriptionErrorˇ";
13177
13178 update_test_language_settings(&mut cx, |settings| {
13179 settings.defaults.completions = Some(CompletionSettings {
13180 words: WordsCompletionMode::Disabled,
13181 words_min_length: 0,
13182 // set the opposite here to ensure that the action is overriding the default behavior
13183 lsp_insert_mode: LspInsertMode::Insert,
13184 lsp: true,
13185 lsp_fetch_timeout_ms: 0,
13186 });
13187 });
13188
13189 cx.set_state(initial_state);
13190 cx.update_editor(|editor, window, cx| {
13191 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13192 });
13193
13194 let counter = Arc::new(AtomicUsize::new(0));
13195 handle_completion_request_with_insert_and_replace(
13196 &mut cx,
13197 buffer_marked_text,
13198 vec![(completion_text, completion_text)],
13199 counter.clone(),
13200 )
13201 .await;
13202 cx.condition(|editor, _| editor.context_menu_visible())
13203 .await;
13204 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13205
13206 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13207 editor
13208 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13209 .unwrap()
13210 });
13211 cx.assert_editor_state(expected_with_replace_mode);
13212 handle_resolve_completion_request(&mut cx, None).await;
13213 apply_additional_edits.await.unwrap();
13214
13215 update_test_language_settings(&mut cx, |settings| {
13216 settings.defaults.completions = Some(CompletionSettings {
13217 words: WordsCompletionMode::Disabled,
13218 words_min_length: 0,
13219 // set the opposite here to ensure that the action is overriding the default behavior
13220 lsp_insert_mode: LspInsertMode::Replace,
13221 lsp: true,
13222 lsp_fetch_timeout_ms: 0,
13223 });
13224 });
13225
13226 cx.set_state(initial_state);
13227 cx.update_editor(|editor, window, cx| {
13228 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13229 });
13230 handle_completion_request_with_insert_and_replace(
13231 &mut cx,
13232 buffer_marked_text,
13233 vec![(completion_text, completion_text)],
13234 counter.clone(),
13235 )
13236 .await;
13237 cx.condition(|editor, _| editor.context_menu_visible())
13238 .await;
13239 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13240
13241 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13242 editor
13243 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13244 .unwrap()
13245 });
13246 cx.assert_editor_state(expected_with_insert_mode);
13247 handle_resolve_completion_request(&mut cx, None).await;
13248 apply_additional_edits.await.unwrap();
13249}
13250
13251#[gpui::test]
13252async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13253 init_test(cx, |_| {});
13254 let mut cx = EditorLspTestContext::new_rust(
13255 lsp::ServerCapabilities {
13256 completion_provider: Some(lsp::CompletionOptions {
13257 resolve_provider: Some(true),
13258 ..Default::default()
13259 }),
13260 ..Default::default()
13261 },
13262 cx,
13263 )
13264 .await;
13265
13266 // scenario: surrounding text matches completion text
13267 let completion_text = "to_offset";
13268 let initial_state = indoc! {"
13269 1. buf.to_offˇsuffix
13270 2. buf.to_offˇsuf
13271 3. buf.to_offˇfix
13272 4. buf.to_offˇ
13273 5. into_offˇensive
13274 6. ˇsuffix
13275 7. let ˇ //
13276 8. aaˇzz
13277 9. buf.to_off«zzzzzˇ»suffix
13278 10. buf.«ˇzzzzz»suffix
13279 11. to_off«ˇzzzzz»
13280
13281 buf.to_offˇsuffix // newest cursor
13282 "};
13283 let completion_marked_buffer = indoc! {"
13284 1. buf.to_offsuffix
13285 2. buf.to_offsuf
13286 3. buf.to_offfix
13287 4. buf.to_off
13288 5. into_offensive
13289 6. suffix
13290 7. let //
13291 8. aazz
13292 9. buf.to_offzzzzzsuffix
13293 10. buf.zzzzzsuffix
13294 11. to_offzzzzz
13295
13296 buf.<to_off|suffix> // newest cursor
13297 "};
13298 let expected = indoc! {"
13299 1. buf.to_offsetˇ
13300 2. buf.to_offsetˇsuf
13301 3. buf.to_offsetˇfix
13302 4. buf.to_offsetˇ
13303 5. into_offsetˇensive
13304 6. to_offsetˇsuffix
13305 7. let to_offsetˇ //
13306 8. aato_offsetˇzz
13307 9. buf.to_offsetˇ
13308 10. buf.to_offsetˇsuffix
13309 11. to_offsetˇ
13310
13311 buf.to_offsetˇ // newest cursor
13312 "};
13313 cx.set_state(initial_state);
13314 cx.update_editor(|editor, window, cx| {
13315 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13316 });
13317 handle_completion_request_with_insert_and_replace(
13318 &mut cx,
13319 completion_marked_buffer,
13320 vec![(completion_text, completion_text)],
13321 Arc::new(AtomicUsize::new(0)),
13322 )
13323 .await;
13324 cx.condition(|editor, _| editor.context_menu_visible())
13325 .await;
13326 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13327 editor
13328 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13329 .unwrap()
13330 });
13331 cx.assert_editor_state(expected);
13332 handle_resolve_completion_request(&mut cx, None).await;
13333 apply_additional_edits.await.unwrap();
13334
13335 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13336 let completion_text = "foo_and_bar";
13337 let initial_state = indoc! {"
13338 1. ooanbˇ
13339 2. zooanbˇ
13340 3. ooanbˇz
13341 4. zooanbˇz
13342 5. ooanˇ
13343 6. oanbˇ
13344
13345 ooanbˇ
13346 "};
13347 let completion_marked_buffer = indoc! {"
13348 1. ooanb
13349 2. zooanb
13350 3. ooanbz
13351 4. zooanbz
13352 5. ooan
13353 6. oanb
13354
13355 <ooanb|>
13356 "};
13357 let expected = indoc! {"
13358 1. foo_and_barˇ
13359 2. zfoo_and_barˇ
13360 3. foo_and_barˇz
13361 4. zfoo_and_barˇz
13362 5. ooanfoo_and_barˇ
13363 6. oanbfoo_and_barˇ
13364
13365 foo_and_barˇ
13366 "};
13367 cx.set_state(initial_state);
13368 cx.update_editor(|editor, window, cx| {
13369 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13370 });
13371 handle_completion_request_with_insert_and_replace(
13372 &mut cx,
13373 completion_marked_buffer,
13374 vec![(completion_text, completion_text)],
13375 Arc::new(AtomicUsize::new(0)),
13376 )
13377 .await;
13378 cx.condition(|editor, _| editor.context_menu_visible())
13379 .await;
13380 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13381 editor
13382 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13383 .unwrap()
13384 });
13385 cx.assert_editor_state(expected);
13386 handle_resolve_completion_request(&mut cx, None).await;
13387 apply_additional_edits.await.unwrap();
13388
13389 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13390 // (expects the same as if it was inserted at the end)
13391 let completion_text = "foo_and_bar";
13392 let initial_state = indoc! {"
13393 1. ooˇanb
13394 2. zooˇanb
13395 3. ooˇanbz
13396 4. zooˇanbz
13397
13398 ooˇanb
13399 "};
13400 let completion_marked_buffer = indoc! {"
13401 1. ooanb
13402 2. zooanb
13403 3. ooanbz
13404 4. zooanbz
13405
13406 <oo|anb>
13407 "};
13408 let expected = indoc! {"
13409 1. foo_and_barˇ
13410 2. zfoo_and_barˇ
13411 3. foo_and_barˇz
13412 4. zfoo_and_barˇz
13413
13414 foo_and_barˇ
13415 "};
13416 cx.set_state(initial_state);
13417 cx.update_editor(|editor, window, cx| {
13418 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13419 });
13420 handle_completion_request_with_insert_and_replace(
13421 &mut cx,
13422 completion_marked_buffer,
13423 vec![(completion_text, completion_text)],
13424 Arc::new(AtomicUsize::new(0)),
13425 )
13426 .await;
13427 cx.condition(|editor, _| editor.context_menu_visible())
13428 .await;
13429 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13430 editor
13431 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13432 .unwrap()
13433 });
13434 cx.assert_editor_state(expected);
13435 handle_resolve_completion_request(&mut cx, None).await;
13436 apply_additional_edits.await.unwrap();
13437}
13438
13439// This used to crash
13440#[gpui::test]
13441async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13442 init_test(cx, |_| {});
13443
13444 let buffer_text = indoc! {"
13445 fn main() {
13446 10.satu;
13447
13448 //
13449 // separate cursors so they open in different excerpts (manually reproducible)
13450 //
13451
13452 10.satu20;
13453 }
13454 "};
13455 let multibuffer_text_with_selections = indoc! {"
13456 fn main() {
13457 10.satuˇ;
13458
13459 //
13460
13461 //
13462
13463 10.satuˇ20;
13464 }
13465 "};
13466 let expected_multibuffer = indoc! {"
13467 fn main() {
13468 10.saturating_sub()ˇ;
13469
13470 //
13471
13472 //
13473
13474 10.saturating_sub()ˇ;
13475 }
13476 "};
13477
13478 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13479 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13480
13481 let fs = FakeFs::new(cx.executor());
13482 fs.insert_tree(
13483 path!("/a"),
13484 json!({
13485 "main.rs": buffer_text,
13486 }),
13487 )
13488 .await;
13489
13490 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13491 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13492 language_registry.add(rust_lang());
13493 let mut fake_servers = language_registry.register_fake_lsp(
13494 "Rust",
13495 FakeLspAdapter {
13496 capabilities: lsp::ServerCapabilities {
13497 completion_provider: Some(lsp::CompletionOptions {
13498 resolve_provider: None,
13499 ..lsp::CompletionOptions::default()
13500 }),
13501 ..lsp::ServerCapabilities::default()
13502 },
13503 ..FakeLspAdapter::default()
13504 },
13505 );
13506 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13507 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13508 let buffer = project
13509 .update(cx, |project, cx| {
13510 project.open_local_buffer(path!("/a/main.rs"), cx)
13511 })
13512 .await
13513 .unwrap();
13514
13515 let multi_buffer = cx.new(|cx| {
13516 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13517 multi_buffer.push_excerpts(
13518 buffer.clone(),
13519 [ExcerptRange::new(0..first_excerpt_end)],
13520 cx,
13521 );
13522 multi_buffer.push_excerpts(
13523 buffer.clone(),
13524 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13525 cx,
13526 );
13527 multi_buffer
13528 });
13529
13530 let editor = workspace
13531 .update(cx, |_, window, cx| {
13532 cx.new(|cx| {
13533 Editor::new(
13534 EditorMode::Full {
13535 scale_ui_elements_with_buffer_font_size: false,
13536 show_active_line_background: false,
13537 sized_by_content: false,
13538 },
13539 multi_buffer.clone(),
13540 Some(project.clone()),
13541 window,
13542 cx,
13543 )
13544 })
13545 })
13546 .unwrap();
13547
13548 let pane = workspace
13549 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13550 .unwrap();
13551 pane.update_in(cx, |pane, window, cx| {
13552 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13553 });
13554
13555 let fake_server = fake_servers.next().await.unwrap();
13556
13557 editor.update_in(cx, |editor, window, cx| {
13558 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13559 s.select_ranges([
13560 Point::new(1, 11)..Point::new(1, 11),
13561 Point::new(7, 11)..Point::new(7, 11),
13562 ])
13563 });
13564
13565 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13566 });
13567
13568 editor.update_in(cx, |editor, window, cx| {
13569 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13570 });
13571
13572 fake_server
13573 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13574 let completion_item = lsp::CompletionItem {
13575 label: "saturating_sub()".into(),
13576 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13577 lsp::InsertReplaceEdit {
13578 new_text: "saturating_sub()".to_owned(),
13579 insert: lsp::Range::new(
13580 lsp::Position::new(7, 7),
13581 lsp::Position::new(7, 11),
13582 ),
13583 replace: lsp::Range::new(
13584 lsp::Position::new(7, 7),
13585 lsp::Position::new(7, 13),
13586 ),
13587 },
13588 )),
13589 ..lsp::CompletionItem::default()
13590 };
13591
13592 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13593 })
13594 .next()
13595 .await
13596 .unwrap();
13597
13598 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13599 .await;
13600
13601 editor
13602 .update_in(cx, |editor, window, cx| {
13603 editor
13604 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13605 .unwrap()
13606 })
13607 .await
13608 .unwrap();
13609
13610 editor.update(cx, |editor, cx| {
13611 assert_text_with_selections(editor, expected_multibuffer, cx);
13612 })
13613}
13614
13615#[gpui::test]
13616async fn test_completion(cx: &mut TestAppContext) {
13617 init_test(cx, |_| {});
13618
13619 let mut cx = EditorLspTestContext::new_rust(
13620 lsp::ServerCapabilities {
13621 completion_provider: Some(lsp::CompletionOptions {
13622 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13623 resolve_provider: Some(true),
13624 ..Default::default()
13625 }),
13626 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13627 ..Default::default()
13628 },
13629 cx,
13630 )
13631 .await;
13632 let counter = Arc::new(AtomicUsize::new(0));
13633
13634 cx.set_state(indoc! {"
13635 oneˇ
13636 two
13637 three
13638 "});
13639 cx.simulate_keystroke(".");
13640 handle_completion_request(
13641 indoc! {"
13642 one.|<>
13643 two
13644 three
13645 "},
13646 vec!["first_completion", "second_completion"],
13647 true,
13648 counter.clone(),
13649 &mut cx,
13650 )
13651 .await;
13652 cx.condition(|editor, _| editor.context_menu_visible())
13653 .await;
13654 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13655
13656 let _handler = handle_signature_help_request(
13657 &mut cx,
13658 lsp::SignatureHelp {
13659 signatures: vec![lsp::SignatureInformation {
13660 label: "test signature".to_string(),
13661 documentation: None,
13662 parameters: Some(vec![lsp::ParameterInformation {
13663 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13664 documentation: None,
13665 }]),
13666 active_parameter: None,
13667 }],
13668 active_signature: None,
13669 active_parameter: None,
13670 },
13671 );
13672 cx.update_editor(|editor, window, cx| {
13673 assert!(
13674 !editor.signature_help_state.is_shown(),
13675 "No signature help was called for"
13676 );
13677 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13678 });
13679 cx.run_until_parked();
13680 cx.update_editor(|editor, _, _| {
13681 assert!(
13682 !editor.signature_help_state.is_shown(),
13683 "No signature help should be shown when completions menu is open"
13684 );
13685 });
13686
13687 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13688 editor.context_menu_next(&Default::default(), window, cx);
13689 editor
13690 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13691 .unwrap()
13692 });
13693 cx.assert_editor_state(indoc! {"
13694 one.second_completionˇ
13695 two
13696 three
13697 "});
13698
13699 handle_resolve_completion_request(
13700 &mut cx,
13701 Some(vec![
13702 (
13703 //This overlaps with the primary completion edit which is
13704 //misbehavior from the LSP spec, test that we filter it out
13705 indoc! {"
13706 one.second_ˇcompletion
13707 two
13708 threeˇ
13709 "},
13710 "overlapping additional edit",
13711 ),
13712 (
13713 indoc! {"
13714 one.second_completion
13715 two
13716 threeˇ
13717 "},
13718 "\nadditional edit",
13719 ),
13720 ]),
13721 )
13722 .await;
13723 apply_additional_edits.await.unwrap();
13724 cx.assert_editor_state(indoc! {"
13725 one.second_completionˇ
13726 two
13727 three
13728 additional edit
13729 "});
13730
13731 cx.set_state(indoc! {"
13732 one.second_completion
13733 twoˇ
13734 threeˇ
13735 additional edit
13736 "});
13737 cx.simulate_keystroke(" ");
13738 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13739 cx.simulate_keystroke("s");
13740 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13741
13742 cx.assert_editor_state(indoc! {"
13743 one.second_completion
13744 two sˇ
13745 three sˇ
13746 additional edit
13747 "});
13748 handle_completion_request(
13749 indoc! {"
13750 one.second_completion
13751 two s
13752 three <s|>
13753 additional edit
13754 "},
13755 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13756 true,
13757 counter.clone(),
13758 &mut cx,
13759 )
13760 .await;
13761 cx.condition(|editor, _| editor.context_menu_visible())
13762 .await;
13763 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13764
13765 cx.simulate_keystroke("i");
13766
13767 handle_completion_request(
13768 indoc! {"
13769 one.second_completion
13770 two si
13771 three <si|>
13772 additional edit
13773 "},
13774 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13775 true,
13776 counter.clone(),
13777 &mut cx,
13778 )
13779 .await;
13780 cx.condition(|editor, _| editor.context_menu_visible())
13781 .await;
13782 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13783
13784 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13785 editor
13786 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13787 .unwrap()
13788 });
13789 cx.assert_editor_state(indoc! {"
13790 one.second_completion
13791 two sixth_completionˇ
13792 three sixth_completionˇ
13793 additional edit
13794 "});
13795
13796 apply_additional_edits.await.unwrap();
13797
13798 update_test_language_settings(&mut cx, |settings| {
13799 settings.defaults.show_completions_on_input = Some(false);
13800 });
13801 cx.set_state("editorˇ");
13802 cx.simulate_keystroke(".");
13803 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13804 cx.simulate_keystrokes("c l o");
13805 cx.assert_editor_state("editor.cloˇ");
13806 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13807 cx.update_editor(|editor, window, cx| {
13808 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13809 });
13810 handle_completion_request(
13811 "editor.<clo|>",
13812 vec!["close", "clobber"],
13813 true,
13814 counter.clone(),
13815 &mut cx,
13816 )
13817 .await;
13818 cx.condition(|editor, _| editor.context_menu_visible())
13819 .await;
13820 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
13821
13822 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13823 editor
13824 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13825 .unwrap()
13826 });
13827 cx.assert_editor_state("editor.clobberˇ");
13828 handle_resolve_completion_request(&mut cx, None).await;
13829 apply_additional_edits.await.unwrap();
13830}
13831
13832#[gpui::test]
13833async fn test_completion_reuse(cx: &mut TestAppContext) {
13834 init_test(cx, |_| {});
13835
13836 let mut cx = EditorLspTestContext::new_rust(
13837 lsp::ServerCapabilities {
13838 completion_provider: Some(lsp::CompletionOptions {
13839 trigger_characters: Some(vec![".".to_string()]),
13840 ..Default::default()
13841 }),
13842 ..Default::default()
13843 },
13844 cx,
13845 )
13846 .await;
13847
13848 let counter = Arc::new(AtomicUsize::new(0));
13849 cx.set_state("objˇ");
13850 cx.simulate_keystroke(".");
13851
13852 // Initial completion request returns complete results
13853 let is_incomplete = false;
13854 handle_completion_request(
13855 "obj.|<>",
13856 vec!["a", "ab", "abc"],
13857 is_incomplete,
13858 counter.clone(),
13859 &mut cx,
13860 )
13861 .await;
13862 cx.run_until_parked();
13863 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13864 cx.assert_editor_state("obj.ˇ");
13865 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13866
13867 // Type "a" - filters existing completions
13868 cx.simulate_keystroke("a");
13869 cx.run_until_parked();
13870 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13871 cx.assert_editor_state("obj.aˇ");
13872 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13873
13874 // Type "b" - filters existing completions
13875 cx.simulate_keystroke("b");
13876 cx.run_until_parked();
13877 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13878 cx.assert_editor_state("obj.abˇ");
13879 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13880
13881 // Type "c" - filters existing completions
13882 cx.simulate_keystroke("c");
13883 cx.run_until_parked();
13884 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13885 cx.assert_editor_state("obj.abcˇ");
13886 check_displayed_completions(vec!["abc"], &mut cx);
13887
13888 // Backspace to delete "c" - filters existing completions
13889 cx.update_editor(|editor, window, cx| {
13890 editor.backspace(&Backspace, window, cx);
13891 });
13892 cx.run_until_parked();
13893 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13894 cx.assert_editor_state("obj.abˇ");
13895 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13896
13897 // Moving cursor to the left dismisses menu.
13898 cx.update_editor(|editor, window, cx| {
13899 editor.move_left(&MoveLeft, window, cx);
13900 });
13901 cx.run_until_parked();
13902 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13903 cx.assert_editor_state("obj.aˇb");
13904 cx.update_editor(|editor, _, _| {
13905 assert_eq!(editor.context_menu_visible(), false);
13906 });
13907
13908 // Type "b" - new request
13909 cx.simulate_keystroke("b");
13910 let is_incomplete = false;
13911 handle_completion_request(
13912 "obj.<ab|>a",
13913 vec!["ab", "abc"],
13914 is_incomplete,
13915 counter.clone(),
13916 &mut cx,
13917 )
13918 .await;
13919 cx.run_until_parked();
13920 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13921 cx.assert_editor_state("obj.abˇb");
13922 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13923
13924 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13925 cx.update_editor(|editor, window, cx| {
13926 editor.backspace(&Backspace, window, cx);
13927 });
13928 let is_incomplete = false;
13929 handle_completion_request(
13930 "obj.<a|>b",
13931 vec!["a", "ab", "abc"],
13932 is_incomplete,
13933 counter.clone(),
13934 &mut cx,
13935 )
13936 .await;
13937 cx.run_until_parked();
13938 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13939 cx.assert_editor_state("obj.aˇb");
13940 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13941
13942 // Backspace to delete "a" - dismisses menu.
13943 cx.update_editor(|editor, window, cx| {
13944 editor.backspace(&Backspace, window, cx);
13945 });
13946 cx.run_until_parked();
13947 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13948 cx.assert_editor_state("obj.ˇb");
13949 cx.update_editor(|editor, _, _| {
13950 assert_eq!(editor.context_menu_visible(), false);
13951 });
13952}
13953
13954#[gpui::test]
13955async fn test_word_completion(cx: &mut TestAppContext) {
13956 let lsp_fetch_timeout_ms = 10;
13957 init_test(cx, |language_settings| {
13958 language_settings.defaults.completions = Some(CompletionSettings {
13959 words: WordsCompletionMode::Fallback,
13960 words_min_length: 0,
13961 lsp: true,
13962 lsp_fetch_timeout_ms: 10,
13963 lsp_insert_mode: LspInsertMode::Insert,
13964 });
13965 });
13966
13967 let mut cx = EditorLspTestContext::new_rust(
13968 lsp::ServerCapabilities {
13969 completion_provider: Some(lsp::CompletionOptions {
13970 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13971 ..lsp::CompletionOptions::default()
13972 }),
13973 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13974 ..lsp::ServerCapabilities::default()
13975 },
13976 cx,
13977 )
13978 .await;
13979
13980 let throttle_completions = Arc::new(AtomicBool::new(false));
13981
13982 let lsp_throttle_completions = throttle_completions.clone();
13983 let _completion_requests_handler =
13984 cx.lsp
13985 .server
13986 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13987 let lsp_throttle_completions = lsp_throttle_completions.clone();
13988 let cx = cx.clone();
13989 async move {
13990 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13991 cx.background_executor()
13992 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13993 .await;
13994 }
13995 Ok(Some(lsp::CompletionResponse::Array(vec![
13996 lsp::CompletionItem {
13997 label: "first".into(),
13998 ..lsp::CompletionItem::default()
13999 },
14000 lsp::CompletionItem {
14001 label: "last".into(),
14002 ..lsp::CompletionItem::default()
14003 },
14004 ])))
14005 }
14006 });
14007
14008 cx.set_state(indoc! {"
14009 oneˇ
14010 two
14011 three
14012 "});
14013 cx.simulate_keystroke(".");
14014 cx.executor().run_until_parked();
14015 cx.condition(|editor, _| editor.context_menu_visible())
14016 .await;
14017 cx.update_editor(|editor, window, cx| {
14018 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14019 {
14020 assert_eq!(
14021 completion_menu_entries(menu),
14022 &["first", "last"],
14023 "When LSP server is fast to reply, no fallback word completions are used"
14024 );
14025 } else {
14026 panic!("expected completion menu to be open");
14027 }
14028 editor.cancel(&Cancel, window, cx);
14029 });
14030 cx.executor().run_until_parked();
14031 cx.condition(|editor, _| !editor.context_menu_visible())
14032 .await;
14033
14034 throttle_completions.store(true, atomic::Ordering::Release);
14035 cx.simulate_keystroke(".");
14036 cx.executor()
14037 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14038 cx.executor().run_until_parked();
14039 cx.condition(|editor, _| editor.context_menu_visible())
14040 .await;
14041 cx.update_editor(|editor, _, _| {
14042 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14043 {
14044 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14045 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14046 } else {
14047 panic!("expected completion menu to be open");
14048 }
14049 });
14050}
14051
14052#[gpui::test]
14053async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14054 init_test(cx, |language_settings| {
14055 language_settings.defaults.completions = Some(CompletionSettings {
14056 words: WordsCompletionMode::Enabled,
14057 words_min_length: 0,
14058 lsp: true,
14059 lsp_fetch_timeout_ms: 0,
14060 lsp_insert_mode: LspInsertMode::Insert,
14061 });
14062 });
14063
14064 let mut cx = EditorLspTestContext::new_rust(
14065 lsp::ServerCapabilities {
14066 completion_provider: Some(lsp::CompletionOptions {
14067 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14068 ..lsp::CompletionOptions::default()
14069 }),
14070 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14071 ..lsp::ServerCapabilities::default()
14072 },
14073 cx,
14074 )
14075 .await;
14076
14077 let _completion_requests_handler =
14078 cx.lsp
14079 .server
14080 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14081 Ok(Some(lsp::CompletionResponse::Array(vec![
14082 lsp::CompletionItem {
14083 label: "first".into(),
14084 ..lsp::CompletionItem::default()
14085 },
14086 lsp::CompletionItem {
14087 label: "last".into(),
14088 ..lsp::CompletionItem::default()
14089 },
14090 ])))
14091 });
14092
14093 cx.set_state(indoc! {"ˇ
14094 first
14095 last
14096 second
14097 "});
14098 cx.simulate_keystroke(".");
14099 cx.executor().run_until_parked();
14100 cx.condition(|editor, _| editor.context_menu_visible())
14101 .await;
14102 cx.update_editor(|editor, _, _| {
14103 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14104 {
14105 assert_eq!(
14106 completion_menu_entries(menu),
14107 &["first", "last", "second"],
14108 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14109 );
14110 } else {
14111 panic!("expected completion menu to be open");
14112 }
14113 });
14114}
14115
14116#[gpui::test]
14117async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14118 init_test(cx, |language_settings| {
14119 language_settings.defaults.completions = Some(CompletionSettings {
14120 words: WordsCompletionMode::Disabled,
14121 words_min_length: 0,
14122 lsp: true,
14123 lsp_fetch_timeout_ms: 0,
14124 lsp_insert_mode: LspInsertMode::Insert,
14125 });
14126 });
14127
14128 let mut cx = EditorLspTestContext::new_rust(
14129 lsp::ServerCapabilities {
14130 completion_provider: Some(lsp::CompletionOptions {
14131 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14132 ..lsp::CompletionOptions::default()
14133 }),
14134 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14135 ..lsp::ServerCapabilities::default()
14136 },
14137 cx,
14138 )
14139 .await;
14140
14141 let _completion_requests_handler =
14142 cx.lsp
14143 .server
14144 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14145 panic!("LSP completions should not be queried when dealing with word completions")
14146 });
14147
14148 cx.set_state(indoc! {"ˇ
14149 first
14150 last
14151 second
14152 "});
14153 cx.update_editor(|editor, window, cx| {
14154 editor.show_word_completions(&ShowWordCompletions, window, cx);
14155 });
14156 cx.executor().run_until_parked();
14157 cx.condition(|editor, _| editor.context_menu_visible())
14158 .await;
14159 cx.update_editor(|editor, _, _| {
14160 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14161 {
14162 assert_eq!(
14163 completion_menu_entries(menu),
14164 &["first", "last", "second"],
14165 "`ShowWordCompletions` action should show word completions"
14166 );
14167 } else {
14168 panic!("expected completion menu to be open");
14169 }
14170 });
14171
14172 cx.simulate_keystroke("l");
14173 cx.executor().run_until_parked();
14174 cx.condition(|editor, _| editor.context_menu_visible())
14175 .await;
14176 cx.update_editor(|editor, _, _| {
14177 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14178 {
14179 assert_eq!(
14180 completion_menu_entries(menu),
14181 &["last"],
14182 "After showing word completions, further editing should filter them and not query the LSP"
14183 );
14184 } else {
14185 panic!("expected completion menu to be open");
14186 }
14187 });
14188}
14189
14190#[gpui::test]
14191async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14192 init_test(cx, |language_settings| {
14193 language_settings.defaults.completions = Some(CompletionSettings {
14194 words: WordsCompletionMode::Fallback,
14195 words_min_length: 0,
14196 lsp: false,
14197 lsp_fetch_timeout_ms: 0,
14198 lsp_insert_mode: LspInsertMode::Insert,
14199 });
14200 });
14201
14202 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14203
14204 cx.set_state(indoc! {"ˇ
14205 0_usize
14206 let
14207 33
14208 4.5f32
14209 "});
14210 cx.update_editor(|editor, window, cx| {
14211 editor.show_completions(&ShowCompletions::default(), window, cx);
14212 });
14213 cx.executor().run_until_parked();
14214 cx.condition(|editor, _| editor.context_menu_visible())
14215 .await;
14216 cx.update_editor(|editor, window, cx| {
14217 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14218 {
14219 assert_eq!(
14220 completion_menu_entries(menu),
14221 &["let"],
14222 "With no digits in the completion query, no digits should be in the word completions"
14223 );
14224 } else {
14225 panic!("expected completion menu to be open");
14226 }
14227 editor.cancel(&Cancel, window, cx);
14228 });
14229
14230 cx.set_state(indoc! {"3ˇ
14231 0_usize
14232 let
14233 3
14234 33.35f32
14235 "});
14236 cx.update_editor(|editor, window, cx| {
14237 editor.show_completions(&ShowCompletions::default(), window, cx);
14238 });
14239 cx.executor().run_until_parked();
14240 cx.condition(|editor, _| editor.context_menu_visible())
14241 .await;
14242 cx.update_editor(|editor, _, _| {
14243 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14244 {
14245 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14246 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14247 } else {
14248 panic!("expected completion menu to be open");
14249 }
14250 });
14251}
14252
14253#[gpui::test]
14254async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14255 init_test(cx, |language_settings| {
14256 language_settings.defaults.completions = Some(CompletionSettings {
14257 words: WordsCompletionMode::Enabled,
14258 words_min_length: 3,
14259 lsp: true,
14260 lsp_fetch_timeout_ms: 0,
14261 lsp_insert_mode: LspInsertMode::Insert,
14262 });
14263 });
14264
14265 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14266 cx.set_state(indoc! {"ˇ
14267 wow
14268 wowen
14269 wowser
14270 "});
14271 cx.simulate_keystroke("w");
14272 cx.executor().run_until_parked();
14273 cx.update_editor(|editor, _, _| {
14274 if editor.context_menu.borrow_mut().is_some() {
14275 panic!(
14276 "expected completion menu to be hidden, as words completion threshold is not met"
14277 );
14278 }
14279 });
14280
14281 cx.update_editor(|editor, window, cx| {
14282 editor.show_word_completions(&ShowWordCompletions, window, cx);
14283 });
14284 cx.executor().run_until_parked();
14285 cx.update_editor(|editor, window, cx| {
14286 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14287 {
14288 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14289 } else {
14290 panic!("expected completion menu to be open after the word completions are called with an action");
14291 }
14292
14293 editor.cancel(&Cancel, window, cx);
14294 });
14295 cx.update_editor(|editor, _, _| {
14296 if editor.context_menu.borrow_mut().is_some() {
14297 panic!("expected completion menu to be hidden after canceling");
14298 }
14299 });
14300
14301 cx.simulate_keystroke("o");
14302 cx.executor().run_until_parked();
14303 cx.update_editor(|editor, _, _| {
14304 if editor.context_menu.borrow_mut().is_some() {
14305 panic!(
14306 "expected completion menu to be hidden, as words completion threshold is not met still"
14307 );
14308 }
14309 });
14310
14311 cx.simulate_keystroke("w");
14312 cx.executor().run_until_parked();
14313 cx.update_editor(|editor, _, _| {
14314 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14315 {
14316 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14317 } else {
14318 panic!("expected completion menu to be open after the word completions threshold is met");
14319 }
14320 });
14321}
14322
14323#[gpui::test]
14324async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14325 init_test(cx, |language_settings| {
14326 language_settings.defaults.completions = Some(CompletionSettings {
14327 words: WordsCompletionMode::Enabled,
14328 words_min_length: 0,
14329 lsp: true,
14330 lsp_fetch_timeout_ms: 0,
14331 lsp_insert_mode: LspInsertMode::Insert,
14332 });
14333 });
14334
14335 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14336 cx.update_editor(|editor, _, _| {
14337 editor.disable_word_completions();
14338 });
14339 cx.set_state(indoc! {"ˇ
14340 wow
14341 wowen
14342 wowser
14343 "});
14344 cx.simulate_keystroke("w");
14345 cx.executor().run_until_parked();
14346 cx.update_editor(|editor, _, _| {
14347 if editor.context_menu.borrow_mut().is_some() {
14348 panic!(
14349 "expected completion menu to be hidden, as words completion are disabled for this editor"
14350 );
14351 }
14352 });
14353
14354 cx.update_editor(|editor, window, cx| {
14355 editor.show_word_completions(&ShowWordCompletions, window, cx);
14356 });
14357 cx.executor().run_until_parked();
14358 cx.update_editor(|editor, _, _| {
14359 if editor.context_menu.borrow_mut().is_some() {
14360 panic!(
14361 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14362 );
14363 }
14364 });
14365}
14366
14367fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14368 let position = || lsp::Position {
14369 line: params.text_document_position.position.line,
14370 character: params.text_document_position.position.character,
14371 };
14372 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14373 range: lsp::Range {
14374 start: position(),
14375 end: position(),
14376 },
14377 new_text: text.to_string(),
14378 }))
14379}
14380
14381#[gpui::test]
14382async fn test_multiline_completion(cx: &mut TestAppContext) {
14383 init_test(cx, |_| {});
14384
14385 let fs = FakeFs::new(cx.executor());
14386 fs.insert_tree(
14387 path!("/a"),
14388 json!({
14389 "main.ts": "a",
14390 }),
14391 )
14392 .await;
14393
14394 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14395 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14396 let typescript_language = Arc::new(Language::new(
14397 LanguageConfig {
14398 name: "TypeScript".into(),
14399 matcher: LanguageMatcher {
14400 path_suffixes: vec!["ts".to_string()],
14401 ..LanguageMatcher::default()
14402 },
14403 line_comments: vec!["// ".into()],
14404 ..LanguageConfig::default()
14405 },
14406 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14407 ));
14408 language_registry.add(typescript_language.clone());
14409 let mut fake_servers = language_registry.register_fake_lsp(
14410 "TypeScript",
14411 FakeLspAdapter {
14412 capabilities: lsp::ServerCapabilities {
14413 completion_provider: Some(lsp::CompletionOptions {
14414 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14415 ..lsp::CompletionOptions::default()
14416 }),
14417 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14418 ..lsp::ServerCapabilities::default()
14419 },
14420 // Emulate vtsls label generation
14421 label_for_completion: Some(Box::new(|item, _| {
14422 let text = if let Some(description) = item
14423 .label_details
14424 .as_ref()
14425 .and_then(|label_details| label_details.description.as_ref())
14426 {
14427 format!("{} {}", item.label, description)
14428 } else if let Some(detail) = &item.detail {
14429 format!("{} {}", item.label, detail)
14430 } else {
14431 item.label.clone()
14432 };
14433 let len = text.len();
14434 Some(language::CodeLabel {
14435 text,
14436 runs: Vec::new(),
14437 filter_range: 0..len,
14438 })
14439 })),
14440 ..FakeLspAdapter::default()
14441 },
14442 );
14443 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14444 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14445 let worktree_id = workspace
14446 .update(cx, |workspace, _window, cx| {
14447 workspace.project().update(cx, |project, cx| {
14448 project.worktrees(cx).next().unwrap().read(cx).id()
14449 })
14450 })
14451 .unwrap();
14452 let _buffer = project
14453 .update(cx, |project, cx| {
14454 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14455 })
14456 .await
14457 .unwrap();
14458 let editor = workspace
14459 .update(cx, |workspace, window, cx| {
14460 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14461 })
14462 .unwrap()
14463 .await
14464 .unwrap()
14465 .downcast::<Editor>()
14466 .unwrap();
14467 let fake_server = fake_servers.next().await.unwrap();
14468
14469 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14470 let multiline_label_2 = "a\nb\nc\n";
14471 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14472 let multiline_description = "d\ne\nf\n";
14473 let multiline_detail_2 = "g\nh\ni\n";
14474
14475 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14476 move |params, _| async move {
14477 Ok(Some(lsp::CompletionResponse::Array(vec![
14478 lsp::CompletionItem {
14479 label: multiline_label.to_string(),
14480 text_edit: gen_text_edit(¶ms, "new_text_1"),
14481 ..lsp::CompletionItem::default()
14482 },
14483 lsp::CompletionItem {
14484 label: "single line label 1".to_string(),
14485 detail: Some(multiline_detail.to_string()),
14486 text_edit: gen_text_edit(¶ms, "new_text_2"),
14487 ..lsp::CompletionItem::default()
14488 },
14489 lsp::CompletionItem {
14490 label: "single line label 2".to_string(),
14491 label_details: Some(lsp::CompletionItemLabelDetails {
14492 description: Some(multiline_description.to_string()),
14493 detail: None,
14494 }),
14495 text_edit: gen_text_edit(¶ms, "new_text_2"),
14496 ..lsp::CompletionItem::default()
14497 },
14498 lsp::CompletionItem {
14499 label: multiline_label_2.to_string(),
14500 detail: Some(multiline_detail_2.to_string()),
14501 text_edit: gen_text_edit(¶ms, "new_text_3"),
14502 ..lsp::CompletionItem::default()
14503 },
14504 lsp::CompletionItem {
14505 label: "Label with many spaces and \t but without newlines".to_string(),
14506 detail: Some(
14507 "Details with many spaces and \t but without newlines".to_string(),
14508 ),
14509 text_edit: gen_text_edit(¶ms, "new_text_4"),
14510 ..lsp::CompletionItem::default()
14511 },
14512 ])))
14513 },
14514 );
14515
14516 editor.update_in(cx, |editor, window, cx| {
14517 cx.focus_self(window);
14518 editor.move_to_end(&MoveToEnd, window, cx);
14519 editor.handle_input(".", window, cx);
14520 });
14521 cx.run_until_parked();
14522 completion_handle.next().await.unwrap();
14523
14524 editor.update(cx, |editor, _| {
14525 assert!(editor.context_menu_visible());
14526 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14527 {
14528 let completion_labels = menu
14529 .completions
14530 .borrow()
14531 .iter()
14532 .map(|c| c.label.text.clone())
14533 .collect::<Vec<_>>();
14534 assert_eq!(
14535 completion_labels,
14536 &[
14537 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14538 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14539 "single line label 2 d e f ",
14540 "a b c g h i ",
14541 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14542 ],
14543 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14544 );
14545
14546 for completion in menu
14547 .completions
14548 .borrow()
14549 .iter() {
14550 assert_eq!(
14551 completion.label.filter_range,
14552 0..completion.label.text.len(),
14553 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14554 );
14555 }
14556 } else {
14557 panic!("expected completion menu to be open");
14558 }
14559 });
14560}
14561
14562#[gpui::test]
14563async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14564 init_test(cx, |_| {});
14565 let mut cx = EditorLspTestContext::new_rust(
14566 lsp::ServerCapabilities {
14567 completion_provider: Some(lsp::CompletionOptions {
14568 trigger_characters: Some(vec![".".to_string()]),
14569 ..Default::default()
14570 }),
14571 ..Default::default()
14572 },
14573 cx,
14574 )
14575 .await;
14576 cx.lsp
14577 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14578 Ok(Some(lsp::CompletionResponse::Array(vec![
14579 lsp::CompletionItem {
14580 label: "first".into(),
14581 ..Default::default()
14582 },
14583 lsp::CompletionItem {
14584 label: "last".into(),
14585 ..Default::default()
14586 },
14587 ])))
14588 });
14589 cx.set_state("variableˇ");
14590 cx.simulate_keystroke(".");
14591 cx.executor().run_until_parked();
14592
14593 cx.update_editor(|editor, _, _| {
14594 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14595 {
14596 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14597 } else {
14598 panic!("expected completion menu to be open");
14599 }
14600 });
14601
14602 cx.update_editor(|editor, window, cx| {
14603 editor.move_page_down(&MovePageDown::default(), window, cx);
14604 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14605 {
14606 assert!(
14607 menu.selected_item == 1,
14608 "expected PageDown to select the last item from the context menu"
14609 );
14610 } else {
14611 panic!("expected completion menu to stay open after PageDown");
14612 }
14613 });
14614
14615 cx.update_editor(|editor, window, cx| {
14616 editor.move_page_up(&MovePageUp::default(), window, cx);
14617 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14618 {
14619 assert!(
14620 menu.selected_item == 0,
14621 "expected PageUp to select the first item from the context menu"
14622 );
14623 } else {
14624 panic!("expected completion menu to stay open after PageUp");
14625 }
14626 });
14627}
14628
14629#[gpui::test]
14630async fn test_as_is_completions(cx: &mut TestAppContext) {
14631 init_test(cx, |_| {});
14632 let mut cx = EditorLspTestContext::new_rust(
14633 lsp::ServerCapabilities {
14634 completion_provider: Some(lsp::CompletionOptions {
14635 ..Default::default()
14636 }),
14637 ..Default::default()
14638 },
14639 cx,
14640 )
14641 .await;
14642 cx.lsp
14643 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14644 Ok(Some(lsp::CompletionResponse::Array(vec![
14645 lsp::CompletionItem {
14646 label: "unsafe".into(),
14647 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14648 range: lsp::Range {
14649 start: lsp::Position {
14650 line: 1,
14651 character: 2,
14652 },
14653 end: lsp::Position {
14654 line: 1,
14655 character: 3,
14656 },
14657 },
14658 new_text: "unsafe".to_string(),
14659 })),
14660 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14661 ..Default::default()
14662 },
14663 ])))
14664 });
14665 cx.set_state("fn a() {}\n nˇ");
14666 cx.executor().run_until_parked();
14667 cx.update_editor(|editor, window, cx| {
14668 editor.show_completions(
14669 &ShowCompletions {
14670 trigger: Some("\n".into()),
14671 },
14672 window,
14673 cx,
14674 );
14675 });
14676 cx.executor().run_until_parked();
14677
14678 cx.update_editor(|editor, window, cx| {
14679 editor.confirm_completion(&Default::default(), window, cx)
14680 });
14681 cx.executor().run_until_parked();
14682 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14683}
14684
14685#[gpui::test]
14686async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14687 init_test(cx, |_| {});
14688 let language =
14689 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14690 let mut cx = EditorLspTestContext::new(
14691 language,
14692 lsp::ServerCapabilities {
14693 completion_provider: Some(lsp::CompletionOptions {
14694 ..lsp::CompletionOptions::default()
14695 }),
14696 ..lsp::ServerCapabilities::default()
14697 },
14698 cx,
14699 )
14700 .await;
14701
14702 cx.set_state(
14703 "#ifndef BAR_H
14704#define BAR_H
14705
14706#include <stdbool.h>
14707
14708int fn_branch(bool do_branch1, bool do_branch2);
14709
14710#endif // BAR_H
14711ˇ",
14712 );
14713 cx.executor().run_until_parked();
14714 cx.update_editor(|editor, window, cx| {
14715 editor.handle_input("#", window, cx);
14716 });
14717 cx.executor().run_until_parked();
14718 cx.update_editor(|editor, window, cx| {
14719 editor.handle_input("i", window, cx);
14720 });
14721 cx.executor().run_until_parked();
14722 cx.update_editor(|editor, window, cx| {
14723 editor.handle_input("n", window, cx);
14724 });
14725 cx.executor().run_until_parked();
14726 cx.assert_editor_state(
14727 "#ifndef BAR_H
14728#define BAR_H
14729
14730#include <stdbool.h>
14731
14732int fn_branch(bool do_branch1, bool do_branch2);
14733
14734#endif // BAR_H
14735#inˇ",
14736 );
14737
14738 cx.lsp
14739 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14740 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14741 is_incomplete: false,
14742 item_defaults: None,
14743 items: vec![lsp::CompletionItem {
14744 kind: Some(lsp::CompletionItemKind::SNIPPET),
14745 label_details: Some(lsp::CompletionItemLabelDetails {
14746 detail: Some("header".to_string()),
14747 description: None,
14748 }),
14749 label: " include".to_string(),
14750 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14751 range: lsp::Range {
14752 start: lsp::Position {
14753 line: 8,
14754 character: 1,
14755 },
14756 end: lsp::Position {
14757 line: 8,
14758 character: 1,
14759 },
14760 },
14761 new_text: "include \"$0\"".to_string(),
14762 })),
14763 sort_text: Some("40b67681include".to_string()),
14764 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14765 filter_text: Some("include".to_string()),
14766 insert_text: Some("include \"$0\"".to_string()),
14767 ..lsp::CompletionItem::default()
14768 }],
14769 })))
14770 });
14771 cx.update_editor(|editor, window, cx| {
14772 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14773 });
14774 cx.executor().run_until_parked();
14775 cx.update_editor(|editor, window, cx| {
14776 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14777 });
14778 cx.executor().run_until_parked();
14779 cx.assert_editor_state(
14780 "#ifndef BAR_H
14781#define BAR_H
14782
14783#include <stdbool.h>
14784
14785int fn_branch(bool do_branch1, bool do_branch2);
14786
14787#endif // BAR_H
14788#include \"ˇ\"",
14789 );
14790
14791 cx.lsp
14792 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14793 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14794 is_incomplete: true,
14795 item_defaults: None,
14796 items: vec![lsp::CompletionItem {
14797 kind: Some(lsp::CompletionItemKind::FILE),
14798 label: "AGL/".to_string(),
14799 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14800 range: lsp::Range {
14801 start: lsp::Position {
14802 line: 8,
14803 character: 10,
14804 },
14805 end: lsp::Position {
14806 line: 8,
14807 character: 11,
14808 },
14809 },
14810 new_text: "AGL/".to_string(),
14811 })),
14812 sort_text: Some("40b67681AGL/".to_string()),
14813 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14814 filter_text: Some("AGL/".to_string()),
14815 insert_text: Some("AGL/".to_string()),
14816 ..lsp::CompletionItem::default()
14817 }],
14818 })))
14819 });
14820 cx.update_editor(|editor, window, cx| {
14821 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14822 });
14823 cx.executor().run_until_parked();
14824 cx.update_editor(|editor, window, cx| {
14825 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14826 });
14827 cx.executor().run_until_parked();
14828 cx.assert_editor_state(
14829 r##"#ifndef BAR_H
14830#define BAR_H
14831
14832#include <stdbool.h>
14833
14834int fn_branch(bool do_branch1, bool do_branch2);
14835
14836#endif // BAR_H
14837#include "AGL/ˇ"##,
14838 );
14839
14840 cx.update_editor(|editor, window, cx| {
14841 editor.handle_input("\"", window, cx);
14842 });
14843 cx.executor().run_until_parked();
14844 cx.assert_editor_state(
14845 r##"#ifndef BAR_H
14846#define BAR_H
14847
14848#include <stdbool.h>
14849
14850int fn_branch(bool do_branch1, bool do_branch2);
14851
14852#endif // BAR_H
14853#include "AGL/"ˇ"##,
14854 );
14855}
14856
14857#[gpui::test]
14858async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
14859 init_test(cx, |_| {});
14860
14861 let mut cx = EditorLspTestContext::new_rust(
14862 lsp::ServerCapabilities {
14863 completion_provider: Some(lsp::CompletionOptions {
14864 trigger_characters: Some(vec![".".to_string()]),
14865 resolve_provider: Some(true),
14866 ..Default::default()
14867 }),
14868 ..Default::default()
14869 },
14870 cx,
14871 )
14872 .await;
14873
14874 cx.set_state("fn main() { let a = 2ˇ; }");
14875 cx.simulate_keystroke(".");
14876 let completion_item = lsp::CompletionItem {
14877 label: "Some".into(),
14878 kind: Some(lsp::CompletionItemKind::SNIPPET),
14879 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14880 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14881 kind: lsp::MarkupKind::Markdown,
14882 value: "```rust\nSome(2)\n```".to_string(),
14883 })),
14884 deprecated: Some(false),
14885 sort_text: Some("Some".to_string()),
14886 filter_text: Some("Some".to_string()),
14887 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14888 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14889 range: lsp::Range {
14890 start: lsp::Position {
14891 line: 0,
14892 character: 22,
14893 },
14894 end: lsp::Position {
14895 line: 0,
14896 character: 22,
14897 },
14898 },
14899 new_text: "Some(2)".to_string(),
14900 })),
14901 additional_text_edits: Some(vec![lsp::TextEdit {
14902 range: lsp::Range {
14903 start: lsp::Position {
14904 line: 0,
14905 character: 20,
14906 },
14907 end: lsp::Position {
14908 line: 0,
14909 character: 22,
14910 },
14911 },
14912 new_text: "".to_string(),
14913 }]),
14914 ..Default::default()
14915 };
14916
14917 let closure_completion_item = completion_item.clone();
14918 let counter = Arc::new(AtomicUsize::new(0));
14919 let counter_clone = counter.clone();
14920 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14921 let task_completion_item = closure_completion_item.clone();
14922 counter_clone.fetch_add(1, atomic::Ordering::Release);
14923 async move {
14924 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14925 is_incomplete: true,
14926 item_defaults: None,
14927 items: vec![task_completion_item],
14928 })))
14929 }
14930 });
14931
14932 cx.condition(|editor, _| editor.context_menu_visible())
14933 .await;
14934 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
14935 assert!(request.next().await.is_some());
14936 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14937
14938 cx.simulate_keystrokes("S o m");
14939 cx.condition(|editor, _| editor.context_menu_visible())
14940 .await;
14941 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
14942 assert!(request.next().await.is_some());
14943 assert!(request.next().await.is_some());
14944 assert!(request.next().await.is_some());
14945 request.close();
14946 assert!(request.next().await.is_none());
14947 assert_eq!(
14948 counter.load(atomic::Ordering::Acquire),
14949 4,
14950 "With the completions menu open, only one LSP request should happen per input"
14951 );
14952}
14953
14954#[gpui::test]
14955async fn test_toggle_comment(cx: &mut TestAppContext) {
14956 init_test(cx, |_| {});
14957 let mut cx = EditorTestContext::new(cx).await;
14958 let language = Arc::new(Language::new(
14959 LanguageConfig {
14960 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14961 ..Default::default()
14962 },
14963 Some(tree_sitter_rust::LANGUAGE.into()),
14964 ));
14965 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14966
14967 // If multiple selections intersect a line, the line is only toggled once.
14968 cx.set_state(indoc! {"
14969 fn a() {
14970 «//b();
14971 ˇ»// «c();
14972 //ˇ» d();
14973 }
14974 "});
14975
14976 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14977
14978 cx.assert_editor_state(indoc! {"
14979 fn a() {
14980 «b();
14981 c();
14982 ˇ» d();
14983 }
14984 "});
14985
14986 // The comment prefix is inserted at the same column for every line in a
14987 // selection.
14988 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14989
14990 cx.assert_editor_state(indoc! {"
14991 fn a() {
14992 // «b();
14993 // c();
14994 ˇ»// d();
14995 }
14996 "});
14997
14998 // If a selection ends at the beginning of a line, that line is not toggled.
14999 cx.set_selections_state(indoc! {"
15000 fn a() {
15001 // b();
15002 «// c();
15003 ˇ» // d();
15004 }
15005 "});
15006
15007 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15008
15009 cx.assert_editor_state(indoc! {"
15010 fn a() {
15011 // b();
15012 «c();
15013 ˇ» // d();
15014 }
15015 "});
15016
15017 // If a selection span a single line and is empty, the line is toggled.
15018 cx.set_state(indoc! {"
15019 fn a() {
15020 a();
15021 b();
15022 ˇ
15023 }
15024 "});
15025
15026 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15027
15028 cx.assert_editor_state(indoc! {"
15029 fn a() {
15030 a();
15031 b();
15032 //•ˇ
15033 }
15034 "});
15035
15036 // If a selection span multiple lines, empty lines are not toggled.
15037 cx.set_state(indoc! {"
15038 fn a() {
15039 «a();
15040
15041 c();ˇ»
15042 }
15043 "});
15044
15045 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15046
15047 cx.assert_editor_state(indoc! {"
15048 fn a() {
15049 // «a();
15050
15051 // c();ˇ»
15052 }
15053 "});
15054
15055 // If a selection includes multiple comment prefixes, all lines are uncommented.
15056 cx.set_state(indoc! {"
15057 fn a() {
15058 «// a();
15059 /// b();
15060 //! c();ˇ»
15061 }
15062 "});
15063
15064 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15065
15066 cx.assert_editor_state(indoc! {"
15067 fn a() {
15068 «a();
15069 b();
15070 c();ˇ»
15071 }
15072 "});
15073}
15074
15075#[gpui::test]
15076async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15077 init_test(cx, |_| {});
15078 let mut cx = EditorTestContext::new(cx).await;
15079 let language = Arc::new(Language::new(
15080 LanguageConfig {
15081 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15082 ..Default::default()
15083 },
15084 Some(tree_sitter_rust::LANGUAGE.into()),
15085 ));
15086 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15087
15088 let toggle_comments = &ToggleComments {
15089 advance_downwards: false,
15090 ignore_indent: true,
15091 };
15092
15093 // If multiple selections intersect a line, the line is only toggled once.
15094 cx.set_state(indoc! {"
15095 fn a() {
15096 // «b();
15097 // c();
15098 // ˇ» d();
15099 }
15100 "});
15101
15102 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15103
15104 cx.assert_editor_state(indoc! {"
15105 fn a() {
15106 «b();
15107 c();
15108 ˇ» d();
15109 }
15110 "});
15111
15112 // The comment prefix is inserted at the beginning of each line
15113 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15114
15115 cx.assert_editor_state(indoc! {"
15116 fn a() {
15117 // «b();
15118 // c();
15119 // ˇ» d();
15120 }
15121 "});
15122
15123 // If a selection ends at the beginning of a line, that line is not toggled.
15124 cx.set_selections_state(indoc! {"
15125 fn a() {
15126 // b();
15127 // «c();
15128 ˇ»// d();
15129 }
15130 "});
15131
15132 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15133
15134 cx.assert_editor_state(indoc! {"
15135 fn a() {
15136 // b();
15137 «c();
15138 ˇ»// d();
15139 }
15140 "});
15141
15142 // If a selection span a single line and is empty, the line is toggled.
15143 cx.set_state(indoc! {"
15144 fn a() {
15145 a();
15146 b();
15147 ˇ
15148 }
15149 "});
15150
15151 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15152
15153 cx.assert_editor_state(indoc! {"
15154 fn a() {
15155 a();
15156 b();
15157 //ˇ
15158 }
15159 "});
15160
15161 // If a selection span multiple lines, empty lines are not toggled.
15162 cx.set_state(indoc! {"
15163 fn a() {
15164 «a();
15165
15166 c();ˇ»
15167 }
15168 "});
15169
15170 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15171
15172 cx.assert_editor_state(indoc! {"
15173 fn a() {
15174 // «a();
15175
15176 // c();ˇ»
15177 }
15178 "});
15179
15180 // If a selection includes multiple comment prefixes, all lines are uncommented.
15181 cx.set_state(indoc! {"
15182 fn a() {
15183 // «a();
15184 /// b();
15185 //! c();ˇ»
15186 }
15187 "});
15188
15189 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15190
15191 cx.assert_editor_state(indoc! {"
15192 fn a() {
15193 «a();
15194 b();
15195 c();ˇ»
15196 }
15197 "});
15198}
15199
15200#[gpui::test]
15201async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15202 init_test(cx, |_| {});
15203
15204 let language = Arc::new(Language::new(
15205 LanguageConfig {
15206 line_comments: vec!["// ".into()],
15207 ..Default::default()
15208 },
15209 Some(tree_sitter_rust::LANGUAGE.into()),
15210 ));
15211
15212 let mut cx = EditorTestContext::new(cx).await;
15213
15214 cx.language_registry().add(language.clone());
15215 cx.update_buffer(|buffer, cx| {
15216 buffer.set_language(Some(language), cx);
15217 });
15218
15219 let toggle_comments = &ToggleComments {
15220 advance_downwards: true,
15221 ignore_indent: false,
15222 };
15223
15224 // Single cursor on one line -> advance
15225 // Cursor moves horizontally 3 characters as well on non-blank line
15226 cx.set_state(indoc!(
15227 "fn a() {
15228 ˇdog();
15229 cat();
15230 }"
15231 ));
15232 cx.update_editor(|editor, window, cx| {
15233 editor.toggle_comments(toggle_comments, window, cx);
15234 });
15235 cx.assert_editor_state(indoc!(
15236 "fn a() {
15237 // dog();
15238 catˇ();
15239 }"
15240 ));
15241
15242 // Single selection on one line -> don't advance
15243 cx.set_state(indoc!(
15244 "fn a() {
15245 «dog()ˇ»;
15246 cat();
15247 }"
15248 ));
15249 cx.update_editor(|editor, window, cx| {
15250 editor.toggle_comments(toggle_comments, window, cx);
15251 });
15252 cx.assert_editor_state(indoc!(
15253 "fn a() {
15254 // «dog()ˇ»;
15255 cat();
15256 }"
15257 ));
15258
15259 // Multiple cursors on one line -> advance
15260 cx.set_state(indoc!(
15261 "fn a() {
15262 ˇdˇog();
15263 cat();
15264 }"
15265 ));
15266 cx.update_editor(|editor, window, cx| {
15267 editor.toggle_comments(toggle_comments, window, cx);
15268 });
15269 cx.assert_editor_state(indoc!(
15270 "fn a() {
15271 // dog();
15272 catˇ(ˇ);
15273 }"
15274 ));
15275
15276 // Multiple cursors on one line, with selection -> don't advance
15277 cx.set_state(indoc!(
15278 "fn a() {
15279 ˇdˇog«()ˇ»;
15280 cat();
15281 }"
15282 ));
15283 cx.update_editor(|editor, window, cx| {
15284 editor.toggle_comments(toggle_comments, window, cx);
15285 });
15286 cx.assert_editor_state(indoc!(
15287 "fn a() {
15288 // ˇdˇog«()ˇ»;
15289 cat();
15290 }"
15291 ));
15292
15293 // Single cursor on one line -> advance
15294 // Cursor moves to column 0 on blank line
15295 cx.set_state(indoc!(
15296 "fn a() {
15297 ˇdog();
15298
15299 cat();
15300 }"
15301 ));
15302 cx.update_editor(|editor, window, cx| {
15303 editor.toggle_comments(toggle_comments, window, cx);
15304 });
15305 cx.assert_editor_state(indoc!(
15306 "fn a() {
15307 // dog();
15308 ˇ
15309 cat();
15310 }"
15311 ));
15312
15313 // Single cursor on one line -> advance
15314 // Cursor starts and ends at column 0
15315 cx.set_state(indoc!(
15316 "fn a() {
15317 ˇ dog();
15318 cat();
15319 }"
15320 ));
15321 cx.update_editor(|editor, window, cx| {
15322 editor.toggle_comments(toggle_comments, window, cx);
15323 });
15324 cx.assert_editor_state(indoc!(
15325 "fn a() {
15326 // dog();
15327 ˇ cat();
15328 }"
15329 ));
15330}
15331
15332#[gpui::test]
15333async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15334 init_test(cx, |_| {});
15335
15336 let mut cx = EditorTestContext::new(cx).await;
15337
15338 let html_language = Arc::new(
15339 Language::new(
15340 LanguageConfig {
15341 name: "HTML".into(),
15342 block_comment: Some(BlockCommentConfig {
15343 start: "<!-- ".into(),
15344 prefix: "".into(),
15345 end: " -->".into(),
15346 tab_size: 0,
15347 }),
15348 ..Default::default()
15349 },
15350 Some(tree_sitter_html::LANGUAGE.into()),
15351 )
15352 .with_injection_query(
15353 r#"
15354 (script_element
15355 (raw_text) @injection.content
15356 (#set! injection.language "javascript"))
15357 "#,
15358 )
15359 .unwrap(),
15360 );
15361
15362 let javascript_language = Arc::new(Language::new(
15363 LanguageConfig {
15364 name: "JavaScript".into(),
15365 line_comments: vec!["// ".into()],
15366 ..Default::default()
15367 },
15368 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15369 ));
15370
15371 cx.language_registry().add(html_language.clone());
15372 cx.language_registry().add(javascript_language);
15373 cx.update_buffer(|buffer, cx| {
15374 buffer.set_language(Some(html_language), cx);
15375 });
15376
15377 // Toggle comments for empty selections
15378 cx.set_state(
15379 &r#"
15380 <p>A</p>ˇ
15381 <p>B</p>ˇ
15382 <p>C</p>ˇ
15383 "#
15384 .unindent(),
15385 );
15386 cx.update_editor(|editor, window, cx| {
15387 editor.toggle_comments(&ToggleComments::default(), window, cx)
15388 });
15389 cx.assert_editor_state(
15390 &r#"
15391 <!-- <p>A</p>ˇ -->
15392 <!-- <p>B</p>ˇ -->
15393 <!-- <p>C</p>ˇ -->
15394 "#
15395 .unindent(),
15396 );
15397 cx.update_editor(|editor, window, cx| {
15398 editor.toggle_comments(&ToggleComments::default(), window, cx)
15399 });
15400 cx.assert_editor_state(
15401 &r#"
15402 <p>A</p>ˇ
15403 <p>B</p>ˇ
15404 <p>C</p>ˇ
15405 "#
15406 .unindent(),
15407 );
15408
15409 // Toggle comments for mixture of empty and non-empty selections, where
15410 // multiple selections occupy a given line.
15411 cx.set_state(
15412 &r#"
15413 <p>A«</p>
15414 <p>ˇ»B</p>ˇ
15415 <p>C«</p>
15416 <p>ˇ»D</p>ˇ
15417 "#
15418 .unindent(),
15419 );
15420
15421 cx.update_editor(|editor, window, cx| {
15422 editor.toggle_comments(&ToggleComments::default(), window, cx)
15423 });
15424 cx.assert_editor_state(
15425 &r#"
15426 <!-- <p>A«</p>
15427 <p>ˇ»B</p>ˇ -->
15428 <!-- <p>C«</p>
15429 <p>ˇ»D</p>ˇ -->
15430 "#
15431 .unindent(),
15432 );
15433 cx.update_editor(|editor, window, cx| {
15434 editor.toggle_comments(&ToggleComments::default(), window, cx)
15435 });
15436 cx.assert_editor_state(
15437 &r#"
15438 <p>A«</p>
15439 <p>ˇ»B</p>ˇ
15440 <p>C«</p>
15441 <p>ˇ»D</p>ˇ
15442 "#
15443 .unindent(),
15444 );
15445
15446 // Toggle comments when different languages are active for different
15447 // selections.
15448 cx.set_state(
15449 &r#"
15450 ˇ<script>
15451 ˇvar x = new Y();
15452 ˇ</script>
15453 "#
15454 .unindent(),
15455 );
15456 cx.executor().run_until_parked();
15457 cx.update_editor(|editor, window, cx| {
15458 editor.toggle_comments(&ToggleComments::default(), window, cx)
15459 });
15460 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15461 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15462 cx.assert_editor_state(
15463 &r#"
15464 <!-- ˇ<script> -->
15465 // ˇvar x = new Y();
15466 <!-- ˇ</script> -->
15467 "#
15468 .unindent(),
15469 );
15470}
15471
15472#[gpui::test]
15473fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15474 init_test(cx, |_| {});
15475
15476 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15477 let multibuffer = cx.new(|cx| {
15478 let mut multibuffer = MultiBuffer::new(ReadWrite);
15479 multibuffer.push_excerpts(
15480 buffer.clone(),
15481 [
15482 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15483 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15484 ],
15485 cx,
15486 );
15487 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15488 multibuffer
15489 });
15490
15491 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15492 editor.update_in(cx, |editor, window, cx| {
15493 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15494 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15495 s.select_ranges([
15496 Point::new(0, 0)..Point::new(0, 0),
15497 Point::new(1, 0)..Point::new(1, 0),
15498 ])
15499 });
15500
15501 editor.handle_input("X", window, cx);
15502 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15503 assert_eq!(
15504 editor.selections.ranges(cx),
15505 [
15506 Point::new(0, 1)..Point::new(0, 1),
15507 Point::new(1, 1)..Point::new(1, 1),
15508 ]
15509 );
15510
15511 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15513 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15514 });
15515 editor.backspace(&Default::default(), window, cx);
15516 assert_eq!(editor.text(cx), "Xa\nbbb");
15517 assert_eq!(
15518 editor.selections.ranges(cx),
15519 [Point::new(1, 0)..Point::new(1, 0)]
15520 );
15521
15522 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15523 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15524 });
15525 editor.backspace(&Default::default(), window, cx);
15526 assert_eq!(editor.text(cx), "X\nbb");
15527 assert_eq!(
15528 editor.selections.ranges(cx),
15529 [Point::new(0, 1)..Point::new(0, 1)]
15530 );
15531 });
15532}
15533
15534#[gpui::test]
15535fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15536 init_test(cx, |_| {});
15537
15538 let markers = vec![('[', ']').into(), ('(', ')').into()];
15539 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15540 indoc! {"
15541 [aaaa
15542 (bbbb]
15543 cccc)",
15544 },
15545 markers.clone(),
15546 );
15547 let excerpt_ranges = markers.into_iter().map(|marker| {
15548 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15549 ExcerptRange::new(context)
15550 });
15551 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15552 let multibuffer = cx.new(|cx| {
15553 let mut multibuffer = MultiBuffer::new(ReadWrite);
15554 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15555 multibuffer
15556 });
15557
15558 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15559 editor.update_in(cx, |editor, window, cx| {
15560 let (expected_text, selection_ranges) = marked_text_ranges(
15561 indoc! {"
15562 aaaa
15563 bˇbbb
15564 bˇbbˇb
15565 cccc"
15566 },
15567 true,
15568 );
15569 assert_eq!(editor.text(cx), expected_text);
15570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15571 s.select_ranges(selection_ranges)
15572 });
15573
15574 editor.handle_input("X", window, cx);
15575
15576 let (expected_text, expected_selections) = marked_text_ranges(
15577 indoc! {"
15578 aaaa
15579 bXˇbbXb
15580 bXˇbbXˇb
15581 cccc"
15582 },
15583 false,
15584 );
15585 assert_eq!(editor.text(cx), expected_text);
15586 assert_eq!(editor.selections.ranges(cx), expected_selections);
15587
15588 editor.newline(&Newline, window, cx);
15589 let (expected_text, expected_selections) = marked_text_ranges(
15590 indoc! {"
15591 aaaa
15592 bX
15593 ˇbbX
15594 b
15595 bX
15596 ˇbbX
15597 ˇb
15598 cccc"
15599 },
15600 false,
15601 );
15602 assert_eq!(editor.text(cx), expected_text);
15603 assert_eq!(editor.selections.ranges(cx), expected_selections);
15604 });
15605}
15606
15607#[gpui::test]
15608fn test_refresh_selections(cx: &mut TestAppContext) {
15609 init_test(cx, |_| {});
15610
15611 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15612 let mut excerpt1_id = None;
15613 let multibuffer = cx.new(|cx| {
15614 let mut multibuffer = MultiBuffer::new(ReadWrite);
15615 excerpt1_id = multibuffer
15616 .push_excerpts(
15617 buffer.clone(),
15618 [
15619 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15620 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15621 ],
15622 cx,
15623 )
15624 .into_iter()
15625 .next();
15626 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15627 multibuffer
15628 });
15629
15630 let editor = cx.add_window(|window, cx| {
15631 let mut editor = build_editor(multibuffer.clone(), window, cx);
15632 let snapshot = editor.snapshot(window, cx);
15633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15634 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15635 });
15636 editor.begin_selection(
15637 Point::new(2, 1).to_display_point(&snapshot),
15638 true,
15639 1,
15640 window,
15641 cx,
15642 );
15643 assert_eq!(
15644 editor.selections.ranges(cx),
15645 [
15646 Point::new(1, 3)..Point::new(1, 3),
15647 Point::new(2, 1)..Point::new(2, 1),
15648 ]
15649 );
15650 editor
15651 });
15652
15653 // Refreshing selections is a no-op when excerpts haven't changed.
15654 _ = editor.update(cx, |editor, window, cx| {
15655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15656 assert_eq!(
15657 editor.selections.ranges(cx),
15658 [
15659 Point::new(1, 3)..Point::new(1, 3),
15660 Point::new(2, 1)..Point::new(2, 1),
15661 ]
15662 );
15663 });
15664
15665 multibuffer.update(cx, |multibuffer, cx| {
15666 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15667 });
15668 _ = editor.update(cx, |editor, window, cx| {
15669 // Removing an excerpt causes the first selection to become degenerate.
15670 assert_eq!(
15671 editor.selections.ranges(cx),
15672 [
15673 Point::new(0, 0)..Point::new(0, 0),
15674 Point::new(0, 1)..Point::new(0, 1)
15675 ]
15676 );
15677
15678 // Refreshing selections will relocate the first selection to the original buffer
15679 // location.
15680 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15681 assert_eq!(
15682 editor.selections.ranges(cx),
15683 [
15684 Point::new(0, 1)..Point::new(0, 1),
15685 Point::new(0, 3)..Point::new(0, 3)
15686 ]
15687 );
15688 assert!(editor.selections.pending_anchor().is_some());
15689 });
15690}
15691
15692#[gpui::test]
15693fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15694 init_test(cx, |_| {});
15695
15696 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15697 let mut excerpt1_id = None;
15698 let multibuffer = cx.new(|cx| {
15699 let mut multibuffer = MultiBuffer::new(ReadWrite);
15700 excerpt1_id = multibuffer
15701 .push_excerpts(
15702 buffer.clone(),
15703 [
15704 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15705 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15706 ],
15707 cx,
15708 )
15709 .into_iter()
15710 .next();
15711 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15712 multibuffer
15713 });
15714
15715 let editor = cx.add_window(|window, cx| {
15716 let mut editor = build_editor(multibuffer.clone(), window, cx);
15717 let snapshot = editor.snapshot(window, cx);
15718 editor.begin_selection(
15719 Point::new(1, 3).to_display_point(&snapshot),
15720 false,
15721 1,
15722 window,
15723 cx,
15724 );
15725 assert_eq!(
15726 editor.selections.ranges(cx),
15727 [Point::new(1, 3)..Point::new(1, 3)]
15728 );
15729 editor
15730 });
15731
15732 multibuffer.update(cx, |multibuffer, cx| {
15733 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15734 });
15735 _ = editor.update(cx, |editor, window, cx| {
15736 assert_eq!(
15737 editor.selections.ranges(cx),
15738 [Point::new(0, 0)..Point::new(0, 0)]
15739 );
15740
15741 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15743 assert_eq!(
15744 editor.selections.ranges(cx),
15745 [Point::new(0, 3)..Point::new(0, 3)]
15746 );
15747 assert!(editor.selections.pending_anchor().is_some());
15748 });
15749}
15750
15751#[gpui::test]
15752async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15753 init_test(cx, |_| {});
15754
15755 let language = Arc::new(
15756 Language::new(
15757 LanguageConfig {
15758 brackets: BracketPairConfig {
15759 pairs: vec![
15760 BracketPair {
15761 start: "{".to_string(),
15762 end: "}".to_string(),
15763 close: true,
15764 surround: true,
15765 newline: true,
15766 },
15767 BracketPair {
15768 start: "/* ".to_string(),
15769 end: " */".to_string(),
15770 close: true,
15771 surround: true,
15772 newline: true,
15773 },
15774 ],
15775 ..Default::default()
15776 },
15777 ..Default::default()
15778 },
15779 Some(tree_sitter_rust::LANGUAGE.into()),
15780 )
15781 .with_indents_query("")
15782 .unwrap(),
15783 );
15784
15785 let text = concat!(
15786 "{ }\n", //
15787 " x\n", //
15788 " /* */\n", //
15789 "x\n", //
15790 "{{} }\n", //
15791 );
15792
15793 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15794 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15795 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15796 editor
15797 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
15798 .await;
15799
15800 editor.update_in(cx, |editor, window, cx| {
15801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15802 s.select_display_ranges([
15803 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
15804 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
15805 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
15806 ])
15807 });
15808 editor.newline(&Newline, window, cx);
15809
15810 assert_eq!(
15811 editor.buffer().read(cx).read(cx).text(),
15812 concat!(
15813 "{ \n", // Suppress rustfmt
15814 "\n", //
15815 "}\n", //
15816 " x\n", //
15817 " /* \n", //
15818 " \n", //
15819 " */\n", //
15820 "x\n", //
15821 "{{} \n", //
15822 "}\n", //
15823 )
15824 );
15825 });
15826}
15827
15828#[gpui::test]
15829fn test_highlighted_ranges(cx: &mut TestAppContext) {
15830 init_test(cx, |_| {});
15831
15832 let editor = cx.add_window(|window, cx| {
15833 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
15834 build_editor(buffer, window, cx)
15835 });
15836
15837 _ = editor.update(cx, |editor, window, cx| {
15838 struct Type1;
15839 struct Type2;
15840
15841 let buffer = editor.buffer.read(cx).snapshot(cx);
15842
15843 let anchor_range =
15844 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
15845
15846 editor.highlight_background::<Type1>(
15847 &[
15848 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
15849 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
15850 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
15851 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
15852 ],
15853 |_| Hsla::red(),
15854 cx,
15855 );
15856 editor.highlight_background::<Type2>(
15857 &[
15858 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
15859 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
15860 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
15861 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
15862 ],
15863 |_| Hsla::green(),
15864 cx,
15865 );
15866
15867 let snapshot = editor.snapshot(window, cx);
15868 let highlighted_ranges = editor.sorted_background_highlights_in_range(
15869 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
15870 &snapshot,
15871 cx.theme(),
15872 );
15873 assert_eq!(
15874 highlighted_ranges,
15875 &[
15876 (
15877 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
15878 Hsla::green(),
15879 ),
15880 (
15881 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
15882 Hsla::red(),
15883 ),
15884 (
15885 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
15886 Hsla::green(),
15887 ),
15888 (
15889 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15890 Hsla::red(),
15891 ),
15892 ]
15893 );
15894 assert_eq!(
15895 editor.sorted_background_highlights_in_range(
15896 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
15897 &snapshot,
15898 cx.theme(),
15899 ),
15900 &[(
15901 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15902 Hsla::red(),
15903 )]
15904 );
15905 });
15906}
15907
15908#[gpui::test]
15909async fn test_following(cx: &mut TestAppContext) {
15910 init_test(cx, |_| {});
15911
15912 let fs = FakeFs::new(cx.executor());
15913 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15914
15915 let buffer = project.update(cx, |project, cx| {
15916 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
15917 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
15918 });
15919 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
15920 let follower = cx.update(|cx| {
15921 cx.open_window(
15922 WindowOptions {
15923 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
15924 gpui::Point::new(px(0.), px(0.)),
15925 gpui::Point::new(px(10.), px(80.)),
15926 ))),
15927 ..Default::default()
15928 },
15929 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
15930 )
15931 .unwrap()
15932 });
15933
15934 let is_still_following = Rc::new(RefCell::new(true));
15935 let follower_edit_event_count = Rc::new(RefCell::new(0));
15936 let pending_update = Rc::new(RefCell::new(None));
15937 let leader_entity = leader.root(cx).unwrap();
15938 let follower_entity = follower.root(cx).unwrap();
15939 _ = follower.update(cx, {
15940 let update = pending_update.clone();
15941 let is_still_following = is_still_following.clone();
15942 let follower_edit_event_count = follower_edit_event_count.clone();
15943 |_, window, cx| {
15944 cx.subscribe_in(
15945 &leader_entity,
15946 window,
15947 move |_, leader, event, window, cx| {
15948 leader.read(cx).add_event_to_update_proto(
15949 event,
15950 &mut update.borrow_mut(),
15951 window,
15952 cx,
15953 );
15954 },
15955 )
15956 .detach();
15957
15958 cx.subscribe_in(
15959 &follower_entity,
15960 window,
15961 move |_, _, event: &EditorEvent, _window, _cx| {
15962 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
15963 *is_still_following.borrow_mut() = false;
15964 }
15965
15966 if let EditorEvent::BufferEdited = event {
15967 *follower_edit_event_count.borrow_mut() += 1;
15968 }
15969 },
15970 )
15971 .detach();
15972 }
15973 });
15974
15975 // Update the selections only
15976 _ = leader.update(cx, |leader, window, cx| {
15977 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15978 s.select_ranges([1..1])
15979 });
15980 });
15981 follower
15982 .update(cx, |follower, window, cx| {
15983 follower.apply_update_proto(
15984 &project,
15985 pending_update.borrow_mut().take().unwrap(),
15986 window,
15987 cx,
15988 )
15989 })
15990 .unwrap()
15991 .await
15992 .unwrap();
15993 _ = follower.update(cx, |follower, _, cx| {
15994 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15995 });
15996 assert!(*is_still_following.borrow());
15997 assert_eq!(*follower_edit_event_count.borrow(), 0);
15998
15999 // Update the scroll position only
16000 _ = leader.update(cx, |leader, window, cx| {
16001 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16002 });
16003 follower
16004 .update(cx, |follower, window, cx| {
16005 follower.apply_update_proto(
16006 &project,
16007 pending_update.borrow_mut().take().unwrap(),
16008 window,
16009 cx,
16010 )
16011 })
16012 .unwrap()
16013 .await
16014 .unwrap();
16015 assert_eq!(
16016 follower
16017 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16018 .unwrap(),
16019 gpui::Point::new(1.5, 3.5)
16020 );
16021 assert!(*is_still_following.borrow());
16022 assert_eq!(*follower_edit_event_count.borrow(), 0);
16023
16024 // Update the selections and scroll position. The follower's scroll position is updated
16025 // via autoscroll, not via the leader's exact scroll position.
16026 _ = leader.update(cx, |leader, window, cx| {
16027 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16028 s.select_ranges([0..0])
16029 });
16030 leader.request_autoscroll(Autoscroll::newest(), cx);
16031 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16032 });
16033 follower
16034 .update(cx, |follower, window, cx| {
16035 follower.apply_update_proto(
16036 &project,
16037 pending_update.borrow_mut().take().unwrap(),
16038 window,
16039 cx,
16040 )
16041 })
16042 .unwrap()
16043 .await
16044 .unwrap();
16045 _ = follower.update(cx, |follower, _, cx| {
16046 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16047 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16048 });
16049 assert!(*is_still_following.borrow());
16050
16051 // Creating a pending selection that precedes another selection
16052 _ = leader.update(cx, |leader, window, cx| {
16053 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16054 s.select_ranges([1..1])
16055 });
16056 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16057 });
16058 follower
16059 .update(cx, |follower, window, cx| {
16060 follower.apply_update_proto(
16061 &project,
16062 pending_update.borrow_mut().take().unwrap(),
16063 window,
16064 cx,
16065 )
16066 })
16067 .unwrap()
16068 .await
16069 .unwrap();
16070 _ = follower.update(cx, |follower, _, cx| {
16071 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16072 });
16073 assert!(*is_still_following.borrow());
16074
16075 // Extend the pending selection so that it surrounds another selection
16076 _ = leader.update(cx, |leader, window, cx| {
16077 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16078 });
16079 follower
16080 .update(cx, |follower, window, cx| {
16081 follower.apply_update_proto(
16082 &project,
16083 pending_update.borrow_mut().take().unwrap(),
16084 window,
16085 cx,
16086 )
16087 })
16088 .unwrap()
16089 .await
16090 .unwrap();
16091 _ = follower.update(cx, |follower, _, cx| {
16092 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16093 });
16094
16095 // Scrolling locally breaks the follow
16096 _ = follower.update(cx, |follower, window, cx| {
16097 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16098 follower.set_scroll_anchor(
16099 ScrollAnchor {
16100 anchor: top_anchor,
16101 offset: gpui::Point::new(0.0, 0.5),
16102 },
16103 window,
16104 cx,
16105 );
16106 });
16107 assert!(!(*is_still_following.borrow()));
16108}
16109
16110#[gpui::test]
16111async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16112 init_test(cx, |_| {});
16113
16114 let fs = FakeFs::new(cx.executor());
16115 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16116 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16117 let pane = workspace
16118 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16119 .unwrap();
16120
16121 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16122
16123 let leader = pane.update_in(cx, |_, window, cx| {
16124 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16125 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16126 });
16127
16128 // Start following the editor when it has no excerpts.
16129 let mut state_message =
16130 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16131 let workspace_entity = workspace.root(cx).unwrap();
16132 let follower_1 = cx
16133 .update_window(*workspace.deref(), |_, window, cx| {
16134 Editor::from_state_proto(
16135 workspace_entity,
16136 ViewId {
16137 creator: CollaboratorId::PeerId(PeerId::default()),
16138 id: 0,
16139 },
16140 &mut state_message,
16141 window,
16142 cx,
16143 )
16144 })
16145 .unwrap()
16146 .unwrap()
16147 .await
16148 .unwrap();
16149
16150 let update_message = Rc::new(RefCell::new(None));
16151 follower_1.update_in(cx, {
16152 let update = update_message.clone();
16153 |_, window, cx| {
16154 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16155 leader.read(cx).add_event_to_update_proto(
16156 event,
16157 &mut update.borrow_mut(),
16158 window,
16159 cx,
16160 );
16161 })
16162 .detach();
16163 }
16164 });
16165
16166 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16167 (
16168 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16169 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16170 )
16171 });
16172
16173 // Insert some excerpts.
16174 leader.update(cx, |leader, cx| {
16175 leader.buffer.update(cx, |multibuffer, cx| {
16176 multibuffer.set_excerpts_for_path(
16177 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16178 buffer_1.clone(),
16179 vec![
16180 Point::row_range(0..3),
16181 Point::row_range(1..6),
16182 Point::row_range(12..15),
16183 ],
16184 0,
16185 cx,
16186 );
16187 multibuffer.set_excerpts_for_path(
16188 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16189 buffer_2.clone(),
16190 vec![Point::row_range(0..6), Point::row_range(8..12)],
16191 0,
16192 cx,
16193 );
16194 });
16195 });
16196
16197 // Apply the update of adding the excerpts.
16198 follower_1
16199 .update_in(cx, |follower, window, cx| {
16200 follower.apply_update_proto(
16201 &project,
16202 update_message.borrow().clone().unwrap(),
16203 window,
16204 cx,
16205 )
16206 })
16207 .await
16208 .unwrap();
16209 assert_eq!(
16210 follower_1.update(cx, |editor, cx| editor.text(cx)),
16211 leader.update(cx, |editor, cx| editor.text(cx))
16212 );
16213 update_message.borrow_mut().take();
16214
16215 // Start following separately after it already has excerpts.
16216 let mut state_message =
16217 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16218 let workspace_entity = workspace.root(cx).unwrap();
16219 let follower_2 = cx
16220 .update_window(*workspace.deref(), |_, window, cx| {
16221 Editor::from_state_proto(
16222 workspace_entity,
16223 ViewId {
16224 creator: CollaboratorId::PeerId(PeerId::default()),
16225 id: 0,
16226 },
16227 &mut state_message,
16228 window,
16229 cx,
16230 )
16231 })
16232 .unwrap()
16233 .unwrap()
16234 .await
16235 .unwrap();
16236 assert_eq!(
16237 follower_2.update(cx, |editor, cx| editor.text(cx)),
16238 leader.update(cx, |editor, cx| editor.text(cx))
16239 );
16240
16241 // Remove some excerpts.
16242 leader.update(cx, |leader, cx| {
16243 leader.buffer.update(cx, |multibuffer, cx| {
16244 let excerpt_ids = multibuffer.excerpt_ids();
16245 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16246 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16247 });
16248 });
16249
16250 // Apply the update of removing the excerpts.
16251 follower_1
16252 .update_in(cx, |follower, window, cx| {
16253 follower.apply_update_proto(
16254 &project,
16255 update_message.borrow().clone().unwrap(),
16256 window,
16257 cx,
16258 )
16259 })
16260 .await
16261 .unwrap();
16262 follower_2
16263 .update_in(cx, |follower, window, cx| {
16264 follower.apply_update_proto(
16265 &project,
16266 update_message.borrow().clone().unwrap(),
16267 window,
16268 cx,
16269 )
16270 })
16271 .await
16272 .unwrap();
16273 update_message.borrow_mut().take();
16274 assert_eq!(
16275 follower_1.update(cx, |editor, cx| editor.text(cx)),
16276 leader.update(cx, |editor, cx| editor.text(cx))
16277 );
16278}
16279
16280#[gpui::test]
16281async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16282 init_test(cx, |_| {});
16283
16284 let mut cx = EditorTestContext::new(cx).await;
16285 let lsp_store =
16286 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16287
16288 cx.set_state(indoc! {"
16289 ˇfn func(abc def: i32) -> u32 {
16290 }
16291 "});
16292
16293 cx.update(|_, cx| {
16294 lsp_store.update(cx, |lsp_store, cx| {
16295 lsp_store
16296 .update_diagnostics(
16297 LanguageServerId(0),
16298 lsp::PublishDiagnosticsParams {
16299 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16300 version: None,
16301 diagnostics: vec![
16302 lsp::Diagnostic {
16303 range: lsp::Range::new(
16304 lsp::Position::new(0, 11),
16305 lsp::Position::new(0, 12),
16306 ),
16307 severity: Some(lsp::DiagnosticSeverity::ERROR),
16308 ..Default::default()
16309 },
16310 lsp::Diagnostic {
16311 range: lsp::Range::new(
16312 lsp::Position::new(0, 12),
16313 lsp::Position::new(0, 15),
16314 ),
16315 severity: Some(lsp::DiagnosticSeverity::ERROR),
16316 ..Default::default()
16317 },
16318 lsp::Diagnostic {
16319 range: lsp::Range::new(
16320 lsp::Position::new(0, 25),
16321 lsp::Position::new(0, 28),
16322 ),
16323 severity: Some(lsp::DiagnosticSeverity::ERROR),
16324 ..Default::default()
16325 },
16326 ],
16327 },
16328 None,
16329 DiagnosticSourceKind::Pushed,
16330 &[],
16331 cx,
16332 )
16333 .unwrap()
16334 });
16335 });
16336
16337 executor.run_until_parked();
16338
16339 cx.update_editor(|editor, window, cx| {
16340 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16341 });
16342
16343 cx.assert_editor_state(indoc! {"
16344 fn func(abc def: i32) -> ˇu32 {
16345 }
16346 "});
16347
16348 cx.update_editor(|editor, window, cx| {
16349 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16350 });
16351
16352 cx.assert_editor_state(indoc! {"
16353 fn func(abc ˇdef: i32) -> u32 {
16354 }
16355 "});
16356
16357 cx.update_editor(|editor, window, cx| {
16358 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16359 });
16360
16361 cx.assert_editor_state(indoc! {"
16362 fn func(abcˇ def: i32) -> u32 {
16363 }
16364 "});
16365
16366 cx.update_editor(|editor, window, cx| {
16367 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16368 });
16369
16370 cx.assert_editor_state(indoc! {"
16371 fn func(abc def: i32) -> ˇu32 {
16372 }
16373 "});
16374}
16375
16376#[gpui::test]
16377async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16378 init_test(cx, |_| {});
16379
16380 let mut cx = EditorTestContext::new(cx).await;
16381
16382 let diff_base = r#"
16383 use some::mod;
16384
16385 const A: u32 = 42;
16386
16387 fn main() {
16388 println!("hello");
16389
16390 println!("world");
16391 }
16392 "#
16393 .unindent();
16394
16395 // Edits are modified, removed, modified, added
16396 cx.set_state(
16397 &r#"
16398 use some::modified;
16399
16400 ˇ
16401 fn main() {
16402 println!("hello there");
16403
16404 println!("around the");
16405 println!("world");
16406 }
16407 "#
16408 .unindent(),
16409 );
16410
16411 cx.set_head_text(&diff_base);
16412 executor.run_until_parked();
16413
16414 cx.update_editor(|editor, window, cx| {
16415 //Wrap around the bottom of the buffer
16416 for _ in 0..3 {
16417 editor.go_to_next_hunk(&GoToHunk, window, cx);
16418 }
16419 });
16420
16421 cx.assert_editor_state(
16422 &r#"
16423 ˇuse some::modified;
16424
16425
16426 fn main() {
16427 println!("hello there");
16428
16429 println!("around the");
16430 println!("world");
16431 }
16432 "#
16433 .unindent(),
16434 );
16435
16436 cx.update_editor(|editor, window, cx| {
16437 //Wrap around the top of the buffer
16438 for _ in 0..2 {
16439 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16440 }
16441 });
16442
16443 cx.assert_editor_state(
16444 &r#"
16445 use some::modified;
16446
16447
16448 fn main() {
16449 ˇ println!("hello there");
16450
16451 println!("around the");
16452 println!("world");
16453 }
16454 "#
16455 .unindent(),
16456 );
16457
16458 cx.update_editor(|editor, window, cx| {
16459 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16460 });
16461
16462 cx.assert_editor_state(
16463 &r#"
16464 use some::modified;
16465
16466 ˇ
16467 fn main() {
16468 println!("hello there");
16469
16470 println!("around the");
16471 println!("world");
16472 }
16473 "#
16474 .unindent(),
16475 );
16476
16477 cx.update_editor(|editor, window, cx| {
16478 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16479 });
16480
16481 cx.assert_editor_state(
16482 &r#"
16483 ˇuse some::modified;
16484
16485
16486 fn main() {
16487 println!("hello there");
16488
16489 println!("around the");
16490 println!("world");
16491 }
16492 "#
16493 .unindent(),
16494 );
16495
16496 cx.update_editor(|editor, window, cx| {
16497 for _ in 0..2 {
16498 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16499 }
16500 });
16501
16502 cx.assert_editor_state(
16503 &r#"
16504 use some::modified;
16505
16506
16507 fn main() {
16508 ˇ println!("hello there");
16509
16510 println!("around the");
16511 println!("world");
16512 }
16513 "#
16514 .unindent(),
16515 );
16516
16517 cx.update_editor(|editor, window, cx| {
16518 editor.fold(&Fold, window, cx);
16519 });
16520
16521 cx.update_editor(|editor, window, cx| {
16522 editor.go_to_next_hunk(&GoToHunk, window, cx);
16523 });
16524
16525 cx.assert_editor_state(
16526 &r#"
16527 ˇuse some::modified;
16528
16529
16530 fn main() {
16531 println!("hello there");
16532
16533 println!("around the");
16534 println!("world");
16535 }
16536 "#
16537 .unindent(),
16538 );
16539}
16540
16541#[test]
16542fn test_split_words() {
16543 fn split(text: &str) -> Vec<&str> {
16544 split_words(text).collect()
16545 }
16546
16547 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16548 assert_eq!(split("hello_world"), &["hello_", "world"]);
16549 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16550 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16551 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16552 assert_eq!(split("helloworld"), &["helloworld"]);
16553
16554 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16555}
16556
16557#[gpui::test]
16558async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16559 init_test(cx, |_| {});
16560
16561 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16562 let mut assert = |before, after| {
16563 let _state_context = cx.set_state(before);
16564 cx.run_until_parked();
16565 cx.update_editor(|editor, window, cx| {
16566 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16567 });
16568 cx.run_until_parked();
16569 cx.assert_editor_state(after);
16570 };
16571
16572 // Outside bracket jumps to outside of matching bracket
16573 assert("console.logˇ(var);", "console.log(var)ˇ;");
16574 assert("console.log(var)ˇ;", "console.logˇ(var);");
16575
16576 // Inside bracket jumps to inside of matching bracket
16577 assert("console.log(ˇvar);", "console.log(varˇ);");
16578 assert("console.log(varˇ);", "console.log(ˇvar);");
16579
16580 // When outside a bracket and inside, favor jumping to the inside bracket
16581 assert(
16582 "console.log('foo', [1, 2, 3]ˇ);",
16583 "console.log(ˇ'foo', [1, 2, 3]);",
16584 );
16585 assert(
16586 "console.log(ˇ'foo', [1, 2, 3]);",
16587 "console.log('foo', [1, 2, 3]ˇ);",
16588 );
16589
16590 // Bias forward if two options are equally likely
16591 assert(
16592 "let result = curried_fun()ˇ();",
16593 "let result = curried_fun()()ˇ;",
16594 );
16595
16596 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16597 assert(
16598 indoc! {"
16599 function test() {
16600 console.log('test')ˇ
16601 }"},
16602 indoc! {"
16603 function test() {
16604 console.logˇ('test')
16605 }"},
16606 );
16607}
16608
16609#[gpui::test]
16610async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16611 init_test(cx, |_| {});
16612
16613 let fs = FakeFs::new(cx.executor());
16614 fs.insert_tree(
16615 path!("/a"),
16616 json!({
16617 "main.rs": "fn main() { let a = 5; }",
16618 "other.rs": "// Test file",
16619 }),
16620 )
16621 .await;
16622 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16623
16624 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16625 language_registry.add(Arc::new(Language::new(
16626 LanguageConfig {
16627 name: "Rust".into(),
16628 matcher: LanguageMatcher {
16629 path_suffixes: vec!["rs".to_string()],
16630 ..Default::default()
16631 },
16632 brackets: BracketPairConfig {
16633 pairs: vec![BracketPair {
16634 start: "{".to_string(),
16635 end: "}".to_string(),
16636 close: true,
16637 surround: true,
16638 newline: true,
16639 }],
16640 disabled_scopes_by_bracket_ix: Vec::new(),
16641 },
16642 ..Default::default()
16643 },
16644 Some(tree_sitter_rust::LANGUAGE.into()),
16645 )));
16646 let mut fake_servers = language_registry.register_fake_lsp(
16647 "Rust",
16648 FakeLspAdapter {
16649 capabilities: lsp::ServerCapabilities {
16650 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16651 first_trigger_character: "{".to_string(),
16652 more_trigger_character: None,
16653 }),
16654 ..Default::default()
16655 },
16656 ..Default::default()
16657 },
16658 );
16659
16660 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16661
16662 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16663
16664 let worktree_id = workspace
16665 .update(cx, |workspace, _, cx| {
16666 workspace.project().update(cx, |project, cx| {
16667 project.worktrees(cx).next().unwrap().read(cx).id()
16668 })
16669 })
16670 .unwrap();
16671
16672 let buffer = project
16673 .update(cx, |project, cx| {
16674 project.open_local_buffer(path!("/a/main.rs"), cx)
16675 })
16676 .await
16677 .unwrap();
16678 let editor_handle = workspace
16679 .update(cx, |workspace, window, cx| {
16680 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16681 })
16682 .unwrap()
16683 .await
16684 .unwrap()
16685 .downcast::<Editor>()
16686 .unwrap();
16687
16688 cx.executor().start_waiting();
16689 let fake_server = fake_servers.next().await.unwrap();
16690
16691 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16692 |params, _| async move {
16693 assert_eq!(
16694 params.text_document_position.text_document.uri,
16695 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16696 );
16697 assert_eq!(
16698 params.text_document_position.position,
16699 lsp::Position::new(0, 21),
16700 );
16701
16702 Ok(Some(vec![lsp::TextEdit {
16703 new_text: "]".to_string(),
16704 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16705 }]))
16706 },
16707 );
16708
16709 editor_handle.update_in(cx, |editor, window, cx| {
16710 window.focus(&editor.focus_handle(cx));
16711 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16712 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16713 });
16714 editor.handle_input("{", window, cx);
16715 });
16716
16717 cx.executor().run_until_parked();
16718
16719 buffer.update(cx, |buffer, _| {
16720 assert_eq!(
16721 buffer.text(),
16722 "fn main() { let a = {5}; }",
16723 "No extra braces from on type formatting should appear in the buffer"
16724 )
16725 });
16726}
16727
16728#[gpui::test(iterations = 20, seeds(31))]
16729async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16730 init_test(cx, |_| {});
16731
16732 let mut cx = EditorLspTestContext::new_rust(
16733 lsp::ServerCapabilities {
16734 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16735 first_trigger_character: ".".to_string(),
16736 more_trigger_character: None,
16737 }),
16738 ..Default::default()
16739 },
16740 cx,
16741 )
16742 .await;
16743
16744 cx.update_buffer(|buffer, _| {
16745 // This causes autoindent to be async.
16746 buffer.set_sync_parse_timeout(Duration::ZERO)
16747 });
16748
16749 cx.set_state("fn c() {\n d()ˇ\n}\n");
16750 cx.simulate_keystroke("\n");
16751 cx.run_until_parked();
16752
16753 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16754 let mut request =
16755 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16756 let buffer_cloned = buffer_cloned.clone();
16757 async move {
16758 buffer_cloned.update(&mut cx, |buffer, _| {
16759 assert_eq!(
16760 buffer.text(),
16761 "fn c() {\n d()\n .\n}\n",
16762 "OnTypeFormatting should triggered after autoindent applied"
16763 )
16764 })?;
16765
16766 Ok(Some(vec![]))
16767 }
16768 });
16769
16770 cx.simulate_keystroke(".");
16771 cx.run_until_parked();
16772
16773 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16774 assert!(request.next().await.is_some());
16775 request.close();
16776 assert!(request.next().await.is_none());
16777}
16778
16779#[gpui::test]
16780async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
16781 init_test(cx, |_| {});
16782
16783 let fs = FakeFs::new(cx.executor());
16784 fs.insert_tree(
16785 path!("/a"),
16786 json!({
16787 "main.rs": "fn main() { let a = 5; }",
16788 "other.rs": "// Test file",
16789 }),
16790 )
16791 .await;
16792
16793 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16794
16795 let server_restarts = Arc::new(AtomicUsize::new(0));
16796 let closure_restarts = Arc::clone(&server_restarts);
16797 let language_server_name = "test language server";
16798 let language_name: LanguageName = "Rust".into();
16799
16800 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16801 language_registry.add(Arc::new(Language::new(
16802 LanguageConfig {
16803 name: language_name.clone(),
16804 matcher: LanguageMatcher {
16805 path_suffixes: vec!["rs".to_string()],
16806 ..Default::default()
16807 },
16808 ..Default::default()
16809 },
16810 Some(tree_sitter_rust::LANGUAGE.into()),
16811 )));
16812 let mut fake_servers = language_registry.register_fake_lsp(
16813 "Rust",
16814 FakeLspAdapter {
16815 name: language_server_name,
16816 initialization_options: Some(json!({
16817 "testOptionValue": true
16818 })),
16819 initializer: Some(Box::new(move |fake_server| {
16820 let task_restarts = Arc::clone(&closure_restarts);
16821 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
16822 task_restarts.fetch_add(1, atomic::Ordering::Release);
16823 futures::future::ready(Ok(()))
16824 });
16825 })),
16826 ..Default::default()
16827 },
16828 );
16829
16830 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16831 let _buffer = project
16832 .update(cx, |project, cx| {
16833 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
16834 })
16835 .await
16836 .unwrap();
16837 let _fake_server = fake_servers.next().await.unwrap();
16838 update_test_language_settings(cx, |language_settings| {
16839 language_settings.languages.0.insert(
16840 language_name.clone(),
16841 LanguageSettingsContent {
16842 tab_size: NonZeroU32::new(8),
16843 ..Default::default()
16844 },
16845 );
16846 });
16847 cx.executor().run_until_parked();
16848 assert_eq!(
16849 server_restarts.load(atomic::Ordering::Acquire),
16850 0,
16851 "Should not restart LSP server on an unrelated change"
16852 );
16853
16854 update_test_project_settings(cx, |project_settings| {
16855 project_settings.lsp.insert(
16856 "Some other server name".into(),
16857 LspSettings {
16858 binary: None,
16859 settings: None,
16860 initialization_options: Some(json!({
16861 "some other init value": false
16862 })),
16863 enable_lsp_tasks: false,
16864 fetch: None,
16865 },
16866 );
16867 });
16868 cx.executor().run_until_parked();
16869 assert_eq!(
16870 server_restarts.load(atomic::Ordering::Acquire),
16871 0,
16872 "Should not restart LSP server on an unrelated LSP settings change"
16873 );
16874
16875 update_test_project_settings(cx, |project_settings| {
16876 project_settings.lsp.insert(
16877 language_server_name.into(),
16878 LspSettings {
16879 binary: None,
16880 settings: None,
16881 initialization_options: Some(json!({
16882 "anotherInitValue": false
16883 })),
16884 enable_lsp_tasks: false,
16885 fetch: None,
16886 },
16887 );
16888 });
16889 cx.executor().run_until_parked();
16890 assert_eq!(
16891 server_restarts.load(atomic::Ordering::Acquire),
16892 1,
16893 "Should restart LSP server on a related LSP settings change"
16894 );
16895
16896 update_test_project_settings(cx, |project_settings| {
16897 project_settings.lsp.insert(
16898 language_server_name.into(),
16899 LspSettings {
16900 binary: None,
16901 settings: None,
16902 initialization_options: Some(json!({
16903 "anotherInitValue": false
16904 })),
16905 enable_lsp_tasks: false,
16906 fetch: None,
16907 },
16908 );
16909 });
16910 cx.executor().run_until_parked();
16911 assert_eq!(
16912 server_restarts.load(atomic::Ordering::Acquire),
16913 1,
16914 "Should not restart LSP server on a related LSP settings change that is the same"
16915 );
16916
16917 update_test_project_settings(cx, |project_settings| {
16918 project_settings.lsp.insert(
16919 language_server_name.into(),
16920 LspSettings {
16921 binary: None,
16922 settings: None,
16923 initialization_options: None,
16924 enable_lsp_tasks: false,
16925 fetch: None,
16926 },
16927 );
16928 });
16929 cx.executor().run_until_parked();
16930 assert_eq!(
16931 server_restarts.load(atomic::Ordering::Acquire),
16932 2,
16933 "Should restart LSP server on another related LSP settings change"
16934 );
16935}
16936
16937#[gpui::test]
16938async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
16939 init_test(cx, |_| {});
16940
16941 let mut cx = EditorLspTestContext::new_rust(
16942 lsp::ServerCapabilities {
16943 completion_provider: Some(lsp::CompletionOptions {
16944 trigger_characters: Some(vec![".".to_string()]),
16945 resolve_provider: Some(true),
16946 ..Default::default()
16947 }),
16948 ..Default::default()
16949 },
16950 cx,
16951 )
16952 .await;
16953
16954 cx.set_state("fn main() { let a = 2ˇ; }");
16955 cx.simulate_keystroke(".");
16956 let completion_item = lsp::CompletionItem {
16957 label: "some".into(),
16958 kind: Some(lsp::CompletionItemKind::SNIPPET),
16959 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16960 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16961 kind: lsp::MarkupKind::Markdown,
16962 value: "```rust\nSome(2)\n```".to_string(),
16963 })),
16964 deprecated: Some(false),
16965 sort_text: Some("fffffff2".to_string()),
16966 filter_text: Some("some".to_string()),
16967 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16968 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16969 range: lsp::Range {
16970 start: lsp::Position {
16971 line: 0,
16972 character: 22,
16973 },
16974 end: lsp::Position {
16975 line: 0,
16976 character: 22,
16977 },
16978 },
16979 new_text: "Some(2)".to_string(),
16980 })),
16981 additional_text_edits: Some(vec![lsp::TextEdit {
16982 range: lsp::Range {
16983 start: lsp::Position {
16984 line: 0,
16985 character: 20,
16986 },
16987 end: lsp::Position {
16988 line: 0,
16989 character: 22,
16990 },
16991 },
16992 new_text: "".to_string(),
16993 }]),
16994 ..Default::default()
16995 };
16996
16997 let closure_completion_item = completion_item.clone();
16998 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16999 let task_completion_item = closure_completion_item.clone();
17000 async move {
17001 Ok(Some(lsp::CompletionResponse::Array(vec![
17002 task_completion_item,
17003 ])))
17004 }
17005 });
17006
17007 request.next().await;
17008
17009 cx.condition(|editor, _| editor.context_menu_visible())
17010 .await;
17011 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17012 editor
17013 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17014 .unwrap()
17015 });
17016 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17017
17018 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17019 let task_completion_item = completion_item.clone();
17020 async move { Ok(task_completion_item) }
17021 })
17022 .next()
17023 .await
17024 .unwrap();
17025 apply_additional_edits.await.unwrap();
17026 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17027}
17028
17029#[gpui::test]
17030async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17031 init_test(cx, |_| {});
17032
17033 let mut cx = EditorLspTestContext::new_rust(
17034 lsp::ServerCapabilities {
17035 completion_provider: Some(lsp::CompletionOptions {
17036 trigger_characters: Some(vec![".".to_string()]),
17037 resolve_provider: Some(true),
17038 ..Default::default()
17039 }),
17040 ..Default::default()
17041 },
17042 cx,
17043 )
17044 .await;
17045
17046 cx.set_state("fn main() { let a = 2ˇ; }");
17047 cx.simulate_keystroke(".");
17048
17049 let item1 = lsp::CompletionItem {
17050 label: "method id()".to_string(),
17051 filter_text: Some("id".to_string()),
17052 detail: None,
17053 documentation: None,
17054 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17055 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17056 new_text: ".id".to_string(),
17057 })),
17058 ..lsp::CompletionItem::default()
17059 };
17060
17061 let item2 = lsp::CompletionItem {
17062 label: "other".to_string(),
17063 filter_text: Some("other".to_string()),
17064 detail: None,
17065 documentation: None,
17066 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17067 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17068 new_text: ".other".to_string(),
17069 })),
17070 ..lsp::CompletionItem::default()
17071 };
17072
17073 let item1 = item1.clone();
17074 cx.set_request_handler::<lsp::request::Completion, _, _>({
17075 let item1 = item1.clone();
17076 move |_, _, _| {
17077 let item1 = item1.clone();
17078 let item2 = item2.clone();
17079 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17080 }
17081 })
17082 .next()
17083 .await;
17084
17085 cx.condition(|editor, _| editor.context_menu_visible())
17086 .await;
17087 cx.update_editor(|editor, _, _| {
17088 let context_menu = editor.context_menu.borrow_mut();
17089 let context_menu = context_menu
17090 .as_ref()
17091 .expect("Should have the context menu deployed");
17092 match context_menu {
17093 CodeContextMenu::Completions(completions_menu) => {
17094 let completions = completions_menu.completions.borrow_mut();
17095 assert_eq!(
17096 completions
17097 .iter()
17098 .map(|completion| &completion.label.text)
17099 .collect::<Vec<_>>(),
17100 vec!["method id()", "other"]
17101 )
17102 }
17103 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17104 }
17105 });
17106
17107 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17108 let item1 = item1.clone();
17109 move |_, item_to_resolve, _| {
17110 let item1 = item1.clone();
17111 async move {
17112 if item1 == item_to_resolve {
17113 Ok(lsp::CompletionItem {
17114 label: "method id()".to_string(),
17115 filter_text: Some("id".to_string()),
17116 detail: Some("Now resolved!".to_string()),
17117 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17118 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17119 range: lsp::Range::new(
17120 lsp::Position::new(0, 22),
17121 lsp::Position::new(0, 22),
17122 ),
17123 new_text: ".id".to_string(),
17124 })),
17125 ..lsp::CompletionItem::default()
17126 })
17127 } else {
17128 Ok(item_to_resolve)
17129 }
17130 }
17131 }
17132 })
17133 .next()
17134 .await
17135 .unwrap();
17136 cx.run_until_parked();
17137
17138 cx.update_editor(|editor, window, cx| {
17139 editor.context_menu_next(&Default::default(), window, cx);
17140 });
17141
17142 cx.update_editor(|editor, _, _| {
17143 let context_menu = editor.context_menu.borrow_mut();
17144 let context_menu = context_menu
17145 .as_ref()
17146 .expect("Should have the context menu deployed");
17147 match context_menu {
17148 CodeContextMenu::Completions(completions_menu) => {
17149 let completions = completions_menu.completions.borrow_mut();
17150 assert_eq!(
17151 completions
17152 .iter()
17153 .map(|completion| &completion.label.text)
17154 .collect::<Vec<_>>(),
17155 vec!["method id() Now resolved!", "other"],
17156 "Should update first completion label, but not second as the filter text did not match."
17157 );
17158 }
17159 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17160 }
17161 });
17162}
17163
17164#[gpui::test]
17165async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17166 init_test(cx, |_| {});
17167 let mut cx = EditorLspTestContext::new_rust(
17168 lsp::ServerCapabilities {
17169 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17170 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17171 completion_provider: Some(lsp::CompletionOptions {
17172 resolve_provider: Some(true),
17173 ..Default::default()
17174 }),
17175 ..Default::default()
17176 },
17177 cx,
17178 )
17179 .await;
17180 cx.set_state(indoc! {"
17181 struct TestStruct {
17182 field: i32
17183 }
17184
17185 fn mainˇ() {
17186 let unused_var = 42;
17187 let test_struct = TestStruct { field: 42 };
17188 }
17189 "});
17190 let symbol_range = cx.lsp_range(indoc! {"
17191 struct TestStruct {
17192 field: i32
17193 }
17194
17195 «fn main»() {
17196 let unused_var = 42;
17197 let test_struct = TestStruct { field: 42 };
17198 }
17199 "});
17200 let mut hover_requests =
17201 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17202 Ok(Some(lsp::Hover {
17203 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17204 kind: lsp::MarkupKind::Markdown,
17205 value: "Function documentation".to_string(),
17206 }),
17207 range: Some(symbol_range),
17208 }))
17209 });
17210
17211 // Case 1: Test that code action menu hide hover popover
17212 cx.dispatch_action(Hover);
17213 hover_requests.next().await;
17214 cx.condition(|editor, _| editor.hover_state.visible()).await;
17215 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17216 move |_, _, _| async move {
17217 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17218 lsp::CodeAction {
17219 title: "Remove unused variable".to_string(),
17220 kind: Some(CodeActionKind::QUICKFIX),
17221 edit: Some(lsp::WorkspaceEdit {
17222 changes: Some(
17223 [(
17224 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17225 vec![lsp::TextEdit {
17226 range: lsp::Range::new(
17227 lsp::Position::new(5, 4),
17228 lsp::Position::new(5, 27),
17229 ),
17230 new_text: "".to_string(),
17231 }],
17232 )]
17233 .into_iter()
17234 .collect(),
17235 ),
17236 ..Default::default()
17237 }),
17238 ..Default::default()
17239 },
17240 )]))
17241 },
17242 );
17243 cx.update_editor(|editor, window, cx| {
17244 editor.toggle_code_actions(
17245 &ToggleCodeActions {
17246 deployed_from: None,
17247 quick_launch: false,
17248 },
17249 window,
17250 cx,
17251 );
17252 });
17253 code_action_requests.next().await;
17254 cx.run_until_parked();
17255 cx.condition(|editor, _| editor.context_menu_visible())
17256 .await;
17257 cx.update_editor(|editor, _, _| {
17258 assert!(
17259 !editor.hover_state.visible(),
17260 "Hover popover should be hidden when code action menu is shown"
17261 );
17262 // Hide code actions
17263 editor.context_menu.take();
17264 });
17265
17266 // Case 2: Test that code completions hide hover popover
17267 cx.dispatch_action(Hover);
17268 hover_requests.next().await;
17269 cx.condition(|editor, _| editor.hover_state.visible()).await;
17270 let counter = Arc::new(AtomicUsize::new(0));
17271 let mut completion_requests =
17272 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17273 let counter = counter.clone();
17274 async move {
17275 counter.fetch_add(1, atomic::Ordering::Release);
17276 Ok(Some(lsp::CompletionResponse::Array(vec![
17277 lsp::CompletionItem {
17278 label: "main".into(),
17279 kind: Some(lsp::CompletionItemKind::FUNCTION),
17280 detail: Some("() -> ()".to_string()),
17281 ..Default::default()
17282 },
17283 lsp::CompletionItem {
17284 label: "TestStruct".into(),
17285 kind: Some(lsp::CompletionItemKind::STRUCT),
17286 detail: Some("struct TestStruct".to_string()),
17287 ..Default::default()
17288 },
17289 ])))
17290 }
17291 });
17292 cx.update_editor(|editor, window, cx| {
17293 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17294 });
17295 completion_requests.next().await;
17296 cx.condition(|editor, _| editor.context_menu_visible())
17297 .await;
17298 cx.update_editor(|editor, _, _| {
17299 assert!(
17300 !editor.hover_state.visible(),
17301 "Hover popover should be hidden when completion menu is shown"
17302 );
17303 });
17304}
17305
17306#[gpui::test]
17307async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17308 init_test(cx, |_| {});
17309
17310 let mut cx = EditorLspTestContext::new_rust(
17311 lsp::ServerCapabilities {
17312 completion_provider: Some(lsp::CompletionOptions {
17313 trigger_characters: Some(vec![".".to_string()]),
17314 resolve_provider: Some(true),
17315 ..Default::default()
17316 }),
17317 ..Default::default()
17318 },
17319 cx,
17320 )
17321 .await;
17322
17323 cx.set_state("fn main() { let a = 2ˇ; }");
17324 cx.simulate_keystroke(".");
17325
17326 let unresolved_item_1 = lsp::CompletionItem {
17327 label: "id".to_string(),
17328 filter_text: Some("id".to_string()),
17329 detail: None,
17330 documentation: None,
17331 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17332 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17333 new_text: ".id".to_string(),
17334 })),
17335 ..lsp::CompletionItem::default()
17336 };
17337 let resolved_item_1 = lsp::CompletionItem {
17338 additional_text_edits: Some(vec![lsp::TextEdit {
17339 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17340 new_text: "!!".to_string(),
17341 }]),
17342 ..unresolved_item_1.clone()
17343 };
17344 let unresolved_item_2 = lsp::CompletionItem {
17345 label: "other".to_string(),
17346 filter_text: Some("other".to_string()),
17347 detail: None,
17348 documentation: None,
17349 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17350 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17351 new_text: ".other".to_string(),
17352 })),
17353 ..lsp::CompletionItem::default()
17354 };
17355 let resolved_item_2 = lsp::CompletionItem {
17356 additional_text_edits: Some(vec![lsp::TextEdit {
17357 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17358 new_text: "??".to_string(),
17359 }]),
17360 ..unresolved_item_2.clone()
17361 };
17362
17363 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17364 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17365 cx.lsp
17366 .server
17367 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17368 let unresolved_item_1 = unresolved_item_1.clone();
17369 let resolved_item_1 = resolved_item_1.clone();
17370 let unresolved_item_2 = unresolved_item_2.clone();
17371 let resolved_item_2 = resolved_item_2.clone();
17372 let resolve_requests_1 = resolve_requests_1.clone();
17373 let resolve_requests_2 = resolve_requests_2.clone();
17374 move |unresolved_request, _| {
17375 let unresolved_item_1 = unresolved_item_1.clone();
17376 let resolved_item_1 = resolved_item_1.clone();
17377 let unresolved_item_2 = unresolved_item_2.clone();
17378 let resolved_item_2 = resolved_item_2.clone();
17379 let resolve_requests_1 = resolve_requests_1.clone();
17380 let resolve_requests_2 = resolve_requests_2.clone();
17381 async move {
17382 if unresolved_request == unresolved_item_1 {
17383 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17384 Ok(resolved_item_1.clone())
17385 } else if unresolved_request == unresolved_item_2 {
17386 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17387 Ok(resolved_item_2.clone())
17388 } else {
17389 panic!("Unexpected completion item {unresolved_request:?}")
17390 }
17391 }
17392 }
17393 })
17394 .detach();
17395
17396 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17397 let unresolved_item_1 = unresolved_item_1.clone();
17398 let unresolved_item_2 = unresolved_item_2.clone();
17399 async move {
17400 Ok(Some(lsp::CompletionResponse::Array(vec![
17401 unresolved_item_1,
17402 unresolved_item_2,
17403 ])))
17404 }
17405 })
17406 .next()
17407 .await;
17408
17409 cx.condition(|editor, _| editor.context_menu_visible())
17410 .await;
17411 cx.update_editor(|editor, _, _| {
17412 let context_menu = editor.context_menu.borrow_mut();
17413 let context_menu = context_menu
17414 .as_ref()
17415 .expect("Should have the context menu deployed");
17416 match context_menu {
17417 CodeContextMenu::Completions(completions_menu) => {
17418 let completions = completions_menu.completions.borrow_mut();
17419 assert_eq!(
17420 completions
17421 .iter()
17422 .map(|completion| &completion.label.text)
17423 .collect::<Vec<_>>(),
17424 vec!["id", "other"]
17425 )
17426 }
17427 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17428 }
17429 });
17430 cx.run_until_parked();
17431
17432 cx.update_editor(|editor, window, cx| {
17433 editor.context_menu_next(&ContextMenuNext, window, cx);
17434 });
17435 cx.run_until_parked();
17436 cx.update_editor(|editor, window, cx| {
17437 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17438 });
17439 cx.run_until_parked();
17440 cx.update_editor(|editor, window, cx| {
17441 editor.context_menu_next(&ContextMenuNext, window, cx);
17442 });
17443 cx.run_until_parked();
17444 cx.update_editor(|editor, window, cx| {
17445 editor
17446 .compose_completion(&ComposeCompletion::default(), window, cx)
17447 .expect("No task returned")
17448 })
17449 .await
17450 .expect("Completion failed");
17451 cx.run_until_parked();
17452
17453 cx.update_editor(|editor, _, cx| {
17454 assert_eq!(
17455 resolve_requests_1.load(atomic::Ordering::Acquire),
17456 1,
17457 "Should always resolve once despite multiple selections"
17458 );
17459 assert_eq!(
17460 resolve_requests_2.load(atomic::Ordering::Acquire),
17461 1,
17462 "Should always resolve once after multiple selections and applying the completion"
17463 );
17464 assert_eq!(
17465 editor.text(cx),
17466 "fn main() { let a = ??.other; }",
17467 "Should use resolved data when applying the completion"
17468 );
17469 });
17470}
17471
17472#[gpui::test]
17473async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17474 init_test(cx, |_| {});
17475
17476 let item_0 = lsp::CompletionItem {
17477 label: "abs".into(),
17478 insert_text: Some("abs".into()),
17479 data: Some(json!({ "very": "special"})),
17480 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17481 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17482 lsp::InsertReplaceEdit {
17483 new_text: "abs".to_string(),
17484 insert: lsp::Range::default(),
17485 replace: lsp::Range::default(),
17486 },
17487 )),
17488 ..lsp::CompletionItem::default()
17489 };
17490 let items = iter::once(item_0.clone())
17491 .chain((11..51).map(|i| lsp::CompletionItem {
17492 label: format!("item_{}", i),
17493 insert_text: Some(format!("item_{}", i)),
17494 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17495 ..lsp::CompletionItem::default()
17496 }))
17497 .collect::<Vec<_>>();
17498
17499 let default_commit_characters = vec!["?".to_string()];
17500 let default_data = json!({ "default": "data"});
17501 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17502 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17503 let default_edit_range = lsp::Range {
17504 start: lsp::Position {
17505 line: 0,
17506 character: 5,
17507 },
17508 end: lsp::Position {
17509 line: 0,
17510 character: 5,
17511 },
17512 };
17513
17514 let mut cx = EditorLspTestContext::new_rust(
17515 lsp::ServerCapabilities {
17516 completion_provider: Some(lsp::CompletionOptions {
17517 trigger_characters: Some(vec![".".to_string()]),
17518 resolve_provider: Some(true),
17519 ..Default::default()
17520 }),
17521 ..Default::default()
17522 },
17523 cx,
17524 )
17525 .await;
17526
17527 cx.set_state("fn main() { let a = 2ˇ; }");
17528 cx.simulate_keystroke(".");
17529
17530 let completion_data = default_data.clone();
17531 let completion_characters = default_commit_characters.clone();
17532 let completion_items = items.clone();
17533 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17534 let default_data = completion_data.clone();
17535 let default_commit_characters = completion_characters.clone();
17536 let items = completion_items.clone();
17537 async move {
17538 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17539 items,
17540 item_defaults: Some(lsp::CompletionListItemDefaults {
17541 data: Some(default_data.clone()),
17542 commit_characters: Some(default_commit_characters.clone()),
17543 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17544 default_edit_range,
17545 )),
17546 insert_text_format: Some(default_insert_text_format),
17547 insert_text_mode: Some(default_insert_text_mode),
17548 }),
17549 ..lsp::CompletionList::default()
17550 })))
17551 }
17552 })
17553 .next()
17554 .await;
17555
17556 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17557 cx.lsp
17558 .server
17559 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17560 let closure_resolved_items = resolved_items.clone();
17561 move |item_to_resolve, _| {
17562 let closure_resolved_items = closure_resolved_items.clone();
17563 async move {
17564 closure_resolved_items.lock().push(item_to_resolve.clone());
17565 Ok(item_to_resolve)
17566 }
17567 }
17568 })
17569 .detach();
17570
17571 cx.condition(|editor, _| editor.context_menu_visible())
17572 .await;
17573 cx.run_until_parked();
17574 cx.update_editor(|editor, _, _| {
17575 let menu = editor.context_menu.borrow_mut();
17576 match menu.as_ref().expect("should have the completions menu") {
17577 CodeContextMenu::Completions(completions_menu) => {
17578 assert_eq!(
17579 completions_menu
17580 .entries
17581 .borrow()
17582 .iter()
17583 .map(|mat| mat.string.clone())
17584 .collect::<Vec<String>>(),
17585 items
17586 .iter()
17587 .map(|completion| completion.label.clone())
17588 .collect::<Vec<String>>()
17589 );
17590 }
17591 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17592 }
17593 });
17594 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17595 // with 4 from the end.
17596 assert_eq!(
17597 *resolved_items.lock(),
17598 [&items[0..16], &items[items.len() - 4..items.len()]]
17599 .concat()
17600 .iter()
17601 .cloned()
17602 .map(|mut item| {
17603 if item.data.is_none() {
17604 item.data = Some(default_data.clone());
17605 }
17606 item
17607 })
17608 .collect::<Vec<lsp::CompletionItem>>(),
17609 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17610 );
17611 resolved_items.lock().clear();
17612
17613 cx.update_editor(|editor, window, cx| {
17614 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17615 });
17616 cx.run_until_parked();
17617 // Completions that have already been resolved are skipped.
17618 assert_eq!(
17619 *resolved_items.lock(),
17620 items[items.len() - 17..items.len() - 4]
17621 .iter()
17622 .cloned()
17623 .map(|mut item| {
17624 if item.data.is_none() {
17625 item.data = Some(default_data.clone());
17626 }
17627 item
17628 })
17629 .collect::<Vec<lsp::CompletionItem>>()
17630 );
17631 resolved_items.lock().clear();
17632}
17633
17634#[gpui::test]
17635async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17636 init_test(cx, |_| {});
17637
17638 let mut cx = EditorLspTestContext::new(
17639 Language::new(
17640 LanguageConfig {
17641 matcher: LanguageMatcher {
17642 path_suffixes: vec!["jsx".into()],
17643 ..Default::default()
17644 },
17645 overrides: [(
17646 "element".into(),
17647 LanguageConfigOverride {
17648 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17649 ..Default::default()
17650 },
17651 )]
17652 .into_iter()
17653 .collect(),
17654 ..Default::default()
17655 },
17656 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17657 )
17658 .with_override_query("(jsx_self_closing_element) @element")
17659 .unwrap(),
17660 lsp::ServerCapabilities {
17661 completion_provider: Some(lsp::CompletionOptions {
17662 trigger_characters: Some(vec![":".to_string()]),
17663 ..Default::default()
17664 }),
17665 ..Default::default()
17666 },
17667 cx,
17668 )
17669 .await;
17670
17671 cx.lsp
17672 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17673 Ok(Some(lsp::CompletionResponse::Array(vec![
17674 lsp::CompletionItem {
17675 label: "bg-blue".into(),
17676 ..Default::default()
17677 },
17678 lsp::CompletionItem {
17679 label: "bg-red".into(),
17680 ..Default::default()
17681 },
17682 lsp::CompletionItem {
17683 label: "bg-yellow".into(),
17684 ..Default::default()
17685 },
17686 ])))
17687 });
17688
17689 cx.set_state(r#"<p class="bgˇ" />"#);
17690
17691 // Trigger completion when typing a dash, because the dash is an extra
17692 // word character in the 'element' scope, which contains the cursor.
17693 cx.simulate_keystroke("-");
17694 cx.executor().run_until_parked();
17695 cx.update_editor(|editor, _, _| {
17696 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17697 {
17698 assert_eq!(
17699 completion_menu_entries(menu),
17700 &["bg-blue", "bg-red", "bg-yellow"]
17701 );
17702 } else {
17703 panic!("expected completion menu to be open");
17704 }
17705 });
17706
17707 cx.simulate_keystroke("l");
17708 cx.executor().run_until_parked();
17709 cx.update_editor(|editor, _, _| {
17710 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17711 {
17712 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17713 } else {
17714 panic!("expected completion menu to be open");
17715 }
17716 });
17717
17718 // When filtering completions, consider the character after the '-' to
17719 // be the start of a subword.
17720 cx.set_state(r#"<p class="yelˇ" />"#);
17721 cx.simulate_keystroke("l");
17722 cx.executor().run_until_parked();
17723 cx.update_editor(|editor, _, _| {
17724 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17725 {
17726 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17727 } else {
17728 panic!("expected completion menu to be open");
17729 }
17730 });
17731}
17732
17733fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17734 let entries = menu.entries.borrow();
17735 entries.iter().map(|mat| mat.string.clone()).collect()
17736}
17737
17738#[gpui::test]
17739async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17740 init_test(cx, |settings| {
17741 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17742 Formatter::Prettier,
17743 )))
17744 });
17745
17746 let fs = FakeFs::new(cx.executor());
17747 fs.insert_file(path!("/file.ts"), Default::default()).await;
17748
17749 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17750 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17751
17752 language_registry.add(Arc::new(Language::new(
17753 LanguageConfig {
17754 name: "TypeScript".into(),
17755 matcher: LanguageMatcher {
17756 path_suffixes: vec!["ts".to_string()],
17757 ..Default::default()
17758 },
17759 ..Default::default()
17760 },
17761 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17762 )));
17763 update_test_language_settings(cx, |settings| {
17764 settings.defaults.prettier = Some(PrettierSettings {
17765 allowed: true,
17766 ..PrettierSettings::default()
17767 });
17768 });
17769
17770 let test_plugin = "test_plugin";
17771 let _ = language_registry.register_fake_lsp(
17772 "TypeScript",
17773 FakeLspAdapter {
17774 prettier_plugins: vec![test_plugin],
17775 ..Default::default()
17776 },
17777 );
17778
17779 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17780 let buffer = project
17781 .update(cx, |project, cx| {
17782 project.open_local_buffer(path!("/file.ts"), cx)
17783 })
17784 .await
17785 .unwrap();
17786
17787 let buffer_text = "one\ntwo\nthree\n";
17788 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17789 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17790 editor.update_in(cx, |editor, window, cx| {
17791 editor.set_text(buffer_text, window, cx)
17792 });
17793
17794 editor
17795 .update_in(cx, |editor, window, cx| {
17796 editor.perform_format(
17797 project.clone(),
17798 FormatTrigger::Manual,
17799 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17800 window,
17801 cx,
17802 )
17803 })
17804 .unwrap()
17805 .await;
17806 assert_eq!(
17807 editor.update(cx, |editor, cx| editor.text(cx)),
17808 buffer_text.to_string() + prettier_format_suffix,
17809 "Test prettier formatting was not applied to the original buffer text",
17810 );
17811
17812 update_test_language_settings(cx, |settings| {
17813 settings.defaults.formatter = Some(SelectedFormatter::Auto)
17814 });
17815 let format = editor.update_in(cx, |editor, window, cx| {
17816 editor.perform_format(
17817 project.clone(),
17818 FormatTrigger::Manual,
17819 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17820 window,
17821 cx,
17822 )
17823 });
17824 format.await.unwrap();
17825 assert_eq!(
17826 editor.update(cx, |editor, cx| editor.text(cx)),
17827 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
17828 "Autoformatting (via test prettier) was not applied to the original buffer text",
17829 );
17830}
17831
17832#[gpui::test]
17833async fn test_addition_reverts(cx: &mut TestAppContext) {
17834 init_test(cx, |_| {});
17835 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17836 let base_text = indoc! {r#"
17837 struct Row;
17838 struct Row1;
17839 struct Row2;
17840
17841 struct Row4;
17842 struct Row5;
17843 struct Row6;
17844
17845 struct Row8;
17846 struct Row9;
17847 struct Row10;"#};
17848
17849 // When addition hunks are not adjacent to carets, no hunk revert is performed
17850 assert_hunk_revert(
17851 indoc! {r#"struct Row;
17852 struct Row1;
17853 struct Row1.1;
17854 struct Row1.2;
17855 struct Row2;ˇ
17856
17857 struct Row4;
17858 struct Row5;
17859 struct Row6;
17860
17861 struct Row8;
17862 ˇstruct Row9;
17863 struct Row9.1;
17864 struct Row9.2;
17865 struct Row9.3;
17866 struct Row10;"#},
17867 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17868 indoc! {r#"struct Row;
17869 struct Row1;
17870 struct Row1.1;
17871 struct Row1.2;
17872 struct Row2;ˇ
17873
17874 struct Row4;
17875 struct Row5;
17876 struct Row6;
17877
17878 struct Row8;
17879 ˇstruct Row9;
17880 struct Row9.1;
17881 struct Row9.2;
17882 struct Row9.3;
17883 struct Row10;"#},
17884 base_text,
17885 &mut cx,
17886 );
17887 // Same for selections
17888 assert_hunk_revert(
17889 indoc! {r#"struct Row;
17890 struct Row1;
17891 struct Row2;
17892 struct Row2.1;
17893 struct Row2.2;
17894 «ˇ
17895 struct Row4;
17896 struct» Row5;
17897 «struct Row6;
17898 ˇ»
17899 struct Row9.1;
17900 struct Row9.2;
17901 struct Row9.3;
17902 struct Row8;
17903 struct Row9;
17904 struct Row10;"#},
17905 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17906 indoc! {r#"struct Row;
17907 struct Row1;
17908 struct Row2;
17909 struct Row2.1;
17910 struct Row2.2;
17911 «ˇ
17912 struct Row4;
17913 struct» Row5;
17914 «struct Row6;
17915 ˇ»
17916 struct Row9.1;
17917 struct Row9.2;
17918 struct Row9.3;
17919 struct Row8;
17920 struct Row9;
17921 struct Row10;"#},
17922 base_text,
17923 &mut cx,
17924 );
17925
17926 // When carets and selections intersect the addition hunks, those are reverted.
17927 // Adjacent carets got merged.
17928 assert_hunk_revert(
17929 indoc! {r#"struct Row;
17930 ˇ// something on the top
17931 struct Row1;
17932 struct Row2;
17933 struct Roˇw3.1;
17934 struct Row2.2;
17935 struct Row2.3;ˇ
17936
17937 struct Row4;
17938 struct ˇRow5.1;
17939 struct Row5.2;
17940 struct «Rowˇ»5.3;
17941 struct Row5;
17942 struct Row6;
17943 ˇ
17944 struct Row9.1;
17945 struct «Rowˇ»9.2;
17946 struct «ˇRow»9.3;
17947 struct Row8;
17948 struct Row9;
17949 «ˇ// something on bottom»
17950 struct Row10;"#},
17951 vec![
17952 DiffHunkStatusKind::Added,
17953 DiffHunkStatusKind::Added,
17954 DiffHunkStatusKind::Added,
17955 DiffHunkStatusKind::Added,
17956 DiffHunkStatusKind::Added,
17957 ],
17958 indoc! {r#"struct Row;
17959 ˇstruct Row1;
17960 struct Row2;
17961 ˇ
17962 struct Row4;
17963 ˇstruct Row5;
17964 struct Row6;
17965 ˇ
17966 ˇstruct Row8;
17967 struct Row9;
17968 ˇstruct Row10;"#},
17969 base_text,
17970 &mut cx,
17971 );
17972}
17973
17974#[gpui::test]
17975async fn test_modification_reverts(cx: &mut TestAppContext) {
17976 init_test(cx, |_| {});
17977 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17978 let base_text = indoc! {r#"
17979 struct Row;
17980 struct Row1;
17981 struct Row2;
17982
17983 struct Row4;
17984 struct Row5;
17985 struct Row6;
17986
17987 struct Row8;
17988 struct Row9;
17989 struct Row10;"#};
17990
17991 // Modification hunks behave the same as the addition ones.
17992 assert_hunk_revert(
17993 indoc! {r#"struct Row;
17994 struct Row1;
17995 struct Row33;
17996 ˇ
17997 struct Row4;
17998 struct Row5;
17999 struct Row6;
18000 ˇ
18001 struct Row99;
18002 struct Row9;
18003 struct Row10;"#},
18004 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18005 indoc! {r#"struct Row;
18006 struct Row1;
18007 struct Row33;
18008 ˇ
18009 struct Row4;
18010 struct Row5;
18011 struct Row6;
18012 ˇ
18013 struct Row99;
18014 struct Row9;
18015 struct Row10;"#},
18016 base_text,
18017 &mut cx,
18018 );
18019 assert_hunk_revert(
18020 indoc! {r#"struct Row;
18021 struct Row1;
18022 struct Row33;
18023 «ˇ
18024 struct Row4;
18025 struct» Row5;
18026 «struct Row6;
18027 ˇ»
18028 struct Row99;
18029 struct Row9;
18030 struct Row10;"#},
18031 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18032 indoc! {r#"struct Row;
18033 struct Row1;
18034 struct Row33;
18035 «ˇ
18036 struct Row4;
18037 struct» Row5;
18038 «struct Row6;
18039 ˇ»
18040 struct Row99;
18041 struct Row9;
18042 struct Row10;"#},
18043 base_text,
18044 &mut cx,
18045 );
18046
18047 assert_hunk_revert(
18048 indoc! {r#"ˇstruct Row1.1;
18049 struct Row1;
18050 «ˇstr»uct Row22;
18051
18052 struct ˇRow44;
18053 struct Row5;
18054 struct «Rˇ»ow66;ˇ
18055
18056 «struˇ»ct Row88;
18057 struct Row9;
18058 struct Row1011;ˇ"#},
18059 vec![
18060 DiffHunkStatusKind::Modified,
18061 DiffHunkStatusKind::Modified,
18062 DiffHunkStatusKind::Modified,
18063 DiffHunkStatusKind::Modified,
18064 DiffHunkStatusKind::Modified,
18065 DiffHunkStatusKind::Modified,
18066 ],
18067 indoc! {r#"struct Row;
18068 ˇstruct Row1;
18069 struct Row2;
18070 ˇ
18071 struct Row4;
18072 ˇstruct Row5;
18073 struct Row6;
18074 ˇ
18075 struct Row8;
18076 ˇstruct Row9;
18077 struct Row10;ˇ"#},
18078 base_text,
18079 &mut cx,
18080 );
18081}
18082
18083#[gpui::test]
18084async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18085 init_test(cx, |_| {});
18086 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18087 let base_text = indoc! {r#"
18088 one
18089
18090 two
18091 three
18092 "#};
18093
18094 cx.set_head_text(base_text);
18095 cx.set_state("\nˇ\n");
18096 cx.executor().run_until_parked();
18097 cx.update_editor(|editor, _window, cx| {
18098 editor.expand_selected_diff_hunks(cx);
18099 });
18100 cx.executor().run_until_parked();
18101 cx.update_editor(|editor, window, cx| {
18102 editor.backspace(&Default::default(), window, cx);
18103 });
18104 cx.run_until_parked();
18105 cx.assert_state_with_diff(
18106 indoc! {r#"
18107
18108 - two
18109 - threeˇ
18110 +
18111 "#}
18112 .to_string(),
18113 );
18114}
18115
18116#[gpui::test]
18117async fn test_deletion_reverts(cx: &mut TestAppContext) {
18118 init_test(cx, |_| {});
18119 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18120 let base_text = indoc! {r#"struct Row;
18121struct Row1;
18122struct Row2;
18123
18124struct Row4;
18125struct Row5;
18126struct Row6;
18127
18128struct Row8;
18129struct Row9;
18130struct Row10;"#};
18131
18132 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18133 assert_hunk_revert(
18134 indoc! {r#"struct Row;
18135 struct Row2;
18136
18137 ˇstruct Row4;
18138 struct Row5;
18139 struct Row6;
18140 ˇ
18141 struct Row8;
18142 struct Row10;"#},
18143 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18144 indoc! {r#"struct Row;
18145 struct Row2;
18146
18147 ˇstruct Row4;
18148 struct Row5;
18149 struct Row6;
18150 ˇ
18151 struct Row8;
18152 struct Row10;"#},
18153 base_text,
18154 &mut cx,
18155 );
18156 assert_hunk_revert(
18157 indoc! {r#"struct Row;
18158 struct Row2;
18159
18160 «ˇstruct Row4;
18161 struct» Row5;
18162 «struct Row6;
18163 ˇ»
18164 struct Row8;
18165 struct Row10;"#},
18166 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18167 indoc! {r#"struct Row;
18168 struct Row2;
18169
18170 «ˇstruct Row4;
18171 struct» Row5;
18172 «struct Row6;
18173 ˇ»
18174 struct Row8;
18175 struct Row10;"#},
18176 base_text,
18177 &mut cx,
18178 );
18179
18180 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18181 assert_hunk_revert(
18182 indoc! {r#"struct Row;
18183 ˇstruct Row2;
18184
18185 struct Row4;
18186 struct Row5;
18187 struct Row6;
18188
18189 struct Row8;ˇ
18190 struct Row10;"#},
18191 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18192 indoc! {r#"struct Row;
18193 struct Row1;
18194 ˇstruct Row2;
18195
18196 struct Row4;
18197 struct Row5;
18198 struct Row6;
18199
18200 struct Row8;ˇ
18201 struct Row9;
18202 struct Row10;"#},
18203 base_text,
18204 &mut cx,
18205 );
18206 assert_hunk_revert(
18207 indoc! {r#"struct Row;
18208 struct Row2«ˇ;
18209 struct Row4;
18210 struct» Row5;
18211 «struct Row6;
18212
18213 struct Row8;ˇ»
18214 struct Row10;"#},
18215 vec![
18216 DiffHunkStatusKind::Deleted,
18217 DiffHunkStatusKind::Deleted,
18218 DiffHunkStatusKind::Deleted,
18219 ],
18220 indoc! {r#"struct Row;
18221 struct Row1;
18222 struct Row2«ˇ;
18223
18224 struct Row4;
18225 struct» Row5;
18226 «struct Row6;
18227
18228 struct Row8;ˇ»
18229 struct Row9;
18230 struct Row10;"#},
18231 base_text,
18232 &mut cx,
18233 );
18234}
18235
18236#[gpui::test]
18237async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18238 init_test(cx, |_| {});
18239
18240 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18241 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18242 let base_text_3 =
18243 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18244
18245 let text_1 = edit_first_char_of_every_line(base_text_1);
18246 let text_2 = edit_first_char_of_every_line(base_text_2);
18247 let text_3 = edit_first_char_of_every_line(base_text_3);
18248
18249 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18250 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18251 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18252
18253 let multibuffer = cx.new(|cx| {
18254 let mut multibuffer = MultiBuffer::new(ReadWrite);
18255 multibuffer.push_excerpts(
18256 buffer_1.clone(),
18257 [
18258 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18259 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18260 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18261 ],
18262 cx,
18263 );
18264 multibuffer.push_excerpts(
18265 buffer_2.clone(),
18266 [
18267 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18268 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18269 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18270 ],
18271 cx,
18272 );
18273 multibuffer.push_excerpts(
18274 buffer_3.clone(),
18275 [
18276 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18277 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18278 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18279 ],
18280 cx,
18281 );
18282 multibuffer
18283 });
18284
18285 let fs = FakeFs::new(cx.executor());
18286 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18287 let (editor, cx) = cx
18288 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18289 editor.update_in(cx, |editor, _window, cx| {
18290 for (buffer, diff_base) in [
18291 (buffer_1.clone(), base_text_1),
18292 (buffer_2.clone(), base_text_2),
18293 (buffer_3.clone(), base_text_3),
18294 ] {
18295 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18296 editor
18297 .buffer
18298 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18299 }
18300 });
18301 cx.executor().run_until_parked();
18302
18303 editor.update_in(cx, |editor, window, cx| {
18304 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}");
18305 editor.select_all(&SelectAll, window, cx);
18306 editor.git_restore(&Default::default(), window, cx);
18307 });
18308 cx.executor().run_until_parked();
18309
18310 // When all ranges are selected, all buffer hunks are reverted.
18311 editor.update(cx, |editor, cx| {
18312 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");
18313 });
18314 buffer_1.update(cx, |buffer, _| {
18315 assert_eq!(buffer.text(), base_text_1);
18316 });
18317 buffer_2.update(cx, |buffer, _| {
18318 assert_eq!(buffer.text(), base_text_2);
18319 });
18320 buffer_3.update(cx, |buffer, _| {
18321 assert_eq!(buffer.text(), base_text_3);
18322 });
18323
18324 editor.update_in(cx, |editor, window, cx| {
18325 editor.undo(&Default::default(), window, cx);
18326 });
18327
18328 editor.update_in(cx, |editor, window, cx| {
18329 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18330 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18331 });
18332 editor.git_restore(&Default::default(), window, cx);
18333 });
18334
18335 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18336 // but not affect buffer_2 and its related excerpts.
18337 editor.update(cx, |editor, cx| {
18338 assert_eq!(
18339 editor.text(cx),
18340 "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}"
18341 );
18342 });
18343 buffer_1.update(cx, |buffer, _| {
18344 assert_eq!(buffer.text(), base_text_1);
18345 });
18346 buffer_2.update(cx, |buffer, _| {
18347 assert_eq!(
18348 buffer.text(),
18349 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18350 );
18351 });
18352 buffer_3.update(cx, |buffer, _| {
18353 assert_eq!(
18354 buffer.text(),
18355 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18356 );
18357 });
18358
18359 fn edit_first_char_of_every_line(text: &str) -> String {
18360 text.split('\n')
18361 .map(|line| format!("X{}", &line[1..]))
18362 .collect::<Vec<_>>()
18363 .join("\n")
18364 }
18365}
18366
18367#[gpui::test]
18368async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18369 init_test(cx, |_| {});
18370
18371 let cols = 4;
18372 let rows = 10;
18373 let sample_text_1 = sample_text(rows, cols, 'a');
18374 assert_eq!(
18375 sample_text_1,
18376 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18377 );
18378 let sample_text_2 = sample_text(rows, cols, 'l');
18379 assert_eq!(
18380 sample_text_2,
18381 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18382 );
18383 let sample_text_3 = sample_text(rows, cols, 'v');
18384 assert_eq!(
18385 sample_text_3,
18386 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18387 );
18388
18389 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18390 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18391 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18392
18393 let multi_buffer = cx.new(|cx| {
18394 let mut multibuffer = MultiBuffer::new(ReadWrite);
18395 multibuffer.push_excerpts(
18396 buffer_1.clone(),
18397 [
18398 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18399 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18400 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18401 ],
18402 cx,
18403 );
18404 multibuffer.push_excerpts(
18405 buffer_2.clone(),
18406 [
18407 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18408 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18409 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18410 ],
18411 cx,
18412 );
18413 multibuffer.push_excerpts(
18414 buffer_3.clone(),
18415 [
18416 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18417 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18418 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18419 ],
18420 cx,
18421 );
18422 multibuffer
18423 });
18424
18425 let fs = FakeFs::new(cx.executor());
18426 fs.insert_tree(
18427 "/a",
18428 json!({
18429 "main.rs": sample_text_1,
18430 "other.rs": sample_text_2,
18431 "lib.rs": sample_text_3,
18432 }),
18433 )
18434 .await;
18435 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18436 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18437 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18438 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18439 Editor::new(
18440 EditorMode::full(),
18441 multi_buffer,
18442 Some(project.clone()),
18443 window,
18444 cx,
18445 )
18446 });
18447 let multibuffer_item_id = workspace
18448 .update(cx, |workspace, window, cx| {
18449 assert!(
18450 workspace.active_item(cx).is_none(),
18451 "active item should be None before the first item is added"
18452 );
18453 workspace.add_item_to_active_pane(
18454 Box::new(multi_buffer_editor.clone()),
18455 None,
18456 true,
18457 window,
18458 cx,
18459 );
18460 let active_item = workspace
18461 .active_item(cx)
18462 .expect("should have an active item after adding the multi buffer");
18463 assert!(
18464 !active_item.is_singleton(cx),
18465 "A multi buffer was expected to active after adding"
18466 );
18467 active_item.item_id()
18468 })
18469 .unwrap();
18470 cx.executor().run_until_parked();
18471
18472 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18473 editor.change_selections(
18474 SelectionEffects::scroll(Autoscroll::Next),
18475 window,
18476 cx,
18477 |s| s.select_ranges(Some(1..2)),
18478 );
18479 editor.open_excerpts(&OpenExcerpts, window, cx);
18480 });
18481 cx.executor().run_until_parked();
18482 let first_item_id = workspace
18483 .update(cx, |workspace, window, cx| {
18484 let active_item = workspace
18485 .active_item(cx)
18486 .expect("should have an active item after navigating into the 1st buffer");
18487 let first_item_id = active_item.item_id();
18488 assert_ne!(
18489 first_item_id, multibuffer_item_id,
18490 "Should navigate into the 1st buffer and activate it"
18491 );
18492 assert!(
18493 active_item.is_singleton(cx),
18494 "New active item should be a singleton buffer"
18495 );
18496 assert_eq!(
18497 active_item
18498 .act_as::<Editor>(cx)
18499 .expect("should have navigated into an editor for the 1st buffer")
18500 .read(cx)
18501 .text(cx),
18502 sample_text_1
18503 );
18504
18505 workspace
18506 .go_back(workspace.active_pane().downgrade(), window, cx)
18507 .detach_and_log_err(cx);
18508
18509 first_item_id
18510 })
18511 .unwrap();
18512 cx.executor().run_until_parked();
18513 workspace
18514 .update(cx, |workspace, _, cx| {
18515 let active_item = workspace
18516 .active_item(cx)
18517 .expect("should have an active item after navigating back");
18518 assert_eq!(
18519 active_item.item_id(),
18520 multibuffer_item_id,
18521 "Should navigate back to the multi buffer"
18522 );
18523 assert!(!active_item.is_singleton(cx));
18524 })
18525 .unwrap();
18526
18527 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18528 editor.change_selections(
18529 SelectionEffects::scroll(Autoscroll::Next),
18530 window,
18531 cx,
18532 |s| s.select_ranges(Some(39..40)),
18533 );
18534 editor.open_excerpts(&OpenExcerpts, window, cx);
18535 });
18536 cx.executor().run_until_parked();
18537 let second_item_id = workspace
18538 .update(cx, |workspace, window, cx| {
18539 let active_item = workspace
18540 .active_item(cx)
18541 .expect("should have an active item after navigating into the 2nd buffer");
18542 let second_item_id = active_item.item_id();
18543 assert_ne!(
18544 second_item_id, multibuffer_item_id,
18545 "Should navigate away from the multibuffer"
18546 );
18547 assert_ne!(
18548 second_item_id, first_item_id,
18549 "Should navigate into the 2nd buffer and activate it"
18550 );
18551 assert!(
18552 active_item.is_singleton(cx),
18553 "New active item should be a singleton buffer"
18554 );
18555 assert_eq!(
18556 active_item
18557 .act_as::<Editor>(cx)
18558 .expect("should have navigated into an editor")
18559 .read(cx)
18560 .text(cx),
18561 sample_text_2
18562 );
18563
18564 workspace
18565 .go_back(workspace.active_pane().downgrade(), window, cx)
18566 .detach_and_log_err(cx);
18567
18568 second_item_id
18569 })
18570 .unwrap();
18571 cx.executor().run_until_parked();
18572 workspace
18573 .update(cx, |workspace, _, cx| {
18574 let active_item = workspace
18575 .active_item(cx)
18576 .expect("should have an active item after navigating back from the 2nd buffer");
18577 assert_eq!(
18578 active_item.item_id(),
18579 multibuffer_item_id,
18580 "Should navigate back from the 2nd buffer to the multi buffer"
18581 );
18582 assert!(!active_item.is_singleton(cx));
18583 })
18584 .unwrap();
18585
18586 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18587 editor.change_selections(
18588 SelectionEffects::scroll(Autoscroll::Next),
18589 window,
18590 cx,
18591 |s| s.select_ranges(Some(70..70)),
18592 );
18593 editor.open_excerpts(&OpenExcerpts, window, cx);
18594 });
18595 cx.executor().run_until_parked();
18596 workspace
18597 .update(cx, |workspace, window, cx| {
18598 let active_item = workspace
18599 .active_item(cx)
18600 .expect("should have an active item after navigating into the 3rd buffer");
18601 let third_item_id = active_item.item_id();
18602 assert_ne!(
18603 third_item_id, multibuffer_item_id,
18604 "Should navigate into the 3rd buffer and activate it"
18605 );
18606 assert_ne!(third_item_id, first_item_id);
18607 assert_ne!(third_item_id, second_item_id);
18608 assert!(
18609 active_item.is_singleton(cx),
18610 "New active item should be a singleton buffer"
18611 );
18612 assert_eq!(
18613 active_item
18614 .act_as::<Editor>(cx)
18615 .expect("should have navigated into an editor")
18616 .read(cx)
18617 .text(cx),
18618 sample_text_3
18619 );
18620
18621 workspace
18622 .go_back(workspace.active_pane().downgrade(), window, cx)
18623 .detach_and_log_err(cx);
18624 })
18625 .unwrap();
18626 cx.executor().run_until_parked();
18627 workspace
18628 .update(cx, |workspace, _, cx| {
18629 let active_item = workspace
18630 .active_item(cx)
18631 .expect("should have an active item after navigating back from the 3rd buffer");
18632 assert_eq!(
18633 active_item.item_id(),
18634 multibuffer_item_id,
18635 "Should navigate back from the 3rd buffer to the multi buffer"
18636 );
18637 assert!(!active_item.is_singleton(cx));
18638 })
18639 .unwrap();
18640}
18641
18642#[gpui::test]
18643async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18644 init_test(cx, |_| {});
18645
18646 let mut cx = EditorTestContext::new(cx).await;
18647
18648 let diff_base = r#"
18649 use some::mod;
18650
18651 const A: u32 = 42;
18652
18653 fn main() {
18654 println!("hello");
18655
18656 println!("world");
18657 }
18658 "#
18659 .unindent();
18660
18661 cx.set_state(
18662 &r#"
18663 use some::modified;
18664
18665 ˇ
18666 fn main() {
18667 println!("hello there");
18668
18669 println!("around the");
18670 println!("world");
18671 }
18672 "#
18673 .unindent(),
18674 );
18675
18676 cx.set_head_text(&diff_base);
18677 executor.run_until_parked();
18678
18679 cx.update_editor(|editor, window, cx| {
18680 editor.go_to_next_hunk(&GoToHunk, window, cx);
18681 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18682 });
18683 executor.run_until_parked();
18684 cx.assert_state_with_diff(
18685 r#"
18686 use some::modified;
18687
18688
18689 fn main() {
18690 - println!("hello");
18691 + ˇ println!("hello there");
18692
18693 println!("around the");
18694 println!("world");
18695 }
18696 "#
18697 .unindent(),
18698 );
18699
18700 cx.update_editor(|editor, window, cx| {
18701 for _ in 0..2 {
18702 editor.go_to_next_hunk(&GoToHunk, window, cx);
18703 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18704 }
18705 });
18706 executor.run_until_parked();
18707 cx.assert_state_with_diff(
18708 r#"
18709 - use some::mod;
18710 + ˇuse some::modified;
18711
18712
18713 fn main() {
18714 - println!("hello");
18715 + println!("hello there");
18716
18717 + println!("around the");
18718 println!("world");
18719 }
18720 "#
18721 .unindent(),
18722 );
18723
18724 cx.update_editor(|editor, window, cx| {
18725 editor.go_to_next_hunk(&GoToHunk, window, cx);
18726 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18727 });
18728 executor.run_until_parked();
18729 cx.assert_state_with_diff(
18730 r#"
18731 - use some::mod;
18732 + use some::modified;
18733
18734 - const A: u32 = 42;
18735 ˇ
18736 fn main() {
18737 - println!("hello");
18738 + println!("hello there");
18739
18740 + println!("around the");
18741 println!("world");
18742 }
18743 "#
18744 .unindent(),
18745 );
18746
18747 cx.update_editor(|editor, window, cx| {
18748 editor.cancel(&Cancel, window, cx);
18749 });
18750
18751 cx.assert_state_with_diff(
18752 r#"
18753 use some::modified;
18754
18755 ˇ
18756 fn main() {
18757 println!("hello there");
18758
18759 println!("around the");
18760 println!("world");
18761 }
18762 "#
18763 .unindent(),
18764 );
18765}
18766
18767#[gpui::test]
18768async fn test_diff_base_change_with_expanded_diff_hunks(
18769 executor: BackgroundExecutor,
18770 cx: &mut TestAppContext,
18771) {
18772 init_test(cx, |_| {});
18773
18774 let mut cx = EditorTestContext::new(cx).await;
18775
18776 let diff_base = r#"
18777 use some::mod1;
18778 use some::mod2;
18779
18780 const A: u32 = 42;
18781 const B: u32 = 42;
18782 const C: u32 = 42;
18783
18784 fn main() {
18785 println!("hello");
18786
18787 println!("world");
18788 }
18789 "#
18790 .unindent();
18791
18792 cx.set_state(
18793 &r#"
18794 use some::mod2;
18795
18796 const A: u32 = 42;
18797 const C: u32 = 42;
18798
18799 fn main(ˇ) {
18800 //println!("hello");
18801
18802 println!("world");
18803 //
18804 //
18805 }
18806 "#
18807 .unindent(),
18808 );
18809
18810 cx.set_head_text(&diff_base);
18811 executor.run_until_parked();
18812
18813 cx.update_editor(|editor, window, cx| {
18814 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18815 });
18816 executor.run_until_parked();
18817 cx.assert_state_with_diff(
18818 r#"
18819 - use some::mod1;
18820 use some::mod2;
18821
18822 const A: u32 = 42;
18823 - const B: u32 = 42;
18824 const C: u32 = 42;
18825
18826 fn main(ˇ) {
18827 - println!("hello");
18828 + //println!("hello");
18829
18830 println!("world");
18831 + //
18832 + //
18833 }
18834 "#
18835 .unindent(),
18836 );
18837
18838 cx.set_head_text("new diff base!");
18839 executor.run_until_parked();
18840 cx.assert_state_with_diff(
18841 r#"
18842 - new diff base!
18843 + use some::mod2;
18844 +
18845 + const A: u32 = 42;
18846 + const C: u32 = 42;
18847 +
18848 + fn main(ˇ) {
18849 + //println!("hello");
18850 +
18851 + println!("world");
18852 + //
18853 + //
18854 + }
18855 "#
18856 .unindent(),
18857 );
18858}
18859
18860#[gpui::test]
18861async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
18862 init_test(cx, |_| {});
18863
18864 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18865 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18866 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18867 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18868 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
18869 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
18870
18871 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
18872 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
18873 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
18874
18875 let multi_buffer = cx.new(|cx| {
18876 let mut multibuffer = MultiBuffer::new(ReadWrite);
18877 multibuffer.push_excerpts(
18878 buffer_1.clone(),
18879 [
18880 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18881 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18882 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18883 ],
18884 cx,
18885 );
18886 multibuffer.push_excerpts(
18887 buffer_2.clone(),
18888 [
18889 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18890 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18891 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18892 ],
18893 cx,
18894 );
18895 multibuffer.push_excerpts(
18896 buffer_3.clone(),
18897 [
18898 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18899 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18900 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18901 ],
18902 cx,
18903 );
18904 multibuffer
18905 });
18906
18907 let editor =
18908 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18909 editor
18910 .update(cx, |editor, _window, cx| {
18911 for (buffer, diff_base) in [
18912 (buffer_1.clone(), file_1_old),
18913 (buffer_2.clone(), file_2_old),
18914 (buffer_3.clone(), file_3_old),
18915 ] {
18916 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18917 editor
18918 .buffer
18919 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18920 }
18921 })
18922 .unwrap();
18923
18924 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18925 cx.run_until_parked();
18926
18927 cx.assert_editor_state(
18928 &"
18929 ˇaaa
18930 ccc
18931 ddd
18932
18933 ggg
18934 hhh
18935
18936
18937 lll
18938 mmm
18939 NNN
18940
18941 qqq
18942 rrr
18943
18944 uuu
18945 111
18946 222
18947 333
18948
18949 666
18950 777
18951
18952 000
18953 !!!"
18954 .unindent(),
18955 );
18956
18957 cx.update_editor(|editor, window, cx| {
18958 editor.select_all(&SelectAll, window, cx);
18959 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18960 });
18961 cx.executor().run_until_parked();
18962
18963 cx.assert_state_with_diff(
18964 "
18965 «aaa
18966 - bbb
18967 ccc
18968 ddd
18969
18970 ggg
18971 hhh
18972
18973
18974 lll
18975 mmm
18976 - nnn
18977 + NNN
18978
18979 qqq
18980 rrr
18981
18982 uuu
18983 111
18984 222
18985 333
18986
18987 + 666
18988 777
18989
18990 000
18991 !!!ˇ»"
18992 .unindent(),
18993 );
18994}
18995
18996#[gpui::test]
18997async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18998 init_test(cx, |_| {});
18999
19000 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19001 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19002
19003 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19004 let multi_buffer = cx.new(|cx| {
19005 let mut multibuffer = MultiBuffer::new(ReadWrite);
19006 multibuffer.push_excerpts(
19007 buffer.clone(),
19008 [
19009 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19010 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19011 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19012 ],
19013 cx,
19014 );
19015 multibuffer
19016 });
19017
19018 let editor =
19019 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19020 editor
19021 .update(cx, |editor, _window, cx| {
19022 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19023 editor
19024 .buffer
19025 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19026 })
19027 .unwrap();
19028
19029 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19030 cx.run_until_parked();
19031
19032 cx.update_editor(|editor, window, cx| {
19033 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19034 });
19035 cx.executor().run_until_parked();
19036
19037 // When the start of a hunk coincides with the start of its excerpt,
19038 // the hunk is expanded. When the start of a a hunk is earlier than
19039 // the start of its excerpt, the hunk is not expanded.
19040 cx.assert_state_with_diff(
19041 "
19042 ˇaaa
19043 - bbb
19044 + BBB
19045
19046 - ddd
19047 - eee
19048 + DDD
19049 + EEE
19050 fff
19051
19052 iii
19053 "
19054 .unindent(),
19055 );
19056}
19057
19058#[gpui::test]
19059async fn test_edits_around_expanded_insertion_hunks(
19060 executor: BackgroundExecutor,
19061 cx: &mut TestAppContext,
19062) {
19063 init_test(cx, |_| {});
19064
19065 let mut cx = EditorTestContext::new(cx).await;
19066
19067 let diff_base = r#"
19068 use some::mod1;
19069 use some::mod2;
19070
19071 const A: u32 = 42;
19072
19073 fn main() {
19074 println!("hello");
19075
19076 println!("world");
19077 }
19078 "#
19079 .unindent();
19080 executor.run_until_parked();
19081 cx.set_state(
19082 &r#"
19083 use some::mod1;
19084 use some::mod2;
19085
19086 const A: u32 = 42;
19087 const B: u32 = 42;
19088 const C: u32 = 42;
19089 ˇ
19090
19091 fn main() {
19092 println!("hello");
19093
19094 println!("world");
19095 }
19096 "#
19097 .unindent(),
19098 );
19099
19100 cx.set_head_text(&diff_base);
19101 executor.run_until_parked();
19102
19103 cx.update_editor(|editor, window, cx| {
19104 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19105 });
19106 executor.run_until_parked();
19107
19108 cx.assert_state_with_diff(
19109 r#"
19110 use some::mod1;
19111 use some::mod2;
19112
19113 const A: u32 = 42;
19114 + const B: u32 = 42;
19115 + const C: u32 = 42;
19116 + ˇ
19117
19118 fn main() {
19119 println!("hello");
19120
19121 println!("world");
19122 }
19123 "#
19124 .unindent(),
19125 );
19126
19127 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19128 executor.run_until_parked();
19129
19130 cx.assert_state_with_diff(
19131 r#"
19132 use some::mod1;
19133 use some::mod2;
19134
19135 const A: u32 = 42;
19136 + const B: u32 = 42;
19137 + const C: u32 = 42;
19138 + const D: u32 = 42;
19139 + ˇ
19140
19141 fn main() {
19142 println!("hello");
19143
19144 println!("world");
19145 }
19146 "#
19147 .unindent(),
19148 );
19149
19150 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19151 executor.run_until_parked();
19152
19153 cx.assert_state_with_diff(
19154 r#"
19155 use some::mod1;
19156 use some::mod2;
19157
19158 const A: u32 = 42;
19159 + const B: u32 = 42;
19160 + const C: u32 = 42;
19161 + const D: u32 = 42;
19162 + const E: u32 = 42;
19163 + ˇ
19164
19165 fn main() {
19166 println!("hello");
19167
19168 println!("world");
19169 }
19170 "#
19171 .unindent(),
19172 );
19173
19174 cx.update_editor(|editor, window, cx| {
19175 editor.delete_line(&DeleteLine, window, cx);
19176 });
19177 executor.run_until_parked();
19178
19179 cx.assert_state_with_diff(
19180 r#"
19181 use some::mod1;
19182 use some::mod2;
19183
19184 const A: u32 = 42;
19185 + const B: u32 = 42;
19186 + const C: u32 = 42;
19187 + const D: u32 = 42;
19188 + const E: u32 = 42;
19189 ˇ
19190 fn main() {
19191 println!("hello");
19192
19193 println!("world");
19194 }
19195 "#
19196 .unindent(),
19197 );
19198
19199 cx.update_editor(|editor, window, cx| {
19200 editor.move_up(&MoveUp, window, cx);
19201 editor.delete_line(&DeleteLine, window, cx);
19202 editor.move_up(&MoveUp, window, cx);
19203 editor.delete_line(&DeleteLine, window, cx);
19204 editor.move_up(&MoveUp, window, cx);
19205 editor.delete_line(&DeleteLine, window, cx);
19206 });
19207 executor.run_until_parked();
19208 cx.assert_state_with_diff(
19209 r#"
19210 use some::mod1;
19211 use some::mod2;
19212
19213 const A: u32 = 42;
19214 + const B: u32 = 42;
19215 ˇ
19216 fn main() {
19217 println!("hello");
19218
19219 println!("world");
19220 }
19221 "#
19222 .unindent(),
19223 );
19224
19225 cx.update_editor(|editor, window, cx| {
19226 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19227 editor.delete_line(&DeleteLine, window, cx);
19228 });
19229 executor.run_until_parked();
19230 cx.assert_state_with_diff(
19231 r#"
19232 ˇ
19233 fn main() {
19234 println!("hello");
19235
19236 println!("world");
19237 }
19238 "#
19239 .unindent(),
19240 );
19241}
19242
19243#[gpui::test]
19244async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19245 init_test(cx, |_| {});
19246
19247 let mut cx = EditorTestContext::new(cx).await;
19248 cx.set_head_text(indoc! { "
19249 one
19250 two
19251 three
19252 four
19253 five
19254 "
19255 });
19256 cx.set_state(indoc! { "
19257 one
19258 ˇthree
19259 five
19260 "});
19261 cx.run_until_parked();
19262 cx.update_editor(|editor, window, cx| {
19263 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19264 });
19265 cx.assert_state_with_diff(
19266 indoc! { "
19267 one
19268 - two
19269 ˇthree
19270 - four
19271 five
19272 "}
19273 .to_string(),
19274 );
19275 cx.update_editor(|editor, window, cx| {
19276 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19277 });
19278
19279 cx.assert_state_with_diff(
19280 indoc! { "
19281 one
19282 ˇthree
19283 five
19284 "}
19285 .to_string(),
19286 );
19287
19288 cx.set_state(indoc! { "
19289 one
19290 ˇTWO
19291 three
19292 four
19293 five
19294 "});
19295 cx.run_until_parked();
19296 cx.update_editor(|editor, window, cx| {
19297 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19298 });
19299
19300 cx.assert_state_with_diff(
19301 indoc! { "
19302 one
19303 - two
19304 + ˇTWO
19305 three
19306 four
19307 five
19308 "}
19309 .to_string(),
19310 );
19311 cx.update_editor(|editor, window, cx| {
19312 editor.move_up(&Default::default(), window, cx);
19313 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19314 });
19315 cx.assert_state_with_diff(
19316 indoc! { "
19317 one
19318 ˇTWO
19319 three
19320 four
19321 five
19322 "}
19323 .to_string(),
19324 );
19325}
19326
19327#[gpui::test]
19328async fn test_edits_around_expanded_deletion_hunks(
19329 executor: BackgroundExecutor,
19330 cx: &mut TestAppContext,
19331) {
19332 init_test(cx, |_| {});
19333
19334 let mut cx = EditorTestContext::new(cx).await;
19335
19336 let diff_base = r#"
19337 use some::mod1;
19338 use some::mod2;
19339
19340 const A: u32 = 42;
19341 const B: u32 = 42;
19342 const C: u32 = 42;
19343
19344
19345 fn main() {
19346 println!("hello");
19347
19348 println!("world");
19349 }
19350 "#
19351 .unindent();
19352 executor.run_until_parked();
19353 cx.set_state(
19354 &r#"
19355 use some::mod1;
19356 use some::mod2;
19357
19358 ˇconst B: u32 = 42;
19359 const C: u32 = 42;
19360
19361
19362 fn main() {
19363 println!("hello");
19364
19365 println!("world");
19366 }
19367 "#
19368 .unindent(),
19369 );
19370
19371 cx.set_head_text(&diff_base);
19372 executor.run_until_parked();
19373
19374 cx.update_editor(|editor, window, cx| {
19375 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19376 });
19377 executor.run_until_parked();
19378
19379 cx.assert_state_with_diff(
19380 r#"
19381 use some::mod1;
19382 use some::mod2;
19383
19384 - const A: u32 = 42;
19385 ˇconst B: u32 = 42;
19386 const C: u32 = 42;
19387
19388
19389 fn main() {
19390 println!("hello");
19391
19392 println!("world");
19393 }
19394 "#
19395 .unindent(),
19396 );
19397
19398 cx.update_editor(|editor, window, cx| {
19399 editor.delete_line(&DeleteLine, window, cx);
19400 });
19401 executor.run_until_parked();
19402 cx.assert_state_with_diff(
19403 r#"
19404 use some::mod1;
19405 use some::mod2;
19406
19407 - const A: u32 = 42;
19408 - const B: u32 = 42;
19409 ˇconst C: u32 = 42;
19410
19411
19412 fn main() {
19413 println!("hello");
19414
19415 println!("world");
19416 }
19417 "#
19418 .unindent(),
19419 );
19420
19421 cx.update_editor(|editor, window, cx| {
19422 editor.delete_line(&DeleteLine, window, cx);
19423 });
19424 executor.run_until_parked();
19425 cx.assert_state_with_diff(
19426 r#"
19427 use some::mod1;
19428 use some::mod2;
19429
19430 - const A: u32 = 42;
19431 - const B: u32 = 42;
19432 - const C: u32 = 42;
19433 ˇ
19434
19435 fn main() {
19436 println!("hello");
19437
19438 println!("world");
19439 }
19440 "#
19441 .unindent(),
19442 );
19443
19444 cx.update_editor(|editor, window, cx| {
19445 editor.handle_input("replacement", window, cx);
19446 });
19447 executor.run_until_parked();
19448 cx.assert_state_with_diff(
19449 r#"
19450 use some::mod1;
19451 use some::mod2;
19452
19453 - const A: u32 = 42;
19454 - const B: u32 = 42;
19455 - const C: u32 = 42;
19456 -
19457 + replacementˇ
19458
19459 fn main() {
19460 println!("hello");
19461
19462 println!("world");
19463 }
19464 "#
19465 .unindent(),
19466 );
19467}
19468
19469#[gpui::test]
19470async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19471 init_test(cx, |_| {});
19472
19473 let mut cx = EditorTestContext::new(cx).await;
19474
19475 let base_text = r#"
19476 one
19477 two
19478 three
19479 four
19480 five
19481 "#
19482 .unindent();
19483 executor.run_until_parked();
19484 cx.set_state(
19485 &r#"
19486 one
19487 two
19488 fˇour
19489 five
19490 "#
19491 .unindent(),
19492 );
19493
19494 cx.set_head_text(&base_text);
19495 executor.run_until_parked();
19496
19497 cx.update_editor(|editor, window, cx| {
19498 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19499 });
19500 executor.run_until_parked();
19501
19502 cx.assert_state_with_diff(
19503 r#"
19504 one
19505 two
19506 - three
19507 fˇour
19508 five
19509 "#
19510 .unindent(),
19511 );
19512
19513 cx.update_editor(|editor, window, cx| {
19514 editor.backspace(&Backspace, window, cx);
19515 editor.backspace(&Backspace, window, cx);
19516 });
19517 executor.run_until_parked();
19518 cx.assert_state_with_diff(
19519 r#"
19520 one
19521 two
19522 - threeˇ
19523 - four
19524 + our
19525 five
19526 "#
19527 .unindent(),
19528 );
19529}
19530
19531#[gpui::test]
19532async fn test_edit_after_expanded_modification_hunk(
19533 executor: BackgroundExecutor,
19534 cx: &mut TestAppContext,
19535) {
19536 init_test(cx, |_| {});
19537
19538 let mut cx = EditorTestContext::new(cx).await;
19539
19540 let diff_base = r#"
19541 use some::mod1;
19542 use some::mod2;
19543
19544 const A: u32 = 42;
19545 const B: u32 = 42;
19546 const C: u32 = 42;
19547 const D: u32 = 42;
19548
19549
19550 fn main() {
19551 println!("hello");
19552
19553 println!("world");
19554 }"#
19555 .unindent();
19556
19557 cx.set_state(
19558 &r#"
19559 use some::mod1;
19560 use some::mod2;
19561
19562 const A: u32 = 42;
19563 const B: u32 = 42;
19564 const C: u32 = 43ˇ
19565 const D: u32 = 42;
19566
19567
19568 fn main() {
19569 println!("hello");
19570
19571 println!("world");
19572 }"#
19573 .unindent(),
19574 );
19575
19576 cx.set_head_text(&diff_base);
19577 executor.run_until_parked();
19578 cx.update_editor(|editor, window, cx| {
19579 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19580 });
19581 executor.run_until_parked();
19582
19583 cx.assert_state_with_diff(
19584 r#"
19585 use some::mod1;
19586 use some::mod2;
19587
19588 const A: u32 = 42;
19589 const B: u32 = 42;
19590 - const C: u32 = 42;
19591 + const C: u32 = 43ˇ
19592 const D: u32 = 42;
19593
19594
19595 fn main() {
19596 println!("hello");
19597
19598 println!("world");
19599 }"#
19600 .unindent(),
19601 );
19602
19603 cx.update_editor(|editor, window, cx| {
19604 editor.handle_input("\nnew_line\n", window, cx);
19605 });
19606 executor.run_until_parked();
19607
19608 cx.assert_state_with_diff(
19609 r#"
19610 use some::mod1;
19611 use some::mod2;
19612
19613 const A: u32 = 42;
19614 const B: u32 = 42;
19615 - const C: u32 = 42;
19616 + const C: u32 = 43
19617 + new_line
19618 + ˇ
19619 const D: u32 = 42;
19620
19621
19622 fn main() {
19623 println!("hello");
19624
19625 println!("world");
19626 }"#
19627 .unindent(),
19628 );
19629}
19630
19631#[gpui::test]
19632async fn test_stage_and_unstage_added_file_hunk(
19633 executor: BackgroundExecutor,
19634 cx: &mut TestAppContext,
19635) {
19636 init_test(cx, |_| {});
19637
19638 let mut cx = EditorTestContext::new(cx).await;
19639 cx.update_editor(|editor, _, cx| {
19640 editor.set_expand_all_diff_hunks(cx);
19641 });
19642
19643 let working_copy = r#"
19644 ˇfn main() {
19645 println!("hello, world!");
19646 }
19647 "#
19648 .unindent();
19649
19650 cx.set_state(&working_copy);
19651 executor.run_until_parked();
19652
19653 cx.assert_state_with_diff(
19654 r#"
19655 + ˇfn main() {
19656 + println!("hello, world!");
19657 + }
19658 "#
19659 .unindent(),
19660 );
19661 cx.assert_index_text(None);
19662
19663 cx.update_editor(|editor, window, cx| {
19664 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19665 });
19666 executor.run_until_parked();
19667 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19668 cx.assert_state_with_diff(
19669 r#"
19670 + ˇfn main() {
19671 + println!("hello, world!");
19672 + }
19673 "#
19674 .unindent(),
19675 );
19676
19677 cx.update_editor(|editor, window, cx| {
19678 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19679 });
19680 executor.run_until_parked();
19681 cx.assert_index_text(None);
19682}
19683
19684async fn setup_indent_guides_editor(
19685 text: &str,
19686 cx: &mut TestAppContext,
19687) -> (BufferId, EditorTestContext) {
19688 init_test(cx, |_| {});
19689
19690 let mut cx = EditorTestContext::new(cx).await;
19691
19692 let buffer_id = cx.update_editor(|editor, window, cx| {
19693 editor.set_text(text, window, cx);
19694 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19695
19696 buffer_ids[0]
19697 });
19698
19699 (buffer_id, cx)
19700}
19701
19702fn assert_indent_guides(
19703 range: Range<u32>,
19704 expected: Vec<IndentGuide>,
19705 active_indices: Option<Vec<usize>>,
19706 cx: &mut EditorTestContext,
19707) {
19708 let indent_guides = cx.update_editor(|editor, window, cx| {
19709 let snapshot = editor.snapshot(window, cx).display_snapshot;
19710 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19711 editor,
19712 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19713 true,
19714 &snapshot,
19715 cx,
19716 );
19717
19718 indent_guides.sort_by(|a, b| {
19719 a.depth.cmp(&b.depth).then(
19720 a.start_row
19721 .cmp(&b.start_row)
19722 .then(a.end_row.cmp(&b.end_row)),
19723 )
19724 });
19725 indent_guides
19726 });
19727
19728 if let Some(expected) = active_indices {
19729 let active_indices = cx.update_editor(|editor, window, cx| {
19730 let snapshot = editor.snapshot(window, cx).display_snapshot;
19731 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19732 });
19733
19734 assert_eq!(
19735 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19736 expected,
19737 "Active indent guide indices do not match"
19738 );
19739 }
19740
19741 assert_eq!(indent_guides, expected, "Indent guides do not match");
19742}
19743
19744fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19745 IndentGuide {
19746 buffer_id,
19747 start_row: MultiBufferRow(start_row),
19748 end_row: MultiBufferRow(end_row),
19749 depth,
19750 tab_size: 4,
19751 settings: IndentGuideSettings {
19752 enabled: true,
19753 line_width: 1,
19754 active_line_width: 1,
19755 ..Default::default()
19756 },
19757 }
19758}
19759
19760#[gpui::test]
19761async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19762 let (buffer_id, mut cx) = setup_indent_guides_editor(
19763 &"
19764 fn main() {
19765 let a = 1;
19766 }"
19767 .unindent(),
19768 cx,
19769 )
19770 .await;
19771
19772 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19773}
19774
19775#[gpui::test]
19776async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19777 let (buffer_id, mut cx) = setup_indent_guides_editor(
19778 &"
19779 fn main() {
19780 let a = 1;
19781 let b = 2;
19782 }"
19783 .unindent(),
19784 cx,
19785 )
19786 .await;
19787
19788 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
19789}
19790
19791#[gpui::test]
19792async fn test_indent_guide_nested(cx: &mut TestAppContext) {
19793 let (buffer_id, mut cx) = setup_indent_guides_editor(
19794 &"
19795 fn main() {
19796 let a = 1;
19797 if a == 3 {
19798 let b = 2;
19799 } else {
19800 let c = 3;
19801 }
19802 }"
19803 .unindent(),
19804 cx,
19805 )
19806 .await;
19807
19808 assert_indent_guides(
19809 0..8,
19810 vec![
19811 indent_guide(buffer_id, 1, 6, 0),
19812 indent_guide(buffer_id, 3, 3, 1),
19813 indent_guide(buffer_id, 5, 5, 1),
19814 ],
19815 None,
19816 &mut cx,
19817 );
19818}
19819
19820#[gpui::test]
19821async fn test_indent_guide_tab(cx: &mut TestAppContext) {
19822 let (buffer_id, mut cx) = setup_indent_guides_editor(
19823 &"
19824 fn main() {
19825 let a = 1;
19826 let b = 2;
19827 let c = 3;
19828 }"
19829 .unindent(),
19830 cx,
19831 )
19832 .await;
19833
19834 assert_indent_guides(
19835 0..5,
19836 vec![
19837 indent_guide(buffer_id, 1, 3, 0),
19838 indent_guide(buffer_id, 2, 2, 1),
19839 ],
19840 None,
19841 &mut cx,
19842 );
19843}
19844
19845#[gpui::test]
19846async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
19847 let (buffer_id, mut cx) = setup_indent_guides_editor(
19848 &"
19849 fn main() {
19850 let a = 1;
19851
19852 let c = 3;
19853 }"
19854 .unindent(),
19855 cx,
19856 )
19857 .await;
19858
19859 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
19860}
19861
19862#[gpui::test]
19863async fn test_indent_guide_complex(cx: &mut TestAppContext) {
19864 let (buffer_id, mut cx) = setup_indent_guides_editor(
19865 &"
19866 fn main() {
19867 let a = 1;
19868
19869 let c = 3;
19870
19871 if a == 3 {
19872 let b = 2;
19873 } else {
19874 let c = 3;
19875 }
19876 }"
19877 .unindent(),
19878 cx,
19879 )
19880 .await;
19881
19882 assert_indent_guides(
19883 0..11,
19884 vec![
19885 indent_guide(buffer_id, 1, 9, 0),
19886 indent_guide(buffer_id, 6, 6, 1),
19887 indent_guide(buffer_id, 8, 8, 1),
19888 ],
19889 None,
19890 &mut cx,
19891 );
19892}
19893
19894#[gpui::test]
19895async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
19896 let (buffer_id, mut cx) = setup_indent_guides_editor(
19897 &"
19898 fn main() {
19899 let a = 1;
19900
19901 let c = 3;
19902
19903 if a == 3 {
19904 let b = 2;
19905 } else {
19906 let c = 3;
19907 }
19908 }"
19909 .unindent(),
19910 cx,
19911 )
19912 .await;
19913
19914 assert_indent_guides(
19915 1..11,
19916 vec![
19917 indent_guide(buffer_id, 1, 9, 0),
19918 indent_guide(buffer_id, 6, 6, 1),
19919 indent_guide(buffer_id, 8, 8, 1),
19920 ],
19921 None,
19922 &mut cx,
19923 );
19924}
19925
19926#[gpui::test]
19927async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
19928 let (buffer_id, mut cx) = setup_indent_guides_editor(
19929 &"
19930 fn main() {
19931 let a = 1;
19932
19933 let c = 3;
19934
19935 if a == 3 {
19936 let b = 2;
19937 } else {
19938 let c = 3;
19939 }
19940 }"
19941 .unindent(),
19942 cx,
19943 )
19944 .await;
19945
19946 assert_indent_guides(
19947 1..10,
19948 vec![
19949 indent_guide(buffer_id, 1, 9, 0),
19950 indent_guide(buffer_id, 6, 6, 1),
19951 indent_guide(buffer_id, 8, 8, 1),
19952 ],
19953 None,
19954 &mut cx,
19955 );
19956}
19957
19958#[gpui::test]
19959async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
19960 let (buffer_id, mut cx) = setup_indent_guides_editor(
19961 &"
19962 fn main() {
19963 if a {
19964 b(
19965 c,
19966 d,
19967 )
19968 } else {
19969 e(
19970 f
19971 )
19972 }
19973 }"
19974 .unindent(),
19975 cx,
19976 )
19977 .await;
19978
19979 assert_indent_guides(
19980 0..11,
19981 vec![
19982 indent_guide(buffer_id, 1, 10, 0),
19983 indent_guide(buffer_id, 2, 5, 1),
19984 indent_guide(buffer_id, 7, 9, 1),
19985 indent_guide(buffer_id, 3, 4, 2),
19986 indent_guide(buffer_id, 8, 8, 2),
19987 ],
19988 None,
19989 &mut cx,
19990 );
19991
19992 cx.update_editor(|editor, window, cx| {
19993 editor.fold_at(MultiBufferRow(2), window, cx);
19994 assert_eq!(
19995 editor.display_text(cx),
19996 "
19997 fn main() {
19998 if a {
19999 b(⋯
20000 )
20001 } else {
20002 e(
20003 f
20004 )
20005 }
20006 }"
20007 .unindent()
20008 );
20009 });
20010
20011 assert_indent_guides(
20012 0..11,
20013 vec![
20014 indent_guide(buffer_id, 1, 10, 0),
20015 indent_guide(buffer_id, 2, 5, 1),
20016 indent_guide(buffer_id, 7, 9, 1),
20017 indent_guide(buffer_id, 8, 8, 2),
20018 ],
20019 None,
20020 &mut cx,
20021 );
20022}
20023
20024#[gpui::test]
20025async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20026 let (buffer_id, mut cx) = setup_indent_guides_editor(
20027 &"
20028 block1
20029 block2
20030 block3
20031 block4
20032 block2
20033 block1
20034 block1"
20035 .unindent(),
20036 cx,
20037 )
20038 .await;
20039
20040 assert_indent_guides(
20041 1..10,
20042 vec![
20043 indent_guide(buffer_id, 1, 4, 0),
20044 indent_guide(buffer_id, 2, 3, 1),
20045 indent_guide(buffer_id, 3, 3, 2),
20046 ],
20047 None,
20048 &mut cx,
20049 );
20050}
20051
20052#[gpui::test]
20053async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20054 let (buffer_id, mut cx) = setup_indent_guides_editor(
20055 &"
20056 block1
20057 block2
20058 block3
20059
20060 block1
20061 block1"
20062 .unindent(),
20063 cx,
20064 )
20065 .await;
20066
20067 assert_indent_guides(
20068 0..6,
20069 vec![
20070 indent_guide(buffer_id, 1, 2, 0),
20071 indent_guide(buffer_id, 2, 2, 1),
20072 ],
20073 None,
20074 &mut cx,
20075 );
20076}
20077
20078#[gpui::test]
20079async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20080 let (buffer_id, mut cx) = setup_indent_guides_editor(
20081 &"
20082 function component() {
20083 \treturn (
20084 \t\t\t
20085 \t\t<div>
20086 \t\t\t<abc></abc>
20087 \t\t</div>
20088 \t)
20089 }"
20090 .unindent(),
20091 cx,
20092 )
20093 .await;
20094
20095 assert_indent_guides(
20096 0..8,
20097 vec![
20098 indent_guide(buffer_id, 1, 6, 0),
20099 indent_guide(buffer_id, 2, 5, 1),
20100 indent_guide(buffer_id, 4, 4, 2),
20101 ],
20102 None,
20103 &mut cx,
20104 );
20105}
20106
20107#[gpui::test]
20108async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20109 let (buffer_id, mut cx) = setup_indent_guides_editor(
20110 &"
20111 function component() {
20112 \treturn (
20113 \t
20114 \t\t<div>
20115 \t\t\t<abc></abc>
20116 \t\t</div>
20117 \t)
20118 }"
20119 .unindent(),
20120 cx,
20121 )
20122 .await;
20123
20124 assert_indent_guides(
20125 0..8,
20126 vec![
20127 indent_guide(buffer_id, 1, 6, 0),
20128 indent_guide(buffer_id, 2, 5, 1),
20129 indent_guide(buffer_id, 4, 4, 2),
20130 ],
20131 None,
20132 &mut cx,
20133 );
20134}
20135
20136#[gpui::test]
20137async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20138 let (buffer_id, mut cx) = setup_indent_guides_editor(
20139 &"
20140 block1
20141
20142
20143
20144 block2
20145 "
20146 .unindent(),
20147 cx,
20148 )
20149 .await;
20150
20151 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20152}
20153
20154#[gpui::test]
20155async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20156 let (buffer_id, mut cx) = setup_indent_guides_editor(
20157 &"
20158 def a:
20159 \tb = 3
20160 \tif True:
20161 \t\tc = 4
20162 \t\td = 5
20163 \tprint(b)
20164 "
20165 .unindent(),
20166 cx,
20167 )
20168 .await;
20169
20170 assert_indent_guides(
20171 0..6,
20172 vec![
20173 indent_guide(buffer_id, 1, 5, 0),
20174 indent_guide(buffer_id, 3, 4, 1),
20175 ],
20176 None,
20177 &mut cx,
20178 );
20179}
20180
20181#[gpui::test]
20182async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20183 let (buffer_id, mut cx) = setup_indent_guides_editor(
20184 &"
20185 fn main() {
20186 let a = 1;
20187 }"
20188 .unindent(),
20189 cx,
20190 )
20191 .await;
20192
20193 cx.update_editor(|editor, window, cx| {
20194 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20195 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20196 });
20197 });
20198
20199 assert_indent_guides(
20200 0..3,
20201 vec![indent_guide(buffer_id, 1, 1, 0)],
20202 Some(vec![0]),
20203 &mut cx,
20204 );
20205}
20206
20207#[gpui::test]
20208async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20209 let (buffer_id, mut cx) = setup_indent_guides_editor(
20210 &"
20211 fn main() {
20212 if 1 == 2 {
20213 let a = 1;
20214 }
20215 }"
20216 .unindent(),
20217 cx,
20218 )
20219 .await;
20220
20221 cx.update_editor(|editor, window, cx| {
20222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20223 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20224 });
20225 });
20226
20227 assert_indent_guides(
20228 0..4,
20229 vec![
20230 indent_guide(buffer_id, 1, 3, 0),
20231 indent_guide(buffer_id, 2, 2, 1),
20232 ],
20233 Some(vec![1]),
20234 &mut cx,
20235 );
20236
20237 cx.update_editor(|editor, window, cx| {
20238 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20239 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20240 });
20241 });
20242
20243 assert_indent_guides(
20244 0..4,
20245 vec![
20246 indent_guide(buffer_id, 1, 3, 0),
20247 indent_guide(buffer_id, 2, 2, 1),
20248 ],
20249 Some(vec![1]),
20250 &mut cx,
20251 );
20252
20253 cx.update_editor(|editor, window, cx| {
20254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20255 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20256 });
20257 });
20258
20259 assert_indent_guides(
20260 0..4,
20261 vec![
20262 indent_guide(buffer_id, 1, 3, 0),
20263 indent_guide(buffer_id, 2, 2, 1),
20264 ],
20265 Some(vec![0]),
20266 &mut cx,
20267 );
20268}
20269
20270#[gpui::test]
20271async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20272 let (buffer_id, mut cx) = setup_indent_guides_editor(
20273 &"
20274 fn main() {
20275 let a = 1;
20276
20277 let b = 2;
20278 }"
20279 .unindent(),
20280 cx,
20281 )
20282 .await;
20283
20284 cx.update_editor(|editor, window, cx| {
20285 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20286 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20287 });
20288 });
20289
20290 assert_indent_guides(
20291 0..5,
20292 vec![indent_guide(buffer_id, 1, 3, 0)],
20293 Some(vec![0]),
20294 &mut cx,
20295 );
20296}
20297
20298#[gpui::test]
20299async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20300 let (buffer_id, mut cx) = setup_indent_guides_editor(
20301 &"
20302 def m:
20303 a = 1
20304 pass"
20305 .unindent(),
20306 cx,
20307 )
20308 .await;
20309
20310 cx.update_editor(|editor, window, cx| {
20311 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20312 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20313 });
20314 });
20315
20316 assert_indent_guides(
20317 0..3,
20318 vec![indent_guide(buffer_id, 1, 2, 0)],
20319 Some(vec![0]),
20320 &mut cx,
20321 );
20322}
20323
20324#[gpui::test]
20325async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20326 init_test(cx, |_| {});
20327 let mut cx = EditorTestContext::new(cx).await;
20328 let text = indoc! {
20329 "
20330 impl A {
20331 fn b() {
20332 0;
20333 3;
20334 5;
20335 6;
20336 7;
20337 }
20338 }
20339 "
20340 };
20341 let base_text = indoc! {
20342 "
20343 impl A {
20344 fn b() {
20345 0;
20346 1;
20347 2;
20348 3;
20349 4;
20350 }
20351 fn c() {
20352 5;
20353 6;
20354 7;
20355 }
20356 }
20357 "
20358 };
20359
20360 cx.update_editor(|editor, window, cx| {
20361 editor.set_text(text, window, cx);
20362
20363 editor.buffer().update(cx, |multibuffer, cx| {
20364 let buffer = multibuffer.as_singleton().unwrap();
20365 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20366
20367 multibuffer.set_all_diff_hunks_expanded(cx);
20368 multibuffer.add_diff(diff, cx);
20369
20370 buffer.read(cx).remote_id()
20371 })
20372 });
20373 cx.run_until_parked();
20374
20375 cx.assert_state_with_diff(
20376 indoc! { "
20377 impl A {
20378 fn b() {
20379 0;
20380 - 1;
20381 - 2;
20382 3;
20383 - 4;
20384 - }
20385 - fn c() {
20386 5;
20387 6;
20388 7;
20389 }
20390 }
20391 ˇ"
20392 }
20393 .to_string(),
20394 );
20395
20396 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20397 editor
20398 .snapshot(window, cx)
20399 .buffer_snapshot
20400 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20401 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20402 .collect::<Vec<_>>()
20403 });
20404 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20405 assert_eq!(
20406 actual_guides,
20407 vec![
20408 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20409 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20410 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20411 ]
20412 );
20413}
20414
20415#[gpui::test]
20416async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20417 init_test(cx, |_| {});
20418 let mut cx = EditorTestContext::new(cx).await;
20419
20420 let diff_base = r#"
20421 a
20422 b
20423 c
20424 "#
20425 .unindent();
20426
20427 cx.set_state(
20428 &r#"
20429 ˇA
20430 b
20431 C
20432 "#
20433 .unindent(),
20434 );
20435 cx.set_head_text(&diff_base);
20436 cx.update_editor(|editor, window, cx| {
20437 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20438 });
20439 executor.run_until_parked();
20440
20441 let both_hunks_expanded = r#"
20442 - a
20443 + ˇA
20444 b
20445 - c
20446 + C
20447 "#
20448 .unindent();
20449
20450 cx.assert_state_with_diff(both_hunks_expanded.clone());
20451
20452 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20453 let snapshot = editor.snapshot(window, cx);
20454 let hunks = editor
20455 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20456 .collect::<Vec<_>>();
20457 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20458 let buffer_id = hunks[0].buffer_id;
20459 hunks
20460 .into_iter()
20461 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20462 .collect::<Vec<_>>()
20463 });
20464 assert_eq!(hunk_ranges.len(), 2);
20465
20466 cx.update_editor(|editor, _, cx| {
20467 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20468 });
20469 executor.run_until_parked();
20470
20471 let second_hunk_expanded = r#"
20472 ˇA
20473 b
20474 - c
20475 + C
20476 "#
20477 .unindent();
20478
20479 cx.assert_state_with_diff(second_hunk_expanded);
20480
20481 cx.update_editor(|editor, _, cx| {
20482 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20483 });
20484 executor.run_until_parked();
20485
20486 cx.assert_state_with_diff(both_hunks_expanded.clone());
20487
20488 cx.update_editor(|editor, _, cx| {
20489 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20490 });
20491 executor.run_until_parked();
20492
20493 let first_hunk_expanded = r#"
20494 - a
20495 + ˇA
20496 b
20497 C
20498 "#
20499 .unindent();
20500
20501 cx.assert_state_with_diff(first_hunk_expanded);
20502
20503 cx.update_editor(|editor, _, cx| {
20504 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20505 });
20506 executor.run_until_parked();
20507
20508 cx.assert_state_with_diff(both_hunks_expanded);
20509
20510 cx.set_state(
20511 &r#"
20512 ˇA
20513 b
20514 "#
20515 .unindent(),
20516 );
20517 cx.run_until_parked();
20518
20519 // TODO this cursor position seems bad
20520 cx.assert_state_with_diff(
20521 r#"
20522 - ˇa
20523 + A
20524 b
20525 "#
20526 .unindent(),
20527 );
20528
20529 cx.update_editor(|editor, window, cx| {
20530 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20531 });
20532
20533 cx.assert_state_with_diff(
20534 r#"
20535 - ˇa
20536 + A
20537 b
20538 - c
20539 "#
20540 .unindent(),
20541 );
20542
20543 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20544 let snapshot = editor.snapshot(window, cx);
20545 let hunks = editor
20546 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20547 .collect::<Vec<_>>();
20548 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20549 let buffer_id = hunks[0].buffer_id;
20550 hunks
20551 .into_iter()
20552 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20553 .collect::<Vec<_>>()
20554 });
20555 assert_eq!(hunk_ranges.len(), 2);
20556
20557 cx.update_editor(|editor, _, cx| {
20558 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20559 });
20560 executor.run_until_parked();
20561
20562 cx.assert_state_with_diff(
20563 r#"
20564 - ˇa
20565 + A
20566 b
20567 "#
20568 .unindent(),
20569 );
20570}
20571
20572#[gpui::test]
20573async fn test_toggle_deletion_hunk_at_start_of_file(
20574 executor: BackgroundExecutor,
20575 cx: &mut TestAppContext,
20576) {
20577 init_test(cx, |_| {});
20578 let mut cx = EditorTestContext::new(cx).await;
20579
20580 let diff_base = r#"
20581 a
20582 b
20583 c
20584 "#
20585 .unindent();
20586
20587 cx.set_state(
20588 &r#"
20589 ˇb
20590 c
20591 "#
20592 .unindent(),
20593 );
20594 cx.set_head_text(&diff_base);
20595 cx.update_editor(|editor, window, cx| {
20596 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20597 });
20598 executor.run_until_parked();
20599
20600 let hunk_expanded = r#"
20601 - a
20602 ˇb
20603 c
20604 "#
20605 .unindent();
20606
20607 cx.assert_state_with_diff(hunk_expanded.clone());
20608
20609 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20610 let snapshot = editor.snapshot(window, cx);
20611 let hunks = editor
20612 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20613 .collect::<Vec<_>>();
20614 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20615 let buffer_id = hunks[0].buffer_id;
20616 hunks
20617 .into_iter()
20618 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20619 .collect::<Vec<_>>()
20620 });
20621 assert_eq!(hunk_ranges.len(), 1);
20622
20623 cx.update_editor(|editor, _, cx| {
20624 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20625 });
20626 executor.run_until_parked();
20627
20628 let hunk_collapsed = r#"
20629 ˇb
20630 c
20631 "#
20632 .unindent();
20633
20634 cx.assert_state_with_diff(hunk_collapsed);
20635
20636 cx.update_editor(|editor, _, cx| {
20637 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20638 });
20639 executor.run_until_parked();
20640
20641 cx.assert_state_with_diff(hunk_expanded);
20642}
20643
20644#[gpui::test]
20645async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20646 init_test(cx, |_| {});
20647
20648 let fs = FakeFs::new(cx.executor());
20649 fs.insert_tree(
20650 path!("/test"),
20651 json!({
20652 ".git": {},
20653 "file-1": "ONE\n",
20654 "file-2": "TWO\n",
20655 "file-3": "THREE\n",
20656 }),
20657 )
20658 .await;
20659
20660 fs.set_head_for_repo(
20661 path!("/test/.git").as_ref(),
20662 &[
20663 ("file-1".into(), "one\n".into()),
20664 ("file-2".into(), "two\n".into()),
20665 ("file-3".into(), "three\n".into()),
20666 ],
20667 "deadbeef",
20668 );
20669
20670 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20671 let mut buffers = vec![];
20672 for i in 1..=3 {
20673 let buffer = project
20674 .update(cx, |project, cx| {
20675 let path = format!(path!("/test/file-{}"), i);
20676 project.open_local_buffer(path, cx)
20677 })
20678 .await
20679 .unwrap();
20680 buffers.push(buffer);
20681 }
20682
20683 let multibuffer = cx.new(|cx| {
20684 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20685 multibuffer.set_all_diff_hunks_expanded(cx);
20686 for buffer in &buffers {
20687 let snapshot = buffer.read(cx).snapshot();
20688 multibuffer.set_excerpts_for_path(
20689 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20690 buffer.clone(),
20691 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20692 2,
20693 cx,
20694 );
20695 }
20696 multibuffer
20697 });
20698
20699 let editor = cx.add_window(|window, cx| {
20700 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20701 });
20702 cx.run_until_parked();
20703
20704 let snapshot = editor
20705 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20706 .unwrap();
20707 let hunks = snapshot
20708 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20709 .map(|hunk| match hunk {
20710 DisplayDiffHunk::Unfolded {
20711 display_row_range, ..
20712 } => display_row_range,
20713 DisplayDiffHunk::Folded { .. } => unreachable!(),
20714 })
20715 .collect::<Vec<_>>();
20716 assert_eq!(
20717 hunks,
20718 [
20719 DisplayRow(2)..DisplayRow(4),
20720 DisplayRow(7)..DisplayRow(9),
20721 DisplayRow(12)..DisplayRow(14),
20722 ]
20723 );
20724}
20725
20726#[gpui::test]
20727async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20728 init_test(cx, |_| {});
20729
20730 let mut cx = EditorTestContext::new(cx).await;
20731 cx.set_head_text(indoc! { "
20732 one
20733 two
20734 three
20735 four
20736 five
20737 "
20738 });
20739 cx.set_index_text(indoc! { "
20740 one
20741 two
20742 three
20743 four
20744 five
20745 "
20746 });
20747 cx.set_state(indoc! {"
20748 one
20749 TWO
20750 ˇTHREE
20751 FOUR
20752 five
20753 "});
20754 cx.run_until_parked();
20755 cx.update_editor(|editor, window, cx| {
20756 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20757 });
20758 cx.run_until_parked();
20759 cx.assert_index_text(Some(indoc! {"
20760 one
20761 TWO
20762 THREE
20763 FOUR
20764 five
20765 "}));
20766 cx.set_state(indoc! { "
20767 one
20768 TWO
20769 ˇTHREE-HUNDRED
20770 FOUR
20771 five
20772 "});
20773 cx.run_until_parked();
20774 cx.update_editor(|editor, window, cx| {
20775 let snapshot = editor.snapshot(window, cx);
20776 let hunks = editor
20777 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20778 .collect::<Vec<_>>();
20779 assert_eq!(hunks.len(), 1);
20780 assert_eq!(
20781 hunks[0].status(),
20782 DiffHunkStatus {
20783 kind: DiffHunkStatusKind::Modified,
20784 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
20785 }
20786 );
20787
20788 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20789 });
20790 cx.run_until_parked();
20791 cx.assert_index_text(Some(indoc! {"
20792 one
20793 TWO
20794 THREE-HUNDRED
20795 FOUR
20796 five
20797 "}));
20798}
20799
20800#[gpui::test]
20801fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
20802 init_test(cx, |_| {});
20803
20804 let editor = cx.add_window(|window, cx| {
20805 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
20806 build_editor(buffer, window, cx)
20807 });
20808
20809 let render_args = Arc::new(Mutex::new(None));
20810 let snapshot = editor
20811 .update(cx, |editor, window, cx| {
20812 let snapshot = editor.buffer().read(cx).snapshot(cx);
20813 let range =
20814 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
20815
20816 struct RenderArgs {
20817 row: MultiBufferRow,
20818 folded: bool,
20819 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
20820 }
20821
20822 let crease = Crease::inline(
20823 range,
20824 FoldPlaceholder::test(),
20825 {
20826 let toggle_callback = render_args.clone();
20827 move |row, folded, callback, _window, _cx| {
20828 *toggle_callback.lock() = Some(RenderArgs {
20829 row,
20830 folded,
20831 callback,
20832 });
20833 div()
20834 }
20835 },
20836 |_row, _folded, _window, _cx| div(),
20837 );
20838
20839 editor.insert_creases(Some(crease), cx);
20840 let snapshot = editor.snapshot(window, cx);
20841 let _div =
20842 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
20843 snapshot
20844 })
20845 .unwrap();
20846
20847 let render_args = render_args.lock().take().unwrap();
20848 assert_eq!(render_args.row, MultiBufferRow(1));
20849 assert!(!render_args.folded);
20850 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20851
20852 cx.update_window(*editor, |_, window, cx| {
20853 (render_args.callback)(true, window, cx)
20854 })
20855 .unwrap();
20856 let snapshot = editor
20857 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20858 .unwrap();
20859 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
20860
20861 cx.update_window(*editor, |_, window, cx| {
20862 (render_args.callback)(false, window, cx)
20863 })
20864 .unwrap();
20865 let snapshot = editor
20866 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20867 .unwrap();
20868 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20869}
20870
20871#[gpui::test]
20872async fn test_input_text(cx: &mut TestAppContext) {
20873 init_test(cx, |_| {});
20874 let mut cx = EditorTestContext::new(cx).await;
20875
20876 cx.set_state(
20877 &r#"ˇone
20878 two
20879
20880 three
20881 fourˇ
20882 five
20883
20884 siˇx"#
20885 .unindent(),
20886 );
20887
20888 cx.dispatch_action(HandleInput(String::new()));
20889 cx.assert_editor_state(
20890 &r#"ˇone
20891 two
20892
20893 three
20894 fourˇ
20895 five
20896
20897 siˇx"#
20898 .unindent(),
20899 );
20900
20901 cx.dispatch_action(HandleInput("AAAA".to_string()));
20902 cx.assert_editor_state(
20903 &r#"AAAAˇone
20904 two
20905
20906 three
20907 fourAAAAˇ
20908 five
20909
20910 siAAAAˇx"#
20911 .unindent(),
20912 );
20913}
20914
20915#[gpui::test]
20916async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
20917 init_test(cx, |_| {});
20918
20919 let mut cx = EditorTestContext::new(cx).await;
20920 cx.set_state(
20921 r#"let foo = 1;
20922let foo = 2;
20923let foo = 3;
20924let fooˇ = 4;
20925let foo = 5;
20926let foo = 6;
20927let foo = 7;
20928let foo = 8;
20929let foo = 9;
20930let foo = 10;
20931let foo = 11;
20932let foo = 12;
20933let foo = 13;
20934let foo = 14;
20935let foo = 15;"#,
20936 );
20937
20938 cx.update_editor(|e, window, cx| {
20939 assert_eq!(
20940 e.next_scroll_position,
20941 NextScrollCursorCenterTopBottom::Center,
20942 "Default next scroll direction is center",
20943 );
20944
20945 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20946 assert_eq!(
20947 e.next_scroll_position,
20948 NextScrollCursorCenterTopBottom::Top,
20949 "After center, next scroll direction should be top",
20950 );
20951
20952 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20953 assert_eq!(
20954 e.next_scroll_position,
20955 NextScrollCursorCenterTopBottom::Bottom,
20956 "After top, next scroll direction should be bottom",
20957 );
20958
20959 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20960 assert_eq!(
20961 e.next_scroll_position,
20962 NextScrollCursorCenterTopBottom::Center,
20963 "After bottom, scrolling should start over",
20964 );
20965
20966 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20967 assert_eq!(
20968 e.next_scroll_position,
20969 NextScrollCursorCenterTopBottom::Top,
20970 "Scrolling continues if retriggered fast enough"
20971 );
20972 });
20973
20974 cx.executor()
20975 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
20976 cx.executor().run_until_parked();
20977 cx.update_editor(|e, _, _| {
20978 assert_eq!(
20979 e.next_scroll_position,
20980 NextScrollCursorCenterTopBottom::Center,
20981 "If scrolling is not triggered fast enough, it should reset"
20982 );
20983 });
20984}
20985
20986#[gpui::test]
20987async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
20988 init_test(cx, |_| {});
20989 let mut cx = EditorLspTestContext::new_rust(
20990 lsp::ServerCapabilities {
20991 definition_provider: Some(lsp::OneOf::Left(true)),
20992 references_provider: Some(lsp::OneOf::Left(true)),
20993 ..lsp::ServerCapabilities::default()
20994 },
20995 cx,
20996 )
20997 .await;
20998
20999 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21000 let go_to_definition = cx
21001 .lsp
21002 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21003 move |params, _| async move {
21004 if empty_go_to_definition {
21005 Ok(None)
21006 } else {
21007 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21008 uri: params.text_document_position_params.text_document.uri,
21009 range: lsp::Range::new(
21010 lsp::Position::new(4, 3),
21011 lsp::Position::new(4, 6),
21012 ),
21013 })))
21014 }
21015 },
21016 );
21017 let references = cx
21018 .lsp
21019 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21020 Ok(Some(vec![lsp::Location {
21021 uri: params.text_document_position.text_document.uri,
21022 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21023 }]))
21024 });
21025 (go_to_definition, references)
21026 };
21027
21028 cx.set_state(
21029 &r#"fn one() {
21030 let mut a = ˇtwo();
21031 }
21032
21033 fn two() {}"#
21034 .unindent(),
21035 );
21036 set_up_lsp_handlers(false, &mut cx);
21037 let navigated = cx
21038 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21039 .await
21040 .expect("Failed to navigate to definition");
21041 assert_eq!(
21042 navigated,
21043 Navigated::Yes,
21044 "Should have navigated to definition from the GetDefinition response"
21045 );
21046 cx.assert_editor_state(
21047 &r#"fn one() {
21048 let mut a = two();
21049 }
21050
21051 fn «twoˇ»() {}"#
21052 .unindent(),
21053 );
21054
21055 let editors = cx.update_workspace(|workspace, _, cx| {
21056 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21057 });
21058 cx.update_editor(|_, _, test_editor_cx| {
21059 assert_eq!(
21060 editors.len(),
21061 1,
21062 "Initially, only one, test, editor should be open in the workspace"
21063 );
21064 assert_eq!(
21065 test_editor_cx.entity(),
21066 editors.last().expect("Asserted len is 1").clone()
21067 );
21068 });
21069
21070 set_up_lsp_handlers(true, &mut cx);
21071 let navigated = cx
21072 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21073 .await
21074 .expect("Failed to navigate to lookup references");
21075 assert_eq!(
21076 navigated,
21077 Navigated::Yes,
21078 "Should have navigated to references as a fallback after empty GoToDefinition response"
21079 );
21080 // We should not change the selections in the existing file,
21081 // if opening another milti buffer with the references
21082 cx.assert_editor_state(
21083 &r#"fn one() {
21084 let mut a = two();
21085 }
21086
21087 fn «twoˇ»() {}"#
21088 .unindent(),
21089 );
21090 let editors = cx.update_workspace(|workspace, _, cx| {
21091 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21092 });
21093 cx.update_editor(|_, _, test_editor_cx| {
21094 assert_eq!(
21095 editors.len(),
21096 2,
21097 "After falling back to references search, we open a new editor with the results"
21098 );
21099 let references_fallback_text = editors
21100 .into_iter()
21101 .find(|new_editor| *new_editor != test_editor_cx.entity())
21102 .expect("Should have one non-test editor now")
21103 .read(test_editor_cx)
21104 .text(test_editor_cx);
21105 assert_eq!(
21106 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21107 "Should use the range from the references response and not the GoToDefinition one"
21108 );
21109 });
21110}
21111
21112#[gpui::test]
21113async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21114 init_test(cx, |_| {});
21115 cx.update(|cx| {
21116 let mut editor_settings = EditorSettings::get_global(cx).clone();
21117 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21118 EditorSettings::override_global(editor_settings, cx);
21119 });
21120 let mut cx = EditorLspTestContext::new_rust(
21121 lsp::ServerCapabilities {
21122 definition_provider: Some(lsp::OneOf::Left(true)),
21123 references_provider: Some(lsp::OneOf::Left(true)),
21124 ..lsp::ServerCapabilities::default()
21125 },
21126 cx,
21127 )
21128 .await;
21129 let original_state = r#"fn one() {
21130 let mut a = ˇtwo();
21131 }
21132
21133 fn two() {}"#
21134 .unindent();
21135 cx.set_state(&original_state);
21136
21137 let mut go_to_definition = cx
21138 .lsp
21139 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21140 move |_, _| async move { Ok(None) },
21141 );
21142 let _references = cx
21143 .lsp
21144 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21145 panic!("Should not call for references with no go to definition fallback")
21146 });
21147
21148 let navigated = cx
21149 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21150 .await
21151 .expect("Failed to navigate to lookup references");
21152 go_to_definition
21153 .next()
21154 .await
21155 .expect("Should have called the go_to_definition handler");
21156
21157 assert_eq!(
21158 navigated,
21159 Navigated::No,
21160 "Should have navigated to references as a fallback after empty GoToDefinition response"
21161 );
21162 cx.assert_editor_state(&original_state);
21163 let editors = cx.update_workspace(|workspace, _, cx| {
21164 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21165 });
21166 cx.update_editor(|_, _, _| {
21167 assert_eq!(
21168 editors.len(),
21169 1,
21170 "After unsuccessful fallback, no other editor should have been opened"
21171 );
21172 });
21173}
21174
21175#[gpui::test]
21176async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21177 init_test(cx, |_| {});
21178
21179 let language = Arc::new(Language::new(
21180 LanguageConfig::default(),
21181 Some(tree_sitter_rust::LANGUAGE.into()),
21182 ));
21183
21184 let text = r#"
21185 #[cfg(test)]
21186 mod tests() {
21187 #[test]
21188 fn runnable_1() {
21189 let a = 1;
21190 }
21191
21192 #[test]
21193 fn runnable_2() {
21194 let a = 1;
21195 let b = 2;
21196 }
21197 }
21198 "#
21199 .unindent();
21200
21201 let fs = FakeFs::new(cx.executor());
21202 fs.insert_file("/file.rs", Default::default()).await;
21203
21204 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21205 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21206 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21207 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21208 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21209
21210 let editor = cx.new_window_entity(|window, cx| {
21211 Editor::new(
21212 EditorMode::full(),
21213 multi_buffer,
21214 Some(project.clone()),
21215 window,
21216 cx,
21217 )
21218 });
21219
21220 editor.update_in(cx, |editor, window, cx| {
21221 let snapshot = editor.buffer().read(cx).snapshot(cx);
21222 editor.tasks.insert(
21223 (buffer.read(cx).remote_id(), 3),
21224 RunnableTasks {
21225 templates: vec![],
21226 offset: snapshot.anchor_before(43),
21227 column: 0,
21228 extra_variables: HashMap::default(),
21229 context_range: BufferOffset(43)..BufferOffset(85),
21230 },
21231 );
21232 editor.tasks.insert(
21233 (buffer.read(cx).remote_id(), 8),
21234 RunnableTasks {
21235 templates: vec![],
21236 offset: snapshot.anchor_before(86),
21237 column: 0,
21238 extra_variables: HashMap::default(),
21239 context_range: BufferOffset(86)..BufferOffset(191),
21240 },
21241 );
21242
21243 // Test finding task when cursor is inside function body
21244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21245 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21246 });
21247 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21248 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21249
21250 // Test finding task when cursor is on function name
21251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21252 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21253 });
21254 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21255 assert_eq!(row, 8, "Should find task when cursor is on function name");
21256 });
21257}
21258
21259#[gpui::test]
21260async fn test_folding_buffers(cx: &mut TestAppContext) {
21261 init_test(cx, |_| {});
21262
21263 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21264 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21265 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21266
21267 let fs = FakeFs::new(cx.executor());
21268 fs.insert_tree(
21269 path!("/a"),
21270 json!({
21271 "first.rs": sample_text_1,
21272 "second.rs": sample_text_2,
21273 "third.rs": sample_text_3,
21274 }),
21275 )
21276 .await;
21277 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21278 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21279 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21280 let worktree = project.update(cx, |project, cx| {
21281 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21282 assert_eq!(worktrees.len(), 1);
21283 worktrees.pop().unwrap()
21284 });
21285 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21286
21287 let buffer_1 = project
21288 .update(cx, |project, cx| {
21289 project.open_buffer((worktree_id, "first.rs"), cx)
21290 })
21291 .await
21292 .unwrap();
21293 let buffer_2 = project
21294 .update(cx, |project, cx| {
21295 project.open_buffer((worktree_id, "second.rs"), cx)
21296 })
21297 .await
21298 .unwrap();
21299 let buffer_3 = project
21300 .update(cx, |project, cx| {
21301 project.open_buffer((worktree_id, "third.rs"), cx)
21302 })
21303 .await
21304 .unwrap();
21305
21306 let multi_buffer = cx.new(|cx| {
21307 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21308 multi_buffer.push_excerpts(
21309 buffer_1.clone(),
21310 [
21311 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21312 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21313 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21314 ],
21315 cx,
21316 );
21317 multi_buffer.push_excerpts(
21318 buffer_2.clone(),
21319 [
21320 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21321 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21322 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21323 ],
21324 cx,
21325 );
21326 multi_buffer.push_excerpts(
21327 buffer_3.clone(),
21328 [
21329 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21330 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21331 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21332 ],
21333 cx,
21334 );
21335 multi_buffer
21336 });
21337 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21338 Editor::new(
21339 EditorMode::full(),
21340 multi_buffer.clone(),
21341 Some(project.clone()),
21342 window,
21343 cx,
21344 )
21345 });
21346
21347 assert_eq!(
21348 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21349 "\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",
21350 );
21351
21352 multi_buffer_editor.update(cx, |editor, cx| {
21353 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21354 });
21355 assert_eq!(
21356 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21357 "\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",
21358 "After folding the first buffer, its text should not be displayed"
21359 );
21360
21361 multi_buffer_editor.update(cx, |editor, cx| {
21362 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21363 });
21364 assert_eq!(
21365 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21366 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21367 "After folding the second buffer, its text should not be displayed"
21368 );
21369
21370 multi_buffer_editor.update(cx, |editor, cx| {
21371 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21372 });
21373 assert_eq!(
21374 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21375 "\n\n\n\n\n",
21376 "After folding the third buffer, its text should not be displayed"
21377 );
21378
21379 // Emulate selection inside the fold logic, that should work
21380 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21381 editor
21382 .snapshot(window, cx)
21383 .next_line_boundary(Point::new(0, 4));
21384 });
21385
21386 multi_buffer_editor.update(cx, |editor, cx| {
21387 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21388 });
21389 assert_eq!(
21390 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21391 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21392 "After unfolding the second buffer, its text should be displayed"
21393 );
21394
21395 // Typing inside of buffer 1 causes that buffer to be unfolded.
21396 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21397 assert_eq!(
21398 multi_buffer
21399 .read(cx)
21400 .snapshot(cx)
21401 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21402 .collect::<String>(),
21403 "bbbb"
21404 );
21405 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21406 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21407 });
21408 editor.handle_input("B", window, cx);
21409 });
21410
21411 assert_eq!(
21412 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21413 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21414 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21415 );
21416
21417 multi_buffer_editor.update(cx, |editor, cx| {
21418 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21419 });
21420 assert_eq!(
21421 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21422 "\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",
21423 "After unfolding the all buffers, all original text should be displayed"
21424 );
21425}
21426
21427#[gpui::test]
21428async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21429 init_test(cx, |_| {});
21430
21431 let sample_text_1 = "1111\n2222\n3333".to_string();
21432 let sample_text_2 = "4444\n5555\n6666".to_string();
21433 let sample_text_3 = "7777\n8888\n9999".to_string();
21434
21435 let fs = FakeFs::new(cx.executor());
21436 fs.insert_tree(
21437 path!("/a"),
21438 json!({
21439 "first.rs": sample_text_1,
21440 "second.rs": sample_text_2,
21441 "third.rs": sample_text_3,
21442 }),
21443 )
21444 .await;
21445 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21446 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21447 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21448 let worktree = project.update(cx, |project, cx| {
21449 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21450 assert_eq!(worktrees.len(), 1);
21451 worktrees.pop().unwrap()
21452 });
21453 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21454
21455 let buffer_1 = project
21456 .update(cx, |project, cx| {
21457 project.open_buffer((worktree_id, "first.rs"), cx)
21458 })
21459 .await
21460 .unwrap();
21461 let buffer_2 = project
21462 .update(cx, |project, cx| {
21463 project.open_buffer((worktree_id, "second.rs"), cx)
21464 })
21465 .await
21466 .unwrap();
21467 let buffer_3 = project
21468 .update(cx, |project, cx| {
21469 project.open_buffer((worktree_id, "third.rs"), cx)
21470 })
21471 .await
21472 .unwrap();
21473
21474 let multi_buffer = cx.new(|cx| {
21475 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21476 multi_buffer.push_excerpts(
21477 buffer_1.clone(),
21478 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21479 cx,
21480 );
21481 multi_buffer.push_excerpts(
21482 buffer_2.clone(),
21483 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21484 cx,
21485 );
21486 multi_buffer.push_excerpts(
21487 buffer_3.clone(),
21488 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21489 cx,
21490 );
21491 multi_buffer
21492 });
21493
21494 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21495 Editor::new(
21496 EditorMode::full(),
21497 multi_buffer,
21498 Some(project.clone()),
21499 window,
21500 cx,
21501 )
21502 });
21503
21504 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21505 assert_eq!(
21506 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21507 full_text,
21508 );
21509
21510 multi_buffer_editor.update(cx, |editor, cx| {
21511 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21512 });
21513 assert_eq!(
21514 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21515 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21516 "After folding the first buffer, its text should not be displayed"
21517 );
21518
21519 multi_buffer_editor.update(cx, |editor, cx| {
21520 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21521 });
21522
21523 assert_eq!(
21524 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21525 "\n\n\n\n\n\n7777\n8888\n9999",
21526 "After folding the second buffer, its text should not be displayed"
21527 );
21528
21529 multi_buffer_editor.update(cx, |editor, cx| {
21530 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21531 });
21532 assert_eq!(
21533 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21534 "\n\n\n\n\n",
21535 "After folding the third buffer, its text should not be displayed"
21536 );
21537
21538 multi_buffer_editor.update(cx, |editor, cx| {
21539 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21540 });
21541 assert_eq!(
21542 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21543 "\n\n\n\n4444\n5555\n6666\n\n",
21544 "After unfolding the second buffer, its text should be displayed"
21545 );
21546
21547 multi_buffer_editor.update(cx, |editor, cx| {
21548 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21549 });
21550 assert_eq!(
21551 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21552 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21553 "After unfolding the first buffer, its text should be displayed"
21554 );
21555
21556 multi_buffer_editor.update(cx, |editor, cx| {
21557 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21558 });
21559 assert_eq!(
21560 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21561 full_text,
21562 "After unfolding all buffers, all original text should be displayed"
21563 );
21564}
21565
21566#[gpui::test]
21567async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21568 init_test(cx, |_| {});
21569
21570 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21571
21572 let fs = FakeFs::new(cx.executor());
21573 fs.insert_tree(
21574 path!("/a"),
21575 json!({
21576 "main.rs": sample_text,
21577 }),
21578 )
21579 .await;
21580 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21581 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21582 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21583 let worktree = project.update(cx, |project, cx| {
21584 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21585 assert_eq!(worktrees.len(), 1);
21586 worktrees.pop().unwrap()
21587 });
21588 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21589
21590 let buffer_1 = project
21591 .update(cx, |project, cx| {
21592 project.open_buffer((worktree_id, "main.rs"), cx)
21593 })
21594 .await
21595 .unwrap();
21596
21597 let multi_buffer = cx.new(|cx| {
21598 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21599 multi_buffer.push_excerpts(
21600 buffer_1.clone(),
21601 [ExcerptRange::new(
21602 Point::new(0, 0)
21603 ..Point::new(
21604 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21605 0,
21606 ),
21607 )],
21608 cx,
21609 );
21610 multi_buffer
21611 });
21612 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21613 Editor::new(
21614 EditorMode::full(),
21615 multi_buffer,
21616 Some(project.clone()),
21617 window,
21618 cx,
21619 )
21620 });
21621
21622 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21623 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21624 enum TestHighlight {}
21625 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21626 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21627 editor.highlight_text::<TestHighlight>(
21628 vec![highlight_range.clone()],
21629 HighlightStyle::color(Hsla::green()),
21630 cx,
21631 );
21632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21633 s.select_ranges(Some(highlight_range))
21634 });
21635 });
21636
21637 let full_text = format!("\n\n{sample_text}");
21638 assert_eq!(
21639 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21640 full_text,
21641 );
21642}
21643
21644#[gpui::test]
21645async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
21646 init_test(cx, |_| {});
21647 cx.update(|cx| {
21648 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
21649 "keymaps/default-linux.json",
21650 cx,
21651 )
21652 .unwrap();
21653 cx.bind_keys(default_key_bindings);
21654 });
21655
21656 let (editor, cx) = cx.add_window_view(|window, cx| {
21657 let multi_buffer = MultiBuffer::build_multi(
21658 [
21659 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
21660 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
21661 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
21662 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
21663 ],
21664 cx,
21665 );
21666 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
21667
21668 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
21669 // fold all but the second buffer, so that we test navigating between two
21670 // adjacent folded buffers, as well as folded buffers at the start and
21671 // end the multibuffer
21672 editor.fold_buffer(buffer_ids[0], cx);
21673 editor.fold_buffer(buffer_ids[2], cx);
21674 editor.fold_buffer(buffer_ids[3], cx);
21675
21676 editor
21677 });
21678 cx.simulate_resize(size(px(1000.), px(1000.)));
21679
21680 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
21681 cx.assert_excerpts_with_selections(indoc! {"
21682 [EXCERPT]
21683 ˇ[FOLDED]
21684 [EXCERPT]
21685 a1
21686 b1
21687 [EXCERPT]
21688 [FOLDED]
21689 [EXCERPT]
21690 [FOLDED]
21691 "
21692 });
21693 cx.simulate_keystroke("down");
21694 cx.assert_excerpts_with_selections(indoc! {"
21695 [EXCERPT]
21696 [FOLDED]
21697 [EXCERPT]
21698 ˇa1
21699 b1
21700 [EXCERPT]
21701 [FOLDED]
21702 [EXCERPT]
21703 [FOLDED]
21704 "
21705 });
21706 cx.simulate_keystroke("down");
21707 cx.assert_excerpts_with_selections(indoc! {"
21708 [EXCERPT]
21709 [FOLDED]
21710 [EXCERPT]
21711 a1
21712 ˇb1
21713 [EXCERPT]
21714 [FOLDED]
21715 [EXCERPT]
21716 [FOLDED]
21717 "
21718 });
21719 cx.simulate_keystroke("down");
21720 cx.assert_excerpts_with_selections(indoc! {"
21721 [EXCERPT]
21722 [FOLDED]
21723 [EXCERPT]
21724 a1
21725 b1
21726 ˇ[EXCERPT]
21727 [FOLDED]
21728 [EXCERPT]
21729 [FOLDED]
21730 "
21731 });
21732 cx.simulate_keystroke("down");
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 for _ in 0..5 {
21746 cx.simulate_keystroke("down");
21747 cx.assert_excerpts_with_selections(indoc! {"
21748 [EXCERPT]
21749 [FOLDED]
21750 [EXCERPT]
21751 a1
21752 b1
21753 [EXCERPT]
21754 [FOLDED]
21755 [EXCERPT]
21756 ˇ[FOLDED]
21757 "
21758 });
21759 }
21760
21761 cx.simulate_keystroke("up");
21762 cx.assert_excerpts_with_selections(indoc! {"
21763 [EXCERPT]
21764 [FOLDED]
21765 [EXCERPT]
21766 a1
21767 b1
21768 [EXCERPT]
21769 ˇ[FOLDED]
21770 [EXCERPT]
21771 [FOLDED]
21772 "
21773 });
21774 cx.simulate_keystroke("up");
21775 cx.assert_excerpts_with_selections(indoc! {"
21776 [EXCERPT]
21777 [FOLDED]
21778 [EXCERPT]
21779 a1
21780 b1
21781 ˇ[EXCERPT]
21782 [FOLDED]
21783 [EXCERPT]
21784 [FOLDED]
21785 "
21786 });
21787 cx.simulate_keystroke("up");
21788 cx.assert_excerpts_with_selections(indoc! {"
21789 [EXCERPT]
21790 [FOLDED]
21791 [EXCERPT]
21792 a1
21793 ˇb1
21794 [EXCERPT]
21795 [FOLDED]
21796 [EXCERPT]
21797 [FOLDED]
21798 "
21799 });
21800 cx.simulate_keystroke("up");
21801 cx.assert_excerpts_with_selections(indoc! {"
21802 [EXCERPT]
21803 [FOLDED]
21804 [EXCERPT]
21805 ˇa1
21806 b1
21807 [EXCERPT]
21808 [FOLDED]
21809 [EXCERPT]
21810 [FOLDED]
21811 "
21812 });
21813 for _ in 0..5 {
21814 cx.simulate_keystroke("up");
21815 cx.assert_excerpts_with_selections(indoc! {"
21816 [EXCERPT]
21817 ˇ[FOLDED]
21818 [EXCERPT]
21819 a1
21820 b1
21821 [EXCERPT]
21822 [FOLDED]
21823 [EXCERPT]
21824 [FOLDED]
21825 "
21826 });
21827 }
21828}
21829
21830#[gpui::test]
21831async fn test_edit_prediction_text(cx: &mut TestAppContext) {
21832 init_test(cx, |_| {});
21833
21834 // Simple insertion
21835 assert_highlighted_edits(
21836 "Hello, world!",
21837 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
21838 true,
21839 cx,
21840 |highlighted_edits, cx| {
21841 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
21842 assert_eq!(highlighted_edits.highlights.len(), 1);
21843 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
21844 assert_eq!(
21845 highlighted_edits.highlights[0].1.background_color,
21846 Some(cx.theme().status().created_background)
21847 );
21848 },
21849 )
21850 .await;
21851
21852 // Replacement
21853 assert_highlighted_edits(
21854 "This is a test.",
21855 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
21856 false,
21857 cx,
21858 |highlighted_edits, cx| {
21859 assert_eq!(highlighted_edits.text, "That is a test.");
21860 assert_eq!(highlighted_edits.highlights.len(), 1);
21861 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
21862 assert_eq!(
21863 highlighted_edits.highlights[0].1.background_color,
21864 Some(cx.theme().status().created_background)
21865 );
21866 },
21867 )
21868 .await;
21869
21870 // Multiple edits
21871 assert_highlighted_edits(
21872 "Hello, world!",
21873 vec![
21874 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
21875 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
21876 ],
21877 false,
21878 cx,
21879 |highlighted_edits, cx| {
21880 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
21881 assert_eq!(highlighted_edits.highlights.len(), 2);
21882 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
21883 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
21884 assert_eq!(
21885 highlighted_edits.highlights[0].1.background_color,
21886 Some(cx.theme().status().created_background)
21887 );
21888 assert_eq!(
21889 highlighted_edits.highlights[1].1.background_color,
21890 Some(cx.theme().status().created_background)
21891 );
21892 },
21893 )
21894 .await;
21895
21896 // Multiple lines with edits
21897 assert_highlighted_edits(
21898 "First line\nSecond line\nThird line\nFourth line",
21899 vec![
21900 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
21901 (
21902 Point::new(2, 0)..Point::new(2, 10),
21903 "New third line".to_string(),
21904 ),
21905 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
21906 ],
21907 false,
21908 cx,
21909 |highlighted_edits, cx| {
21910 assert_eq!(
21911 highlighted_edits.text,
21912 "Second modified\nNew third line\nFourth updated line"
21913 );
21914 assert_eq!(highlighted_edits.highlights.len(), 3);
21915 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
21916 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
21917 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
21918 for highlight in &highlighted_edits.highlights {
21919 assert_eq!(
21920 highlight.1.background_color,
21921 Some(cx.theme().status().created_background)
21922 );
21923 }
21924 },
21925 )
21926 .await;
21927}
21928
21929#[gpui::test]
21930async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
21931 init_test(cx, |_| {});
21932
21933 // Deletion
21934 assert_highlighted_edits(
21935 "Hello, world!",
21936 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
21937 true,
21938 cx,
21939 |highlighted_edits, cx| {
21940 assert_eq!(highlighted_edits.text, "Hello, world!");
21941 assert_eq!(highlighted_edits.highlights.len(), 1);
21942 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
21943 assert_eq!(
21944 highlighted_edits.highlights[0].1.background_color,
21945 Some(cx.theme().status().deleted_background)
21946 );
21947 },
21948 )
21949 .await;
21950
21951 // Insertion
21952 assert_highlighted_edits(
21953 "Hello, world!",
21954 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
21955 true,
21956 cx,
21957 |highlighted_edits, cx| {
21958 assert_eq!(highlighted_edits.highlights.len(), 1);
21959 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
21960 assert_eq!(
21961 highlighted_edits.highlights[0].1.background_color,
21962 Some(cx.theme().status().created_background)
21963 );
21964 },
21965 )
21966 .await;
21967}
21968
21969async fn assert_highlighted_edits(
21970 text: &str,
21971 edits: Vec<(Range<Point>, String)>,
21972 include_deletions: bool,
21973 cx: &mut TestAppContext,
21974 assertion_fn: impl Fn(HighlightedText, &App),
21975) {
21976 let window = cx.add_window(|window, cx| {
21977 let buffer = MultiBuffer::build_simple(text, cx);
21978 Editor::new(EditorMode::full(), buffer, None, window, cx)
21979 });
21980 let cx = &mut VisualTestContext::from_window(*window, cx);
21981
21982 let (buffer, snapshot) = window
21983 .update(cx, |editor, _window, cx| {
21984 (
21985 editor.buffer().clone(),
21986 editor.buffer().read(cx).snapshot(cx),
21987 )
21988 })
21989 .unwrap();
21990
21991 let edits = edits
21992 .into_iter()
21993 .map(|(range, edit)| {
21994 (
21995 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21996 edit,
21997 )
21998 })
21999 .collect::<Vec<_>>();
22000
22001 let text_anchor_edits = edits
22002 .clone()
22003 .into_iter()
22004 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22005 .collect::<Vec<_>>();
22006
22007 let edit_preview = window
22008 .update(cx, |_, _window, cx| {
22009 buffer
22010 .read(cx)
22011 .as_singleton()
22012 .unwrap()
22013 .read(cx)
22014 .preview_edits(text_anchor_edits.into(), cx)
22015 })
22016 .unwrap()
22017 .await;
22018
22019 cx.update(|_window, cx| {
22020 let highlighted_edits = edit_prediction_edit_text(
22021 snapshot.as_singleton().unwrap().2,
22022 &edits,
22023 &edit_preview,
22024 include_deletions,
22025 cx,
22026 );
22027 assertion_fn(highlighted_edits, cx)
22028 });
22029}
22030
22031#[track_caller]
22032fn assert_breakpoint(
22033 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22034 path: &Arc<Path>,
22035 expected: Vec<(u32, Breakpoint)>,
22036) {
22037 if expected.is_empty() {
22038 assert!(!breakpoints.contains_key(path), "{}", path.display());
22039 } else {
22040 let mut breakpoint = breakpoints
22041 .get(path)
22042 .unwrap()
22043 .iter()
22044 .map(|breakpoint| {
22045 (
22046 breakpoint.row,
22047 Breakpoint {
22048 message: breakpoint.message.clone(),
22049 state: breakpoint.state,
22050 condition: breakpoint.condition.clone(),
22051 hit_condition: breakpoint.hit_condition.clone(),
22052 },
22053 )
22054 })
22055 .collect::<Vec<_>>();
22056
22057 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22058
22059 assert_eq!(expected, breakpoint);
22060 }
22061}
22062
22063fn add_log_breakpoint_at_cursor(
22064 editor: &mut Editor,
22065 log_message: &str,
22066 window: &mut Window,
22067 cx: &mut Context<Editor>,
22068) {
22069 let (anchor, bp) = editor
22070 .breakpoints_at_cursors(window, cx)
22071 .first()
22072 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22073 .unwrap_or_else(|| {
22074 let cursor_position: Point = editor.selections.newest(cx).head();
22075
22076 let breakpoint_position = editor
22077 .snapshot(window, cx)
22078 .display_snapshot
22079 .buffer_snapshot
22080 .anchor_before(Point::new(cursor_position.row, 0));
22081
22082 (breakpoint_position, Breakpoint::new_log(log_message))
22083 });
22084
22085 editor.edit_breakpoint_at_anchor(
22086 anchor,
22087 bp,
22088 BreakpointEditAction::EditLogMessage(log_message.into()),
22089 cx,
22090 );
22091}
22092
22093#[gpui::test]
22094async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22095 init_test(cx, |_| {});
22096
22097 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22098 let fs = FakeFs::new(cx.executor());
22099 fs.insert_tree(
22100 path!("/a"),
22101 json!({
22102 "main.rs": sample_text,
22103 }),
22104 )
22105 .await;
22106 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22107 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22108 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22109
22110 let fs = FakeFs::new(cx.executor());
22111 fs.insert_tree(
22112 path!("/a"),
22113 json!({
22114 "main.rs": sample_text,
22115 }),
22116 )
22117 .await;
22118 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22119 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22120 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22121 let worktree_id = workspace
22122 .update(cx, |workspace, _window, cx| {
22123 workspace.project().update(cx, |project, cx| {
22124 project.worktrees(cx).next().unwrap().read(cx).id()
22125 })
22126 })
22127 .unwrap();
22128
22129 let buffer = project
22130 .update(cx, |project, cx| {
22131 project.open_buffer((worktree_id, "main.rs"), cx)
22132 })
22133 .await
22134 .unwrap();
22135
22136 let (editor, cx) = cx.add_window_view(|window, cx| {
22137 Editor::new(
22138 EditorMode::full(),
22139 MultiBuffer::build_from_buffer(buffer, cx),
22140 Some(project.clone()),
22141 window,
22142 cx,
22143 )
22144 });
22145
22146 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22147 let abs_path = project.read_with(cx, |project, cx| {
22148 project
22149 .absolute_path(&project_path, cx)
22150 .map(Arc::from)
22151 .unwrap()
22152 });
22153
22154 // assert we can add breakpoint on the first line
22155 editor.update_in(cx, |editor, window, cx| {
22156 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22157 editor.move_to_end(&MoveToEnd, window, cx);
22158 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22159 });
22160
22161 let breakpoints = editor.update(cx, |editor, cx| {
22162 editor
22163 .breakpoint_store()
22164 .as_ref()
22165 .unwrap()
22166 .read(cx)
22167 .all_source_breakpoints(cx)
22168 });
22169
22170 assert_eq!(1, breakpoints.len());
22171 assert_breakpoint(
22172 &breakpoints,
22173 &abs_path,
22174 vec![
22175 (0, Breakpoint::new_standard()),
22176 (3, Breakpoint::new_standard()),
22177 ],
22178 );
22179
22180 editor.update_in(cx, |editor, window, cx| {
22181 editor.move_to_beginning(&MoveToBeginning, window, cx);
22182 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22183 });
22184
22185 let breakpoints = editor.update(cx, |editor, cx| {
22186 editor
22187 .breakpoint_store()
22188 .as_ref()
22189 .unwrap()
22190 .read(cx)
22191 .all_source_breakpoints(cx)
22192 });
22193
22194 assert_eq!(1, breakpoints.len());
22195 assert_breakpoint(
22196 &breakpoints,
22197 &abs_path,
22198 vec![(3, Breakpoint::new_standard())],
22199 );
22200
22201 editor.update_in(cx, |editor, window, cx| {
22202 editor.move_to_end(&MoveToEnd, window, cx);
22203 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22204 });
22205
22206 let breakpoints = editor.update(cx, |editor, cx| {
22207 editor
22208 .breakpoint_store()
22209 .as_ref()
22210 .unwrap()
22211 .read(cx)
22212 .all_source_breakpoints(cx)
22213 });
22214
22215 assert_eq!(0, breakpoints.len());
22216 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22217}
22218
22219#[gpui::test]
22220async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22221 init_test(cx, |_| {});
22222
22223 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22224
22225 let fs = FakeFs::new(cx.executor());
22226 fs.insert_tree(
22227 path!("/a"),
22228 json!({
22229 "main.rs": sample_text,
22230 }),
22231 )
22232 .await;
22233 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22234 let (workspace, cx) =
22235 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22236
22237 let worktree_id = workspace.update(cx, |workspace, cx| {
22238 workspace.project().update(cx, |project, cx| {
22239 project.worktrees(cx).next().unwrap().read(cx).id()
22240 })
22241 });
22242
22243 let buffer = project
22244 .update(cx, |project, cx| {
22245 project.open_buffer((worktree_id, "main.rs"), cx)
22246 })
22247 .await
22248 .unwrap();
22249
22250 let (editor, cx) = cx.add_window_view(|window, cx| {
22251 Editor::new(
22252 EditorMode::full(),
22253 MultiBuffer::build_from_buffer(buffer, cx),
22254 Some(project.clone()),
22255 window,
22256 cx,
22257 )
22258 });
22259
22260 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22261 let abs_path = project.read_with(cx, |project, cx| {
22262 project
22263 .absolute_path(&project_path, cx)
22264 .map(Arc::from)
22265 .unwrap()
22266 });
22267
22268 editor.update_in(cx, |editor, window, cx| {
22269 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22270 });
22271
22272 let breakpoints = editor.update(cx, |editor, cx| {
22273 editor
22274 .breakpoint_store()
22275 .as_ref()
22276 .unwrap()
22277 .read(cx)
22278 .all_source_breakpoints(cx)
22279 });
22280
22281 assert_breakpoint(
22282 &breakpoints,
22283 &abs_path,
22284 vec![(0, Breakpoint::new_log("hello world"))],
22285 );
22286
22287 // Removing a log message from a log breakpoint should remove it
22288 editor.update_in(cx, |editor, window, cx| {
22289 add_log_breakpoint_at_cursor(editor, "", window, cx);
22290 });
22291
22292 let breakpoints = editor.update(cx, |editor, cx| {
22293 editor
22294 .breakpoint_store()
22295 .as_ref()
22296 .unwrap()
22297 .read(cx)
22298 .all_source_breakpoints(cx)
22299 });
22300
22301 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22302
22303 editor.update_in(cx, |editor, window, cx| {
22304 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22305 editor.move_to_end(&MoveToEnd, window, cx);
22306 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22307 // Not adding a log message to a standard breakpoint shouldn't remove it
22308 add_log_breakpoint_at_cursor(editor, "", window, cx);
22309 });
22310
22311 let breakpoints = editor.update(cx, |editor, cx| {
22312 editor
22313 .breakpoint_store()
22314 .as_ref()
22315 .unwrap()
22316 .read(cx)
22317 .all_source_breakpoints(cx)
22318 });
22319
22320 assert_breakpoint(
22321 &breakpoints,
22322 &abs_path,
22323 vec![
22324 (0, Breakpoint::new_standard()),
22325 (3, Breakpoint::new_standard()),
22326 ],
22327 );
22328
22329 editor.update_in(cx, |editor, window, cx| {
22330 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22331 });
22332
22333 let breakpoints = editor.update(cx, |editor, cx| {
22334 editor
22335 .breakpoint_store()
22336 .as_ref()
22337 .unwrap()
22338 .read(cx)
22339 .all_source_breakpoints(cx)
22340 });
22341
22342 assert_breakpoint(
22343 &breakpoints,
22344 &abs_path,
22345 vec![
22346 (0, Breakpoint::new_standard()),
22347 (3, Breakpoint::new_log("hello world")),
22348 ],
22349 );
22350
22351 editor.update_in(cx, |editor, window, cx| {
22352 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22353 });
22354
22355 let breakpoints = editor.update(cx, |editor, cx| {
22356 editor
22357 .breakpoint_store()
22358 .as_ref()
22359 .unwrap()
22360 .read(cx)
22361 .all_source_breakpoints(cx)
22362 });
22363
22364 assert_breakpoint(
22365 &breakpoints,
22366 &abs_path,
22367 vec![
22368 (0, Breakpoint::new_standard()),
22369 (3, Breakpoint::new_log("hello Earth!!")),
22370 ],
22371 );
22372}
22373
22374/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22375/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22376/// or when breakpoints were placed out of order. This tests for a regression too
22377#[gpui::test]
22378async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22379 init_test(cx, |_| {});
22380
22381 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22382 let fs = FakeFs::new(cx.executor());
22383 fs.insert_tree(
22384 path!("/a"),
22385 json!({
22386 "main.rs": sample_text,
22387 }),
22388 )
22389 .await;
22390 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22391 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22392 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22393
22394 let fs = FakeFs::new(cx.executor());
22395 fs.insert_tree(
22396 path!("/a"),
22397 json!({
22398 "main.rs": sample_text,
22399 }),
22400 )
22401 .await;
22402 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22403 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22404 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22405 let worktree_id = workspace
22406 .update(cx, |workspace, _window, cx| {
22407 workspace.project().update(cx, |project, cx| {
22408 project.worktrees(cx).next().unwrap().read(cx).id()
22409 })
22410 })
22411 .unwrap();
22412
22413 let buffer = project
22414 .update(cx, |project, cx| {
22415 project.open_buffer((worktree_id, "main.rs"), cx)
22416 })
22417 .await
22418 .unwrap();
22419
22420 let (editor, cx) = cx.add_window_view(|window, cx| {
22421 Editor::new(
22422 EditorMode::full(),
22423 MultiBuffer::build_from_buffer(buffer, cx),
22424 Some(project.clone()),
22425 window,
22426 cx,
22427 )
22428 });
22429
22430 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22431 let abs_path = project.read_with(cx, |project, cx| {
22432 project
22433 .absolute_path(&project_path, cx)
22434 .map(Arc::from)
22435 .unwrap()
22436 });
22437
22438 // assert we can add breakpoint on the first line
22439 editor.update_in(cx, |editor, window, cx| {
22440 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22441 editor.move_to_end(&MoveToEnd, window, cx);
22442 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22443 editor.move_up(&MoveUp, window, cx);
22444 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22445 });
22446
22447 let breakpoints = editor.update(cx, |editor, cx| {
22448 editor
22449 .breakpoint_store()
22450 .as_ref()
22451 .unwrap()
22452 .read(cx)
22453 .all_source_breakpoints(cx)
22454 });
22455
22456 assert_eq!(1, breakpoints.len());
22457 assert_breakpoint(
22458 &breakpoints,
22459 &abs_path,
22460 vec![
22461 (0, Breakpoint::new_standard()),
22462 (2, Breakpoint::new_standard()),
22463 (3, Breakpoint::new_standard()),
22464 ],
22465 );
22466
22467 editor.update_in(cx, |editor, window, cx| {
22468 editor.move_to_beginning(&MoveToBeginning, window, cx);
22469 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22470 editor.move_to_end(&MoveToEnd, window, cx);
22471 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22472 // Disabling a breakpoint that doesn't exist should do nothing
22473 editor.move_up(&MoveUp, window, cx);
22474 editor.move_up(&MoveUp, window, cx);
22475 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22476 });
22477
22478 let breakpoints = editor.update(cx, |editor, cx| {
22479 editor
22480 .breakpoint_store()
22481 .as_ref()
22482 .unwrap()
22483 .read(cx)
22484 .all_source_breakpoints(cx)
22485 });
22486
22487 let disable_breakpoint = {
22488 let mut bp = Breakpoint::new_standard();
22489 bp.state = BreakpointState::Disabled;
22490 bp
22491 };
22492
22493 assert_eq!(1, breakpoints.len());
22494 assert_breakpoint(
22495 &breakpoints,
22496 &abs_path,
22497 vec![
22498 (0, disable_breakpoint.clone()),
22499 (2, Breakpoint::new_standard()),
22500 (3, disable_breakpoint.clone()),
22501 ],
22502 );
22503
22504 editor.update_in(cx, |editor, window, cx| {
22505 editor.move_to_beginning(&MoveToBeginning, window, cx);
22506 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22507 editor.move_to_end(&MoveToEnd, window, cx);
22508 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22509 editor.move_up(&MoveUp, window, cx);
22510 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22511 });
22512
22513 let breakpoints = editor.update(cx, |editor, cx| {
22514 editor
22515 .breakpoint_store()
22516 .as_ref()
22517 .unwrap()
22518 .read(cx)
22519 .all_source_breakpoints(cx)
22520 });
22521
22522 assert_eq!(1, breakpoints.len());
22523 assert_breakpoint(
22524 &breakpoints,
22525 &abs_path,
22526 vec![
22527 (0, Breakpoint::new_standard()),
22528 (2, disable_breakpoint),
22529 (3, Breakpoint::new_standard()),
22530 ],
22531 );
22532}
22533
22534#[gpui::test]
22535async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22536 init_test(cx, |_| {});
22537 let capabilities = lsp::ServerCapabilities {
22538 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22539 prepare_provider: Some(true),
22540 work_done_progress_options: Default::default(),
22541 })),
22542 ..Default::default()
22543 };
22544 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22545
22546 cx.set_state(indoc! {"
22547 struct Fˇoo {}
22548 "});
22549
22550 cx.update_editor(|editor, _, cx| {
22551 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22552 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22553 editor.highlight_background::<DocumentHighlightRead>(
22554 &[highlight_range],
22555 |theme| theme.colors().editor_document_highlight_read_background,
22556 cx,
22557 );
22558 });
22559
22560 let mut prepare_rename_handler = cx
22561 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22562 move |_, _, _| async move {
22563 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22564 start: lsp::Position {
22565 line: 0,
22566 character: 7,
22567 },
22568 end: lsp::Position {
22569 line: 0,
22570 character: 10,
22571 },
22572 })))
22573 },
22574 );
22575 let prepare_rename_task = cx
22576 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22577 .expect("Prepare rename was not started");
22578 prepare_rename_handler.next().await.unwrap();
22579 prepare_rename_task.await.expect("Prepare rename failed");
22580
22581 let mut rename_handler =
22582 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22583 let edit = lsp::TextEdit {
22584 range: lsp::Range {
22585 start: lsp::Position {
22586 line: 0,
22587 character: 7,
22588 },
22589 end: lsp::Position {
22590 line: 0,
22591 character: 10,
22592 },
22593 },
22594 new_text: "FooRenamed".to_string(),
22595 };
22596 Ok(Some(lsp::WorkspaceEdit::new(
22597 // Specify the same edit twice
22598 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22599 )))
22600 });
22601 let rename_task = cx
22602 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22603 .expect("Confirm rename was not started");
22604 rename_handler.next().await.unwrap();
22605 rename_task.await.expect("Confirm rename failed");
22606 cx.run_until_parked();
22607
22608 // Despite two edits, only one is actually applied as those are identical
22609 cx.assert_editor_state(indoc! {"
22610 struct FooRenamedˇ {}
22611 "});
22612}
22613
22614#[gpui::test]
22615async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22616 init_test(cx, |_| {});
22617 // These capabilities indicate that the server does not support prepare rename.
22618 let capabilities = lsp::ServerCapabilities {
22619 rename_provider: Some(lsp::OneOf::Left(true)),
22620 ..Default::default()
22621 };
22622 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22623
22624 cx.set_state(indoc! {"
22625 struct Fˇoo {}
22626 "});
22627
22628 cx.update_editor(|editor, _window, cx| {
22629 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22630 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22631 editor.highlight_background::<DocumentHighlightRead>(
22632 &[highlight_range],
22633 |theme| theme.colors().editor_document_highlight_read_background,
22634 cx,
22635 );
22636 });
22637
22638 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22639 .expect("Prepare rename was not started")
22640 .await
22641 .expect("Prepare rename failed");
22642
22643 let mut rename_handler =
22644 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22645 let edit = lsp::TextEdit {
22646 range: lsp::Range {
22647 start: lsp::Position {
22648 line: 0,
22649 character: 7,
22650 },
22651 end: lsp::Position {
22652 line: 0,
22653 character: 10,
22654 },
22655 },
22656 new_text: "FooRenamed".to_string(),
22657 };
22658 Ok(Some(lsp::WorkspaceEdit::new(
22659 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
22660 )))
22661 });
22662 let rename_task = cx
22663 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22664 .expect("Confirm rename was not started");
22665 rename_handler.next().await.unwrap();
22666 rename_task.await.expect("Confirm rename failed");
22667 cx.run_until_parked();
22668
22669 // Correct range is renamed, as `surrounding_word` is used to find it.
22670 cx.assert_editor_state(indoc! {"
22671 struct FooRenamedˇ {}
22672 "});
22673}
22674
22675#[gpui::test]
22676async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
22677 init_test(cx, |_| {});
22678 let mut cx = EditorTestContext::new(cx).await;
22679
22680 let language = Arc::new(
22681 Language::new(
22682 LanguageConfig::default(),
22683 Some(tree_sitter_html::LANGUAGE.into()),
22684 )
22685 .with_brackets_query(
22686 r#"
22687 ("<" @open "/>" @close)
22688 ("</" @open ">" @close)
22689 ("<" @open ">" @close)
22690 ("\"" @open "\"" @close)
22691 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
22692 "#,
22693 )
22694 .unwrap(),
22695 );
22696 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22697
22698 cx.set_state(indoc! {"
22699 <span>ˇ</span>
22700 "});
22701 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22702 cx.assert_editor_state(indoc! {"
22703 <span>
22704 ˇ
22705 </span>
22706 "});
22707
22708 cx.set_state(indoc! {"
22709 <span><span></span>ˇ</span>
22710 "});
22711 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22712 cx.assert_editor_state(indoc! {"
22713 <span><span></span>
22714 ˇ</span>
22715 "});
22716
22717 cx.set_state(indoc! {"
22718 <span>ˇ
22719 </span>
22720 "});
22721 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22722 cx.assert_editor_state(indoc! {"
22723 <span>
22724 ˇ
22725 </span>
22726 "});
22727}
22728
22729#[gpui::test(iterations = 10)]
22730async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
22731 init_test(cx, |_| {});
22732
22733 let fs = FakeFs::new(cx.executor());
22734 fs.insert_tree(
22735 path!("/dir"),
22736 json!({
22737 "a.ts": "a",
22738 }),
22739 )
22740 .await;
22741
22742 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
22743 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22744 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22745
22746 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22747 language_registry.add(Arc::new(Language::new(
22748 LanguageConfig {
22749 name: "TypeScript".into(),
22750 matcher: LanguageMatcher {
22751 path_suffixes: vec!["ts".to_string()],
22752 ..Default::default()
22753 },
22754 ..Default::default()
22755 },
22756 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
22757 )));
22758 let mut fake_language_servers = language_registry.register_fake_lsp(
22759 "TypeScript",
22760 FakeLspAdapter {
22761 capabilities: lsp::ServerCapabilities {
22762 code_lens_provider: Some(lsp::CodeLensOptions {
22763 resolve_provider: Some(true),
22764 }),
22765 execute_command_provider: Some(lsp::ExecuteCommandOptions {
22766 commands: vec!["_the/command".to_string()],
22767 ..lsp::ExecuteCommandOptions::default()
22768 }),
22769 ..lsp::ServerCapabilities::default()
22770 },
22771 ..FakeLspAdapter::default()
22772 },
22773 );
22774
22775 let editor = workspace
22776 .update(cx, |workspace, window, cx| {
22777 workspace.open_abs_path(
22778 PathBuf::from(path!("/dir/a.ts")),
22779 OpenOptions::default(),
22780 window,
22781 cx,
22782 )
22783 })
22784 .unwrap()
22785 .await
22786 .unwrap()
22787 .downcast::<Editor>()
22788 .unwrap();
22789 cx.executor().run_until_parked();
22790
22791 let fake_server = fake_language_servers.next().await.unwrap();
22792
22793 let buffer = editor.update(cx, |editor, cx| {
22794 editor
22795 .buffer()
22796 .read(cx)
22797 .as_singleton()
22798 .expect("have opened a single file by path")
22799 });
22800
22801 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
22802 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
22803 drop(buffer_snapshot);
22804 let actions = cx
22805 .update_window(*workspace, |_, window, cx| {
22806 project.code_actions(&buffer, anchor..anchor, window, cx)
22807 })
22808 .unwrap();
22809
22810 fake_server
22811 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22812 Ok(Some(vec![
22813 lsp::CodeLens {
22814 range: lsp::Range::default(),
22815 command: Some(lsp::Command {
22816 title: "Code lens command".to_owned(),
22817 command: "_the/command".to_owned(),
22818 arguments: None,
22819 }),
22820 data: None,
22821 },
22822 lsp::CodeLens {
22823 range: lsp::Range::default(),
22824 command: Some(lsp::Command {
22825 title: "Command not in capabilities".to_owned(),
22826 command: "not in capabilities".to_owned(),
22827 arguments: None,
22828 }),
22829 data: None,
22830 },
22831 lsp::CodeLens {
22832 range: lsp::Range {
22833 start: lsp::Position {
22834 line: 1,
22835 character: 1,
22836 },
22837 end: lsp::Position {
22838 line: 1,
22839 character: 1,
22840 },
22841 },
22842 command: Some(lsp::Command {
22843 title: "Command not in range".to_owned(),
22844 command: "_the/command".to_owned(),
22845 arguments: None,
22846 }),
22847 data: None,
22848 },
22849 ]))
22850 })
22851 .next()
22852 .await;
22853
22854 let actions = actions.await.unwrap();
22855 assert_eq!(
22856 actions.len(),
22857 1,
22858 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
22859 );
22860 let action = actions[0].clone();
22861 let apply = project.update(cx, |project, cx| {
22862 project.apply_code_action(buffer.clone(), action, true, cx)
22863 });
22864
22865 // Resolving the code action does not populate its edits. In absence of
22866 // edits, we must execute the given command.
22867 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
22868 |mut lens, _| async move {
22869 let lens_command = lens.command.as_mut().expect("should have a command");
22870 assert_eq!(lens_command.title, "Code lens command");
22871 lens_command.arguments = Some(vec![json!("the-argument")]);
22872 Ok(lens)
22873 },
22874 );
22875
22876 // While executing the command, the language server sends the editor
22877 // a `workspaceEdit` request.
22878 fake_server
22879 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
22880 let fake = fake_server.clone();
22881 move |params, _| {
22882 assert_eq!(params.command, "_the/command");
22883 let fake = fake.clone();
22884 async move {
22885 fake.server
22886 .request::<lsp::request::ApplyWorkspaceEdit>(
22887 lsp::ApplyWorkspaceEditParams {
22888 label: None,
22889 edit: lsp::WorkspaceEdit {
22890 changes: Some(
22891 [(
22892 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
22893 vec![lsp::TextEdit {
22894 range: lsp::Range::new(
22895 lsp::Position::new(0, 0),
22896 lsp::Position::new(0, 0),
22897 ),
22898 new_text: "X".into(),
22899 }],
22900 )]
22901 .into_iter()
22902 .collect(),
22903 ),
22904 ..lsp::WorkspaceEdit::default()
22905 },
22906 },
22907 )
22908 .await
22909 .into_response()
22910 .unwrap();
22911 Ok(Some(json!(null)))
22912 }
22913 }
22914 })
22915 .next()
22916 .await;
22917
22918 // Applying the code lens command returns a project transaction containing the edits
22919 // sent by the language server in its `workspaceEdit` request.
22920 let transaction = apply.await.unwrap();
22921 assert!(transaction.0.contains_key(&buffer));
22922 buffer.update(cx, |buffer, cx| {
22923 assert_eq!(buffer.text(), "Xa");
22924 buffer.undo(cx);
22925 assert_eq!(buffer.text(), "a");
22926 });
22927
22928 let actions_after_edits = cx
22929 .update_window(*workspace, |_, window, cx| {
22930 project.code_actions(&buffer, anchor..anchor, window, cx)
22931 })
22932 .unwrap()
22933 .await
22934 .unwrap();
22935 assert_eq!(
22936 actions, actions_after_edits,
22937 "For the same selection, same code lens actions should be returned"
22938 );
22939
22940 let _responses =
22941 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22942 panic!("No more code lens requests are expected");
22943 });
22944 editor.update_in(cx, |editor, window, cx| {
22945 editor.select_all(&SelectAll, window, cx);
22946 });
22947 cx.executor().run_until_parked();
22948 let new_actions = cx
22949 .update_window(*workspace, |_, window, cx| {
22950 project.code_actions(&buffer, anchor..anchor, window, cx)
22951 })
22952 .unwrap()
22953 .await
22954 .unwrap();
22955 assert_eq!(
22956 actions, new_actions,
22957 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
22958 );
22959}
22960
22961#[gpui::test]
22962async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
22963 init_test(cx, |_| {});
22964
22965 let fs = FakeFs::new(cx.executor());
22966 let main_text = r#"fn main() {
22967println!("1");
22968println!("2");
22969println!("3");
22970println!("4");
22971println!("5");
22972}"#;
22973 let lib_text = "mod foo {}";
22974 fs.insert_tree(
22975 path!("/a"),
22976 json!({
22977 "lib.rs": lib_text,
22978 "main.rs": main_text,
22979 }),
22980 )
22981 .await;
22982
22983 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22984 let (workspace, cx) =
22985 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22986 let worktree_id = workspace.update(cx, |workspace, cx| {
22987 workspace.project().update(cx, |project, cx| {
22988 project.worktrees(cx).next().unwrap().read(cx).id()
22989 })
22990 });
22991
22992 let expected_ranges = vec![
22993 Point::new(0, 0)..Point::new(0, 0),
22994 Point::new(1, 0)..Point::new(1, 1),
22995 Point::new(2, 0)..Point::new(2, 2),
22996 Point::new(3, 0)..Point::new(3, 3),
22997 ];
22998
22999 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23000 let editor_1 = workspace
23001 .update_in(cx, |workspace, window, cx| {
23002 workspace.open_path(
23003 (worktree_id, "main.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.update(cx, |pane, cx| {
23015 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23016 open_editor.update(cx, |editor, cx| {
23017 assert_eq!(
23018 editor.display_text(cx),
23019 main_text,
23020 "Original main.rs text on initial open",
23021 );
23022 assert_eq!(
23023 editor
23024 .selections
23025 .all::<Point>(cx)
23026 .into_iter()
23027 .map(|s| s.range())
23028 .collect::<Vec<_>>(),
23029 vec![Point::zero()..Point::zero()],
23030 "Default selections on initial open",
23031 );
23032 })
23033 });
23034 editor_1.update_in(cx, |editor, window, cx| {
23035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23036 s.select_ranges(expected_ranges.clone());
23037 });
23038 });
23039
23040 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23041 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23042 });
23043 let editor_2 = workspace
23044 .update_in(cx, |workspace, window, cx| {
23045 workspace.open_path(
23046 (worktree_id, "main.rs"),
23047 Some(pane_2.downgrade()),
23048 true,
23049 window,
23050 cx,
23051 )
23052 })
23053 .unwrap()
23054 .await
23055 .downcast::<Editor>()
23056 .unwrap();
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 main_text,
23063 "Original main.rs text on initial open in another panel",
23064 );
23065 assert_eq!(
23066 editor
23067 .selections
23068 .all::<Point>(cx)
23069 .into_iter()
23070 .map(|s| s.range())
23071 .collect::<Vec<_>>(),
23072 vec![Point::zero()..Point::zero()],
23073 "Default selections on initial open in another panel",
23074 );
23075 })
23076 });
23077
23078 editor_2.update_in(cx, |editor, window, cx| {
23079 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23080 });
23081
23082 let _other_editor_1 = workspace
23083 .update_in(cx, |workspace, window, cx| {
23084 workspace.open_path(
23085 (worktree_id, "lib.rs"),
23086 Some(pane_1.downgrade()),
23087 true,
23088 window,
23089 cx,
23090 )
23091 })
23092 .unwrap()
23093 .await
23094 .downcast::<Editor>()
23095 .unwrap();
23096 pane_1
23097 .update_in(cx, |pane, window, cx| {
23098 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23099 })
23100 .await
23101 .unwrap();
23102 drop(editor_1);
23103 pane_1.update(cx, |pane, cx| {
23104 pane.active_item()
23105 .unwrap()
23106 .downcast::<Editor>()
23107 .unwrap()
23108 .update(cx, |editor, cx| {
23109 assert_eq!(
23110 editor.display_text(cx),
23111 lib_text,
23112 "Other file should be open and active",
23113 );
23114 });
23115 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23116 });
23117
23118 let _other_editor_2 = workspace
23119 .update_in(cx, |workspace, window, cx| {
23120 workspace.open_path(
23121 (worktree_id, "lib.rs"),
23122 Some(pane_2.downgrade()),
23123 true,
23124 window,
23125 cx,
23126 )
23127 })
23128 .unwrap()
23129 .await
23130 .downcast::<Editor>()
23131 .unwrap();
23132 pane_2
23133 .update_in(cx, |pane, window, cx| {
23134 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23135 })
23136 .await
23137 .unwrap();
23138 drop(editor_2);
23139 pane_2.update(cx, |pane, cx| {
23140 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23141 open_editor.update(cx, |editor, cx| {
23142 assert_eq!(
23143 editor.display_text(cx),
23144 lib_text,
23145 "Other file should be open and active in another panel too",
23146 );
23147 });
23148 assert_eq!(
23149 pane.items().count(),
23150 1,
23151 "No other editors should be open in another pane",
23152 );
23153 });
23154
23155 let _editor_1_reopened = workspace
23156 .update_in(cx, |workspace, window, cx| {
23157 workspace.open_path(
23158 (worktree_id, "main.rs"),
23159 Some(pane_1.downgrade()),
23160 true,
23161 window,
23162 cx,
23163 )
23164 })
23165 .unwrap()
23166 .await
23167 .downcast::<Editor>()
23168 .unwrap();
23169 let _editor_2_reopened = workspace
23170 .update_in(cx, |workspace, window, cx| {
23171 workspace.open_path(
23172 (worktree_id, "main.rs"),
23173 Some(pane_2.downgrade()),
23174 true,
23175 window,
23176 cx,
23177 )
23178 })
23179 .unwrap()
23180 .await
23181 .downcast::<Editor>()
23182 .unwrap();
23183 pane_1.update(cx, |pane, cx| {
23184 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23185 open_editor.update(cx, |editor, cx| {
23186 assert_eq!(
23187 editor.display_text(cx),
23188 main_text,
23189 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23190 );
23191 assert_eq!(
23192 editor
23193 .selections
23194 .all::<Point>(cx)
23195 .into_iter()
23196 .map(|s| s.range())
23197 .collect::<Vec<_>>(),
23198 expected_ranges,
23199 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23200 );
23201 })
23202 });
23203 pane_2.update(cx, |pane, cx| {
23204 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23205 open_editor.update(cx, |editor, cx| {
23206 assert_eq!(
23207 editor.display_text(cx),
23208 r#"fn main() {
23209⋯rintln!("1");
23210⋯intln!("2");
23211⋯ntln!("3");
23212println!("4");
23213println!("5");
23214}"#,
23215 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23216 );
23217 assert_eq!(
23218 editor
23219 .selections
23220 .all::<Point>(cx)
23221 .into_iter()
23222 .map(|s| s.range())
23223 .collect::<Vec<_>>(),
23224 vec![Point::zero()..Point::zero()],
23225 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23226 );
23227 })
23228 });
23229}
23230
23231#[gpui::test]
23232async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23233 init_test(cx, |_| {});
23234
23235 let fs = FakeFs::new(cx.executor());
23236 let main_text = r#"fn main() {
23237println!("1");
23238println!("2");
23239println!("3");
23240println!("4");
23241println!("5");
23242}"#;
23243 let lib_text = "mod foo {}";
23244 fs.insert_tree(
23245 path!("/a"),
23246 json!({
23247 "lib.rs": lib_text,
23248 "main.rs": main_text,
23249 }),
23250 )
23251 .await;
23252
23253 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23254 let (workspace, cx) =
23255 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23256 let worktree_id = workspace.update(cx, |workspace, cx| {
23257 workspace.project().update(cx, |project, cx| {
23258 project.worktrees(cx).next().unwrap().read(cx).id()
23259 })
23260 });
23261
23262 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23263 let editor = workspace
23264 .update_in(cx, |workspace, window, cx| {
23265 workspace.open_path(
23266 (worktree_id, "main.rs"),
23267 Some(pane.downgrade()),
23268 true,
23269 window,
23270 cx,
23271 )
23272 })
23273 .unwrap()
23274 .await
23275 .downcast::<Editor>()
23276 .unwrap();
23277 pane.update(cx, |pane, cx| {
23278 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23279 open_editor.update(cx, |editor, cx| {
23280 assert_eq!(
23281 editor.display_text(cx),
23282 main_text,
23283 "Original main.rs text on initial open",
23284 );
23285 })
23286 });
23287 editor.update_in(cx, |editor, window, cx| {
23288 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23289 });
23290
23291 cx.update_global(|store: &mut SettingsStore, cx| {
23292 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23293 s.restore_on_file_reopen = Some(false);
23294 });
23295 });
23296 editor.update_in(cx, |editor, window, cx| {
23297 editor.fold_ranges(
23298 vec![
23299 Point::new(1, 0)..Point::new(1, 1),
23300 Point::new(2, 0)..Point::new(2, 2),
23301 Point::new(3, 0)..Point::new(3, 3),
23302 ],
23303 false,
23304 window,
23305 cx,
23306 );
23307 });
23308 pane.update_in(cx, |pane, window, cx| {
23309 pane.close_all_items(&CloseAllItems::default(), window, cx)
23310 })
23311 .await
23312 .unwrap();
23313 pane.update(cx, |pane, _| {
23314 assert!(pane.active_item().is_none());
23315 });
23316 cx.update_global(|store: &mut SettingsStore, cx| {
23317 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23318 s.restore_on_file_reopen = Some(true);
23319 });
23320 });
23321
23322 let _editor_reopened = workspace
23323 .update_in(cx, |workspace, window, cx| {
23324 workspace.open_path(
23325 (worktree_id, "main.rs"),
23326 Some(pane.downgrade()),
23327 true,
23328 window,
23329 cx,
23330 )
23331 })
23332 .unwrap()
23333 .await
23334 .downcast::<Editor>()
23335 .unwrap();
23336 pane.update(cx, |pane, cx| {
23337 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23338 open_editor.update(cx, |editor, cx| {
23339 assert_eq!(
23340 editor.display_text(cx),
23341 main_text,
23342 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23343 );
23344 })
23345 });
23346}
23347
23348#[gpui::test]
23349async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23350 struct EmptyModalView {
23351 focus_handle: gpui::FocusHandle,
23352 }
23353 impl EventEmitter<DismissEvent> for EmptyModalView {}
23354 impl Render for EmptyModalView {
23355 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23356 div()
23357 }
23358 }
23359 impl Focusable for EmptyModalView {
23360 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23361 self.focus_handle.clone()
23362 }
23363 }
23364 impl workspace::ModalView for EmptyModalView {}
23365 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23366 EmptyModalView {
23367 focus_handle: cx.focus_handle(),
23368 }
23369 }
23370
23371 init_test(cx, |_| {});
23372
23373 let fs = FakeFs::new(cx.executor());
23374 let project = Project::test(fs, [], cx).await;
23375 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23376 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23377 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23378 let editor = cx.new_window_entity(|window, cx| {
23379 Editor::new(
23380 EditorMode::full(),
23381 buffer,
23382 Some(project.clone()),
23383 window,
23384 cx,
23385 )
23386 });
23387 workspace
23388 .update(cx, |workspace, window, cx| {
23389 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23390 })
23391 .unwrap();
23392 editor.update_in(cx, |editor, window, cx| {
23393 editor.open_context_menu(&OpenContextMenu, window, cx);
23394 assert!(editor.mouse_context_menu.is_some());
23395 });
23396 workspace
23397 .update(cx, |workspace, window, cx| {
23398 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23399 })
23400 .unwrap();
23401 cx.read(|cx| {
23402 assert!(editor.read(cx).mouse_context_menu.is_none());
23403 });
23404}
23405
23406#[gpui::test]
23407async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23408 init_test(cx, |_| {});
23409
23410 let fs = FakeFs::new(cx.executor());
23411 fs.insert_file(path!("/file.html"), Default::default())
23412 .await;
23413
23414 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23415
23416 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23417 let html_language = Arc::new(Language::new(
23418 LanguageConfig {
23419 name: "HTML".into(),
23420 matcher: LanguageMatcher {
23421 path_suffixes: vec!["html".to_string()],
23422 ..LanguageMatcher::default()
23423 },
23424 brackets: BracketPairConfig {
23425 pairs: vec![BracketPair {
23426 start: "<".into(),
23427 end: ">".into(),
23428 close: true,
23429 ..Default::default()
23430 }],
23431 ..Default::default()
23432 },
23433 ..Default::default()
23434 },
23435 Some(tree_sitter_html::LANGUAGE.into()),
23436 ));
23437 language_registry.add(html_language);
23438 let mut fake_servers = language_registry.register_fake_lsp(
23439 "HTML",
23440 FakeLspAdapter {
23441 capabilities: lsp::ServerCapabilities {
23442 completion_provider: Some(lsp::CompletionOptions {
23443 resolve_provider: Some(true),
23444 ..Default::default()
23445 }),
23446 ..Default::default()
23447 },
23448 ..Default::default()
23449 },
23450 );
23451
23452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23453 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23454
23455 let worktree_id = workspace
23456 .update(cx, |workspace, _window, cx| {
23457 workspace.project().update(cx, |project, cx| {
23458 project.worktrees(cx).next().unwrap().read(cx).id()
23459 })
23460 })
23461 .unwrap();
23462 project
23463 .update(cx, |project, cx| {
23464 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23465 })
23466 .await
23467 .unwrap();
23468 let editor = workspace
23469 .update(cx, |workspace, window, cx| {
23470 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23471 })
23472 .unwrap()
23473 .await
23474 .unwrap()
23475 .downcast::<Editor>()
23476 .unwrap();
23477
23478 let fake_server = fake_servers.next().await.unwrap();
23479 editor.update_in(cx, |editor, window, cx| {
23480 editor.set_text("<ad></ad>", window, cx);
23481 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23482 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23483 });
23484 let Some((buffer, _)) = editor
23485 .buffer
23486 .read(cx)
23487 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23488 else {
23489 panic!("Failed to get buffer for selection position");
23490 };
23491 let buffer = buffer.read(cx);
23492 let buffer_id = buffer.remote_id();
23493 let opening_range =
23494 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23495 let closing_range =
23496 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23497 let mut linked_ranges = HashMap::default();
23498 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23499 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23500 });
23501 let mut completion_handle =
23502 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23503 Ok(Some(lsp::CompletionResponse::Array(vec![
23504 lsp::CompletionItem {
23505 label: "head".to_string(),
23506 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23507 lsp::InsertReplaceEdit {
23508 new_text: "head".to_string(),
23509 insert: lsp::Range::new(
23510 lsp::Position::new(0, 1),
23511 lsp::Position::new(0, 3),
23512 ),
23513 replace: lsp::Range::new(
23514 lsp::Position::new(0, 1),
23515 lsp::Position::new(0, 3),
23516 ),
23517 },
23518 )),
23519 ..Default::default()
23520 },
23521 ])))
23522 });
23523 editor.update_in(cx, |editor, window, cx| {
23524 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23525 });
23526 cx.run_until_parked();
23527 completion_handle.next().await.unwrap();
23528 editor.update(cx, |editor, _| {
23529 assert!(
23530 editor.context_menu_visible(),
23531 "Completion menu should be visible"
23532 );
23533 });
23534 editor.update_in(cx, |editor, window, cx| {
23535 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23536 });
23537 cx.executor().run_until_parked();
23538 editor.update(cx, |editor, cx| {
23539 assert_eq!(editor.text(cx), "<head></head>");
23540 });
23541}
23542
23543#[gpui::test]
23544async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23545 init_test(cx, |_| {});
23546
23547 let fs = FakeFs::new(cx.executor());
23548 fs.insert_tree(
23549 path!("/root"),
23550 json!({
23551 "a": {
23552 "main.rs": "fn main() {}",
23553 },
23554 "foo": {
23555 "bar": {
23556 "external_file.rs": "pub mod external {}",
23557 }
23558 }
23559 }),
23560 )
23561 .await;
23562
23563 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23564 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23565 language_registry.add(rust_lang());
23566 let _fake_servers = language_registry.register_fake_lsp(
23567 "Rust",
23568 FakeLspAdapter {
23569 ..FakeLspAdapter::default()
23570 },
23571 );
23572 let (workspace, cx) =
23573 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23574 let worktree_id = workspace.update(cx, |workspace, cx| {
23575 workspace.project().update(cx, |project, cx| {
23576 project.worktrees(cx).next().unwrap().read(cx).id()
23577 })
23578 });
23579
23580 let assert_language_servers_count =
23581 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23582 project.update(cx, |project, cx| {
23583 let current = project
23584 .lsp_store()
23585 .read(cx)
23586 .as_local()
23587 .unwrap()
23588 .language_servers
23589 .len();
23590 assert_eq!(expected, current, "{context}");
23591 });
23592 };
23593
23594 assert_language_servers_count(
23595 0,
23596 "No servers should be running before any file is open",
23597 cx,
23598 );
23599 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23600 let main_editor = workspace
23601 .update_in(cx, |workspace, window, cx| {
23602 workspace.open_path(
23603 (worktree_id, "main.rs"),
23604 Some(pane.downgrade()),
23605 true,
23606 window,
23607 cx,
23608 )
23609 })
23610 .unwrap()
23611 .await
23612 .downcast::<Editor>()
23613 .unwrap();
23614 pane.update(cx, |pane, cx| {
23615 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23616 open_editor.update(cx, |editor, cx| {
23617 assert_eq!(
23618 editor.display_text(cx),
23619 "fn main() {}",
23620 "Original main.rs text on initial open",
23621 );
23622 });
23623 assert_eq!(open_editor, main_editor);
23624 });
23625 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
23626
23627 let external_editor = workspace
23628 .update_in(cx, |workspace, window, cx| {
23629 workspace.open_abs_path(
23630 PathBuf::from("/root/foo/bar/external_file.rs"),
23631 OpenOptions::default(),
23632 window,
23633 cx,
23634 )
23635 })
23636 .await
23637 .expect("opening external file")
23638 .downcast::<Editor>()
23639 .expect("downcasted external file's open element to editor");
23640 pane.update(cx, |pane, cx| {
23641 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23642 open_editor.update(cx, |editor, cx| {
23643 assert_eq!(
23644 editor.display_text(cx),
23645 "pub mod external {}",
23646 "External file is open now",
23647 );
23648 });
23649 assert_eq!(open_editor, external_editor);
23650 });
23651 assert_language_servers_count(
23652 1,
23653 "Second, external, *.rs file should join the existing server",
23654 cx,
23655 );
23656
23657 pane.update_in(cx, |pane, window, cx| {
23658 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23659 })
23660 .await
23661 .unwrap();
23662 pane.update_in(cx, |pane, window, cx| {
23663 pane.navigate_backward(&Default::default(), window, cx);
23664 });
23665 cx.run_until_parked();
23666 pane.update(cx, |pane, cx| {
23667 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23668 open_editor.update(cx, |editor, cx| {
23669 assert_eq!(
23670 editor.display_text(cx),
23671 "pub mod external {}",
23672 "External file is open now",
23673 );
23674 });
23675 });
23676 assert_language_servers_count(
23677 1,
23678 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
23679 cx,
23680 );
23681
23682 cx.update(|_, cx| {
23683 workspace::reload(cx);
23684 });
23685 assert_language_servers_count(
23686 1,
23687 "After reloading the worktree with local and external files opened, only one project should be started",
23688 cx,
23689 );
23690}
23691
23692#[gpui::test]
23693async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
23694 init_test(cx, |_| {});
23695
23696 let mut cx = EditorTestContext::new(cx).await;
23697 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23698 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23699
23700 // test cursor move to start of each line on tab
23701 // for `if`, `elif`, `else`, `while`, `with` and `for`
23702 cx.set_state(indoc! {"
23703 def main():
23704 ˇ for item in items:
23705 ˇ while item.active:
23706 ˇ if item.value > 10:
23707 ˇ continue
23708 ˇ elif item.value < 0:
23709 ˇ break
23710 ˇ else:
23711 ˇ with item.context() as ctx:
23712 ˇ yield count
23713 ˇ else:
23714 ˇ log('while else')
23715 ˇ else:
23716 ˇ log('for else')
23717 "});
23718 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23719 cx.assert_editor_state(indoc! {"
23720 def main():
23721 ˇfor item in items:
23722 ˇwhile item.active:
23723 ˇif item.value > 10:
23724 ˇcontinue
23725 ˇelif item.value < 0:
23726 ˇbreak
23727 ˇelse:
23728 ˇwith item.context() as ctx:
23729 ˇyield count
23730 ˇelse:
23731 ˇlog('while else')
23732 ˇelse:
23733 ˇlog('for else')
23734 "});
23735 // test relative indent is preserved when tab
23736 // for `if`, `elif`, `else`, `while`, `with` and `for`
23737 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23738 cx.assert_editor_state(indoc! {"
23739 def main():
23740 ˇfor item in items:
23741 ˇwhile item.active:
23742 ˇif item.value > 10:
23743 ˇcontinue
23744 ˇelif item.value < 0:
23745 ˇbreak
23746 ˇelse:
23747 ˇwith item.context() as ctx:
23748 ˇyield count
23749 ˇelse:
23750 ˇlog('while else')
23751 ˇelse:
23752 ˇlog('for else')
23753 "});
23754
23755 // test cursor move to start of each line on tab
23756 // for `try`, `except`, `else`, `finally`, `match` and `def`
23757 cx.set_state(indoc! {"
23758 def main():
23759 ˇ try:
23760 ˇ fetch()
23761 ˇ except ValueError:
23762 ˇ handle_error()
23763 ˇ else:
23764 ˇ match value:
23765 ˇ case _:
23766 ˇ finally:
23767 ˇ def status():
23768 ˇ return 0
23769 "});
23770 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23771 cx.assert_editor_state(indoc! {"
23772 def main():
23773 ˇtry:
23774 ˇfetch()
23775 ˇexcept ValueError:
23776 ˇhandle_error()
23777 ˇelse:
23778 ˇmatch value:
23779 ˇcase _:
23780 ˇfinally:
23781 ˇdef status():
23782 ˇreturn 0
23783 "});
23784 // test relative indent is preserved when tab
23785 // for `try`, `except`, `else`, `finally`, `match` and `def`
23786 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23787 cx.assert_editor_state(indoc! {"
23788 def main():
23789 ˇtry:
23790 ˇfetch()
23791 ˇexcept ValueError:
23792 ˇhandle_error()
23793 ˇelse:
23794 ˇmatch value:
23795 ˇcase _:
23796 ˇfinally:
23797 ˇdef status():
23798 ˇreturn 0
23799 "});
23800}
23801
23802#[gpui::test]
23803async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
23804 init_test(cx, |_| {});
23805
23806 let mut cx = EditorTestContext::new(cx).await;
23807 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23808 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23809
23810 // test `else` auto outdents when typed inside `if` block
23811 cx.set_state(indoc! {"
23812 def main():
23813 if i == 2:
23814 return
23815 ˇ
23816 "});
23817 cx.update_editor(|editor, window, cx| {
23818 editor.handle_input("else:", window, cx);
23819 });
23820 cx.assert_editor_state(indoc! {"
23821 def main():
23822 if i == 2:
23823 return
23824 else:ˇ
23825 "});
23826
23827 // test `except` auto outdents when typed inside `try` block
23828 cx.set_state(indoc! {"
23829 def main():
23830 try:
23831 i = 2
23832 ˇ
23833 "});
23834 cx.update_editor(|editor, window, cx| {
23835 editor.handle_input("except:", window, cx);
23836 });
23837 cx.assert_editor_state(indoc! {"
23838 def main():
23839 try:
23840 i = 2
23841 except:ˇ
23842 "});
23843
23844 // test `else` auto outdents when typed inside `except` block
23845 cx.set_state(indoc! {"
23846 def main():
23847 try:
23848 i = 2
23849 except:
23850 j = 2
23851 ˇ
23852 "});
23853 cx.update_editor(|editor, window, cx| {
23854 editor.handle_input("else:", window, cx);
23855 });
23856 cx.assert_editor_state(indoc! {"
23857 def main():
23858 try:
23859 i = 2
23860 except:
23861 j = 2
23862 else:ˇ
23863 "});
23864
23865 // test `finally` auto outdents when typed inside `else` block
23866 cx.set_state(indoc! {"
23867 def main():
23868 try:
23869 i = 2
23870 except:
23871 j = 2
23872 else:
23873 k = 2
23874 ˇ
23875 "});
23876 cx.update_editor(|editor, window, cx| {
23877 editor.handle_input("finally:", window, cx);
23878 });
23879 cx.assert_editor_state(indoc! {"
23880 def main():
23881 try:
23882 i = 2
23883 except:
23884 j = 2
23885 else:
23886 k = 2
23887 finally:ˇ
23888 "});
23889
23890 // test `else` does not outdents when typed inside `except` block right after for block
23891 cx.set_state(indoc! {"
23892 def main():
23893 try:
23894 i = 2
23895 except:
23896 for i in range(n):
23897 pass
23898 ˇ
23899 "});
23900 cx.update_editor(|editor, window, cx| {
23901 editor.handle_input("else:", window, cx);
23902 });
23903 cx.assert_editor_state(indoc! {"
23904 def main():
23905 try:
23906 i = 2
23907 except:
23908 for i in range(n):
23909 pass
23910 else:ˇ
23911 "});
23912
23913 // test `finally` auto outdents when typed inside `else` block right after for block
23914 cx.set_state(indoc! {"
23915 def main():
23916 try:
23917 i = 2
23918 except:
23919 j = 2
23920 else:
23921 for i in range(n):
23922 pass
23923 ˇ
23924 "});
23925 cx.update_editor(|editor, window, cx| {
23926 editor.handle_input("finally:", window, cx);
23927 });
23928 cx.assert_editor_state(indoc! {"
23929 def main():
23930 try:
23931 i = 2
23932 except:
23933 j = 2
23934 else:
23935 for i in range(n):
23936 pass
23937 finally:ˇ
23938 "});
23939
23940 // test `except` outdents to inner "try" block
23941 cx.set_state(indoc! {"
23942 def main():
23943 try:
23944 i = 2
23945 if i == 2:
23946 try:
23947 i = 3
23948 ˇ
23949 "});
23950 cx.update_editor(|editor, window, cx| {
23951 editor.handle_input("except:", window, cx);
23952 });
23953 cx.assert_editor_state(indoc! {"
23954 def main():
23955 try:
23956 i = 2
23957 if i == 2:
23958 try:
23959 i = 3
23960 except:ˇ
23961 "});
23962
23963 // test `except` outdents to outer "try" block
23964 cx.set_state(indoc! {"
23965 def main():
23966 try:
23967 i = 2
23968 if i == 2:
23969 try:
23970 i = 3
23971 ˇ
23972 "});
23973 cx.update_editor(|editor, window, cx| {
23974 editor.handle_input("except:", window, cx);
23975 });
23976 cx.assert_editor_state(indoc! {"
23977 def main():
23978 try:
23979 i = 2
23980 if i == 2:
23981 try:
23982 i = 3
23983 except:ˇ
23984 "});
23985
23986 // test `else` stays at correct indent when typed after `for` block
23987 cx.set_state(indoc! {"
23988 def main():
23989 for i in range(10):
23990 if i == 3:
23991 break
23992 ˇ
23993 "});
23994 cx.update_editor(|editor, window, cx| {
23995 editor.handle_input("else:", window, cx);
23996 });
23997 cx.assert_editor_state(indoc! {"
23998 def main():
23999 for i in range(10):
24000 if i == 3:
24001 break
24002 else:ˇ
24003 "});
24004
24005 // test does not outdent on typing after line with square brackets
24006 cx.set_state(indoc! {"
24007 def f() -> list[str]:
24008 ˇ
24009 "});
24010 cx.update_editor(|editor, window, cx| {
24011 editor.handle_input("a", window, cx);
24012 });
24013 cx.assert_editor_state(indoc! {"
24014 def f() -> list[str]:
24015 aˇ
24016 "});
24017
24018 // test does not outdent on typing : after case keyword
24019 cx.set_state(indoc! {"
24020 match 1:
24021 caseˇ
24022 "});
24023 cx.update_editor(|editor, window, cx| {
24024 editor.handle_input(":", window, cx);
24025 });
24026 cx.assert_editor_state(indoc! {"
24027 match 1:
24028 case:ˇ
24029 "});
24030}
24031
24032#[gpui::test]
24033async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24034 init_test(cx, |_| {});
24035 update_test_language_settings(cx, |settings| {
24036 settings.defaults.extend_comment_on_newline = Some(false);
24037 });
24038 let mut cx = EditorTestContext::new(cx).await;
24039 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24040 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24041
24042 // test correct indent after newline on comment
24043 cx.set_state(indoc! {"
24044 # COMMENT:ˇ
24045 "});
24046 cx.update_editor(|editor, window, cx| {
24047 editor.newline(&Newline, window, cx);
24048 });
24049 cx.assert_editor_state(indoc! {"
24050 # COMMENT:
24051 ˇ
24052 "});
24053
24054 // test correct indent after newline in brackets
24055 cx.set_state(indoc! {"
24056 {ˇ}
24057 "});
24058 cx.update_editor(|editor, window, cx| {
24059 editor.newline(&Newline, window, cx);
24060 });
24061 cx.run_until_parked();
24062 cx.assert_editor_state(indoc! {"
24063 {
24064 ˇ
24065 }
24066 "});
24067
24068 cx.set_state(indoc! {"
24069 (ˇ)
24070 "});
24071 cx.update_editor(|editor, window, cx| {
24072 editor.newline(&Newline, window, cx);
24073 });
24074 cx.run_until_parked();
24075 cx.assert_editor_state(indoc! {"
24076 (
24077 ˇ
24078 )
24079 "});
24080
24081 // do not indent after empty lists or dictionaries
24082 cx.set_state(indoc! {"
24083 a = []ˇ
24084 "});
24085 cx.update_editor(|editor, window, cx| {
24086 editor.newline(&Newline, window, cx);
24087 });
24088 cx.run_until_parked();
24089 cx.assert_editor_state(indoc! {"
24090 a = []
24091 ˇ
24092 "});
24093}
24094
24095#[gpui::test]
24096async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24097 init_test(cx, |_| {});
24098
24099 let mut cx = EditorTestContext::new(cx).await;
24100 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24101 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24102
24103 // test cursor move to start of each line on tab
24104 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24105 cx.set_state(indoc! {"
24106 function main() {
24107 ˇ for item in $items; do
24108 ˇ while [ -n \"$item\" ]; do
24109 ˇ if [ \"$value\" -gt 10 ]; then
24110 ˇ continue
24111 ˇ elif [ \"$value\" -lt 0 ]; then
24112 ˇ break
24113 ˇ else
24114 ˇ echo \"$item\"
24115 ˇ fi
24116 ˇ done
24117 ˇ done
24118 ˇ}
24119 "});
24120 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24121 cx.assert_editor_state(indoc! {"
24122 function main() {
24123 ˇfor item in $items; do
24124 ˇwhile [ -n \"$item\" ]; do
24125 ˇif [ \"$value\" -gt 10 ]; then
24126 ˇcontinue
24127 ˇelif [ \"$value\" -lt 0 ]; then
24128 ˇbreak
24129 ˇelse
24130 ˇecho \"$item\"
24131 ˇfi
24132 ˇdone
24133 ˇdone
24134 ˇ}
24135 "});
24136 // test relative indent is preserved when tab
24137 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24138 cx.assert_editor_state(indoc! {"
24139 function main() {
24140 ˇfor item in $items; do
24141 ˇwhile [ -n \"$item\" ]; do
24142 ˇif [ \"$value\" -gt 10 ]; then
24143 ˇcontinue
24144 ˇelif [ \"$value\" -lt 0 ]; then
24145 ˇbreak
24146 ˇelse
24147 ˇecho \"$item\"
24148 ˇfi
24149 ˇdone
24150 ˇdone
24151 ˇ}
24152 "});
24153
24154 // test cursor move to start of each line on tab
24155 // for `case` statement with patterns
24156 cx.set_state(indoc! {"
24157 function handle() {
24158 ˇ case \"$1\" in
24159 ˇ start)
24160 ˇ echo \"a\"
24161 ˇ ;;
24162 ˇ stop)
24163 ˇ echo \"b\"
24164 ˇ ;;
24165 ˇ *)
24166 ˇ echo \"c\"
24167 ˇ ;;
24168 ˇ esac
24169 ˇ}
24170 "});
24171 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24172 cx.assert_editor_state(indoc! {"
24173 function handle() {
24174 ˇcase \"$1\" in
24175 ˇstart)
24176 ˇecho \"a\"
24177 ˇ;;
24178 ˇstop)
24179 ˇecho \"b\"
24180 ˇ;;
24181 ˇ*)
24182 ˇecho \"c\"
24183 ˇ;;
24184 ˇesac
24185 ˇ}
24186 "});
24187}
24188
24189#[gpui::test]
24190async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24191 init_test(cx, |_| {});
24192
24193 let mut cx = EditorTestContext::new(cx).await;
24194 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24196
24197 // test indents on comment insert
24198 cx.set_state(indoc! {"
24199 function main() {
24200 ˇ for item in $items; do
24201 ˇ while [ -n \"$item\" ]; do
24202 ˇ if [ \"$value\" -gt 10 ]; then
24203 ˇ continue
24204 ˇ elif [ \"$value\" -lt 0 ]; then
24205 ˇ break
24206 ˇ else
24207 ˇ echo \"$item\"
24208 ˇ fi
24209 ˇ done
24210 ˇ done
24211 ˇ}
24212 "});
24213 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24214 cx.assert_editor_state(indoc! {"
24215 function main() {
24216 #ˇ for item in $items; do
24217 #ˇ while [ -n \"$item\" ]; do
24218 #ˇ if [ \"$value\" -gt 10 ]; then
24219 #ˇ continue
24220 #ˇ elif [ \"$value\" -lt 0 ]; then
24221 #ˇ break
24222 #ˇ else
24223 #ˇ echo \"$item\"
24224 #ˇ fi
24225 #ˇ done
24226 #ˇ done
24227 #ˇ}
24228 "});
24229}
24230
24231#[gpui::test]
24232async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24233 init_test(cx, |_| {});
24234
24235 let mut cx = EditorTestContext::new(cx).await;
24236 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24237 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24238
24239 // test `else` auto outdents when typed inside `if` block
24240 cx.set_state(indoc! {"
24241 if [ \"$1\" = \"test\" ]; then
24242 echo \"foo bar\"
24243 ˇ
24244 "});
24245 cx.update_editor(|editor, window, cx| {
24246 editor.handle_input("else", window, cx);
24247 });
24248 cx.assert_editor_state(indoc! {"
24249 if [ \"$1\" = \"test\" ]; then
24250 echo \"foo bar\"
24251 elseˇ
24252 "});
24253
24254 // test `elif` auto outdents when typed inside `if` block
24255 cx.set_state(indoc! {"
24256 if [ \"$1\" = \"test\" ]; then
24257 echo \"foo bar\"
24258 ˇ
24259 "});
24260 cx.update_editor(|editor, window, cx| {
24261 editor.handle_input("elif", window, cx);
24262 });
24263 cx.assert_editor_state(indoc! {"
24264 if [ \"$1\" = \"test\" ]; then
24265 echo \"foo bar\"
24266 elifˇ
24267 "});
24268
24269 // test `fi` auto outdents when typed inside `else` block
24270 cx.set_state(indoc! {"
24271 if [ \"$1\" = \"test\" ]; then
24272 echo \"foo bar\"
24273 else
24274 echo \"bar baz\"
24275 ˇ
24276 "});
24277 cx.update_editor(|editor, window, cx| {
24278 editor.handle_input("fi", window, cx);
24279 });
24280 cx.assert_editor_state(indoc! {"
24281 if [ \"$1\" = \"test\" ]; then
24282 echo \"foo bar\"
24283 else
24284 echo \"bar baz\"
24285 fiˇ
24286 "});
24287
24288 // test `done` auto outdents when typed inside `while` block
24289 cx.set_state(indoc! {"
24290 while read line; do
24291 echo \"$line\"
24292 ˇ
24293 "});
24294 cx.update_editor(|editor, window, cx| {
24295 editor.handle_input("done", window, cx);
24296 });
24297 cx.assert_editor_state(indoc! {"
24298 while read line; do
24299 echo \"$line\"
24300 doneˇ
24301 "});
24302
24303 // test `done` auto outdents when typed inside `for` block
24304 cx.set_state(indoc! {"
24305 for file in *.txt; do
24306 cat \"$file\"
24307 ˇ
24308 "});
24309 cx.update_editor(|editor, window, cx| {
24310 editor.handle_input("done", window, cx);
24311 });
24312 cx.assert_editor_state(indoc! {"
24313 for file in *.txt; do
24314 cat \"$file\"
24315 doneˇ
24316 "});
24317
24318 // test `esac` auto outdents when typed inside `case` block
24319 cx.set_state(indoc! {"
24320 case \"$1\" in
24321 start)
24322 echo \"foo bar\"
24323 ;;
24324 stop)
24325 echo \"bar baz\"
24326 ;;
24327 ˇ
24328 "});
24329 cx.update_editor(|editor, window, cx| {
24330 editor.handle_input("esac", window, cx);
24331 });
24332 cx.assert_editor_state(indoc! {"
24333 case \"$1\" in
24334 start)
24335 echo \"foo bar\"
24336 ;;
24337 stop)
24338 echo \"bar baz\"
24339 ;;
24340 esacˇ
24341 "});
24342
24343 // test `*)` auto outdents when typed inside `case` block
24344 cx.set_state(indoc! {"
24345 case \"$1\" in
24346 start)
24347 echo \"foo bar\"
24348 ;;
24349 ˇ
24350 "});
24351 cx.update_editor(|editor, window, cx| {
24352 editor.handle_input("*)", window, cx);
24353 });
24354 cx.assert_editor_state(indoc! {"
24355 case \"$1\" in
24356 start)
24357 echo \"foo bar\"
24358 ;;
24359 *)ˇ
24360 "});
24361
24362 // test `fi` outdents to correct level with nested if blocks
24363 cx.set_state(indoc! {"
24364 if [ \"$1\" = \"test\" ]; then
24365 echo \"outer if\"
24366 if [ \"$2\" = \"debug\" ]; then
24367 echo \"inner if\"
24368 ˇ
24369 "});
24370 cx.update_editor(|editor, window, cx| {
24371 editor.handle_input("fi", window, cx);
24372 });
24373 cx.assert_editor_state(indoc! {"
24374 if [ \"$1\" = \"test\" ]; then
24375 echo \"outer if\"
24376 if [ \"$2\" = \"debug\" ]; then
24377 echo \"inner if\"
24378 fiˇ
24379 "});
24380}
24381
24382#[gpui::test]
24383async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24384 init_test(cx, |_| {});
24385 update_test_language_settings(cx, |settings| {
24386 settings.defaults.extend_comment_on_newline = Some(false);
24387 });
24388 let mut cx = EditorTestContext::new(cx).await;
24389 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24390 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24391
24392 // test correct indent after newline on comment
24393 cx.set_state(indoc! {"
24394 # COMMENT:ˇ
24395 "});
24396 cx.update_editor(|editor, window, cx| {
24397 editor.newline(&Newline, window, cx);
24398 });
24399 cx.assert_editor_state(indoc! {"
24400 # COMMENT:
24401 ˇ
24402 "});
24403
24404 // test correct indent after newline after `then`
24405 cx.set_state(indoc! {"
24406
24407 if [ \"$1\" = \"test\" ]; thenˇ
24408 "});
24409 cx.update_editor(|editor, window, cx| {
24410 editor.newline(&Newline, window, cx);
24411 });
24412 cx.run_until_parked();
24413 cx.assert_editor_state(indoc! {"
24414
24415 if [ \"$1\" = \"test\" ]; then
24416 ˇ
24417 "});
24418
24419 // test correct indent after newline after `else`
24420 cx.set_state(indoc! {"
24421 if [ \"$1\" = \"test\" ]; then
24422 elseˇ
24423 "});
24424 cx.update_editor(|editor, window, cx| {
24425 editor.newline(&Newline, window, cx);
24426 });
24427 cx.run_until_parked();
24428 cx.assert_editor_state(indoc! {"
24429 if [ \"$1\" = \"test\" ]; then
24430 else
24431 ˇ
24432 "});
24433
24434 // test correct indent after newline after `elif`
24435 cx.set_state(indoc! {"
24436 if [ \"$1\" = \"test\" ]; then
24437 elifˇ
24438 "});
24439 cx.update_editor(|editor, window, cx| {
24440 editor.newline(&Newline, window, cx);
24441 });
24442 cx.run_until_parked();
24443 cx.assert_editor_state(indoc! {"
24444 if [ \"$1\" = \"test\" ]; then
24445 elif
24446 ˇ
24447 "});
24448
24449 // test correct indent after newline after `do`
24450 cx.set_state(indoc! {"
24451 for file in *.txt; doˇ
24452 "});
24453 cx.update_editor(|editor, window, cx| {
24454 editor.newline(&Newline, window, cx);
24455 });
24456 cx.run_until_parked();
24457 cx.assert_editor_state(indoc! {"
24458 for file in *.txt; do
24459 ˇ
24460 "});
24461
24462 // test correct indent after newline after case pattern
24463 cx.set_state(indoc! {"
24464 case \"$1\" in
24465 start)ˇ
24466 "});
24467 cx.update_editor(|editor, window, cx| {
24468 editor.newline(&Newline, window, cx);
24469 });
24470 cx.run_until_parked();
24471 cx.assert_editor_state(indoc! {"
24472 case \"$1\" in
24473 start)
24474 ˇ
24475 "});
24476
24477 // test correct indent after newline after case pattern
24478 cx.set_state(indoc! {"
24479 case \"$1\" in
24480 start)
24481 ;;
24482 *)ˇ
24483 "});
24484 cx.update_editor(|editor, window, cx| {
24485 editor.newline(&Newline, window, cx);
24486 });
24487 cx.run_until_parked();
24488 cx.assert_editor_state(indoc! {"
24489 case \"$1\" in
24490 start)
24491 ;;
24492 *)
24493 ˇ
24494 "});
24495
24496 // test correct indent after newline after function opening brace
24497 cx.set_state(indoc! {"
24498 function test() {ˇ}
24499 "});
24500 cx.update_editor(|editor, window, cx| {
24501 editor.newline(&Newline, window, cx);
24502 });
24503 cx.run_until_parked();
24504 cx.assert_editor_state(indoc! {"
24505 function test() {
24506 ˇ
24507 }
24508 "});
24509
24510 // test no extra indent after semicolon on same line
24511 cx.set_state(indoc! {"
24512 echo \"test\";ˇ
24513 "});
24514 cx.update_editor(|editor, window, cx| {
24515 editor.newline(&Newline, window, cx);
24516 });
24517 cx.run_until_parked();
24518 cx.assert_editor_state(indoc! {"
24519 echo \"test\";
24520 ˇ
24521 "});
24522}
24523
24524fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24525 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24526 point..point
24527}
24528
24529#[track_caller]
24530fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24531 let (text, ranges) = marked_text_ranges(marked_text, true);
24532 assert_eq!(editor.text(cx), text);
24533 assert_eq!(
24534 editor.selections.ranges(cx),
24535 ranges,
24536 "Assert selections are {}",
24537 marked_text
24538 );
24539}
24540
24541pub fn handle_signature_help_request(
24542 cx: &mut EditorLspTestContext,
24543 mocked_response: lsp::SignatureHelp,
24544) -> impl Future<Output = ()> + use<> {
24545 let mut request =
24546 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24547 let mocked_response = mocked_response.clone();
24548 async move { Ok(Some(mocked_response)) }
24549 });
24550
24551 async move {
24552 request.next().await;
24553 }
24554}
24555
24556#[track_caller]
24557pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24558 cx.update_editor(|editor, _, _| {
24559 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24560 let entries = menu.entries.borrow();
24561 let entries = entries
24562 .iter()
24563 .map(|entry| entry.string.as_str())
24564 .collect::<Vec<_>>();
24565 assert_eq!(entries, expected);
24566 } else {
24567 panic!("Expected completions menu");
24568 }
24569 });
24570}
24571
24572/// Handle completion request passing a marked string specifying where the completion
24573/// should be triggered from using '|' character, what range should be replaced, and what completions
24574/// should be returned using '<' and '>' to delimit the range.
24575///
24576/// Also see `handle_completion_request_with_insert_and_replace`.
24577#[track_caller]
24578pub fn handle_completion_request(
24579 marked_string: &str,
24580 completions: Vec<&'static str>,
24581 is_incomplete: bool,
24582 counter: Arc<AtomicUsize>,
24583 cx: &mut EditorLspTestContext,
24584) -> impl Future<Output = ()> {
24585 let complete_from_marker: TextRangeMarker = '|'.into();
24586 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24587 let (_, mut marked_ranges) = marked_text_ranges_by(
24588 marked_string,
24589 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24590 );
24591
24592 let complete_from_position =
24593 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24594 let replace_range =
24595 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24596
24597 let mut request =
24598 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24599 let completions = completions.clone();
24600 counter.fetch_add(1, atomic::Ordering::Release);
24601 async move {
24602 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24603 assert_eq!(
24604 params.text_document_position.position,
24605 complete_from_position
24606 );
24607 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24608 is_incomplete,
24609 item_defaults: None,
24610 items: completions
24611 .iter()
24612 .map(|completion_text| lsp::CompletionItem {
24613 label: completion_text.to_string(),
24614 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24615 range: replace_range,
24616 new_text: completion_text.to_string(),
24617 })),
24618 ..Default::default()
24619 })
24620 .collect(),
24621 })))
24622 }
24623 });
24624
24625 async move {
24626 request.next().await;
24627 }
24628}
24629
24630/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
24631/// given instead, which also contains an `insert` range.
24632///
24633/// This function uses markers to define ranges:
24634/// - `|` marks the cursor position
24635/// - `<>` marks the replace range
24636/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
24637pub fn handle_completion_request_with_insert_and_replace(
24638 cx: &mut EditorLspTestContext,
24639 marked_string: &str,
24640 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
24641 counter: Arc<AtomicUsize>,
24642) -> impl Future<Output = ()> {
24643 let complete_from_marker: TextRangeMarker = '|'.into();
24644 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24645 let insert_range_marker: TextRangeMarker = ('{', '}').into();
24646
24647 let (_, mut marked_ranges) = marked_text_ranges_by(
24648 marked_string,
24649 vec![
24650 complete_from_marker.clone(),
24651 replace_range_marker.clone(),
24652 insert_range_marker.clone(),
24653 ],
24654 );
24655
24656 let complete_from_position =
24657 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24658 let replace_range =
24659 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24660
24661 let insert_range = match marked_ranges.remove(&insert_range_marker) {
24662 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
24663 _ => lsp::Range {
24664 start: replace_range.start,
24665 end: complete_from_position,
24666 },
24667 };
24668
24669 let mut request =
24670 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24671 let completions = completions.clone();
24672 counter.fetch_add(1, atomic::Ordering::Release);
24673 async move {
24674 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24675 assert_eq!(
24676 params.text_document_position.position, complete_from_position,
24677 "marker `|` position doesn't match",
24678 );
24679 Ok(Some(lsp::CompletionResponse::Array(
24680 completions
24681 .iter()
24682 .map(|(label, new_text)| lsp::CompletionItem {
24683 label: label.to_string(),
24684 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24685 lsp::InsertReplaceEdit {
24686 insert: insert_range,
24687 replace: replace_range,
24688 new_text: new_text.to_string(),
24689 },
24690 )),
24691 ..Default::default()
24692 })
24693 .collect(),
24694 )))
24695 }
24696 });
24697
24698 async move {
24699 request.next().await;
24700 }
24701}
24702
24703fn handle_resolve_completion_request(
24704 cx: &mut EditorLspTestContext,
24705 edits: Option<Vec<(&'static str, &'static str)>>,
24706) -> impl Future<Output = ()> {
24707 let edits = edits.map(|edits| {
24708 edits
24709 .iter()
24710 .map(|(marked_string, new_text)| {
24711 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
24712 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
24713 lsp::TextEdit::new(replace_range, new_text.to_string())
24714 })
24715 .collect::<Vec<_>>()
24716 });
24717
24718 let mut request =
24719 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
24720 let edits = edits.clone();
24721 async move {
24722 Ok(lsp::CompletionItem {
24723 additional_text_edits: edits,
24724 ..Default::default()
24725 })
24726 }
24727 });
24728
24729 async move {
24730 request.next().await;
24731 }
24732}
24733
24734pub(crate) fn update_test_language_settings(
24735 cx: &mut TestAppContext,
24736 f: impl Fn(&mut AllLanguageSettingsContent),
24737) {
24738 cx.update(|cx| {
24739 SettingsStore::update_global(cx, |store, cx| {
24740 store.update_user_settings::<AllLanguageSettings>(cx, f);
24741 });
24742 });
24743}
24744
24745pub(crate) fn update_test_project_settings(
24746 cx: &mut TestAppContext,
24747 f: impl Fn(&mut ProjectSettings),
24748) {
24749 cx.update(|cx| {
24750 SettingsStore::update_global(cx, |store, cx| {
24751 store.update_user_settings::<ProjectSettings>(cx, f);
24752 });
24753 });
24754}
24755
24756pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
24757 cx.update(|cx| {
24758 assets::Assets.load_test_fonts(cx);
24759 let store = SettingsStore::test(cx);
24760 cx.set_global(store);
24761 theme::init(theme::LoadThemes::JustBase, cx);
24762 release_channel::init(SemanticVersion::default(), cx);
24763 client::init_settings(cx);
24764 language::init(cx);
24765 Project::init_settings(cx);
24766 workspace::init_settings(cx);
24767 crate::init(cx);
24768 });
24769 zlog::init_test();
24770 update_test_language_settings(cx, f);
24771}
24772
24773#[track_caller]
24774fn assert_hunk_revert(
24775 not_reverted_text_with_selections: &str,
24776 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
24777 expected_reverted_text_with_selections: &str,
24778 base_text: &str,
24779 cx: &mut EditorLspTestContext,
24780) {
24781 cx.set_state(not_reverted_text_with_selections);
24782 cx.set_head_text(base_text);
24783 cx.executor().run_until_parked();
24784
24785 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
24786 let snapshot = editor.snapshot(window, cx);
24787 let reverted_hunk_statuses = snapshot
24788 .buffer_snapshot
24789 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
24790 .map(|hunk| hunk.status().kind)
24791 .collect::<Vec<_>>();
24792
24793 editor.git_restore(&Default::default(), window, cx);
24794 reverted_hunk_statuses
24795 });
24796 cx.executor().run_until_parked();
24797 cx.assert_editor_state(expected_reverted_text_with_selections);
24798 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
24799}
24800
24801#[gpui::test(iterations = 10)]
24802async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
24803 init_test(cx, |_| {});
24804
24805 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
24806 let counter = diagnostic_requests.clone();
24807
24808 let fs = FakeFs::new(cx.executor());
24809 fs.insert_tree(
24810 path!("/a"),
24811 json!({
24812 "first.rs": "fn main() { let a = 5; }",
24813 "second.rs": "// Test file",
24814 }),
24815 )
24816 .await;
24817
24818 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24819 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24820 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24821
24822 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24823 language_registry.add(rust_lang());
24824 let mut fake_servers = language_registry.register_fake_lsp(
24825 "Rust",
24826 FakeLspAdapter {
24827 capabilities: lsp::ServerCapabilities {
24828 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
24829 lsp::DiagnosticOptions {
24830 identifier: None,
24831 inter_file_dependencies: true,
24832 workspace_diagnostics: true,
24833 work_done_progress_options: Default::default(),
24834 },
24835 )),
24836 ..Default::default()
24837 },
24838 ..Default::default()
24839 },
24840 );
24841
24842 let editor = workspace
24843 .update(cx, |workspace, window, cx| {
24844 workspace.open_abs_path(
24845 PathBuf::from(path!("/a/first.rs")),
24846 OpenOptions::default(),
24847 window,
24848 cx,
24849 )
24850 })
24851 .unwrap()
24852 .await
24853 .unwrap()
24854 .downcast::<Editor>()
24855 .unwrap();
24856 let fake_server = fake_servers.next().await.unwrap();
24857 let server_id = fake_server.server.server_id();
24858 let mut first_request = fake_server
24859 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
24860 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
24861 let result_id = Some(new_result_id.to_string());
24862 assert_eq!(
24863 params.text_document.uri,
24864 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24865 );
24866 async move {
24867 Ok(lsp::DocumentDiagnosticReportResult::Report(
24868 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
24869 related_documents: None,
24870 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
24871 items: Vec::new(),
24872 result_id,
24873 },
24874 }),
24875 ))
24876 }
24877 });
24878
24879 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
24880 project.update(cx, |project, cx| {
24881 let buffer_id = editor
24882 .read(cx)
24883 .buffer()
24884 .read(cx)
24885 .as_singleton()
24886 .expect("created a singleton buffer")
24887 .read(cx)
24888 .remote_id();
24889 let buffer_result_id = project
24890 .lsp_store()
24891 .read(cx)
24892 .result_id(server_id, buffer_id, cx);
24893 assert_eq!(expected, buffer_result_id);
24894 });
24895 };
24896
24897 ensure_result_id(None, cx);
24898 cx.executor().advance_clock(Duration::from_millis(60));
24899 cx.executor().run_until_parked();
24900 assert_eq!(
24901 diagnostic_requests.load(atomic::Ordering::Acquire),
24902 1,
24903 "Opening file should trigger diagnostic request"
24904 );
24905 first_request
24906 .next()
24907 .await
24908 .expect("should have sent the first diagnostics pull request");
24909 ensure_result_id(Some("1".to_string()), cx);
24910
24911 // Editing should trigger diagnostics
24912 editor.update_in(cx, |editor, window, cx| {
24913 editor.handle_input("2", window, cx)
24914 });
24915 cx.executor().advance_clock(Duration::from_millis(60));
24916 cx.executor().run_until_parked();
24917 assert_eq!(
24918 diagnostic_requests.load(atomic::Ordering::Acquire),
24919 2,
24920 "Editing should trigger diagnostic request"
24921 );
24922 ensure_result_id(Some("2".to_string()), cx);
24923
24924 // Moving cursor should not trigger diagnostic request
24925 editor.update_in(cx, |editor, window, cx| {
24926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24927 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
24928 });
24929 });
24930 cx.executor().advance_clock(Duration::from_millis(60));
24931 cx.executor().run_until_parked();
24932 assert_eq!(
24933 diagnostic_requests.load(atomic::Ordering::Acquire),
24934 2,
24935 "Cursor movement should not trigger diagnostic request"
24936 );
24937 ensure_result_id(Some("2".to_string()), cx);
24938 // Multiple rapid edits should be debounced
24939 for _ in 0..5 {
24940 editor.update_in(cx, |editor, window, cx| {
24941 editor.handle_input("x", window, cx)
24942 });
24943 }
24944 cx.executor().advance_clock(Duration::from_millis(60));
24945 cx.executor().run_until_parked();
24946
24947 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
24948 assert!(
24949 final_requests <= 4,
24950 "Multiple rapid edits should be debounced (got {final_requests} requests)",
24951 );
24952 ensure_result_id(Some(final_requests.to_string()), cx);
24953}
24954
24955#[gpui::test]
24956async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
24957 // Regression test for issue #11671
24958 // Previously, adding a cursor after moving multiple cursors would reset
24959 // the cursor count instead of adding to the existing cursors.
24960 init_test(cx, |_| {});
24961 let mut cx = EditorTestContext::new(cx).await;
24962
24963 // Create a simple buffer with cursor at start
24964 cx.set_state(indoc! {"
24965 ˇaaaa
24966 bbbb
24967 cccc
24968 dddd
24969 eeee
24970 ffff
24971 gggg
24972 hhhh"});
24973
24974 // Add 2 cursors below (so we have 3 total)
24975 cx.update_editor(|editor, window, cx| {
24976 editor.add_selection_below(&Default::default(), window, cx);
24977 editor.add_selection_below(&Default::default(), window, cx);
24978 });
24979
24980 // Verify we have 3 cursors
24981 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24982 assert_eq!(
24983 initial_count, 3,
24984 "Should have 3 cursors after adding 2 below"
24985 );
24986
24987 // Move down one line
24988 cx.update_editor(|editor, window, cx| {
24989 editor.move_down(&MoveDown, window, cx);
24990 });
24991
24992 // Add another cursor below
24993 cx.update_editor(|editor, window, cx| {
24994 editor.add_selection_below(&Default::default(), window, cx);
24995 });
24996
24997 // Should now have 4 cursors (3 original + 1 new)
24998 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24999 assert_eq!(
25000 final_count, 4,
25001 "Should have 4 cursors after moving and adding another"
25002 );
25003}
25004
25005#[gpui::test(iterations = 10)]
25006async fn test_document_colors(cx: &mut TestAppContext) {
25007 let expected_color = Rgba {
25008 r: 0.33,
25009 g: 0.33,
25010 b: 0.33,
25011 a: 0.33,
25012 };
25013
25014 init_test(cx, |_| {});
25015
25016 let fs = FakeFs::new(cx.executor());
25017 fs.insert_tree(
25018 path!("/a"),
25019 json!({
25020 "first.rs": "fn main() { let a = 5; }",
25021 }),
25022 )
25023 .await;
25024
25025 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25026 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25027 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25028
25029 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25030 language_registry.add(rust_lang());
25031 let mut fake_servers = language_registry.register_fake_lsp(
25032 "Rust",
25033 FakeLspAdapter {
25034 capabilities: lsp::ServerCapabilities {
25035 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25036 ..lsp::ServerCapabilities::default()
25037 },
25038 name: "rust-analyzer",
25039 ..FakeLspAdapter::default()
25040 },
25041 );
25042 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25043 "Rust",
25044 FakeLspAdapter {
25045 capabilities: lsp::ServerCapabilities {
25046 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25047 ..lsp::ServerCapabilities::default()
25048 },
25049 name: "not-rust-analyzer",
25050 ..FakeLspAdapter::default()
25051 },
25052 );
25053
25054 let editor = workspace
25055 .update(cx, |workspace, window, cx| {
25056 workspace.open_abs_path(
25057 PathBuf::from(path!("/a/first.rs")),
25058 OpenOptions::default(),
25059 window,
25060 cx,
25061 )
25062 })
25063 .unwrap()
25064 .await
25065 .unwrap()
25066 .downcast::<Editor>()
25067 .unwrap();
25068 let fake_language_server = fake_servers.next().await.unwrap();
25069 let fake_language_server_without_capabilities =
25070 fake_servers_without_capabilities.next().await.unwrap();
25071 let requests_made = Arc::new(AtomicUsize::new(0));
25072 let closure_requests_made = Arc::clone(&requests_made);
25073 let mut color_request_handle = fake_language_server
25074 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25075 let requests_made = Arc::clone(&closure_requests_made);
25076 async move {
25077 assert_eq!(
25078 params.text_document.uri,
25079 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25080 );
25081 requests_made.fetch_add(1, atomic::Ordering::Release);
25082 Ok(vec![
25083 lsp::ColorInformation {
25084 range: lsp::Range {
25085 start: lsp::Position {
25086 line: 0,
25087 character: 0,
25088 },
25089 end: lsp::Position {
25090 line: 0,
25091 character: 1,
25092 },
25093 },
25094 color: lsp::Color {
25095 red: 0.33,
25096 green: 0.33,
25097 blue: 0.33,
25098 alpha: 0.33,
25099 },
25100 },
25101 lsp::ColorInformation {
25102 range: lsp::Range {
25103 start: lsp::Position {
25104 line: 0,
25105 character: 0,
25106 },
25107 end: lsp::Position {
25108 line: 0,
25109 character: 1,
25110 },
25111 },
25112 color: lsp::Color {
25113 red: 0.33,
25114 green: 0.33,
25115 blue: 0.33,
25116 alpha: 0.33,
25117 },
25118 },
25119 ])
25120 }
25121 });
25122
25123 let _handle = fake_language_server_without_capabilities
25124 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25125 panic!("Should not be called");
25126 });
25127 cx.executor().advance_clock(Duration::from_millis(100));
25128 color_request_handle.next().await.unwrap();
25129 cx.run_until_parked();
25130 assert_eq!(
25131 1,
25132 requests_made.load(atomic::Ordering::Acquire),
25133 "Should query for colors once per editor open"
25134 );
25135 editor.update_in(cx, |editor, _, cx| {
25136 assert_eq!(
25137 vec![expected_color],
25138 extract_color_inlays(editor, cx),
25139 "Should have an initial inlay"
25140 );
25141 });
25142
25143 // opening another file in a split should not influence the LSP query counter
25144 workspace
25145 .update(cx, |workspace, window, cx| {
25146 assert_eq!(
25147 workspace.panes().len(),
25148 1,
25149 "Should have one pane with one editor"
25150 );
25151 workspace.move_item_to_pane_in_direction(
25152 &MoveItemToPaneInDirection {
25153 direction: SplitDirection::Right,
25154 focus: false,
25155 clone: true,
25156 },
25157 window,
25158 cx,
25159 );
25160 })
25161 .unwrap();
25162 cx.run_until_parked();
25163 workspace
25164 .update(cx, |workspace, _, cx| {
25165 let panes = workspace.panes();
25166 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25167 for pane in panes {
25168 let editor = pane
25169 .read(cx)
25170 .active_item()
25171 .and_then(|item| item.downcast::<Editor>())
25172 .expect("Should have opened an editor in each split");
25173 let editor_file = editor
25174 .read(cx)
25175 .buffer()
25176 .read(cx)
25177 .as_singleton()
25178 .expect("test deals with singleton buffers")
25179 .read(cx)
25180 .file()
25181 .expect("test buffese should have a file")
25182 .path();
25183 assert_eq!(
25184 editor_file.as_ref(),
25185 Path::new("first.rs"),
25186 "Both editors should be opened for the same file"
25187 )
25188 }
25189 })
25190 .unwrap();
25191
25192 cx.executor().advance_clock(Duration::from_millis(500));
25193 let save = editor.update_in(cx, |editor, window, cx| {
25194 editor.move_to_end(&MoveToEnd, window, cx);
25195 editor.handle_input("dirty", window, cx);
25196 editor.save(
25197 SaveOptions {
25198 format: true,
25199 autosave: true,
25200 },
25201 project.clone(),
25202 window,
25203 cx,
25204 )
25205 });
25206 save.await.unwrap();
25207
25208 color_request_handle.next().await.unwrap();
25209 cx.run_until_parked();
25210 assert_eq!(
25211 3,
25212 requests_made.load(atomic::Ordering::Acquire),
25213 "Should query for colors once per save and once per formatting after save"
25214 );
25215
25216 drop(editor);
25217 let close = workspace
25218 .update(cx, |workspace, window, cx| {
25219 workspace.active_pane().update(cx, |pane, cx| {
25220 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25221 })
25222 })
25223 .unwrap();
25224 close.await.unwrap();
25225 let close = workspace
25226 .update(cx, |workspace, window, cx| {
25227 workspace.active_pane().update(cx, |pane, cx| {
25228 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25229 })
25230 })
25231 .unwrap();
25232 close.await.unwrap();
25233 assert_eq!(
25234 3,
25235 requests_made.load(atomic::Ordering::Acquire),
25236 "After saving and closing all editors, no extra requests should be made"
25237 );
25238 workspace
25239 .update(cx, |workspace, _, cx| {
25240 assert!(
25241 workspace.active_item(cx).is_none(),
25242 "Should close all editors"
25243 )
25244 })
25245 .unwrap();
25246
25247 workspace
25248 .update(cx, |workspace, window, cx| {
25249 workspace.active_pane().update(cx, |pane, cx| {
25250 pane.navigate_backward(&Default::default(), window, cx);
25251 })
25252 })
25253 .unwrap();
25254 cx.executor().advance_clock(Duration::from_millis(100));
25255 cx.run_until_parked();
25256 let editor = workspace
25257 .update(cx, |workspace, _, cx| {
25258 workspace
25259 .active_item(cx)
25260 .expect("Should have reopened the editor again after navigating back")
25261 .downcast::<Editor>()
25262 .expect("Should be an editor")
25263 })
25264 .unwrap();
25265 color_request_handle.next().await.unwrap();
25266 assert_eq!(
25267 3,
25268 requests_made.load(atomic::Ordering::Acquire),
25269 "Cache should be reused on buffer close and reopen"
25270 );
25271 editor.update(cx, |editor, cx| {
25272 assert_eq!(
25273 vec![expected_color],
25274 extract_color_inlays(editor, cx),
25275 "Should have an initial inlay"
25276 );
25277 });
25278}
25279
25280#[gpui::test]
25281async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25282 init_test(cx, |_| {});
25283 let (editor, cx) = cx.add_window_view(Editor::single_line);
25284 editor.update_in(cx, |editor, window, cx| {
25285 editor.set_text("oops\n\nwow\n", window, cx)
25286 });
25287 cx.run_until_parked();
25288 editor.update(cx, |editor, cx| {
25289 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25290 });
25291 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25292 cx.run_until_parked();
25293 editor.update(cx, |editor, cx| {
25294 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25295 });
25296}
25297
25298#[gpui::test]
25299async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25300 init_test(cx, |_| {});
25301
25302 cx.update(|cx| {
25303 register_project_item::<Editor>(cx);
25304 });
25305
25306 let fs = FakeFs::new(cx.executor());
25307 fs.insert_tree("/root1", json!({})).await;
25308 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25309 .await;
25310
25311 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25312 let (workspace, cx) =
25313 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25314
25315 let worktree_id = project.update(cx, |project, cx| {
25316 project.worktrees(cx).next().unwrap().read(cx).id()
25317 });
25318
25319 let handle = workspace
25320 .update_in(cx, |workspace, window, cx| {
25321 let project_path = (worktree_id, "one.pdf");
25322 workspace.open_path(project_path, None, true, window, cx)
25323 })
25324 .await
25325 .unwrap();
25326
25327 assert_eq!(
25328 handle.to_any().entity_type(),
25329 TypeId::of::<InvalidBufferView>()
25330 );
25331}
25332
25333#[gpui::test]
25334async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25335 init_test(cx, |_| {});
25336
25337 let language = Arc::new(Language::new(
25338 LanguageConfig::default(),
25339 Some(tree_sitter_rust::LANGUAGE.into()),
25340 ));
25341
25342 // Test hierarchical sibling navigation
25343 let text = r#"
25344 fn outer() {
25345 if condition {
25346 let a = 1;
25347 }
25348 let b = 2;
25349 }
25350
25351 fn another() {
25352 let c = 3;
25353 }
25354 "#;
25355
25356 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25357 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25358 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25359
25360 // Wait for parsing to complete
25361 editor
25362 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25363 .await;
25364
25365 editor.update_in(cx, |editor, window, cx| {
25366 // Start by selecting "let a = 1;" inside the if block
25367 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25368 s.select_display_ranges([
25369 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25370 ]);
25371 });
25372
25373 let initial_selection = editor.selections.display_ranges(cx);
25374 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25375
25376 // Test select next sibling - should move up levels to find the next sibling
25377 // Since "let a = 1;" has no siblings in the if block, it should move up
25378 // to find "let b = 2;" which is a sibling of the if block
25379 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25380 let next_selection = editor.selections.display_ranges(cx);
25381
25382 // Should have a selection and it should be different from the initial
25383 assert_eq!(
25384 next_selection.len(),
25385 1,
25386 "Should have one selection after next"
25387 );
25388 assert_ne!(
25389 next_selection[0], initial_selection[0],
25390 "Next sibling selection should be different"
25391 );
25392
25393 // Test hierarchical navigation by going to the end of the current function
25394 // and trying to navigate to the next function
25395 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25396 s.select_display_ranges([
25397 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25398 ]);
25399 });
25400
25401 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25402 let function_next_selection = editor.selections.display_ranges(cx);
25403
25404 // Should move to the next function
25405 assert_eq!(
25406 function_next_selection.len(),
25407 1,
25408 "Should have one selection after function next"
25409 );
25410
25411 // Test select previous sibling navigation
25412 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25413 let prev_selection = editor.selections.display_ranges(cx);
25414
25415 // Should have a selection and it should be different
25416 assert_eq!(
25417 prev_selection.len(),
25418 1,
25419 "Should have one selection after prev"
25420 );
25421 assert_ne!(
25422 prev_selection[0], function_next_selection[0],
25423 "Previous sibling selection should be different from next"
25424 );
25425 });
25426}
25427
25428#[track_caller]
25429fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25430 editor
25431 .all_inlays(cx)
25432 .into_iter()
25433 .filter_map(|inlay| inlay.get_color())
25434 .map(Rgba::from)
25435 .collect()
25436}