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.simulate_keystroke("o");
14282 cx.executor().run_until_parked();
14283 cx.update_editor(|editor, _, _| {
14284 if editor.context_menu.borrow_mut().is_some() {
14285 panic!(
14286 "expected completion menu to be hidden, as words completion threshold is not met still"
14287 );
14288 }
14289 });
14290
14291 cx.simulate_keystroke("w");
14292 cx.executor().run_until_parked();
14293 cx.update_editor(|editor, _, _| {
14294 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14295 {
14296 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14297 } else {
14298 panic!("expected completion menu to be open after the word completions threshold is met");
14299 }
14300 });
14301}
14302
14303fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14304 let position = || lsp::Position {
14305 line: params.text_document_position.position.line,
14306 character: params.text_document_position.position.character,
14307 };
14308 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14309 range: lsp::Range {
14310 start: position(),
14311 end: position(),
14312 },
14313 new_text: text.to_string(),
14314 }))
14315}
14316
14317#[gpui::test]
14318async fn test_multiline_completion(cx: &mut TestAppContext) {
14319 init_test(cx, |_| {});
14320
14321 let fs = FakeFs::new(cx.executor());
14322 fs.insert_tree(
14323 path!("/a"),
14324 json!({
14325 "main.ts": "a",
14326 }),
14327 )
14328 .await;
14329
14330 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14331 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14332 let typescript_language = Arc::new(Language::new(
14333 LanguageConfig {
14334 name: "TypeScript".into(),
14335 matcher: LanguageMatcher {
14336 path_suffixes: vec!["ts".to_string()],
14337 ..LanguageMatcher::default()
14338 },
14339 line_comments: vec!["// ".into()],
14340 ..LanguageConfig::default()
14341 },
14342 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14343 ));
14344 language_registry.add(typescript_language.clone());
14345 let mut fake_servers = language_registry.register_fake_lsp(
14346 "TypeScript",
14347 FakeLspAdapter {
14348 capabilities: lsp::ServerCapabilities {
14349 completion_provider: Some(lsp::CompletionOptions {
14350 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14351 ..lsp::CompletionOptions::default()
14352 }),
14353 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14354 ..lsp::ServerCapabilities::default()
14355 },
14356 // Emulate vtsls label generation
14357 label_for_completion: Some(Box::new(|item, _| {
14358 let text = if let Some(description) = item
14359 .label_details
14360 .as_ref()
14361 .and_then(|label_details| label_details.description.as_ref())
14362 {
14363 format!("{} {}", item.label, description)
14364 } else if let Some(detail) = &item.detail {
14365 format!("{} {}", item.label, detail)
14366 } else {
14367 item.label.clone()
14368 };
14369 let len = text.len();
14370 Some(language::CodeLabel {
14371 text,
14372 runs: Vec::new(),
14373 filter_range: 0..len,
14374 })
14375 })),
14376 ..FakeLspAdapter::default()
14377 },
14378 );
14379 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14380 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14381 let worktree_id = workspace
14382 .update(cx, |workspace, _window, cx| {
14383 workspace.project().update(cx, |project, cx| {
14384 project.worktrees(cx).next().unwrap().read(cx).id()
14385 })
14386 })
14387 .unwrap();
14388 let _buffer = project
14389 .update(cx, |project, cx| {
14390 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14391 })
14392 .await
14393 .unwrap();
14394 let editor = workspace
14395 .update(cx, |workspace, window, cx| {
14396 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14397 })
14398 .unwrap()
14399 .await
14400 .unwrap()
14401 .downcast::<Editor>()
14402 .unwrap();
14403 let fake_server = fake_servers.next().await.unwrap();
14404
14405 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14406 let multiline_label_2 = "a\nb\nc\n";
14407 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14408 let multiline_description = "d\ne\nf\n";
14409 let multiline_detail_2 = "g\nh\ni\n";
14410
14411 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14412 move |params, _| async move {
14413 Ok(Some(lsp::CompletionResponse::Array(vec![
14414 lsp::CompletionItem {
14415 label: multiline_label.to_string(),
14416 text_edit: gen_text_edit(¶ms, "new_text_1"),
14417 ..lsp::CompletionItem::default()
14418 },
14419 lsp::CompletionItem {
14420 label: "single line label 1".to_string(),
14421 detail: Some(multiline_detail.to_string()),
14422 text_edit: gen_text_edit(¶ms, "new_text_2"),
14423 ..lsp::CompletionItem::default()
14424 },
14425 lsp::CompletionItem {
14426 label: "single line label 2".to_string(),
14427 label_details: Some(lsp::CompletionItemLabelDetails {
14428 description: Some(multiline_description.to_string()),
14429 detail: None,
14430 }),
14431 text_edit: gen_text_edit(¶ms, "new_text_2"),
14432 ..lsp::CompletionItem::default()
14433 },
14434 lsp::CompletionItem {
14435 label: multiline_label_2.to_string(),
14436 detail: Some(multiline_detail_2.to_string()),
14437 text_edit: gen_text_edit(¶ms, "new_text_3"),
14438 ..lsp::CompletionItem::default()
14439 },
14440 lsp::CompletionItem {
14441 label: "Label with many spaces and \t but without newlines".to_string(),
14442 detail: Some(
14443 "Details with many spaces and \t but without newlines".to_string(),
14444 ),
14445 text_edit: gen_text_edit(¶ms, "new_text_4"),
14446 ..lsp::CompletionItem::default()
14447 },
14448 ])))
14449 },
14450 );
14451
14452 editor.update_in(cx, |editor, window, cx| {
14453 cx.focus_self(window);
14454 editor.move_to_end(&MoveToEnd, window, cx);
14455 editor.handle_input(".", window, cx);
14456 });
14457 cx.run_until_parked();
14458 completion_handle.next().await.unwrap();
14459
14460 editor.update(cx, |editor, _| {
14461 assert!(editor.context_menu_visible());
14462 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14463 {
14464 let completion_labels = menu
14465 .completions
14466 .borrow()
14467 .iter()
14468 .map(|c| c.label.text.clone())
14469 .collect::<Vec<_>>();
14470 assert_eq!(
14471 completion_labels,
14472 &[
14473 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14474 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14475 "single line label 2 d e f ",
14476 "a b c g h i ",
14477 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14478 ],
14479 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14480 );
14481
14482 for completion in menu
14483 .completions
14484 .borrow()
14485 .iter() {
14486 assert_eq!(
14487 completion.label.filter_range,
14488 0..completion.label.text.len(),
14489 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14490 );
14491 }
14492 } else {
14493 panic!("expected completion menu to be open");
14494 }
14495 });
14496}
14497
14498#[gpui::test]
14499async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14500 init_test(cx, |_| {});
14501 let mut cx = EditorLspTestContext::new_rust(
14502 lsp::ServerCapabilities {
14503 completion_provider: Some(lsp::CompletionOptions {
14504 trigger_characters: Some(vec![".".to_string()]),
14505 ..Default::default()
14506 }),
14507 ..Default::default()
14508 },
14509 cx,
14510 )
14511 .await;
14512 cx.lsp
14513 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14514 Ok(Some(lsp::CompletionResponse::Array(vec![
14515 lsp::CompletionItem {
14516 label: "first".into(),
14517 ..Default::default()
14518 },
14519 lsp::CompletionItem {
14520 label: "last".into(),
14521 ..Default::default()
14522 },
14523 ])))
14524 });
14525 cx.set_state("variableˇ");
14526 cx.simulate_keystroke(".");
14527 cx.executor().run_until_parked();
14528
14529 cx.update_editor(|editor, _, _| {
14530 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14531 {
14532 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14533 } else {
14534 panic!("expected completion menu to be open");
14535 }
14536 });
14537
14538 cx.update_editor(|editor, window, cx| {
14539 editor.move_page_down(&MovePageDown::default(), window, cx);
14540 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14541 {
14542 assert!(
14543 menu.selected_item == 1,
14544 "expected PageDown to select the last item from the context menu"
14545 );
14546 } else {
14547 panic!("expected completion menu to stay open after PageDown");
14548 }
14549 });
14550
14551 cx.update_editor(|editor, window, cx| {
14552 editor.move_page_up(&MovePageUp::default(), window, cx);
14553 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14554 {
14555 assert!(
14556 menu.selected_item == 0,
14557 "expected PageUp to select the first item from the context menu"
14558 );
14559 } else {
14560 panic!("expected completion menu to stay open after PageUp");
14561 }
14562 });
14563}
14564
14565#[gpui::test]
14566async fn test_as_is_completions(cx: &mut TestAppContext) {
14567 init_test(cx, |_| {});
14568 let mut cx = EditorLspTestContext::new_rust(
14569 lsp::ServerCapabilities {
14570 completion_provider: Some(lsp::CompletionOptions {
14571 ..Default::default()
14572 }),
14573 ..Default::default()
14574 },
14575 cx,
14576 )
14577 .await;
14578 cx.lsp
14579 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14580 Ok(Some(lsp::CompletionResponse::Array(vec![
14581 lsp::CompletionItem {
14582 label: "unsafe".into(),
14583 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14584 range: lsp::Range {
14585 start: lsp::Position {
14586 line: 1,
14587 character: 2,
14588 },
14589 end: lsp::Position {
14590 line: 1,
14591 character: 3,
14592 },
14593 },
14594 new_text: "unsafe".to_string(),
14595 })),
14596 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14597 ..Default::default()
14598 },
14599 ])))
14600 });
14601 cx.set_state("fn a() {}\n nˇ");
14602 cx.executor().run_until_parked();
14603 cx.update_editor(|editor, window, cx| {
14604 editor.show_completions(
14605 &ShowCompletions {
14606 trigger: Some("\n".into()),
14607 },
14608 window,
14609 cx,
14610 );
14611 });
14612 cx.executor().run_until_parked();
14613
14614 cx.update_editor(|editor, window, cx| {
14615 editor.confirm_completion(&Default::default(), window, cx)
14616 });
14617 cx.executor().run_until_parked();
14618 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14619}
14620
14621#[gpui::test]
14622async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14623 init_test(cx, |_| {});
14624 let language =
14625 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14626 let mut cx = EditorLspTestContext::new(
14627 language,
14628 lsp::ServerCapabilities {
14629 completion_provider: Some(lsp::CompletionOptions {
14630 ..lsp::CompletionOptions::default()
14631 }),
14632 ..lsp::ServerCapabilities::default()
14633 },
14634 cx,
14635 )
14636 .await;
14637
14638 cx.set_state(
14639 "#ifndef BAR_H
14640#define BAR_H
14641
14642#include <stdbool.h>
14643
14644int fn_branch(bool do_branch1, bool do_branch2);
14645
14646#endif // BAR_H
14647ˇ",
14648 );
14649 cx.executor().run_until_parked();
14650 cx.update_editor(|editor, window, cx| {
14651 editor.handle_input("#", window, cx);
14652 });
14653 cx.executor().run_until_parked();
14654 cx.update_editor(|editor, window, cx| {
14655 editor.handle_input("i", window, cx);
14656 });
14657 cx.executor().run_until_parked();
14658 cx.update_editor(|editor, window, cx| {
14659 editor.handle_input("n", window, cx);
14660 });
14661 cx.executor().run_until_parked();
14662 cx.assert_editor_state(
14663 "#ifndef BAR_H
14664#define BAR_H
14665
14666#include <stdbool.h>
14667
14668int fn_branch(bool do_branch1, bool do_branch2);
14669
14670#endif // BAR_H
14671#inˇ",
14672 );
14673
14674 cx.lsp
14675 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14676 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14677 is_incomplete: false,
14678 item_defaults: None,
14679 items: vec![lsp::CompletionItem {
14680 kind: Some(lsp::CompletionItemKind::SNIPPET),
14681 label_details: Some(lsp::CompletionItemLabelDetails {
14682 detail: Some("header".to_string()),
14683 description: None,
14684 }),
14685 label: " include".to_string(),
14686 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14687 range: lsp::Range {
14688 start: lsp::Position {
14689 line: 8,
14690 character: 1,
14691 },
14692 end: lsp::Position {
14693 line: 8,
14694 character: 1,
14695 },
14696 },
14697 new_text: "include \"$0\"".to_string(),
14698 })),
14699 sort_text: Some("40b67681include".to_string()),
14700 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14701 filter_text: Some("include".to_string()),
14702 insert_text: Some("include \"$0\"".to_string()),
14703 ..lsp::CompletionItem::default()
14704 }],
14705 })))
14706 });
14707 cx.update_editor(|editor, window, cx| {
14708 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14709 });
14710 cx.executor().run_until_parked();
14711 cx.update_editor(|editor, window, cx| {
14712 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14713 });
14714 cx.executor().run_until_parked();
14715 cx.assert_editor_state(
14716 "#ifndef BAR_H
14717#define BAR_H
14718
14719#include <stdbool.h>
14720
14721int fn_branch(bool do_branch1, bool do_branch2);
14722
14723#endif // BAR_H
14724#include \"ˇ\"",
14725 );
14726
14727 cx.lsp
14728 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14729 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14730 is_incomplete: true,
14731 item_defaults: None,
14732 items: vec![lsp::CompletionItem {
14733 kind: Some(lsp::CompletionItemKind::FILE),
14734 label: "AGL/".to_string(),
14735 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14736 range: lsp::Range {
14737 start: lsp::Position {
14738 line: 8,
14739 character: 10,
14740 },
14741 end: lsp::Position {
14742 line: 8,
14743 character: 11,
14744 },
14745 },
14746 new_text: "AGL/".to_string(),
14747 })),
14748 sort_text: Some("40b67681AGL/".to_string()),
14749 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14750 filter_text: Some("AGL/".to_string()),
14751 insert_text: Some("AGL/".to_string()),
14752 ..lsp::CompletionItem::default()
14753 }],
14754 })))
14755 });
14756 cx.update_editor(|editor, window, cx| {
14757 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14758 });
14759 cx.executor().run_until_parked();
14760 cx.update_editor(|editor, window, cx| {
14761 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14762 });
14763 cx.executor().run_until_parked();
14764 cx.assert_editor_state(
14765 r##"#ifndef BAR_H
14766#define BAR_H
14767
14768#include <stdbool.h>
14769
14770int fn_branch(bool do_branch1, bool do_branch2);
14771
14772#endif // BAR_H
14773#include "AGL/ˇ"##,
14774 );
14775
14776 cx.update_editor(|editor, window, cx| {
14777 editor.handle_input("\"", window, cx);
14778 });
14779 cx.executor().run_until_parked();
14780 cx.assert_editor_state(
14781 r##"#ifndef BAR_H
14782#define BAR_H
14783
14784#include <stdbool.h>
14785
14786int fn_branch(bool do_branch1, bool do_branch2);
14787
14788#endif // BAR_H
14789#include "AGL/"ˇ"##,
14790 );
14791}
14792
14793#[gpui::test]
14794async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
14795 init_test(cx, |_| {});
14796
14797 let mut cx = EditorLspTestContext::new_rust(
14798 lsp::ServerCapabilities {
14799 completion_provider: Some(lsp::CompletionOptions {
14800 trigger_characters: Some(vec![".".to_string()]),
14801 resolve_provider: Some(true),
14802 ..Default::default()
14803 }),
14804 ..Default::default()
14805 },
14806 cx,
14807 )
14808 .await;
14809
14810 cx.set_state("fn main() { let a = 2ˇ; }");
14811 cx.simulate_keystroke(".");
14812 let completion_item = lsp::CompletionItem {
14813 label: "Some".into(),
14814 kind: Some(lsp::CompletionItemKind::SNIPPET),
14815 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14816 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14817 kind: lsp::MarkupKind::Markdown,
14818 value: "```rust\nSome(2)\n```".to_string(),
14819 })),
14820 deprecated: Some(false),
14821 sort_text: Some("Some".to_string()),
14822 filter_text: Some("Some".to_string()),
14823 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14824 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14825 range: lsp::Range {
14826 start: lsp::Position {
14827 line: 0,
14828 character: 22,
14829 },
14830 end: lsp::Position {
14831 line: 0,
14832 character: 22,
14833 },
14834 },
14835 new_text: "Some(2)".to_string(),
14836 })),
14837 additional_text_edits: Some(vec![lsp::TextEdit {
14838 range: lsp::Range {
14839 start: lsp::Position {
14840 line: 0,
14841 character: 20,
14842 },
14843 end: lsp::Position {
14844 line: 0,
14845 character: 22,
14846 },
14847 },
14848 new_text: "".to_string(),
14849 }]),
14850 ..Default::default()
14851 };
14852
14853 let closure_completion_item = completion_item.clone();
14854 let counter = Arc::new(AtomicUsize::new(0));
14855 let counter_clone = counter.clone();
14856 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14857 let task_completion_item = closure_completion_item.clone();
14858 counter_clone.fetch_add(1, atomic::Ordering::Release);
14859 async move {
14860 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14861 is_incomplete: true,
14862 item_defaults: None,
14863 items: vec![task_completion_item],
14864 })))
14865 }
14866 });
14867
14868 cx.condition(|editor, _| editor.context_menu_visible())
14869 .await;
14870 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
14871 assert!(request.next().await.is_some());
14872 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14873
14874 cx.simulate_keystrokes("S o m");
14875 cx.condition(|editor, _| editor.context_menu_visible())
14876 .await;
14877 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
14878 assert!(request.next().await.is_some());
14879 assert!(request.next().await.is_some());
14880 assert!(request.next().await.is_some());
14881 request.close();
14882 assert!(request.next().await.is_none());
14883 assert_eq!(
14884 counter.load(atomic::Ordering::Acquire),
14885 4,
14886 "With the completions menu open, only one LSP request should happen per input"
14887 );
14888}
14889
14890#[gpui::test]
14891async fn test_toggle_comment(cx: &mut TestAppContext) {
14892 init_test(cx, |_| {});
14893 let mut cx = EditorTestContext::new(cx).await;
14894 let language = Arc::new(Language::new(
14895 LanguageConfig {
14896 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14897 ..Default::default()
14898 },
14899 Some(tree_sitter_rust::LANGUAGE.into()),
14900 ));
14901 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14902
14903 // If multiple selections intersect a line, the line is only toggled once.
14904 cx.set_state(indoc! {"
14905 fn a() {
14906 «//b();
14907 ˇ»// «c();
14908 //ˇ» d();
14909 }
14910 "});
14911
14912 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14913
14914 cx.assert_editor_state(indoc! {"
14915 fn a() {
14916 «b();
14917 c();
14918 ˇ» d();
14919 }
14920 "});
14921
14922 // The comment prefix is inserted at the same column for every line in a
14923 // selection.
14924 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14925
14926 cx.assert_editor_state(indoc! {"
14927 fn a() {
14928 // «b();
14929 // c();
14930 ˇ»// d();
14931 }
14932 "});
14933
14934 // If a selection ends at the beginning of a line, that line is not toggled.
14935 cx.set_selections_state(indoc! {"
14936 fn a() {
14937 // b();
14938 «// c();
14939 ˇ» // d();
14940 }
14941 "});
14942
14943 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14944
14945 cx.assert_editor_state(indoc! {"
14946 fn a() {
14947 // b();
14948 «c();
14949 ˇ» // d();
14950 }
14951 "});
14952
14953 // If a selection span a single line and is empty, the line is toggled.
14954 cx.set_state(indoc! {"
14955 fn a() {
14956 a();
14957 b();
14958 ˇ
14959 }
14960 "});
14961
14962 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14963
14964 cx.assert_editor_state(indoc! {"
14965 fn a() {
14966 a();
14967 b();
14968 //•ˇ
14969 }
14970 "});
14971
14972 // If a selection span multiple lines, empty lines are not toggled.
14973 cx.set_state(indoc! {"
14974 fn a() {
14975 «a();
14976
14977 c();ˇ»
14978 }
14979 "});
14980
14981 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14982
14983 cx.assert_editor_state(indoc! {"
14984 fn a() {
14985 // «a();
14986
14987 // c();ˇ»
14988 }
14989 "});
14990
14991 // If a selection includes multiple comment prefixes, all lines are uncommented.
14992 cx.set_state(indoc! {"
14993 fn a() {
14994 «// a();
14995 /// b();
14996 //! c();ˇ»
14997 }
14998 "});
14999
15000 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15001
15002 cx.assert_editor_state(indoc! {"
15003 fn a() {
15004 «a();
15005 b();
15006 c();ˇ»
15007 }
15008 "});
15009}
15010
15011#[gpui::test]
15012async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15013 init_test(cx, |_| {});
15014 let mut cx = EditorTestContext::new(cx).await;
15015 let language = Arc::new(Language::new(
15016 LanguageConfig {
15017 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15018 ..Default::default()
15019 },
15020 Some(tree_sitter_rust::LANGUAGE.into()),
15021 ));
15022 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15023
15024 let toggle_comments = &ToggleComments {
15025 advance_downwards: false,
15026 ignore_indent: true,
15027 };
15028
15029 // If multiple selections intersect a line, the line is only toggled once.
15030 cx.set_state(indoc! {"
15031 fn a() {
15032 // «b();
15033 // c();
15034 // ˇ» d();
15035 }
15036 "});
15037
15038 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15039
15040 cx.assert_editor_state(indoc! {"
15041 fn a() {
15042 «b();
15043 c();
15044 ˇ» d();
15045 }
15046 "});
15047
15048 // The comment prefix is inserted at the beginning of each line
15049 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15050
15051 cx.assert_editor_state(indoc! {"
15052 fn a() {
15053 // «b();
15054 // c();
15055 // ˇ» d();
15056 }
15057 "});
15058
15059 // If a selection ends at the beginning of a line, that line is not toggled.
15060 cx.set_selections_state(indoc! {"
15061 fn a() {
15062 // b();
15063 // «c();
15064 ˇ»// d();
15065 }
15066 "});
15067
15068 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15069
15070 cx.assert_editor_state(indoc! {"
15071 fn a() {
15072 // b();
15073 «c();
15074 ˇ»// d();
15075 }
15076 "});
15077
15078 // If a selection span a single line and is empty, the line is toggled.
15079 cx.set_state(indoc! {"
15080 fn a() {
15081 a();
15082 b();
15083 ˇ
15084 }
15085 "});
15086
15087 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15088
15089 cx.assert_editor_state(indoc! {"
15090 fn a() {
15091 a();
15092 b();
15093 //ˇ
15094 }
15095 "});
15096
15097 // If a selection span multiple lines, empty lines are not toggled.
15098 cx.set_state(indoc! {"
15099 fn a() {
15100 «a();
15101
15102 c();ˇ»
15103 }
15104 "});
15105
15106 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15107
15108 cx.assert_editor_state(indoc! {"
15109 fn a() {
15110 // «a();
15111
15112 // c();ˇ»
15113 }
15114 "});
15115
15116 // If a selection includes multiple comment prefixes, all lines are uncommented.
15117 cx.set_state(indoc! {"
15118 fn a() {
15119 // «a();
15120 /// b();
15121 //! c();ˇ»
15122 }
15123 "});
15124
15125 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15126
15127 cx.assert_editor_state(indoc! {"
15128 fn a() {
15129 «a();
15130 b();
15131 c();ˇ»
15132 }
15133 "});
15134}
15135
15136#[gpui::test]
15137async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15138 init_test(cx, |_| {});
15139
15140 let language = Arc::new(Language::new(
15141 LanguageConfig {
15142 line_comments: vec!["// ".into()],
15143 ..Default::default()
15144 },
15145 Some(tree_sitter_rust::LANGUAGE.into()),
15146 ));
15147
15148 let mut cx = EditorTestContext::new(cx).await;
15149
15150 cx.language_registry().add(language.clone());
15151 cx.update_buffer(|buffer, cx| {
15152 buffer.set_language(Some(language), cx);
15153 });
15154
15155 let toggle_comments = &ToggleComments {
15156 advance_downwards: true,
15157 ignore_indent: false,
15158 };
15159
15160 // Single cursor on one line -> advance
15161 // Cursor moves horizontally 3 characters as well on non-blank line
15162 cx.set_state(indoc!(
15163 "fn a() {
15164 ˇdog();
15165 cat();
15166 }"
15167 ));
15168 cx.update_editor(|editor, window, cx| {
15169 editor.toggle_comments(toggle_comments, window, cx);
15170 });
15171 cx.assert_editor_state(indoc!(
15172 "fn a() {
15173 // dog();
15174 catˇ();
15175 }"
15176 ));
15177
15178 // Single selection on one line -> don't advance
15179 cx.set_state(indoc!(
15180 "fn a() {
15181 «dog()ˇ»;
15182 cat();
15183 }"
15184 ));
15185 cx.update_editor(|editor, window, cx| {
15186 editor.toggle_comments(toggle_comments, window, cx);
15187 });
15188 cx.assert_editor_state(indoc!(
15189 "fn a() {
15190 // «dog()ˇ»;
15191 cat();
15192 }"
15193 ));
15194
15195 // Multiple cursors on one line -> advance
15196 cx.set_state(indoc!(
15197 "fn a() {
15198 ˇdˇog();
15199 cat();
15200 }"
15201 ));
15202 cx.update_editor(|editor, window, cx| {
15203 editor.toggle_comments(toggle_comments, window, cx);
15204 });
15205 cx.assert_editor_state(indoc!(
15206 "fn a() {
15207 // dog();
15208 catˇ(ˇ);
15209 }"
15210 ));
15211
15212 // Multiple cursors on one line, with selection -> don't advance
15213 cx.set_state(indoc!(
15214 "fn a() {
15215 ˇdˇog«()ˇ»;
15216 cat();
15217 }"
15218 ));
15219 cx.update_editor(|editor, window, cx| {
15220 editor.toggle_comments(toggle_comments, window, cx);
15221 });
15222 cx.assert_editor_state(indoc!(
15223 "fn a() {
15224 // ˇdˇog«()ˇ»;
15225 cat();
15226 }"
15227 ));
15228
15229 // Single cursor on one line -> advance
15230 // Cursor moves to column 0 on blank line
15231 cx.set_state(indoc!(
15232 "fn a() {
15233 ˇdog();
15234
15235 cat();
15236 }"
15237 ));
15238 cx.update_editor(|editor, window, cx| {
15239 editor.toggle_comments(toggle_comments, window, cx);
15240 });
15241 cx.assert_editor_state(indoc!(
15242 "fn a() {
15243 // dog();
15244 ˇ
15245 cat();
15246 }"
15247 ));
15248
15249 // Single cursor on one line -> advance
15250 // Cursor starts and ends at column 0
15251 cx.set_state(indoc!(
15252 "fn a() {
15253 ˇ dog();
15254 cat();
15255 }"
15256 ));
15257 cx.update_editor(|editor, window, cx| {
15258 editor.toggle_comments(toggle_comments, window, cx);
15259 });
15260 cx.assert_editor_state(indoc!(
15261 "fn a() {
15262 // dog();
15263 ˇ cat();
15264 }"
15265 ));
15266}
15267
15268#[gpui::test]
15269async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15270 init_test(cx, |_| {});
15271
15272 let mut cx = EditorTestContext::new(cx).await;
15273
15274 let html_language = Arc::new(
15275 Language::new(
15276 LanguageConfig {
15277 name: "HTML".into(),
15278 block_comment: Some(BlockCommentConfig {
15279 start: "<!-- ".into(),
15280 prefix: "".into(),
15281 end: " -->".into(),
15282 tab_size: 0,
15283 }),
15284 ..Default::default()
15285 },
15286 Some(tree_sitter_html::LANGUAGE.into()),
15287 )
15288 .with_injection_query(
15289 r#"
15290 (script_element
15291 (raw_text) @injection.content
15292 (#set! injection.language "javascript"))
15293 "#,
15294 )
15295 .unwrap(),
15296 );
15297
15298 let javascript_language = Arc::new(Language::new(
15299 LanguageConfig {
15300 name: "JavaScript".into(),
15301 line_comments: vec!["// ".into()],
15302 ..Default::default()
15303 },
15304 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15305 ));
15306
15307 cx.language_registry().add(html_language.clone());
15308 cx.language_registry().add(javascript_language);
15309 cx.update_buffer(|buffer, cx| {
15310 buffer.set_language(Some(html_language), cx);
15311 });
15312
15313 // Toggle comments for empty selections
15314 cx.set_state(
15315 &r#"
15316 <p>A</p>ˇ
15317 <p>B</p>ˇ
15318 <p>C</p>ˇ
15319 "#
15320 .unindent(),
15321 );
15322 cx.update_editor(|editor, window, cx| {
15323 editor.toggle_comments(&ToggleComments::default(), window, cx)
15324 });
15325 cx.assert_editor_state(
15326 &r#"
15327 <!-- <p>A</p>ˇ -->
15328 <!-- <p>B</p>ˇ -->
15329 <!-- <p>C</p>ˇ -->
15330 "#
15331 .unindent(),
15332 );
15333 cx.update_editor(|editor, window, cx| {
15334 editor.toggle_comments(&ToggleComments::default(), window, cx)
15335 });
15336 cx.assert_editor_state(
15337 &r#"
15338 <p>A</p>ˇ
15339 <p>B</p>ˇ
15340 <p>C</p>ˇ
15341 "#
15342 .unindent(),
15343 );
15344
15345 // Toggle comments for mixture of empty and non-empty selections, where
15346 // multiple selections occupy a given line.
15347 cx.set_state(
15348 &r#"
15349 <p>A«</p>
15350 <p>ˇ»B</p>ˇ
15351 <p>C«</p>
15352 <p>ˇ»D</p>ˇ
15353 "#
15354 .unindent(),
15355 );
15356
15357 cx.update_editor(|editor, window, cx| {
15358 editor.toggle_comments(&ToggleComments::default(), window, cx)
15359 });
15360 cx.assert_editor_state(
15361 &r#"
15362 <!-- <p>A«</p>
15363 <p>ˇ»B</p>ˇ -->
15364 <!-- <p>C«</p>
15365 <p>ˇ»D</p>ˇ -->
15366 "#
15367 .unindent(),
15368 );
15369 cx.update_editor(|editor, window, cx| {
15370 editor.toggle_comments(&ToggleComments::default(), window, cx)
15371 });
15372 cx.assert_editor_state(
15373 &r#"
15374 <p>A«</p>
15375 <p>ˇ»B</p>ˇ
15376 <p>C«</p>
15377 <p>ˇ»D</p>ˇ
15378 "#
15379 .unindent(),
15380 );
15381
15382 // Toggle comments when different languages are active for different
15383 // selections.
15384 cx.set_state(
15385 &r#"
15386 ˇ<script>
15387 ˇvar x = new Y();
15388 ˇ</script>
15389 "#
15390 .unindent(),
15391 );
15392 cx.executor().run_until_parked();
15393 cx.update_editor(|editor, window, cx| {
15394 editor.toggle_comments(&ToggleComments::default(), window, cx)
15395 });
15396 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15397 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15398 cx.assert_editor_state(
15399 &r#"
15400 <!-- ˇ<script> -->
15401 // ˇvar x = new Y();
15402 <!-- ˇ</script> -->
15403 "#
15404 .unindent(),
15405 );
15406}
15407
15408#[gpui::test]
15409fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15410 init_test(cx, |_| {});
15411
15412 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15413 let multibuffer = cx.new(|cx| {
15414 let mut multibuffer = MultiBuffer::new(ReadWrite);
15415 multibuffer.push_excerpts(
15416 buffer.clone(),
15417 [
15418 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15419 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15420 ],
15421 cx,
15422 );
15423 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15424 multibuffer
15425 });
15426
15427 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15428 editor.update_in(cx, |editor, window, cx| {
15429 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15430 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15431 s.select_ranges([
15432 Point::new(0, 0)..Point::new(0, 0),
15433 Point::new(1, 0)..Point::new(1, 0),
15434 ])
15435 });
15436
15437 editor.handle_input("X", window, cx);
15438 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15439 assert_eq!(
15440 editor.selections.ranges(cx),
15441 [
15442 Point::new(0, 1)..Point::new(0, 1),
15443 Point::new(1, 1)..Point::new(1, 1),
15444 ]
15445 );
15446
15447 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15448 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15449 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15450 });
15451 editor.backspace(&Default::default(), window, cx);
15452 assert_eq!(editor.text(cx), "Xa\nbbb");
15453 assert_eq!(
15454 editor.selections.ranges(cx),
15455 [Point::new(1, 0)..Point::new(1, 0)]
15456 );
15457
15458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15459 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15460 });
15461 editor.backspace(&Default::default(), window, cx);
15462 assert_eq!(editor.text(cx), "X\nbb");
15463 assert_eq!(
15464 editor.selections.ranges(cx),
15465 [Point::new(0, 1)..Point::new(0, 1)]
15466 );
15467 });
15468}
15469
15470#[gpui::test]
15471fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15472 init_test(cx, |_| {});
15473
15474 let markers = vec![('[', ']').into(), ('(', ')').into()];
15475 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15476 indoc! {"
15477 [aaaa
15478 (bbbb]
15479 cccc)",
15480 },
15481 markers.clone(),
15482 );
15483 let excerpt_ranges = markers.into_iter().map(|marker| {
15484 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15485 ExcerptRange::new(context)
15486 });
15487 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15488 let multibuffer = cx.new(|cx| {
15489 let mut multibuffer = MultiBuffer::new(ReadWrite);
15490 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15491 multibuffer
15492 });
15493
15494 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15495 editor.update_in(cx, |editor, window, cx| {
15496 let (expected_text, selection_ranges) = marked_text_ranges(
15497 indoc! {"
15498 aaaa
15499 bˇbbb
15500 bˇbbˇb
15501 cccc"
15502 },
15503 true,
15504 );
15505 assert_eq!(editor.text(cx), expected_text);
15506 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15507 s.select_ranges(selection_ranges)
15508 });
15509
15510 editor.handle_input("X", window, cx);
15511
15512 let (expected_text, expected_selections) = marked_text_ranges(
15513 indoc! {"
15514 aaaa
15515 bXˇbbXb
15516 bXˇbbXˇb
15517 cccc"
15518 },
15519 false,
15520 );
15521 assert_eq!(editor.text(cx), expected_text);
15522 assert_eq!(editor.selections.ranges(cx), expected_selections);
15523
15524 editor.newline(&Newline, window, cx);
15525 let (expected_text, expected_selections) = marked_text_ranges(
15526 indoc! {"
15527 aaaa
15528 bX
15529 ˇbbX
15530 b
15531 bX
15532 ˇbbX
15533 ˇb
15534 cccc"
15535 },
15536 false,
15537 );
15538 assert_eq!(editor.text(cx), expected_text);
15539 assert_eq!(editor.selections.ranges(cx), expected_selections);
15540 });
15541}
15542
15543#[gpui::test]
15544fn test_refresh_selections(cx: &mut TestAppContext) {
15545 init_test(cx, |_| {});
15546
15547 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15548 let mut excerpt1_id = None;
15549 let multibuffer = cx.new(|cx| {
15550 let mut multibuffer = MultiBuffer::new(ReadWrite);
15551 excerpt1_id = multibuffer
15552 .push_excerpts(
15553 buffer.clone(),
15554 [
15555 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15556 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15557 ],
15558 cx,
15559 )
15560 .into_iter()
15561 .next();
15562 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15563 multibuffer
15564 });
15565
15566 let editor = cx.add_window(|window, cx| {
15567 let mut editor = build_editor(multibuffer.clone(), window, cx);
15568 let snapshot = editor.snapshot(window, cx);
15569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15570 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15571 });
15572 editor.begin_selection(
15573 Point::new(2, 1).to_display_point(&snapshot),
15574 true,
15575 1,
15576 window,
15577 cx,
15578 );
15579 assert_eq!(
15580 editor.selections.ranges(cx),
15581 [
15582 Point::new(1, 3)..Point::new(1, 3),
15583 Point::new(2, 1)..Point::new(2, 1),
15584 ]
15585 );
15586 editor
15587 });
15588
15589 // Refreshing selections is a no-op when excerpts haven't changed.
15590 _ = editor.update(cx, |editor, window, cx| {
15591 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15592 assert_eq!(
15593 editor.selections.ranges(cx),
15594 [
15595 Point::new(1, 3)..Point::new(1, 3),
15596 Point::new(2, 1)..Point::new(2, 1),
15597 ]
15598 );
15599 });
15600
15601 multibuffer.update(cx, |multibuffer, cx| {
15602 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15603 });
15604 _ = editor.update(cx, |editor, window, cx| {
15605 // Removing an excerpt causes the first selection to become degenerate.
15606 assert_eq!(
15607 editor.selections.ranges(cx),
15608 [
15609 Point::new(0, 0)..Point::new(0, 0),
15610 Point::new(0, 1)..Point::new(0, 1)
15611 ]
15612 );
15613
15614 // Refreshing selections will relocate the first selection to the original buffer
15615 // location.
15616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15617 assert_eq!(
15618 editor.selections.ranges(cx),
15619 [
15620 Point::new(0, 1)..Point::new(0, 1),
15621 Point::new(0, 3)..Point::new(0, 3)
15622 ]
15623 );
15624 assert!(editor.selections.pending_anchor().is_some());
15625 });
15626}
15627
15628#[gpui::test]
15629fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15630 init_test(cx, |_| {});
15631
15632 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15633 let mut excerpt1_id = None;
15634 let multibuffer = cx.new(|cx| {
15635 let mut multibuffer = MultiBuffer::new(ReadWrite);
15636 excerpt1_id = multibuffer
15637 .push_excerpts(
15638 buffer.clone(),
15639 [
15640 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15641 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15642 ],
15643 cx,
15644 )
15645 .into_iter()
15646 .next();
15647 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15648 multibuffer
15649 });
15650
15651 let editor = cx.add_window(|window, cx| {
15652 let mut editor = build_editor(multibuffer.clone(), window, cx);
15653 let snapshot = editor.snapshot(window, cx);
15654 editor.begin_selection(
15655 Point::new(1, 3).to_display_point(&snapshot),
15656 false,
15657 1,
15658 window,
15659 cx,
15660 );
15661 assert_eq!(
15662 editor.selections.ranges(cx),
15663 [Point::new(1, 3)..Point::new(1, 3)]
15664 );
15665 editor
15666 });
15667
15668 multibuffer.update(cx, |multibuffer, cx| {
15669 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15670 });
15671 _ = editor.update(cx, |editor, window, cx| {
15672 assert_eq!(
15673 editor.selections.ranges(cx),
15674 [Point::new(0, 0)..Point::new(0, 0)]
15675 );
15676
15677 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15678 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15679 assert_eq!(
15680 editor.selections.ranges(cx),
15681 [Point::new(0, 3)..Point::new(0, 3)]
15682 );
15683 assert!(editor.selections.pending_anchor().is_some());
15684 });
15685}
15686
15687#[gpui::test]
15688async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15689 init_test(cx, |_| {});
15690
15691 let language = Arc::new(
15692 Language::new(
15693 LanguageConfig {
15694 brackets: BracketPairConfig {
15695 pairs: vec![
15696 BracketPair {
15697 start: "{".to_string(),
15698 end: "}".to_string(),
15699 close: true,
15700 surround: true,
15701 newline: true,
15702 },
15703 BracketPair {
15704 start: "/* ".to_string(),
15705 end: " */".to_string(),
15706 close: true,
15707 surround: true,
15708 newline: true,
15709 },
15710 ],
15711 ..Default::default()
15712 },
15713 ..Default::default()
15714 },
15715 Some(tree_sitter_rust::LANGUAGE.into()),
15716 )
15717 .with_indents_query("")
15718 .unwrap(),
15719 );
15720
15721 let text = concat!(
15722 "{ }\n", //
15723 " x\n", //
15724 " /* */\n", //
15725 "x\n", //
15726 "{{} }\n", //
15727 );
15728
15729 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15730 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15731 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15732 editor
15733 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
15734 .await;
15735
15736 editor.update_in(cx, |editor, window, cx| {
15737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15738 s.select_display_ranges([
15739 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
15740 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
15741 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
15742 ])
15743 });
15744 editor.newline(&Newline, window, cx);
15745
15746 assert_eq!(
15747 editor.buffer().read(cx).read(cx).text(),
15748 concat!(
15749 "{ \n", // Suppress rustfmt
15750 "\n", //
15751 "}\n", //
15752 " x\n", //
15753 " /* \n", //
15754 " \n", //
15755 " */\n", //
15756 "x\n", //
15757 "{{} \n", //
15758 "}\n", //
15759 )
15760 );
15761 });
15762}
15763
15764#[gpui::test]
15765fn test_highlighted_ranges(cx: &mut TestAppContext) {
15766 init_test(cx, |_| {});
15767
15768 let editor = cx.add_window(|window, cx| {
15769 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
15770 build_editor(buffer, window, cx)
15771 });
15772
15773 _ = editor.update(cx, |editor, window, cx| {
15774 struct Type1;
15775 struct Type2;
15776
15777 let buffer = editor.buffer.read(cx).snapshot(cx);
15778
15779 let anchor_range =
15780 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
15781
15782 editor.highlight_background::<Type1>(
15783 &[
15784 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
15785 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
15786 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
15787 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
15788 ],
15789 |_| Hsla::red(),
15790 cx,
15791 );
15792 editor.highlight_background::<Type2>(
15793 &[
15794 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
15795 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
15796 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
15797 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
15798 ],
15799 |_| Hsla::green(),
15800 cx,
15801 );
15802
15803 let snapshot = editor.snapshot(window, cx);
15804 let highlighted_ranges = editor.sorted_background_highlights_in_range(
15805 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
15806 &snapshot,
15807 cx.theme(),
15808 );
15809 assert_eq!(
15810 highlighted_ranges,
15811 &[
15812 (
15813 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
15814 Hsla::green(),
15815 ),
15816 (
15817 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
15818 Hsla::red(),
15819 ),
15820 (
15821 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
15822 Hsla::green(),
15823 ),
15824 (
15825 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15826 Hsla::red(),
15827 ),
15828 ]
15829 );
15830 assert_eq!(
15831 editor.sorted_background_highlights_in_range(
15832 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
15833 &snapshot,
15834 cx.theme(),
15835 ),
15836 &[(
15837 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15838 Hsla::red(),
15839 )]
15840 );
15841 });
15842}
15843
15844#[gpui::test]
15845async fn test_following(cx: &mut TestAppContext) {
15846 init_test(cx, |_| {});
15847
15848 let fs = FakeFs::new(cx.executor());
15849 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15850
15851 let buffer = project.update(cx, |project, cx| {
15852 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
15853 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
15854 });
15855 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
15856 let follower = cx.update(|cx| {
15857 cx.open_window(
15858 WindowOptions {
15859 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
15860 gpui::Point::new(px(0.), px(0.)),
15861 gpui::Point::new(px(10.), px(80.)),
15862 ))),
15863 ..Default::default()
15864 },
15865 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
15866 )
15867 .unwrap()
15868 });
15869
15870 let is_still_following = Rc::new(RefCell::new(true));
15871 let follower_edit_event_count = Rc::new(RefCell::new(0));
15872 let pending_update = Rc::new(RefCell::new(None));
15873 let leader_entity = leader.root(cx).unwrap();
15874 let follower_entity = follower.root(cx).unwrap();
15875 _ = follower.update(cx, {
15876 let update = pending_update.clone();
15877 let is_still_following = is_still_following.clone();
15878 let follower_edit_event_count = follower_edit_event_count.clone();
15879 |_, window, cx| {
15880 cx.subscribe_in(
15881 &leader_entity,
15882 window,
15883 move |_, leader, event, window, cx| {
15884 leader.read(cx).add_event_to_update_proto(
15885 event,
15886 &mut update.borrow_mut(),
15887 window,
15888 cx,
15889 );
15890 },
15891 )
15892 .detach();
15893
15894 cx.subscribe_in(
15895 &follower_entity,
15896 window,
15897 move |_, _, event: &EditorEvent, _window, _cx| {
15898 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
15899 *is_still_following.borrow_mut() = false;
15900 }
15901
15902 if let EditorEvent::BufferEdited = event {
15903 *follower_edit_event_count.borrow_mut() += 1;
15904 }
15905 },
15906 )
15907 .detach();
15908 }
15909 });
15910
15911 // Update the selections only
15912 _ = leader.update(cx, |leader, window, cx| {
15913 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15914 s.select_ranges([1..1])
15915 });
15916 });
15917 follower
15918 .update(cx, |follower, window, cx| {
15919 follower.apply_update_proto(
15920 &project,
15921 pending_update.borrow_mut().take().unwrap(),
15922 window,
15923 cx,
15924 )
15925 })
15926 .unwrap()
15927 .await
15928 .unwrap();
15929 _ = follower.update(cx, |follower, _, cx| {
15930 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15931 });
15932 assert!(*is_still_following.borrow());
15933 assert_eq!(*follower_edit_event_count.borrow(), 0);
15934
15935 // Update the scroll position only
15936 _ = leader.update(cx, |leader, window, cx| {
15937 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15938 });
15939 follower
15940 .update(cx, |follower, window, cx| {
15941 follower.apply_update_proto(
15942 &project,
15943 pending_update.borrow_mut().take().unwrap(),
15944 window,
15945 cx,
15946 )
15947 })
15948 .unwrap()
15949 .await
15950 .unwrap();
15951 assert_eq!(
15952 follower
15953 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15954 .unwrap(),
15955 gpui::Point::new(1.5, 3.5)
15956 );
15957 assert!(*is_still_following.borrow());
15958 assert_eq!(*follower_edit_event_count.borrow(), 0);
15959
15960 // Update the selections and scroll position. The follower's scroll position is updated
15961 // via autoscroll, not via the leader's exact scroll position.
15962 _ = leader.update(cx, |leader, window, cx| {
15963 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15964 s.select_ranges([0..0])
15965 });
15966 leader.request_autoscroll(Autoscroll::newest(), cx);
15967 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15968 });
15969 follower
15970 .update(cx, |follower, window, cx| {
15971 follower.apply_update_proto(
15972 &project,
15973 pending_update.borrow_mut().take().unwrap(),
15974 window,
15975 cx,
15976 )
15977 })
15978 .unwrap()
15979 .await
15980 .unwrap();
15981 _ = follower.update(cx, |follower, _, cx| {
15982 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15983 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15984 });
15985 assert!(*is_still_following.borrow());
15986
15987 // Creating a pending selection that precedes another selection
15988 _ = leader.update(cx, |leader, window, cx| {
15989 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15990 s.select_ranges([1..1])
15991 });
15992 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15993 });
15994 follower
15995 .update(cx, |follower, window, cx| {
15996 follower.apply_update_proto(
15997 &project,
15998 pending_update.borrow_mut().take().unwrap(),
15999 window,
16000 cx,
16001 )
16002 })
16003 .unwrap()
16004 .await
16005 .unwrap();
16006 _ = follower.update(cx, |follower, _, cx| {
16007 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16008 });
16009 assert!(*is_still_following.borrow());
16010
16011 // Extend the pending selection so that it surrounds another selection
16012 _ = leader.update(cx, |leader, window, cx| {
16013 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16014 });
16015 follower
16016 .update(cx, |follower, window, cx| {
16017 follower.apply_update_proto(
16018 &project,
16019 pending_update.borrow_mut().take().unwrap(),
16020 window,
16021 cx,
16022 )
16023 })
16024 .unwrap()
16025 .await
16026 .unwrap();
16027 _ = follower.update(cx, |follower, _, cx| {
16028 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16029 });
16030
16031 // Scrolling locally breaks the follow
16032 _ = follower.update(cx, |follower, window, cx| {
16033 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16034 follower.set_scroll_anchor(
16035 ScrollAnchor {
16036 anchor: top_anchor,
16037 offset: gpui::Point::new(0.0, 0.5),
16038 },
16039 window,
16040 cx,
16041 );
16042 });
16043 assert!(!(*is_still_following.borrow()));
16044}
16045
16046#[gpui::test]
16047async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16048 init_test(cx, |_| {});
16049
16050 let fs = FakeFs::new(cx.executor());
16051 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16052 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16053 let pane = workspace
16054 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16055 .unwrap();
16056
16057 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16058
16059 let leader = pane.update_in(cx, |_, window, cx| {
16060 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16061 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16062 });
16063
16064 // Start following the editor when it has no excerpts.
16065 let mut state_message =
16066 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16067 let workspace_entity = workspace.root(cx).unwrap();
16068 let follower_1 = cx
16069 .update_window(*workspace.deref(), |_, window, cx| {
16070 Editor::from_state_proto(
16071 workspace_entity,
16072 ViewId {
16073 creator: CollaboratorId::PeerId(PeerId::default()),
16074 id: 0,
16075 },
16076 &mut state_message,
16077 window,
16078 cx,
16079 )
16080 })
16081 .unwrap()
16082 .unwrap()
16083 .await
16084 .unwrap();
16085
16086 let update_message = Rc::new(RefCell::new(None));
16087 follower_1.update_in(cx, {
16088 let update = update_message.clone();
16089 |_, window, cx| {
16090 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16091 leader.read(cx).add_event_to_update_proto(
16092 event,
16093 &mut update.borrow_mut(),
16094 window,
16095 cx,
16096 );
16097 })
16098 .detach();
16099 }
16100 });
16101
16102 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16103 (
16104 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
16105 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
16106 )
16107 });
16108
16109 // Insert some excerpts.
16110 leader.update(cx, |leader, cx| {
16111 leader.buffer.update(cx, |multibuffer, cx| {
16112 multibuffer.set_excerpts_for_path(
16113 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16114 buffer_1.clone(),
16115 vec![
16116 Point::row_range(0..3),
16117 Point::row_range(1..6),
16118 Point::row_range(12..15),
16119 ],
16120 0,
16121 cx,
16122 );
16123 multibuffer.set_excerpts_for_path(
16124 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16125 buffer_2.clone(),
16126 vec![Point::row_range(0..6), Point::row_range(8..12)],
16127 0,
16128 cx,
16129 );
16130 });
16131 });
16132
16133 // Apply the update of adding the excerpts.
16134 follower_1
16135 .update_in(cx, |follower, window, cx| {
16136 follower.apply_update_proto(
16137 &project,
16138 update_message.borrow().clone().unwrap(),
16139 window,
16140 cx,
16141 )
16142 })
16143 .await
16144 .unwrap();
16145 assert_eq!(
16146 follower_1.update(cx, |editor, cx| editor.text(cx)),
16147 leader.update(cx, |editor, cx| editor.text(cx))
16148 );
16149 update_message.borrow_mut().take();
16150
16151 // Start following separately after it already has excerpts.
16152 let mut state_message =
16153 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16154 let workspace_entity = workspace.root(cx).unwrap();
16155 let follower_2 = cx
16156 .update_window(*workspace.deref(), |_, window, cx| {
16157 Editor::from_state_proto(
16158 workspace_entity,
16159 ViewId {
16160 creator: CollaboratorId::PeerId(PeerId::default()),
16161 id: 0,
16162 },
16163 &mut state_message,
16164 window,
16165 cx,
16166 )
16167 })
16168 .unwrap()
16169 .unwrap()
16170 .await
16171 .unwrap();
16172 assert_eq!(
16173 follower_2.update(cx, |editor, cx| editor.text(cx)),
16174 leader.update(cx, |editor, cx| editor.text(cx))
16175 );
16176
16177 // Remove some excerpts.
16178 leader.update(cx, |leader, cx| {
16179 leader.buffer.update(cx, |multibuffer, cx| {
16180 let excerpt_ids = multibuffer.excerpt_ids();
16181 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16182 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16183 });
16184 });
16185
16186 // Apply the update of removing the excerpts.
16187 follower_1
16188 .update_in(cx, |follower, window, cx| {
16189 follower.apply_update_proto(
16190 &project,
16191 update_message.borrow().clone().unwrap(),
16192 window,
16193 cx,
16194 )
16195 })
16196 .await
16197 .unwrap();
16198 follower_2
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 update_message.borrow_mut().take();
16210 assert_eq!(
16211 follower_1.update(cx, |editor, cx| editor.text(cx)),
16212 leader.update(cx, |editor, cx| editor.text(cx))
16213 );
16214}
16215
16216#[gpui::test]
16217async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16218 init_test(cx, |_| {});
16219
16220 let mut cx = EditorTestContext::new(cx).await;
16221 let lsp_store =
16222 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16223
16224 cx.set_state(indoc! {"
16225 ˇfn func(abc def: i32) -> u32 {
16226 }
16227 "});
16228
16229 cx.update(|_, cx| {
16230 lsp_store.update(cx, |lsp_store, cx| {
16231 lsp_store
16232 .update_diagnostics(
16233 LanguageServerId(0),
16234 lsp::PublishDiagnosticsParams {
16235 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16236 version: None,
16237 diagnostics: vec![
16238 lsp::Diagnostic {
16239 range: lsp::Range::new(
16240 lsp::Position::new(0, 11),
16241 lsp::Position::new(0, 12),
16242 ),
16243 severity: Some(lsp::DiagnosticSeverity::ERROR),
16244 ..Default::default()
16245 },
16246 lsp::Diagnostic {
16247 range: lsp::Range::new(
16248 lsp::Position::new(0, 12),
16249 lsp::Position::new(0, 15),
16250 ),
16251 severity: Some(lsp::DiagnosticSeverity::ERROR),
16252 ..Default::default()
16253 },
16254 lsp::Diagnostic {
16255 range: lsp::Range::new(
16256 lsp::Position::new(0, 25),
16257 lsp::Position::new(0, 28),
16258 ),
16259 severity: Some(lsp::DiagnosticSeverity::ERROR),
16260 ..Default::default()
16261 },
16262 ],
16263 },
16264 None,
16265 DiagnosticSourceKind::Pushed,
16266 &[],
16267 cx,
16268 )
16269 .unwrap()
16270 });
16271 });
16272
16273 executor.run_until_parked();
16274
16275 cx.update_editor(|editor, window, cx| {
16276 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16277 });
16278
16279 cx.assert_editor_state(indoc! {"
16280 fn func(abc def: i32) -> ˇu32 {
16281 }
16282 "});
16283
16284 cx.update_editor(|editor, window, cx| {
16285 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16286 });
16287
16288 cx.assert_editor_state(indoc! {"
16289 fn func(abc ˇdef: i32) -> u32 {
16290 }
16291 "});
16292
16293 cx.update_editor(|editor, window, cx| {
16294 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16295 });
16296
16297 cx.assert_editor_state(indoc! {"
16298 fn func(abcˇ def: i32) -> u32 {
16299 }
16300 "});
16301
16302 cx.update_editor(|editor, window, cx| {
16303 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16304 });
16305
16306 cx.assert_editor_state(indoc! {"
16307 fn func(abc def: i32) -> ˇu32 {
16308 }
16309 "});
16310}
16311
16312#[gpui::test]
16313async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16314 init_test(cx, |_| {});
16315
16316 let mut cx = EditorTestContext::new(cx).await;
16317
16318 let diff_base = r#"
16319 use some::mod;
16320
16321 const A: u32 = 42;
16322
16323 fn main() {
16324 println!("hello");
16325
16326 println!("world");
16327 }
16328 "#
16329 .unindent();
16330
16331 // Edits are modified, removed, modified, added
16332 cx.set_state(
16333 &r#"
16334 use some::modified;
16335
16336 ˇ
16337 fn main() {
16338 println!("hello there");
16339
16340 println!("around the");
16341 println!("world");
16342 }
16343 "#
16344 .unindent(),
16345 );
16346
16347 cx.set_head_text(&diff_base);
16348 executor.run_until_parked();
16349
16350 cx.update_editor(|editor, window, cx| {
16351 //Wrap around the bottom of the buffer
16352 for _ in 0..3 {
16353 editor.go_to_next_hunk(&GoToHunk, window, cx);
16354 }
16355 });
16356
16357 cx.assert_editor_state(
16358 &r#"
16359 ˇuse some::modified;
16360
16361
16362 fn main() {
16363 println!("hello there");
16364
16365 println!("around the");
16366 println!("world");
16367 }
16368 "#
16369 .unindent(),
16370 );
16371
16372 cx.update_editor(|editor, window, cx| {
16373 //Wrap around the top of the buffer
16374 for _ in 0..2 {
16375 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16376 }
16377 });
16378
16379 cx.assert_editor_state(
16380 &r#"
16381 use some::modified;
16382
16383
16384 fn main() {
16385 ˇ println!("hello there");
16386
16387 println!("around the");
16388 println!("world");
16389 }
16390 "#
16391 .unindent(),
16392 );
16393
16394 cx.update_editor(|editor, window, cx| {
16395 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16396 });
16397
16398 cx.assert_editor_state(
16399 &r#"
16400 use some::modified;
16401
16402 ˇ
16403 fn main() {
16404 println!("hello there");
16405
16406 println!("around the");
16407 println!("world");
16408 }
16409 "#
16410 .unindent(),
16411 );
16412
16413 cx.update_editor(|editor, window, cx| {
16414 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16415 });
16416
16417 cx.assert_editor_state(
16418 &r#"
16419 ˇuse some::modified;
16420
16421
16422 fn main() {
16423 println!("hello there");
16424
16425 println!("around the");
16426 println!("world");
16427 }
16428 "#
16429 .unindent(),
16430 );
16431
16432 cx.update_editor(|editor, window, cx| {
16433 for _ in 0..2 {
16434 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16435 }
16436 });
16437
16438 cx.assert_editor_state(
16439 &r#"
16440 use some::modified;
16441
16442
16443 fn main() {
16444 ˇ println!("hello there");
16445
16446 println!("around the");
16447 println!("world");
16448 }
16449 "#
16450 .unindent(),
16451 );
16452
16453 cx.update_editor(|editor, window, cx| {
16454 editor.fold(&Fold, window, cx);
16455 });
16456
16457 cx.update_editor(|editor, window, cx| {
16458 editor.go_to_next_hunk(&GoToHunk, window, cx);
16459 });
16460
16461 cx.assert_editor_state(
16462 &r#"
16463 ˇuse some::modified;
16464
16465
16466 fn main() {
16467 println!("hello there");
16468
16469 println!("around the");
16470 println!("world");
16471 }
16472 "#
16473 .unindent(),
16474 );
16475}
16476
16477#[test]
16478fn test_split_words() {
16479 fn split(text: &str) -> Vec<&str> {
16480 split_words(text).collect()
16481 }
16482
16483 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16484 assert_eq!(split("hello_world"), &["hello_", "world"]);
16485 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16486 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16487 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16488 assert_eq!(split("helloworld"), &["helloworld"]);
16489
16490 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16491}
16492
16493#[gpui::test]
16494async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16495 init_test(cx, |_| {});
16496
16497 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16498 let mut assert = |before, after| {
16499 let _state_context = cx.set_state(before);
16500 cx.run_until_parked();
16501 cx.update_editor(|editor, window, cx| {
16502 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16503 });
16504 cx.run_until_parked();
16505 cx.assert_editor_state(after);
16506 };
16507
16508 // Outside bracket jumps to outside of matching bracket
16509 assert("console.logˇ(var);", "console.log(var)ˇ;");
16510 assert("console.log(var)ˇ;", "console.logˇ(var);");
16511
16512 // Inside bracket jumps to inside of matching bracket
16513 assert("console.log(ˇvar);", "console.log(varˇ);");
16514 assert("console.log(varˇ);", "console.log(ˇvar);");
16515
16516 // When outside a bracket and inside, favor jumping to the inside bracket
16517 assert(
16518 "console.log('foo', [1, 2, 3]ˇ);",
16519 "console.log(ˇ'foo', [1, 2, 3]);",
16520 );
16521 assert(
16522 "console.log(ˇ'foo', [1, 2, 3]);",
16523 "console.log('foo', [1, 2, 3]ˇ);",
16524 );
16525
16526 // Bias forward if two options are equally likely
16527 assert(
16528 "let result = curried_fun()ˇ();",
16529 "let result = curried_fun()()ˇ;",
16530 );
16531
16532 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16533 assert(
16534 indoc! {"
16535 function test() {
16536 console.log('test')ˇ
16537 }"},
16538 indoc! {"
16539 function test() {
16540 console.logˇ('test')
16541 }"},
16542 );
16543}
16544
16545#[gpui::test]
16546async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16547 init_test(cx, |_| {});
16548
16549 let fs = FakeFs::new(cx.executor());
16550 fs.insert_tree(
16551 path!("/a"),
16552 json!({
16553 "main.rs": "fn main() { let a = 5; }",
16554 "other.rs": "// Test file",
16555 }),
16556 )
16557 .await;
16558 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16559
16560 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16561 language_registry.add(Arc::new(Language::new(
16562 LanguageConfig {
16563 name: "Rust".into(),
16564 matcher: LanguageMatcher {
16565 path_suffixes: vec!["rs".to_string()],
16566 ..Default::default()
16567 },
16568 brackets: BracketPairConfig {
16569 pairs: vec![BracketPair {
16570 start: "{".to_string(),
16571 end: "}".to_string(),
16572 close: true,
16573 surround: true,
16574 newline: true,
16575 }],
16576 disabled_scopes_by_bracket_ix: Vec::new(),
16577 },
16578 ..Default::default()
16579 },
16580 Some(tree_sitter_rust::LANGUAGE.into()),
16581 )));
16582 let mut fake_servers = language_registry.register_fake_lsp(
16583 "Rust",
16584 FakeLspAdapter {
16585 capabilities: lsp::ServerCapabilities {
16586 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16587 first_trigger_character: "{".to_string(),
16588 more_trigger_character: None,
16589 }),
16590 ..Default::default()
16591 },
16592 ..Default::default()
16593 },
16594 );
16595
16596 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16597
16598 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16599
16600 let worktree_id = workspace
16601 .update(cx, |workspace, _, cx| {
16602 workspace.project().update(cx, |project, cx| {
16603 project.worktrees(cx).next().unwrap().read(cx).id()
16604 })
16605 })
16606 .unwrap();
16607
16608 let buffer = project
16609 .update(cx, |project, cx| {
16610 project.open_local_buffer(path!("/a/main.rs"), cx)
16611 })
16612 .await
16613 .unwrap();
16614 let editor_handle = workspace
16615 .update(cx, |workspace, window, cx| {
16616 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16617 })
16618 .unwrap()
16619 .await
16620 .unwrap()
16621 .downcast::<Editor>()
16622 .unwrap();
16623
16624 cx.executor().start_waiting();
16625 let fake_server = fake_servers.next().await.unwrap();
16626
16627 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16628 |params, _| async move {
16629 assert_eq!(
16630 params.text_document_position.text_document.uri,
16631 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16632 );
16633 assert_eq!(
16634 params.text_document_position.position,
16635 lsp::Position::new(0, 21),
16636 );
16637
16638 Ok(Some(vec![lsp::TextEdit {
16639 new_text: "]".to_string(),
16640 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16641 }]))
16642 },
16643 );
16644
16645 editor_handle.update_in(cx, |editor, window, cx| {
16646 window.focus(&editor.focus_handle(cx));
16647 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16648 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16649 });
16650 editor.handle_input("{", window, cx);
16651 });
16652
16653 cx.executor().run_until_parked();
16654
16655 buffer.update(cx, |buffer, _| {
16656 assert_eq!(
16657 buffer.text(),
16658 "fn main() { let a = {5}; }",
16659 "No extra braces from on type formatting should appear in the buffer"
16660 )
16661 });
16662}
16663
16664#[gpui::test(iterations = 20, seeds(31))]
16665async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16666 init_test(cx, |_| {});
16667
16668 let mut cx = EditorLspTestContext::new_rust(
16669 lsp::ServerCapabilities {
16670 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16671 first_trigger_character: ".".to_string(),
16672 more_trigger_character: None,
16673 }),
16674 ..Default::default()
16675 },
16676 cx,
16677 )
16678 .await;
16679
16680 cx.update_buffer(|buffer, _| {
16681 // This causes autoindent to be async.
16682 buffer.set_sync_parse_timeout(Duration::ZERO)
16683 });
16684
16685 cx.set_state("fn c() {\n d()ˇ\n}\n");
16686 cx.simulate_keystroke("\n");
16687 cx.run_until_parked();
16688
16689 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16690 let mut request =
16691 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16692 let buffer_cloned = buffer_cloned.clone();
16693 async move {
16694 buffer_cloned.update(&mut cx, |buffer, _| {
16695 assert_eq!(
16696 buffer.text(),
16697 "fn c() {\n d()\n .\n}\n",
16698 "OnTypeFormatting should triggered after autoindent applied"
16699 )
16700 })?;
16701
16702 Ok(Some(vec![]))
16703 }
16704 });
16705
16706 cx.simulate_keystroke(".");
16707 cx.run_until_parked();
16708
16709 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16710 assert!(request.next().await.is_some());
16711 request.close();
16712 assert!(request.next().await.is_none());
16713}
16714
16715#[gpui::test]
16716async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
16717 init_test(cx, |_| {});
16718
16719 let fs = FakeFs::new(cx.executor());
16720 fs.insert_tree(
16721 path!("/a"),
16722 json!({
16723 "main.rs": "fn main() { let a = 5; }",
16724 "other.rs": "// Test file",
16725 }),
16726 )
16727 .await;
16728
16729 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16730
16731 let server_restarts = Arc::new(AtomicUsize::new(0));
16732 let closure_restarts = Arc::clone(&server_restarts);
16733 let language_server_name = "test language server";
16734 let language_name: LanguageName = "Rust".into();
16735
16736 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16737 language_registry.add(Arc::new(Language::new(
16738 LanguageConfig {
16739 name: language_name.clone(),
16740 matcher: LanguageMatcher {
16741 path_suffixes: vec!["rs".to_string()],
16742 ..Default::default()
16743 },
16744 ..Default::default()
16745 },
16746 Some(tree_sitter_rust::LANGUAGE.into()),
16747 )));
16748 let mut fake_servers = language_registry.register_fake_lsp(
16749 "Rust",
16750 FakeLspAdapter {
16751 name: language_server_name,
16752 initialization_options: Some(json!({
16753 "testOptionValue": true
16754 })),
16755 initializer: Some(Box::new(move |fake_server| {
16756 let task_restarts = Arc::clone(&closure_restarts);
16757 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
16758 task_restarts.fetch_add(1, atomic::Ordering::Release);
16759 futures::future::ready(Ok(()))
16760 });
16761 })),
16762 ..Default::default()
16763 },
16764 );
16765
16766 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16767 let _buffer = project
16768 .update(cx, |project, cx| {
16769 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
16770 })
16771 .await
16772 .unwrap();
16773 let _fake_server = fake_servers.next().await.unwrap();
16774 update_test_language_settings(cx, |language_settings| {
16775 language_settings.languages.0.insert(
16776 language_name.clone(),
16777 LanguageSettingsContent {
16778 tab_size: NonZeroU32::new(8),
16779 ..Default::default()
16780 },
16781 );
16782 });
16783 cx.executor().run_until_parked();
16784 assert_eq!(
16785 server_restarts.load(atomic::Ordering::Acquire),
16786 0,
16787 "Should not restart LSP server on an unrelated change"
16788 );
16789
16790 update_test_project_settings(cx, |project_settings| {
16791 project_settings.lsp.insert(
16792 "Some other server name".into(),
16793 LspSettings {
16794 binary: None,
16795 settings: None,
16796 initialization_options: Some(json!({
16797 "some other init value": false
16798 })),
16799 enable_lsp_tasks: false,
16800 fetch: None,
16801 },
16802 );
16803 });
16804 cx.executor().run_until_parked();
16805 assert_eq!(
16806 server_restarts.load(atomic::Ordering::Acquire),
16807 0,
16808 "Should not restart LSP server on an unrelated LSP settings change"
16809 );
16810
16811 update_test_project_settings(cx, |project_settings| {
16812 project_settings.lsp.insert(
16813 language_server_name.into(),
16814 LspSettings {
16815 binary: None,
16816 settings: None,
16817 initialization_options: Some(json!({
16818 "anotherInitValue": false
16819 })),
16820 enable_lsp_tasks: false,
16821 fetch: None,
16822 },
16823 );
16824 });
16825 cx.executor().run_until_parked();
16826 assert_eq!(
16827 server_restarts.load(atomic::Ordering::Acquire),
16828 1,
16829 "Should restart LSP server on a related LSP settings change"
16830 );
16831
16832 update_test_project_settings(cx, |project_settings| {
16833 project_settings.lsp.insert(
16834 language_server_name.into(),
16835 LspSettings {
16836 binary: None,
16837 settings: None,
16838 initialization_options: Some(json!({
16839 "anotherInitValue": false
16840 })),
16841 enable_lsp_tasks: false,
16842 fetch: None,
16843 },
16844 );
16845 });
16846 cx.executor().run_until_parked();
16847 assert_eq!(
16848 server_restarts.load(atomic::Ordering::Acquire),
16849 1,
16850 "Should not restart LSP server on a related LSP settings change that is the same"
16851 );
16852
16853 update_test_project_settings(cx, |project_settings| {
16854 project_settings.lsp.insert(
16855 language_server_name.into(),
16856 LspSettings {
16857 binary: None,
16858 settings: None,
16859 initialization_options: None,
16860 enable_lsp_tasks: false,
16861 fetch: None,
16862 },
16863 );
16864 });
16865 cx.executor().run_until_parked();
16866 assert_eq!(
16867 server_restarts.load(atomic::Ordering::Acquire),
16868 2,
16869 "Should restart LSP server on another related LSP settings change"
16870 );
16871}
16872
16873#[gpui::test]
16874async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
16875 init_test(cx, |_| {});
16876
16877 let mut cx = EditorLspTestContext::new_rust(
16878 lsp::ServerCapabilities {
16879 completion_provider: Some(lsp::CompletionOptions {
16880 trigger_characters: Some(vec![".".to_string()]),
16881 resolve_provider: Some(true),
16882 ..Default::default()
16883 }),
16884 ..Default::default()
16885 },
16886 cx,
16887 )
16888 .await;
16889
16890 cx.set_state("fn main() { let a = 2ˇ; }");
16891 cx.simulate_keystroke(".");
16892 let completion_item = lsp::CompletionItem {
16893 label: "some".into(),
16894 kind: Some(lsp::CompletionItemKind::SNIPPET),
16895 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16896 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16897 kind: lsp::MarkupKind::Markdown,
16898 value: "```rust\nSome(2)\n```".to_string(),
16899 })),
16900 deprecated: Some(false),
16901 sort_text: Some("fffffff2".to_string()),
16902 filter_text: Some("some".to_string()),
16903 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16904 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16905 range: lsp::Range {
16906 start: lsp::Position {
16907 line: 0,
16908 character: 22,
16909 },
16910 end: lsp::Position {
16911 line: 0,
16912 character: 22,
16913 },
16914 },
16915 new_text: "Some(2)".to_string(),
16916 })),
16917 additional_text_edits: Some(vec![lsp::TextEdit {
16918 range: lsp::Range {
16919 start: lsp::Position {
16920 line: 0,
16921 character: 20,
16922 },
16923 end: lsp::Position {
16924 line: 0,
16925 character: 22,
16926 },
16927 },
16928 new_text: "".to_string(),
16929 }]),
16930 ..Default::default()
16931 };
16932
16933 let closure_completion_item = completion_item.clone();
16934 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16935 let task_completion_item = closure_completion_item.clone();
16936 async move {
16937 Ok(Some(lsp::CompletionResponse::Array(vec![
16938 task_completion_item,
16939 ])))
16940 }
16941 });
16942
16943 request.next().await;
16944
16945 cx.condition(|editor, _| editor.context_menu_visible())
16946 .await;
16947 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16948 editor
16949 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16950 .unwrap()
16951 });
16952 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16953
16954 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16955 let task_completion_item = completion_item.clone();
16956 async move { Ok(task_completion_item) }
16957 })
16958 .next()
16959 .await
16960 .unwrap();
16961 apply_additional_edits.await.unwrap();
16962 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16963}
16964
16965#[gpui::test]
16966async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16967 init_test(cx, |_| {});
16968
16969 let mut cx = EditorLspTestContext::new_rust(
16970 lsp::ServerCapabilities {
16971 completion_provider: Some(lsp::CompletionOptions {
16972 trigger_characters: Some(vec![".".to_string()]),
16973 resolve_provider: Some(true),
16974 ..Default::default()
16975 }),
16976 ..Default::default()
16977 },
16978 cx,
16979 )
16980 .await;
16981
16982 cx.set_state("fn main() { let a = 2ˇ; }");
16983 cx.simulate_keystroke(".");
16984
16985 let item1 = lsp::CompletionItem {
16986 label: "method id()".to_string(),
16987 filter_text: Some("id".to_string()),
16988 detail: None,
16989 documentation: None,
16990 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16991 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16992 new_text: ".id".to_string(),
16993 })),
16994 ..lsp::CompletionItem::default()
16995 };
16996
16997 let item2 = lsp::CompletionItem {
16998 label: "other".to_string(),
16999 filter_text: Some("other".to_string()),
17000 detail: None,
17001 documentation: None,
17002 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17003 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17004 new_text: ".other".to_string(),
17005 })),
17006 ..lsp::CompletionItem::default()
17007 };
17008
17009 let item1 = item1.clone();
17010 cx.set_request_handler::<lsp::request::Completion, _, _>({
17011 let item1 = item1.clone();
17012 move |_, _, _| {
17013 let item1 = item1.clone();
17014 let item2 = item2.clone();
17015 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17016 }
17017 })
17018 .next()
17019 .await;
17020
17021 cx.condition(|editor, _| editor.context_menu_visible())
17022 .await;
17023 cx.update_editor(|editor, _, _| {
17024 let context_menu = editor.context_menu.borrow_mut();
17025 let context_menu = context_menu
17026 .as_ref()
17027 .expect("Should have the context menu deployed");
17028 match context_menu {
17029 CodeContextMenu::Completions(completions_menu) => {
17030 let completions = completions_menu.completions.borrow_mut();
17031 assert_eq!(
17032 completions
17033 .iter()
17034 .map(|completion| &completion.label.text)
17035 .collect::<Vec<_>>(),
17036 vec!["method id()", "other"]
17037 )
17038 }
17039 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17040 }
17041 });
17042
17043 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17044 let item1 = item1.clone();
17045 move |_, item_to_resolve, _| {
17046 let item1 = item1.clone();
17047 async move {
17048 if item1 == item_to_resolve {
17049 Ok(lsp::CompletionItem {
17050 label: "method id()".to_string(),
17051 filter_text: Some("id".to_string()),
17052 detail: Some("Now resolved!".to_string()),
17053 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17054 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17055 range: lsp::Range::new(
17056 lsp::Position::new(0, 22),
17057 lsp::Position::new(0, 22),
17058 ),
17059 new_text: ".id".to_string(),
17060 })),
17061 ..lsp::CompletionItem::default()
17062 })
17063 } else {
17064 Ok(item_to_resolve)
17065 }
17066 }
17067 }
17068 })
17069 .next()
17070 .await
17071 .unwrap();
17072 cx.run_until_parked();
17073
17074 cx.update_editor(|editor, window, cx| {
17075 editor.context_menu_next(&Default::default(), window, cx);
17076 });
17077
17078 cx.update_editor(|editor, _, _| {
17079 let context_menu = editor.context_menu.borrow_mut();
17080 let context_menu = context_menu
17081 .as_ref()
17082 .expect("Should have the context menu deployed");
17083 match context_menu {
17084 CodeContextMenu::Completions(completions_menu) => {
17085 let completions = completions_menu.completions.borrow_mut();
17086 assert_eq!(
17087 completions
17088 .iter()
17089 .map(|completion| &completion.label.text)
17090 .collect::<Vec<_>>(),
17091 vec!["method id() Now resolved!", "other"],
17092 "Should update first completion label, but not second as the filter text did not match."
17093 );
17094 }
17095 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17096 }
17097 });
17098}
17099
17100#[gpui::test]
17101async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17102 init_test(cx, |_| {});
17103 let mut cx = EditorLspTestContext::new_rust(
17104 lsp::ServerCapabilities {
17105 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17106 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17107 completion_provider: Some(lsp::CompletionOptions {
17108 resolve_provider: Some(true),
17109 ..Default::default()
17110 }),
17111 ..Default::default()
17112 },
17113 cx,
17114 )
17115 .await;
17116 cx.set_state(indoc! {"
17117 struct TestStruct {
17118 field: i32
17119 }
17120
17121 fn mainˇ() {
17122 let unused_var = 42;
17123 let test_struct = TestStruct { field: 42 };
17124 }
17125 "});
17126 let symbol_range = cx.lsp_range(indoc! {"
17127 struct TestStruct {
17128 field: i32
17129 }
17130
17131 «fn main»() {
17132 let unused_var = 42;
17133 let test_struct = TestStruct { field: 42 };
17134 }
17135 "});
17136 let mut hover_requests =
17137 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17138 Ok(Some(lsp::Hover {
17139 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17140 kind: lsp::MarkupKind::Markdown,
17141 value: "Function documentation".to_string(),
17142 }),
17143 range: Some(symbol_range),
17144 }))
17145 });
17146
17147 // Case 1: Test that code action menu hide hover popover
17148 cx.dispatch_action(Hover);
17149 hover_requests.next().await;
17150 cx.condition(|editor, _| editor.hover_state.visible()).await;
17151 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17152 move |_, _, _| async move {
17153 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17154 lsp::CodeAction {
17155 title: "Remove unused variable".to_string(),
17156 kind: Some(CodeActionKind::QUICKFIX),
17157 edit: Some(lsp::WorkspaceEdit {
17158 changes: Some(
17159 [(
17160 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17161 vec![lsp::TextEdit {
17162 range: lsp::Range::new(
17163 lsp::Position::new(5, 4),
17164 lsp::Position::new(5, 27),
17165 ),
17166 new_text: "".to_string(),
17167 }],
17168 )]
17169 .into_iter()
17170 .collect(),
17171 ),
17172 ..Default::default()
17173 }),
17174 ..Default::default()
17175 },
17176 )]))
17177 },
17178 );
17179 cx.update_editor(|editor, window, cx| {
17180 editor.toggle_code_actions(
17181 &ToggleCodeActions {
17182 deployed_from: None,
17183 quick_launch: false,
17184 },
17185 window,
17186 cx,
17187 );
17188 });
17189 code_action_requests.next().await;
17190 cx.run_until_parked();
17191 cx.condition(|editor, _| editor.context_menu_visible())
17192 .await;
17193 cx.update_editor(|editor, _, _| {
17194 assert!(
17195 !editor.hover_state.visible(),
17196 "Hover popover should be hidden when code action menu is shown"
17197 );
17198 // Hide code actions
17199 editor.context_menu.take();
17200 });
17201
17202 // Case 2: Test that code completions hide hover popover
17203 cx.dispatch_action(Hover);
17204 hover_requests.next().await;
17205 cx.condition(|editor, _| editor.hover_state.visible()).await;
17206 let counter = Arc::new(AtomicUsize::new(0));
17207 let mut completion_requests =
17208 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17209 let counter = counter.clone();
17210 async move {
17211 counter.fetch_add(1, atomic::Ordering::Release);
17212 Ok(Some(lsp::CompletionResponse::Array(vec![
17213 lsp::CompletionItem {
17214 label: "main".into(),
17215 kind: Some(lsp::CompletionItemKind::FUNCTION),
17216 detail: Some("() -> ()".to_string()),
17217 ..Default::default()
17218 },
17219 lsp::CompletionItem {
17220 label: "TestStruct".into(),
17221 kind: Some(lsp::CompletionItemKind::STRUCT),
17222 detail: Some("struct TestStruct".to_string()),
17223 ..Default::default()
17224 },
17225 ])))
17226 }
17227 });
17228 cx.update_editor(|editor, window, cx| {
17229 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17230 });
17231 completion_requests.next().await;
17232 cx.condition(|editor, _| editor.context_menu_visible())
17233 .await;
17234 cx.update_editor(|editor, _, _| {
17235 assert!(
17236 !editor.hover_state.visible(),
17237 "Hover popover should be hidden when completion menu is shown"
17238 );
17239 });
17240}
17241
17242#[gpui::test]
17243async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17244 init_test(cx, |_| {});
17245
17246 let mut cx = EditorLspTestContext::new_rust(
17247 lsp::ServerCapabilities {
17248 completion_provider: Some(lsp::CompletionOptions {
17249 trigger_characters: Some(vec![".".to_string()]),
17250 resolve_provider: Some(true),
17251 ..Default::default()
17252 }),
17253 ..Default::default()
17254 },
17255 cx,
17256 )
17257 .await;
17258
17259 cx.set_state("fn main() { let a = 2ˇ; }");
17260 cx.simulate_keystroke(".");
17261
17262 let unresolved_item_1 = lsp::CompletionItem {
17263 label: "id".to_string(),
17264 filter_text: Some("id".to_string()),
17265 detail: None,
17266 documentation: None,
17267 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17268 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17269 new_text: ".id".to_string(),
17270 })),
17271 ..lsp::CompletionItem::default()
17272 };
17273 let resolved_item_1 = lsp::CompletionItem {
17274 additional_text_edits: Some(vec![lsp::TextEdit {
17275 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17276 new_text: "!!".to_string(),
17277 }]),
17278 ..unresolved_item_1.clone()
17279 };
17280 let unresolved_item_2 = lsp::CompletionItem {
17281 label: "other".to_string(),
17282 filter_text: Some("other".to_string()),
17283 detail: None,
17284 documentation: None,
17285 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17286 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17287 new_text: ".other".to_string(),
17288 })),
17289 ..lsp::CompletionItem::default()
17290 };
17291 let resolved_item_2 = lsp::CompletionItem {
17292 additional_text_edits: Some(vec![lsp::TextEdit {
17293 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17294 new_text: "??".to_string(),
17295 }]),
17296 ..unresolved_item_2.clone()
17297 };
17298
17299 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17300 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17301 cx.lsp
17302 .server
17303 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17304 let unresolved_item_1 = unresolved_item_1.clone();
17305 let resolved_item_1 = resolved_item_1.clone();
17306 let unresolved_item_2 = unresolved_item_2.clone();
17307 let resolved_item_2 = resolved_item_2.clone();
17308 let resolve_requests_1 = resolve_requests_1.clone();
17309 let resolve_requests_2 = resolve_requests_2.clone();
17310 move |unresolved_request, _| {
17311 let unresolved_item_1 = unresolved_item_1.clone();
17312 let resolved_item_1 = resolved_item_1.clone();
17313 let unresolved_item_2 = unresolved_item_2.clone();
17314 let resolved_item_2 = resolved_item_2.clone();
17315 let resolve_requests_1 = resolve_requests_1.clone();
17316 let resolve_requests_2 = resolve_requests_2.clone();
17317 async move {
17318 if unresolved_request == unresolved_item_1 {
17319 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17320 Ok(resolved_item_1.clone())
17321 } else if unresolved_request == unresolved_item_2 {
17322 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17323 Ok(resolved_item_2.clone())
17324 } else {
17325 panic!("Unexpected completion item {unresolved_request:?}")
17326 }
17327 }
17328 }
17329 })
17330 .detach();
17331
17332 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17333 let unresolved_item_1 = unresolved_item_1.clone();
17334 let unresolved_item_2 = unresolved_item_2.clone();
17335 async move {
17336 Ok(Some(lsp::CompletionResponse::Array(vec![
17337 unresolved_item_1,
17338 unresolved_item_2,
17339 ])))
17340 }
17341 })
17342 .next()
17343 .await;
17344
17345 cx.condition(|editor, _| editor.context_menu_visible())
17346 .await;
17347 cx.update_editor(|editor, _, _| {
17348 let context_menu = editor.context_menu.borrow_mut();
17349 let context_menu = context_menu
17350 .as_ref()
17351 .expect("Should have the context menu deployed");
17352 match context_menu {
17353 CodeContextMenu::Completions(completions_menu) => {
17354 let completions = completions_menu.completions.borrow_mut();
17355 assert_eq!(
17356 completions
17357 .iter()
17358 .map(|completion| &completion.label.text)
17359 .collect::<Vec<_>>(),
17360 vec!["id", "other"]
17361 )
17362 }
17363 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17364 }
17365 });
17366 cx.run_until_parked();
17367
17368 cx.update_editor(|editor, window, cx| {
17369 editor.context_menu_next(&ContextMenuNext, window, cx);
17370 });
17371 cx.run_until_parked();
17372 cx.update_editor(|editor, window, cx| {
17373 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17374 });
17375 cx.run_until_parked();
17376 cx.update_editor(|editor, window, cx| {
17377 editor.context_menu_next(&ContextMenuNext, window, cx);
17378 });
17379 cx.run_until_parked();
17380 cx.update_editor(|editor, window, cx| {
17381 editor
17382 .compose_completion(&ComposeCompletion::default(), window, cx)
17383 .expect("No task returned")
17384 })
17385 .await
17386 .expect("Completion failed");
17387 cx.run_until_parked();
17388
17389 cx.update_editor(|editor, _, cx| {
17390 assert_eq!(
17391 resolve_requests_1.load(atomic::Ordering::Acquire),
17392 1,
17393 "Should always resolve once despite multiple selections"
17394 );
17395 assert_eq!(
17396 resolve_requests_2.load(atomic::Ordering::Acquire),
17397 1,
17398 "Should always resolve once after multiple selections and applying the completion"
17399 );
17400 assert_eq!(
17401 editor.text(cx),
17402 "fn main() { let a = ??.other; }",
17403 "Should use resolved data when applying the completion"
17404 );
17405 });
17406}
17407
17408#[gpui::test]
17409async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17410 init_test(cx, |_| {});
17411
17412 let item_0 = lsp::CompletionItem {
17413 label: "abs".into(),
17414 insert_text: Some("abs".into()),
17415 data: Some(json!({ "very": "special"})),
17416 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17417 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17418 lsp::InsertReplaceEdit {
17419 new_text: "abs".to_string(),
17420 insert: lsp::Range::default(),
17421 replace: lsp::Range::default(),
17422 },
17423 )),
17424 ..lsp::CompletionItem::default()
17425 };
17426 let items = iter::once(item_0.clone())
17427 .chain((11..51).map(|i| lsp::CompletionItem {
17428 label: format!("item_{}", i),
17429 insert_text: Some(format!("item_{}", i)),
17430 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17431 ..lsp::CompletionItem::default()
17432 }))
17433 .collect::<Vec<_>>();
17434
17435 let default_commit_characters = vec!["?".to_string()];
17436 let default_data = json!({ "default": "data"});
17437 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17438 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17439 let default_edit_range = lsp::Range {
17440 start: lsp::Position {
17441 line: 0,
17442 character: 5,
17443 },
17444 end: lsp::Position {
17445 line: 0,
17446 character: 5,
17447 },
17448 };
17449
17450 let mut cx = EditorLspTestContext::new_rust(
17451 lsp::ServerCapabilities {
17452 completion_provider: Some(lsp::CompletionOptions {
17453 trigger_characters: Some(vec![".".to_string()]),
17454 resolve_provider: Some(true),
17455 ..Default::default()
17456 }),
17457 ..Default::default()
17458 },
17459 cx,
17460 )
17461 .await;
17462
17463 cx.set_state("fn main() { let a = 2ˇ; }");
17464 cx.simulate_keystroke(".");
17465
17466 let completion_data = default_data.clone();
17467 let completion_characters = default_commit_characters.clone();
17468 let completion_items = items.clone();
17469 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17470 let default_data = completion_data.clone();
17471 let default_commit_characters = completion_characters.clone();
17472 let items = completion_items.clone();
17473 async move {
17474 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17475 items,
17476 item_defaults: Some(lsp::CompletionListItemDefaults {
17477 data: Some(default_data.clone()),
17478 commit_characters: Some(default_commit_characters.clone()),
17479 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17480 default_edit_range,
17481 )),
17482 insert_text_format: Some(default_insert_text_format),
17483 insert_text_mode: Some(default_insert_text_mode),
17484 }),
17485 ..lsp::CompletionList::default()
17486 })))
17487 }
17488 })
17489 .next()
17490 .await;
17491
17492 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17493 cx.lsp
17494 .server
17495 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17496 let closure_resolved_items = resolved_items.clone();
17497 move |item_to_resolve, _| {
17498 let closure_resolved_items = closure_resolved_items.clone();
17499 async move {
17500 closure_resolved_items.lock().push(item_to_resolve.clone());
17501 Ok(item_to_resolve)
17502 }
17503 }
17504 })
17505 .detach();
17506
17507 cx.condition(|editor, _| editor.context_menu_visible())
17508 .await;
17509 cx.run_until_parked();
17510 cx.update_editor(|editor, _, _| {
17511 let menu = editor.context_menu.borrow_mut();
17512 match menu.as_ref().expect("should have the completions menu") {
17513 CodeContextMenu::Completions(completions_menu) => {
17514 assert_eq!(
17515 completions_menu
17516 .entries
17517 .borrow()
17518 .iter()
17519 .map(|mat| mat.string.clone())
17520 .collect::<Vec<String>>(),
17521 items
17522 .iter()
17523 .map(|completion| completion.label.clone())
17524 .collect::<Vec<String>>()
17525 );
17526 }
17527 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17528 }
17529 });
17530 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17531 // with 4 from the end.
17532 assert_eq!(
17533 *resolved_items.lock(),
17534 [&items[0..16], &items[items.len() - 4..items.len()]]
17535 .concat()
17536 .iter()
17537 .cloned()
17538 .map(|mut item| {
17539 if item.data.is_none() {
17540 item.data = Some(default_data.clone());
17541 }
17542 item
17543 })
17544 .collect::<Vec<lsp::CompletionItem>>(),
17545 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17546 );
17547 resolved_items.lock().clear();
17548
17549 cx.update_editor(|editor, window, cx| {
17550 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17551 });
17552 cx.run_until_parked();
17553 // Completions that have already been resolved are skipped.
17554 assert_eq!(
17555 *resolved_items.lock(),
17556 items[items.len() - 17..items.len() - 4]
17557 .iter()
17558 .cloned()
17559 .map(|mut item| {
17560 if item.data.is_none() {
17561 item.data = Some(default_data.clone());
17562 }
17563 item
17564 })
17565 .collect::<Vec<lsp::CompletionItem>>()
17566 );
17567 resolved_items.lock().clear();
17568}
17569
17570#[gpui::test]
17571async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17572 init_test(cx, |_| {});
17573
17574 let mut cx = EditorLspTestContext::new(
17575 Language::new(
17576 LanguageConfig {
17577 matcher: LanguageMatcher {
17578 path_suffixes: vec!["jsx".into()],
17579 ..Default::default()
17580 },
17581 overrides: [(
17582 "element".into(),
17583 LanguageConfigOverride {
17584 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17585 ..Default::default()
17586 },
17587 )]
17588 .into_iter()
17589 .collect(),
17590 ..Default::default()
17591 },
17592 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17593 )
17594 .with_override_query("(jsx_self_closing_element) @element")
17595 .unwrap(),
17596 lsp::ServerCapabilities {
17597 completion_provider: Some(lsp::CompletionOptions {
17598 trigger_characters: Some(vec![":".to_string()]),
17599 ..Default::default()
17600 }),
17601 ..Default::default()
17602 },
17603 cx,
17604 )
17605 .await;
17606
17607 cx.lsp
17608 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17609 Ok(Some(lsp::CompletionResponse::Array(vec![
17610 lsp::CompletionItem {
17611 label: "bg-blue".into(),
17612 ..Default::default()
17613 },
17614 lsp::CompletionItem {
17615 label: "bg-red".into(),
17616 ..Default::default()
17617 },
17618 lsp::CompletionItem {
17619 label: "bg-yellow".into(),
17620 ..Default::default()
17621 },
17622 ])))
17623 });
17624
17625 cx.set_state(r#"<p class="bgˇ" />"#);
17626
17627 // Trigger completion when typing a dash, because the dash is an extra
17628 // word character in the 'element' scope, which contains the cursor.
17629 cx.simulate_keystroke("-");
17630 cx.executor().run_until_parked();
17631 cx.update_editor(|editor, _, _| {
17632 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17633 {
17634 assert_eq!(
17635 completion_menu_entries(menu),
17636 &["bg-blue", "bg-red", "bg-yellow"]
17637 );
17638 } else {
17639 panic!("expected completion menu to be open");
17640 }
17641 });
17642
17643 cx.simulate_keystroke("l");
17644 cx.executor().run_until_parked();
17645 cx.update_editor(|editor, _, _| {
17646 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17647 {
17648 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17649 } else {
17650 panic!("expected completion menu to be open");
17651 }
17652 });
17653
17654 // When filtering completions, consider the character after the '-' to
17655 // be the start of a subword.
17656 cx.set_state(r#"<p class="yelˇ" />"#);
17657 cx.simulate_keystroke("l");
17658 cx.executor().run_until_parked();
17659 cx.update_editor(|editor, _, _| {
17660 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17661 {
17662 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17663 } else {
17664 panic!("expected completion menu to be open");
17665 }
17666 });
17667}
17668
17669fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17670 let entries = menu.entries.borrow();
17671 entries.iter().map(|mat| mat.string.clone()).collect()
17672}
17673
17674#[gpui::test]
17675async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17676 init_test(cx, |settings| {
17677 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17678 Formatter::Prettier,
17679 )))
17680 });
17681
17682 let fs = FakeFs::new(cx.executor());
17683 fs.insert_file(path!("/file.ts"), Default::default()).await;
17684
17685 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17686 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17687
17688 language_registry.add(Arc::new(Language::new(
17689 LanguageConfig {
17690 name: "TypeScript".into(),
17691 matcher: LanguageMatcher {
17692 path_suffixes: vec!["ts".to_string()],
17693 ..Default::default()
17694 },
17695 ..Default::default()
17696 },
17697 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17698 )));
17699 update_test_language_settings(cx, |settings| {
17700 settings.defaults.prettier = Some(PrettierSettings {
17701 allowed: true,
17702 ..PrettierSettings::default()
17703 });
17704 });
17705
17706 let test_plugin = "test_plugin";
17707 let _ = language_registry.register_fake_lsp(
17708 "TypeScript",
17709 FakeLspAdapter {
17710 prettier_plugins: vec![test_plugin],
17711 ..Default::default()
17712 },
17713 );
17714
17715 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17716 let buffer = project
17717 .update(cx, |project, cx| {
17718 project.open_local_buffer(path!("/file.ts"), cx)
17719 })
17720 .await
17721 .unwrap();
17722
17723 let buffer_text = "one\ntwo\nthree\n";
17724 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17725 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17726 editor.update_in(cx, |editor, window, cx| {
17727 editor.set_text(buffer_text, window, cx)
17728 });
17729
17730 editor
17731 .update_in(cx, |editor, window, cx| {
17732 editor.perform_format(
17733 project.clone(),
17734 FormatTrigger::Manual,
17735 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17736 window,
17737 cx,
17738 )
17739 })
17740 .unwrap()
17741 .await;
17742 assert_eq!(
17743 editor.update(cx, |editor, cx| editor.text(cx)),
17744 buffer_text.to_string() + prettier_format_suffix,
17745 "Test prettier formatting was not applied to the original buffer text",
17746 );
17747
17748 update_test_language_settings(cx, |settings| {
17749 settings.defaults.formatter = Some(SelectedFormatter::Auto)
17750 });
17751 let format = editor.update_in(cx, |editor, window, cx| {
17752 editor.perform_format(
17753 project.clone(),
17754 FormatTrigger::Manual,
17755 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17756 window,
17757 cx,
17758 )
17759 });
17760 format.await.unwrap();
17761 assert_eq!(
17762 editor.update(cx, |editor, cx| editor.text(cx)),
17763 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
17764 "Autoformatting (via test prettier) was not applied to the original buffer text",
17765 );
17766}
17767
17768#[gpui::test]
17769async fn test_addition_reverts(cx: &mut TestAppContext) {
17770 init_test(cx, |_| {});
17771 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17772 let base_text = indoc! {r#"
17773 struct Row;
17774 struct Row1;
17775 struct Row2;
17776
17777 struct Row4;
17778 struct Row5;
17779 struct Row6;
17780
17781 struct Row8;
17782 struct Row9;
17783 struct Row10;"#};
17784
17785 // When addition hunks are not adjacent to carets, no hunk revert is performed
17786 assert_hunk_revert(
17787 indoc! {r#"struct Row;
17788 struct Row1;
17789 struct Row1.1;
17790 struct Row1.2;
17791 struct Row2;ˇ
17792
17793 struct Row4;
17794 struct Row5;
17795 struct Row6;
17796
17797 struct Row8;
17798 ˇstruct Row9;
17799 struct Row9.1;
17800 struct Row9.2;
17801 struct Row9.3;
17802 struct Row10;"#},
17803 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17804 indoc! {r#"struct Row;
17805 struct Row1;
17806 struct Row1.1;
17807 struct Row1.2;
17808 struct Row2;ˇ
17809
17810 struct Row4;
17811 struct Row5;
17812 struct Row6;
17813
17814 struct Row8;
17815 ˇstruct Row9;
17816 struct Row9.1;
17817 struct Row9.2;
17818 struct Row9.3;
17819 struct Row10;"#},
17820 base_text,
17821 &mut cx,
17822 );
17823 // Same for selections
17824 assert_hunk_revert(
17825 indoc! {r#"struct Row;
17826 struct Row1;
17827 struct Row2;
17828 struct Row2.1;
17829 struct Row2.2;
17830 «ˇ
17831 struct Row4;
17832 struct» Row5;
17833 «struct Row6;
17834 ˇ»
17835 struct Row9.1;
17836 struct Row9.2;
17837 struct Row9.3;
17838 struct Row8;
17839 struct Row9;
17840 struct Row10;"#},
17841 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17842 indoc! {r#"struct Row;
17843 struct Row1;
17844 struct Row2;
17845 struct Row2.1;
17846 struct Row2.2;
17847 «ˇ
17848 struct Row4;
17849 struct» Row5;
17850 «struct Row6;
17851 ˇ»
17852 struct Row9.1;
17853 struct Row9.2;
17854 struct Row9.3;
17855 struct Row8;
17856 struct Row9;
17857 struct Row10;"#},
17858 base_text,
17859 &mut cx,
17860 );
17861
17862 // When carets and selections intersect the addition hunks, those are reverted.
17863 // Adjacent carets got merged.
17864 assert_hunk_revert(
17865 indoc! {r#"struct Row;
17866 ˇ// something on the top
17867 struct Row1;
17868 struct Row2;
17869 struct Roˇw3.1;
17870 struct Row2.2;
17871 struct Row2.3;ˇ
17872
17873 struct Row4;
17874 struct ˇRow5.1;
17875 struct Row5.2;
17876 struct «Rowˇ»5.3;
17877 struct Row5;
17878 struct Row6;
17879 ˇ
17880 struct Row9.1;
17881 struct «Rowˇ»9.2;
17882 struct «ˇRow»9.3;
17883 struct Row8;
17884 struct Row9;
17885 «ˇ// something on bottom»
17886 struct Row10;"#},
17887 vec![
17888 DiffHunkStatusKind::Added,
17889 DiffHunkStatusKind::Added,
17890 DiffHunkStatusKind::Added,
17891 DiffHunkStatusKind::Added,
17892 DiffHunkStatusKind::Added,
17893 ],
17894 indoc! {r#"struct Row;
17895 ˇstruct Row1;
17896 struct Row2;
17897 ˇ
17898 struct Row4;
17899 ˇstruct Row5;
17900 struct Row6;
17901 ˇ
17902 ˇstruct Row8;
17903 struct Row9;
17904 ˇstruct Row10;"#},
17905 base_text,
17906 &mut cx,
17907 );
17908}
17909
17910#[gpui::test]
17911async fn test_modification_reverts(cx: &mut TestAppContext) {
17912 init_test(cx, |_| {});
17913 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17914 let base_text = indoc! {r#"
17915 struct Row;
17916 struct Row1;
17917 struct Row2;
17918
17919 struct Row4;
17920 struct Row5;
17921 struct Row6;
17922
17923 struct Row8;
17924 struct Row9;
17925 struct Row10;"#};
17926
17927 // Modification hunks behave the same as the addition ones.
17928 assert_hunk_revert(
17929 indoc! {r#"struct Row;
17930 struct Row1;
17931 struct Row33;
17932 ˇ
17933 struct Row4;
17934 struct Row5;
17935 struct Row6;
17936 ˇ
17937 struct Row99;
17938 struct Row9;
17939 struct Row10;"#},
17940 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17941 indoc! {r#"struct Row;
17942 struct Row1;
17943 struct Row33;
17944 ˇ
17945 struct Row4;
17946 struct Row5;
17947 struct Row6;
17948 ˇ
17949 struct Row99;
17950 struct Row9;
17951 struct Row10;"#},
17952 base_text,
17953 &mut cx,
17954 );
17955 assert_hunk_revert(
17956 indoc! {r#"struct Row;
17957 struct Row1;
17958 struct Row33;
17959 «ˇ
17960 struct Row4;
17961 struct» Row5;
17962 «struct Row6;
17963 ˇ»
17964 struct Row99;
17965 struct Row9;
17966 struct Row10;"#},
17967 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17968 indoc! {r#"struct Row;
17969 struct Row1;
17970 struct Row33;
17971 «ˇ
17972 struct Row4;
17973 struct» Row5;
17974 «struct Row6;
17975 ˇ»
17976 struct Row99;
17977 struct Row9;
17978 struct Row10;"#},
17979 base_text,
17980 &mut cx,
17981 );
17982
17983 assert_hunk_revert(
17984 indoc! {r#"ˇstruct Row1.1;
17985 struct Row1;
17986 «ˇstr»uct Row22;
17987
17988 struct ˇRow44;
17989 struct Row5;
17990 struct «Rˇ»ow66;ˇ
17991
17992 «struˇ»ct Row88;
17993 struct Row9;
17994 struct Row1011;ˇ"#},
17995 vec![
17996 DiffHunkStatusKind::Modified,
17997 DiffHunkStatusKind::Modified,
17998 DiffHunkStatusKind::Modified,
17999 DiffHunkStatusKind::Modified,
18000 DiffHunkStatusKind::Modified,
18001 DiffHunkStatusKind::Modified,
18002 ],
18003 indoc! {r#"struct Row;
18004 ˇstruct Row1;
18005 struct Row2;
18006 ˇ
18007 struct Row4;
18008 ˇstruct Row5;
18009 struct Row6;
18010 ˇ
18011 struct Row8;
18012 ˇstruct Row9;
18013 struct Row10;ˇ"#},
18014 base_text,
18015 &mut cx,
18016 );
18017}
18018
18019#[gpui::test]
18020async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18021 init_test(cx, |_| {});
18022 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18023 let base_text = indoc! {r#"
18024 one
18025
18026 two
18027 three
18028 "#};
18029
18030 cx.set_head_text(base_text);
18031 cx.set_state("\nˇ\n");
18032 cx.executor().run_until_parked();
18033 cx.update_editor(|editor, _window, cx| {
18034 editor.expand_selected_diff_hunks(cx);
18035 });
18036 cx.executor().run_until_parked();
18037 cx.update_editor(|editor, window, cx| {
18038 editor.backspace(&Default::default(), window, cx);
18039 });
18040 cx.run_until_parked();
18041 cx.assert_state_with_diff(
18042 indoc! {r#"
18043
18044 - two
18045 - threeˇ
18046 +
18047 "#}
18048 .to_string(),
18049 );
18050}
18051
18052#[gpui::test]
18053async fn test_deletion_reverts(cx: &mut TestAppContext) {
18054 init_test(cx, |_| {});
18055 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18056 let base_text = indoc! {r#"struct Row;
18057struct Row1;
18058struct Row2;
18059
18060struct Row4;
18061struct Row5;
18062struct Row6;
18063
18064struct Row8;
18065struct Row9;
18066struct Row10;"#};
18067
18068 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18069 assert_hunk_revert(
18070 indoc! {r#"struct Row;
18071 struct Row2;
18072
18073 ˇstruct Row4;
18074 struct Row5;
18075 struct Row6;
18076 ˇ
18077 struct Row8;
18078 struct Row10;"#},
18079 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18080 indoc! {r#"struct Row;
18081 struct Row2;
18082
18083 ˇstruct Row4;
18084 struct Row5;
18085 struct Row6;
18086 ˇ
18087 struct Row8;
18088 struct Row10;"#},
18089 base_text,
18090 &mut cx,
18091 );
18092 assert_hunk_revert(
18093 indoc! {r#"struct Row;
18094 struct Row2;
18095
18096 «ˇstruct Row4;
18097 struct» Row5;
18098 «struct Row6;
18099 ˇ»
18100 struct Row8;
18101 struct Row10;"#},
18102 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18103 indoc! {r#"struct Row;
18104 struct Row2;
18105
18106 «ˇstruct Row4;
18107 struct» Row5;
18108 «struct Row6;
18109 ˇ»
18110 struct Row8;
18111 struct Row10;"#},
18112 base_text,
18113 &mut cx,
18114 );
18115
18116 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18117 assert_hunk_revert(
18118 indoc! {r#"struct Row;
18119 ˇstruct Row2;
18120
18121 struct Row4;
18122 struct Row5;
18123 struct Row6;
18124
18125 struct Row8;ˇ
18126 struct Row10;"#},
18127 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18128 indoc! {r#"struct Row;
18129 struct Row1;
18130 ˇstruct Row2;
18131
18132 struct Row4;
18133 struct Row5;
18134 struct Row6;
18135
18136 struct Row8;ˇ
18137 struct Row9;
18138 struct Row10;"#},
18139 base_text,
18140 &mut cx,
18141 );
18142 assert_hunk_revert(
18143 indoc! {r#"struct Row;
18144 struct Row2«ˇ;
18145 struct Row4;
18146 struct» Row5;
18147 «struct Row6;
18148
18149 struct Row8;ˇ»
18150 struct Row10;"#},
18151 vec![
18152 DiffHunkStatusKind::Deleted,
18153 DiffHunkStatusKind::Deleted,
18154 DiffHunkStatusKind::Deleted,
18155 ],
18156 indoc! {r#"struct Row;
18157 struct Row1;
18158 struct Row2«ˇ;
18159
18160 struct Row4;
18161 struct» Row5;
18162 «struct Row6;
18163
18164 struct Row8;ˇ»
18165 struct Row9;
18166 struct Row10;"#},
18167 base_text,
18168 &mut cx,
18169 );
18170}
18171
18172#[gpui::test]
18173async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18174 init_test(cx, |_| {});
18175
18176 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18177 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18178 let base_text_3 =
18179 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18180
18181 let text_1 = edit_first_char_of_every_line(base_text_1);
18182 let text_2 = edit_first_char_of_every_line(base_text_2);
18183 let text_3 = edit_first_char_of_every_line(base_text_3);
18184
18185 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18186 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18187 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18188
18189 let multibuffer = cx.new(|cx| {
18190 let mut multibuffer = MultiBuffer::new(ReadWrite);
18191 multibuffer.push_excerpts(
18192 buffer_1.clone(),
18193 [
18194 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18195 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18196 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18197 ],
18198 cx,
18199 );
18200 multibuffer.push_excerpts(
18201 buffer_2.clone(),
18202 [
18203 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18204 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18205 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18206 ],
18207 cx,
18208 );
18209 multibuffer.push_excerpts(
18210 buffer_3.clone(),
18211 [
18212 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18213 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18214 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18215 ],
18216 cx,
18217 );
18218 multibuffer
18219 });
18220
18221 let fs = FakeFs::new(cx.executor());
18222 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18223 let (editor, cx) = cx
18224 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18225 editor.update_in(cx, |editor, _window, cx| {
18226 for (buffer, diff_base) in [
18227 (buffer_1.clone(), base_text_1),
18228 (buffer_2.clone(), base_text_2),
18229 (buffer_3.clone(), base_text_3),
18230 ] {
18231 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18232 editor
18233 .buffer
18234 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18235 }
18236 });
18237 cx.executor().run_until_parked();
18238
18239 editor.update_in(cx, |editor, window, cx| {
18240 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}");
18241 editor.select_all(&SelectAll, window, cx);
18242 editor.git_restore(&Default::default(), window, cx);
18243 });
18244 cx.executor().run_until_parked();
18245
18246 // When all ranges are selected, all buffer hunks are reverted.
18247 editor.update(cx, |editor, cx| {
18248 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");
18249 });
18250 buffer_1.update(cx, |buffer, _| {
18251 assert_eq!(buffer.text(), base_text_1);
18252 });
18253 buffer_2.update(cx, |buffer, _| {
18254 assert_eq!(buffer.text(), base_text_2);
18255 });
18256 buffer_3.update(cx, |buffer, _| {
18257 assert_eq!(buffer.text(), base_text_3);
18258 });
18259
18260 editor.update_in(cx, |editor, window, cx| {
18261 editor.undo(&Default::default(), window, cx);
18262 });
18263
18264 editor.update_in(cx, |editor, window, cx| {
18265 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18266 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18267 });
18268 editor.git_restore(&Default::default(), window, cx);
18269 });
18270
18271 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18272 // but not affect buffer_2 and its related excerpts.
18273 editor.update(cx, |editor, cx| {
18274 assert_eq!(
18275 editor.text(cx),
18276 "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}"
18277 );
18278 });
18279 buffer_1.update(cx, |buffer, _| {
18280 assert_eq!(buffer.text(), base_text_1);
18281 });
18282 buffer_2.update(cx, |buffer, _| {
18283 assert_eq!(
18284 buffer.text(),
18285 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18286 );
18287 });
18288 buffer_3.update(cx, |buffer, _| {
18289 assert_eq!(
18290 buffer.text(),
18291 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18292 );
18293 });
18294
18295 fn edit_first_char_of_every_line(text: &str) -> String {
18296 text.split('\n')
18297 .map(|line| format!("X{}", &line[1..]))
18298 .collect::<Vec<_>>()
18299 .join("\n")
18300 }
18301}
18302
18303#[gpui::test]
18304async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18305 init_test(cx, |_| {});
18306
18307 let cols = 4;
18308 let rows = 10;
18309 let sample_text_1 = sample_text(rows, cols, 'a');
18310 assert_eq!(
18311 sample_text_1,
18312 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18313 );
18314 let sample_text_2 = sample_text(rows, cols, 'l');
18315 assert_eq!(
18316 sample_text_2,
18317 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18318 );
18319 let sample_text_3 = sample_text(rows, cols, 'v');
18320 assert_eq!(
18321 sample_text_3,
18322 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18323 );
18324
18325 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18326 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18327 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18328
18329 let multi_buffer = cx.new(|cx| {
18330 let mut multibuffer = MultiBuffer::new(ReadWrite);
18331 multibuffer.push_excerpts(
18332 buffer_1.clone(),
18333 [
18334 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18335 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18336 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18337 ],
18338 cx,
18339 );
18340 multibuffer.push_excerpts(
18341 buffer_2.clone(),
18342 [
18343 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18344 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18345 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18346 ],
18347 cx,
18348 );
18349 multibuffer.push_excerpts(
18350 buffer_3.clone(),
18351 [
18352 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18353 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18354 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18355 ],
18356 cx,
18357 );
18358 multibuffer
18359 });
18360
18361 let fs = FakeFs::new(cx.executor());
18362 fs.insert_tree(
18363 "/a",
18364 json!({
18365 "main.rs": sample_text_1,
18366 "other.rs": sample_text_2,
18367 "lib.rs": sample_text_3,
18368 }),
18369 )
18370 .await;
18371 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18372 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18373 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18374 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18375 Editor::new(
18376 EditorMode::full(),
18377 multi_buffer,
18378 Some(project.clone()),
18379 window,
18380 cx,
18381 )
18382 });
18383 let multibuffer_item_id = workspace
18384 .update(cx, |workspace, window, cx| {
18385 assert!(
18386 workspace.active_item(cx).is_none(),
18387 "active item should be None before the first item is added"
18388 );
18389 workspace.add_item_to_active_pane(
18390 Box::new(multi_buffer_editor.clone()),
18391 None,
18392 true,
18393 window,
18394 cx,
18395 );
18396 let active_item = workspace
18397 .active_item(cx)
18398 .expect("should have an active item after adding the multi buffer");
18399 assert!(
18400 !active_item.is_singleton(cx),
18401 "A multi buffer was expected to active after adding"
18402 );
18403 active_item.item_id()
18404 })
18405 .unwrap();
18406 cx.executor().run_until_parked();
18407
18408 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18409 editor.change_selections(
18410 SelectionEffects::scroll(Autoscroll::Next),
18411 window,
18412 cx,
18413 |s| s.select_ranges(Some(1..2)),
18414 );
18415 editor.open_excerpts(&OpenExcerpts, window, cx);
18416 });
18417 cx.executor().run_until_parked();
18418 let first_item_id = workspace
18419 .update(cx, |workspace, window, cx| {
18420 let active_item = workspace
18421 .active_item(cx)
18422 .expect("should have an active item after navigating into the 1st buffer");
18423 let first_item_id = active_item.item_id();
18424 assert_ne!(
18425 first_item_id, multibuffer_item_id,
18426 "Should navigate into the 1st buffer and activate it"
18427 );
18428 assert!(
18429 active_item.is_singleton(cx),
18430 "New active item should be a singleton buffer"
18431 );
18432 assert_eq!(
18433 active_item
18434 .act_as::<Editor>(cx)
18435 .expect("should have navigated into an editor for the 1st buffer")
18436 .read(cx)
18437 .text(cx),
18438 sample_text_1
18439 );
18440
18441 workspace
18442 .go_back(workspace.active_pane().downgrade(), window, cx)
18443 .detach_and_log_err(cx);
18444
18445 first_item_id
18446 })
18447 .unwrap();
18448 cx.executor().run_until_parked();
18449 workspace
18450 .update(cx, |workspace, _, cx| {
18451 let active_item = workspace
18452 .active_item(cx)
18453 .expect("should have an active item after navigating back");
18454 assert_eq!(
18455 active_item.item_id(),
18456 multibuffer_item_id,
18457 "Should navigate back to the multi buffer"
18458 );
18459 assert!(!active_item.is_singleton(cx));
18460 })
18461 .unwrap();
18462
18463 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18464 editor.change_selections(
18465 SelectionEffects::scroll(Autoscroll::Next),
18466 window,
18467 cx,
18468 |s| s.select_ranges(Some(39..40)),
18469 );
18470 editor.open_excerpts(&OpenExcerpts, window, cx);
18471 });
18472 cx.executor().run_until_parked();
18473 let second_item_id = workspace
18474 .update(cx, |workspace, window, cx| {
18475 let active_item = workspace
18476 .active_item(cx)
18477 .expect("should have an active item after navigating into the 2nd buffer");
18478 let second_item_id = active_item.item_id();
18479 assert_ne!(
18480 second_item_id, multibuffer_item_id,
18481 "Should navigate away from the multibuffer"
18482 );
18483 assert_ne!(
18484 second_item_id, first_item_id,
18485 "Should navigate into the 2nd buffer and activate it"
18486 );
18487 assert!(
18488 active_item.is_singleton(cx),
18489 "New active item should be a singleton buffer"
18490 );
18491 assert_eq!(
18492 active_item
18493 .act_as::<Editor>(cx)
18494 .expect("should have navigated into an editor")
18495 .read(cx)
18496 .text(cx),
18497 sample_text_2
18498 );
18499
18500 workspace
18501 .go_back(workspace.active_pane().downgrade(), window, cx)
18502 .detach_and_log_err(cx);
18503
18504 second_item_id
18505 })
18506 .unwrap();
18507 cx.executor().run_until_parked();
18508 workspace
18509 .update(cx, |workspace, _, cx| {
18510 let active_item = workspace
18511 .active_item(cx)
18512 .expect("should have an active item after navigating back from the 2nd buffer");
18513 assert_eq!(
18514 active_item.item_id(),
18515 multibuffer_item_id,
18516 "Should navigate back from the 2nd buffer to the multi buffer"
18517 );
18518 assert!(!active_item.is_singleton(cx));
18519 })
18520 .unwrap();
18521
18522 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18523 editor.change_selections(
18524 SelectionEffects::scroll(Autoscroll::Next),
18525 window,
18526 cx,
18527 |s| s.select_ranges(Some(70..70)),
18528 );
18529 editor.open_excerpts(&OpenExcerpts, window, cx);
18530 });
18531 cx.executor().run_until_parked();
18532 workspace
18533 .update(cx, |workspace, window, cx| {
18534 let active_item = workspace
18535 .active_item(cx)
18536 .expect("should have an active item after navigating into the 3rd buffer");
18537 let third_item_id = active_item.item_id();
18538 assert_ne!(
18539 third_item_id, multibuffer_item_id,
18540 "Should navigate into the 3rd buffer and activate it"
18541 );
18542 assert_ne!(third_item_id, first_item_id);
18543 assert_ne!(third_item_id, second_item_id);
18544 assert!(
18545 active_item.is_singleton(cx),
18546 "New active item should be a singleton buffer"
18547 );
18548 assert_eq!(
18549 active_item
18550 .act_as::<Editor>(cx)
18551 .expect("should have navigated into an editor")
18552 .read(cx)
18553 .text(cx),
18554 sample_text_3
18555 );
18556
18557 workspace
18558 .go_back(workspace.active_pane().downgrade(), window, cx)
18559 .detach_and_log_err(cx);
18560 })
18561 .unwrap();
18562 cx.executor().run_until_parked();
18563 workspace
18564 .update(cx, |workspace, _, cx| {
18565 let active_item = workspace
18566 .active_item(cx)
18567 .expect("should have an active item after navigating back from the 3rd buffer");
18568 assert_eq!(
18569 active_item.item_id(),
18570 multibuffer_item_id,
18571 "Should navigate back from the 3rd buffer to the multi buffer"
18572 );
18573 assert!(!active_item.is_singleton(cx));
18574 })
18575 .unwrap();
18576}
18577
18578#[gpui::test]
18579async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18580 init_test(cx, |_| {});
18581
18582 let mut cx = EditorTestContext::new(cx).await;
18583
18584 let diff_base = r#"
18585 use some::mod;
18586
18587 const A: u32 = 42;
18588
18589 fn main() {
18590 println!("hello");
18591
18592 println!("world");
18593 }
18594 "#
18595 .unindent();
18596
18597 cx.set_state(
18598 &r#"
18599 use some::modified;
18600
18601 ˇ
18602 fn main() {
18603 println!("hello there");
18604
18605 println!("around the");
18606 println!("world");
18607 }
18608 "#
18609 .unindent(),
18610 );
18611
18612 cx.set_head_text(&diff_base);
18613 executor.run_until_parked();
18614
18615 cx.update_editor(|editor, window, cx| {
18616 editor.go_to_next_hunk(&GoToHunk, window, cx);
18617 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18618 });
18619 executor.run_until_parked();
18620 cx.assert_state_with_diff(
18621 r#"
18622 use some::modified;
18623
18624
18625 fn main() {
18626 - println!("hello");
18627 + ˇ println!("hello there");
18628
18629 println!("around the");
18630 println!("world");
18631 }
18632 "#
18633 .unindent(),
18634 );
18635
18636 cx.update_editor(|editor, window, cx| {
18637 for _ in 0..2 {
18638 editor.go_to_next_hunk(&GoToHunk, window, cx);
18639 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18640 }
18641 });
18642 executor.run_until_parked();
18643 cx.assert_state_with_diff(
18644 r#"
18645 - use some::mod;
18646 + ˇuse some::modified;
18647
18648
18649 fn main() {
18650 - println!("hello");
18651 + println!("hello there");
18652
18653 + println!("around the");
18654 println!("world");
18655 }
18656 "#
18657 .unindent(),
18658 );
18659
18660 cx.update_editor(|editor, window, cx| {
18661 editor.go_to_next_hunk(&GoToHunk, window, cx);
18662 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18663 });
18664 executor.run_until_parked();
18665 cx.assert_state_with_diff(
18666 r#"
18667 - use some::mod;
18668 + use some::modified;
18669
18670 - const A: u32 = 42;
18671 ˇ
18672 fn main() {
18673 - println!("hello");
18674 + println!("hello there");
18675
18676 + println!("around the");
18677 println!("world");
18678 }
18679 "#
18680 .unindent(),
18681 );
18682
18683 cx.update_editor(|editor, window, cx| {
18684 editor.cancel(&Cancel, window, cx);
18685 });
18686
18687 cx.assert_state_with_diff(
18688 r#"
18689 use some::modified;
18690
18691 ˇ
18692 fn main() {
18693 println!("hello there");
18694
18695 println!("around the");
18696 println!("world");
18697 }
18698 "#
18699 .unindent(),
18700 );
18701}
18702
18703#[gpui::test]
18704async fn test_diff_base_change_with_expanded_diff_hunks(
18705 executor: BackgroundExecutor,
18706 cx: &mut TestAppContext,
18707) {
18708 init_test(cx, |_| {});
18709
18710 let mut cx = EditorTestContext::new(cx).await;
18711
18712 let diff_base = r#"
18713 use some::mod1;
18714 use some::mod2;
18715
18716 const A: u32 = 42;
18717 const B: u32 = 42;
18718 const C: u32 = 42;
18719
18720 fn main() {
18721 println!("hello");
18722
18723 println!("world");
18724 }
18725 "#
18726 .unindent();
18727
18728 cx.set_state(
18729 &r#"
18730 use some::mod2;
18731
18732 const A: u32 = 42;
18733 const C: u32 = 42;
18734
18735 fn main(ˇ) {
18736 //println!("hello");
18737
18738 println!("world");
18739 //
18740 //
18741 }
18742 "#
18743 .unindent(),
18744 );
18745
18746 cx.set_head_text(&diff_base);
18747 executor.run_until_parked();
18748
18749 cx.update_editor(|editor, window, cx| {
18750 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18751 });
18752 executor.run_until_parked();
18753 cx.assert_state_with_diff(
18754 r#"
18755 - use some::mod1;
18756 use some::mod2;
18757
18758 const A: u32 = 42;
18759 - const B: u32 = 42;
18760 const C: u32 = 42;
18761
18762 fn main(ˇ) {
18763 - println!("hello");
18764 + //println!("hello");
18765
18766 println!("world");
18767 + //
18768 + //
18769 }
18770 "#
18771 .unindent(),
18772 );
18773
18774 cx.set_head_text("new diff base!");
18775 executor.run_until_parked();
18776 cx.assert_state_with_diff(
18777 r#"
18778 - new diff base!
18779 + use some::mod2;
18780 +
18781 + const A: u32 = 42;
18782 + const C: u32 = 42;
18783 +
18784 + fn main(ˇ) {
18785 + //println!("hello");
18786 +
18787 + println!("world");
18788 + //
18789 + //
18790 + }
18791 "#
18792 .unindent(),
18793 );
18794}
18795
18796#[gpui::test]
18797async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
18798 init_test(cx, |_| {});
18799
18800 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18801 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18802 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18803 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18804 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
18805 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
18806
18807 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
18808 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
18809 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
18810
18811 let multi_buffer = cx.new(|cx| {
18812 let mut multibuffer = MultiBuffer::new(ReadWrite);
18813 multibuffer.push_excerpts(
18814 buffer_1.clone(),
18815 [
18816 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18817 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18818 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18819 ],
18820 cx,
18821 );
18822 multibuffer.push_excerpts(
18823 buffer_2.clone(),
18824 [
18825 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18826 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18827 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18828 ],
18829 cx,
18830 );
18831 multibuffer.push_excerpts(
18832 buffer_3.clone(),
18833 [
18834 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18835 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18836 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18837 ],
18838 cx,
18839 );
18840 multibuffer
18841 });
18842
18843 let editor =
18844 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18845 editor
18846 .update(cx, |editor, _window, cx| {
18847 for (buffer, diff_base) in [
18848 (buffer_1.clone(), file_1_old),
18849 (buffer_2.clone(), file_2_old),
18850 (buffer_3.clone(), file_3_old),
18851 ] {
18852 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18853 editor
18854 .buffer
18855 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18856 }
18857 })
18858 .unwrap();
18859
18860 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18861 cx.run_until_parked();
18862
18863 cx.assert_editor_state(
18864 &"
18865 ˇaaa
18866 ccc
18867 ddd
18868
18869 ggg
18870 hhh
18871
18872
18873 lll
18874 mmm
18875 NNN
18876
18877 qqq
18878 rrr
18879
18880 uuu
18881 111
18882 222
18883 333
18884
18885 666
18886 777
18887
18888 000
18889 !!!"
18890 .unindent(),
18891 );
18892
18893 cx.update_editor(|editor, window, cx| {
18894 editor.select_all(&SelectAll, window, cx);
18895 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18896 });
18897 cx.executor().run_until_parked();
18898
18899 cx.assert_state_with_diff(
18900 "
18901 «aaa
18902 - bbb
18903 ccc
18904 ddd
18905
18906 ggg
18907 hhh
18908
18909
18910 lll
18911 mmm
18912 - nnn
18913 + NNN
18914
18915 qqq
18916 rrr
18917
18918 uuu
18919 111
18920 222
18921 333
18922
18923 + 666
18924 777
18925
18926 000
18927 !!!ˇ»"
18928 .unindent(),
18929 );
18930}
18931
18932#[gpui::test]
18933async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18934 init_test(cx, |_| {});
18935
18936 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
18937 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
18938
18939 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18940 let multi_buffer = cx.new(|cx| {
18941 let mut multibuffer = MultiBuffer::new(ReadWrite);
18942 multibuffer.push_excerpts(
18943 buffer.clone(),
18944 [
18945 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18946 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18947 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18948 ],
18949 cx,
18950 );
18951 multibuffer
18952 });
18953
18954 let editor =
18955 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18956 editor
18957 .update(cx, |editor, _window, cx| {
18958 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18959 editor
18960 .buffer
18961 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18962 })
18963 .unwrap();
18964
18965 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18966 cx.run_until_parked();
18967
18968 cx.update_editor(|editor, window, cx| {
18969 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18970 });
18971 cx.executor().run_until_parked();
18972
18973 // When the start of a hunk coincides with the start of its excerpt,
18974 // the hunk is expanded. When the start of a a hunk is earlier than
18975 // the start of its excerpt, the hunk is not expanded.
18976 cx.assert_state_with_diff(
18977 "
18978 ˇaaa
18979 - bbb
18980 + BBB
18981
18982 - ddd
18983 - eee
18984 + DDD
18985 + EEE
18986 fff
18987
18988 iii
18989 "
18990 .unindent(),
18991 );
18992}
18993
18994#[gpui::test]
18995async fn test_edits_around_expanded_insertion_hunks(
18996 executor: BackgroundExecutor,
18997 cx: &mut TestAppContext,
18998) {
18999 init_test(cx, |_| {});
19000
19001 let mut cx = EditorTestContext::new(cx).await;
19002
19003 let diff_base = r#"
19004 use some::mod1;
19005 use some::mod2;
19006
19007 const A: u32 = 42;
19008
19009 fn main() {
19010 println!("hello");
19011
19012 println!("world");
19013 }
19014 "#
19015 .unindent();
19016 executor.run_until_parked();
19017 cx.set_state(
19018 &r#"
19019 use some::mod1;
19020 use some::mod2;
19021
19022 const A: u32 = 42;
19023 const B: u32 = 42;
19024 const C: u32 = 42;
19025 ˇ
19026
19027 fn main() {
19028 println!("hello");
19029
19030 println!("world");
19031 }
19032 "#
19033 .unindent(),
19034 );
19035
19036 cx.set_head_text(&diff_base);
19037 executor.run_until_parked();
19038
19039 cx.update_editor(|editor, window, cx| {
19040 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19041 });
19042 executor.run_until_parked();
19043
19044 cx.assert_state_with_diff(
19045 r#"
19046 use some::mod1;
19047 use some::mod2;
19048
19049 const A: u32 = 42;
19050 + const B: u32 = 42;
19051 + const C: u32 = 42;
19052 + ˇ
19053
19054 fn main() {
19055 println!("hello");
19056
19057 println!("world");
19058 }
19059 "#
19060 .unindent(),
19061 );
19062
19063 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19064 executor.run_until_parked();
19065
19066 cx.assert_state_with_diff(
19067 r#"
19068 use some::mod1;
19069 use some::mod2;
19070
19071 const A: u32 = 42;
19072 + const B: u32 = 42;
19073 + const C: u32 = 42;
19074 + const D: u32 = 42;
19075 + ˇ
19076
19077 fn main() {
19078 println!("hello");
19079
19080 println!("world");
19081 }
19082 "#
19083 .unindent(),
19084 );
19085
19086 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19087 executor.run_until_parked();
19088
19089 cx.assert_state_with_diff(
19090 r#"
19091 use some::mod1;
19092 use some::mod2;
19093
19094 const A: u32 = 42;
19095 + const B: u32 = 42;
19096 + const C: u32 = 42;
19097 + const D: u32 = 42;
19098 + const E: u32 = 42;
19099 + ˇ
19100
19101 fn main() {
19102 println!("hello");
19103
19104 println!("world");
19105 }
19106 "#
19107 .unindent(),
19108 );
19109
19110 cx.update_editor(|editor, window, cx| {
19111 editor.delete_line(&DeleteLine, window, cx);
19112 });
19113 executor.run_until_parked();
19114
19115 cx.assert_state_with_diff(
19116 r#"
19117 use some::mod1;
19118 use some::mod2;
19119
19120 const A: u32 = 42;
19121 + const B: u32 = 42;
19122 + const C: u32 = 42;
19123 + const D: u32 = 42;
19124 + const E: u32 = 42;
19125 ˇ
19126 fn main() {
19127 println!("hello");
19128
19129 println!("world");
19130 }
19131 "#
19132 .unindent(),
19133 );
19134
19135 cx.update_editor(|editor, window, cx| {
19136 editor.move_up(&MoveUp, window, cx);
19137 editor.delete_line(&DeleteLine, window, cx);
19138 editor.move_up(&MoveUp, window, cx);
19139 editor.delete_line(&DeleteLine, window, cx);
19140 editor.move_up(&MoveUp, window, cx);
19141 editor.delete_line(&DeleteLine, window, cx);
19142 });
19143 executor.run_until_parked();
19144 cx.assert_state_with_diff(
19145 r#"
19146 use some::mod1;
19147 use some::mod2;
19148
19149 const A: u32 = 42;
19150 + const B: u32 = 42;
19151 ˇ
19152 fn main() {
19153 println!("hello");
19154
19155 println!("world");
19156 }
19157 "#
19158 .unindent(),
19159 );
19160
19161 cx.update_editor(|editor, window, cx| {
19162 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19163 editor.delete_line(&DeleteLine, window, cx);
19164 });
19165 executor.run_until_parked();
19166 cx.assert_state_with_diff(
19167 r#"
19168 ˇ
19169 fn main() {
19170 println!("hello");
19171
19172 println!("world");
19173 }
19174 "#
19175 .unindent(),
19176 );
19177}
19178
19179#[gpui::test]
19180async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19181 init_test(cx, |_| {});
19182
19183 let mut cx = EditorTestContext::new(cx).await;
19184 cx.set_head_text(indoc! { "
19185 one
19186 two
19187 three
19188 four
19189 five
19190 "
19191 });
19192 cx.set_state(indoc! { "
19193 one
19194 ˇthree
19195 five
19196 "});
19197 cx.run_until_parked();
19198 cx.update_editor(|editor, window, cx| {
19199 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19200 });
19201 cx.assert_state_with_diff(
19202 indoc! { "
19203 one
19204 - two
19205 ˇthree
19206 - four
19207 five
19208 "}
19209 .to_string(),
19210 );
19211 cx.update_editor(|editor, window, cx| {
19212 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19213 });
19214
19215 cx.assert_state_with_diff(
19216 indoc! { "
19217 one
19218 ˇthree
19219 five
19220 "}
19221 .to_string(),
19222 );
19223
19224 cx.set_state(indoc! { "
19225 one
19226 ˇTWO
19227 three
19228 four
19229 five
19230 "});
19231 cx.run_until_parked();
19232 cx.update_editor(|editor, window, cx| {
19233 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19234 });
19235
19236 cx.assert_state_with_diff(
19237 indoc! { "
19238 one
19239 - two
19240 + ˇTWO
19241 three
19242 four
19243 five
19244 "}
19245 .to_string(),
19246 );
19247 cx.update_editor(|editor, window, cx| {
19248 editor.move_up(&Default::default(), window, cx);
19249 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19250 });
19251 cx.assert_state_with_diff(
19252 indoc! { "
19253 one
19254 ˇTWO
19255 three
19256 four
19257 five
19258 "}
19259 .to_string(),
19260 );
19261}
19262
19263#[gpui::test]
19264async fn test_edits_around_expanded_deletion_hunks(
19265 executor: BackgroundExecutor,
19266 cx: &mut TestAppContext,
19267) {
19268 init_test(cx, |_| {});
19269
19270 let mut cx = EditorTestContext::new(cx).await;
19271
19272 let diff_base = r#"
19273 use some::mod1;
19274 use some::mod2;
19275
19276 const A: u32 = 42;
19277 const B: u32 = 42;
19278 const C: u32 = 42;
19279
19280
19281 fn main() {
19282 println!("hello");
19283
19284 println!("world");
19285 }
19286 "#
19287 .unindent();
19288 executor.run_until_parked();
19289 cx.set_state(
19290 &r#"
19291 use some::mod1;
19292 use some::mod2;
19293
19294 ˇconst B: u32 = 42;
19295 const C: u32 = 42;
19296
19297
19298 fn main() {
19299 println!("hello");
19300
19301 println!("world");
19302 }
19303 "#
19304 .unindent(),
19305 );
19306
19307 cx.set_head_text(&diff_base);
19308 executor.run_until_parked();
19309
19310 cx.update_editor(|editor, window, cx| {
19311 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19312 });
19313 executor.run_until_parked();
19314
19315 cx.assert_state_with_diff(
19316 r#"
19317 use some::mod1;
19318 use some::mod2;
19319
19320 - const A: u32 = 42;
19321 ˇconst B: u32 = 42;
19322 const C: u32 = 42;
19323
19324
19325 fn main() {
19326 println!("hello");
19327
19328 println!("world");
19329 }
19330 "#
19331 .unindent(),
19332 );
19333
19334 cx.update_editor(|editor, window, cx| {
19335 editor.delete_line(&DeleteLine, window, cx);
19336 });
19337 executor.run_until_parked();
19338 cx.assert_state_with_diff(
19339 r#"
19340 use some::mod1;
19341 use some::mod2;
19342
19343 - const A: u32 = 42;
19344 - const B: u32 = 42;
19345 ˇconst C: u32 = 42;
19346
19347
19348 fn main() {
19349 println!("hello");
19350
19351 println!("world");
19352 }
19353 "#
19354 .unindent(),
19355 );
19356
19357 cx.update_editor(|editor, window, cx| {
19358 editor.delete_line(&DeleteLine, window, cx);
19359 });
19360 executor.run_until_parked();
19361 cx.assert_state_with_diff(
19362 r#"
19363 use some::mod1;
19364 use some::mod2;
19365
19366 - const A: u32 = 42;
19367 - const B: u32 = 42;
19368 - const C: u32 = 42;
19369 ˇ
19370
19371 fn main() {
19372 println!("hello");
19373
19374 println!("world");
19375 }
19376 "#
19377 .unindent(),
19378 );
19379
19380 cx.update_editor(|editor, window, cx| {
19381 editor.handle_input("replacement", window, cx);
19382 });
19383 executor.run_until_parked();
19384 cx.assert_state_with_diff(
19385 r#"
19386 use some::mod1;
19387 use some::mod2;
19388
19389 - const A: u32 = 42;
19390 - const B: u32 = 42;
19391 - const C: u32 = 42;
19392 -
19393 + replacementˇ
19394
19395 fn main() {
19396 println!("hello");
19397
19398 println!("world");
19399 }
19400 "#
19401 .unindent(),
19402 );
19403}
19404
19405#[gpui::test]
19406async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19407 init_test(cx, |_| {});
19408
19409 let mut cx = EditorTestContext::new(cx).await;
19410
19411 let base_text = r#"
19412 one
19413 two
19414 three
19415 four
19416 five
19417 "#
19418 .unindent();
19419 executor.run_until_parked();
19420 cx.set_state(
19421 &r#"
19422 one
19423 two
19424 fˇour
19425 five
19426 "#
19427 .unindent(),
19428 );
19429
19430 cx.set_head_text(&base_text);
19431 executor.run_until_parked();
19432
19433 cx.update_editor(|editor, window, cx| {
19434 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19435 });
19436 executor.run_until_parked();
19437
19438 cx.assert_state_with_diff(
19439 r#"
19440 one
19441 two
19442 - three
19443 fˇour
19444 five
19445 "#
19446 .unindent(),
19447 );
19448
19449 cx.update_editor(|editor, window, cx| {
19450 editor.backspace(&Backspace, window, cx);
19451 editor.backspace(&Backspace, window, cx);
19452 });
19453 executor.run_until_parked();
19454 cx.assert_state_with_diff(
19455 r#"
19456 one
19457 two
19458 - threeˇ
19459 - four
19460 + our
19461 five
19462 "#
19463 .unindent(),
19464 );
19465}
19466
19467#[gpui::test]
19468async fn test_edit_after_expanded_modification_hunk(
19469 executor: BackgroundExecutor,
19470 cx: &mut TestAppContext,
19471) {
19472 init_test(cx, |_| {});
19473
19474 let mut cx = EditorTestContext::new(cx).await;
19475
19476 let diff_base = r#"
19477 use some::mod1;
19478 use some::mod2;
19479
19480 const A: u32 = 42;
19481 const B: u32 = 42;
19482 const C: u32 = 42;
19483 const D: u32 = 42;
19484
19485
19486 fn main() {
19487 println!("hello");
19488
19489 println!("world");
19490 }"#
19491 .unindent();
19492
19493 cx.set_state(
19494 &r#"
19495 use some::mod1;
19496 use some::mod2;
19497
19498 const A: u32 = 42;
19499 const B: u32 = 42;
19500 const C: u32 = 43ˇ
19501 const D: u32 = 42;
19502
19503
19504 fn main() {
19505 println!("hello");
19506
19507 println!("world");
19508 }"#
19509 .unindent(),
19510 );
19511
19512 cx.set_head_text(&diff_base);
19513 executor.run_until_parked();
19514 cx.update_editor(|editor, window, cx| {
19515 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19516 });
19517 executor.run_until_parked();
19518
19519 cx.assert_state_with_diff(
19520 r#"
19521 use some::mod1;
19522 use some::mod2;
19523
19524 const A: u32 = 42;
19525 const B: u32 = 42;
19526 - const C: u32 = 42;
19527 + const C: u32 = 43ˇ
19528 const D: u32 = 42;
19529
19530
19531 fn main() {
19532 println!("hello");
19533
19534 println!("world");
19535 }"#
19536 .unindent(),
19537 );
19538
19539 cx.update_editor(|editor, window, cx| {
19540 editor.handle_input("\nnew_line\n", window, cx);
19541 });
19542 executor.run_until_parked();
19543
19544 cx.assert_state_with_diff(
19545 r#"
19546 use some::mod1;
19547 use some::mod2;
19548
19549 const A: u32 = 42;
19550 const B: u32 = 42;
19551 - const C: u32 = 42;
19552 + const C: u32 = 43
19553 + new_line
19554 + ˇ
19555 const D: u32 = 42;
19556
19557
19558 fn main() {
19559 println!("hello");
19560
19561 println!("world");
19562 }"#
19563 .unindent(),
19564 );
19565}
19566
19567#[gpui::test]
19568async fn test_stage_and_unstage_added_file_hunk(
19569 executor: BackgroundExecutor,
19570 cx: &mut TestAppContext,
19571) {
19572 init_test(cx, |_| {});
19573
19574 let mut cx = EditorTestContext::new(cx).await;
19575 cx.update_editor(|editor, _, cx| {
19576 editor.set_expand_all_diff_hunks(cx);
19577 });
19578
19579 let working_copy = r#"
19580 ˇfn main() {
19581 println!("hello, world!");
19582 }
19583 "#
19584 .unindent();
19585
19586 cx.set_state(&working_copy);
19587 executor.run_until_parked();
19588
19589 cx.assert_state_with_diff(
19590 r#"
19591 + ˇfn main() {
19592 + println!("hello, world!");
19593 + }
19594 "#
19595 .unindent(),
19596 );
19597 cx.assert_index_text(None);
19598
19599 cx.update_editor(|editor, window, cx| {
19600 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19601 });
19602 executor.run_until_parked();
19603 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19604 cx.assert_state_with_diff(
19605 r#"
19606 + ˇfn main() {
19607 + println!("hello, world!");
19608 + }
19609 "#
19610 .unindent(),
19611 );
19612
19613 cx.update_editor(|editor, window, cx| {
19614 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19615 });
19616 executor.run_until_parked();
19617 cx.assert_index_text(None);
19618}
19619
19620async fn setup_indent_guides_editor(
19621 text: &str,
19622 cx: &mut TestAppContext,
19623) -> (BufferId, EditorTestContext) {
19624 init_test(cx, |_| {});
19625
19626 let mut cx = EditorTestContext::new(cx).await;
19627
19628 let buffer_id = cx.update_editor(|editor, window, cx| {
19629 editor.set_text(text, window, cx);
19630 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19631
19632 buffer_ids[0]
19633 });
19634
19635 (buffer_id, cx)
19636}
19637
19638fn assert_indent_guides(
19639 range: Range<u32>,
19640 expected: Vec<IndentGuide>,
19641 active_indices: Option<Vec<usize>>,
19642 cx: &mut EditorTestContext,
19643) {
19644 let indent_guides = cx.update_editor(|editor, window, cx| {
19645 let snapshot = editor.snapshot(window, cx).display_snapshot;
19646 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19647 editor,
19648 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19649 true,
19650 &snapshot,
19651 cx,
19652 );
19653
19654 indent_guides.sort_by(|a, b| {
19655 a.depth.cmp(&b.depth).then(
19656 a.start_row
19657 .cmp(&b.start_row)
19658 .then(a.end_row.cmp(&b.end_row)),
19659 )
19660 });
19661 indent_guides
19662 });
19663
19664 if let Some(expected) = active_indices {
19665 let active_indices = cx.update_editor(|editor, window, cx| {
19666 let snapshot = editor.snapshot(window, cx).display_snapshot;
19667 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19668 });
19669
19670 assert_eq!(
19671 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19672 expected,
19673 "Active indent guide indices do not match"
19674 );
19675 }
19676
19677 assert_eq!(indent_guides, expected, "Indent guides do not match");
19678}
19679
19680fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19681 IndentGuide {
19682 buffer_id,
19683 start_row: MultiBufferRow(start_row),
19684 end_row: MultiBufferRow(end_row),
19685 depth,
19686 tab_size: 4,
19687 settings: IndentGuideSettings {
19688 enabled: true,
19689 line_width: 1,
19690 active_line_width: 1,
19691 ..Default::default()
19692 },
19693 }
19694}
19695
19696#[gpui::test]
19697async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19698 let (buffer_id, mut cx) = setup_indent_guides_editor(
19699 &"
19700 fn main() {
19701 let a = 1;
19702 }"
19703 .unindent(),
19704 cx,
19705 )
19706 .await;
19707
19708 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19709}
19710
19711#[gpui::test]
19712async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19713 let (buffer_id, mut cx) = setup_indent_guides_editor(
19714 &"
19715 fn main() {
19716 let a = 1;
19717 let b = 2;
19718 }"
19719 .unindent(),
19720 cx,
19721 )
19722 .await;
19723
19724 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
19725}
19726
19727#[gpui::test]
19728async fn test_indent_guide_nested(cx: &mut TestAppContext) {
19729 let (buffer_id, mut cx) = setup_indent_guides_editor(
19730 &"
19731 fn main() {
19732 let a = 1;
19733 if a == 3 {
19734 let b = 2;
19735 } else {
19736 let c = 3;
19737 }
19738 }"
19739 .unindent(),
19740 cx,
19741 )
19742 .await;
19743
19744 assert_indent_guides(
19745 0..8,
19746 vec![
19747 indent_guide(buffer_id, 1, 6, 0),
19748 indent_guide(buffer_id, 3, 3, 1),
19749 indent_guide(buffer_id, 5, 5, 1),
19750 ],
19751 None,
19752 &mut cx,
19753 );
19754}
19755
19756#[gpui::test]
19757async fn test_indent_guide_tab(cx: &mut TestAppContext) {
19758 let (buffer_id, mut cx) = setup_indent_guides_editor(
19759 &"
19760 fn main() {
19761 let a = 1;
19762 let b = 2;
19763 let c = 3;
19764 }"
19765 .unindent(),
19766 cx,
19767 )
19768 .await;
19769
19770 assert_indent_guides(
19771 0..5,
19772 vec![
19773 indent_guide(buffer_id, 1, 3, 0),
19774 indent_guide(buffer_id, 2, 2, 1),
19775 ],
19776 None,
19777 &mut cx,
19778 );
19779}
19780
19781#[gpui::test]
19782async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
19783 let (buffer_id, mut cx) = setup_indent_guides_editor(
19784 &"
19785 fn main() {
19786 let a = 1;
19787
19788 let c = 3;
19789 }"
19790 .unindent(),
19791 cx,
19792 )
19793 .await;
19794
19795 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
19796}
19797
19798#[gpui::test]
19799async fn test_indent_guide_complex(cx: &mut TestAppContext) {
19800 let (buffer_id, mut cx) = setup_indent_guides_editor(
19801 &"
19802 fn main() {
19803 let a = 1;
19804
19805 let c = 3;
19806
19807 if a == 3 {
19808 let b = 2;
19809 } else {
19810 let c = 3;
19811 }
19812 }"
19813 .unindent(),
19814 cx,
19815 )
19816 .await;
19817
19818 assert_indent_guides(
19819 0..11,
19820 vec![
19821 indent_guide(buffer_id, 1, 9, 0),
19822 indent_guide(buffer_id, 6, 6, 1),
19823 indent_guide(buffer_id, 8, 8, 1),
19824 ],
19825 None,
19826 &mut cx,
19827 );
19828}
19829
19830#[gpui::test]
19831async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
19832 let (buffer_id, mut cx) = setup_indent_guides_editor(
19833 &"
19834 fn main() {
19835 let a = 1;
19836
19837 let c = 3;
19838
19839 if a == 3 {
19840 let b = 2;
19841 } else {
19842 let c = 3;
19843 }
19844 }"
19845 .unindent(),
19846 cx,
19847 )
19848 .await;
19849
19850 assert_indent_guides(
19851 1..11,
19852 vec![
19853 indent_guide(buffer_id, 1, 9, 0),
19854 indent_guide(buffer_id, 6, 6, 1),
19855 indent_guide(buffer_id, 8, 8, 1),
19856 ],
19857 None,
19858 &mut cx,
19859 );
19860}
19861
19862#[gpui::test]
19863async fn test_indent_guide_ends_off_screen(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 1..10,
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_with_folds(cx: &mut TestAppContext) {
19896 let (buffer_id, mut cx) = setup_indent_guides_editor(
19897 &"
19898 fn main() {
19899 if a {
19900 b(
19901 c,
19902 d,
19903 )
19904 } else {
19905 e(
19906 f
19907 )
19908 }
19909 }"
19910 .unindent(),
19911 cx,
19912 )
19913 .await;
19914
19915 assert_indent_guides(
19916 0..11,
19917 vec![
19918 indent_guide(buffer_id, 1, 10, 0),
19919 indent_guide(buffer_id, 2, 5, 1),
19920 indent_guide(buffer_id, 7, 9, 1),
19921 indent_guide(buffer_id, 3, 4, 2),
19922 indent_guide(buffer_id, 8, 8, 2),
19923 ],
19924 None,
19925 &mut cx,
19926 );
19927
19928 cx.update_editor(|editor, window, cx| {
19929 editor.fold_at(MultiBufferRow(2), window, cx);
19930 assert_eq!(
19931 editor.display_text(cx),
19932 "
19933 fn main() {
19934 if a {
19935 b(⋯
19936 )
19937 } else {
19938 e(
19939 f
19940 )
19941 }
19942 }"
19943 .unindent()
19944 );
19945 });
19946
19947 assert_indent_guides(
19948 0..11,
19949 vec![
19950 indent_guide(buffer_id, 1, 10, 0),
19951 indent_guide(buffer_id, 2, 5, 1),
19952 indent_guide(buffer_id, 7, 9, 1),
19953 indent_guide(buffer_id, 8, 8, 2),
19954 ],
19955 None,
19956 &mut cx,
19957 );
19958}
19959
19960#[gpui::test]
19961async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19962 let (buffer_id, mut cx) = setup_indent_guides_editor(
19963 &"
19964 block1
19965 block2
19966 block3
19967 block4
19968 block2
19969 block1
19970 block1"
19971 .unindent(),
19972 cx,
19973 )
19974 .await;
19975
19976 assert_indent_guides(
19977 1..10,
19978 vec![
19979 indent_guide(buffer_id, 1, 4, 0),
19980 indent_guide(buffer_id, 2, 3, 1),
19981 indent_guide(buffer_id, 3, 3, 2),
19982 ],
19983 None,
19984 &mut cx,
19985 );
19986}
19987
19988#[gpui::test]
19989async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19990 let (buffer_id, mut cx) = setup_indent_guides_editor(
19991 &"
19992 block1
19993 block2
19994 block3
19995
19996 block1
19997 block1"
19998 .unindent(),
19999 cx,
20000 )
20001 .await;
20002
20003 assert_indent_guides(
20004 0..6,
20005 vec![
20006 indent_guide(buffer_id, 1, 2, 0),
20007 indent_guide(buffer_id, 2, 2, 1),
20008 ],
20009 None,
20010 &mut cx,
20011 );
20012}
20013
20014#[gpui::test]
20015async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20016 let (buffer_id, mut cx) = setup_indent_guides_editor(
20017 &"
20018 function component() {
20019 \treturn (
20020 \t\t\t
20021 \t\t<div>
20022 \t\t\t<abc></abc>
20023 \t\t</div>
20024 \t)
20025 }"
20026 .unindent(),
20027 cx,
20028 )
20029 .await;
20030
20031 assert_indent_guides(
20032 0..8,
20033 vec![
20034 indent_guide(buffer_id, 1, 6, 0),
20035 indent_guide(buffer_id, 2, 5, 1),
20036 indent_guide(buffer_id, 4, 4, 2),
20037 ],
20038 None,
20039 &mut cx,
20040 );
20041}
20042
20043#[gpui::test]
20044async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20045 let (buffer_id, mut cx) = setup_indent_guides_editor(
20046 &"
20047 function component() {
20048 \treturn (
20049 \t
20050 \t\t<div>
20051 \t\t\t<abc></abc>
20052 \t\t</div>
20053 \t)
20054 }"
20055 .unindent(),
20056 cx,
20057 )
20058 .await;
20059
20060 assert_indent_guides(
20061 0..8,
20062 vec![
20063 indent_guide(buffer_id, 1, 6, 0),
20064 indent_guide(buffer_id, 2, 5, 1),
20065 indent_guide(buffer_id, 4, 4, 2),
20066 ],
20067 None,
20068 &mut cx,
20069 );
20070}
20071
20072#[gpui::test]
20073async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20074 let (buffer_id, mut cx) = setup_indent_guides_editor(
20075 &"
20076 block1
20077
20078
20079
20080 block2
20081 "
20082 .unindent(),
20083 cx,
20084 )
20085 .await;
20086
20087 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20088}
20089
20090#[gpui::test]
20091async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20092 let (buffer_id, mut cx) = setup_indent_guides_editor(
20093 &"
20094 def a:
20095 \tb = 3
20096 \tif True:
20097 \t\tc = 4
20098 \t\td = 5
20099 \tprint(b)
20100 "
20101 .unindent(),
20102 cx,
20103 )
20104 .await;
20105
20106 assert_indent_guides(
20107 0..6,
20108 vec![
20109 indent_guide(buffer_id, 1, 5, 0),
20110 indent_guide(buffer_id, 3, 4, 1),
20111 ],
20112 None,
20113 &mut cx,
20114 );
20115}
20116
20117#[gpui::test]
20118async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20119 let (buffer_id, mut cx) = setup_indent_guides_editor(
20120 &"
20121 fn main() {
20122 let a = 1;
20123 }"
20124 .unindent(),
20125 cx,
20126 )
20127 .await;
20128
20129 cx.update_editor(|editor, window, cx| {
20130 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20131 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20132 });
20133 });
20134
20135 assert_indent_guides(
20136 0..3,
20137 vec![indent_guide(buffer_id, 1, 1, 0)],
20138 Some(vec![0]),
20139 &mut cx,
20140 );
20141}
20142
20143#[gpui::test]
20144async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20145 let (buffer_id, mut cx) = setup_indent_guides_editor(
20146 &"
20147 fn main() {
20148 if 1 == 2 {
20149 let a = 1;
20150 }
20151 }"
20152 .unindent(),
20153 cx,
20154 )
20155 .await;
20156
20157 cx.update_editor(|editor, window, cx| {
20158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20159 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20160 });
20161 });
20162
20163 assert_indent_guides(
20164 0..4,
20165 vec![
20166 indent_guide(buffer_id, 1, 3, 0),
20167 indent_guide(buffer_id, 2, 2, 1),
20168 ],
20169 Some(vec![1]),
20170 &mut cx,
20171 );
20172
20173 cx.update_editor(|editor, window, cx| {
20174 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20175 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20176 });
20177 });
20178
20179 assert_indent_guides(
20180 0..4,
20181 vec![
20182 indent_guide(buffer_id, 1, 3, 0),
20183 indent_guide(buffer_id, 2, 2, 1),
20184 ],
20185 Some(vec![1]),
20186 &mut cx,
20187 );
20188
20189 cx.update_editor(|editor, window, cx| {
20190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20191 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20192 });
20193 });
20194
20195 assert_indent_guides(
20196 0..4,
20197 vec![
20198 indent_guide(buffer_id, 1, 3, 0),
20199 indent_guide(buffer_id, 2, 2, 1),
20200 ],
20201 Some(vec![0]),
20202 &mut cx,
20203 );
20204}
20205
20206#[gpui::test]
20207async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20208 let (buffer_id, mut cx) = setup_indent_guides_editor(
20209 &"
20210 fn main() {
20211 let a = 1;
20212
20213 let b = 2;
20214 }"
20215 .unindent(),
20216 cx,
20217 )
20218 .await;
20219
20220 cx.update_editor(|editor, window, cx| {
20221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20222 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20223 });
20224 });
20225
20226 assert_indent_guides(
20227 0..5,
20228 vec![indent_guide(buffer_id, 1, 3, 0)],
20229 Some(vec![0]),
20230 &mut cx,
20231 );
20232}
20233
20234#[gpui::test]
20235async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20236 let (buffer_id, mut cx) = setup_indent_guides_editor(
20237 &"
20238 def m:
20239 a = 1
20240 pass"
20241 .unindent(),
20242 cx,
20243 )
20244 .await;
20245
20246 cx.update_editor(|editor, window, cx| {
20247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20248 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20249 });
20250 });
20251
20252 assert_indent_guides(
20253 0..3,
20254 vec![indent_guide(buffer_id, 1, 2, 0)],
20255 Some(vec![0]),
20256 &mut cx,
20257 );
20258}
20259
20260#[gpui::test]
20261async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20262 init_test(cx, |_| {});
20263 let mut cx = EditorTestContext::new(cx).await;
20264 let text = indoc! {
20265 "
20266 impl A {
20267 fn b() {
20268 0;
20269 3;
20270 5;
20271 6;
20272 7;
20273 }
20274 }
20275 "
20276 };
20277 let base_text = indoc! {
20278 "
20279 impl A {
20280 fn b() {
20281 0;
20282 1;
20283 2;
20284 3;
20285 4;
20286 }
20287 fn c() {
20288 5;
20289 6;
20290 7;
20291 }
20292 }
20293 "
20294 };
20295
20296 cx.update_editor(|editor, window, cx| {
20297 editor.set_text(text, window, cx);
20298
20299 editor.buffer().update(cx, |multibuffer, cx| {
20300 let buffer = multibuffer.as_singleton().unwrap();
20301 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20302
20303 multibuffer.set_all_diff_hunks_expanded(cx);
20304 multibuffer.add_diff(diff, cx);
20305
20306 buffer.read(cx).remote_id()
20307 })
20308 });
20309 cx.run_until_parked();
20310
20311 cx.assert_state_with_diff(
20312 indoc! { "
20313 impl A {
20314 fn b() {
20315 0;
20316 - 1;
20317 - 2;
20318 3;
20319 - 4;
20320 - }
20321 - fn c() {
20322 5;
20323 6;
20324 7;
20325 }
20326 }
20327 ˇ"
20328 }
20329 .to_string(),
20330 );
20331
20332 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20333 editor
20334 .snapshot(window, cx)
20335 .buffer_snapshot
20336 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20337 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20338 .collect::<Vec<_>>()
20339 });
20340 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20341 assert_eq!(
20342 actual_guides,
20343 vec![
20344 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20345 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20346 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20347 ]
20348 );
20349}
20350
20351#[gpui::test]
20352async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20353 init_test(cx, |_| {});
20354 let mut cx = EditorTestContext::new(cx).await;
20355
20356 let diff_base = r#"
20357 a
20358 b
20359 c
20360 "#
20361 .unindent();
20362
20363 cx.set_state(
20364 &r#"
20365 ˇA
20366 b
20367 C
20368 "#
20369 .unindent(),
20370 );
20371 cx.set_head_text(&diff_base);
20372 cx.update_editor(|editor, window, cx| {
20373 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20374 });
20375 executor.run_until_parked();
20376
20377 let both_hunks_expanded = r#"
20378 - a
20379 + ˇA
20380 b
20381 - c
20382 + C
20383 "#
20384 .unindent();
20385
20386 cx.assert_state_with_diff(both_hunks_expanded.clone());
20387
20388 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20389 let snapshot = editor.snapshot(window, cx);
20390 let hunks = editor
20391 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20392 .collect::<Vec<_>>();
20393 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20394 let buffer_id = hunks[0].buffer_id;
20395 hunks
20396 .into_iter()
20397 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20398 .collect::<Vec<_>>()
20399 });
20400 assert_eq!(hunk_ranges.len(), 2);
20401
20402 cx.update_editor(|editor, _, cx| {
20403 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20404 });
20405 executor.run_until_parked();
20406
20407 let second_hunk_expanded = r#"
20408 ˇA
20409 b
20410 - c
20411 + C
20412 "#
20413 .unindent();
20414
20415 cx.assert_state_with_diff(second_hunk_expanded);
20416
20417 cx.update_editor(|editor, _, cx| {
20418 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20419 });
20420 executor.run_until_parked();
20421
20422 cx.assert_state_with_diff(both_hunks_expanded.clone());
20423
20424 cx.update_editor(|editor, _, cx| {
20425 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20426 });
20427 executor.run_until_parked();
20428
20429 let first_hunk_expanded = r#"
20430 - a
20431 + ˇA
20432 b
20433 C
20434 "#
20435 .unindent();
20436
20437 cx.assert_state_with_diff(first_hunk_expanded);
20438
20439 cx.update_editor(|editor, _, cx| {
20440 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20441 });
20442 executor.run_until_parked();
20443
20444 cx.assert_state_with_diff(both_hunks_expanded);
20445
20446 cx.set_state(
20447 &r#"
20448 ˇA
20449 b
20450 "#
20451 .unindent(),
20452 );
20453 cx.run_until_parked();
20454
20455 // TODO this cursor position seems bad
20456 cx.assert_state_with_diff(
20457 r#"
20458 - ˇa
20459 + A
20460 b
20461 "#
20462 .unindent(),
20463 );
20464
20465 cx.update_editor(|editor, window, cx| {
20466 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20467 });
20468
20469 cx.assert_state_with_diff(
20470 r#"
20471 - ˇa
20472 + A
20473 b
20474 - c
20475 "#
20476 .unindent(),
20477 );
20478
20479 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20480 let snapshot = editor.snapshot(window, cx);
20481 let hunks = editor
20482 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20483 .collect::<Vec<_>>();
20484 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20485 let buffer_id = hunks[0].buffer_id;
20486 hunks
20487 .into_iter()
20488 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20489 .collect::<Vec<_>>()
20490 });
20491 assert_eq!(hunk_ranges.len(), 2);
20492
20493 cx.update_editor(|editor, _, cx| {
20494 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20495 });
20496 executor.run_until_parked();
20497
20498 cx.assert_state_with_diff(
20499 r#"
20500 - ˇa
20501 + A
20502 b
20503 "#
20504 .unindent(),
20505 );
20506}
20507
20508#[gpui::test]
20509async fn test_toggle_deletion_hunk_at_start_of_file(
20510 executor: BackgroundExecutor,
20511 cx: &mut TestAppContext,
20512) {
20513 init_test(cx, |_| {});
20514 let mut cx = EditorTestContext::new(cx).await;
20515
20516 let diff_base = r#"
20517 a
20518 b
20519 c
20520 "#
20521 .unindent();
20522
20523 cx.set_state(
20524 &r#"
20525 ˇb
20526 c
20527 "#
20528 .unindent(),
20529 );
20530 cx.set_head_text(&diff_base);
20531 cx.update_editor(|editor, window, cx| {
20532 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20533 });
20534 executor.run_until_parked();
20535
20536 let hunk_expanded = r#"
20537 - a
20538 ˇb
20539 c
20540 "#
20541 .unindent();
20542
20543 cx.assert_state_with_diff(hunk_expanded.clone());
20544
20545 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20546 let snapshot = editor.snapshot(window, cx);
20547 let hunks = editor
20548 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20549 .collect::<Vec<_>>();
20550 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20551 let buffer_id = hunks[0].buffer_id;
20552 hunks
20553 .into_iter()
20554 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20555 .collect::<Vec<_>>()
20556 });
20557 assert_eq!(hunk_ranges.len(), 1);
20558
20559 cx.update_editor(|editor, _, cx| {
20560 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20561 });
20562 executor.run_until_parked();
20563
20564 let hunk_collapsed = r#"
20565 ˇb
20566 c
20567 "#
20568 .unindent();
20569
20570 cx.assert_state_with_diff(hunk_collapsed);
20571
20572 cx.update_editor(|editor, _, cx| {
20573 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20574 });
20575 executor.run_until_parked();
20576
20577 cx.assert_state_with_diff(hunk_expanded);
20578}
20579
20580#[gpui::test]
20581async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20582 init_test(cx, |_| {});
20583
20584 let fs = FakeFs::new(cx.executor());
20585 fs.insert_tree(
20586 path!("/test"),
20587 json!({
20588 ".git": {},
20589 "file-1": "ONE\n",
20590 "file-2": "TWO\n",
20591 "file-3": "THREE\n",
20592 }),
20593 )
20594 .await;
20595
20596 fs.set_head_for_repo(
20597 path!("/test/.git").as_ref(),
20598 &[
20599 ("file-1".into(), "one\n".into()),
20600 ("file-2".into(), "two\n".into()),
20601 ("file-3".into(), "three\n".into()),
20602 ],
20603 "deadbeef",
20604 );
20605
20606 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20607 let mut buffers = vec![];
20608 for i in 1..=3 {
20609 let buffer = project
20610 .update(cx, |project, cx| {
20611 let path = format!(path!("/test/file-{}"), i);
20612 project.open_local_buffer(path, cx)
20613 })
20614 .await
20615 .unwrap();
20616 buffers.push(buffer);
20617 }
20618
20619 let multibuffer = cx.new(|cx| {
20620 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20621 multibuffer.set_all_diff_hunks_expanded(cx);
20622 for buffer in &buffers {
20623 let snapshot = buffer.read(cx).snapshot();
20624 multibuffer.set_excerpts_for_path(
20625 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20626 buffer.clone(),
20627 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20628 2,
20629 cx,
20630 );
20631 }
20632 multibuffer
20633 });
20634
20635 let editor = cx.add_window(|window, cx| {
20636 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20637 });
20638 cx.run_until_parked();
20639
20640 let snapshot = editor
20641 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20642 .unwrap();
20643 let hunks = snapshot
20644 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20645 .map(|hunk| match hunk {
20646 DisplayDiffHunk::Unfolded {
20647 display_row_range, ..
20648 } => display_row_range,
20649 DisplayDiffHunk::Folded { .. } => unreachable!(),
20650 })
20651 .collect::<Vec<_>>();
20652 assert_eq!(
20653 hunks,
20654 [
20655 DisplayRow(2)..DisplayRow(4),
20656 DisplayRow(7)..DisplayRow(9),
20657 DisplayRow(12)..DisplayRow(14),
20658 ]
20659 );
20660}
20661
20662#[gpui::test]
20663async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20664 init_test(cx, |_| {});
20665
20666 let mut cx = EditorTestContext::new(cx).await;
20667 cx.set_head_text(indoc! { "
20668 one
20669 two
20670 three
20671 four
20672 five
20673 "
20674 });
20675 cx.set_index_text(indoc! { "
20676 one
20677 two
20678 three
20679 four
20680 five
20681 "
20682 });
20683 cx.set_state(indoc! {"
20684 one
20685 TWO
20686 ˇTHREE
20687 FOUR
20688 five
20689 "});
20690 cx.run_until_parked();
20691 cx.update_editor(|editor, window, cx| {
20692 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20693 });
20694 cx.run_until_parked();
20695 cx.assert_index_text(Some(indoc! {"
20696 one
20697 TWO
20698 THREE
20699 FOUR
20700 five
20701 "}));
20702 cx.set_state(indoc! { "
20703 one
20704 TWO
20705 ˇTHREE-HUNDRED
20706 FOUR
20707 five
20708 "});
20709 cx.run_until_parked();
20710 cx.update_editor(|editor, window, cx| {
20711 let snapshot = editor.snapshot(window, cx);
20712 let hunks = editor
20713 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20714 .collect::<Vec<_>>();
20715 assert_eq!(hunks.len(), 1);
20716 assert_eq!(
20717 hunks[0].status(),
20718 DiffHunkStatus {
20719 kind: DiffHunkStatusKind::Modified,
20720 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
20721 }
20722 );
20723
20724 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20725 });
20726 cx.run_until_parked();
20727 cx.assert_index_text(Some(indoc! {"
20728 one
20729 TWO
20730 THREE-HUNDRED
20731 FOUR
20732 five
20733 "}));
20734}
20735
20736#[gpui::test]
20737fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
20738 init_test(cx, |_| {});
20739
20740 let editor = cx.add_window(|window, cx| {
20741 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
20742 build_editor(buffer, window, cx)
20743 });
20744
20745 let render_args = Arc::new(Mutex::new(None));
20746 let snapshot = editor
20747 .update(cx, |editor, window, cx| {
20748 let snapshot = editor.buffer().read(cx).snapshot(cx);
20749 let range =
20750 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
20751
20752 struct RenderArgs {
20753 row: MultiBufferRow,
20754 folded: bool,
20755 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
20756 }
20757
20758 let crease = Crease::inline(
20759 range,
20760 FoldPlaceholder::test(),
20761 {
20762 let toggle_callback = render_args.clone();
20763 move |row, folded, callback, _window, _cx| {
20764 *toggle_callback.lock() = Some(RenderArgs {
20765 row,
20766 folded,
20767 callback,
20768 });
20769 div()
20770 }
20771 },
20772 |_row, _folded, _window, _cx| div(),
20773 );
20774
20775 editor.insert_creases(Some(crease), cx);
20776 let snapshot = editor.snapshot(window, cx);
20777 let _div =
20778 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
20779 snapshot
20780 })
20781 .unwrap();
20782
20783 let render_args = render_args.lock().take().unwrap();
20784 assert_eq!(render_args.row, MultiBufferRow(1));
20785 assert!(!render_args.folded);
20786 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20787
20788 cx.update_window(*editor, |_, window, cx| {
20789 (render_args.callback)(true, window, cx)
20790 })
20791 .unwrap();
20792 let snapshot = editor
20793 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20794 .unwrap();
20795 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
20796
20797 cx.update_window(*editor, |_, window, cx| {
20798 (render_args.callback)(false, window, cx)
20799 })
20800 .unwrap();
20801 let snapshot = editor
20802 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20803 .unwrap();
20804 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20805}
20806
20807#[gpui::test]
20808async fn test_input_text(cx: &mut TestAppContext) {
20809 init_test(cx, |_| {});
20810 let mut cx = EditorTestContext::new(cx).await;
20811
20812 cx.set_state(
20813 &r#"ˇone
20814 two
20815
20816 three
20817 fourˇ
20818 five
20819
20820 siˇx"#
20821 .unindent(),
20822 );
20823
20824 cx.dispatch_action(HandleInput(String::new()));
20825 cx.assert_editor_state(
20826 &r#"ˇone
20827 two
20828
20829 three
20830 fourˇ
20831 five
20832
20833 siˇx"#
20834 .unindent(),
20835 );
20836
20837 cx.dispatch_action(HandleInput("AAAA".to_string()));
20838 cx.assert_editor_state(
20839 &r#"AAAAˇone
20840 two
20841
20842 three
20843 fourAAAAˇ
20844 five
20845
20846 siAAAAˇx"#
20847 .unindent(),
20848 );
20849}
20850
20851#[gpui::test]
20852async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
20853 init_test(cx, |_| {});
20854
20855 let mut cx = EditorTestContext::new(cx).await;
20856 cx.set_state(
20857 r#"let foo = 1;
20858let foo = 2;
20859let foo = 3;
20860let fooˇ = 4;
20861let foo = 5;
20862let foo = 6;
20863let foo = 7;
20864let foo = 8;
20865let foo = 9;
20866let foo = 10;
20867let foo = 11;
20868let foo = 12;
20869let foo = 13;
20870let foo = 14;
20871let foo = 15;"#,
20872 );
20873
20874 cx.update_editor(|e, window, cx| {
20875 assert_eq!(
20876 e.next_scroll_position,
20877 NextScrollCursorCenterTopBottom::Center,
20878 "Default next scroll direction is center",
20879 );
20880
20881 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20882 assert_eq!(
20883 e.next_scroll_position,
20884 NextScrollCursorCenterTopBottom::Top,
20885 "After center, next scroll direction should be top",
20886 );
20887
20888 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20889 assert_eq!(
20890 e.next_scroll_position,
20891 NextScrollCursorCenterTopBottom::Bottom,
20892 "After top, next scroll direction should be bottom",
20893 );
20894
20895 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20896 assert_eq!(
20897 e.next_scroll_position,
20898 NextScrollCursorCenterTopBottom::Center,
20899 "After bottom, scrolling should start over",
20900 );
20901
20902 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20903 assert_eq!(
20904 e.next_scroll_position,
20905 NextScrollCursorCenterTopBottom::Top,
20906 "Scrolling continues if retriggered fast enough"
20907 );
20908 });
20909
20910 cx.executor()
20911 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
20912 cx.executor().run_until_parked();
20913 cx.update_editor(|e, _, _| {
20914 assert_eq!(
20915 e.next_scroll_position,
20916 NextScrollCursorCenterTopBottom::Center,
20917 "If scrolling is not triggered fast enough, it should reset"
20918 );
20919 });
20920}
20921
20922#[gpui::test]
20923async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
20924 init_test(cx, |_| {});
20925 let mut cx = EditorLspTestContext::new_rust(
20926 lsp::ServerCapabilities {
20927 definition_provider: Some(lsp::OneOf::Left(true)),
20928 references_provider: Some(lsp::OneOf::Left(true)),
20929 ..lsp::ServerCapabilities::default()
20930 },
20931 cx,
20932 )
20933 .await;
20934
20935 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
20936 let go_to_definition = cx
20937 .lsp
20938 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20939 move |params, _| async move {
20940 if empty_go_to_definition {
20941 Ok(None)
20942 } else {
20943 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20944 uri: params.text_document_position_params.text_document.uri,
20945 range: lsp::Range::new(
20946 lsp::Position::new(4, 3),
20947 lsp::Position::new(4, 6),
20948 ),
20949 })))
20950 }
20951 },
20952 );
20953 let references = cx
20954 .lsp
20955 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20956 Ok(Some(vec![lsp::Location {
20957 uri: params.text_document_position.text_document.uri,
20958 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20959 }]))
20960 });
20961 (go_to_definition, references)
20962 };
20963
20964 cx.set_state(
20965 &r#"fn one() {
20966 let mut a = ˇtwo();
20967 }
20968
20969 fn two() {}"#
20970 .unindent(),
20971 );
20972 set_up_lsp_handlers(false, &mut cx);
20973 let navigated = cx
20974 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20975 .await
20976 .expect("Failed to navigate to definition");
20977 assert_eq!(
20978 navigated,
20979 Navigated::Yes,
20980 "Should have navigated to definition from the GetDefinition response"
20981 );
20982 cx.assert_editor_state(
20983 &r#"fn one() {
20984 let mut a = two();
20985 }
20986
20987 fn «twoˇ»() {}"#
20988 .unindent(),
20989 );
20990
20991 let editors = cx.update_workspace(|workspace, _, cx| {
20992 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20993 });
20994 cx.update_editor(|_, _, test_editor_cx| {
20995 assert_eq!(
20996 editors.len(),
20997 1,
20998 "Initially, only one, test, editor should be open in the workspace"
20999 );
21000 assert_eq!(
21001 test_editor_cx.entity(),
21002 editors.last().expect("Asserted len is 1").clone()
21003 );
21004 });
21005
21006 set_up_lsp_handlers(true, &mut cx);
21007 let navigated = cx
21008 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21009 .await
21010 .expect("Failed to navigate to lookup references");
21011 assert_eq!(
21012 navigated,
21013 Navigated::Yes,
21014 "Should have navigated to references as a fallback after empty GoToDefinition response"
21015 );
21016 // We should not change the selections in the existing file,
21017 // if opening another milti buffer with the references
21018 cx.assert_editor_state(
21019 &r#"fn one() {
21020 let mut a = two();
21021 }
21022
21023 fn «twoˇ»() {}"#
21024 .unindent(),
21025 );
21026 let editors = cx.update_workspace(|workspace, _, cx| {
21027 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21028 });
21029 cx.update_editor(|_, _, test_editor_cx| {
21030 assert_eq!(
21031 editors.len(),
21032 2,
21033 "After falling back to references search, we open a new editor with the results"
21034 );
21035 let references_fallback_text = editors
21036 .into_iter()
21037 .find(|new_editor| *new_editor != test_editor_cx.entity())
21038 .expect("Should have one non-test editor now")
21039 .read(test_editor_cx)
21040 .text(test_editor_cx);
21041 assert_eq!(
21042 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21043 "Should use the range from the references response and not the GoToDefinition one"
21044 );
21045 });
21046}
21047
21048#[gpui::test]
21049async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21050 init_test(cx, |_| {});
21051 cx.update(|cx| {
21052 let mut editor_settings = EditorSettings::get_global(cx).clone();
21053 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21054 EditorSettings::override_global(editor_settings, cx);
21055 });
21056 let mut cx = EditorLspTestContext::new_rust(
21057 lsp::ServerCapabilities {
21058 definition_provider: Some(lsp::OneOf::Left(true)),
21059 references_provider: Some(lsp::OneOf::Left(true)),
21060 ..lsp::ServerCapabilities::default()
21061 },
21062 cx,
21063 )
21064 .await;
21065 let original_state = r#"fn one() {
21066 let mut a = ˇtwo();
21067 }
21068
21069 fn two() {}"#
21070 .unindent();
21071 cx.set_state(&original_state);
21072
21073 let mut go_to_definition = cx
21074 .lsp
21075 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21076 move |_, _| async move { Ok(None) },
21077 );
21078 let _references = cx
21079 .lsp
21080 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21081 panic!("Should not call for references with no go to definition fallback")
21082 });
21083
21084 let navigated = cx
21085 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21086 .await
21087 .expect("Failed to navigate to lookup references");
21088 go_to_definition
21089 .next()
21090 .await
21091 .expect("Should have called the go_to_definition handler");
21092
21093 assert_eq!(
21094 navigated,
21095 Navigated::No,
21096 "Should have navigated to references as a fallback after empty GoToDefinition response"
21097 );
21098 cx.assert_editor_state(&original_state);
21099 let editors = cx.update_workspace(|workspace, _, cx| {
21100 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21101 });
21102 cx.update_editor(|_, _, _| {
21103 assert_eq!(
21104 editors.len(),
21105 1,
21106 "After unsuccessful fallback, no other editor should have been opened"
21107 );
21108 });
21109}
21110
21111#[gpui::test]
21112async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21113 init_test(cx, |_| {});
21114
21115 let language = Arc::new(Language::new(
21116 LanguageConfig::default(),
21117 Some(tree_sitter_rust::LANGUAGE.into()),
21118 ));
21119
21120 let text = r#"
21121 #[cfg(test)]
21122 mod tests() {
21123 #[test]
21124 fn runnable_1() {
21125 let a = 1;
21126 }
21127
21128 #[test]
21129 fn runnable_2() {
21130 let a = 1;
21131 let b = 2;
21132 }
21133 }
21134 "#
21135 .unindent();
21136
21137 let fs = FakeFs::new(cx.executor());
21138 fs.insert_file("/file.rs", Default::default()).await;
21139
21140 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21141 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21142 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21143 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21144 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21145
21146 let editor = cx.new_window_entity(|window, cx| {
21147 Editor::new(
21148 EditorMode::full(),
21149 multi_buffer,
21150 Some(project.clone()),
21151 window,
21152 cx,
21153 )
21154 });
21155
21156 editor.update_in(cx, |editor, window, cx| {
21157 let snapshot = editor.buffer().read(cx).snapshot(cx);
21158 editor.tasks.insert(
21159 (buffer.read(cx).remote_id(), 3),
21160 RunnableTasks {
21161 templates: vec![],
21162 offset: snapshot.anchor_before(43),
21163 column: 0,
21164 extra_variables: HashMap::default(),
21165 context_range: BufferOffset(43)..BufferOffset(85),
21166 },
21167 );
21168 editor.tasks.insert(
21169 (buffer.read(cx).remote_id(), 8),
21170 RunnableTasks {
21171 templates: vec![],
21172 offset: snapshot.anchor_before(86),
21173 column: 0,
21174 extra_variables: HashMap::default(),
21175 context_range: BufferOffset(86)..BufferOffset(191),
21176 },
21177 );
21178
21179 // Test finding task when cursor is inside function body
21180 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21181 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21182 });
21183 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21184 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21185
21186 // Test finding task when cursor is on function name
21187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21188 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21189 });
21190 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21191 assert_eq!(row, 8, "Should find task when cursor is on function name");
21192 });
21193}
21194
21195#[gpui::test]
21196async fn test_folding_buffers(cx: &mut TestAppContext) {
21197 init_test(cx, |_| {});
21198
21199 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21200 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21201 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21202
21203 let fs = FakeFs::new(cx.executor());
21204 fs.insert_tree(
21205 path!("/a"),
21206 json!({
21207 "first.rs": sample_text_1,
21208 "second.rs": sample_text_2,
21209 "third.rs": sample_text_3,
21210 }),
21211 )
21212 .await;
21213 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21214 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21215 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21216 let worktree = project.update(cx, |project, cx| {
21217 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21218 assert_eq!(worktrees.len(), 1);
21219 worktrees.pop().unwrap()
21220 });
21221 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21222
21223 let buffer_1 = project
21224 .update(cx, |project, cx| {
21225 project.open_buffer((worktree_id, "first.rs"), cx)
21226 })
21227 .await
21228 .unwrap();
21229 let buffer_2 = project
21230 .update(cx, |project, cx| {
21231 project.open_buffer((worktree_id, "second.rs"), cx)
21232 })
21233 .await
21234 .unwrap();
21235 let buffer_3 = project
21236 .update(cx, |project, cx| {
21237 project.open_buffer((worktree_id, "third.rs"), cx)
21238 })
21239 .await
21240 .unwrap();
21241
21242 let multi_buffer = cx.new(|cx| {
21243 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21244 multi_buffer.push_excerpts(
21245 buffer_1.clone(),
21246 [
21247 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21248 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21249 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21250 ],
21251 cx,
21252 );
21253 multi_buffer.push_excerpts(
21254 buffer_2.clone(),
21255 [
21256 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21257 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21258 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21259 ],
21260 cx,
21261 );
21262 multi_buffer.push_excerpts(
21263 buffer_3.clone(),
21264 [
21265 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21266 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21267 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21268 ],
21269 cx,
21270 );
21271 multi_buffer
21272 });
21273 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21274 Editor::new(
21275 EditorMode::full(),
21276 multi_buffer.clone(),
21277 Some(project.clone()),
21278 window,
21279 cx,
21280 )
21281 });
21282
21283 assert_eq!(
21284 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21285 "\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",
21286 );
21287
21288 multi_buffer_editor.update(cx, |editor, cx| {
21289 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21290 });
21291 assert_eq!(
21292 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21293 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21294 "After folding the first buffer, its text should not be displayed"
21295 );
21296
21297 multi_buffer_editor.update(cx, |editor, cx| {
21298 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21299 });
21300 assert_eq!(
21301 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21302 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21303 "After folding the second buffer, its text should not be displayed"
21304 );
21305
21306 multi_buffer_editor.update(cx, |editor, cx| {
21307 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21308 });
21309 assert_eq!(
21310 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21311 "\n\n\n\n\n",
21312 "After folding the third buffer, its text should not be displayed"
21313 );
21314
21315 // Emulate selection inside the fold logic, that should work
21316 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21317 editor
21318 .snapshot(window, cx)
21319 .next_line_boundary(Point::new(0, 4));
21320 });
21321
21322 multi_buffer_editor.update(cx, |editor, cx| {
21323 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21324 });
21325 assert_eq!(
21326 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21327 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21328 "After unfolding the second buffer, its text should be displayed"
21329 );
21330
21331 // Typing inside of buffer 1 causes that buffer to be unfolded.
21332 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21333 assert_eq!(
21334 multi_buffer
21335 .read(cx)
21336 .snapshot(cx)
21337 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21338 .collect::<String>(),
21339 "bbbb"
21340 );
21341 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21342 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21343 });
21344 editor.handle_input("B", window, cx);
21345 });
21346
21347 assert_eq!(
21348 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21349 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21350 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21351 );
21352
21353 multi_buffer_editor.update(cx, |editor, cx| {
21354 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21355 });
21356 assert_eq!(
21357 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21358 "\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",
21359 "After unfolding the all buffers, all original text should be displayed"
21360 );
21361}
21362
21363#[gpui::test]
21364async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21365 init_test(cx, |_| {});
21366
21367 let sample_text_1 = "1111\n2222\n3333".to_string();
21368 let sample_text_2 = "4444\n5555\n6666".to_string();
21369 let sample_text_3 = "7777\n8888\n9999".to_string();
21370
21371 let fs = FakeFs::new(cx.executor());
21372 fs.insert_tree(
21373 path!("/a"),
21374 json!({
21375 "first.rs": sample_text_1,
21376 "second.rs": sample_text_2,
21377 "third.rs": sample_text_3,
21378 }),
21379 )
21380 .await;
21381 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21382 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21383 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21384 let worktree = project.update(cx, |project, cx| {
21385 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21386 assert_eq!(worktrees.len(), 1);
21387 worktrees.pop().unwrap()
21388 });
21389 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21390
21391 let buffer_1 = project
21392 .update(cx, |project, cx| {
21393 project.open_buffer((worktree_id, "first.rs"), cx)
21394 })
21395 .await
21396 .unwrap();
21397 let buffer_2 = project
21398 .update(cx, |project, cx| {
21399 project.open_buffer((worktree_id, "second.rs"), cx)
21400 })
21401 .await
21402 .unwrap();
21403 let buffer_3 = project
21404 .update(cx, |project, cx| {
21405 project.open_buffer((worktree_id, "third.rs"), cx)
21406 })
21407 .await
21408 .unwrap();
21409
21410 let multi_buffer = cx.new(|cx| {
21411 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21412 multi_buffer.push_excerpts(
21413 buffer_1.clone(),
21414 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21415 cx,
21416 );
21417 multi_buffer.push_excerpts(
21418 buffer_2.clone(),
21419 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21420 cx,
21421 );
21422 multi_buffer.push_excerpts(
21423 buffer_3.clone(),
21424 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21425 cx,
21426 );
21427 multi_buffer
21428 });
21429
21430 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21431 Editor::new(
21432 EditorMode::full(),
21433 multi_buffer,
21434 Some(project.clone()),
21435 window,
21436 cx,
21437 )
21438 });
21439
21440 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21441 assert_eq!(
21442 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21443 full_text,
21444 );
21445
21446 multi_buffer_editor.update(cx, |editor, cx| {
21447 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21448 });
21449 assert_eq!(
21450 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21451 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21452 "After folding the first buffer, its text should not be displayed"
21453 );
21454
21455 multi_buffer_editor.update(cx, |editor, cx| {
21456 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21457 });
21458
21459 assert_eq!(
21460 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21461 "\n\n\n\n\n\n7777\n8888\n9999",
21462 "After folding the second buffer, its text should not be displayed"
21463 );
21464
21465 multi_buffer_editor.update(cx, |editor, cx| {
21466 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21467 });
21468 assert_eq!(
21469 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21470 "\n\n\n\n\n",
21471 "After folding the third buffer, its text should not be displayed"
21472 );
21473
21474 multi_buffer_editor.update(cx, |editor, cx| {
21475 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21476 });
21477 assert_eq!(
21478 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21479 "\n\n\n\n4444\n5555\n6666\n\n",
21480 "After unfolding the second buffer, its text should be displayed"
21481 );
21482
21483 multi_buffer_editor.update(cx, |editor, cx| {
21484 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21485 });
21486 assert_eq!(
21487 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21488 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21489 "After unfolding the first buffer, its text should be displayed"
21490 );
21491
21492 multi_buffer_editor.update(cx, |editor, cx| {
21493 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21494 });
21495 assert_eq!(
21496 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21497 full_text,
21498 "After unfolding all buffers, all original text should be displayed"
21499 );
21500}
21501
21502#[gpui::test]
21503async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21504 init_test(cx, |_| {});
21505
21506 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21507
21508 let fs = FakeFs::new(cx.executor());
21509 fs.insert_tree(
21510 path!("/a"),
21511 json!({
21512 "main.rs": sample_text,
21513 }),
21514 )
21515 .await;
21516 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21517 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21518 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21519 let worktree = project.update(cx, |project, cx| {
21520 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21521 assert_eq!(worktrees.len(), 1);
21522 worktrees.pop().unwrap()
21523 });
21524 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21525
21526 let buffer_1 = project
21527 .update(cx, |project, cx| {
21528 project.open_buffer((worktree_id, "main.rs"), cx)
21529 })
21530 .await
21531 .unwrap();
21532
21533 let multi_buffer = cx.new(|cx| {
21534 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21535 multi_buffer.push_excerpts(
21536 buffer_1.clone(),
21537 [ExcerptRange::new(
21538 Point::new(0, 0)
21539 ..Point::new(
21540 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21541 0,
21542 ),
21543 )],
21544 cx,
21545 );
21546 multi_buffer
21547 });
21548 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21549 Editor::new(
21550 EditorMode::full(),
21551 multi_buffer,
21552 Some(project.clone()),
21553 window,
21554 cx,
21555 )
21556 });
21557
21558 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21559 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21560 enum TestHighlight {}
21561 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21562 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21563 editor.highlight_text::<TestHighlight>(
21564 vec![highlight_range.clone()],
21565 HighlightStyle::color(Hsla::green()),
21566 cx,
21567 );
21568 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21569 s.select_ranges(Some(highlight_range))
21570 });
21571 });
21572
21573 let full_text = format!("\n\n{sample_text}");
21574 assert_eq!(
21575 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21576 full_text,
21577 );
21578}
21579
21580#[gpui::test]
21581async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
21582 init_test(cx, |_| {});
21583 cx.update(|cx| {
21584 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
21585 "keymaps/default-linux.json",
21586 cx,
21587 )
21588 .unwrap();
21589 cx.bind_keys(default_key_bindings);
21590 });
21591
21592 let (editor, cx) = cx.add_window_view(|window, cx| {
21593 let multi_buffer = MultiBuffer::build_multi(
21594 [
21595 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
21596 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
21597 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
21598 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
21599 ],
21600 cx,
21601 );
21602 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
21603
21604 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
21605 // fold all but the second buffer, so that we test navigating between two
21606 // adjacent folded buffers, as well as folded buffers at the start and
21607 // end the multibuffer
21608 editor.fold_buffer(buffer_ids[0], cx);
21609 editor.fold_buffer(buffer_ids[2], cx);
21610 editor.fold_buffer(buffer_ids[3], cx);
21611
21612 editor
21613 });
21614 cx.simulate_resize(size(px(1000.), px(1000.)));
21615
21616 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
21617 cx.assert_excerpts_with_selections(indoc! {"
21618 [EXCERPT]
21619 ˇ[FOLDED]
21620 [EXCERPT]
21621 a1
21622 b1
21623 [EXCERPT]
21624 [FOLDED]
21625 [EXCERPT]
21626 [FOLDED]
21627 "
21628 });
21629 cx.simulate_keystroke("down");
21630 cx.assert_excerpts_with_selections(indoc! {"
21631 [EXCERPT]
21632 [FOLDED]
21633 [EXCERPT]
21634 ˇa1
21635 b1
21636 [EXCERPT]
21637 [FOLDED]
21638 [EXCERPT]
21639 [FOLDED]
21640 "
21641 });
21642 cx.simulate_keystroke("down");
21643 cx.assert_excerpts_with_selections(indoc! {"
21644 [EXCERPT]
21645 [FOLDED]
21646 [EXCERPT]
21647 a1
21648 ˇb1
21649 [EXCERPT]
21650 [FOLDED]
21651 [EXCERPT]
21652 [FOLDED]
21653 "
21654 });
21655 cx.simulate_keystroke("down");
21656 cx.assert_excerpts_with_selections(indoc! {"
21657 [EXCERPT]
21658 [FOLDED]
21659 [EXCERPT]
21660 a1
21661 b1
21662 ˇ[EXCERPT]
21663 [FOLDED]
21664 [EXCERPT]
21665 [FOLDED]
21666 "
21667 });
21668 cx.simulate_keystroke("down");
21669 cx.assert_excerpts_with_selections(indoc! {"
21670 [EXCERPT]
21671 [FOLDED]
21672 [EXCERPT]
21673 a1
21674 b1
21675 [EXCERPT]
21676 ˇ[FOLDED]
21677 [EXCERPT]
21678 [FOLDED]
21679 "
21680 });
21681 for _ in 0..5 {
21682 cx.simulate_keystroke("down");
21683 cx.assert_excerpts_with_selections(indoc! {"
21684 [EXCERPT]
21685 [FOLDED]
21686 [EXCERPT]
21687 a1
21688 b1
21689 [EXCERPT]
21690 [FOLDED]
21691 [EXCERPT]
21692 ˇ[FOLDED]
21693 "
21694 });
21695 }
21696
21697 cx.simulate_keystroke("up");
21698 cx.assert_excerpts_with_selections(indoc! {"
21699 [EXCERPT]
21700 [FOLDED]
21701 [EXCERPT]
21702 a1
21703 b1
21704 [EXCERPT]
21705 ˇ[FOLDED]
21706 [EXCERPT]
21707 [FOLDED]
21708 "
21709 });
21710 cx.simulate_keystroke("up");
21711 cx.assert_excerpts_with_selections(indoc! {"
21712 [EXCERPT]
21713 [FOLDED]
21714 [EXCERPT]
21715 a1
21716 b1
21717 ˇ[EXCERPT]
21718 [FOLDED]
21719 [EXCERPT]
21720 [FOLDED]
21721 "
21722 });
21723 cx.simulate_keystroke("up");
21724 cx.assert_excerpts_with_selections(indoc! {"
21725 [EXCERPT]
21726 [FOLDED]
21727 [EXCERPT]
21728 a1
21729 ˇb1
21730 [EXCERPT]
21731 [FOLDED]
21732 [EXCERPT]
21733 [FOLDED]
21734 "
21735 });
21736 cx.simulate_keystroke("up");
21737 cx.assert_excerpts_with_selections(indoc! {"
21738 [EXCERPT]
21739 [FOLDED]
21740 [EXCERPT]
21741 ˇa1
21742 b1
21743 [EXCERPT]
21744 [FOLDED]
21745 [EXCERPT]
21746 [FOLDED]
21747 "
21748 });
21749 for _ in 0..5 {
21750 cx.simulate_keystroke("up");
21751 cx.assert_excerpts_with_selections(indoc! {"
21752 [EXCERPT]
21753 ˇ[FOLDED]
21754 [EXCERPT]
21755 a1
21756 b1
21757 [EXCERPT]
21758 [FOLDED]
21759 [EXCERPT]
21760 [FOLDED]
21761 "
21762 });
21763 }
21764}
21765
21766#[gpui::test]
21767async fn test_edit_prediction_text(cx: &mut TestAppContext) {
21768 init_test(cx, |_| {});
21769
21770 // Simple insertion
21771 assert_highlighted_edits(
21772 "Hello, world!",
21773 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
21774 true,
21775 cx,
21776 |highlighted_edits, cx| {
21777 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
21778 assert_eq!(highlighted_edits.highlights.len(), 1);
21779 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
21780 assert_eq!(
21781 highlighted_edits.highlights[0].1.background_color,
21782 Some(cx.theme().status().created_background)
21783 );
21784 },
21785 )
21786 .await;
21787
21788 // Replacement
21789 assert_highlighted_edits(
21790 "This is a test.",
21791 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
21792 false,
21793 cx,
21794 |highlighted_edits, cx| {
21795 assert_eq!(highlighted_edits.text, "That is a test.");
21796 assert_eq!(highlighted_edits.highlights.len(), 1);
21797 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
21798 assert_eq!(
21799 highlighted_edits.highlights[0].1.background_color,
21800 Some(cx.theme().status().created_background)
21801 );
21802 },
21803 )
21804 .await;
21805
21806 // Multiple edits
21807 assert_highlighted_edits(
21808 "Hello, world!",
21809 vec![
21810 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
21811 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
21812 ],
21813 false,
21814 cx,
21815 |highlighted_edits, cx| {
21816 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
21817 assert_eq!(highlighted_edits.highlights.len(), 2);
21818 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
21819 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
21820 assert_eq!(
21821 highlighted_edits.highlights[0].1.background_color,
21822 Some(cx.theme().status().created_background)
21823 );
21824 assert_eq!(
21825 highlighted_edits.highlights[1].1.background_color,
21826 Some(cx.theme().status().created_background)
21827 );
21828 },
21829 )
21830 .await;
21831
21832 // Multiple lines with edits
21833 assert_highlighted_edits(
21834 "First line\nSecond line\nThird line\nFourth line",
21835 vec![
21836 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
21837 (
21838 Point::new(2, 0)..Point::new(2, 10),
21839 "New third line".to_string(),
21840 ),
21841 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
21842 ],
21843 false,
21844 cx,
21845 |highlighted_edits, cx| {
21846 assert_eq!(
21847 highlighted_edits.text,
21848 "Second modified\nNew third line\nFourth updated line"
21849 );
21850 assert_eq!(highlighted_edits.highlights.len(), 3);
21851 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
21852 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
21853 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
21854 for highlight in &highlighted_edits.highlights {
21855 assert_eq!(
21856 highlight.1.background_color,
21857 Some(cx.theme().status().created_background)
21858 );
21859 }
21860 },
21861 )
21862 .await;
21863}
21864
21865#[gpui::test]
21866async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
21867 init_test(cx, |_| {});
21868
21869 // Deletion
21870 assert_highlighted_edits(
21871 "Hello, world!",
21872 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
21873 true,
21874 cx,
21875 |highlighted_edits, cx| {
21876 assert_eq!(highlighted_edits.text, "Hello, world!");
21877 assert_eq!(highlighted_edits.highlights.len(), 1);
21878 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
21879 assert_eq!(
21880 highlighted_edits.highlights[0].1.background_color,
21881 Some(cx.theme().status().deleted_background)
21882 );
21883 },
21884 )
21885 .await;
21886
21887 // Insertion
21888 assert_highlighted_edits(
21889 "Hello, world!",
21890 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
21891 true,
21892 cx,
21893 |highlighted_edits, cx| {
21894 assert_eq!(highlighted_edits.highlights.len(), 1);
21895 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
21896 assert_eq!(
21897 highlighted_edits.highlights[0].1.background_color,
21898 Some(cx.theme().status().created_background)
21899 );
21900 },
21901 )
21902 .await;
21903}
21904
21905async fn assert_highlighted_edits(
21906 text: &str,
21907 edits: Vec<(Range<Point>, String)>,
21908 include_deletions: bool,
21909 cx: &mut TestAppContext,
21910 assertion_fn: impl Fn(HighlightedText, &App),
21911) {
21912 let window = cx.add_window(|window, cx| {
21913 let buffer = MultiBuffer::build_simple(text, cx);
21914 Editor::new(EditorMode::full(), buffer, None, window, cx)
21915 });
21916 let cx = &mut VisualTestContext::from_window(*window, cx);
21917
21918 let (buffer, snapshot) = window
21919 .update(cx, |editor, _window, cx| {
21920 (
21921 editor.buffer().clone(),
21922 editor.buffer().read(cx).snapshot(cx),
21923 )
21924 })
21925 .unwrap();
21926
21927 let edits = edits
21928 .into_iter()
21929 .map(|(range, edit)| {
21930 (
21931 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21932 edit,
21933 )
21934 })
21935 .collect::<Vec<_>>();
21936
21937 let text_anchor_edits = edits
21938 .clone()
21939 .into_iter()
21940 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21941 .collect::<Vec<_>>();
21942
21943 let edit_preview = window
21944 .update(cx, |_, _window, cx| {
21945 buffer
21946 .read(cx)
21947 .as_singleton()
21948 .unwrap()
21949 .read(cx)
21950 .preview_edits(text_anchor_edits.into(), cx)
21951 })
21952 .unwrap()
21953 .await;
21954
21955 cx.update(|_window, cx| {
21956 let highlighted_edits = edit_prediction_edit_text(
21957 snapshot.as_singleton().unwrap().2,
21958 &edits,
21959 &edit_preview,
21960 include_deletions,
21961 cx,
21962 );
21963 assertion_fn(highlighted_edits, cx)
21964 });
21965}
21966
21967#[track_caller]
21968fn assert_breakpoint(
21969 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21970 path: &Arc<Path>,
21971 expected: Vec<(u32, Breakpoint)>,
21972) {
21973 if expected.is_empty() {
21974 assert!(!breakpoints.contains_key(path), "{}", path.display());
21975 } else {
21976 let mut breakpoint = breakpoints
21977 .get(path)
21978 .unwrap()
21979 .iter()
21980 .map(|breakpoint| {
21981 (
21982 breakpoint.row,
21983 Breakpoint {
21984 message: breakpoint.message.clone(),
21985 state: breakpoint.state,
21986 condition: breakpoint.condition.clone(),
21987 hit_condition: breakpoint.hit_condition.clone(),
21988 },
21989 )
21990 })
21991 .collect::<Vec<_>>();
21992
21993 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21994
21995 assert_eq!(expected, breakpoint);
21996 }
21997}
21998
21999fn add_log_breakpoint_at_cursor(
22000 editor: &mut Editor,
22001 log_message: &str,
22002 window: &mut Window,
22003 cx: &mut Context<Editor>,
22004) {
22005 let (anchor, bp) = editor
22006 .breakpoints_at_cursors(window, cx)
22007 .first()
22008 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22009 .unwrap_or_else(|| {
22010 let cursor_position: Point = editor.selections.newest(cx).head();
22011
22012 let breakpoint_position = editor
22013 .snapshot(window, cx)
22014 .display_snapshot
22015 .buffer_snapshot
22016 .anchor_before(Point::new(cursor_position.row, 0));
22017
22018 (breakpoint_position, Breakpoint::new_log(log_message))
22019 });
22020
22021 editor.edit_breakpoint_at_anchor(
22022 anchor,
22023 bp,
22024 BreakpointEditAction::EditLogMessage(log_message.into()),
22025 cx,
22026 );
22027}
22028
22029#[gpui::test]
22030async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22031 init_test(cx, |_| {});
22032
22033 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22034 let fs = FakeFs::new(cx.executor());
22035 fs.insert_tree(
22036 path!("/a"),
22037 json!({
22038 "main.rs": sample_text,
22039 }),
22040 )
22041 .await;
22042 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22043 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22044 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22045
22046 let fs = FakeFs::new(cx.executor());
22047 fs.insert_tree(
22048 path!("/a"),
22049 json!({
22050 "main.rs": sample_text,
22051 }),
22052 )
22053 .await;
22054 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22055 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22056 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22057 let worktree_id = workspace
22058 .update(cx, |workspace, _window, cx| {
22059 workspace.project().update(cx, |project, cx| {
22060 project.worktrees(cx).next().unwrap().read(cx).id()
22061 })
22062 })
22063 .unwrap();
22064
22065 let buffer = project
22066 .update(cx, |project, cx| {
22067 project.open_buffer((worktree_id, "main.rs"), cx)
22068 })
22069 .await
22070 .unwrap();
22071
22072 let (editor, cx) = cx.add_window_view(|window, cx| {
22073 Editor::new(
22074 EditorMode::full(),
22075 MultiBuffer::build_from_buffer(buffer, cx),
22076 Some(project.clone()),
22077 window,
22078 cx,
22079 )
22080 });
22081
22082 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22083 let abs_path = project.read_with(cx, |project, cx| {
22084 project
22085 .absolute_path(&project_path, cx)
22086 .map(Arc::from)
22087 .unwrap()
22088 });
22089
22090 // assert we can add breakpoint on the first line
22091 editor.update_in(cx, |editor, window, cx| {
22092 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22093 editor.move_to_end(&MoveToEnd, window, cx);
22094 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22095 });
22096
22097 let breakpoints = editor.update(cx, |editor, cx| {
22098 editor
22099 .breakpoint_store()
22100 .as_ref()
22101 .unwrap()
22102 .read(cx)
22103 .all_source_breakpoints(cx)
22104 });
22105
22106 assert_eq!(1, breakpoints.len());
22107 assert_breakpoint(
22108 &breakpoints,
22109 &abs_path,
22110 vec![
22111 (0, Breakpoint::new_standard()),
22112 (3, Breakpoint::new_standard()),
22113 ],
22114 );
22115
22116 editor.update_in(cx, |editor, window, cx| {
22117 editor.move_to_beginning(&MoveToBeginning, window, cx);
22118 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22119 });
22120
22121 let breakpoints = editor.update(cx, |editor, cx| {
22122 editor
22123 .breakpoint_store()
22124 .as_ref()
22125 .unwrap()
22126 .read(cx)
22127 .all_source_breakpoints(cx)
22128 });
22129
22130 assert_eq!(1, breakpoints.len());
22131 assert_breakpoint(
22132 &breakpoints,
22133 &abs_path,
22134 vec![(3, Breakpoint::new_standard())],
22135 );
22136
22137 editor.update_in(cx, |editor, window, cx| {
22138 editor.move_to_end(&MoveToEnd, window, cx);
22139 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22140 });
22141
22142 let breakpoints = editor.update(cx, |editor, cx| {
22143 editor
22144 .breakpoint_store()
22145 .as_ref()
22146 .unwrap()
22147 .read(cx)
22148 .all_source_breakpoints(cx)
22149 });
22150
22151 assert_eq!(0, breakpoints.len());
22152 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22153}
22154
22155#[gpui::test]
22156async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22157 init_test(cx, |_| {});
22158
22159 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22160
22161 let fs = FakeFs::new(cx.executor());
22162 fs.insert_tree(
22163 path!("/a"),
22164 json!({
22165 "main.rs": sample_text,
22166 }),
22167 )
22168 .await;
22169 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22170 let (workspace, cx) =
22171 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22172
22173 let worktree_id = workspace.update(cx, |workspace, cx| {
22174 workspace.project().update(cx, |project, cx| {
22175 project.worktrees(cx).next().unwrap().read(cx).id()
22176 })
22177 });
22178
22179 let buffer = project
22180 .update(cx, |project, cx| {
22181 project.open_buffer((worktree_id, "main.rs"), cx)
22182 })
22183 .await
22184 .unwrap();
22185
22186 let (editor, cx) = cx.add_window_view(|window, cx| {
22187 Editor::new(
22188 EditorMode::full(),
22189 MultiBuffer::build_from_buffer(buffer, cx),
22190 Some(project.clone()),
22191 window,
22192 cx,
22193 )
22194 });
22195
22196 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22197 let abs_path = project.read_with(cx, |project, cx| {
22198 project
22199 .absolute_path(&project_path, cx)
22200 .map(Arc::from)
22201 .unwrap()
22202 });
22203
22204 editor.update_in(cx, |editor, window, cx| {
22205 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22206 });
22207
22208 let breakpoints = editor.update(cx, |editor, cx| {
22209 editor
22210 .breakpoint_store()
22211 .as_ref()
22212 .unwrap()
22213 .read(cx)
22214 .all_source_breakpoints(cx)
22215 });
22216
22217 assert_breakpoint(
22218 &breakpoints,
22219 &abs_path,
22220 vec![(0, Breakpoint::new_log("hello world"))],
22221 );
22222
22223 // Removing a log message from a log breakpoint should remove it
22224 editor.update_in(cx, |editor, window, cx| {
22225 add_log_breakpoint_at_cursor(editor, "", window, cx);
22226 });
22227
22228 let breakpoints = editor.update(cx, |editor, cx| {
22229 editor
22230 .breakpoint_store()
22231 .as_ref()
22232 .unwrap()
22233 .read(cx)
22234 .all_source_breakpoints(cx)
22235 });
22236
22237 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22238
22239 editor.update_in(cx, |editor, window, cx| {
22240 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22241 editor.move_to_end(&MoveToEnd, window, cx);
22242 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22243 // Not adding a log message to a standard breakpoint shouldn't remove it
22244 add_log_breakpoint_at_cursor(editor, "", window, cx);
22245 });
22246
22247 let breakpoints = editor.update(cx, |editor, cx| {
22248 editor
22249 .breakpoint_store()
22250 .as_ref()
22251 .unwrap()
22252 .read(cx)
22253 .all_source_breakpoints(cx)
22254 });
22255
22256 assert_breakpoint(
22257 &breakpoints,
22258 &abs_path,
22259 vec![
22260 (0, Breakpoint::new_standard()),
22261 (3, Breakpoint::new_standard()),
22262 ],
22263 );
22264
22265 editor.update_in(cx, |editor, window, cx| {
22266 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22267 });
22268
22269 let breakpoints = editor.update(cx, |editor, cx| {
22270 editor
22271 .breakpoint_store()
22272 .as_ref()
22273 .unwrap()
22274 .read(cx)
22275 .all_source_breakpoints(cx)
22276 });
22277
22278 assert_breakpoint(
22279 &breakpoints,
22280 &abs_path,
22281 vec![
22282 (0, Breakpoint::new_standard()),
22283 (3, Breakpoint::new_log("hello world")),
22284 ],
22285 );
22286
22287 editor.update_in(cx, |editor, window, cx| {
22288 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22289 });
22290
22291 let breakpoints = editor.update(cx, |editor, cx| {
22292 editor
22293 .breakpoint_store()
22294 .as_ref()
22295 .unwrap()
22296 .read(cx)
22297 .all_source_breakpoints(cx)
22298 });
22299
22300 assert_breakpoint(
22301 &breakpoints,
22302 &abs_path,
22303 vec![
22304 (0, Breakpoint::new_standard()),
22305 (3, Breakpoint::new_log("hello Earth!!")),
22306 ],
22307 );
22308}
22309
22310/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22311/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22312/// or when breakpoints were placed out of order. This tests for a regression too
22313#[gpui::test]
22314async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22315 init_test(cx, |_| {});
22316
22317 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22318 let fs = FakeFs::new(cx.executor());
22319 fs.insert_tree(
22320 path!("/a"),
22321 json!({
22322 "main.rs": sample_text,
22323 }),
22324 )
22325 .await;
22326 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22327 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22328 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22329
22330 let fs = FakeFs::new(cx.executor());
22331 fs.insert_tree(
22332 path!("/a"),
22333 json!({
22334 "main.rs": sample_text,
22335 }),
22336 )
22337 .await;
22338 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22339 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22340 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22341 let worktree_id = workspace
22342 .update(cx, |workspace, _window, cx| {
22343 workspace.project().update(cx, |project, cx| {
22344 project.worktrees(cx).next().unwrap().read(cx).id()
22345 })
22346 })
22347 .unwrap();
22348
22349 let buffer = project
22350 .update(cx, |project, cx| {
22351 project.open_buffer((worktree_id, "main.rs"), cx)
22352 })
22353 .await
22354 .unwrap();
22355
22356 let (editor, cx) = cx.add_window_view(|window, cx| {
22357 Editor::new(
22358 EditorMode::full(),
22359 MultiBuffer::build_from_buffer(buffer, cx),
22360 Some(project.clone()),
22361 window,
22362 cx,
22363 )
22364 });
22365
22366 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22367 let abs_path = project.read_with(cx, |project, cx| {
22368 project
22369 .absolute_path(&project_path, cx)
22370 .map(Arc::from)
22371 .unwrap()
22372 });
22373
22374 // assert we can add breakpoint on the first line
22375 editor.update_in(cx, |editor, window, cx| {
22376 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22377 editor.move_to_end(&MoveToEnd, window, cx);
22378 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22379 editor.move_up(&MoveUp, window, cx);
22380 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22381 });
22382
22383 let breakpoints = editor.update(cx, |editor, cx| {
22384 editor
22385 .breakpoint_store()
22386 .as_ref()
22387 .unwrap()
22388 .read(cx)
22389 .all_source_breakpoints(cx)
22390 });
22391
22392 assert_eq!(1, breakpoints.len());
22393 assert_breakpoint(
22394 &breakpoints,
22395 &abs_path,
22396 vec![
22397 (0, Breakpoint::new_standard()),
22398 (2, Breakpoint::new_standard()),
22399 (3, Breakpoint::new_standard()),
22400 ],
22401 );
22402
22403 editor.update_in(cx, |editor, window, cx| {
22404 editor.move_to_beginning(&MoveToBeginning, window, cx);
22405 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22406 editor.move_to_end(&MoveToEnd, window, cx);
22407 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22408 // Disabling a breakpoint that doesn't exist should do nothing
22409 editor.move_up(&MoveUp, window, cx);
22410 editor.move_up(&MoveUp, window, cx);
22411 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22412 });
22413
22414 let breakpoints = editor.update(cx, |editor, cx| {
22415 editor
22416 .breakpoint_store()
22417 .as_ref()
22418 .unwrap()
22419 .read(cx)
22420 .all_source_breakpoints(cx)
22421 });
22422
22423 let disable_breakpoint = {
22424 let mut bp = Breakpoint::new_standard();
22425 bp.state = BreakpointState::Disabled;
22426 bp
22427 };
22428
22429 assert_eq!(1, breakpoints.len());
22430 assert_breakpoint(
22431 &breakpoints,
22432 &abs_path,
22433 vec![
22434 (0, disable_breakpoint.clone()),
22435 (2, Breakpoint::new_standard()),
22436 (3, disable_breakpoint.clone()),
22437 ],
22438 );
22439
22440 editor.update_in(cx, |editor, window, cx| {
22441 editor.move_to_beginning(&MoveToBeginning, window, cx);
22442 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22443 editor.move_to_end(&MoveToEnd, window, cx);
22444 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22445 editor.move_up(&MoveUp, window, cx);
22446 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22447 });
22448
22449 let breakpoints = editor.update(cx, |editor, cx| {
22450 editor
22451 .breakpoint_store()
22452 .as_ref()
22453 .unwrap()
22454 .read(cx)
22455 .all_source_breakpoints(cx)
22456 });
22457
22458 assert_eq!(1, breakpoints.len());
22459 assert_breakpoint(
22460 &breakpoints,
22461 &abs_path,
22462 vec![
22463 (0, Breakpoint::new_standard()),
22464 (2, disable_breakpoint),
22465 (3, Breakpoint::new_standard()),
22466 ],
22467 );
22468}
22469
22470#[gpui::test]
22471async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22472 init_test(cx, |_| {});
22473 let capabilities = lsp::ServerCapabilities {
22474 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22475 prepare_provider: Some(true),
22476 work_done_progress_options: Default::default(),
22477 })),
22478 ..Default::default()
22479 };
22480 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22481
22482 cx.set_state(indoc! {"
22483 struct Fˇoo {}
22484 "});
22485
22486 cx.update_editor(|editor, _, cx| {
22487 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22488 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22489 editor.highlight_background::<DocumentHighlightRead>(
22490 &[highlight_range],
22491 |theme| theme.colors().editor_document_highlight_read_background,
22492 cx,
22493 );
22494 });
22495
22496 let mut prepare_rename_handler = cx
22497 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22498 move |_, _, _| async move {
22499 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22500 start: lsp::Position {
22501 line: 0,
22502 character: 7,
22503 },
22504 end: lsp::Position {
22505 line: 0,
22506 character: 10,
22507 },
22508 })))
22509 },
22510 );
22511 let prepare_rename_task = cx
22512 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22513 .expect("Prepare rename was not started");
22514 prepare_rename_handler.next().await.unwrap();
22515 prepare_rename_task.await.expect("Prepare rename failed");
22516
22517 let mut rename_handler =
22518 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22519 let edit = lsp::TextEdit {
22520 range: lsp::Range {
22521 start: lsp::Position {
22522 line: 0,
22523 character: 7,
22524 },
22525 end: lsp::Position {
22526 line: 0,
22527 character: 10,
22528 },
22529 },
22530 new_text: "FooRenamed".to_string(),
22531 };
22532 Ok(Some(lsp::WorkspaceEdit::new(
22533 // Specify the same edit twice
22534 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22535 )))
22536 });
22537 let rename_task = cx
22538 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22539 .expect("Confirm rename was not started");
22540 rename_handler.next().await.unwrap();
22541 rename_task.await.expect("Confirm rename failed");
22542 cx.run_until_parked();
22543
22544 // Despite two edits, only one is actually applied as those are identical
22545 cx.assert_editor_state(indoc! {"
22546 struct FooRenamedˇ {}
22547 "});
22548}
22549
22550#[gpui::test]
22551async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22552 init_test(cx, |_| {});
22553 // These capabilities indicate that the server does not support prepare rename.
22554 let capabilities = lsp::ServerCapabilities {
22555 rename_provider: Some(lsp::OneOf::Left(true)),
22556 ..Default::default()
22557 };
22558 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22559
22560 cx.set_state(indoc! {"
22561 struct Fˇoo {}
22562 "});
22563
22564 cx.update_editor(|editor, _window, cx| {
22565 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22566 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22567 editor.highlight_background::<DocumentHighlightRead>(
22568 &[highlight_range],
22569 |theme| theme.colors().editor_document_highlight_read_background,
22570 cx,
22571 );
22572 });
22573
22574 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22575 .expect("Prepare rename was not started")
22576 .await
22577 .expect("Prepare rename failed");
22578
22579 let mut rename_handler =
22580 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22581 let edit = lsp::TextEdit {
22582 range: lsp::Range {
22583 start: lsp::Position {
22584 line: 0,
22585 character: 7,
22586 },
22587 end: lsp::Position {
22588 line: 0,
22589 character: 10,
22590 },
22591 },
22592 new_text: "FooRenamed".to_string(),
22593 };
22594 Ok(Some(lsp::WorkspaceEdit::new(
22595 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
22596 )))
22597 });
22598 let rename_task = cx
22599 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22600 .expect("Confirm rename was not started");
22601 rename_handler.next().await.unwrap();
22602 rename_task.await.expect("Confirm rename failed");
22603 cx.run_until_parked();
22604
22605 // Correct range is renamed, as `surrounding_word` is used to find it.
22606 cx.assert_editor_state(indoc! {"
22607 struct FooRenamedˇ {}
22608 "});
22609}
22610
22611#[gpui::test]
22612async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
22613 init_test(cx, |_| {});
22614 let mut cx = EditorTestContext::new(cx).await;
22615
22616 let language = Arc::new(
22617 Language::new(
22618 LanguageConfig::default(),
22619 Some(tree_sitter_html::LANGUAGE.into()),
22620 )
22621 .with_brackets_query(
22622 r#"
22623 ("<" @open "/>" @close)
22624 ("</" @open ">" @close)
22625 ("<" @open ">" @close)
22626 ("\"" @open "\"" @close)
22627 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
22628 "#,
22629 )
22630 .unwrap(),
22631 );
22632 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22633
22634 cx.set_state(indoc! {"
22635 <span>ˇ</span>
22636 "});
22637 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22638 cx.assert_editor_state(indoc! {"
22639 <span>
22640 ˇ
22641 </span>
22642 "});
22643
22644 cx.set_state(indoc! {"
22645 <span><span></span>ˇ</span>
22646 "});
22647 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22648 cx.assert_editor_state(indoc! {"
22649 <span><span></span>
22650 ˇ</span>
22651 "});
22652
22653 cx.set_state(indoc! {"
22654 <span>ˇ
22655 </span>
22656 "});
22657 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22658 cx.assert_editor_state(indoc! {"
22659 <span>
22660 ˇ
22661 </span>
22662 "});
22663}
22664
22665#[gpui::test(iterations = 10)]
22666async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
22667 init_test(cx, |_| {});
22668
22669 let fs = FakeFs::new(cx.executor());
22670 fs.insert_tree(
22671 path!("/dir"),
22672 json!({
22673 "a.ts": "a",
22674 }),
22675 )
22676 .await;
22677
22678 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
22679 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22680 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22681
22682 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22683 language_registry.add(Arc::new(Language::new(
22684 LanguageConfig {
22685 name: "TypeScript".into(),
22686 matcher: LanguageMatcher {
22687 path_suffixes: vec!["ts".to_string()],
22688 ..Default::default()
22689 },
22690 ..Default::default()
22691 },
22692 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
22693 )));
22694 let mut fake_language_servers = language_registry.register_fake_lsp(
22695 "TypeScript",
22696 FakeLspAdapter {
22697 capabilities: lsp::ServerCapabilities {
22698 code_lens_provider: Some(lsp::CodeLensOptions {
22699 resolve_provider: Some(true),
22700 }),
22701 execute_command_provider: Some(lsp::ExecuteCommandOptions {
22702 commands: vec!["_the/command".to_string()],
22703 ..lsp::ExecuteCommandOptions::default()
22704 }),
22705 ..lsp::ServerCapabilities::default()
22706 },
22707 ..FakeLspAdapter::default()
22708 },
22709 );
22710
22711 let editor = workspace
22712 .update(cx, |workspace, window, cx| {
22713 workspace.open_abs_path(
22714 PathBuf::from(path!("/dir/a.ts")),
22715 OpenOptions::default(),
22716 window,
22717 cx,
22718 )
22719 })
22720 .unwrap()
22721 .await
22722 .unwrap()
22723 .downcast::<Editor>()
22724 .unwrap();
22725 cx.executor().run_until_parked();
22726
22727 let fake_server = fake_language_servers.next().await.unwrap();
22728
22729 let buffer = editor.update(cx, |editor, cx| {
22730 editor
22731 .buffer()
22732 .read(cx)
22733 .as_singleton()
22734 .expect("have opened a single file by path")
22735 });
22736
22737 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
22738 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
22739 drop(buffer_snapshot);
22740 let actions = cx
22741 .update_window(*workspace, |_, window, cx| {
22742 project.code_actions(&buffer, anchor..anchor, window, cx)
22743 })
22744 .unwrap();
22745
22746 fake_server
22747 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22748 Ok(Some(vec![
22749 lsp::CodeLens {
22750 range: lsp::Range::default(),
22751 command: Some(lsp::Command {
22752 title: "Code lens command".to_owned(),
22753 command: "_the/command".to_owned(),
22754 arguments: None,
22755 }),
22756 data: None,
22757 },
22758 lsp::CodeLens {
22759 range: lsp::Range::default(),
22760 command: Some(lsp::Command {
22761 title: "Command not in capabilities".to_owned(),
22762 command: "not in capabilities".to_owned(),
22763 arguments: None,
22764 }),
22765 data: None,
22766 },
22767 lsp::CodeLens {
22768 range: lsp::Range {
22769 start: lsp::Position {
22770 line: 1,
22771 character: 1,
22772 },
22773 end: lsp::Position {
22774 line: 1,
22775 character: 1,
22776 },
22777 },
22778 command: Some(lsp::Command {
22779 title: "Command not in range".to_owned(),
22780 command: "_the/command".to_owned(),
22781 arguments: None,
22782 }),
22783 data: None,
22784 },
22785 ]))
22786 })
22787 .next()
22788 .await;
22789
22790 let actions = actions.await.unwrap();
22791 assert_eq!(
22792 actions.len(),
22793 1,
22794 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
22795 );
22796 let action = actions[0].clone();
22797 let apply = project.update(cx, |project, cx| {
22798 project.apply_code_action(buffer.clone(), action, true, cx)
22799 });
22800
22801 // Resolving the code action does not populate its edits. In absence of
22802 // edits, we must execute the given command.
22803 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
22804 |mut lens, _| async move {
22805 let lens_command = lens.command.as_mut().expect("should have a command");
22806 assert_eq!(lens_command.title, "Code lens command");
22807 lens_command.arguments = Some(vec![json!("the-argument")]);
22808 Ok(lens)
22809 },
22810 );
22811
22812 // While executing the command, the language server sends the editor
22813 // a `workspaceEdit` request.
22814 fake_server
22815 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
22816 let fake = fake_server.clone();
22817 move |params, _| {
22818 assert_eq!(params.command, "_the/command");
22819 let fake = fake.clone();
22820 async move {
22821 fake.server
22822 .request::<lsp::request::ApplyWorkspaceEdit>(
22823 lsp::ApplyWorkspaceEditParams {
22824 label: None,
22825 edit: lsp::WorkspaceEdit {
22826 changes: Some(
22827 [(
22828 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
22829 vec![lsp::TextEdit {
22830 range: lsp::Range::new(
22831 lsp::Position::new(0, 0),
22832 lsp::Position::new(0, 0),
22833 ),
22834 new_text: "X".into(),
22835 }],
22836 )]
22837 .into_iter()
22838 .collect(),
22839 ),
22840 ..lsp::WorkspaceEdit::default()
22841 },
22842 },
22843 )
22844 .await
22845 .into_response()
22846 .unwrap();
22847 Ok(Some(json!(null)))
22848 }
22849 }
22850 })
22851 .next()
22852 .await;
22853
22854 // Applying the code lens command returns a project transaction containing the edits
22855 // sent by the language server in its `workspaceEdit` request.
22856 let transaction = apply.await.unwrap();
22857 assert!(transaction.0.contains_key(&buffer));
22858 buffer.update(cx, |buffer, cx| {
22859 assert_eq!(buffer.text(), "Xa");
22860 buffer.undo(cx);
22861 assert_eq!(buffer.text(), "a");
22862 });
22863
22864 let actions_after_edits = cx
22865 .update_window(*workspace, |_, window, cx| {
22866 project.code_actions(&buffer, anchor..anchor, window, cx)
22867 })
22868 .unwrap()
22869 .await
22870 .unwrap();
22871 assert_eq!(
22872 actions, actions_after_edits,
22873 "For the same selection, same code lens actions should be returned"
22874 );
22875
22876 let _responses =
22877 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22878 panic!("No more code lens requests are expected");
22879 });
22880 editor.update_in(cx, |editor, window, cx| {
22881 editor.select_all(&SelectAll, window, cx);
22882 });
22883 cx.executor().run_until_parked();
22884 let new_actions = cx
22885 .update_window(*workspace, |_, window, cx| {
22886 project.code_actions(&buffer, anchor..anchor, window, cx)
22887 })
22888 .unwrap()
22889 .await
22890 .unwrap();
22891 assert_eq!(
22892 actions, new_actions,
22893 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
22894 );
22895}
22896
22897#[gpui::test]
22898async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
22899 init_test(cx, |_| {});
22900
22901 let fs = FakeFs::new(cx.executor());
22902 let main_text = r#"fn main() {
22903println!("1");
22904println!("2");
22905println!("3");
22906println!("4");
22907println!("5");
22908}"#;
22909 let lib_text = "mod foo {}";
22910 fs.insert_tree(
22911 path!("/a"),
22912 json!({
22913 "lib.rs": lib_text,
22914 "main.rs": main_text,
22915 }),
22916 )
22917 .await;
22918
22919 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22920 let (workspace, cx) =
22921 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22922 let worktree_id = workspace.update(cx, |workspace, cx| {
22923 workspace.project().update(cx, |project, cx| {
22924 project.worktrees(cx).next().unwrap().read(cx).id()
22925 })
22926 });
22927
22928 let expected_ranges = vec![
22929 Point::new(0, 0)..Point::new(0, 0),
22930 Point::new(1, 0)..Point::new(1, 1),
22931 Point::new(2, 0)..Point::new(2, 2),
22932 Point::new(3, 0)..Point::new(3, 3),
22933 ];
22934
22935 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22936 let editor_1 = workspace
22937 .update_in(cx, |workspace, window, cx| {
22938 workspace.open_path(
22939 (worktree_id, "main.rs"),
22940 Some(pane_1.downgrade()),
22941 true,
22942 window,
22943 cx,
22944 )
22945 })
22946 .unwrap()
22947 .await
22948 .downcast::<Editor>()
22949 .unwrap();
22950 pane_1.update(cx, |pane, cx| {
22951 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22952 open_editor.update(cx, |editor, cx| {
22953 assert_eq!(
22954 editor.display_text(cx),
22955 main_text,
22956 "Original main.rs text on initial open",
22957 );
22958 assert_eq!(
22959 editor
22960 .selections
22961 .all::<Point>(cx)
22962 .into_iter()
22963 .map(|s| s.range())
22964 .collect::<Vec<_>>(),
22965 vec![Point::zero()..Point::zero()],
22966 "Default selections on initial open",
22967 );
22968 })
22969 });
22970 editor_1.update_in(cx, |editor, window, cx| {
22971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22972 s.select_ranges(expected_ranges.clone());
22973 });
22974 });
22975
22976 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22977 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22978 });
22979 let editor_2 = workspace
22980 .update_in(cx, |workspace, window, cx| {
22981 workspace.open_path(
22982 (worktree_id, "main.rs"),
22983 Some(pane_2.downgrade()),
22984 true,
22985 window,
22986 cx,
22987 )
22988 })
22989 .unwrap()
22990 .await
22991 .downcast::<Editor>()
22992 .unwrap();
22993 pane_2.update(cx, |pane, cx| {
22994 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22995 open_editor.update(cx, |editor, cx| {
22996 assert_eq!(
22997 editor.display_text(cx),
22998 main_text,
22999 "Original main.rs text on initial open in another panel",
23000 );
23001 assert_eq!(
23002 editor
23003 .selections
23004 .all::<Point>(cx)
23005 .into_iter()
23006 .map(|s| s.range())
23007 .collect::<Vec<_>>(),
23008 vec![Point::zero()..Point::zero()],
23009 "Default selections on initial open in another panel",
23010 );
23011 })
23012 });
23013
23014 editor_2.update_in(cx, |editor, window, cx| {
23015 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23016 });
23017
23018 let _other_editor_1 = workspace
23019 .update_in(cx, |workspace, window, cx| {
23020 workspace.open_path(
23021 (worktree_id, "lib.rs"),
23022 Some(pane_1.downgrade()),
23023 true,
23024 window,
23025 cx,
23026 )
23027 })
23028 .unwrap()
23029 .await
23030 .downcast::<Editor>()
23031 .unwrap();
23032 pane_1
23033 .update_in(cx, |pane, window, cx| {
23034 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23035 })
23036 .await
23037 .unwrap();
23038 drop(editor_1);
23039 pane_1.update(cx, |pane, cx| {
23040 pane.active_item()
23041 .unwrap()
23042 .downcast::<Editor>()
23043 .unwrap()
23044 .update(cx, |editor, cx| {
23045 assert_eq!(
23046 editor.display_text(cx),
23047 lib_text,
23048 "Other file should be open and active",
23049 );
23050 });
23051 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23052 });
23053
23054 let _other_editor_2 = workspace
23055 .update_in(cx, |workspace, window, cx| {
23056 workspace.open_path(
23057 (worktree_id, "lib.rs"),
23058 Some(pane_2.downgrade()),
23059 true,
23060 window,
23061 cx,
23062 )
23063 })
23064 .unwrap()
23065 .await
23066 .downcast::<Editor>()
23067 .unwrap();
23068 pane_2
23069 .update_in(cx, |pane, window, cx| {
23070 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23071 })
23072 .await
23073 .unwrap();
23074 drop(editor_2);
23075 pane_2.update(cx, |pane, cx| {
23076 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23077 open_editor.update(cx, |editor, cx| {
23078 assert_eq!(
23079 editor.display_text(cx),
23080 lib_text,
23081 "Other file should be open and active in another panel too",
23082 );
23083 });
23084 assert_eq!(
23085 pane.items().count(),
23086 1,
23087 "No other editors should be open in another pane",
23088 );
23089 });
23090
23091 let _editor_1_reopened = workspace
23092 .update_in(cx, |workspace, window, cx| {
23093 workspace.open_path(
23094 (worktree_id, "main.rs"),
23095 Some(pane_1.downgrade()),
23096 true,
23097 window,
23098 cx,
23099 )
23100 })
23101 .unwrap()
23102 .await
23103 .downcast::<Editor>()
23104 .unwrap();
23105 let _editor_2_reopened = workspace
23106 .update_in(cx, |workspace, window, cx| {
23107 workspace.open_path(
23108 (worktree_id, "main.rs"),
23109 Some(pane_2.downgrade()),
23110 true,
23111 window,
23112 cx,
23113 )
23114 })
23115 .unwrap()
23116 .await
23117 .downcast::<Editor>()
23118 .unwrap();
23119 pane_1.update(cx, |pane, cx| {
23120 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23121 open_editor.update(cx, |editor, cx| {
23122 assert_eq!(
23123 editor.display_text(cx),
23124 main_text,
23125 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23126 );
23127 assert_eq!(
23128 editor
23129 .selections
23130 .all::<Point>(cx)
23131 .into_iter()
23132 .map(|s| s.range())
23133 .collect::<Vec<_>>(),
23134 expected_ranges,
23135 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23136 );
23137 })
23138 });
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 r#"fn main() {
23145⋯rintln!("1");
23146⋯intln!("2");
23147⋯ntln!("3");
23148println!("4");
23149println!("5");
23150}"#,
23151 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23152 );
23153 assert_eq!(
23154 editor
23155 .selections
23156 .all::<Point>(cx)
23157 .into_iter()
23158 .map(|s| s.range())
23159 .collect::<Vec<_>>(),
23160 vec![Point::zero()..Point::zero()],
23161 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23162 );
23163 })
23164 });
23165}
23166
23167#[gpui::test]
23168async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23169 init_test(cx, |_| {});
23170
23171 let fs = FakeFs::new(cx.executor());
23172 let main_text = r#"fn main() {
23173println!("1");
23174println!("2");
23175println!("3");
23176println!("4");
23177println!("5");
23178}"#;
23179 let lib_text = "mod foo {}";
23180 fs.insert_tree(
23181 path!("/a"),
23182 json!({
23183 "lib.rs": lib_text,
23184 "main.rs": main_text,
23185 }),
23186 )
23187 .await;
23188
23189 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23190 let (workspace, cx) =
23191 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23192 let worktree_id = workspace.update(cx, |workspace, cx| {
23193 workspace.project().update(cx, |project, cx| {
23194 project.worktrees(cx).next().unwrap().read(cx).id()
23195 })
23196 });
23197
23198 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23199 let editor = workspace
23200 .update_in(cx, |workspace, window, cx| {
23201 workspace.open_path(
23202 (worktree_id, "main.rs"),
23203 Some(pane.downgrade()),
23204 true,
23205 window,
23206 cx,
23207 )
23208 })
23209 .unwrap()
23210 .await
23211 .downcast::<Editor>()
23212 .unwrap();
23213 pane.update(cx, |pane, cx| {
23214 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23215 open_editor.update(cx, |editor, cx| {
23216 assert_eq!(
23217 editor.display_text(cx),
23218 main_text,
23219 "Original main.rs text on initial open",
23220 );
23221 })
23222 });
23223 editor.update_in(cx, |editor, window, cx| {
23224 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23225 });
23226
23227 cx.update_global(|store: &mut SettingsStore, cx| {
23228 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23229 s.restore_on_file_reopen = Some(false);
23230 });
23231 });
23232 editor.update_in(cx, |editor, window, cx| {
23233 editor.fold_ranges(
23234 vec![
23235 Point::new(1, 0)..Point::new(1, 1),
23236 Point::new(2, 0)..Point::new(2, 2),
23237 Point::new(3, 0)..Point::new(3, 3),
23238 ],
23239 false,
23240 window,
23241 cx,
23242 );
23243 });
23244 pane.update_in(cx, |pane, window, cx| {
23245 pane.close_all_items(&CloseAllItems::default(), window, cx)
23246 })
23247 .await
23248 .unwrap();
23249 pane.update(cx, |pane, _| {
23250 assert!(pane.active_item().is_none());
23251 });
23252 cx.update_global(|store: &mut SettingsStore, cx| {
23253 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23254 s.restore_on_file_reopen = Some(true);
23255 });
23256 });
23257
23258 let _editor_reopened = workspace
23259 .update_in(cx, |workspace, window, cx| {
23260 workspace.open_path(
23261 (worktree_id, "main.rs"),
23262 Some(pane.downgrade()),
23263 true,
23264 window,
23265 cx,
23266 )
23267 })
23268 .unwrap()
23269 .await
23270 .downcast::<Editor>()
23271 .unwrap();
23272 pane.update(cx, |pane, cx| {
23273 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23274 open_editor.update(cx, |editor, cx| {
23275 assert_eq!(
23276 editor.display_text(cx),
23277 main_text,
23278 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23279 );
23280 })
23281 });
23282}
23283
23284#[gpui::test]
23285async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23286 struct EmptyModalView {
23287 focus_handle: gpui::FocusHandle,
23288 }
23289 impl EventEmitter<DismissEvent> for EmptyModalView {}
23290 impl Render for EmptyModalView {
23291 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23292 div()
23293 }
23294 }
23295 impl Focusable for EmptyModalView {
23296 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23297 self.focus_handle.clone()
23298 }
23299 }
23300 impl workspace::ModalView for EmptyModalView {}
23301 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23302 EmptyModalView {
23303 focus_handle: cx.focus_handle(),
23304 }
23305 }
23306
23307 init_test(cx, |_| {});
23308
23309 let fs = FakeFs::new(cx.executor());
23310 let project = Project::test(fs, [], cx).await;
23311 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23312 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23313 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23314 let editor = cx.new_window_entity(|window, cx| {
23315 Editor::new(
23316 EditorMode::full(),
23317 buffer,
23318 Some(project.clone()),
23319 window,
23320 cx,
23321 )
23322 });
23323 workspace
23324 .update(cx, |workspace, window, cx| {
23325 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23326 })
23327 .unwrap();
23328 editor.update_in(cx, |editor, window, cx| {
23329 editor.open_context_menu(&OpenContextMenu, window, cx);
23330 assert!(editor.mouse_context_menu.is_some());
23331 });
23332 workspace
23333 .update(cx, |workspace, window, cx| {
23334 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23335 })
23336 .unwrap();
23337 cx.read(|cx| {
23338 assert!(editor.read(cx).mouse_context_menu.is_none());
23339 });
23340}
23341
23342#[gpui::test]
23343async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23344 init_test(cx, |_| {});
23345
23346 let fs = FakeFs::new(cx.executor());
23347 fs.insert_file(path!("/file.html"), Default::default())
23348 .await;
23349
23350 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23351
23352 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23353 let html_language = Arc::new(Language::new(
23354 LanguageConfig {
23355 name: "HTML".into(),
23356 matcher: LanguageMatcher {
23357 path_suffixes: vec!["html".to_string()],
23358 ..LanguageMatcher::default()
23359 },
23360 brackets: BracketPairConfig {
23361 pairs: vec![BracketPair {
23362 start: "<".into(),
23363 end: ">".into(),
23364 close: true,
23365 ..Default::default()
23366 }],
23367 ..Default::default()
23368 },
23369 ..Default::default()
23370 },
23371 Some(tree_sitter_html::LANGUAGE.into()),
23372 ));
23373 language_registry.add(html_language);
23374 let mut fake_servers = language_registry.register_fake_lsp(
23375 "HTML",
23376 FakeLspAdapter {
23377 capabilities: lsp::ServerCapabilities {
23378 completion_provider: Some(lsp::CompletionOptions {
23379 resolve_provider: Some(true),
23380 ..Default::default()
23381 }),
23382 ..Default::default()
23383 },
23384 ..Default::default()
23385 },
23386 );
23387
23388 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23389 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23390
23391 let worktree_id = workspace
23392 .update(cx, |workspace, _window, cx| {
23393 workspace.project().update(cx, |project, cx| {
23394 project.worktrees(cx).next().unwrap().read(cx).id()
23395 })
23396 })
23397 .unwrap();
23398 project
23399 .update(cx, |project, cx| {
23400 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23401 })
23402 .await
23403 .unwrap();
23404 let editor = workspace
23405 .update(cx, |workspace, window, cx| {
23406 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23407 })
23408 .unwrap()
23409 .await
23410 .unwrap()
23411 .downcast::<Editor>()
23412 .unwrap();
23413
23414 let fake_server = fake_servers.next().await.unwrap();
23415 editor.update_in(cx, |editor, window, cx| {
23416 editor.set_text("<ad></ad>", window, cx);
23417 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23418 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23419 });
23420 let Some((buffer, _)) = editor
23421 .buffer
23422 .read(cx)
23423 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23424 else {
23425 panic!("Failed to get buffer for selection position");
23426 };
23427 let buffer = buffer.read(cx);
23428 let buffer_id = buffer.remote_id();
23429 let opening_range =
23430 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23431 let closing_range =
23432 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23433 let mut linked_ranges = HashMap::default();
23434 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23435 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23436 });
23437 let mut completion_handle =
23438 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23439 Ok(Some(lsp::CompletionResponse::Array(vec![
23440 lsp::CompletionItem {
23441 label: "head".to_string(),
23442 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23443 lsp::InsertReplaceEdit {
23444 new_text: "head".to_string(),
23445 insert: lsp::Range::new(
23446 lsp::Position::new(0, 1),
23447 lsp::Position::new(0, 3),
23448 ),
23449 replace: lsp::Range::new(
23450 lsp::Position::new(0, 1),
23451 lsp::Position::new(0, 3),
23452 ),
23453 },
23454 )),
23455 ..Default::default()
23456 },
23457 ])))
23458 });
23459 editor.update_in(cx, |editor, window, cx| {
23460 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23461 });
23462 cx.run_until_parked();
23463 completion_handle.next().await.unwrap();
23464 editor.update(cx, |editor, _| {
23465 assert!(
23466 editor.context_menu_visible(),
23467 "Completion menu should be visible"
23468 );
23469 });
23470 editor.update_in(cx, |editor, window, cx| {
23471 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23472 });
23473 cx.executor().run_until_parked();
23474 editor.update(cx, |editor, cx| {
23475 assert_eq!(editor.text(cx), "<head></head>");
23476 });
23477}
23478
23479#[gpui::test]
23480async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23481 init_test(cx, |_| {});
23482
23483 let fs = FakeFs::new(cx.executor());
23484 fs.insert_tree(
23485 path!("/root"),
23486 json!({
23487 "a": {
23488 "main.rs": "fn main() {}",
23489 },
23490 "foo": {
23491 "bar": {
23492 "external_file.rs": "pub mod external {}",
23493 }
23494 }
23495 }),
23496 )
23497 .await;
23498
23499 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23500 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23501 language_registry.add(rust_lang());
23502 let _fake_servers = language_registry.register_fake_lsp(
23503 "Rust",
23504 FakeLspAdapter {
23505 ..FakeLspAdapter::default()
23506 },
23507 );
23508 let (workspace, cx) =
23509 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23510 let worktree_id = workspace.update(cx, |workspace, cx| {
23511 workspace.project().update(cx, |project, cx| {
23512 project.worktrees(cx).next().unwrap().read(cx).id()
23513 })
23514 });
23515
23516 let assert_language_servers_count =
23517 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23518 project.update(cx, |project, cx| {
23519 let current = project
23520 .lsp_store()
23521 .read(cx)
23522 .as_local()
23523 .unwrap()
23524 .language_servers
23525 .len();
23526 assert_eq!(expected, current, "{context}");
23527 });
23528 };
23529
23530 assert_language_servers_count(
23531 0,
23532 "No servers should be running before any file is open",
23533 cx,
23534 );
23535 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23536 let main_editor = workspace
23537 .update_in(cx, |workspace, window, cx| {
23538 workspace.open_path(
23539 (worktree_id, "main.rs"),
23540 Some(pane.downgrade()),
23541 true,
23542 window,
23543 cx,
23544 )
23545 })
23546 .unwrap()
23547 .await
23548 .downcast::<Editor>()
23549 .unwrap();
23550 pane.update(cx, |pane, cx| {
23551 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23552 open_editor.update(cx, |editor, cx| {
23553 assert_eq!(
23554 editor.display_text(cx),
23555 "fn main() {}",
23556 "Original main.rs text on initial open",
23557 );
23558 });
23559 assert_eq!(open_editor, main_editor);
23560 });
23561 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
23562
23563 let external_editor = workspace
23564 .update_in(cx, |workspace, window, cx| {
23565 workspace.open_abs_path(
23566 PathBuf::from("/root/foo/bar/external_file.rs"),
23567 OpenOptions::default(),
23568 window,
23569 cx,
23570 )
23571 })
23572 .await
23573 .expect("opening external file")
23574 .downcast::<Editor>()
23575 .expect("downcasted external file's open element to editor");
23576 pane.update(cx, |pane, cx| {
23577 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23578 open_editor.update(cx, |editor, cx| {
23579 assert_eq!(
23580 editor.display_text(cx),
23581 "pub mod external {}",
23582 "External file is open now",
23583 );
23584 });
23585 assert_eq!(open_editor, external_editor);
23586 });
23587 assert_language_servers_count(
23588 1,
23589 "Second, external, *.rs file should join the existing server",
23590 cx,
23591 );
23592
23593 pane.update_in(cx, |pane, window, cx| {
23594 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23595 })
23596 .await
23597 .unwrap();
23598 pane.update_in(cx, |pane, window, cx| {
23599 pane.navigate_backward(&Default::default(), window, cx);
23600 });
23601 cx.run_until_parked();
23602 pane.update(cx, |pane, cx| {
23603 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23604 open_editor.update(cx, |editor, cx| {
23605 assert_eq!(
23606 editor.display_text(cx),
23607 "pub mod external {}",
23608 "External file is open now",
23609 );
23610 });
23611 });
23612 assert_language_servers_count(
23613 1,
23614 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
23615 cx,
23616 );
23617
23618 cx.update(|_, cx| {
23619 workspace::reload(cx);
23620 });
23621 assert_language_servers_count(
23622 1,
23623 "After reloading the worktree with local and external files opened, only one project should be started",
23624 cx,
23625 );
23626}
23627
23628#[gpui::test]
23629async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
23630 init_test(cx, |_| {});
23631
23632 let mut cx = EditorTestContext::new(cx).await;
23633 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23634 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23635
23636 // test cursor move to start of each line on tab
23637 // for `if`, `elif`, `else`, `while`, `with` and `for`
23638 cx.set_state(indoc! {"
23639 def main():
23640 ˇ for item in items:
23641 ˇ while item.active:
23642 ˇ if item.value > 10:
23643 ˇ continue
23644 ˇ elif item.value < 0:
23645 ˇ break
23646 ˇ else:
23647 ˇ with item.context() as ctx:
23648 ˇ yield count
23649 ˇ else:
23650 ˇ log('while else')
23651 ˇ else:
23652 ˇ log('for else')
23653 "});
23654 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23655 cx.assert_editor_state(indoc! {"
23656 def main():
23657 ˇfor item in items:
23658 ˇwhile item.active:
23659 ˇif item.value > 10:
23660 ˇcontinue
23661 ˇelif item.value < 0:
23662 ˇbreak
23663 ˇelse:
23664 ˇwith item.context() as ctx:
23665 ˇyield count
23666 ˇelse:
23667 ˇlog('while else')
23668 ˇelse:
23669 ˇlog('for else')
23670 "});
23671 // test relative indent is preserved when tab
23672 // for `if`, `elif`, `else`, `while`, `with` and `for`
23673 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23674 cx.assert_editor_state(indoc! {"
23675 def main():
23676 ˇfor item in items:
23677 ˇwhile item.active:
23678 ˇif item.value > 10:
23679 ˇcontinue
23680 ˇelif item.value < 0:
23681 ˇbreak
23682 ˇelse:
23683 ˇwith item.context() as ctx:
23684 ˇyield count
23685 ˇelse:
23686 ˇlog('while else')
23687 ˇelse:
23688 ˇlog('for else')
23689 "});
23690
23691 // test cursor move to start of each line on tab
23692 // for `try`, `except`, `else`, `finally`, `match` and `def`
23693 cx.set_state(indoc! {"
23694 def main():
23695 ˇ try:
23696 ˇ fetch()
23697 ˇ except ValueError:
23698 ˇ handle_error()
23699 ˇ else:
23700 ˇ match value:
23701 ˇ case _:
23702 ˇ finally:
23703 ˇ def status():
23704 ˇ return 0
23705 "});
23706 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23707 cx.assert_editor_state(indoc! {"
23708 def main():
23709 ˇtry:
23710 ˇfetch()
23711 ˇexcept ValueError:
23712 ˇhandle_error()
23713 ˇelse:
23714 ˇmatch value:
23715 ˇcase _:
23716 ˇfinally:
23717 ˇdef status():
23718 ˇreturn 0
23719 "});
23720 // test relative indent is preserved when tab
23721 // for `try`, `except`, `else`, `finally`, `match` and `def`
23722 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23723 cx.assert_editor_state(indoc! {"
23724 def main():
23725 ˇtry:
23726 ˇfetch()
23727 ˇexcept ValueError:
23728 ˇhandle_error()
23729 ˇelse:
23730 ˇmatch value:
23731 ˇcase _:
23732 ˇfinally:
23733 ˇdef status():
23734 ˇreturn 0
23735 "});
23736}
23737
23738#[gpui::test]
23739async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
23740 init_test(cx, |_| {});
23741
23742 let mut cx = EditorTestContext::new(cx).await;
23743 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23744 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23745
23746 // test `else` auto outdents when typed inside `if` block
23747 cx.set_state(indoc! {"
23748 def main():
23749 if i == 2:
23750 return
23751 ˇ
23752 "});
23753 cx.update_editor(|editor, window, cx| {
23754 editor.handle_input("else:", window, cx);
23755 });
23756 cx.assert_editor_state(indoc! {"
23757 def main():
23758 if i == 2:
23759 return
23760 else:ˇ
23761 "});
23762
23763 // test `except` auto outdents when typed inside `try` block
23764 cx.set_state(indoc! {"
23765 def main():
23766 try:
23767 i = 2
23768 ˇ
23769 "});
23770 cx.update_editor(|editor, window, cx| {
23771 editor.handle_input("except:", window, cx);
23772 });
23773 cx.assert_editor_state(indoc! {"
23774 def main():
23775 try:
23776 i = 2
23777 except:ˇ
23778 "});
23779
23780 // test `else` auto outdents when typed inside `except` block
23781 cx.set_state(indoc! {"
23782 def main():
23783 try:
23784 i = 2
23785 except:
23786 j = 2
23787 ˇ
23788 "});
23789 cx.update_editor(|editor, window, cx| {
23790 editor.handle_input("else:", window, cx);
23791 });
23792 cx.assert_editor_state(indoc! {"
23793 def main():
23794 try:
23795 i = 2
23796 except:
23797 j = 2
23798 else:ˇ
23799 "});
23800
23801 // test `finally` auto outdents when typed inside `else` block
23802 cx.set_state(indoc! {"
23803 def main():
23804 try:
23805 i = 2
23806 except:
23807 j = 2
23808 else:
23809 k = 2
23810 ˇ
23811 "});
23812 cx.update_editor(|editor, window, cx| {
23813 editor.handle_input("finally:", window, cx);
23814 });
23815 cx.assert_editor_state(indoc! {"
23816 def main():
23817 try:
23818 i = 2
23819 except:
23820 j = 2
23821 else:
23822 k = 2
23823 finally:ˇ
23824 "});
23825
23826 // test `else` does not outdents when typed inside `except` block right after for block
23827 cx.set_state(indoc! {"
23828 def main():
23829 try:
23830 i = 2
23831 except:
23832 for i in range(n):
23833 pass
23834 ˇ
23835 "});
23836 cx.update_editor(|editor, window, cx| {
23837 editor.handle_input("else:", window, cx);
23838 });
23839 cx.assert_editor_state(indoc! {"
23840 def main():
23841 try:
23842 i = 2
23843 except:
23844 for i in range(n):
23845 pass
23846 else:ˇ
23847 "});
23848
23849 // test `finally` auto outdents when typed inside `else` block right after for block
23850 cx.set_state(indoc! {"
23851 def main():
23852 try:
23853 i = 2
23854 except:
23855 j = 2
23856 else:
23857 for i in range(n):
23858 pass
23859 ˇ
23860 "});
23861 cx.update_editor(|editor, window, cx| {
23862 editor.handle_input("finally:", window, cx);
23863 });
23864 cx.assert_editor_state(indoc! {"
23865 def main():
23866 try:
23867 i = 2
23868 except:
23869 j = 2
23870 else:
23871 for i in range(n):
23872 pass
23873 finally:ˇ
23874 "});
23875
23876 // test `except` outdents to inner "try" block
23877 cx.set_state(indoc! {"
23878 def main():
23879 try:
23880 i = 2
23881 if i == 2:
23882 try:
23883 i = 3
23884 ˇ
23885 "});
23886 cx.update_editor(|editor, window, cx| {
23887 editor.handle_input("except:", window, cx);
23888 });
23889 cx.assert_editor_state(indoc! {"
23890 def main():
23891 try:
23892 i = 2
23893 if i == 2:
23894 try:
23895 i = 3
23896 except:ˇ
23897 "});
23898
23899 // test `except` outdents to outer "try" block
23900 cx.set_state(indoc! {"
23901 def main():
23902 try:
23903 i = 2
23904 if i == 2:
23905 try:
23906 i = 3
23907 ˇ
23908 "});
23909 cx.update_editor(|editor, window, cx| {
23910 editor.handle_input("except:", window, cx);
23911 });
23912 cx.assert_editor_state(indoc! {"
23913 def main():
23914 try:
23915 i = 2
23916 if i == 2:
23917 try:
23918 i = 3
23919 except:ˇ
23920 "});
23921
23922 // test `else` stays at correct indent when typed after `for` block
23923 cx.set_state(indoc! {"
23924 def main():
23925 for i in range(10):
23926 if i == 3:
23927 break
23928 ˇ
23929 "});
23930 cx.update_editor(|editor, window, cx| {
23931 editor.handle_input("else:", window, cx);
23932 });
23933 cx.assert_editor_state(indoc! {"
23934 def main():
23935 for i in range(10):
23936 if i == 3:
23937 break
23938 else:ˇ
23939 "});
23940
23941 // test does not outdent on typing after line with square brackets
23942 cx.set_state(indoc! {"
23943 def f() -> list[str]:
23944 ˇ
23945 "});
23946 cx.update_editor(|editor, window, cx| {
23947 editor.handle_input("a", window, cx);
23948 });
23949 cx.assert_editor_state(indoc! {"
23950 def f() -> list[str]:
23951 aˇ
23952 "});
23953
23954 // test does not outdent on typing : after case keyword
23955 cx.set_state(indoc! {"
23956 match 1:
23957 caseˇ
23958 "});
23959 cx.update_editor(|editor, window, cx| {
23960 editor.handle_input(":", window, cx);
23961 });
23962 cx.assert_editor_state(indoc! {"
23963 match 1:
23964 case:ˇ
23965 "});
23966}
23967
23968#[gpui::test]
23969async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23970 init_test(cx, |_| {});
23971 update_test_language_settings(cx, |settings| {
23972 settings.defaults.extend_comment_on_newline = Some(false);
23973 });
23974 let mut cx = EditorTestContext::new(cx).await;
23975 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23976 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23977
23978 // test correct indent after newline on comment
23979 cx.set_state(indoc! {"
23980 # COMMENT:ˇ
23981 "});
23982 cx.update_editor(|editor, window, cx| {
23983 editor.newline(&Newline, window, cx);
23984 });
23985 cx.assert_editor_state(indoc! {"
23986 # COMMENT:
23987 ˇ
23988 "});
23989
23990 // test correct indent after newline in brackets
23991 cx.set_state(indoc! {"
23992 {ˇ}
23993 "});
23994 cx.update_editor(|editor, window, cx| {
23995 editor.newline(&Newline, window, cx);
23996 });
23997 cx.run_until_parked();
23998 cx.assert_editor_state(indoc! {"
23999 {
24000 ˇ
24001 }
24002 "});
24003
24004 cx.set_state(indoc! {"
24005 (ˇ)
24006 "});
24007 cx.update_editor(|editor, window, cx| {
24008 editor.newline(&Newline, window, cx);
24009 });
24010 cx.run_until_parked();
24011 cx.assert_editor_state(indoc! {"
24012 (
24013 ˇ
24014 )
24015 "});
24016
24017 // do not indent after empty lists or dictionaries
24018 cx.set_state(indoc! {"
24019 a = []ˇ
24020 "});
24021 cx.update_editor(|editor, window, cx| {
24022 editor.newline(&Newline, window, cx);
24023 });
24024 cx.run_until_parked();
24025 cx.assert_editor_state(indoc! {"
24026 a = []
24027 ˇ
24028 "});
24029}
24030
24031#[gpui::test]
24032async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24033 init_test(cx, |_| {});
24034
24035 let mut cx = EditorTestContext::new(cx).await;
24036 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24037 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24038
24039 // test cursor move to start of each line on tab
24040 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24041 cx.set_state(indoc! {"
24042 function main() {
24043 ˇ for item in $items; do
24044 ˇ while [ -n \"$item\" ]; do
24045 ˇ if [ \"$value\" -gt 10 ]; then
24046 ˇ continue
24047 ˇ elif [ \"$value\" -lt 0 ]; then
24048 ˇ break
24049 ˇ else
24050 ˇ echo \"$item\"
24051 ˇ fi
24052 ˇ done
24053 ˇ done
24054 ˇ}
24055 "});
24056 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24057 cx.assert_editor_state(indoc! {"
24058 function main() {
24059 ˇfor item in $items; do
24060 ˇwhile [ -n \"$item\" ]; do
24061 ˇif [ \"$value\" -gt 10 ]; then
24062 ˇcontinue
24063 ˇelif [ \"$value\" -lt 0 ]; then
24064 ˇbreak
24065 ˇelse
24066 ˇecho \"$item\"
24067 ˇfi
24068 ˇdone
24069 ˇdone
24070 ˇ}
24071 "});
24072 // test relative indent is preserved when tab
24073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24074 cx.assert_editor_state(indoc! {"
24075 function main() {
24076 ˇfor item in $items; do
24077 ˇwhile [ -n \"$item\" ]; do
24078 ˇif [ \"$value\" -gt 10 ]; then
24079 ˇcontinue
24080 ˇelif [ \"$value\" -lt 0 ]; then
24081 ˇbreak
24082 ˇelse
24083 ˇecho \"$item\"
24084 ˇfi
24085 ˇdone
24086 ˇdone
24087 ˇ}
24088 "});
24089
24090 // test cursor move to start of each line on tab
24091 // for `case` statement with patterns
24092 cx.set_state(indoc! {"
24093 function handle() {
24094 ˇ case \"$1\" in
24095 ˇ start)
24096 ˇ echo \"a\"
24097 ˇ ;;
24098 ˇ stop)
24099 ˇ echo \"b\"
24100 ˇ ;;
24101 ˇ *)
24102 ˇ echo \"c\"
24103 ˇ ;;
24104 ˇ esac
24105 ˇ}
24106 "});
24107 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24108 cx.assert_editor_state(indoc! {"
24109 function handle() {
24110 ˇcase \"$1\" in
24111 ˇstart)
24112 ˇecho \"a\"
24113 ˇ;;
24114 ˇstop)
24115 ˇecho \"b\"
24116 ˇ;;
24117 ˇ*)
24118 ˇecho \"c\"
24119 ˇ;;
24120 ˇesac
24121 ˇ}
24122 "});
24123}
24124
24125#[gpui::test]
24126async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24127 init_test(cx, |_| {});
24128
24129 let mut cx = EditorTestContext::new(cx).await;
24130 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24131 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24132
24133 // test indents on comment insert
24134 cx.set_state(indoc! {"
24135 function main() {
24136 ˇ for item in $items; do
24137 ˇ while [ -n \"$item\" ]; do
24138 ˇ if [ \"$value\" -gt 10 ]; then
24139 ˇ continue
24140 ˇ elif [ \"$value\" -lt 0 ]; then
24141 ˇ break
24142 ˇ else
24143 ˇ echo \"$item\"
24144 ˇ fi
24145 ˇ done
24146 ˇ done
24147 ˇ}
24148 "});
24149 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24150 cx.assert_editor_state(indoc! {"
24151 function main() {
24152 #ˇ for item in $items; do
24153 #ˇ while [ -n \"$item\" ]; do
24154 #ˇ if [ \"$value\" -gt 10 ]; then
24155 #ˇ continue
24156 #ˇ elif [ \"$value\" -lt 0 ]; then
24157 #ˇ break
24158 #ˇ else
24159 #ˇ echo \"$item\"
24160 #ˇ fi
24161 #ˇ done
24162 #ˇ done
24163 #ˇ}
24164 "});
24165}
24166
24167#[gpui::test]
24168async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24169 init_test(cx, |_| {});
24170
24171 let mut cx = EditorTestContext::new(cx).await;
24172 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24173 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24174
24175 // test `else` auto outdents when typed inside `if` block
24176 cx.set_state(indoc! {"
24177 if [ \"$1\" = \"test\" ]; then
24178 echo \"foo bar\"
24179 ˇ
24180 "});
24181 cx.update_editor(|editor, window, cx| {
24182 editor.handle_input("else", window, cx);
24183 });
24184 cx.assert_editor_state(indoc! {"
24185 if [ \"$1\" = \"test\" ]; then
24186 echo \"foo bar\"
24187 elseˇ
24188 "});
24189
24190 // test `elif` auto outdents when typed inside `if` block
24191 cx.set_state(indoc! {"
24192 if [ \"$1\" = \"test\" ]; then
24193 echo \"foo bar\"
24194 ˇ
24195 "});
24196 cx.update_editor(|editor, window, cx| {
24197 editor.handle_input("elif", window, cx);
24198 });
24199 cx.assert_editor_state(indoc! {"
24200 if [ \"$1\" = \"test\" ]; then
24201 echo \"foo bar\"
24202 elifˇ
24203 "});
24204
24205 // test `fi` auto outdents when typed inside `else` block
24206 cx.set_state(indoc! {"
24207 if [ \"$1\" = \"test\" ]; then
24208 echo \"foo bar\"
24209 else
24210 echo \"bar baz\"
24211 ˇ
24212 "});
24213 cx.update_editor(|editor, window, cx| {
24214 editor.handle_input("fi", window, cx);
24215 });
24216 cx.assert_editor_state(indoc! {"
24217 if [ \"$1\" = \"test\" ]; then
24218 echo \"foo bar\"
24219 else
24220 echo \"bar baz\"
24221 fiˇ
24222 "});
24223
24224 // test `done` auto outdents when typed inside `while` block
24225 cx.set_state(indoc! {"
24226 while read line; do
24227 echo \"$line\"
24228 ˇ
24229 "});
24230 cx.update_editor(|editor, window, cx| {
24231 editor.handle_input("done", window, cx);
24232 });
24233 cx.assert_editor_state(indoc! {"
24234 while read line; do
24235 echo \"$line\"
24236 doneˇ
24237 "});
24238
24239 // test `done` auto outdents when typed inside `for` block
24240 cx.set_state(indoc! {"
24241 for file in *.txt; do
24242 cat \"$file\"
24243 ˇ
24244 "});
24245 cx.update_editor(|editor, window, cx| {
24246 editor.handle_input("done", window, cx);
24247 });
24248 cx.assert_editor_state(indoc! {"
24249 for file in *.txt; do
24250 cat \"$file\"
24251 doneˇ
24252 "});
24253
24254 // test `esac` auto outdents when typed inside `case` block
24255 cx.set_state(indoc! {"
24256 case \"$1\" in
24257 start)
24258 echo \"foo bar\"
24259 ;;
24260 stop)
24261 echo \"bar baz\"
24262 ;;
24263 ˇ
24264 "});
24265 cx.update_editor(|editor, window, cx| {
24266 editor.handle_input("esac", window, cx);
24267 });
24268 cx.assert_editor_state(indoc! {"
24269 case \"$1\" in
24270 start)
24271 echo \"foo bar\"
24272 ;;
24273 stop)
24274 echo \"bar baz\"
24275 ;;
24276 esacˇ
24277 "});
24278
24279 // test `*)` auto outdents when typed inside `case` block
24280 cx.set_state(indoc! {"
24281 case \"$1\" in
24282 start)
24283 echo \"foo bar\"
24284 ;;
24285 ˇ
24286 "});
24287 cx.update_editor(|editor, window, cx| {
24288 editor.handle_input("*)", window, cx);
24289 });
24290 cx.assert_editor_state(indoc! {"
24291 case \"$1\" in
24292 start)
24293 echo \"foo bar\"
24294 ;;
24295 *)ˇ
24296 "});
24297
24298 // test `fi` outdents to correct level with nested if blocks
24299 cx.set_state(indoc! {"
24300 if [ \"$1\" = \"test\" ]; then
24301 echo \"outer if\"
24302 if [ \"$2\" = \"debug\" ]; then
24303 echo \"inner if\"
24304 ˇ
24305 "});
24306 cx.update_editor(|editor, window, cx| {
24307 editor.handle_input("fi", window, cx);
24308 });
24309 cx.assert_editor_state(indoc! {"
24310 if [ \"$1\" = \"test\" ]; then
24311 echo \"outer if\"
24312 if [ \"$2\" = \"debug\" ]; then
24313 echo \"inner if\"
24314 fiˇ
24315 "});
24316}
24317
24318#[gpui::test]
24319async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24320 init_test(cx, |_| {});
24321 update_test_language_settings(cx, |settings| {
24322 settings.defaults.extend_comment_on_newline = Some(false);
24323 });
24324 let mut cx = EditorTestContext::new(cx).await;
24325 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24326 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24327
24328 // test correct indent after newline on comment
24329 cx.set_state(indoc! {"
24330 # COMMENT:ˇ
24331 "});
24332 cx.update_editor(|editor, window, cx| {
24333 editor.newline(&Newline, window, cx);
24334 });
24335 cx.assert_editor_state(indoc! {"
24336 # COMMENT:
24337 ˇ
24338 "});
24339
24340 // test correct indent after newline after `then`
24341 cx.set_state(indoc! {"
24342
24343 if [ \"$1\" = \"test\" ]; thenˇ
24344 "});
24345 cx.update_editor(|editor, window, cx| {
24346 editor.newline(&Newline, window, cx);
24347 });
24348 cx.run_until_parked();
24349 cx.assert_editor_state(indoc! {"
24350
24351 if [ \"$1\" = \"test\" ]; then
24352 ˇ
24353 "});
24354
24355 // test correct indent after newline after `else`
24356 cx.set_state(indoc! {"
24357 if [ \"$1\" = \"test\" ]; then
24358 elseˇ
24359 "});
24360 cx.update_editor(|editor, window, cx| {
24361 editor.newline(&Newline, window, cx);
24362 });
24363 cx.run_until_parked();
24364 cx.assert_editor_state(indoc! {"
24365 if [ \"$1\" = \"test\" ]; then
24366 else
24367 ˇ
24368 "});
24369
24370 // test correct indent after newline after `elif`
24371 cx.set_state(indoc! {"
24372 if [ \"$1\" = \"test\" ]; then
24373 elifˇ
24374 "});
24375 cx.update_editor(|editor, window, cx| {
24376 editor.newline(&Newline, window, cx);
24377 });
24378 cx.run_until_parked();
24379 cx.assert_editor_state(indoc! {"
24380 if [ \"$1\" = \"test\" ]; then
24381 elif
24382 ˇ
24383 "});
24384
24385 // test correct indent after newline after `do`
24386 cx.set_state(indoc! {"
24387 for file in *.txt; doˇ
24388 "});
24389 cx.update_editor(|editor, window, cx| {
24390 editor.newline(&Newline, window, cx);
24391 });
24392 cx.run_until_parked();
24393 cx.assert_editor_state(indoc! {"
24394 for file in *.txt; do
24395 ˇ
24396 "});
24397
24398 // test correct indent after newline after case pattern
24399 cx.set_state(indoc! {"
24400 case \"$1\" in
24401 start)ˇ
24402 "});
24403 cx.update_editor(|editor, window, cx| {
24404 editor.newline(&Newline, window, cx);
24405 });
24406 cx.run_until_parked();
24407 cx.assert_editor_state(indoc! {"
24408 case \"$1\" in
24409 start)
24410 ˇ
24411 "});
24412
24413 // test correct indent after newline after case pattern
24414 cx.set_state(indoc! {"
24415 case \"$1\" in
24416 start)
24417 ;;
24418 *)ˇ
24419 "});
24420 cx.update_editor(|editor, window, cx| {
24421 editor.newline(&Newline, window, cx);
24422 });
24423 cx.run_until_parked();
24424 cx.assert_editor_state(indoc! {"
24425 case \"$1\" in
24426 start)
24427 ;;
24428 *)
24429 ˇ
24430 "});
24431
24432 // test correct indent after newline after function opening brace
24433 cx.set_state(indoc! {"
24434 function test() {ˇ}
24435 "});
24436 cx.update_editor(|editor, window, cx| {
24437 editor.newline(&Newline, window, cx);
24438 });
24439 cx.run_until_parked();
24440 cx.assert_editor_state(indoc! {"
24441 function test() {
24442 ˇ
24443 }
24444 "});
24445
24446 // test no extra indent after semicolon on same line
24447 cx.set_state(indoc! {"
24448 echo \"test\";ˇ
24449 "});
24450 cx.update_editor(|editor, window, cx| {
24451 editor.newline(&Newline, window, cx);
24452 });
24453 cx.run_until_parked();
24454 cx.assert_editor_state(indoc! {"
24455 echo \"test\";
24456 ˇ
24457 "});
24458}
24459
24460fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24461 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24462 point..point
24463}
24464
24465#[track_caller]
24466fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24467 let (text, ranges) = marked_text_ranges(marked_text, true);
24468 assert_eq!(editor.text(cx), text);
24469 assert_eq!(
24470 editor.selections.ranges(cx),
24471 ranges,
24472 "Assert selections are {}",
24473 marked_text
24474 );
24475}
24476
24477pub fn handle_signature_help_request(
24478 cx: &mut EditorLspTestContext,
24479 mocked_response: lsp::SignatureHelp,
24480) -> impl Future<Output = ()> + use<> {
24481 let mut request =
24482 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24483 let mocked_response = mocked_response.clone();
24484 async move { Ok(Some(mocked_response)) }
24485 });
24486
24487 async move {
24488 request.next().await;
24489 }
24490}
24491
24492#[track_caller]
24493pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24494 cx.update_editor(|editor, _, _| {
24495 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24496 let entries = menu.entries.borrow();
24497 let entries = entries
24498 .iter()
24499 .map(|entry| entry.string.as_str())
24500 .collect::<Vec<_>>();
24501 assert_eq!(entries, expected);
24502 } else {
24503 panic!("Expected completions menu");
24504 }
24505 });
24506}
24507
24508/// Handle completion request passing a marked string specifying where the completion
24509/// should be triggered from using '|' character, what range should be replaced, and what completions
24510/// should be returned using '<' and '>' to delimit the range.
24511///
24512/// Also see `handle_completion_request_with_insert_and_replace`.
24513#[track_caller]
24514pub fn handle_completion_request(
24515 marked_string: &str,
24516 completions: Vec<&'static str>,
24517 is_incomplete: bool,
24518 counter: Arc<AtomicUsize>,
24519 cx: &mut EditorLspTestContext,
24520) -> impl Future<Output = ()> {
24521 let complete_from_marker: TextRangeMarker = '|'.into();
24522 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24523 let (_, mut marked_ranges) = marked_text_ranges_by(
24524 marked_string,
24525 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24526 );
24527
24528 let complete_from_position =
24529 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24530 let replace_range =
24531 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24532
24533 let mut request =
24534 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24535 let completions = completions.clone();
24536 counter.fetch_add(1, atomic::Ordering::Release);
24537 async move {
24538 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24539 assert_eq!(
24540 params.text_document_position.position,
24541 complete_from_position
24542 );
24543 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24544 is_incomplete,
24545 item_defaults: None,
24546 items: completions
24547 .iter()
24548 .map(|completion_text| lsp::CompletionItem {
24549 label: completion_text.to_string(),
24550 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24551 range: replace_range,
24552 new_text: completion_text.to_string(),
24553 })),
24554 ..Default::default()
24555 })
24556 .collect(),
24557 })))
24558 }
24559 });
24560
24561 async move {
24562 request.next().await;
24563 }
24564}
24565
24566/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
24567/// given instead, which also contains an `insert` range.
24568///
24569/// This function uses markers to define ranges:
24570/// - `|` marks the cursor position
24571/// - `<>` marks the replace range
24572/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
24573pub fn handle_completion_request_with_insert_and_replace(
24574 cx: &mut EditorLspTestContext,
24575 marked_string: &str,
24576 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
24577 counter: Arc<AtomicUsize>,
24578) -> impl Future<Output = ()> {
24579 let complete_from_marker: TextRangeMarker = '|'.into();
24580 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24581 let insert_range_marker: TextRangeMarker = ('{', '}').into();
24582
24583 let (_, mut marked_ranges) = marked_text_ranges_by(
24584 marked_string,
24585 vec![
24586 complete_from_marker.clone(),
24587 replace_range_marker.clone(),
24588 insert_range_marker.clone(),
24589 ],
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 insert_range = match marked_ranges.remove(&insert_range_marker) {
24598 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
24599 _ => lsp::Range {
24600 start: replace_range.start,
24601 end: complete_from_position,
24602 },
24603 };
24604
24605 let mut request =
24606 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24607 let completions = completions.clone();
24608 counter.fetch_add(1, atomic::Ordering::Release);
24609 async move {
24610 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24611 assert_eq!(
24612 params.text_document_position.position, complete_from_position,
24613 "marker `|` position doesn't match",
24614 );
24615 Ok(Some(lsp::CompletionResponse::Array(
24616 completions
24617 .iter()
24618 .map(|(label, new_text)| lsp::CompletionItem {
24619 label: label.to_string(),
24620 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24621 lsp::InsertReplaceEdit {
24622 insert: insert_range,
24623 replace: replace_range,
24624 new_text: new_text.to_string(),
24625 },
24626 )),
24627 ..Default::default()
24628 })
24629 .collect(),
24630 )))
24631 }
24632 });
24633
24634 async move {
24635 request.next().await;
24636 }
24637}
24638
24639fn handle_resolve_completion_request(
24640 cx: &mut EditorLspTestContext,
24641 edits: Option<Vec<(&'static str, &'static str)>>,
24642) -> impl Future<Output = ()> {
24643 let edits = edits.map(|edits| {
24644 edits
24645 .iter()
24646 .map(|(marked_string, new_text)| {
24647 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
24648 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
24649 lsp::TextEdit::new(replace_range, new_text.to_string())
24650 })
24651 .collect::<Vec<_>>()
24652 });
24653
24654 let mut request =
24655 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
24656 let edits = edits.clone();
24657 async move {
24658 Ok(lsp::CompletionItem {
24659 additional_text_edits: edits,
24660 ..Default::default()
24661 })
24662 }
24663 });
24664
24665 async move {
24666 request.next().await;
24667 }
24668}
24669
24670pub(crate) fn update_test_language_settings(
24671 cx: &mut TestAppContext,
24672 f: impl Fn(&mut AllLanguageSettingsContent),
24673) {
24674 cx.update(|cx| {
24675 SettingsStore::update_global(cx, |store, cx| {
24676 store.update_user_settings::<AllLanguageSettings>(cx, f);
24677 });
24678 });
24679}
24680
24681pub(crate) fn update_test_project_settings(
24682 cx: &mut TestAppContext,
24683 f: impl Fn(&mut ProjectSettings),
24684) {
24685 cx.update(|cx| {
24686 SettingsStore::update_global(cx, |store, cx| {
24687 store.update_user_settings::<ProjectSettings>(cx, f);
24688 });
24689 });
24690}
24691
24692pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
24693 cx.update(|cx| {
24694 assets::Assets.load_test_fonts(cx);
24695 let store = SettingsStore::test(cx);
24696 cx.set_global(store);
24697 theme::init(theme::LoadThemes::JustBase, cx);
24698 release_channel::init(SemanticVersion::default(), cx);
24699 client::init_settings(cx);
24700 language::init(cx);
24701 Project::init_settings(cx);
24702 workspace::init_settings(cx);
24703 crate::init(cx);
24704 });
24705 zlog::init_test();
24706 update_test_language_settings(cx, f);
24707}
24708
24709#[track_caller]
24710fn assert_hunk_revert(
24711 not_reverted_text_with_selections: &str,
24712 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
24713 expected_reverted_text_with_selections: &str,
24714 base_text: &str,
24715 cx: &mut EditorLspTestContext,
24716) {
24717 cx.set_state(not_reverted_text_with_selections);
24718 cx.set_head_text(base_text);
24719 cx.executor().run_until_parked();
24720
24721 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
24722 let snapshot = editor.snapshot(window, cx);
24723 let reverted_hunk_statuses = snapshot
24724 .buffer_snapshot
24725 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
24726 .map(|hunk| hunk.status().kind)
24727 .collect::<Vec<_>>();
24728
24729 editor.git_restore(&Default::default(), window, cx);
24730 reverted_hunk_statuses
24731 });
24732 cx.executor().run_until_parked();
24733 cx.assert_editor_state(expected_reverted_text_with_selections);
24734 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
24735}
24736
24737#[gpui::test(iterations = 10)]
24738async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
24739 init_test(cx, |_| {});
24740
24741 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
24742 let counter = diagnostic_requests.clone();
24743
24744 let fs = FakeFs::new(cx.executor());
24745 fs.insert_tree(
24746 path!("/a"),
24747 json!({
24748 "first.rs": "fn main() { let a = 5; }",
24749 "second.rs": "// Test file",
24750 }),
24751 )
24752 .await;
24753
24754 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24755 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24756 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24757
24758 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24759 language_registry.add(rust_lang());
24760 let mut fake_servers = language_registry.register_fake_lsp(
24761 "Rust",
24762 FakeLspAdapter {
24763 capabilities: lsp::ServerCapabilities {
24764 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
24765 lsp::DiagnosticOptions {
24766 identifier: None,
24767 inter_file_dependencies: true,
24768 workspace_diagnostics: true,
24769 work_done_progress_options: Default::default(),
24770 },
24771 )),
24772 ..Default::default()
24773 },
24774 ..Default::default()
24775 },
24776 );
24777
24778 let editor = workspace
24779 .update(cx, |workspace, window, cx| {
24780 workspace.open_abs_path(
24781 PathBuf::from(path!("/a/first.rs")),
24782 OpenOptions::default(),
24783 window,
24784 cx,
24785 )
24786 })
24787 .unwrap()
24788 .await
24789 .unwrap()
24790 .downcast::<Editor>()
24791 .unwrap();
24792 let fake_server = fake_servers.next().await.unwrap();
24793 let server_id = fake_server.server.server_id();
24794 let mut first_request = fake_server
24795 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
24796 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
24797 let result_id = Some(new_result_id.to_string());
24798 assert_eq!(
24799 params.text_document.uri,
24800 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24801 );
24802 async move {
24803 Ok(lsp::DocumentDiagnosticReportResult::Report(
24804 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
24805 related_documents: None,
24806 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
24807 items: Vec::new(),
24808 result_id,
24809 },
24810 }),
24811 ))
24812 }
24813 });
24814
24815 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
24816 project.update(cx, |project, cx| {
24817 let buffer_id = editor
24818 .read(cx)
24819 .buffer()
24820 .read(cx)
24821 .as_singleton()
24822 .expect("created a singleton buffer")
24823 .read(cx)
24824 .remote_id();
24825 let buffer_result_id = project
24826 .lsp_store()
24827 .read(cx)
24828 .result_id(server_id, buffer_id, cx);
24829 assert_eq!(expected, buffer_result_id);
24830 });
24831 };
24832
24833 ensure_result_id(None, cx);
24834 cx.executor().advance_clock(Duration::from_millis(60));
24835 cx.executor().run_until_parked();
24836 assert_eq!(
24837 diagnostic_requests.load(atomic::Ordering::Acquire),
24838 1,
24839 "Opening file should trigger diagnostic request"
24840 );
24841 first_request
24842 .next()
24843 .await
24844 .expect("should have sent the first diagnostics pull request");
24845 ensure_result_id(Some("1".to_string()), cx);
24846
24847 // Editing should trigger diagnostics
24848 editor.update_in(cx, |editor, window, cx| {
24849 editor.handle_input("2", window, cx)
24850 });
24851 cx.executor().advance_clock(Duration::from_millis(60));
24852 cx.executor().run_until_parked();
24853 assert_eq!(
24854 diagnostic_requests.load(atomic::Ordering::Acquire),
24855 2,
24856 "Editing should trigger diagnostic request"
24857 );
24858 ensure_result_id(Some("2".to_string()), cx);
24859
24860 // Moving cursor should not trigger diagnostic request
24861 editor.update_in(cx, |editor, window, cx| {
24862 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24863 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
24864 });
24865 });
24866 cx.executor().advance_clock(Duration::from_millis(60));
24867 cx.executor().run_until_parked();
24868 assert_eq!(
24869 diagnostic_requests.load(atomic::Ordering::Acquire),
24870 2,
24871 "Cursor movement should not trigger diagnostic request"
24872 );
24873 ensure_result_id(Some("2".to_string()), cx);
24874 // Multiple rapid edits should be debounced
24875 for _ in 0..5 {
24876 editor.update_in(cx, |editor, window, cx| {
24877 editor.handle_input("x", window, cx)
24878 });
24879 }
24880 cx.executor().advance_clock(Duration::from_millis(60));
24881 cx.executor().run_until_parked();
24882
24883 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
24884 assert!(
24885 final_requests <= 4,
24886 "Multiple rapid edits should be debounced (got {final_requests} requests)",
24887 );
24888 ensure_result_id(Some(final_requests.to_string()), cx);
24889}
24890
24891#[gpui::test]
24892async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
24893 // Regression test for issue #11671
24894 // Previously, adding a cursor after moving multiple cursors would reset
24895 // the cursor count instead of adding to the existing cursors.
24896 init_test(cx, |_| {});
24897 let mut cx = EditorTestContext::new(cx).await;
24898
24899 // Create a simple buffer with cursor at start
24900 cx.set_state(indoc! {"
24901 ˇaaaa
24902 bbbb
24903 cccc
24904 dddd
24905 eeee
24906 ffff
24907 gggg
24908 hhhh"});
24909
24910 // Add 2 cursors below (so we have 3 total)
24911 cx.update_editor(|editor, window, cx| {
24912 editor.add_selection_below(&Default::default(), window, cx);
24913 editor.add_selection_below(&Default::default(), window, cx);
24914 });
24915
24916 // Verify we have 3 cursors
24917 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24918 assert_eq!(
24919 initial_count, 3,
24920 "Should have 3 cursors after adding 2 below"
24921 );
24922
24923 // Move down one line
24924 cx.update_editor(|editor, window, cx| {
24925 editor.move_down(&MoveDown, window, cx);
24926 });
24927
24928 // Add another cursor below
24929 cx.update_editor(|editor, window, cx| {
24930 editor.add_selection_below(&Default::default(), window, cx);
24931 });
24932
24933 // Should now have 4 cursors (3 original + 1 new)
24934 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24935 assert_eq!(
24936 final_count, 4,
24937 "Should have 4 cursors after moving and adding another"
24938 );
24939}
24940
24941#[gpui::test(iterations = 10)]
24942async fn test_document_colors(cx: &mut TestAppContext) {
24943 let expected_color = Rgba {
24944 r: 0.33,
24945 g: 0.33,
24946 b: 0.33,
24947 a: 0.33,
24948 };
24949
24950 init_test(cx, |_| {});
24951
24952 let fs = FakeFs::new(cx.executor());
24953 fs.insert_tree(
24954 path!("/a"),
24955 json!({
24956 "first.rs": "fn main() { let a = 5; }",
24957 }),
24958 )
24959 .await;
24960
24961 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24962 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24963 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24964
24965 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24966 language_registry.add(rust_lang());
24967 let mut fake_servers = language_registry.register_fake_lsp(
24968 "Rust",
24969 FakeLspAdapter {
24970 capabilities: lsp::ServerCapabilities {
24971 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24972 ..lsp::ServerCapabilities::default()
24973 },
24974 name: "rust-analyzer",
24975 ..FakeLspAdapter::default()
24976 },
24977 );
24978 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24979 "Rust",
24980 FakeLspAdapter {
24981 capabilities: lsp::ServerCapabilities {
24982 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24983 ..lsp::ServerCapabilities::default()
24984 },
24985 name: "not-rust-analyzer",
24986 ..FakeLspAdapter::default()
24987 },
24988 );
24989
24990 let editor = workspace
24991 .update(cx, |workspace, window, cx| {
24992 workspace.open_abs_path(
24993 PathBuf::from(path!("/a/first.rs")),
24994 OpenOptions::default(),
24995 window,
24996 cx,
24997 )
24998 })
24999 .unwrap()
25000 .await
25001 .unwrap()
25002 .downcast::<Editor>()
25003 .unwrap();
25004 let fake_language_server = fake_servers.next().await.unwrap();
25005 let fake_language_server_without_capabilities =
25006 fake_servers_without_capabilities.next().await.unwrap();
25007 let requests_made = Arc::new(AtomicUsize::new(0));
25008 let closure_requests_made = Arc::clone(&requests_made);
25009 let mut color_request_handle = fake_language_server
25010 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25011 let requests_made = Arc::clone(&closure_requests_made);
25012 async move {
25013 assert_eq!(
25014 params.text_document.uri,
25015 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25016 );
25017 requests_made.fetch_add(1, atomic::Ordering::Release);
25018 Ok(vec![
25019 lsp::ColorInformation {
25020 range: lsp::Range {
25021 start: lsp::Position {
25022 line: 0,
25023 character: 0,
25024 },
25025 end: lsp::Position {
25026 line: 0,
25027 character: 1,
25028 },
25029 },
25030 color: lsp::Color {
25031 red: 0.33,
25032 green: 0.33,
25033 blue: 0.33,
25034 alpha: 0.33,
25035 },
25036 },
25037 lsp::ColorInformation {
25038 range: lsp::Range {
25039 start: lsp::Position {
25040 line: 0,
25041 character: 0,
25042 },
25043 end: lsp::Position {
25044 line: 0,
25045 character: 1,
25046 },
25047 },
25048 color: lsp::Color {
25049 red: 0.33,
25050 green: 0.33,
25051 blue: 0.33,
25052 alpha: 0.33,
25053 },
25054 },
25055 ])
25056 }
25057 });
25058
25059 let _handle = fake_language_server_without_capabilities
25060 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25061 panic!("Should not be called");
25062 });
25063 cx.executor().advance_clock(Duration::from_millis(100));
25064 color_request_handle.next().await.unwrap();
25065 cx.run_until_parked();
25066 assert_eq!(
25067 1,
25068 requests_made.load(atomic::Ordering::Acquire),
25069 "Should query for colors once per editor open"
25070 );
25071 editor.update_in(cx, |editor, _, cx| {
25072 assert_eq!(
25073 vec![expected_color],
25074 extract_color_inlays(editor, cx),
25075 "Should have an initial inlay"
25076 );
25077 });
25078
25079 // opening another file in a split should not influence the LSP query counter
25080 workspace
25081 .update(cx, |workspace, window, cx| {
25082 assert_eq!(
25083 workspace.panes().len(),
25084 1,
25085 "Should have one pane with one editor"
25086 );
25087 workspace.move_item_to_pane_in_direction(
25088 &MoveItemToPaneInDirection {
25089 direction: SplitDirection::Right,
25090 focus: false,
25091 clone: true,
25092 },
25093 window,
25094 cx,
25095 );
25096 })
25097 .unwrap();
25098 cx.run_until_parked();
25099 workspace
25100 .update(cx, |workspace, _, cx| {
25101 let panes = workspace.panes();
25102 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25103 for pane in panes {
25104 let editor = pane
25105 .read(cx)
25106 .active_item()
25107 .and_then(|item| item.downcast::<Editor>())
25108 .expect("Should have opened an editor in each split");
25109 let editor_file = editor
25110 .read(cx)
25111 .buffer()
25112 .read(cx)
25113 .as_singleton()
25114 .expect("test deals with singleton buffers")
25115 .read(cx)
25116 .file()
25117 .expect("test buffese should have a file")
25118 .path();
25119 assert_eq!(
25120 editor_file.as_ref(),
25121 Path::new("first.rs"),
25122 "Both editors should be opened for the same file"
25123 )
25124 }
25125 })
25126 .unwrap();
25127
25128 cx.executor().advance_clock(Duration::from_millis(500));
25129 let save = editor.update_in(cx, |editor, window, cx| {
25130 editor.move_to_end(&MoveToEnd, window, cx);
25131 editor.handle_input("dirty", window, cx);
25132 editor.save(
25133 SaveOptions {
25134 format: true,
25135 autosave: true,
25136 },
25137 project.clone(),
25138 window,
25139 cx,
25140 )
25141 });
25142 save.await.unwrap();
25143
25144 color_request_handle.next().await.unwrap();
25145 cx.run_until_parked();
25146 assert_eq!(
25147 3,
25148 requests_made.load(atomic::Ordering::Acquire),
25149 "Should query for colors once per save and once per formatting after save"
25150 );
25151
25152 drop(editor);
25153 let close = workspace
25154 .update(cx, |workspace, window, cx| {
25155 workspace.active_pane().update(cx, |pane, cx| {
25156 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25157 })
25158 })
25159 .unwrap();
25160 close.await.unwrap();
25161 let close = workspace
25162 .update(cx, |workspace, window, cx| {
25163 workspace.active_pane().update(cx, |pane, cx| {
25164 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25165 })
25166 })
25167 .unwrap();
25168 close.await.unwrap();
25169 assert_eq!(
25170 3,
25171 requests_made.load(atomic::Ordering::Acquire),
25172 "After saving and closing all editors, no extra requests should be made"
25173 );
25174 workspace
25175 .update(cx, |workspace, _, cx| {
25176 assert!(
25177 workspace.active_item(cx).is_none(),
25178 "Should close all editors"
25179 )
25180 })
25181 .unwrap();
25182
25183 workspace
25184 .update(cx, |workspace, window, cx| {
25185 workspace.active_pane().update(cx, |pane, cx| {
25186 pane.navigate_backward(&Default::default(), window, cx);
25187 })
25188 })
25189 .unwrap();
25190 cx.executor().advance_clock(Duration::from_millis(100));
25191 cx.run_until_parked();
25192 let editor = workspace
25193 .update(cx, |workspace, _, cx| {
25194 workspace
25195 .active_item(cx)
25196 .expect("Should have reopened the editor again after navigating back")
25197 .downcast::<Editor>()
25198 .expect("Should be an editor")
25199 })
25200 .unwrap();
25201 color_request_handle.next().await.unwrap();
25202 assert_eq!(
25203 3,
25204 requests_made.load(atomic::Ordering::Acquire),
25205 "Cache should be reused on buffer close and reopen"
25206 );
25207 editor.update(cx, |editor, cx| {
25208 assert_eq!(
25209 vec![expected_color],
25210 extract_color_inlays(editor, cx),
25211 "Should have an initial inlay"
25212 );
25213 });
25214}
25215
25216#[gpui::test]
25217async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25218 init_test(cx, |_| {});
25219 let (editor, cx) = cx.add_window_view(Editor::single_line);
25220 editor.update_in(cx, |editor, window, cx| {
25221 editor.set_text("oops\n\nwow\n", window, cx)
25222 });
25223 cx.run_until_parked();
25224 editor.update(cx, |editor, cx| {
25225 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25226 });
25227 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25228 cx.run_until_parked();
25229 editor.update(cx, |editor, cx| {
25230 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25231 });
25232}
25233
25234#[gpui::test]
25235async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25236 init_test(cx, |_| {});
25237
25238 cx.update(|cx| {
25239 register_project_item::<Editor>(cx);
25240 });
25241
25242 let fs = FakeFs::new(cx.executor());
25243 fs.insert_tree("/root1", json!({})).await;
25244 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25245 .await;
25246
25247 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25248 let (workspace, cx) =
25249 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25250
25251 let worktree_id = project.update(cx, |project, cx| {
25252 project.worktrees(cx).next().unwrap().read(cx).id()
25253 });
25254
25255 let handle = workspace
25256 .update_in(cx, |workspace, window, cx| {
25257 let project_path = (worktree_id, "one.pdf");
25258 workspace.open_path(project_path, None, true, window, cx)
25259 })
25260 .await
25261 .unwrap();
25262
25263 assert_eq!(
25264 handle.to_any().entity_type(),
25265 TypeId::of::<InvalidBufferView>()
25266 );
25267}
25268
25269#[track_caller]
25270fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25271 editor
25272 .all_inlays(cx)
25273 .into_iter()
25274 .filter_map(|inlay| inlay.get_color())
25275 .map(Rgba::from)
25276 .collect()
25277}