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_cut_line_ends(cx: &mut TestAppContext) {
6735 init_test(cx, |_| {});
6736
6737 let mut cx = EditorTestContext::new(cx).await;
6738
6739 cx.set_state(indoc! {"
6740 The quick« brownˇ»
6741 fox jumps overˇ
6742 the lazy dog"});
6743 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6744 cx.assert_editor_state(indoc! {"
6745 The quickˇ
6746 ˇthe lazy dog"});
6747
6748 cx.set_state(indoc! {"
6749 The quick« brownˇ»
6750 fox jumps overˇ
6751 the lazy dog"});
6752 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6753 cx.assert_editor_state(indoc! {"
6754 The quickˇ
6755 fox jumps overˇthe lazy dog"});
6756
6757 cx.set_state(indoc! {"
6758 The quick« brownˇ»
6759 fox jumps overˇ
6760 the lazy dog"});
6761 cx.update_editor(|e, window, cx| {
6762 e.cut_to_end_of_line(
6763 &CutToEndOfLine {
6764 stop_at_newlines: true,
6765 },
6766 window,
6767 cx,
6768 )
6769 });
6770 cx.assert_editor_state(indoc! {"
6771 The quickˇ
6772 fox jumps overˇ
6773 the lazy dog"});
6774
6775 cx.set_state(indoc! {"
6776 The quick« brownˇ»
6777 fox jumps overˇ
6778 the lazy dog"});
6779 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6780 cx.assert_editor_state(indoc! {"
6781 The quickˇ
6782 fox jumps overˇthe lazy dog"});
6783}
6784
6785#[gpui::test]
6786async fn test_clipboard(cx: &mut TestAppContext) {
6787 init_test(cx, |_| {});
6788
6789 let mut cx = EditorTestContext::new(cx).await;
6790
6791 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6792 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6793 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6794
6795 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6796 cx.set_state("two ˇfour ˇsix ˇ");
6797 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6798 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6799
6800 // Paste again but with only two cursors. Since the number of cursors doesn't
6801 // match the number of slices in the clipboard, the entire clipboard text
6802 // is pasted at each cursor.
6803 cx.set_state("ˇtwo one✅ four three six five ˇ");
6804 cx.update_editor(|e, window, cx| {
6805 e.handle_input("( ", window, cx);
6806 e.paste(&Paste, window, cx);
6807 e.handle_input(") ", window, cx);
6808 });
6809 cx.assert_editor_state(
6810 &([
6811 "( one✅ ",
6812 "three ",
6813 "five ) ˇtwo one✅ four three six five ( one✅ ",
6814 "three ",
6815 "five ) ˇ",
6816 ]
6817 .join("\n")),
6818 );
6819
6820 // Cut with three selections, one of which is full-line.
6821 cx.set_state(indoc! {"
6822 1«2ˇ»3
6823 4ˇ567
6824 «8ˇ»9"});
6825 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6826 cx.assert_editor_state(indoc! {"
6827 1ˇ3
6828 ˇ9"});
6829
6830 // Paste with three selections, noticing how the copied selection that was full-line
6831 // gets inserted before the second cursor.
6832 cx.set_state(indoc! {"
6833 1ˇ3
6834 9ˇ
6835 «oˇ»ne"});
6836 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6837 cx.assert_editor_state(indoc! {"
6838 12ˇ3
6839 4567
6840 9ˇ
6841 8ˇne"});
6842
6843 // Copy with a single cursor only, which writes the whole line into the clipboard.
6844 cx.set_state(indoc! {"
6845 The quick brown
6846 fox juˇmps over
6847 the lazy dog"});
6848 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6849 assert_eq!(
6850 cx.read_from_clipboard()
6851 .and_then(|item| item.text().as_deref().map(str::to_string)),
6852 Some("fox jumps over\n".to_string())
6853 );
6854
6855 // Paste with three selections, noticing how the copied full-line selection is inserted
6856 // before the empty selections but replaces the selection that is non-empty.
6857 cx.set_state(indoc! {"
6858 Tˇhe quick brown
6859 «foˇ»x jumps over
6860 tˇhe lazy dog"});
6861 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6862 cx.assert_editor_state(indoc! {"
6863 fox jumps over
6864 Tˇhe quick brown
6865 fox jumps over
6866 ˇx jumps over
6867 fox jumps over
6868 tˇhe lazy dog"});
6869}
6870
6871#[gpui::test]
6872async fn test_copy_trim(cx: &mut TestAppContext) {
6873 init_test(cx, |_| {});
6874
6875 let mut cx = EditorTestContext::new(cx).await;
6876 cx.set_state(
6877 r#" «for selection in selections.iter() {
6878 let mut start = selection.start;
6879 let mut end = selection.end;
6880 let is_entire_line = selection.is_empty();
6881 if is_entire_line {
6882 start = Point::new(start.row, 0);ˇ»
6883 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6884 }
6885 "#,
6886 );
6887 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6888 assert_eq!(
6889 cx.read_from_clipboard()
6890 .and_then(|item| item.text().as_deref().map(str::to_string)),
6891 Some(
6892 "for selection in selections.iter() {
6893 let mut start = selection.start;
6894 let mut end = selection.end;
6895 let is_entire_line = selection.is_empty();
6896 if is_entire_line {
6897 start = Point::new(start.row, 0);"
6898 .to_string()
6899 ),
6900 "Regular copying preserves all indentation selected",
6901 );
6902 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6903 assert_eq!(
6904 cx.read_from_clipboard()
6905 .and_then(|item| item.text().as_deref().map(str::to_string)),
6906 Some(
6907 "for selection in selections.iter() {
6908let mut start = selection.start;
6909let mut end = selection.end;
6910let is_entire_line = selection.is_empty();
6911if is_entire_line {
6912 start = Point::new(start.row, 0);"
6913 .to_string()
6914 ),
6915 "Copying with stripping should strip all leading whitespaces"
6916 );
6917
6918 cx.set_state(
6919 r#" « for selection in selections.iter() {
6920 let mut start = selection.start;
6921 let mut end = selection.end;
6922 let is_entire_line = selection.is_empty();
6923 if is_entire_line {
6924 start = Point::new(start.row, 0);ˇ»
6925 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6926 }
6927 "#,
6928 );
6929 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6930 assert_eq!(
6931 cx.read_from_clipboard()
6932 .and_then(|item| item.text().as_deref().map(str::to_string)),
6933 Some(
6934 " for selection in selections.iter() {
6935 let mut start = selection.start;
6936 let mut end = selection.end;
6937 let is_entire_line = selection.is_empty();
6938 if is_entire_line {
6939 start = Point::new(start.row, 0);"
6940 .to_string()
6941 ),
6942 "Regular copying preserves all indentation selected",
6943 );
6944 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6945 assert_eq!(
6946 cx.read_from_clipboard()
6947 .and_then(|item| item.text().as_deref().map(str::to_string)),
6948 Some(
6949 "for selection in selections.iter() {
6950let mut start = selection.start;
6951let mut end = selection.end;
6952let is_entire_line = selection.is_empty();
6953if is_entire_line {
6954 start = Point::new(start.row, 0);"
6955 .to_string()
6956 ),
6957 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6958 );
6959
6960 cx.set_state(
6961 r#" «ˇ for selection in selections.iter() {
6962 let mut start = selection.start;
6963 let mut end = selection.end;
6964 let is_entire_line = selection.is_empty();
6965 if is_entire_line {
6966 start = Point::new(start.row, 0);»
6967 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6968 }
6969 "#,
6970 );
6971 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6972 assert_eq!(
6973 cx.read_from_clipboard()
6974 .and_then(|item| item.text().as_deref().map(str::to_string)),
6975 Some(
6976 " for selection in selections.iter() {
6977 let mut start = selection.start;
6978 let mut end = selection.end;
6979 let is_entire_line = selection.is_empty();
6980 if is_entire_line {
6981 start = Point::new(start.row, 0);"
6982 .to_string()
6983 ),
6984 "Regular copying for reverse selection works the same",
6985 );
6986 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6987 assert_eq!(
6988 cx.read_from_clipboard()
6989 .and_then(|item| item.text().as_deref().map(str::to_string)),
6990 Some(
6991 "for selection in selections.iter() {
6992let mut start = selection.start;
6993let mut end = selection.end;
6994let is_entire_line = selection.is_empty();
6995if is_entire_line {
6996 start = Point::new(start.row, 0);"
6997 .to_string()
6998 ),
6999 "Copying with stripping for reverse selection works the same"
7000 );
7001
7002 cx.set_state(
7003 r#" for selection «in selections.iter() {
7004 let mut start = selection.start;
7005 let mut end = selection.end;
7006 let is_entire_line = selection.is_empty();
7007 if is_entire_line {
7008 start = Point::new(start.row, 0);ˇ»
7009 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7010 }
7011 "#,
7012 );
7013 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7014 assert_eq!(
7015 cx.read_from_clipboard()
7016 .and_then(|item| item.text().as_deref().map(str::to_string)),
7017 Some(
7018 "in selections.iter() {
7019 let mut start = selection.start;
7020 let mut end = selection.end;
7021 let is_entire_line = selection.is_empty();
7022 if is_entire_line {
7023 start = Point::new(start.row, 0);"
7024 .to_string()
7025 ),
7026 "When selecting past the indent, the copying works as usual",
7027 );
7028 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7029 assert_eq!(
7030 cx.read_from_clipboard()
7031 .and_then(|item| item.text().as_deref().map(str::to_string)),
7032 Some(
7033 "in selections.iter() {
7034 let mut start = selection.start;
7035 let mut end = selection.end;
7036 let is_entire_line = selection.is_empty();
7037 if is_entire_line {
7038 start = Point::new(start.row, 0);"
7039 .to_string()
7040 ),
7041 "When selecting past the indent, nothing is trimmed"
7042 );
7043
7044 cx.set_state(
7045 r#" «for selection in selections.iter() {
7046 let mut start = selection.start;
7047
7048 let mut end = selection.end;
7049 let is_entire_line = selection.is_empty();
7050 if is_entire_line {
7051 start = Point::new(start.row, 0);
7052ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7053 }
7054 "#,
7055 );
7056 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7057 assert_eq!(
7058 cx.read_from_clipboard()
7059 .and_then(|item| item.text().as_deref().map(str::to_string)),
7060 Some(
7061 "for selection in selections.iter() {
7062let mut start = selection.start;
7063
7064let mut end = selection.end;
7065let is_entire_line = selection.is_empty();
7066if is_entire_line {
7067 start = Point::new(start.row, 0);
7068"
7069 .to_string()
7070 ),
7071 "Copying with stripping should ignore empty lines"
7072 );
7073}
7074
7075#[gpui::test]
7076async fn test_paste_multiline(cx: &mut TestAppContext) {
7077 init_test(cx, |_| {});
7078
7079 let mut cx = EditorTestContext::new(cx).await;
7080 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7081
7082 // Cut an indented block, without the leading whitespace.
7083 cx.set_state(indoc! {"
7084 const a: B = (
7085 c(),
7086 «d(
7087 e,
7088 f
7089 )ˇ»
7090 );
7091 "});
7092 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7093 cx.assert_editor_state(indoc! {"
7094 const a: B = (
7095 c(),
7096 ˇ
7097 );
7098 "});
7099
7100 // Paste it at the same position.
7101 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7102 cx.assert_editor_state(indoc! {"
7103 const a: B = (
7104 c(),
7105 d(
7106 e,
7107 f
7108 )ˇ
7109 );
7110 "});
7111
7112 // Paste it at a line with a lower indent level.
7113 cx.set_state(indoc! {"
7114 ˇ
7115 const a: B = (
7116 c(),
7117 );
7118 "});
7119 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7120 cx.assert_editor_state(indoc! {"
7121 d(
7122 e,
7123 f
7124 )ˇ
7125 const a: B = (
7126 c(),
7127 );
7128 "});
7129
7130 // Cut an indented block, with the leading whitespace.
7131 cx.set_state(indoc! {"
7132 const a: B = (
7133 c(),
7134 « d(
7135 e,
7136 f
7137 )
7138 ˇ»);
7139 "});
7140 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7141 cx.assert_editor_state(indoc! {"
7142 const a: B = (
7143 c(),
7144 ˇ);
7145 "});
7146
7147 // Paste it at the same position.
7148 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7149 cx.assert_editor_state(indoc! {"
7150 const a: B = (
7151 c(),
7152 d(
7153 e,
7154 f
7155 )
7156 ˇ);
7157 "});
7158
7159 // Paste it at a line with a higher indent level.
7160 cx.set_state(indoc! {"
7161 const a: B = (
7162 c(),
7163 d(
7164 e,
7165 fˇ
7166 )
7167 );
7168 "});
7169 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7170 cx.assert_editor_state(indoc! {"
7171 const a: B = (
7172 c(),
7173 d(
7174 e,
7175 f d(
7176 e,
7177 f
7178 )
7179 ˇ
7180 )
7181 );
7182 "});
7183
7184 // Copy an indented block, starting mid-line
7185 cx.set_state(indoc! {"
7186 const a: B = (
7187 c(),
7188 somethin«g(
7189 e,
7190 f
7191 )ˇ»
7192 );
7193 "});
7194 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7195
7196 // Paste it on a line with a lower indent level
7197 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7198 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7199 cx.assert_editor_state(indoc! {"
7200 const a: B = (
7201 c(),
7202 something(
7203 e,
7204 f
7205 )
7206 );
7207 g(
7208 e,
7209 f
7210 )ˇ"});
7211}
7212
7213#[gpui::test]
7214async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7215 init_test(cx, |_| {});
7216
7217 cx.write_to_clipboard(ClipboardItem::new_string(
7218 " d(\n e\n );\n".into(),
7219 ));
7220
7221 let mut cx = EditorTestContext::new(cx).await;
7222 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7223
7224 cx.set_state(indoc! {"
7225 fn a() {
7226 b();
7227 if c() {
7228 ˇ
7229 }
7230 }
7231 "});
7232
7233 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7234 cx.assert_editor_state(indoc! {"
7235 fn a() {
7236 b();
7237 if c() {
7238 d(
7239 e
7240 );
7241 ˇ
7242 }
7243 }
7244 "});
7245
7246 cx.set_state(indoc! {"
7247 fn a() {
7248 b();
7249 ˇ
7250 }
7251 "});
7252
7253 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7254 cx.assert_editor_state(indoc! {"
7255 fn a() {
7256 b();
7257 d(
7258 e
7259 );
7260 ˇ
7261 }
7262 "});
7263}
7264
7265#[gpui::test]
7266fn test_select_all(cx: &mut TestAppContext) {
7267 init_test(cx, |_| {});
7268
7269 let editor = cx.add_window(|window, cx| {
7270 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7271 build_editor(buffer, window, cx)
7272 });
7273 _ = editor.update(cx, |editor, window, cx| {
7274 editor.select_all(&SelectAll, window, cx);
7275 assert_eq!(
7276 editor.selections.display_ranges(cx),
7277 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7278 );
7279 });
7280}
7281
7282#[gpui::test]
7283fn test_select_line(cx: &mut TestAppContext) {
7284 init_test(cx, |_| {});
7285
7286 let editor = cx.add_window(|window, cx| {
7287 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7288 build_editor(buffer, window, cx)
7289 });
7290 _ = editor.update(cx, |editor, window, cx| {
7291 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7292 s.select_display_ranges([
7293 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7294 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7295 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7296 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7297 ])
7298 });
7299 editor.select_line(&SelectLine, window, cx);
7300 assert_eq!(
7301 editor.selections.display_ranges(cx),
7302 vec![
7303 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7304 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7305 ]
7306 );
7307 });
7308
7309 _ = editor.update(cx, |editor, window, cx| {
7310 editor.select_line(&SelectLine, window, cx);
7311 assert_eq!(
7312 editor.selections.display_ranges(cx),
7313 vec![
7314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7315 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7316 ]
7317 );
7318 });
7319
7320 _ = editor.update(cx, |editor, window, cx| {
7321 editor.select_line(&SelectLine, window, cx);
7322 assert_eq!(
7323 editor.selections.display_ranges(cx),
7324 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7325 );
7326 });
7327}
7328
7329#[gpui::test]
7330async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7331 init_test(cx, |_| {});
7332 let mut cx = EditorTestContext::new(cx).await;
7333
7334 #[track_caller]
7335 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7336 cx.set_state(initial_state);
7337 cx.update_editor(|e, window, cx| {
7338 e.split_selection_into_lines(&Default::default(), window, cx)
7339 });
7340 cx.assert_editor_state(expected_state);
7341 }
7342
7343 // Selection starts and ends at the middle of lines, left-to-right
7344 test(
7345 &mut cx,
7346 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7347 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7348 );
7349 // Same thing, right-to-left
7350 test(
7351 &mut cx,
7352 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7353 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7354 );
7355
7356 // Whole buffer, left-to-right, last line *doesn't* end with newline
7357 test(
7358 &mut cx,
7359 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7360 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7361 );
7362 // Same thing, right-to-left
7363 test(
7364 &mut cx,
7365 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7366 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7367 );
7368
7369 // Whole buffer, left-to-right, last line ends with newline
7370 test(
7371 &mut cx,
7372 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7373 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7374 );
7375 // Same thing, right-to-left
7376 test(
7377 &mut cx,
7378 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7379 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7380 );
7381
7382 // Starts at the end of a line, ends at the start of another
7383 test(
7384 &mut cx,
7385 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7386 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7387 );
7388}
7389
7390#[gpui::test]
7391async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7392 init_test(cx, |_| {});
7393
7394 let editor = cx.add_window(|window, cx| {
7395 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7396 build_editor(buffer, window, cx)
7397 });
7398
7399 // setup
7400 _ = editor.update(cx, |editor, window, cx| {
7401 editor.fold_creases(
7402 vec![
7403 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7404 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7405 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7406 ],
7407 true,
7408 window,
7409 cx,
7410 );
7411 assert_eq!(
7412 editor.display_text(cx),
7413 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7414 );
7415 });
7416
7417 _ = editor.update(cx, |editor, window, cx| {
7418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7419 s.select_display_ranges([
7420 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7421 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7422 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7423 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7424 ])
7425 });
7426 editor.split_selection_into_lines(&Default::default(), window, cx);
7427 assert_eq!(
7428 editor.display_text(cx),
7429 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7430 );
7431 });
7432 EditorTestContext::for_editor(editor, cx)
7433 .await
7434 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7435
7436 _ = editor.update(cx, |editor, window, cx| {
7437 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7438 s.select_display_ranges([
7439 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7440 ])
7441 });
7442 editor.split_selection_into_lines(&Default::default(), window, cx);
7443 assert_eq!(
7444 editor.display_text(cx),
7445 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7446 );
7447 assert_eq!(
7448 editor.selections.display_ranges(cx),
7449 [
7450 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7451 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7452 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7453 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7454 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7455 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7456 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7457 ]
7458 );
7459 });
7460 EditorTestContext::for_editor(editor, cx)
7461 .await
7462 .assert_editor_state(
7463 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7464 );
7465}
7466
7467#[gpui::test]
7468async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7469 init_test(cx, |_| {});
7470
7471 let mut cx = EditorTestContext::new(cx).await;
7472
7473 cx.set_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.add_selection_above(&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_above(&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.undo_selection(&Default::default(), window, cx);
7523 });
7524
7525 cx.assert_editor_state(indoc!(
7526 r#"abcˇ
7527 defˇghi
7528
7529 jk
7530 nlmo
7531 "#
7532 ));
7533
7534 cx.update_editor(|editor, window, cx| {
7535 editor.redo_selection(&Default::default(), window, cx);
7536 });
7537
7538 cx.assert_editor_state(indoc!(
7539 r#"abc
7540 defˇghi
7541
7542 jk
7543 nlmo
7544 "#
7545 ));
7546
7547 cx.update_editor(|editor, window, cx| {
7548 editor.add_selection_below(&Default::default(), window, cx);
7549 });
7550
7551 cx.assert_editor_state(indoc!(
7552 r#"abc
7553 defˇghi
7554 ˇ
7555 jk
7556 nlmo
7557 "#
7558 ));
7559
7560 cx.update_editor(|editor, window, cx| {
7561 editor.add_selection_below(&Default::default(), window, cx);
7562 });
7563
7564 cx.assert_editor_state(indoc!(
7565 r#"abc
7566 defˇghi
7567 ˇ
7568 jkˇ
7569 nlmo
7570 "#
7571 ));
7572
7573 cx.update_editor(|editor, window, cx| {
7574 editor.add_selection_below(&Default::default(), window, cx);
7575 });
7576
7577 cx.assert_editor_state(indoc!(
7578 r#"abc
7579 defˇghi
7580 ˇ
7581 jkˇ
7582 nlmˇo
7583 "#
7584 ));
7585
7586 cx.update_editor(|editor, window, cx| {
7587 editor.add_selection_below(&Default::default(), window, cx);
7588 });
7589
7590 cx.assert_editor_state(indoc!(
7591 r#"abc
7592 defˇghi
7593 ˇ
7594 jkˇ
7595 nlmˇo
7596 ˇ"#
7597 ));
7598
7599 // change selections
7600 cx.set_state(indoc!(
7601 r#"abc
7602 def«ˇg»hi
7603
7604 jk
7605 nlmo
7606 "#
7607 ));
7608
7609 cx.update_editor(|editor, window, cx| {
7610 editor.add_selection_below(&Default::default(), window, cx);
7611 });
7612
7613 cx.assert_editor_state(indoc!(
7614 r#"abc
7615 def«ˇg»hi
7616
7617 jk
7618 nlm«ˇo»
7619 "#
7620 ));
7621
7622 cx.update_editor(|editor, window, cx| {
7623 editor.add_selection_below(&Default::default(), window, cx);
7624 });
7625
7626 cx.assert_editor_state(indoc!(
7627 r#"abc
7628 def«ˇg»hi
7629
7630 jk
7631 nlm«ˇo»
7632 "#
7633 ));
7634
7635 cx.update_editor(|editor, window, cx| {
7636 editor.add_selection_above(&Default::default(), window, cx);
7637 });
7638
7639 cx.assert_editor_state(indoc!(
7640 r#"abc
7641 def«ˇg»hi
7642
7643 jk
7644 nlmo
7645 "#
7646 ));
7647
7648 cx.update_editor(|editor, window, cx| {
7649 editor.add_selection_above(&Default::default(), window, cx);
7650 });
7651
7652 cx.assert_editor_state(indoc!(
7653 r#"abc
7654 def«ˇg»hi
7655
7656 jk
7657 nlmo
7658 "#
7659 ));
7660
7661 // Change selections again
7662 cx.set_state(indoc!(
7663 r#"a«bc
7664 defgˇ»hi
7665
7666 jk
7667 nlmo
7668 "#
7669 ));
7670
7671 cx.update_editor(|editor, window, cx| {
7672 editor.add_selection_below(&Default::default(), window, cx);
7673 });
7674
7675 cx.assert_editor_state(indoc!(
7676 r#"a«bcˇ»
7677 d«efgˇ»hi
7678
7679 j«kˇ»
7680 nlmo
7681 "#
7682 ));
7683
7684 cx.update_editor(|editor, window, cx| {
7685 editor.add_selection_below(&Default::default(), window, cx);
7686 });
7687 cx.assert_editor_state(indoc!(
7688 r#"a«bcˇ»
7689 d«efgˇ»hi
7690
7691 j«kˇ»
7692 n«lmoˇ»
7693 "#
7694 ));
7695 cx.update_editor(|editor, window, cx| {
7696 editor.add_selection_above(&Default::default(), window, cx);
7697 });
7698
7699 cx.assert_editor_state(indoc!(
7700 r#"a«bcˇ»
7701 d«efgˇ»hi
7702
7703 j«kˇ»
7704 nlmo
7705 "#
7706 ));
7707
7708 // Change selections again
7709 cx.set_state(indoc!(
7710 r#"abc
7711 d«ˇefghi
7712
7713 jk
7714 nlm»o
7715 "#
7716 ));
7717
7718 cx.update_editor(|editor, window, cx| {
7719 editor.add_selection_above(&Default::default(), window, cx);
7720 });
7721
7722 cx.assert_editor_state(indoc!(
7723 r#"a«ˇbc»
7724 d«ˇef»ghi
7725
7726 j«ˇk»
7727 n«ˇlm»o
7728 "#
7729 ));
7730
7731 cx.update_editor(|editor, window, cx| {
7732 editor.add_selection_below(&Default::default(), window, cx);
7733 });
7734
7735 cx.assert_editor_state(indoc!(
7736 r#"abc
7737 d«ˇef»ghi
7738
7739 j«ˇk»
7740 n«ˇlm»o
7741 "#
7742 ));
7743}
7744
7745#[gpui::test]
7746async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7747 init_test(cx, |_| {});
7748 let mut cx = EditorTestContext::new(cx).await;
7749
7750 cx.set_state(indoc!(
7751 r#"line onˇe
7752 liˇne two
7753 line three
7754 line four"#
7755 ));
7756
7757 cx.update_editor(|editor, window, cx| {
7758 editor.add_selection_below(&Default::default(), window, cx);
7759 });
7760
7761 // test multiple cursors expand in the same direction
7762 cx.assert_editor_state(indoc!(
7763 r#"line onˇe
7764 liˇne twˇo
7765 liˇne three
7766 line four"#
7767 ));
7768
7769 cx.update_editor(|editor, window, cx| {
7770 editor.add_selection_below(&Default::default(), window, cx);
7771 });
7772
7773 cx.update_editor(|editor, window, cx| {
7774 editor.add_selection_below(&Default::default(), window, cx);
7775 });
7776
7777 // test multiple cursors expand below overflow
7778 cx.assert_editor_state(indoc!(
7779 r#"line onˇe
7780 liˇne twˇo
7781 liˇne thˇree
7782 liˇne foˇur"#
7783 ));
7784
7785 cx.update_editor(|editor, window, cx| {
7786 editor.add_selection_above(&Default::default(), window, cx);
7787 });
7788
7789 // test multiple cursors retrieves back correctly
7790 cx.assert_editor_state(indoc!(
7791 r#"line onˇe
7792 liˇne twˇo
7793 liˇne thˇree
7794 line four"#
7795 ));
7796
7797 cx.update_editor(|editor, window, cx| {
7798 editor.add_selection_above(&Default::default(), window, cx);
7799 });
7800
7801 cx.update_editor(|editor, window, cx| {
7802 editor.add_selection_above(&Default::default(), window, cx);
7803 });
7804
7805 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7806 cx.assert_editor_state(indoc!(
7807 r#"liˇne onˇe
7808 liˇne two
7809 line three
7810 line four"#
7811 ));
7812
7813 cx.update_editor(|editor, window, cx| {
7814 editor.undo_selection(&Default::default(), window, cx);
7815 });
7816
7817 // test undo
7818 cx.assert_editor_state(indoc!(
7819 r#"line onˇe
7820 liˇne twˇo
7821 line three
7822 line four"#
7823 ));
7824
7825 cx.update_editor(|editor, window, cx| {
7826 editor.redo_selection(&Default::default(), window, cx);
7827 });
7828
7829 // test redo
7830 cx.assert_editor_state(indoc!(
7831 r#"liˇne onˇe
7832 liˇne two
7833 line three
7834 line four"#
7835 ));
7836
7837 cx.set_state(indoc!(
7838 r#"abcd
7839 ef«ghˇ»
7840 ijkl
7841 «mˇ»nop"#
7842 ));
7843
7844 cx.update_editor(|editor, window, cx| {
7845 editor.add_selection_above(&Default::default(), window, cx);
7846 });
7847
7848 // test multiple selections expand in the same direction
7849 cx.assert_editor_state(indoc!(
7850 r#"ab«cdˇ»
7851 ef«ghˇ»
7852 «iˇ»jkl
7853 «mˇ»nop"#
7854 ));
7855
7856 cx.update_editor(|editor, window, cx| {
7857 editor.add_selection_above(&Default::default(), window, cx);
7858 });
7859
7860 // test multiple selection upward overflow
7861 cx.assert_editor_state(indoc!(
7862 r#"ab«cdˇ»
7863 «eˇ»f«ghˇ»
7864 «iˇ»jkl
7865 «mˇ»nop"#
7866 ));
7867
7868 cx.update_editor(|editor, window, cx| {
7869 editor.add_selection_below(&Default::default(), window, cx);
7870 });
7871
7872 // test multiple selection retrieves back correctly
7873 cx.assert_editor_state(indoc!(
7874 r#"abcd
7875 ef«ghˇ»
7876 «iˇ»jkl
7877 «mˇ»nop"#
7878 ));
7879
7880 cx.update_editor(|editor, window, cx| {
7881 editor.add_selection_below(&Default::default(), window, cx);
7882 });
7883
7884 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7885 cx.assert_editor_state(indoc!(
7886 r#"abcd
7887 ef«ghˇ»
7888 ij«klˇ»
7889 «mˇ»nop"#
7890 ));
7891
7892 cx.update_editor(|editor, window, cx| {
7893 editor.undo_selection(&Default::default(), window, cx);
7894 });
7895
7896 // test undo
7897 cx.assert_editor_state(indoc!(
7898 r#"abcd
7899 ef«ghˇ»
7900 «iˇ»jkl
7901 «mˇ»nop"#
7902 ));
7903
7904 cx.update_editor(|editor, window, cx| {
7905 editor.redo_selection(&Default::default(), window, cx);
7906 });
7907
7908 // test redo
7909 cx.assert_editor_state(indoc!(
7910 r#"abcd
7911 ef«ghˇ»
7912 ij«klˇ»
7913 «mˇ»nop"#
7914 ));
7915}
7916
7917#[gpui::test]
7918async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7919 init_test(cx, |_| {});
7920 let mut cx = EditorTestContext::new(cx).await;
7921
7922 cx.set_state(indoc!(
7923 r#"line onˇe
7924 liˇne two
7925 line three
7926 line four"#
7927 ));
7928
7929 cx.update_editor(|editor, window, cx| {
7930 editor.add_selection_below(&Default::default(), window, cx);
7931 editor.add_selection_below(&Default::default(), window, cx);
7932 editor.add_selection_below(&Default::default(), window, cx);
7933 });
7934
7935 // initial state with two multi cursor groups
7936 cx.assert_editor_state(indoc!(
7937 r#"line onˇe
7938 liˇne twˇo
7939 liˇne thˇree
7940 liˇne foˇur"#
7941 ));
7942
7943 // add single cursor in middle - simulate opt click
7944 cx.update_editor(|editor, window, cx| {
7945 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7946 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7947 editor.end_selection(window, cx);
7948 });
7949
7950 cx.assert_editor_state(indoc!(
7951 r#"line onˇe
7952 liˇne twˇo
7953 liˇneˇ thˇree
7954 liˇne foˇur"#
7955 ));
7956
7957 cx.update_editor(|editor, window, cx| {
7958 editor.add_selection_above(&Default::default(), window, cx);
7959 });
7960
7961 // test new added selection expands above and existing selection shrinks
7962 cx.assert_editor_state(indoc!(
7963 r#"line onˇe
7964 liˇneˇ twˇo
7965 liˇneˇ thˇree
7966 line four"#
7967 ));
7968
7969 cx.update_editor(|editor, window, cx| {
7970 editor.add_selection_above(&Default::default(), window, cx);
7971 });
7972
7973 // test new added selection expands above and existing selection shrinks
7974 cx.assert_editor_state(indoc!(
7975 r#"lineˇ onˇe
7976 liˇneˇ twˇo
7977 lineˇ three
7978 line four"#
7979 ));
7980
7981 // intial state with two selection groups
7982 cx.set_state(indoc!(
7983 r#"abcd
7984 ef«ghˇ»
7985 ijkl
7986 «mˇ»nop"#
7987 ));
7988
7989 cx.update_editor(|editor, window, cx| {
7990 editor.add_selection_above(&Default::default(), window, cx);
7991 editor.add_selection_above(&Default::default(), window, cx);
7992 });
7993
7994 cx.assert_editor_state(indoc!(
7995 r#"ab«cdˇ»
7996 «eˇ»f«ghˇ»
7997 «iˇ»jkl
7998 «mˇ»nop"#
7999 ));
8000
8001 // add single selection in middle - simulate opt drag
8002 cx.update_editor(|editor, window, cx| {
8003 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8004 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8005 editor.update_selection(
8006 DisplayPoint::new(DisplayRow(2), 4),
8007 0,
8008 gpui::Point::<f32>::default(),
8009 window,
8010 cx,
8011 );
8012 editor.end_selection(window, cx);
8013 });
8014
8015 cx.assert_editor_state(indoc!(
8016 r#"ab«cdˇ»
8017 «eˇ»f«ghˇ»
8018 «iˇ»jk«lˇ»
8019 «mˇ»nop"#
8020 ));
8021
8022 cx.update_editor(|editor, window, cx| {
8023 editor.add_selection_below(&Default::default(), window, cx);
8024 });
8025
8026 // test new added selection expands below, others shrinks from above
8027 cx.assert_editor_state(indoc!(
8028 r#"abcd
8029 ef«ghˇ»
8030 «iˇ»jk«lˇ»
8031 «mˇ»no«pˇ»"#
8032 ));
8033}
8034
8035#[gpui::test]
8036async fn test_select_next(cx: &mut TestAppContext) {
8037 init_test(cx, |_| {});
8038
8039 let mut cx = EditorTestContext::new(cx).await;
8040 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8041
8042 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8043 .unwrap();
8044 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8045
8046 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8047 .unwrap();
8048 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8049
8050 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8051 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8052
8053 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8054 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8055
8056 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8057 .unwrap();
8058 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8059
8060 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8061 .unwrap();
8062 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8063
8064 // Test selection direction should be preserved
8065 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8066
8067 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8068 .unwrap();
8069 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8070}
8071
8072#[gpui::test]
8073async fn test_select_all_matches(cx: &mut TestAppContext) {
8074 init_test(cx, |_| {});
8075
8076 let mut cx = EditorTestContext::new(cx).await;
8077
8078 // Test caret-only selections
8079 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8080 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8081 .unwrap();
8082 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8083
8084 // Test left-to-right selections
8085 cx.set_state("abc\n«abcˇ»\nabc");
8086 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8087 .unwrap();
8088 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8089
8090 // Test right-to-left selections
8091 cx.set_state("abc\n«ˇabc»\nabc");
8092 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8093 .unwrap();
8094 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8095
8096 // Test selecting whitespace with caret selection
8097 cx.set_state("abc\nˇ abc\nabc");
8098 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8099 .unwrap();
8100 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8101
8102 // Test selecting whitespace with left-to-right selection
8103 cx.set_state("abc\n«ˇ »abc\nabc");
8104 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8105 .unwrap();
8106 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8107
8108 // Test no matches with right-to-left selection
8109 cx.set_state("abc\n« ˇ»abc\nabc");
8110 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8111 .unwrap();
8112 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8113
8114 // Test with a single word and clip_at_line_ends=true (#29823)
8115 cx.set_state("aˇbc");
8116 cx.update_editor(|e, window, cx| {
8117 e.set_clip_at_line_ends(true, cx);
8118 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8119 e.set_clip_at_line_ends(false, cx);
8120 });
8121 cx.assert_editor_state("«abcˇ»");
8122}
8123
8124#[gpui::test]
8125async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8126 init_test(cx, |_| {});
8127
8128 let mut cx = EditorTestContext::new(cx).await;
8129
8130 let large_body_1 = "\nd".repeat(200);
8131 let large_body_2 = "\ne".repeat(200);
8132
8133 cx.set_state(&format!(
8134 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8135 ));
8136 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8137 let scroll_position = editor.scroll_position(cx);
8138 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8139 scroll_position
8140 });
8141
8142 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8143 .unwrap();
8144 cx.assert_editor_state(&format!(
8145 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8146 ));
8147 let scroll_position_after_selection =
8148 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8149 assert_eq!(
8150 initial_scroll_position, scroll_position_after_selection,
8151 "Scroll position should not change after selecting all matches"
8152 );
8153}
8154
8155#[gpui::test]
8156async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8157 init_test(cx, |_| {});
8158
8159 let mut cx = EditorLspTestContext::new_rust(
8160 lsp::ServerCapabilities {
8161 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8162 ..Default::default()
8163 },
8164 cx,
8165 )
8166 .await;
8167
8168 cx.set_state(indoc! {"
8169 line 1
8170 line 2
8171 linˇe 3
8172 line 4
8173 line 5
8174 "});
8175
8176 // Make an edit
8177 cx.update_editor(|editor, window, cx| {
8178 editor.handle_input("X", window, cx);
8179 });
8180
8181 // Move cursor to a different position
8182 cx.update_editor(|editor, window, cx| {
8183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8184 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8185 });
8186 });
8187
8188 cx.assert_editor_state(indoc! {"
8189 line 1
8190 line 2
8191 linXe 3
8192 line 4
8193 liˇne 5
8194 "});
8195
8196 cx.lsp
8197 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8198 Ok(Some(vec![lsp::TextEdit::new(
8199 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8200 "PREFIX ".to_string(),
8201 )]))
8202 });
8203
8204 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8205 .unwrap()
8206 .await
8207 .unwrap();
8208
8209 cx.assert_editor_state(indoc! {"
8210 PREFIX line 1
8211 line 2
8212 linXe 3
8213 line 4
8214 liˇne 5
8215 "});
8216
8217 // Undo formatting
8218 cx.update_editor(|editor, window, cx| {
8219 editor.undo(&Default::default(), window, cx);
8220 });
8221
8222 // Verify cursor moved back to position after edit
8223 cx.assert_editor_state(indoc! {"
8224 line 1
8225 line 2
8226 linXˇe 3
8227 line 4
8228 line 5
8229 "});
8230}
8231
8232#[gpui::test]
8233async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8234 init_test(cx, |_| {});
8235
8236 let mut cx = EditorTestContext::new(cx).await;
8237
8238 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8239 cx.update_editor(|editor, window, cx| {
8240 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8241 });
8242
8243 cx.set_state(indoc! {"
8244 line 1
8245 line 2
8246 linˇe 3
8247 line 4
8248 line 5
8249 line 6
8250 line 7
8251 line 8
8252 line 9
8253 line 10
8254 "});
8255
8256 let snapshot = cx.buffer_snapshot();
8257 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8258
8259 cx.update(|_, cx| {
8260 provider.update(cx, |provider, _| {
8261 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8262 id: None,
8263 edits: vec![(edit_position..edit_position, "X".into())],
8264 edit_preview: None,
8265 }))
8266 })
8267 });
8268
8269 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8270 cx.update_editor(|editor, window, cx| {
8271 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8272 });
8273
8274 cx.assert_editor_state(indoc! {"
8275 line 1
8276 line 2
8277 lineXˇ 3
8278 line 4
8279 line 5
8280 line 6
8281 line 7
8282 line 8
8283 line 9
8284 line 10
8285 "});
8286
8287 cx.update_editor(|editor, window, cx| {
8288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8289 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8290 });
8291 });
8292
8293 cx.assert_editor_state(indoc! {"
8294 line 1
8295 line 2
8296 lineX 3
8297 line 4
8298 line 5
8299 line 6
8300 line 7
8301 line 8
8302 line 9
8303 liˇne 10
8304 "});
8305
8306 cx.update_editor(|editor, window, cx| {
8307 editor.undo(&Default::default(), window, cx);
8308 });
8309
8310 cx.assert_editor_state(indoc! {"
8311 line 1
8312 line 2
8313 lineˇ 3
8314 line 4
8315 line 5
8316 line 6
8317 line 7
8318 line 8
8319 line 9
8320 line 10
8321 "});
8322}
8323
8324#[gpui::test]
8325async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8326 init_test(cx, |_| {});
8327
8328 let mut cx = EditorTestContext::new(cx).await;
8329 cx.set_state(
8330 r#"let foo = 2;
8331lˇet foo = 2;
8332let fooˇ = 2;
8333let foo = 2;
8334let foo = ˇ2;"#,
8335 );
8336
8337 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8338 .unwrap();
8339 cx.assert_editor_state(
8340 r#"let foo = 2;
8341«letˇ» foo = 2;
8342let «fooˇ» = 2;
8343let foo = 2;
8344let foo = «2ˇ»;"#,
8345 );
8346
8347 // noop for multiple selections with different contents
8348 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8349 .unwrap();
8350 cx.assert_editor_state(
8351 r#"let foo = 2;
8352«letˇ» foo = 2;
8353let «fooˇ» = 2;
8354let foo = 2;
8355let foo = «2ˇ»;"#,
8356 );
8357
8358 // Test last selection direction should be preserved
8359 cx.set_state(
8360 r#"let foo = 2;
8361let foo = 2;
8362let «fooˇ» = 2;
8363let «ˇfoo» = 2;
8364let foo = 2;"#,
8365 );
8366
8367 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8368 .unwrap();
8369 cx.assert_editor_state(
8370 r#"let foo = 2;
8371let foo = 2;
8372let «fooˇ» = 2;
8373let «ˇfoo» = 2;
8374let «ˇfoo» = 2;"#,
8375 );
8376}
8377
8378#[gpui::test]
8379async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8380 init_test(cx, |_| {});
8381
8382 let mut cx =
8383 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8384
8385 cx.assert_editor_state(indoc! {"
8386 ˇbbb
8387 ccc
8388
8389 bbb
8390 ccc
8391 "});
8392 cx.dispatch_action(SelectPrevious::default());
8393 cx.assert_editor_state(indoc! {"
8394 «bbbˇ»
8395 ccc
8396
8397 bbb
8398 ccc
8399 "});
8400 cx.dispatch_action(SelectPrevious::default());
8401 cx.assert_editor_state(indoc! {"
8402 «bbbˇ»
8403 ccc
8404
8405 «bbbˇ»
8406 ccc
8407 "});
8408}
8409
8410#[gpui::test]
8411async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8412 init_test(cx, |_| {});
8413
8414 let mut cx = EditorTestContext::new(cx).await;
8415 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8416
8417 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8418 .unwrap();
8419 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8420
8421 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8422 .unwrap();
8423 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8424
8425 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8426 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8427
8428 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8429 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8430
8431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8432 .unwrap();
8433 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8434
8435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8436 .unwrap();
8437 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8438}
8439
8440#[gpui::test]
8441async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8442 init_test(cx, |_| {});
8443
8444 let mut cx = EditorTestContext::new(cx).await;
8445 cx.set_state("aˇ");
8446
8447 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8448 .unwrap();
8449 cx.assert_editor_state("«aˇ»");
8450 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8451 .unwrap();
8452 cx.assert_editor_state("«aˇ»");
8453}
8454
8455#[gpui::test]
8456async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8457 init_test(cx, |_| {});
8458
8459 let mut cx = EditorTestContext::new(cx).await;
8460 cx.set_state(
8461 r#"let foo = 2;
8462lˇet foo = 2;
8463let fooˇ = 2;
8464let foo = 2;
8465let foo = ˇ2;"#,
8466 );
8467
8468 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8469 .unwrap();
8470 cx.assert_editor_state(
8471 r#"let foo = 2;
8472«letˇ» foo = 2;
8473let «fooˇ» = 2;
8474let foo = 2;
8475let foo = «2ˇ»;"#,
8476 );
8477
8478 // noop for multiple selections with different contents
8479 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8480 .unwrap();
8481 cx.assert_editor_state(
8482 r#"let foo = 2;
8483«letˇ» foo = 2;
8484let «fooˇ» = 2;
8485let foo = 2;
8486let foo = «2ˇ»;"#,
8487 );
8488}
8489
8490#[gpui::test]
8491async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8492 init_test(cx, |_| {});
8493
8494 let mut cx = EditorTestContext::new(cx).await;
8495 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8496
8497 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8498 .unwrap();
8499 // selection direction is preserved
8500 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8501
8502 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8503 .unwrap();
8504 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8505
8506 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8507 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8508
8509 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8510 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8511
8512 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8513 .unwrap();
8514 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8515
8516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8517 .unwrap();
8518 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8519}
8520
8521#[gpui::test]
8522async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8523 init_test(cx, |_| {});
8524
8525 let language = Arc::new(Language::new(
8526 LanguageConfig::default(),
8527 Some(tree_sitter_rust::LANGUAGE.into()),
8528 ));
8529
8530 let text = r#"
8531 use mod1::mod2::{mod3, mod4};
8532
8533 fn fn_1(param1: bool, param2: &str) {
8534 let var1 = "text";
8535 }
8536 "#
8537 .unindent();
8538
8539 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8540 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8541 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8542
8543 editor
8544 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8545 .await;
8546
8547 editor.update_in(cx, |editor, window, cx| {
8548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8549 s.select_display_ranges([
8550 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8551 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8552 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8553 ]);
8554 });
8555 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8556 });
8557 editor.update(cx, |editor, cx| {
8558 assert_text_with_selections(
8559 editor,
8560 indoc! {r#"
8561 use mod1::mod2::{mod3, «mod4ˇ»};
8562
8563 fn fn_1«ˇ(param1: bool, param2: &str)» {
8564 let var1 = "«ˇtext»";
8565 }
8566 "#},
8567 cx,
8568 );
8569 });
8570
8571 editor.update_in(cx, |editor, window, cx| {
8572 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8573 });
8574 editor.update(cx, |editor, cx| {
8575 assert_text_with_selections(
8576 editor,
8577 indoc! {r#"
8578 use mod1::mod2::«{mod3, mod4}ˇ»;
8579
8580 «ˇfn fn_1(param1: bool, param2: &str) {
8581 let var1 = "text";
8582 }»
8583 "#},
8584 cx,
8585 );
8586 });
8587
8588 editor.update_in(cx, |editor, window, cx| {
8589 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8590 });
8591 assert_eq!(
8592 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8593 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8594 );
8595
8596 // Trying to expand the selected syntax node one more time has no effect.
8597 editor.update_in(cx, |editor, window, cx| {
8598 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8599 });
8600 assert_eq!(
8601 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8602 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8603 );
8604
8605 editor.update_in(cx, |editor, window, cx| {
8606 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8607 });
8608 editor.update(cx, |editor, cx| {
8609 assert_text_with_selections(
8610 editor,
8611 indoc! {r#"
8612 use mod1::mod2::«{mod3, mod4}ˇ»;
8613
8614 «ˇfn fn_1(param1: bool, param2: &str) {
8615 let var1 = "text";
8616 }»
8617 "#},
8618 cx,
8619 );
8620 });
8621
8622 editor.update_in(cx, |editor, window, cx| {
8623 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8624 });
8625 editor.update(cx, |editor, cx| {
8626 assert_text_with_selections(
8627 editor,
8628 indoc! {r#"
8629 use mod1::mod2::{mod3, «mod4ˇ»};
8630
8631 fn fn_1«ˇ(param1: bool, param2: &str)» {
8632 let var1 = "«ˇtext»";
8633 }
8634 "#},
8635 cx,
8636 );
8637 });
8638
8639 editor.update_in(cx, |editor, window, cx| {
8640 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8641 });
8642 editor.update(cx, |editor, cx| {
8643 assert_text_with_selections(
8644 editor,
8645 indoc! {r#"
8646 use mod1::mod2::{mod3, moˇd4};
8647
8648 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8649 let var1 = "teˇxt";
8650 }
8651 "#},
8652 cx,
8653 );
8654 });
8655
8656 // Trying to shrink the selected syntax node one more time has no effect.
8657 editor.update_in(cx, |editor, window, cx| {
8658 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8659 });
8660 editor.update_in(cx, |editor, _, cx| {
8661 assert_text_with_selections(
8662 editor,
8663 indoc! {r#"
8664 use mod1::mod2::{mod3, moˇd4};
8665
8666 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8667 let var1 = "teˇxt";
8668 }
8669 "#},
8670 cx,
8671 );
8672 });
8673
8674 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8675 // a fold.
8676 editor.update_in(cx, |editor, window, cx| {
8677 editor.fold_creases(
8678 vec![
8679 Crease::simple(
8680 Point::new(0, 21)..Point::new(0, 24),
8681 FoldPlaceholder::test(),
8682 ),
8683 Crease::simple(
8684 Point::new(3, 20)..Point::new(3, 22),
8685 FoldPlaceholder::test(),
8686 ),
8687 ],
8688 true,
8689 window,
8690 cx,
8691 );
8692 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8693 });
8694 editor.update(cx, |editor, cx| {
8695 assert_text_with_selections(
8696 editor,
8697 indoc! {r#"
8698 use mod1::mod2::«{mod3, mod4}ˇ»;
8699
8700 fn fn_1«ˇ(param1: bool, param2: &str)» {
8701 let var1 = "«ˇtext»";
8702 }
8703 "#},
8704 cx,
8705 );
8706 });
8707}
8708
8709#[gpui::test]
8710async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8711 init_test(cx, |_| {});
8712
8713 let language = Arc::new(Language::new(
8714 LanguageConfig::default(),
8715 Some(tree_sitter_rust::LANGUAGE.into()),
8716 ));
8717
8718 let text = "let a = 2;";
8719
8720 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8721 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8722 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8723
8724 editor
8725 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8726 .await;
8727
8728 // Test case 1: Cursor at end of word
8729 editor.update_in(cx, |editor, window, cx| {
8730 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8731 s.select_display_ranges([
8732 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8733 ]);
8734 });
8735 });
8736 editor.update(cx, |editor, cx| {
8737 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8738 });
8739 editor.update_in(cx, |editor, window, cx| {
8740 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8741 });
8742 editor.update(cx, |editor, cx| {
8743 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8744 });
8745 editor.update_in(cx, |editor, window, cx| {
8746 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8747 });
8748 editor.update(cx, |editor, cx| {
8749 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8750 });
8751
8752 // Test case 2: Cursor at end of statement
8753 editor.update_in(cx, |editor, window, cx| {
8754 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8755 s.select_display_ranges([
8756 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8757 ]);
8758 });
8759 });
8760 editor.update(cx, |editor, cx| {
8761 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8762 });
8763 editor.update_in(cx, |editor, window, cx| {
8764 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8765 });
8766 editor.update(cx, |editor, cx| {
8767 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8768 });
8769}
8770
8771#[gpui::test]
8772async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8773 init_test(cx, |_| {});
8774
8775 let language = Arc::new(Language::new(
8776 LanguageConfig {
8777 name: "JavaScript".into(),
8778 ..Default::default()
8779 },
8780 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8781 ));
8782
8783 let text = r#"
8784 let a = {
8785 key: "value",
8786 };
8787 "#
8788 .unindent();
8789
8790 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8791 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8792 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8793
8794 editor
8795 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8796 .await;
8797
8798 // Test case 1: Cursor after '{'
8799 editor.update_in(cx, |editor, window, cx| {
8800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8801 s.select_display_ranges([
8802 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8803 ]);
8804 });
8805 });
8806 editor.update(cx, |editor, cx| {
8807 assert_text_with_selections(
8808 editor,
8809 indoc! {r#"
8810 let a = {ˇ
8811 key: "value",
8812 };
8813 "#},
8814 cx,
8815 );
8816 });
8817 editor.update_in(cx, |editor, window, cx| {
8818 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8819 });
8820 editor.update(cx, |editor, cx| {
8821 assert_text_with_selections(
8822 editor,
8823 indoc! {r#"
8824 let a = «ˇ{
8825 key: "value",
8826 }»;
8827 "#},
8828 cx,
8829 );
8830 });
8831
8832 // Test case 2: Cursor after ':'
8833 editor.update_in(cx, |editor, window, cx| {
8834 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8835 s.select_display_ranges([
8836 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8837 ]);
8838 });
8839 });
8840 editor.update(cx, |editor, cx| {
8841 assert_text_with_selections(
8842 editor,
8843 indoc! {r#"
8844 let a = {
8845 key:ˇ "value",
8846 };
8847 "#},
8848 cx,
8849 );
8850 });
8851 editor.update_in(cx, |editor, window, cx| {
8852 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8853 });
8854 editor.update(cx, |editor, cx| {
8855 assert_text_with_selections(
8856 editor,
8857 indoc! {r#"
8858 let a = {
8859 «ˇkey: "value"»,
8860 };
8861 "#},
8862 cx,
8863 );
8864 });
8865 editor.update_in(cx, |editor, window, cx| {
8866 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8867 });
8868 editor.update(cx, |editor, cx| {
8869 assert_text_with_selections(
8870 editor,
8871 indoc! {r#"
8872 let a = «ˇ{
8873 key: "value",
8874 }»;
8875 "#},
8876 cx,
8877 );
8878 });
8879
8880 // Test case 3: Cursor after ','
8881 editor.update_in(cx, |editor, window, cx| {
8882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8883 s.select_display_ranges([
8884 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8885 ]);
8886 });
8887 });
8888 editor.update(cx, |editor, cx| {
8889 assert_text_with_selections(
8890 editor,
8891 indoc! {r#"
8892 let a = {
8893 key: "value",ˇ
8894 };
8895 "#},
8896 cx,
8897 );
8898 });
8899 editor.update_in(cx, |editor, window, cx| {
8900 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8901 });
8902 editor.update(cx, |editor, cx| {
8903 assert_text_with_selections(
8904 editor,
8905 indoc! {r#"
8906 let a = «ˇ{
8907 key: "value",
8908 }»;
8909 "#},
8910 cx,
8911 );
8912 });
8913
8914 // Test case 4: Cursor after ';'
8915 editor.update_in(cx, |editor, window, cx| {
8916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8917 s.select_display_ranges([
8918 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8919 ]);
8920 });
8921 });
8922 editor.update(cx, |editor, cx| {
8923 assert_text_with_selections(
8924 editor,
8925 indoc! {r#"
8926 let a = {
8927 key: "value",
8928 };ˇ
8929 "#},
8930 cx,
8931 );
8932 });
8933 editor.update_in(cx, |editor, window, cx| {
8934 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8935 });
8936 editor.update(cx, |editor, cx| {
8937 assert_text_with_selections(
8938 editor,
8939 indoc! {r#"
8940 «ˇlet a = {
8941 key: "value",
8942 };
8943 »"#},
8944 cx,
8945 );
8946 });
8947}
8948
8949#[gpui::test]
8950async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8951 init_test(cx, |_| {});
8952
8953 let language = Arc::new(Language::new(
8954 LanguageConfig::default(),
8955 Some(tree_sitter_rust::LANGUAGE.into()),
8956 ));
8957
8958 let text = r#"
8959 use mod1::mod2::{mod3, mod4};
8960
8961 fn fn_1(param1: bool, param2: &str) {
8962 let var1 = "hello world";
8963 }
8964 "#
8965 .unindent();
8966
8967 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8968 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8969 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8970
8971 editor
8972 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8973 .await;
8974
8975 // Test 1: Cursor on a letter of a string word
8976 editor.update_in(cx, |editor, window, cx| {
8977 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8978 s.select_display_ranges([
8979 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8980 ]);
8981 });
8982 });
8983 editor.update_in(cx, |editor, window, cx| {
8984 assert_text_with_selections(
8985 editor,
8986 indoc! {r#"
8987 use mod1::mod2::{mod3, mod4};
8988
8989 fn fn_1(param1: bool, param2: &str) {
8990 let var1 = "hˇello world";
8991 }
8992 "#},
8993 cx,
8994 );
8995 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8996 assert_text_with_selections(
8997 editor,
8998 indoc! {r#"
8999 use mod1::mod2::{mod3, mod4};
9000
9001 fn fn_1(param1: bool, param2: &str) {
9002 let var1 = "«ˇhello» world";
9003 }
9004 "#},
9005 cx,
9006 );
9007 });
9008
9009 // Test 2: Partial selection within a word
9010 editor.update_in(cx, |editor, window, cx| {
9011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9012 s.select_display_ranges([
9013 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9014 ]);
9015 });
9016 });
9017 editor.update_in(cx, |editor, window, cx| {
9018 assert_text_with_selections(
9019 editor,
9020 indoc! {r#"
9021 use mod1::mod2::{mod3, mod4};
9022
9023 fn fn_1(param1: bool, param2: &str) {
9024 let var1 = "h«elˇ»lo world";
9025 }
9026 "#},
9027 cx,
9028 );
9029 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9030 assert_text_with_selections(
9031 editor,
9032 indoc! {r#"
9033 use mod1::mod2::{mod3, mod4};
9034
9035 fn fn_1(param1: bool, param2: &str) {
9036 let var1 = "«ˇhello» world";
9037 }
9038 "#},
9039 cx,
9040 );
9041 });
9042
9043 // Test 3: Complete word already selected
9044 editor.update_in(cx, |editor, window, cx| {
9045 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9046 s.select_display_ranges([
9047 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9048 ]);
9049 });
9050 });
9051 editor.update_in(cx, |editor, window, cx| {
9052 assert_text_with_selections(
9053 editor,
9054 indoc! {r#"
9055 use mod1::mod2::{mod3, mod4};
9056
9057 fn fn_1(param1: bool, param2: &str) {
9058 let var1 = "«helloˇ» world";
9059 }
9060 "#},
9061 cx,
9062 );
9063 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9064 assert_text_with_selections(
9065 editor,
9066 indoc! {r#"
9067 use mod1::mod2::{mod3, mod4};
9068
9069 fn fn_1(param1: bool, param2: &str) {
9070 let var1 = "«hello worldˇ»";
9071 }
9072 "#},
9073 cx,
9074 );
9075 });
9076
9077 // Test 4: Selection spanning across words
9078 editor.update_in(cx, |editor, window, cx| {
9079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9080 s.select_display_ranges([
9081 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9082 ]);
9083 });
9084 });
9085 editor.update_in(cx, |editor, window, cx| {
9086 assert_text_with_selections(
9087 editor,
9088 indoc! {r#"
9089 use mod1::mod2::{mod3, mod4};
9090
9091 fn fn_1(param1: bool, param2: &str) {
9092 let var1 = "hel«lo woˇ»rld";
9093 }
9094 "#},
9095 cx,
9096 );
9097 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9098 assert_text_with_selections(
9099 editor,
9100 indoc! {r#"
9101 use mod1::mod2::{mod3, mod4};
9102
9103 fn fn_1(param1: bool, param2: &str) {
9104 let var1 = "«ˇhello world»";
9105 }
9106 "#},
9107 cx,
9108 );
9109 });
9110
9111 // Test 5: Expansion beyond string
9112 editor.update_in(cx, |editor, window, cx| {
9113 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9114 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9115 assert_text_with_selections(
9116 editor,
9117 indoc! {r#"
9118 use mod1::mod2::{mod3, mod4};
9119
9120 fn fn_1(param1: bool, param2: &str) {
9121 «ˇlet var1 = "hello world";»
9122 }
9123 "#},
9124 cx,
9125 );
9126 });
9127}
9128
9129#[gpui::test]
9130async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9131 init_test(cx, |_| {});
9132
9133 let mut cx = EditorTestContext::new(cx).await;
9134
9135 let language = Arc::new(Language::new(
9136 LanguageConfig::default(),
9137 Some(tree_sitter_rust::LANGUAGE.into()),
9138 ));
9139
9140 cx.update_buffer(|buffer, cx| {
9141 buffer.set_language(Some(language), cx);
9142 });
9143
9144 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9145 cx.update_editor(|editor, window, cx| {
9146 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9147 });
9148
9149 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9150
9151 cx.set_state(indoc! { r#"fn a() {
9152 // what
9153 // a
9154 // ˇlong
9155 // method
9156 // I
9157 // sure
9158 // hope
9159 // it
9160 // works
9161 }"# });
9162
9163 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9164 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9165 cx.update(|_, cx| {
9166 multi_buffer.update(cx, |multi_buffer, cx| {
9167 multi_buffer.set_excerpts_for_path(
9168 PathKey::for_buffer(&buffer, cx),
9169 buffer,
9170 [Point::new(1, 0)..Point::new(1, 0)],
9171 3,
9172 cx,
9173 );
9174 });
9175 });
9176
9177 let editor2 = cx.new_window_entity(|window, cx| {
9178 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9179 });
9180
9181 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9182 cx.update_editor(|editor, window, cx| {
9183 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9184 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9185 })
9186 });
9187
9188 cx.assert_editor_state(indoc! { "
9189 fn a() {
9190 // what
9191 // a
9192 ˇ // long
9193 // method"});
9194
9195 cx.update_editor(|editor, window, cx| {
9196 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9197 });
9198
9199 // Although we could potentially make the action work when the syntax node
9200 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9201 // did. Maybe we could also expand the excerpt to contain the range?
9202 cx.assert_editor_state(indoc! { "
9203 fn a() {
9204 // what
9205 // a
9206 ˇ // long
9207 // method"});
9208}
9209
9210#[gpui::test]
9211async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9212 init_test(cx, |_| {});
9213
9214 let base_text = r#"
9215 impl A {
9216 // this is an uncommitted comment
9217
9218 fn b() {
9219 c();
9220 }
9221
9222 // this is another uncommitted comment
9223
9224 fn d() {
9225 // e
9226 // f
9227 }
9228 }
9229
9230 fn g() {
9231 // h
9232 }
9233 "#
9234 .unindent();
9235
9236 let text = r#"
9237 ˇimpl A {
9238
9239 fn b() {
9240 c();
9241 }
9242
9243 fn d() {
9244 // e
9245 // f
9246 }
9247 }
9248
9249 fn g() {
9250 // h
9251 }
9252 "#
9253 .unindent();
9254
9255 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9256 cx.set_state(&text);
9257 cx.set_head_text(&base_text);
9258 cx.update_editor(|editor, window, cx| {
9259 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9260 });
9261
9262 cx.assert_state_with_diff(
9263 "
9264 ˇimpl A {
9265 - // this is an uncommitted comment
9266
9267 fn b() {
9268 c();
9269 }
9270
9271 - // this is another uncommitted comment
9272 -
9273 fn d() {
9274 // e
9275 // f
9276 }
9277 }
9278
9279 fn g() {
9280 // h
9281 }
9282 "
9283 .unindent(),
9284 );
9285
9286 let expected_display_text = "
9287 impl A {
9288 // this is an uncommitted comment
9289
9290 fn b() {
9291 ⋯
9292 }
9293
9294 // this is another uncommitted comment
9295
9296 fn d() {
9297 ⋯
9298 }
9299 }
9300
9301 fn g() {
9302 ⋯
9303 }
9304 "
9305 .unindent();
9306
9307 cx.update_editor(|editor, window, cx| {
9308 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9309 assert_eq!(editor.display_text(cx), expected_display_text);
9310 });
9311}
9312
9313#[gpui::test]
9314async fn test_autoindent(cx: &mut TestAppContext) {
9315 init_test(cx, |_| {});
9316
9317 let language = Arc::new(
9318 Language::new(
9319 LanguageConfig {
9320 brackets: BracketPairConfig {
9321 pairs: vec![
9322 BracketPair {
9323 start: "{".to_string(),
9324 end: "}".to_string(),
9325 close: false,
9326 surround: false,
9327 newline: true,
9328 },
9329 BracketPair {
9330 start: "(".to_string(),
9331 end: ")".to_string(),
9332 close: false,
9333 surround: false,
9334 newline: true,
9335 },
9336 ],
9337 ..Default::default()
9338 },
9339 ..Default::default()
9340 },
9341 Some(tree_sitter_rust::LANGUAGE.into()),
9342 )
9343 .with_indents_query(
9344 r#"
9345 (_ "(" ")" @end) @indent
9346 (_ "{" "}" @end) @indent
9347 "#,
9348 )
9349 .unwrap(),
9350 );
9351
9352 let text = "fn a() {}";
9353
9354 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9355 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9356 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9357 editor
9358 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9359 .await;
9360
9361 editor.update_in(cx, |editor, window, cx| {
9362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9363 s.select_ranges([5..5, 8..8, 9..9])
9364 });
9365 editor.newline(&Newline, window, cx);
9366 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9367 assert_eq!(
9368 editor.selections.ranges(cx),
9369 &[
9370 Point::new(1, 4)..Point::new(1, 4),
9371 Point::new(3, 4)..Point::new(3, 4),
9372 Point::new(5, 0)..Point::new(5, 0)
9373 ]
9374 );
9375 });
9376}
9377
9378#[gpui::test]
9379async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9380 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9381
9382 let language = Arc::new(
9383 Language::new(
9384 LanguageConfig {
9385 brackets: BracketPairConfig {
9386 pairs: vec![
9387 BracketPair {
9388 start: "{".to_string(),
9389 end: "}".to_string(),
9390 close: false,
9391 surround: false,
9392 newline: true,
9393 },
9394 BracketPair {
9395 start: "(".to_string(),
9396 end: ")".to_string(),
9397 close: false,
9398 surround: false,
9399 newline: true,
9400 },
9401 ],
9402 ..Default::default()
9403 },
9404 ..Default::default()
9405 },
9406 Some(tree_sitter_rust::LANGUAGE.into()),
9407 )
9408 .with_indents_query(
9409 r#"
9410 (_ "(" ")" @end) @indent
9411 (_ "{" "}" @end) @indent
9412 "#,
9413 )
9414 .unwrap(),
9415 );
9416
9417 let text = "fn a() {}";
9418
9419 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9420 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9421 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9422 editor
9423 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9424 .await;
9425
9426 editor.update_in(cx, |editor, window, cx| {
9427 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9428 s.select_ranges([5..5, 8..8, 9..9])
9429 });
9430 editor.newline(&Newline, window, cx);
9431 assert_eq!(
9432 editor.text(cx),
9433 indoc!(
9434 "
9435 fn a(
9436
9437 ) {
9438
9439 }
9440 "
9441 )
9442 );
9443 assert_eq!(
9444 editor.selections.ranges(cx),
9445 &[
9446 Point::new(1, 0)..Point::new(1, 0),
9447 Point::new(3, 0)..Point::new(3, 0),
9448 Point::new(5, 0)..Point::new(5, 0)
9449 ]
9450 );
9451 });
9452}
9453
9454#[gpui::test]
9455async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9456 init_test(cx, |settings| {
9457 settings.defaults.auto_indent = Some(true);
9458 settings.languages.0.insert(
9459 "python".into(),
9460 LanguageSettingsContent {
9461 auto_indent: Some(false),
9462 ..Default::default()
9463 },
9464 );
9465 });
9466
9467 let mut cx = EditorTestContext::new(cx).await;
9468
9469 let injected_language = Arc::new(
9470 Language::new(
9471 LanguageConfig {
9472 brackets: BracketPairConfig {
9473 pairs: vec![
9474 BracketPair {
9475 start: "{".to_string(),
9476 end: "}".to_string(),
9477 close: false,
9478 surround: false,
9479 newline: true,
9480 },
9481 BracketPair {
9482 start: "(".to_string(),
9483 end: ")".to_string(),
9484 close: true,
9485 surround: false,
9486 newline: true,
9487 },
9488 ],
9489 ..Default::default()
9490 },
9491 name: "python".into(),
9492 ..Default::default()
9493 },
9494 Some(tree_sitter_python::LANGUAGE.into()),
9495 )
9496 .with_indents_query(
9497 r#"
9498 (_ "(" ")" @end) @indent
9499 (_ "{" "}" @end) @indent
9500 "#,
9501 )
9502 .unwrap(),
9503 );
9504
9505 let language = Arc::new(
9506 Language::new(
9507 LanguageConfig {
9508 brackets: BracketPairConfig {
9509 pairs: vec![
9510 BracketPair {
9511 start: "{".to_string(),
9512 end: "}".to_string(),
9513 close: false,
9514 surround: false,
9515 newline: true,
9516 },
9517 BracketPair {
9518 start: "(".to_string(),
9519 end: ")".to_string(),
9520 close: true,
9521 surround: false,
9522 newline: true,
9523 },
9524 ],
9525 ..Default::default()
9526 },
9527 name: LanguageName::new("rust"),
9528 ..Default::default()
9529 },
9530 Some(tree_sitter_rust::LANGUAGE.into()),
9531 )
9532 .with_indents_query(
9533 r#"
9534 (_ "(" ")" @end) @indent
9535 (_ "{" "}" @end) @indent
9536 "#,
9537 )
9538 .unwrap()
9539 .with_injection_query(
9540 r#"
9541 (macro_invocation
9542 macro: (identifier) @_macro_name
9543 (token_tree) @injection.content
9544 (#set! injection.language "python"))
9545 "#,
9546 )
9547 .unwrap(),
9548 );
9549
9550 cx.language_registry().add(injected_language);
9551 cx.language_registry().add(language.clone());
9552
9553 cx.update_buffer(|buffer, cx| {
9554 buffer.set_language(Some(language), cx);
9555 });
9556
9557 cx.set_state(r#"struct A {ˇ}"#);
9558
9559 cx.update_editor(|editor, window, cx| {
9560 editor.newline(&Default::default(), window, cx);
9561 });
9562
9563 cx.assert_editor_state(indoc!(
9564 "struct A {
9565 ˇ
9566 }"
9567 ));
9568
9569 cx.set_state(r#"select_biased!(ˇ)"#);
9570
9571 cx.update_editor(|editor, window, cx| {
9572 editor.newline(&Default::default(), window, cx);
9573 editor.handle_input("def ", window, cx);
9574 editor.handle_input("(", window, cx);
9575 editor.newline(&Default::default(), window, cx);
9576 editor.handle_input("a", window, cx);
9577 });
9578
9579 cx.assert_editor_state(indoc!(
9580 "select_biased!(
9581 def (
9582 aˇ
9583 )
9584 )"
9585 ));
9586}
9587
9588#[gpui::test]
9589async fn test_autoindent_selections(cx: &mut TestAppContext) {
9590 init_test(cx, |_| {});
9591
9592 {
9593 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9594 cx.set_state(indoc! {"
9595 impl A {
9596
9597 fn b() {}
9598
9599 «fn c() {
9600
9601 }ˇ»
9602 }
9603 "});
9604
9605 cx.update_editor(|editor, window, cx| {
9606 editor.autoindent(&Default::default(), window, cx);
9607 });
9608
9609 cx.assert_editor_state(indoc! {"
9610 impl A {
9611
9612 fn b() {}
9613
9614 «fn c() {
9615
9616 }ˇ»
9617 }
9618 "});
9619 }
9620
9621 {
9622 let mut cx = EditorTestContext::new_multibuffer(
9623 cx,
9624 [indoc! { "
9625 impl A {
9626 «
9627 // a
9628 fn b(){}
9629 »
9630 «
9631 }
9632 fn c(){}
9633 »
9634 "}],
9635 );
9636
9637 let buffer = cx.update_editor(|editor, _, cx| {
9638 let buffer = editor.buffer().update(cx, |buffer, _| {
9639 buffer.all_buffers().iter().next().unwrap().clone()
9640 });
9641 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9642 buffer
9643 });
9644
9645 cx.run_until_parked();
9646 cx.update_editor(|editor, window, cx| {
9647 editor.select_all(&Default::default(), window, cx);
9648 editor.autoindent(&Default::default(), window, cx)
9649 });
9650 cx.run_until_parked();
9651
9652 cx.update(|_, cx| {
9653 assert_eq!(
9654 buffer.read(cx).text(),
9655 indoc! { "
9656 impl A {
9657
9658 // a
9659 fn b(){}
9660
9661
9662 }
9663 fn c(){}
9664
9665 " }
9666 )
9667 });
9668 }
9669}
9670
9671#[gpui::test]
9672async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9673 init_test(cx, |_| {});
9674
9675 let mut cx = EditorTestContext::new(cx).await;
9676
9677 let language = Arc::new(Language::new(
9678 LanguageConfig {
9679 brackets: BracketPairConfig {
9680 pairs: vec![
9681 BracketPair {
9682 start: "{".to_string(),
9683 end: "}".to_string(),
9684 close: true,
9685 surround: true,
9686 newline: true,
9687 },
9688 BracketPair {
9689 start: "(".to_string(),
9690 end: ")".to_string(),
9691 close: true,
9692 surround: true,
9693 newline: true,
9694 },
9695 BracketPair {
9696 start: "/*".to_string(),
9697 end: " */".to_string(),
9698 close: true,
9699 surround: true,
9700 newline: true,
9701 },
9702 BracketPair {
9703 start: "[".to_string(),
9704 end: "]".to_string(),
9705 close: false,
9706 surround: false,
9707 newline: true,
9708 },
9709 BracketPair {
9710 start: "\"".to_string(),
9711 end: "\"".to_string(),
9712 close: true,
9713 surround: true,
9714 newline: false,
9715 },
9716 BracketPair {
9717 start: "<".to_string(),
9718 end: ">".to_string(),
9719 close: false,
9720 surround: true,
9721 newline: true,
9722 },
9723 ],
9724 ..Default::default()
9725 },
9726 autoclose_before: "})]".to_string(),
9727 ..Default::default()
9728 },
9729 Some(tree_sitter_rust::LANGUAGE.into()),
9730 ));
9731
9732 cx.language_registry().add(language.clone());
9733 cx.update_buffer(|buffer, cx| {
9734 buffer.set_language(Some(language), cx);
9735 });
9736
9737 cx.set_state(
9738 &r#"
9739 🏀ˇ
9740 εˇ
9741 ❤️ˇ
9742 "#
9743 .unindent(),
9744 );
9745
9746 // autoclose multiple nested brackets at multiple cursors
9747 cx.update_editor(|editor, window, cx| {
9748 editor.handle_input("{", window, cx);
9749 editor.handle_input("{", window, cx);
9750 editor.handle_input("{", window, cx);
9751 });
9752 cx.assert_editor_state(
9753 &"
9754 🏀{{{ˇ}}}
9755 ε{{{ˇ}}}
9756 ❤️{{{ˇ}}}
9757 "
9758 .unindent(),
9759 );
9760
9761 // insert a different closing bracket
9762 cx.update_editor(|editor, window, cx| {
9763 editor.handle_input(")", window, cx);
9764 });
9765 cx.assert_editor_state(
9766 &"
9767 🏀{{{)ˇ}}}
9768 ε{{{)ˇ}}}
9769 ❤️{{{)ˇ}}}
9770 "
9771 .unindent(),
9772 );
9773
9774 // skip over the auto-closed brackets when typing a closing bracket
9775 cx.update_editor(|editor, window, cx| {
9776 editor.move_right(&MoveRight, window, cx);
9777 editor.handle_input("}", window, cx);
9778 editor.handle_input("}", window, cx);
9779 editor.handle_input("}", window, cx);
9780 });
9781 cx.assert_editor_state(
9782 &"
9783 🏀{{{)}}}}ˇ
9784 ε{{{)}}}}ˇ
9785 ❤️{{{)}}}}ˇ
9786 "
9787 .unindent(),
9788 );
9789
9790 // autoclose multi-character pairs
9791 cx.set_state(
9792 &"
9793 ˇ
9794 ˇ
9795 "
9796 .unindent(),
9797 );
9798 cx.update_editor(|editor, window, cx| {
9799 editor.handle_input("/", window, cx);
9800 editor.handle_input("*", window, cx);
9801 });
9802 cx.assert_editor_state(
9803 &"
9804 /*ˇ */
9805 /*ˇ */
9806 "
9807 .unindent(),
9808 );
9809
9810 // one cursor autocloses a multi-character pair, one cursor
9811 // does not autoclose.
9812 cx.set_state(
9813 &"
9814 /ˇ
9815 ˇ
9816 "
9817 .unindent(),
9818 );
9819 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9820 cx.assert_editor_state(
9821 &"
9822 /*ˇ */
9823 *ˇ
9824 "
9825 .unindent(),
9826 );
9827
9828 // Don't autoclose if the next character isn't whitespace and isn't
9829 // listed in the language's "autoclose_before" section.
9830 cx.set_state("ˇa b");
9831 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9832 cx.assert_editor_state("{ˇa b");
9833
9834 // Don't autoclose if `close` is false for the bracket pair
9835 cx.set_state("ˇ");
9836 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9837 cx.assert_editor_state("[ˇ");
9838
9839 // Surround with brackets if text is selected
9840 cx.set_state("«aˇ» b");
9841 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9842 cx.assert_editor_state("{«aˇ»} b");
9843
9844 // Autoclose when not immediately after a word character
9845 cx.set_state("a ˇ");
9846 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9847 cx.assert_editor_state("a \"ˇ\"");
9848
9849 // Autoclose pair where the start and end characters are the same
9850 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9851 cx.assert_editor_state("a \"\"ˇ");
9852
9853 // Don't autoclose when immediately after a word character
9854 cx.set_state("aˇ");
9855 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9856 cx.assert_editor_state("a\"ˇ");
9857
9858 // Do autoclose when after a non-word character
9859 cx.set_state("{ˇ");
9860 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9861 cx.assert_editor_state("{\"ˇ\"");
9862
9863 // Non identical pairs autoclose regardless of preceding character
9864 cx.set_state("aˇ");
9865 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9866 cx.assert_editor_state("a{ˇ}");
9867
9868 // Don't autoclose pair if autoclose is disabled
9869 cx.set_state("ˇ");
9870 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9871 cx.assert_editor_state("<ˇ");
9872
9873 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9874 cx.set_state("«aˇ» b");
9875 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9876 cx.assert_editor_state("<«aˇ»> b");
9877}
9878
9879#[gpui::test]
9880async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9881 init_test(cx, |settings| {
9882 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9883 });
9884
9885 let mut cx = EditorTestContext::new(cx).await;
9886
9887 let language = Arc::new(Language::new(
9888 LanguageConfig {
9889 brackets: BracketPairConfig {
9890 pairs: vec![
9891 BracketPair {
9892 start: "{".to_string(),
9893 end: "}".to_string(),
9894 close: true,
9895 surround: true,
9896 newline: true,
9897 },
9898 BracketPair {
9899 start: "(".to_string(),
9900 end: ")".to_string(),
9901 close: true,
9902 surround: true,
9903 newline: true,
9904 },
9905 BracketPair {
9906 start: "[".to_string(),
9907 end: "]".to_string(),
9908 close: false,
9909 surround: false,
9910 newline: true,
9911 },
9912 ],
9913 ..Default::default()
9914 },
9915 autoclose_before: "})]".to_string(),
9916 ..Default::default()
9917 },
9918 Some(tree_sitter_rust::LANGUAGE.into()),
9919 ));
9920
9921 cx.language_registry().add(language.clone());
9922 cx.update_buffer(|buffer, cx| {
9923 buffer.set_language(Some(language), cx);
9924 });
9925
9926 cx.set_state(
9927 &"
9928 ˇ
9929 ˇ
9930 ˇ
9931 "
9932 .unindent(),
9933 );
9934
9935 // ensure only matching closing brackets are skipped over
9936 cx.update_editor(|editor, window, cx| {
9937 editor.handle_input("}", window, cx);
9938 editor.move_left(&MoveLeft, window, cx);
9939 editor.handle_input(")", window, cx);
9940 editor.move_left(&MoveLeft, window, cx);
9941 });
9942 cx.assert_editor_state(
9943 &"
9944 ˇ)}
9945 ˇ)}
9946 ˇ)}
9947 "
9948 .unindent(),
9949 );
9950
9951 // skip-over closing brackets at multiple cursors
9952 cx.update_editor(|editor, window, cx| {
9953 editor.handle_input(")", window, cx);
9954 editor.handle_input("}", window, cx);
9955 });
9956 cx.assert_editor_state(
9957 &"
9958 )}ˇ
9959 )}ˇ
9960 )}ˇ
9961 "
9962 .unindent(),
9963 );
9964
9965 // ignore non-close brackets
9966 cx.update_editor(|editor, window, cx| {
9967 editor.handle_input("]", window, cx);
9968 editor.move_left(&MoveLeft, window, cx);
9969 editor.handle_input("]", window, cx);
9970 });
9971 cx.assert_editor_state(
9972 &"
9973 )}]ˇ]
9974 )}]ˇ]
9975 )}]ˇ]
9976 "
9977 .unindent(),
9978 );
9979}
9980
9981#[gpui::test]
9982async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9983 init_test(cx, |_| {});
9984
9985 let mut cx = EditorTestContext::new(cx).await;
9986
9987 let html_language = Arc::new(
9988 Language::new(
9989 LanguageConfig {
9990 name: "HTML".into(),
9991 brackets: BracketPairConfig {
9992 pairs: vec![
9993 BracketPair {
9994 start: "<".into(),
9995 end: ">".into(),
9996 close: true,
9997 ..Default::default()
9998 },
9999 BracketPair {
10000 start: "{".into(),
10001 end: "}".into(),
10002 close: true,
10003 ..Default::default()
10004 },
10005 BracketPair {
10006 start: "(".into(),
10007 end: ")".into(),
10008 close: true,
10009 ..Default::default()
10010 },
10011 ],
10012 ..Default::default()
10013 },
10014 autoclose_before: "})]>".into(),
10015 ..Default::default()
10016 },
10017 Some(tree_sitter_html::LANGUAGE.into()),
10018 )
10019 .with_injection_query(
10020 r#"
10021 (script_element
10022 (raw_text) @injection.content
10023 (#set! injection.language "javascript"))
10024 "#,
10025 )
10026 .unwrap(),
10027 );
10028
10029 let javascript_language = Arc::new(Language::new(
10030 LanguageConfig {
10031 name: "JavaScript".into(),
10032 brackets: BracketPairConfig {
10033 pairs: vec![
10034 BracketPair {
10035 start: "/*".into(),
10036 end: " */".into(),
10037 close: true,
10038 ..Default::default()
10039 },
10040 BracketPair {
10041 start: "{".into(),
10042 end: "}".into(),
10043 close: true,
10044 ..Default::default()
10045 },
10046 BracketPair {
10047 start: "(".into(),
10048 end: ")".into(),
10049 close: true,
10050 ..Default::default()
10051 },
10052 ],
10053 ..Default::default()
10054 },
10055 autoclose_before: "})]>".into(),
10056 ..Default::default()
10057 },
10058 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10059 ));
10060
10061 cx.language_registry().add(html_language.clone());
10062 cx.language_registry().add(javascript_language);
10063 cx.executor().run_until_parked();
10064
10065 cx.update_buffer(|buffer, cx| {
10066 buffer.set_language(Some(html_language), cx);
10067 });
10068
10069 cx.set_state(
10070 &r#"
10071 <body>ˇ
10072 <script>
10073 var x = 1;ˇ
10074 </script>
10075 </body>ˇ
10076 "#
10077 .unindent(),
10078 );
10079
10080 // Precondition: different languages are active at different locations.
10081 cx.update_editor(|editor, window, cx| {
10082 let snapshot = editor.snapshot(window, cx);
10083 let cursors = editor.selections.ranges::<usize>(cx);
10084 let languages = cursors
10085 .iter()
10086 .map(|c| snapshot.language_at(c.start).unwrap().name())
10087 .collect::<Vec<_>>();
10088 assert_eq!(
10089 languages,
10090 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10091 );
10092 });
10093
10094 // Angle brackets autoclose in HTML, but not JavaScript.
10095 cx.update_editor(|editor, window, cx| {
10096 editor.handle_input("<", window, cx);
10097 editor.handle_input("a", window, cx);
10098 });
10099 cx.assert_editor_state(
10100 &r#"
10101 <body><aˇ>
10102 <script>
10103 var x = 1;<aˇ
10104 </script>
10105 </body><aˇ>
10106 "#
10107 .unindent(),
10108 );
10109
10110 // Curly braces and parens autoclose in both HTML and JavaScript.
10111 cx.update_editor(|editor, window, cx| {
10112 editor.handle_input(" b=", window, cx);
10113 editor.handle_input("{", window, cx);
10114 editor.handle_input("c", window, cx);
10115 editor.handle_input("(", window, cx);
10116 });
10117 cx.assert_editor_state(
10118 &r#"
10119 <body><a b={c(ˇ)}>
10120 <script>
10121 var x = 1;<a b={c(ˇ)}
10122 </script>
10123 </body><a b={c(ˇ)}>
10124 "#
10125 .unindent(),
10126 );
10127
10128 // Brackets that were already autoclosed are skipped.
10129 cx.update_editor(|editor, window, cx| {
10130 editor.handle_input(")", window, cx);
10131 editor.handle_input("d", window, cx);
10132 editor.handle_input("}", window, cx);
10133 });
10134 cx.assert_editor_state(
10135 &r#"
10136 <body><a b={c()d}ˇ>
10137 <script>
10138 var x = 1;<a b={c()d}ˇ
10139 </script>
10140 </body><a b={c()d}ˇ>
10141 "#
10142 .unindent(),
10143 );
10144 cx.update_editor(|editor, window, cx| {
10145 editor.handle_input(">", window, cx);
10146 });
10147 cx.assert_editor_state(
10148 &r#"
10149 <body><a b={c()d}>ˇ
10150 <script>
10151 var x = 1;<a b={c()d}>ˇ
10152 </script>
10153 </body><a b={c()d}>ˇ
10154 "#
10155 .unindent(),
10156 );
10157
10158 // Reset
10159 cx.set_state(
10160 &r#"
10161 <body>ˇ
10162 <script>
10163 var x = 1;ˇ
10164 </script>
10165 </body>ˇ
10166 "#
10167 .unindent(),
10168 );
10169
10170 cx.update_editor(|editor, window, cx| {
10171 editor.handle_input("<", window, cx);
10172 });
10173 cx.assert_editor_state(
10174 &r#"
10175 <body><ˇ>
10176 <script>
10177 var x = 1;<ˇ
10178 </script>
10179 </body><ˇ>
10180 "#
10181 .unindent(),
10182 );
10183
10184 // When backspacing, the closing angle brackets are removed.
10185 cx.update_editor(|editor, window, cx| {
10186 editor.backspace(&Backspace, window, cx);
10187 });
10188 cx.assert_editor_state(
10189 &r#"
10190 <body>ˇ
10191 <script>
10192 var x = 1;ˇ
10193 </script>
10194 </body>ˇ
10195 "#
10196 .unindent(),
10197 );
10198
10199 // Block comments autoclose in JavaScript, but not HTML.
10200 cx.update_editor(|editor, window, cx| {
10201 editor.handle_input("/", window, cx);
10202 editor.handle_input("*", window, cx);
10203 });
10204 cx.assert_editor_state(
10205 &r#"
10206 <body>/*ˇ
10207 <script>
10208 var x = 1;/*ˇ */
10209 </script>
10210 </body>/*ˇ
10211 "#
10212 .unindent(),
10213 );
10214}
10215
10216#[gpui::test]
10217async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10218 init_test(cx, |_| {});
10219
10220 let mut cx = EditorTestContext::new(cx).await;
10221
10222 let rust_language = Arc::new(
10223 Language::new(
10224 LanguageConfig {
10225 name: "Rust".into(),
10226 brackets: serde_json::from_value(json!([
10227 { "start": "{", "end": "}", "close": true, "newline": true },
10228 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10229 ]))
10230 .unwrap(),
10231 autoclose_before: "})]>".into(),
10232 ..Default::default()
10233 },
10234 Some(tree_sitter_rust::LANGUAGE.into()),
10235 )
10236 .with_override_query("(string_literal) @string")
10237 .unwrap(),
10238 );
10239
10240 cx.language_registry().add(rust_language.clone());
10241 cx.update_buffer(|buffer, cx| {
10242 buffer.set_language(Some(rust_language), cx);
10243 });
10244
10245 cx.set_state(
10246 &r#"
10247 let x = ˇ
10248 "#
10249 .unindent(),
10250 );
10251
10252 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10253 cx.update_editor(|editor, window, cx| {
10254 editor.handle_input("\"", window, cx);
10255 });
10256 cx.assert_editor_state(
10257 &r#"
10258 let x = "ˇ"
10259 "#
10260 .unindent(),
10261 );
10262
10263 // Inserting another quotation mark. The cursor moves across the existing
10264 // automatically-inserted quotation mark.
10265 cx.update_editor(|editor, window, cx| {
10266 editor.handle_input("\"", window, cx);
10267 });
10268 cx.assert_editor_state(
10269 &r#"
10270 let x = ""ˇ
10271 "#
10272 .unindent(),
10273 );
10274
10275 // Reset
10276 cx.set_state(
10277 &r#"
10278 let x = ˇ
10279 "#
10280 .unindent(),
10281 );
10282
10283 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10284 cx.update_editor(|editor, window, cx| {
10285 editor.handle_input("\"", window, cx);
10286 editor.handle_input(" ", window, cx);
10287 editor.move_left(&Default::default(), window, cx);
10288 editor.handle_input("\\", window, cx);
10289 editor.handle_input("\"", window, cx);
10290 });
10291 cx.assert_editor_state(
10292 &r#"
10293 let x = "\"ˇ "
10294 "#
10295 .unindent(),
10296 );
10297
10298 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10299 // mark. Nothing is inserted.
10300 cx.update_editor(|editor, window, cx| {
10301 editor.move_right(&Default::default(), window, cx);
10302 editor.handle_input("\"", window, cx);
10303 });
10304 cx.assert_editor_state(
10305 &r#"
10306 let x = "\" "ˇ
10307 "#
10308 .unindent(),
10309 );
10310}
10311
10312#[gpui::test]
10313async fn test_surround_with_pair(cx: &mut TestAppContext) {
10314 init_test(cx, |_| {});
10315
10316 let language = Arc::new(Language::new(
10317 LanguageConfig {
10318 brackets: BracketPairConfig {
10319 pairs: vec![
10320 BracketPair {
10321 start: "{".to_string(),
10322 end: "}".to_string(),
10323 close: true,
10324 surround: true,
10325 newline: true,
10326 },
10327 BracketPair {
10328 start: "/* ".to_string(),
10329 end: "*/".to_string(),
10330 close: true,
10331 surround: true,
10332 ..Default::default()
10333 },
10334 ],
10335 ..Default::default()
10336 },
10337 ..Default::default()
10338 },
10339 Some(tree_sitter_rust::LANGUAGE.into()),
10340 ));
10341
10342 let text = r#"
10343 a
10344 b
10345 c
10346 "#
10347 .unindent();
10348
10349 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10350 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10351 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10352 editor
10353 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10354 .await;
10355
10356 editor.update_in(cx, |editor, window, cx| {
10357 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10358 s.select_display_ranges([
10359 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10360 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10361 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10362 ])
10363 });
10364
10365 editor.handle_input("{", window, cx);
10366 editor.handle_input("{", window, cx);
10367 editor.handle_input("{", window, cx);
10368 assert_eq!(
10369 editor.text(cx),
10370 "
10371 {{{a}}}
10372 {{{b}}}
10373 {{{c}}}
10374 "
10375 .unindent()
10376 );
10377 assert_eq!(
10378 editor.selections.display_ranges(cx),
10379 [
10380 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10381 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10382 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10383 ]
10384 );
10385
10386 editor.undo(&Undo, window, cx);
10387 editor.undo(&Undo, window, cx);
10388 editor.undo(&Undo, window, cx);
10389 assert_eq!(
10390 editor.text(cx),
10391 "
10392 a
10393 b
10394 c
10395 "
10396 .unindent()
10397 );
10398 assert_eq!(
10399 editor.selections.display_ranges(cx),
10400 [
10401 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10402 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10403 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10404 ]
10405 );
10406
10407 // Ensure inserting the first character of a multi-byte bracket pair
10408 // doesn't surround the selections with the bracket.
10409 editor.handle_input("/", window, cx);
10410 assert_eq!(
10411 editor.text(cx),
10412 "
10413 /
10414 /
10415 /
10416 "
10417 .unindent()
10418 );
10419 assert_eq!(
10420 editor.selections.display_ranges(cx),
10421 [
10422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10423 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10424 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10425 ]
10426 );
10427
10428 editor.undo(&Undo, window, cx);
10429 assert_eq!(
10430 editor.text(cx),
10431 "
10432 a
10433 b
10434 c
10435 "
10436 .unindent()
10437 );
10438 assert_eq!(
10439 editor.selections.display_ranges(cx),
10440 [
10441 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10442 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10443 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10444 ]
10445 );
10446
10447 // Ensure inserting the last character of a multi-byte bracket pair
10448 // doesn't surround the selections with the bracket.
10449 editor.handle_input("*", window, cx);
10450 assert_eq!(
10451 editor.text(cx),
10452 "
10453 *
10454 *
10455 *
10456 "
10457 .unindent()
10458 );
10459 assert_eq!(
10460 editor.selections.display_ranges(cx),
10461 [
10462 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10463 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10464 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10465 ]
10466 );
10467 });
10468}
10469
10470#[gpui::test]
10471async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10472 init_test(cx, |_| {});
10473
10474 let language = Arc::new(Language::new(
10475 LanguageConfig {
10476 brackets: BracketPairConfig {
10477 pairs: vec![BracketPair {
10478 start: "{".to_string(),
10479 end: "}".to_string(),
10480 close: true,
10481 surround: true,
10482 newline: true,
10483 }],
10484 ..Default::default()
10485 },
10486 autoclose_before: "}".to_string(),
10487 ..Default::default()
10488 },
10489 Some(tree_sitter_rust::LANGUAGE.into()),
10490 ));
10491
10492 let text = r#"
10493 a
10494 b
10495 c
10496 "#
10497 .unindent();
10498
10499 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10500 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10501 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10502 editor
10503 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10504 .await;
10505
10506 editor.update_in(cx, |editor, window, cx| {
10507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10508 s.select_ranges([
10509 Point::new(0, 1)..Point::new(0, 1),
10510 Point::new(1, 1)..Point::new(1, 1),
10511 Point::new(2, 1)..Point::new(2, 1),
10512 ])
10513 });
10514
10515 editor.handle_input("{", window, cx);
10516 editor.handle_input("{", window, cx);
10517 editor.handle_input("_", window, cx);
10518 assert_eq!(
10519 editor.text(cx),
10520 "
10521 a{{_}}
10522 b{{_}}
10523 c{{_}}
10524 "
10525 .unindent()
10526 );
10527 assert_eq!(
10528 editor.selections.ranges::<Point>(cx),
10529 [
10530 Point::new(0, 4)..Point::new(0, 4),
10531 Point::new(1, 4)..Point::new(1, 4),
10532 Point::new(2, 4)..Point::new(2, 4)
10533 ]
10534 );
10535
10536 editor.backspace(&Default::default(), window, cx);
10537 editor.backspace(&Default::default(), window, cx);
10538 assert_eq!(
10539 editor.text(cx),
10540 "
10541 a{}
10542 b{}
10543 c{}
10544 "
10545 .unindent()
10546 );
10547 assert_eq!(
10548 editor.selections.ranges::<Point>(cx),
10549 [
10550 Point::new(0, 2)..Point::new(0, 2),
10551 Point::new(1, 2)..Point::new(1, 2),
10552 Point::new(2, 2)..Point::new(2, 2)
10553 ]
10554 );
10555
10556 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10557 assert_eq!(
10558 editor.text(cx),
10559 "
10560 a
10561 b
10562 c
10563 "
10564 .unindent()
10565 );
10566 assert_eq!(
10567 editor.selections.ranges::<Point>(cx),
10568 [
10569 Point::new(0, 1)..Point::new(0, 1),
10570 Point::new(1, 1)..Point::new(1, 1),
10571 Point::new(2, 1)..Point::new(2, 1)
10572 ]
10573 );
10574 });
10575}
10576
10577#[gpui::test]
10578async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10579 init_test(cx, |settings| {
10580 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10581 });
10582
10583 let mut cx = EditorTestContext::new(cx).await;
10584
10585 let language = Arc::new(Language::new(
10586 LanguageConfig {
10587 brackets: BracketPairConfig {
10588 pairs: vec![
10589 BracketPair {
10590 start: "{".to_string(),
10591 end: "}".to_string(),
10592 close: true,
10593 surround: true,
10594 newline: true,
10595 },
10596 BracketPair {
10597 start: "(".to_string(),
10598 end: ")".to_string(),
10599 close: true,
10600 surround: true,
10601 newline: true,
10602 },
10603 BracketPair {
10604 start: "[".to_string(),
10605 end: "]".to_string(),
10606 close: false,
10607 surround: true,
10608 newline: true,
10609 },
10610 ],
10611 ..Default::default()
10612 },
10613 autoclose_before: "})]".to_string(),
10614 ..Default::default()
10615 },
10616 Some(tree_sitter_rust::LANGUAGE.into()),
10617 ));
10618
10619 cx.language_registry().add(language.clone());
10620 cx.update_buffer(|buffer, cx| {
10621 buffer.set_language(Some(language), cx);
10622 });
10623
10624 cx.set_state(
10625 &"
10626 {(ˇ)}
10627 [[ˇ]]
10628 {(ˇ)}
10629 "
10630 .unindent(),
10631 );
10632
10633 cx.update_editor(|editor, window, cx| {
10634 editor.backspace(&Default::default(), window, cx);
10635 editor.backspace(&Default::default(), window, cx);
10636 });
10637
10638 cx.assert_editor_state(
10639 &"
10640 ˇ
10641 ˇ]]
10642 ˇ
10643 "
10644 .unindent(),
10645 );
10646
10647 cx.update_editor(|editor, window, cx| {
10648 editor.handle_input("{", window, cx);
10649 editor.handle_input("{", window, cx);
10650 editor.move_right(&MoveRight, window, cx);
10651 editor.move_right(&MoveRight, window, cx);
10652 editor.move_left(&MoveLeft, window, cx);
10653 editor.move_left(&MoveLeft, window, cx);
10654 editor.backspace(&Default::default(), window, cx);
10655 });
10656
10657 cx.assert_editor_state(
10658 &"
10659 {ˇ}
10660 {ˇ}]]
10661 {ˇ}
10662 "
10663 .unindent(),
10664 );
10665
10666 cx.update_editor(|editor, window, cx| {
10667 editor.backspace(&Default::default(), window, cx);
10668 });
10669
10670 cx.assert_editor_state(
10671 &"
10672 ˇ
10673 ˇ]]
10674 ˇ
10675 "
10676 .unindent(),
10677 );
10678}
10679
10680#[gpui::test]
10681async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10682 init_test(cx, |_| {});
10683
10684 let language = Arc::new(Language::new(
10685 LanguageConfig::default(),
10686 Some(tree_sitter_rust::LANGUAGE.into()),
10687 ));
10688
10689 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10690 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10691 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10692 editor
10693 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10694 .await;
10695
10696 editor.update_in(cx, |editor, window, cx| {
10697 editor.set_auto_replace_emoji_shortcode(true);
10698
10699 editor.handle_input("Hello ", window, cx);
10700 editor.handle_input(":wave", window, cx);
10701 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10702
10703 editor.handle_input(":", window, cx);
10704 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10705
10706 editor.handle_input(" :smile", window, cx);
10707 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10708
10709 editor.handle_input(":", window, cx);
10710 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10711
10712 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10713 editor.handle_input(":wave", window, cx);
10714 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10715
10716 editor.handle_input(":", window, cx);
10717 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10718
10719 editor.handle_input(":1", window, cx);
10720 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10721
10722 editor.handle_input(":", window, cx);
10723 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10724
10725 // Ensure shortcode does not get replaced when it is part of a word
10726 editor.handle_input(" Test:wave", window, cx);
10727 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10728
10729 editor.handle_input(":", window, cx);
10730 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10731
10732 editor.set_auto_replace_emoji_shortcode(false);
10733
10734 // Ensure shortcode does not get replaced when auto replace is off
10735 editor.handle_input(" :wave", window, cx);
10736 assert_eq!(
10737 editor.text(cx),
10738 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10739 );
10740
10741 editor.handle_input(":", window, cx);
10742 assert_eq!(
10743 editor.text(cx),
10744 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10745 );
10746 });
10747}
10748
10749#[gpui::test]
10750async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10751 init_test(cx, |_| {});
10752
10753 let (text, insertion_ranges) = marked_text_ranges(
10754 indoc! {"
10755 ˇ
10756 "},
10757 false,
10758 );
10759
10760 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10761 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10762
10763 _ = editor.update_in(cx, |editor, window, cx| {
10764 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10765
10766 editor
10767 .insert_snippet(&insertion_ranges, snippet, window, cx)
10768 .unwrap();
10769
10770 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10771 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10772 assert_eq!(editor.text(cx), expected_text);
10773 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10774 }
10775
10776 assert(
10777 editor,
10778 cx,
10779 indoc! {"
10780 type «» =•
10781 "},
10782 );
10783
10784 assert!(editor.context_menu_visible(), "There should be a matches");
10785 });
10786}
10787
10788#[gpui::test]
10789async fn test_snippets(cx: &mut TestAppContext) {
10790 init_test(cx, |_| {});
10791
10792 let mut cx = EditorTestContext::new(cx).await;
10793
10794 cx.set_state(indoc! {"
10795 a.ˇ b
10796 a.ˇ b
10797 a.ˇ b
10798 "});
10799
10800 cx.update_editor(|editor, window, cx| {
10801 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10802 let insertion_ranges = editor
10803 .selections
10804 .all(cx)
10805 .iter()
10806 .map(|s| s.range())
10807 .collect::<Vec<_>>();
10808 editor
10809 .insert_snippet(&insertion_ranges, snippet, window, cx)
10810 .unwrap();
10811 });
10812
10813 cx.assert_editor_state(indoc! {"
10814 a.f(«oneˇ», two, «threeˇ») b
10815 a.f(«oneˇ», two, «threeˇ») b
10816 a.f(«oneˇ», two, «threeˇ») b
10817 "});
10818
10819 // Can't move earlier than the first tab stop
10820 cx.update_editor(|editor, window, cx| {
10821 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10822 });
10823 cx.assert_editor_state(indoc! {"
10824 a.f(«oneˇ», two, «threeˇ») b
10825 a.f(«oneˇ», two, «threeˇ») b
10826 a.f(«oneˇ», two, «threeˇ») b
10827 "});
10828
10829 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10830 cx.assert_editor_state(indoc! {"
10831 a.f(one, «twoˇ», three) b
10832 a.f(one, «twoˇ», three) b
10833 a.f(one, «twoˇ», three) b
10834 "});
10835
10836 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10837 cx.assert_editor_state(indoc! {"
10838 a.f(«oneˇ», two, «threeˇ») b
10839 a.f(«oneˇ», two, «threeˇ») b
10840 a.f(«oneˇ», two, «threeˇ») b
10841 "});
10842
10843 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10844 cx.assert_editor_state(indoc! {"
10845 a.f(one, «twoˇ», three) b
10846 a.f(one, «twoˇ», three) b
10847 a.f(one, «twoˇ», three) b
10848 "});
10849 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10850 cx.assert_editor_state(indoc! {"
10851 a.f(one, two, three)ˇ b
10852 a.f(one, two, three)ˇ b
10853 a.f(one, two, three)ˇ b
10854 "});
10855
10856 // As soon as the last tab stop is reached, snippet state is gone
10857 cx.update_editor(|editor, window, cx| {
10858 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10859 });
10860 cx.assert_editor_state(indoc! {"
10861 a.f(one, two, three)ˇ b
10862 a.f(one, two, three)ˇ b
10863 a.f(one, two, three)ˇ b
10864 "});
10865}
10866
10867#[gpui::test]
10868async fn test_snippet_indentation(cx: &mut TestAppContext) {
10869 init_test(cx, |_| {});
10870
10871 let mut cx = EditorTestContext::new(cx).await;
10872
10873 cx.update_editor(|editor, window, cx| {
10874 let snippet = Snippet::parse(indoc! {"
10875 /*
10876 * Multiline comment with leading indentation
10877 *
10878 * $1
10879 */
10880 $0"})
10881 .unwrap();
10882 let insertion_ranges = editor
10883 .selections
10884 .all(cx)
10885 .iter()
10886 .map(|s| s.range())
10887 .collect::<Vec<_>>();
10888 editor
10889 .insert_snippet(&insertion_ranges, snippet, window, cx)
10890 .unwrap();
10891 });
10892
10893 cx.assert_editor_state(indoc! {"
10894 /*
10895 * Multiline comment with leading indentation
10896 *
10897 * ˇ
10898 */
10899 "});
10900
10901 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10902 cx.assert_editor_state(indoc! {"
10903 /*
10904 * Multiline comment with leading indentation
10905 *
10906 *•
10907 */
10908 ˇ"});
10909}
10910
10911#[gpui::test]
10912async fn test_document_format_during_save(cx: &mut TestAppContext) {
10913 init_test(cx, |_| {});
10914
10915 let fs = FakeFs::new(cx.executor());
10916 fs.insert_file(path!("/file.rs"), Default::default()).await;
10917
10918 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10919
10920 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10921 language_registry.add(rust_lang());
10922 let mut fake_servers = language_registry.register_fake_lsp(
10923 "Rust",
10924 FakeLspAdapter {
10925 capabilities: lsp::ServerCapabilities {
10926 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10927 ..Default::default()
10928 },
10929 ..Default::default()
10930 },
10931 );
10932
10933 let buffer = project
10934 .update(cx, |project, cx| {
10935 project.open_local_buffer(path!("/file.rs"), cx)
10936 })
10937 .await
10938 .unwrap();
10939
10940 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10941 let (editor, cx) = cx.add_window_view(|window, cx| {
10942 build_editor_with_project(project.clone(), buffer, window, cx)
10943 });
10944 editor.update_in(cx, |editor, window, cx| {
10945 editor.set_text("one\ntwo\nthree\n", window, cx)
10946 });
10947 assert!(cx.read(|cx| editor.is_dirty(cx)));
10948
10949 cx.executor().start_waiting();
10950 let fake_server = fake_servers.next().await.unwrap();
10951
10952 {
10953 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10954 move |params, _| async move {
10955 assert_eq!(
10956 params.text_document.uri,
10957 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10958 );
10959 assert_eq!(params.options.tab_size, 4);
10960 Ok(Some(vec![lsp::TextEdit::new(
10961 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10962 ", ".to_string(),
10963 )]))
10964 },
10965 );
10966 let save = editor
10967 .update_in(cx, |editor, window, cx| {
10968 editor.save(
10969 SaveOptions {
10970 format: true,
10971 autosave: false,
10972 },
10973 project.clone(),
10974 window,
10975 cx,
10976 )
10977 })
10978 .unwrap();
10979 cx.executor().start_waiting();
10980 save.await;
10981
10982 assert_eq!(
10983 editor.update(cx, |editor, cx| editor.text(cx)),
10984 "one, two\nthree\n"
10985 );
10986 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10987 }
10988
10989 {
10990 editor.update_in(cx, |editor, window, cx| {
10991 editor.set_text("one\ntwo\nthree\n", window, cx)
10992 });
10993 assert!(cx.read(|cx| editor.is_dirty(cx)));
10994
10995 // Ensure we can still save even if formatting hangs.
10996 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10997 move |params, _| async move {
10998 assert_eq!(
10999 params.text_document.uri,
11000 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11001 );
11002 futures::future::pending::<()>().await;
11003 unreachable!()
11004 },
11005 );
11006 let save = editor
11007 .update_in(cx, |editor, window, cx| {
11008 editor.save(
11009 SaveOptions {
11010 format: true,
11011 autosave: false,
11012 },
11013 project.clone(),
11014 window,
11015 cx,
11016 )
11017 })
11018 .unwrap();
11019 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11020 cx.executor().start_waiting();
11021 save.await;
11022 assert_eq!(
11023 editor.update(cx, |editor, cx| editor.text(cx)),
11024 "one\ntwo\nthree\n"
11025 );
11026 }
11027
11028 // Set rust language override and assert overridden tabsize is sent to language server
11029 update_test_language_settings(cx, |settings| {
11030 settings.languages.0.insert(
11031 "Rust".into(),
11032 LanguageSettingsContent {
11033 tab_size: NonZeroU32::new(8),
11034 ..Default::default()
11035 },
11036 );
11037 });
11038
11039 {
11040 editor.update_in(cx, |editor, window, cx| {
11041 editor.set_text("somehting_new\n", window, cx)
11042 });
11043 assert!(cx.read(|cx| editor.is_dirty(cx)));
11044 let _formatting_request_signal = fake_server
11045 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11046 assert_eq!(
11047 params.text_document.uri,
11048 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11049 );
11050 assert_eq!(params.options.tab_size, 8);
11051 Ok(Some(vec![]))
11052 });
11053 let save = editor
11054 .update_in(cx, |editor, window, cx| {
11055 editor.save(
11056 SaveOptions {
11057 format: true,
11058 autosave: false,
11059 },
11060 project.clone(),
11061 window,
11062 cx,
11063 )
11064 })
11065 .unwrap();
11066 cx.executor().start_waiting();
11067 save.await;
11068 }
11069}
11070
11071#[gpui::test]
11072async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11073 init_test(cx, |settings| {
11074 settings.defaults.ensure_final_newline_on_save = Some(false);
11075 });
11076
11077 let fs = FakeFs::new(cx.executor());
11078 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11079
11080 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11081
11082 let buffer = project
11083 .update(cx, |project, cx| {
11084 project.open_local_buffer(path!("/file.txt"), cx)
11085 })
11086 .await
11087 .unwrap();
11088
11089 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11090 let (editor, cx) = cx.add_window_view(|window, cx| {
11091 build_editor_with_project(project.clone(), buffer, window, cx)
11092 });
11093 editor.update_in(cx, |editor, window, cx| {
11094 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11095 s.select_ranges([0..0])
11096 });
11097 });
11098 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11099
11100 editor.update_in(cx, |editor, window, cx| {
11101 editor.handle_input("\n", window, cx)
11102 });
11103 cx.run_until_parked();
11104 save(&editor, &project, cx).await;
11105 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11106
11107 editor.update_in(cx, |editor, window, cx| {
11108 editor.undo(&Default::default(), window, cx);
11109 });
11110 save(&editor, &project, cx).await;
11111 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11112
11113 editor.update_in(cx, |editor, window, cx| {
11114 editor.redo(&Default::default(), window, cx);
11115 });
11116 cx.run_until_parked();
11117 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11118
11119 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11120 let save = editor
11121 .update_in(cx, |editor, window, cx| {
11122 editor.save(
11123 SaveOptions {
11124 format: true,
11125 autosave: false,
11126 },
11127 project.clone(),
11128 window,
11129 cx,
11130 )
11131 })
11132 .unwrap();
11133 cx.executor().start_waiting();
11134 save.await;
11135 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11136 }
11137}
11138
11139#[gpui::test]
11140async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11141 init_test(cx, |_| {});
11142
11143 let cols = 4;
11144 let rows = 10;
11145 let sample_text_1 = sample_text(rows, cols, 'a');
11146 assert_eq!(
11147 sample_text_1,
11148 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11149 );
11150 let sample_text_2 = sample_text(rows, cols, 'l');
11151 assert_eq!(
11152 sample_text_2,
11153 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11154 );
11155 let sample_text_3 = sample_text(rows, cols, 'v');
11156 assert_eq!(
11157 sample_text_3,
11158 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11159 );
11160
11161 let fs = FakeFs::new(cx.executor());
11162 fs.insert_tree(
11163 path!("/a"),
11164 json!({
11165 "main.rs": sample_text_1,
11166 "other.rs": sample_text_2,
11167 "lib.rs": sample_text_3,
11168 }),
11169 )
11170 .await;
11171
11172 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11173 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11174 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11175
11176 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11177 language_registry.add(rust_lang());
11178 let mut fake_servers = language_registry.register_fake_lsp(
11179 "Rust",
11180 FakeLspAdapter {
11181 capabilities: lsp::ServerCapabilities {
11182 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11183 ..Default::default()
11184 },
11185 ..Default::default()
11186 },
11187 );
11188
11189 let worktree = project.update(cx, |project, cx| {
11190 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11191 assert_eq!(worktrees.len(), 1);
11192 worktrees.pop().unwrap()
11193 });
11194 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11195
11196 let buffer_1 = project
11197 .update(cx, |project, cx| {
11198 project.open_buffer((worktree_id, "main.rs"), cx)
11199 })
11200 .await
11201 .unwrap();
11202 let buffer_2 = project
11203 .update(cx, |project, cx| {
11204 project.open_buffer((worktree_id, "other.rs"), cx)
11205 })
11206 .await
11207 .unwrap();
11208 let buffer_3 = project
11209 .update(cx, |project, cx| {
11210 project.open_buffer((worktree_id, "lib.rs"), cx)
11211 })
11212 .await
11213 .unwrap();
11214
11215 let multi_buffer = cx.new(|cx| {
11216 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11217 multi_buffer.push_excerpts(
11218 buffer_1.clone(),
11219 [
11220 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11221 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11222 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11223 ],
11224 cx,
11225 );
11226 multi_buffer.push_excerpts(
11227 buffer_2.clone(),
11228 [
11229 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11230 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11231 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11232 ],
11233 cx,
11234 );
11235 multi_buffer.push_excerpts(
11236 buffer_3.clone(),
11237 [
11238 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11239 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11240 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11241 ],
11242 cx,
11243 );
11244 multi_buffer
11245 });
11246 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11247 Editor::new(
11248 EditorMode::full(),
11249 multi_buffer,
11250 Some(project.clone()),
11251 window,
11252 cx,
11253 )
11254 });
11255
11256 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11257 editor.change_selections(
11258 SelectionEffects::scroll(Autoscroll::Next),
11259 window,
11260 cx,
11261 |s| s.select_ranges(Some(1..2)),
11262 );
11263 editor.insert("|one|two|three|", window, cx);
11264 });
11265 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11266 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11267 editor.change_selections(
11268 SelectionEffects::scroll(Autoscroll::Next),
11269 window,
11270 cx,
11271 |s| s.select_ranges(Some(60..70)),
11272 );
11273 editor.insert("|four|five|six|", window, cx);
11274 });
11275 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11276
11277 // First two buffers should be edited, but not the third one.
11278 assert_eq!(
11279 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11280 "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}",
11281 );
11282 buffer_1.update(cx, |buffer, _| {
11283 assert!(buffer.is_dirty());
11284 assert_eq!(
11285 buffer.text(),
11286 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11287 )
11288 });
11289 buffer_2.update(cx, |buffer, _| {
11290 assert!(buffer.is_dirty());
11291 assert_eq!(
11292 buffer.text(),
11293 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11294 )
11295 });
11296 buffer_3.update(cx, |buffer, _| {
11297 assert!(!buffer.is_dirty());
11298 assert_eq!(buffer.text(), sample_text_3,)
11299 });
11300 cx.executor().run_until_parked();
11301
11302 cx.executor().start_waiting();
11303 let save = multi_buffer_editor
11304 .update_in(cx, |editor, window, cx| {
11305 editor.save(
11306 SaveOptions {
11307 format: true,
11308 autosave: false,
11309 },
11310 project.clone(),
11311 window,
11312 cx,
11313 )
11314 })
11315 .unwrap();
11316
11317 let fake_server = fake_servers.next().await.unwrap();
11318 fake_server
11319 .server
11320 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11321 Ok(Some(vec![lsp::TextEdit::new(
11322 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11323 format!("[{} formatted]", params.text_document.uri),
11324 )]))
11325 })
11326 .detach();
11327 save.await;
11328
11329 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11330 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11331 assert_eq!(
11332 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11333 uri!(
11334 "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}"
11335 ),
11336 );
11337 buffer_1.update(cx, |buffer, _| {
11338 assert!(!buffer.is_dirty());
11339 assert_eq!(
11340 buffer.text(),
11341 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11342 )
11343 });
11344 buffer_2.update(cx, |buffer, _| {
11345 assert!(!buffer.is_dirty());
11346 assert_eq!(
11347 buffer.text(),
11348 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11349 )
11350 });
11351 buffer_3.update(cx, |buffer, _| {
11352 assert!(!buffer.is_dirty());
11353 assert_eq!(buffer.text(), sample_text_3,)
11354 });
11355}
11356
11357#[gpui::test]
11358async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11359 init_test(cx, |_| {});
11360
11361 let fs = FakeFs::new(cx.executor());
11362 fs.insert_tree(
11363 path!("/dir"),
11364 json!({
11365 "file1.rs": "fn main() { println!(\"hello\"); }",
11366 "file2.rs": "fn test() { println!(\"test\"); }",
11367 "file3.rs": "fn other() { println!(\"other\"); }\n",
11368 }),
11369 )
11370 .await;
11371
11372 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11373 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11374 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11375
11376 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11377 language_registry.add(rust_lang());
11378
11379 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11380 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11381
11382 // Open three buffers
11383 let buffer_1 = project
11384 .update(cx, |project, cx| {
11385 project.open_buffer((worktree_id, "file1.rs"), cx)
11386 })
11387 .await
11388 .unwrap();
11389 let buffer_2 = project
11390 .update(cx, |project, cx| {
11391 project.open_buffer((worktree_id, "file2.rs"), cx)
11392 })
11393 .await
11394 .unwrap();
11395 let buffer_3 = project
11396 .update(cx, |project, cx| {
11397 project.open_buffer((worktree_id, "file3.rs"), cx)
11398 })
11399 .await
11400 .unwrap();
11401
11402 // Create a multi-buffer with all three buffers
11403 let multi_buffer = cx.new(|cx| {
11404 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11405 multi_buffer.push_excerpts(
11406 buffer_1.clone(),
11407 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11408 cx,
11409 );
11410 multi_buffer.push_excerpts(
11411 buffer_2.clone(),
11412 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11413 cx,
11414 );
11415 multi_buffer.push_excerpts(
11416 buffer_3.clone(),
11417 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11418 cx,
11419 );
11420 multi_buffer
11421 });
11422
11423 let editor = cx.new_window_entity(|window, cx| {
11424 Editor::new(
11425 EditorMode::full(),
11426 multi_buffer,
11427 Some(project.clone()),
11428 window,
11429 cx,
11430 )
11431 });
11432
11433 // Edit only the first buffer
11434 editor.update_in(cx, |editor, window, cx| {
11435 editor.change_selections(
11436 SelectionEffects::scroll(Autoscroll::Next),
11437 window,
11438 cx,
11439 |s| s.select_ranges(Some(10..10)),
11440 );
11441 editor.insert("// edited", window, cx);
11442 });
11443
11444 // Verify that only buffer 1 is dirty
11445 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11446 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11447 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11448
11449 // Get write counts after file creation (files were created with initial content)
11450 // We expect each file to have been written once during creation
11451 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11452 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11453 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11454
11455 // Perform autosave
11456 let save_task = editor.update_in(cx, |editor, window, cx| {
11457 editor.save(
11458 SaveOptions {
11459 format: true,
11460 autosave: true,
11461 },
11462 project.clone(),
11463 window,
11464 cx,
11465 )
11466 });
11467 save_task.await.unwrap();
11468
11469 // Only the dirty buffer should have been saved
11470 assert_eq!(
11471 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11472 1,
11473 "Buffer 1 was dirty, so it should have been written once during autosave"
11474 );
11475 assert_eq!(
11476 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11477 0,
11478 "Buffer 2 was clean, so it should not have been written during autosave"
11479 );
11480 assert_eq!(
11481 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11482 0,
11483 "Buffer 3 was clean, so it should not have been written during autosave"
11484 );
11485
11486 // Verify buffer states after autosave
11487 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11488 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11489 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11490
11491 // Now perform a manual save (format = true)
11492 let save_task = editor.update_in(cx, |editor, window, cx| {
11493 editor.save(
11494 SaveOptions {
11495 format: true,
11496 autosave: false,
11497 },
11498 project.clone(),
11499 window,
11500 cx,
11501 )
11502 });
11503 save_task.await.unwrap();
11504
11505 // During manual save, clean buffers don't get written to disk
11506 // They just get did_save called for language server notifications
11507 assert_eq!(
11508 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11509 1,
11510 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11511 );
11512 assert_eq!(
11513 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11514 0,
11515 "Buffer 2 should not have been written at all"
11516 );
11517 assert_eq!(
11518 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11519 0,
11520 "Buffer 3 should not have been written at all"
11521 );
11522}
11523
11524async fn setup_range_format_test(
11525 cx: &mut TestAppContext,
11526) -> (
11527 Entity<Project>,
11528 Entity<Editor>,
11529 &mut gpui::VisualTestContext,
11530 lsp::FakeLanguageServer,
11531) {
11532 init_test(cx, |_| {});
11533
11534 let fs = FakeFs::new(cx.executor());
11535 fs.insert_file(path!("/file.rs"), Default::default()).await;
11536
11537 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11538
11539 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11540 language_registry.add(rust_lang());
11541 let mut fake_servers = language_registry.register_fake_lsp(
11542 "Rust",
11543 FakeLspAdapter {
11544 capabilities: lsp::ServerCapabilities {
11545 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11546 ..lsp::ServerCapabilities::default()
11547 },
11548 ..FakeLspAdapter::default()
11549 },
11550 );
11551
11552 let buffer = project
11553 .update(cx, |project, cx| {
11554 project.open_local_buffer(path!("/file.rs"), cx)
11555 })
11556 .await
11557 .unwrap();
11558
11559 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11560 let (editor, cx) = cx.add_window_view(|window, cx| {
11561 build_editor_with_project(project.clone(), buffer, window, cx)
11562 });
11563
11564 cx.executor().start_waiting();
11565 let fake_server = fake_servers.next().await.unwrap();
11566
11567 (project, editor, cx, fake_server)
11568}
11569
11570#[gpui::test]
11571async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11572 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11573
11574 editor.update_in(cx, |editor, window, cx| {
11575 editor.set_text("one\ntwo\nthree\n", window, cx)
11576 });
11577 assert!(cx.read(|cx| editor.is_dirty(cx)));
11578
11579 let save = editor
11580 .update_in(cx, |editor, window, cx| {
11581 editor.save(
11582 SaveOptions {
11583 format: true,
11584 autosave: false,
11585 },
11586 project.clone(),
11587 window,
11588 cx,
11589 )
11590 })
11591 .unwrap();
11592 fake_server
11593 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11594 assert_eq!(
11595 params.text_document.uri,
11596 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11597 );
11598 assert_eq!(params.options.tab_size, 4);
11599 Ok(Some(vec![lsp::TextEdit::new(
11600 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11601 ", ".to_string(),
11602 )]))
11603 })
11604 .next()
11605 .await;
11606 cx.executor().start_waiting();
11607 save.await;
11608 assert_eq!(
11609 editor.update(cx, |editor, cx| editor.text(cx)),
11610 "one, two\nthree\n"
11611 );
11612 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11613}
11614
11615#[gpui::test]
11616async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11617 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11618
11619 editor.update_in(cx, |editor, window, cx| {
11620 editor.set_text("one\ntwo\nthree\n", window, cx)
11621 });
11622 assert!(cx.read(|cx| editor.is_dirty(cx)));
11623
11624 // Test that save still works when formatting hangs
11625 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11626 move |params, _| async move {
11627 assert_eq!(
11628 params.text_document.uri,
11629 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11630 );
11631 futures::future::pending::<()>().await;
11632 unreachable!()
11633 },
11634 );
11635 let save = editor
11636 .update_in(cx, |editor, window, cx| {
11637 editor.save(
11638 SaveOptions {
11639 format: true,
11640 autosave: false,
11641 },
11642 project.clone(),
11643 window,
11644 cx,
11645 )
11646 })
11647 .unwrap();
11648 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11649 cx.executor().start_waiting();
11650 save.await;
11651 assert_eq!(
11652 editor.update(cx, |editor, cx| editor.text(cx)),
11653 "one\ntwo\nthree\n"
11654 );
11655 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11656}
11657
11658#[gpui::test]
11659async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11660 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11661
11662 // Buffer starts clean, no formatting should be requested
11663 let save = editor
11664 .update_in(cx, |editor, window, cx| {
11665 editor.save(
11666 SaveOptions {
11667 format: false,
11668 autosave: false,
11669 },
11670 project.clone(),
11671 window,
11672 cx,
11673 )
11674 })
11675 .unwrap();
11676 let _pending_format_request = fake_server
11677 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11678 panic!("Should not be invoked");
11679 })
11680 .next();
11681 cx.executor().start_waiting();
11682 save.await;
11683 cx.run_until_parked();
11684}
11685
11686#[gpui::test]
11687async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11688 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11689
11690 // Set Rust language override and assert overridden tabsize is sent to language server
11691 update_test_language_settings(cx, |settings| {
11692 settings.languages.0.insert(
11693 "Rust".into(),
11694 LanguageSettingsContent {
11695 tab_size: NonZeroU32::new(8),
11696 ..Default::default()
11697 },
11698 );
11699 });
11700
11701 editor.update_in(cx, |editor, window, cx| {
11702 editor.set_text("something_new\n", window, cx)
11703 });
11704 assert!(cx.read(|cx| editor.is_dirty(cx)));
11705 let save = editor
11706 .update_in(cx, |editor, window, cx| {
11707 editor.save(
11708 SaveOptions {
11709 format: true,
11710 autosave: false,
11711 },
11712 project.clone(),
11713 window,
11714 cx,
11715 )
11716 })
11717 .unwrap();
11718 fake_server
11719 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11720 assert_eq!(
11721 params.text_document.uri,
11722 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11723 );
11724 assert_eq!(params.options.tab_size, 8);
11725 Ok(Some(Vec::new()))
11726 })
11727 .next()
11728 .await;
11729 save.await;
11730}
11731
11732#[gpui::test]
11733async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11734 init_test(cx, |settings| {
11735 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11736 Formatter::LanguageServer { name: None },
11737 )))
11738 });
11739
11740 let fs = FakeFs::new(cx.executor());
11741 fs.insert_file(path!("/file.rs"), Default::default()).await;
11742
11743 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11744
11745 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11746 language_registry.add(Arc::new(Language::new(
11747 LanguageConfig {
11748 name: "Rust".into(),
11749 matcher: LanguageMatcher {
11750 path_suffixes: vec!["rs".to_string()],
11751 ..Default::default()
11752 },
11753 ..LanguageConfig::default()
11754 },
11755 Some(tree_sitter_rust::LANGUAGE.into()),
11756 )));
11757 update_test_language_settings(cx, |settings| {
11758 // Enable Prettier formatting for the same buffer, and ensure
11759 // LSP is called instead of Prettier.
11760 settings.defaults.prettier = Some(PrettierSettings {
11761 allowed: true,
11762 ..PrettierSettings::default()
11763 });
11764 });
11765 let mut fake_servers = language_registry.register_fake_lsp(
11766 "Rust",
11767 FakeLspAdapter {
11768 capabilities: lsp::ServerCapabilities {
11769 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11770 ..Default::default()
11771 },
11772 ..Default::default()
11773 },
11774 );
11775
11776 let buffer = project
11777 .update(cx, |project, cx| {
11778 project.open_local_buffer(path!("/file.rs"), cx)
11779 })
11780 .await
11781 .unwrap();
11782
11783 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11784 let (editor, cx) = cx.add_window_view(|window, cx| {
11785 build_editor_with_project(project.clone(), buffer, window, cx)
11786 });
11787 editor.update_in(cx, |editor, window, cx| {
11788 editor.set_text("one\ntwo\nthree\n", window, cx)
11789 });
11790
11791 cx.executor().start_waiting();
11792 let fake_server = fake_servers.next().await.unwrap();
11793
11794 let format = editor
11795 .update_in(cx, |editor, window, cx| {
11796 editor.perform_format(
11797 project.clone(),
11798 FormatTrigger::Manual,
11799 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11800 window,
11801 cx,
11802 )
11803 })
11804 .unwrap();
11805 fake_server
11806 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11807 assert_eq!(
11808 params.text_document.uri,
11809 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11810 );
11811 assert_eq!(params.options.tab_size, 4);
11812 Ok(Some(vec![lsp::TextEdit::new(
11813 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11814 ", ".to_string(),
11815 )]))
11816 })
11817 .next()
11818 .await;
11819 cx.executor().start_waiting();
11820 format.await;
11821 assert_eq!(
11822 editor.update(cx, |editor, cx| editor.text(cx)),
11823 "one, two\nthree\n"
11824 );
11825
11826 editor.update_in(cx, |editor, window, cx| {
11827 editor.set_text("one\ntwo\nthree\n", window, cx)
11828 });
11829 // Ensure we don't lock if formatting hangs.
11830 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11831 move |params, _| async move {
11832 assert_eq!(
11833 params.text_document.uri,
11834 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11835 );
11836 futures::future::pending::<()>().await;
11837 unreachable!()
11838 },
11839 );
11840 let format = editor
11841 .update_in(cx, |editor, window, cx| {
11842 editor.perform_format(
11843 project,
11844 FormatTrigger::Manual,
11845 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11846 window,
11847 cx,
11848 )
11849 })
11850 .unwrap();
11851 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11852 cx.executor().start_waiting();
11853 format.await;
11854 assert_eq!(
11855 editor.update(cx, |editor, cx| editor.text(cx)),
11856 "one\ntwo\nthree\n"
11857 );
11858}
11859
11860#[gpui::test]
11861async fn test_multiple_formatters(cx: &mut TestAppContext) {
11862 init_test(cx, |settings| {
11863 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11864 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11865 Formatter::LanguageServer { name: None },
11866 Formatter::CodeActions(
11867 [
11868 ("code-action-1".into(), true),
11869 ("code-action-2".into(), true),
11870 ]
11871 .into_iter()
11872 .collect(),
11873 ),
11874 ])))
11875 });
11876
11877 let fs = FakeFs::new(cx.executor());
11878 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11879 .await;
11880
11881 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11882 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11883 language_registry.add(rust_lang());
11884
11885 let mut fake_servers = language_registry.register_fake_lsp(
11886 "Rust",
11887 FakeLspAdapter {
11888 capabilities: lsp::ServerCapabilities {
11889 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11890 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11891 commands: vec!["the-command-for-code-action-1".into()],
11892 ..Default::default()
11893 }),
11894 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11895 ..Default::default()
11896 },
11897 ..Default::default()
11898 },
11899 );
11900
11901 let buffer = project
11902 .update(cx, |project, cx| {
11903 project.open_local_buffer(path!("/file.rs"), cx)
11904 })
11905 .await
11906 .unwrap();
11907
11908 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11909 let (editor, cx) = cx.add_window_view(|window, cx| {
11910 build_editor_with_project(project.clone(), buffer, window, cx)
11911 });
11912
11913 cx.executor().start_waiting();
11914
11915 let fake_server = fake_servers.next().await.unwrap();
11916 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11917 move |_params, _| async move {
11918 Ok(Some(vec![lsp::TextEdit::new(
11919 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11920 "applied-formatting\n".to_string(),
11921 )]))
11922 },
11923 );
11924 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11925 move |params, _| async move {
11926 assert_eq!(
11927 params.context.only,
11928 Some(vec!["code-action-1".into(), "code-action-2".into()])
11929 );
11930 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11931 Ok(Some(vec![
11932 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11933 kind: Some("code-action-1".into()),
11934 edit: Some(lsp::WorkspaceEdit::new(
11935 [(
11936 uri.clone(),
11937 vec![lsp::TextEdit::new(
11938 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11939 "applied-code-action-1-edit\n".to_string(),
11940 )],
11941 )]
11942 .into_iter()
11943 .collect(),
11944 )),
11945 command: Some(lsp::Command {
11946 command: "the-command-for-code-action-1".into(),
11947 ..Default::default()
11948 }),
11949 ..Default::default()
11950 }),
11951 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11952 kind: Some("code-action-2".into()),
11953 edit: Some(lsp::WorkspaceEdit::new(
11954 [(
11955 uri,
11956 vec![lsp::TextEdit::new(
11957 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11958 "applied-code-action-2-edit\n".to_string(),
11959 )],
11960 )]
11961 .into_iter()
11962 .collect(),
11963 )),
11964 ..Default::default()
11965 }),
11966 ]))
11967 },
11968 );
11969
11970 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11971 move |params, _| async move { Ok(params) }
11972 });
11973
11974 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11975 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11976 let fake = fake_server.clone();
11977 let lock = command_lock.clone();
11978 move |params, _| {
11979 assert_eq!(params.command, "the-command-for-code-action-1");
11980 let fake = fake.clone();
11981 let lock = lock.clone();
11982 async move {
11983 lock.lock().await;
11984 fake.server
11985 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11986 label: None,
11987 edit: lsp::WorkspaceEdit {
11988 changes: Some(
11989 [(
11990 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11991 vec![lsp::TextEdit {
11992 range: lsp::Range::new(
11993 lsp::Position::new(0, 0),
11994 lsp::Position::new(0, 0),
11995 ),
11996 new_text: "applied-code-action-1-command\n".into(),
11997 }],
11998 )]
11999 .into_iter()
12000 .collect(),
12001 ),
12002 ..Default::default()
12003 },
12004 })
12005 .await
12006 .into_response()
12007 .unwrap();
12008 Ok(Some(json!(null)))
12009 }
12010 }
12011 });
12012
12013 cx.executor().start_waiting();
12014 editor
12015 .update_in(cx, |editor, window, cx| {
12016 editor.perform_format(
12017 project.clone(),
12018 FormatTrigger::Manual,
12019 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12020 window,
12021 cx,
12022 )
12023 })
12024 .unwrap()
12025 .await;
12026 editor.update(cx, |editor, cx| {
12027 assert_eq!(
12028 editor.text(cx),
12029 r#"
12030 applied-code-action-2-edit
12031 applied-code-action-1-command
12032 applied-code-action-1-edit
12033 applied-formatting
12034 one
12035 two
12036 three
12037 "#
12038 .unindent()
12039 );
12040 });
12041
12042 editor.update_in(cx, |editor, window, cx| {
12043 editor.undo(&Default::default(), window, cx);
12044 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12045 });
12046
12047 // Perform a manual edit while waiting for an LSP command
12048 // that's being run as part of a formatting code action.
12049 let lock_guard = command_lock.lock().await;
12050 let format = editor
12051 .update_in(cx, |editor, window, cx| {
12052 editor.perform_format(
12053 project.clone(),
12054 FormatTrigger::Manual,
12055 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12056 window,
12057 cx,
12058 )
12059 })
12060 .unwrap();
12061 cx.run_until_parked();
12062 editor.update(cx, |editor, cx| {
12063 assert_eq!(
12064 editor.text(cx),
12065 r#"
12066 applied-code-action-1-edit
12067 applied-formatting
12068 one
12069 two
12070 three
12071 "#
12072 .unindent()
12073 );
12074
12075 editor.buffer.update(cx, |buffer, cx| {
12076 let ix = buffer.len(cx);
12077 buffer.edit([(ix..ix, "edited\n")], None, cx);
12078 });
12079 });
12080
12081 // Allow the LSP command to proceed. Because the buffer was edited,
12082 // the second code action will not be run.
12083 drop(lock_guard);
12084 format.await;
12085 editor.update_in(cx, |editor, window, cx| {
12086 assert_eq!(
12087 editor.text(cx),
12088 r#"
12089 applied-code-action-1-command
12090 applied-code-action-1-edit
12091 applied-formatting
12092 one
12093 two
12094 three
12095 edited
12096 "#
12097 .unindent()
12098 );
12099
12100 // The manual edit is undone first, because it is the last thing the user did
12101 // (even though the command completed afterwards).
12102 editor.undo(&Default::default(), window, cx);
12103 assert_eq!(
12104 editor.text(cx),
12105 r#"
12106 applied-code-action-1-command
12107 applied-code-action-1-edit
12108 applied-formatting
12109 one
12110 two
12111 three
12112 "#
12113 .unindent()
12114 );
12115
12116 // All the formatting (including the command, which completed after the manual edit)
12117 // is undone together.
12118 editor.undo(&Default::default(), window, cx);
12119 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12120 });
12121}
12122
12123#[gpui::test]
12124async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12125 init_test(cx, |settings| {
12126 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12127 Formatter::LanguageServer { name: None },
12128 ])))
12129 });
12130
12131 let fs = FakeFs::new(cx.executor());
12132 fs.insert_file(path!("/file.ts"), Default::default()).await;
12133
12134 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12135
12136 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12137 language_registry.add(Arc::new(Language::new(
12138 LanguageConfig {
12139 name: "TypeScript".into(),
12140 matcher: LanguageMatcher {
12141 path_suffixes: vec!["ts".to_string()],
12142 ..Default::default()
12143 },
12144 ..LanguageConfig::default()
12145 },
12146 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12147 )));
12148 update_test_language_settings(cx, |settings| {
12149 settings.defaults.prettier = Some(PrettierSettings {
12150 allowed: true,
12151 ..PrettierSettings::default()
12152 });
12153 });
12154 let mut fake_servers = language_registry.register_fake_lsp(
12155 "TypeScript",
12156 FakeLspAdapter {
12157 capabilities: lsp::ServerCapabilities {
12158 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12159 ..Default::default()
12160 },
12161 ..Default::default()
12162 },
12163 );
12164
12165 let buffer = project
12166 .update(cx, |project, cx| {
12167 project.open_local_buffer(path!("/file.ts"), cx)
12168 })
12169 .await
12170 .unwrap();
12171
12172 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12173 let (editor, cx) = cx.add_window_view(|window, cx| {
12174 build_editor_with_project(project.clone(), buffer, window, cx)
12175 });
12176 editor.update_in(cx, |editor, window, cx| {
12177 editor.set_text(
12178 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12179 window,
12180 cx,
12181 )
12182 });
12183
12184 cx.executor().start_waiting();
12185 let fake_server = fake_servers.next().await.unwrap();
12186
12187 let format = editor
12188 .update_in(cx, |editor, window, cx| {
12189 editor.perform_code_action_kind(
12190 project.clone(),
12191 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12192 window,
12193 cx,
12194 )
12195 })
12196 .unwrap();
12197 fake_server
12198 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12199 assert_eq!(
12200 params.text_document.uri,
12201 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12202 );
12203 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12204 lsp::CodeAction {
12205 title: "Organize Imports".to_string(),
12206 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12207 edit: Some(lsp::WorkspaceEdit {
12208 changes: Some(
12209 [(
12210 params.text_document.uri.clone(),
12211 vec![lsp::TextEdit::new(
12212 lsp::Range::new(
12213 lsp::Position::new(1, 0),
12214 lsp::Position::new(2, 0),
12215 ),
12216 "".to_string(),
12217 )],
12218 )]
12219 .into_iter()
12220 .collect(),
12221 ),
12222 ..Default::default()
12223 }),
12224 ..Default::default()
12225 },
12226 )]))
12227 })
12228 .next()
12229 .await;
12230 cx.executor().start_waiting();
12231 format.await;
12232 assert_eq!(
12233 editor.update(cx, |editor, cx| editor.text(cx)),
12234 "import { a } from 'module';\n\nconst x = a;\n"
12235 );
12236
12237 editor.update_in(cx, |editor, window, cx| {
12238 editor.set_text(
12239 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12240 window,
12241 cx,
12242 )
12243 });
12244 // Ensure we don't lock if code action hangs.
12245 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12246 move |params, _| async move {
12247 assert_eq!(
12248 params.text_document.uri,
12249 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12250 );
12251 futures::future::pending::<()>().await;
12252 unreachable!()
12253 },
12254 );
12255 let format = editor
12256 .update_in(cx, |editor, window, cx| {
12257 editor.perform_code_action_kind(
12258 project,
12259 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12260 window,
12261 cx,
12262 )
12263 })
12264 .unwrap();
12265 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12266 cx.executor().start_waiting();
12267 format.await;
12268 assert_eq!(
12269 editor.update(cx, |editor, cx| editor.text(cx)),
12270 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12271 );
12272}
12273
12274#[gpui::test]
12275async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12276 init_test(cx, |_| {});
12277
12278 let mut cx = EditorLspTestContext::new_rust(
12279 lsp::ServerCapabilities {
12280 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12281 ..Default::default()
12282 },
12283 cx,
12284 )
12285 .await;
12286
12287 cx.set_state(indoc! {"
12288 one.twoˇ
12289 "});
12290
12291 // The format request takes a long time. When it completes, it inserts
12292 // a newline and an indent before the `.`
12293 cx.lsp
12294 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12295 let executor = cx.background_executor().clone();
12296 async move {
12297 executor.timer(Duration::from_millis(100)).await;
12298 Ok(Some(vec![lsp::TextEdit {
12299 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12300 new_text: "\n ".into(),
12301 }]))
12302 }
12303 });
12304
12305 // Submit a format request.
12306 let format_1 = cx
12307 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12308 .unwrap();
12309 cx.executor().run_until_parked();
12310
12311 // Submit a second format request.
12312 let format_2 = cx
12313 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12314 .unwrap();
12315 cx.executor().run_until_parked();
12316
12317 // Wait for both format requests to complete
12318 cx.executor().advance_clock(Duration::from_millis(200));
12319 cx.executor().start_waiting();
12320 format_1.await.unwrap();
12321 cx.executor().start_waiting();
12322 format_2.await.unwrap();
12323
12324 // The formatting edits only happens once.
12325 cx.assert_editor_state(indoc! {"
12326 one
12327 .twoˇ
12328 "});
12329}
12330
12331#[gpui::test]
12332async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12333 init_test(cx, |settings| {
12334 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12335 });
12336
12337 let mut cx = EditorLspTestContext::new_rust(
12338 lsp::ServerCapabilities {
12339 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12340 ..Default::default()
12341 },
12342 cx,
12343 )
12344 .await;
12345
12346 // Set up a buffer white some trailing whitespace and no trailing newline.
12347 cx.set_state(
12348 &[
12349 "one ", //
12350 "twoˇ", //
12351 "three ", //
12352 "four", //
12353 ]
12354 .join("\n"),
12355 );
12356
12357 // Submit a format request.
12358 let format = cx
12359 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12360 .unwrap();
12361
12362 // Record which buffer changes have been sent to the language server
12363 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12364 cx.lsp
12365 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12366 let buffer_changes = buffer_changes.clone();
12367 move |params, _| {
12368 buffer_changes.lock().extend(
12369 params
12370 .content_changes
12371 .into_iter()
12372 .map(|e| (e.range.unwrap(), e.text)),
12373 );
12374 }
12375 });
12376
12377 // Handle formatting requests to the language server.
12378 cx.lsp
12379 .set_request_handler::<lsp::request::Formatting, _, _>({
12380 let buffer_changes = buffer_changes.clone();
12381 move |_, _| {
12382 // When formatting is requested, trailing whitespace has already been stripped,
12383 // and the trailing newline has already been added.
12384 assert_eq!(
12385 &buffer_changes.lock()[1..],
12386 &[
12387 (
12388 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12389 "".into()
12390 ),
12391 (
12392 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12393 "".into()
12394 ),
12395 (
12396 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12397 "\n".into()
12398 ),
12399 ]
12400 );
12401
12402 // Insert blank lines between each line of the buffer.
12403 async move {
12404 Ok(Some(vec![
12405 lsp::TextEdit {
12406 range: lsp::Range::new(
12407 lsp::Position::new(1, 0),
12408 lsp::Position::new(1, 0),
12409 ),
12410 new_text: "\n".into(),
12411 },
12412 lsp::TextEdit {
12413 range: lsp::Range::new(
12414 lsp::Position::new(2, 0),
12415 lsp::Position::new(2, 0),
12416 ),
12417 new_text: "\n".into(),
12418 },
12419 ]))
12420 }
12421 }
12422 });
12423
12424 // After formatting the buffer, the trailing whitespace is stripped,
12425 // a newline is appended, and the edits provided by the language server
12426 // have been applied.
12427 format.await.unwrap();
12428 cx.assert_editor_state(
12429 &[
12430 "one", //
12431 "", //
12432 "twoˇ", //
12433 "", //
12434 "three", //
12435 "four", //
12436 "", //
12437 ]
12438 .join("\n"),
12439 );
12440
12441 // Undoing the formatting undoes the trailing whitespace removal, the
12442 // trailing newline, and the LSP edits.
12443 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12444 cx.assert_editor_state(
12445 &[
12446 "one ", //
12447 "twoˇ", //
12448 "three ", //
12449 "four", //
12450 ]
12451 .join("\n"),
12452 );
12453}
12454
12455#[gpui::test]
12456async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12457 cx: &mut TestAppContext,
12458) {
12459 init_test(cx, |_| {});
12460
12461 cx.update(|cx| {
12462 cx.update_global::<SettingsStore, _>(|settings, cx| {
12463 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12464 settings.auto_signature_help = Some(true);
12465 });
12466 });
12467 });
12468
12469 let mut cx = EditorLspTestContext::new_rust(
12470 lsp::ServerCapabilities {
12471 signature_help_provider: Some(lsp::SignatureHelpOptions {
12472 ..Default::default()
12473 }),
12474 ..Default::default()
12475 },
12476 cx,
12477 )
12478 .await;
12479
12480 let language = Language::new(
12481 LanguageConfig {
12482 name: "Rust".into(),
12483 brackets: BracketPairConfig {
12484 pairs: vec![
12485 BracketPair {
12486 start: "{".to_string(),
12487 end: "}".to_string(),
12488 close: true,
12489 surround: true,
12490 newline: true,
12491 },
12492 BracketPair {
12493 start: "(".to_string(),
12494 end: ")".to_string(),
12495 close: true,
12496 surround: true,
12497 newline: true,
12498 },
12499 BracketPair {
12500 start: "/*".to_string(),
12501 end: " */".to_string(),
12502 close: true,
12503 surround: true,
12504 newline: true,
12505 },
12506 BracketPair {
12507 start: "[".to_string(),
12508 end: "]".to_string(),
12509 close: false,
12510 surround: false,
12511 newline: true,
12512 },
12513 BracketPair {
12514 start: "\"".to_string(),
12515 end: "\"".to_string(),
12516 close: true,
12517 surround: true,
12518 newline: false,
12519 },
12520 BracketPair {
12521 start: "<".to_string(),
12522 end: ">".to_string(),
12523 close: false,
12524 surround: true,
12525 newline: true,
12526 },
12527 ],
12528 ..Default::default()
12529 },
12530 autoclose_before: "})]".to_string(),
12531 ..Default::default()
12532 },
12533 Some(tree_sitter_rust::LANGUAGE.into()),
12534 );
12535 let language = Arc::new(language);
12536
12537 cx.language_registry().add(language.clone());
12538 cx.update_buffer(|buffer, cx| {
12539 buffer.set_language(Some(language), cx);
12540 });
12541
12542 cx.set_state(
12543 &r#"
12544 fn main() {
12545 sampleˇ
12546 }
12547 "#
12548 .unindent(),
12549 );
12550
12551 cx.update_editor(|editor, window, cx| {
12552 editor.handle_input("(", window, cx);
12553 });
12554 cx.assert_editor_state(
12555 &"
12556 fn main() {
12557 sample(ˇ)
12558 }
12559 "
12560 .unindent(),
12561 );
12562
12563 let mocked_response = lsp::SignatureHelp {
12564 signatures: vec![lsp::SignatureInformation {
12565 label: "fn sample(param1: u8, param2: u8)".to_string(),
12566 documentation: None,
12567 parameters: Some(vec![
12568 lsp::ParameterInformation {
12569 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12570 documentation: None,
12571 },
12572 lsp::ParameterInformation {
12573 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12574 documentation: None,
12575 },
12576 ]),
12577 active_parameter: None,
12578 }],
12579 active_signature: Some(0),
12580 active_parameter: Some(0),
12581 };
12582 handle_signature_help_request(&mut cx, mocked_response).await;
12583
12584 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12585 .await;
12586
12587 cx.editor(|editor, _, _| {
12588 let signature_help_state = editor.signature_help_state.popover().cloned();
12589 let signature = signature_help_state.unwrap();
12590 assert_eq!(
12591 signature.signatures[signature.current_signature].label,
12592 "fn sample(param1: u8, param2: u8)"
12593 );
12594 });
12595}
12596
12597#[gpui::test]
12598async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12599 init_test(cx, |_| {});
12600
12601 cx.update(|cx| {
12602 cx.update_global::<SettingsStore, _>(|settings, cx| {
12603 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12604 settings.auto_signature_help = Some(false);
12605 settings.show_signature_help_after_edits = Some(false);
12606 });
12607 });
12608 });
12609
12610 let mut cx = EditorLspTestContext::new_rust(
12611 lsp::ServerCapabilities {
12612 signature_help_provider: Some(lsp::SignatureHelpOptions {
12613 ..Default::default()
12614 }),
12615 ..Default::default()
12616 },
12617 cx,
12618 )
12619 .await;
12620
12621 let language = Language::new(
12622 LanguageConfig {
12623 name: "Rust".into(),
12624 brackets: BracketPairConfig {
12625 pairs: vec![
12626 BracketPair {
12627 start: "{".to_string(),
12628 end: "}".to_string(),
12629 close: true,
12630 surround: true,
12631 newline: true,
12632 },
12633 BracketPair {
12634 start: "(".to_string(),
12635 end: ")".to_string(),
12636 close: true,
12637 surround: true,
12638 newline: true,
12639 },
12640 BracketPair {
12641 start: "/*".to_string(),
12642 end: " */".to_string(),
12643 close: true,
12644 surround: true,
12645 newline: true,
12646 },
12647 BracketPair {
12648 start: "[".to_string(),
12649 end: "]".to_string(),
12650 close: false,
12651 surround: false,
12652 newline: true,
12653 },
12654 BracketPair {
12655 start: "\"".to_string(),
12656 end: "\"".to_string(),
12657 close: true,
12658 surround: true,
12659 newline: false,
12660 },
12661 BracketPair {
12662 start: "<".to_string(),
12663 end: ">".to_string(),
12664 close: false,
12665 surround: true,
12666 newline: true,
12667 },
12668 ],
12669 ..Default::default()
12670 },
12671 autoclose_before: "})]".to_string(),
12672 ..Default::default()
12673 },
12674 Some(tree_sitter_rust::LANGUAGE.into()),
12675 );
12676 let language = Arc::new(language);
12677
12678 cx.language_registry().add(language.clone());
12679 cx.update_buffer(|buffer, cx| {
12680 buffer.set_language(Some(language), cx);
12681 });
12682
12683 // Ensure that signature_help is not called when no signature help is enabled.
12684 cx.set_state(
12685 &r#"
12686 fn main() {
12687 sampleˇ
12688 }
12689 "#
12690 .unindent(),
12691 );
12692 cx.update_editor(|editor, window, cx| {
12693 editor.handle_input("(", window, cx);
12694 });
12695 cx.assert_editor_state(
12696 &"
12697 fn main() {
12698 sample(ˇ)
12699 }
12700 "
12701 .unindent(),
12702 );
12703 cx.editor(|editor, _, _| {
12704 assert!(editor.signature_help_state.task().is_none());
12705 });
12706
12707 let mocked_response = lsp::SignatureHelp {
12708 signatures: vec![lsp::SignatureInformation {
12709 label: "fn sample(param1: u8, param2: u8)".to_string(),
12710 documentation: None,
12711 parameters: Some(vec![
12712 lsp::ParameterInformation {
12713 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12714 documentation: None,
12715 },
12716 lsp::ParameterInformation {
12717 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12718 documentation: None,
12719 },
12720 ]),
12721 active_parameter: None,
12722 }],
12723 active_signature: Some(0),
12724 active_parameter: Some(0),
12725 };
12726
12727 // Ensure that signature_help is called when enabled afte edits
12728 cx.update(|_, cx| {
12729 cx.update_global::<SettingsStore, _>(|settings, cx| {
12730 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12731 settings.auto_signature_help = Some(false);
12732 settings.show_signature_help_after_edits = Some(true);
12733 });
12734 });
12735 });
12736 cx.set_state(
12737 &r#"
12738 fn main() {
12739 sampleˇ
12740 }
12741 "#
12742 .unindent(),
12743 );
12744 cx.update_editor(|editor, window, cx| {
12745 editor.handle_input("(", window, cx);
12746 });
12747 cx.assert_editor_state(
12748 &"
12749 fn main() {
12750 sample(ˇ)
12751 }
12752 "
12753 .unindent(),
12754 );
12755 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12756 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12757 .await;
12758 cx.update_editor(|editor, _, _| {
12759 let signature_help_state = editor.signature_help_state.popover().cloned();
12760 assert!(signature_help_state.is_some());
12761 let signature = signature_help_state.unwrap();
12762 assert_eq!(
12763 signature.signatures[signature.current_signature].label,
12764 "fn sample(param1: u8, param2: u8)"
12765 );
12766 editor.signature_help_state = SignatureHelpState::default();
12767 });
12768
12769 // Ensure that signature_help is called when auto signature help override is enabled
12770 cx.update(|_, cx| {
12771 cx.update_global::<SettingsStore, _>(|settings, cx| {
12772 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12773 settings.auto_signature_help = Some(true);
12774 settings.show_signature_help_after_edits = Some(false);
12775 });
12776 });
12777 });
12778 cx.set_state(
12779 &r#"
12780 fn main() {
12781 sampleˇ
12782 }
12783 "#
12784 .unindent(),
12785 );
12786 cx.update_editor(|editor, window, cx| {
12787 editor.handle_input("(", window, cx);
12788 });
12789 cx.assert_editor_state(
12790 &"
12791 fn main() {
12792 sample(ˇ)
12793 }
12794 "
12795 .unindent(),
12796 );
12797 handle_signature_help_request(&mut cx, mocked_response).await;
12798 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12799 .await;
12800 cx.editor(|editor, _, _| {
12801 let signature_help_state = editor.signature_help_state.popover().cloned();
12802 assert!(signature_help_state.is_some());
12803 let signature = signature_help_state.unwrap();
12804 assert_eq!(
12805 signature.signatures[signature.current_signature].label,
12806 "fn sample(param1: u8, param2: u8)"
12807 );
12808 });
12809}
12810
12811#[gpui::test]
12812async fn test_signature_help(cx: &mut TestAppContext) {
12813 init_test(cx, |_| {});
12814 cx.update(|cx| {
12815 cx.update_global::<SettingsStore, _>(|settings, cx| {
12816 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12817 settings.auto_signature_help = Some(true);
12818 });
12819 });
12820 });
12821
12822 let mut cx = EditorLspTestContext::new_rust(
12823 lsp::ServerCapabilities {
12824 signature_help_provider: Some(lsp::SignatureHelpOptions {
12825 ..Default::default()
12826 }),
12827 ..Default::default()
12828 },
12829 cx,
12830 )
12831 .await;
12832
12833 // A test that directly calls `show_signature_help`
12834 cx.update_editor(|editor, window, cx| {
12835 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12836 });
12837
12838 let mocked_response = lsp::SignatureHelp {
12839 signatures: vec![lsp::SignatureInformation {
12840 label: "fn sample(param1: u8, param2: u8)".to_string(),
12841 documentation: None,
12842 parameters: Some(vec![
12843 lsp::ParameterInformation {
12844 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12845 documentation: None,
12846 },
12847 lsp::ParameterInformation {
12848 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12849 documentation: None,
12850 },
12851 ]),
12852 active_parameter: None,
12853 }],
12854 active_signature: Some(0),
12855 active_parameter: Some(0),
12856 };
12857 handle_signature_help_request(&mut cx, mocked_response).await;
12858
12859 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12860 .await;
12861
12862 cx.editor(|editor, _, _| {
12863 let signature_help_state = editor.signature_help_state.popover().cloned();
12864 assert!(signature_help_state.is_some());
12865 let signature = signature_help_state.unwrap();
12866 assert_eq!(
12867 signature.signatures[signature.current_signature].label,
12868 "fn sample(param1: u8, param2: u8)"
12869 );
12870 });
12871
12872 // When exiting outside from inside the brackets, `signature_help` is closed.
12873 cx.set_state(indoc! {"
12874 fn main() {
12875 sample(ˇ);
12876 }
12877
12878 fn sample(param1: u8, param2: u8) {}
12879 "});
12880
12881 cx.update_editor(|editor, window, cx| {
12882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12883 s.select_ranges([0..0])
12884 });
12885 });
12886
12887 let mocked_response = lsp::SignatureHelp {
12888 signatures: Vec::new(),
12889 active_signature: None,
12890 active_parameter: None,
12891 };
12892 handle_signature_help_request(&mut cx, mocked_response).await;
12893
12894 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12895 .await;
12896
12897 cx.editor(|editor, _, _| {
12898 assert!(!editor.signature_help_state.is_shown());
12899 });
12900
12901 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12902 cx.set_state(indoc! {"
12903 fn main() {
12904 sample(ˇ);
12905 }
12906
12907 fn sample(param1: u8, param2: u8) {}
12908 "});
12909
12910 let mocked_response = lsp::SignatureHelp {
12911 signatures: vec![lsp::SignatureInformation {
12912 label: "fn sample(param1: u8, param2: u8)".to_string(),
12913 documentation: None,
12914 parameters: Some(vec![
12915 lsp::ParameterInformation {
12916 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12917 documentation: None,
12918 },
12919 lsp::ParameterInformation {
12920 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12921 documentation: None,
12922 },
12923 ]),
12924 active_parameter: None,
12925 }],
12926 active_signature: Some(0),
12927 active_parameter: Some(0),
12928 };
12929 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12930 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12931 .await;
12932 cx.editor(|editor, _, _| {
12933 assert!(editor.signature_help_state.is_shown());
12934 });
12935
12936 // Restore the popover with more parameter input
12937 cx.set_state(indoc! {"
12938 fn main() {
12939 sample(param1, param2ˇ);
12940 }
12941
12942 fn sample(param1: u8, param2: u8) {}
12943 "});
12944
12945 let mocked_response = lsp::SignatureHelp {
12946 signatures: vec![lsp::SignatureInformation {
12947 label: "fn sample(param1: u8, param2: u8)".to_string(),
12948 documentation: None,
12949 parameters: Some(vec![
12950 lsp::ParameterInformation {
12951 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12952 documentation: None,
12953 },
12954 lsp::ParameterInformation {
12955 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12956 documentation: None,
12957 },
12958 ]),
12959 active_parameter: None,
12960 }],
12961 active_signature: Some(0),
12962 active_parameter: Some(1),
12963 };
12964 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12965 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12966 .await;
12967
12968 // When selecting a range, the popover is gone.
12969 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12970 cx.update_editor(|editor, window, cx| {
12971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12972 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12973 })
12974 });
12975 cx.assert_editor_state(indoc! {"
12976 fn main() {
12977 sample(param1, «ˇparam2»);
12978 }
12979
12980 fn sample(param1: u8, param2: u8) {}
12981 "});
12982 cx.editor(|editor, _, _| {
12983 assert!(!editor.signature_help_state.is_shown());
12984 });
12985
12986 // When unselecting again, the popover is back if within the brackets.
12987 cx.update_editor(|editor, window, cx| {
12988 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12989 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12990 })
12991 });
12992 cx.assert_editor_state(indoc! {"
12993 fn main() {
12994 sample(param1, ˇparam2);
12995 }
12996
12997 fn sample(param1: u8, param2: u8) {}
12998 "});
12999 handle_signature_help_request(&mut cx, mocked_response).await;
13000 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13001 .await;
13002 cx.editor(|editor, _, _| {
13003 assert!(editor.signature_help_state.is_shown());
13004 });
13005
13006 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13007 cx.update_editor(|editor, window, cx| {
13008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13009 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13010 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13011 })
13012 });
13013 cx.assert_editor_state(indoc! {"
13014 fn main() {
13015 sample(param1, ˇparam2);
13016 }
13017
13018 fn sample(param1: u8, param2: u8) {}
13019 "});
13020
13021 let mocked_response = lsp::SignatureHelp {
13022 signatures: vec![lsp::SignatureInformation {
13023 label: "fn sample(param1: u8, param2: u8)".to_string(),
13024 documentation: None,
13025 parameters: Some(vec![
13026 lsp::ParameterInformation {
13027 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13028 documentation: None,
13029 },
13030 lsp::ParameterInformation {
13031 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13032 documentation: None,
13033 },
13034 ]),
13035 active_parameter: None,
13036 }],
13037 active_signature: Some(0),
13038 active_parameter: Some(1),
13039 };
13040 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13041 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13042 .await;
13043 cx.update_editor(|editor, _, cx| {
13044 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13045 });
13046 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13047 .await;
13048 cx.update_editor(|editor, window, cx| {
13049 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13050 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13051 })
13052 });
13053 cx.assert_editor_state(indoc! {"
13054 fn main() {
13055 sample(param1, «ˇparam2»);
13056 }
13057
13058 fn sample(param1: u8, param2: u8) {}
13059 "});
13060 cx.update_editor(|editor, window, cx| {
13061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13062 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13063 })
13064 });
13065 cx.assert_editor_state(indoc! {"
13066 fn main() {
13067 sample(param1, ˇparam2);
13068 }
13069
13070 fn sample(param1: u8, param2: u8) {}
13071 "});
13072 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13073 .await;
13074}
13075
13076#[gpui::test]
13077async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13078 init_test(cx, |_| {});
13079
13080 let mut cx = EditorLspTestContext::new_rust(
13081 lsp::ServerCapabilities {
13082 signature_help_provider: Some(lsp::SignatureHelpOptions {
13083 ..Default::default()
13084 }),
13085 ..Default::default()
13086 },
13087 cx,
13088 )
13089 .await;
13090
13091 cx.set_state(indoc! {"
13092 fn main() {
13093 overloadedˇ
13094 }
13095 "});
13096
13097 cx.update_editor(|editor, window, cx| {
13098 editor.handle_input("(", window, cx);
13099 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13100 });
13101
13102 // Mock response with 3 signatures
13103 let mocked_response = lsp::SignatureHelp {
13104 signatures: vec![
13105 lsp::SignatureInformation {
13106 label: "fn overloaded(x: i32)".to_string(),
13107 documentation: None,
13108 parameters: Some(vec![lsp::ParameterInformation {
13109 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13110 documentation: None,
13111 }]),
13112 active_parameter: None,
13113 },
13114 lsp::SignatureInformation {
13115 label: "fn overloaded(x: i32, y: i32)".to_string(),
13116 documentation: None,
13117 parameters: Some(vec![
13118 lsp::ParameterInformation {
13119 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13120 documentation: None,
13121 },
13122 lsp::ParameterInformation {
13123 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13124 documentation: None,
13125 },
13126 ]),
13127 active_parameter: None,
13128 },
13129 lsp::SignatureInformation {
13130 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13131 documentation: None,
13132 parameters: Some(vec![
13133 lsp::ParameterInformation {
13134 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13135 documentation: None,
13136 },
13137 lsp::ParameterInformation {
13138 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13139 documentation: None,
13140 },
13141 lsp::ParameterInformation {
13142 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13143 documentation: None,
13144 },
13145 ]),
13146 active_parameter: None,
13147 },
13148 ],
13149 active_signature: Some(1),
13150 active_parameter: Some(0),
13151 };
13152 handle_signature_help_request(&mut cx, mocked_response).await;
13153
13154 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13155 .await;
13156
13157 // Verify we have multiple signatures and the right one is selected
13158 cx.editor(|editor, _, _| {
13159 let popover = editor.signature_help_state.popover().cloned().unwrap();
13160 assert_eq!(popover.signatures.len(), 3);
13161 // active_signature was 1, so that should be the current
13162 assert_eq!(popover.current_signature, 1);
13163 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13164 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13165 assert_eq!(
13166 popover.signatures[2].label,
13167 "fn overloaded(x: i32, y: i32, z: i32)"
13168 );
13169 });
13170
13171 // Test navigation functionality
13172 cx.update_editor(|editor, window, cx| {
13173 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13174 });
13175
13176 cx.editor(|editor, _, _| {
13177 let popover = editor.signature_help_state.popover().cloned().unwrap();
13178 assert_eq!(popover.current_signature, 2);
13179 });
13180
13181 // Test wrap around
13182 cx.update_editor(|editor, window, cx| {
13183 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13184 });
13185
13186 cx.editor(|editor, _, _| {
13187 let popover = editor.signature_help_state.popover().cloned().unwrap();
13188 assert_eq!(popover.current_signature, 0);
13189 });
13190
13191 // Test previous navigation
13192 cx.update_editor(|editor, window, cx| {
13193 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13194 });
13195
13196 cx.editor(|editor, _, _| {
13197 let popover = editor.signature_help_state.popover().cloned().unwrap();
13198 assert_eq!(popover.current_signature, 2);
13199 });
13200}
13201
13202#[gpui::test]
13203async fn test_completion_mode(cx: &mut TestAppContext) {
13204 init_test(cx, |_| {});
13205 let mut cx = EditorLspTestContext::new_rust(
13206 lsp::ServerCapabilities {
13207 completion_provider: Some(lsp::CompletionOptions {
13208 resolve_provider: Some(true),
13209 ..Default::default()
13210 }),
13211 ..Default::default()
13212 },
13213 cx,
13214 )
13215 .await;
13216
13217 struct Run {
13218 run_description: &'static str,
13219 initial_state: String,
13220 buffer_marked_text: String,
13221 completion_label: &'static str,
13222 completion_text: &'static str,
13223 expected_with_insert_mode: String,
13224 expected_with_replace_mode: String,
13225 expected_with_replace_subsequence_mode: String,
13226 expected_with_replace_suffix_mode: String,
13227 }
13228
13229 let runs = [
13230 Run {
13231 run_description: "Start of word matches completion text",
13232 initial_state: "before ediˇ after".into(),
13233 buffer_marked_text: "before <edi|> after".into(),
13234 completion_label: "editor",
13235 completion_text: "editor",
13236 expected_with_insert_mode: "before editorˇ after".into(),
13237 expected_with_replace_mode: "before editorˇ after".into(),
13238 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13239 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13240 },
13241 Run {
13242 run_description: "Accept same text at the middle of the word",
13243 initial_state: "before ediˇtor after".into(),
13244 buffer_marked_text: "before <edi|tor> after".into(),
13245 completion_label: "editor",
13246 completion_text: "editor",
13247 expected_with_insert_mode: "before editorˇtor after".into(),
13248 expected_with_replace_mode: "before editorˇ after".into(),
13249 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13250 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13251 },
13252 Run {
13253 run_description: "End of word matches completion text -- cursor at end",
13254 initial_state: "before torˇ after".into(),
13255 buffer_marked_text: "before <tor|> after".into(),
13256 completion_label: "editor",
13257 completion_text: "editor",
13258 expected_with_insert_mode: "before editorˇ after".into(),
13259 expected_with_replace_mode: "before editorˇ after".into(),
13260 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13261 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13262 },
13263 Run {
13264 run_description: "End of word matches completion text -- cursor at start",
13265 initial_state: "before ˇtor after".into(),
13266 buffer_marked_text: "before <|tor> after".into(),
13267 completion_label: "editor",
13268 completion_text: "editor",
13269 expected_with_insert_mode: "before editorˇtor after".into(),
13270 expected_with_replace_mode: "before editorˇ after".into(),
13271 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13272 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13273 },
13274 Run {
13275 run_description: "Prepend text containing whitespace",
13276 initial_state: "pˇfield: bool".into(),
13277 buffer_marked_text: "<p|field>: bool".into(),
13278 completion_label: "pub ",
13279 completion_text: "pub ",
13280 expected_with_insert_mode: "pub ˇfield: bool".into(),
13281 expected_with_replace_mode: "pub ˇ: bool".into(),
13282 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13283 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13284 },
13285 Run {
13286 run_description: "Add element to start of list",
13287 initial_state: "[element_ˇelement_2]".into(),
13288 buffer_marked_text: "[<element_|element_2>]".into(),
13289 completion_label: "element_1",
13290 completion_text: "element_1",
13291 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13292 expected_with_replace_mode: "[element_1ˇ]".into(),
13293 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13294 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13295 },
13296 Run {
13297 run_description: "Add element to start of list -- first and second elements are equal",
13298 initial_state: "[elˇelement]".into(),
13299 buffer_marked_text: "[<el|element>]".into(),
13300 completion_label: "element",
13301 completion_text: "element",
13302 expected_with_insert_mode: "[elementˇelement]".into(),
13303 expected_with_replace_mode: "[elementˇ]".into(),
13304 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13305 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13306 },
13307 Run {
13308 run_description: "Ends with matching suffix",
13309 initial_state: "SubˇError".into(),
13310 buffer_marked_text: "<Sub|Error>".into(),
13311 completion_label: "SubscriptionError",
13312 completion_text: "SubscriptionError",
13313 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13314 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13315 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13316 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13317 },
13318 Run {
13319 run_description: "Suffix is a subsequence -- contiguous",
13320 initial_state: "SubˇErr".into(),
13321 buffer_marked_text: "<Sub|Err>".into(),
13322 completion_label: "SubscriptionError",
13323 completion_text: "SubscriptionError",
13324 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13325 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13326 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13327 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13328 },
13329 Run {
13330 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13331 initial_state: "Suˇscrirr".into(),
13332 buffer_marked_text: "<Su|scrirr>".into(),
13333 completion_label: "SubscriptionError",
13334 completion_text: "SubscriptionError",
13335 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13336 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13337 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13338 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13339 },
13340 Run {
13341 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13342 initial_state: "foo(indˇix)".into(),
13343 buffer_marked_text: "foo(<ind|ix>)".into(),
13344 completion_label: "node_index",
13345 completion_text: "node_index",
13346 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13347 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13348 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13349 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13350 },
13351 Run {
13352 run_description: "Replace range ends before cursor - should extend to cursor",
13353 initial_state: "before editˇo after".into(),
13354 buffer_marked_text: "before <{ed}>it|o after".into(),
13355 completion_label: "editor",
13356 completion_text: "editor",
13357 expected_with_insert_mode: "before editorˇo after".into(),
13358 expected_with_replace_mode: "before editorˇo after".into(),
13359 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13360 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13361 },
13362 Run {
13363 run_description: "Uses label for suffix matching",
13364 initial_state: "before ediˇtor after".into(),
13365 buffer_marked_text: "before <edi|tor> after".into(),
13366 completion_label: "editor",
13367 completion_text: "editor()",
13368 expected_with_insert_mode: "before editor()ˇtor after".into(),
13369 expected_with_replace_mode: "before editor()ˇ after".into(),
13370 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13371 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13372 },
13373 Run {
13374 run_description: "Case insensitive subsequence and suffix matching",
13375 initial_state: "before EDiˇtoR after".into(),
13376 buffer_marked_text: "before <EDi|toR> after".into(),
13377 completion_label: "editor",
13378 completion_text: "editor",
13379 expected_with_insert_mode: "before editorˇtoR after".into(),
13380 expected_with_replace_mode: "before editorˇ after".into(),
13381 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13382 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13383 },
13384 ];
13385
13386 for run in runs {
13387 let run_variations = [
13388 (LspInsertMode::Insert, run.expected_with_insert_mode),
13389 (LspInsertMode::Replace, run.expected_with_replace_mode),
13390 (
13391 LspInsertMode::ReplaceSubsequence,
13392 run.expected_with_replace_subsequence_mode,
13393 ),
13394 (
13395 LspInsertMode::ReplaceSuffix,
13396 run.expected_with_replace_suffix_mode,
13397 ),
13398 ];
13399
13400 for (lsp_insert_mode, expected_text) in run_variations {
13401 eprintln!(
13402 "run = {:?}, mode = {lsp_insert_mode:.?}",
13403 run.run_description,
13404 );
13405
13406 update_test_language_settings(&mut cx, |settings| {
13407 settings.defaults.completions = Some(CompletionSettings {
13408 lsp_insert_mode,
13409 words: WordsCompletionMode::Disabled,
13410 words_min_length: 0,
13411 lsp: true,
13412 lsp_fetch_timeout_ms: 0,
13413 });
13414 });
13415
13416 cx.set_state(&run.initial_state);
13417 cx.update_editor(|editor, window, cx| {
13418 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13419 });
13420
13421 let counter = Arc::new(AtomicUsize::new(0));
13422 handle_completion_request_with_insert_and_replace(
13423 &mut cx,
13424 &run.buffer_marked_text,
13425 vec![(run.completion_label, run.completion_text)],
13426 counter.clone(),
13427 )
13428 .await;
13429 cx.condition(|editor, _| editor.context_menu_visible())
13430 .await;
13431 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13432
13433 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13434 editor
13435 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13436 .unwrap()
13437 });
13438 cx.assert_editor_state(&expected_text);
13439 handle_resolve_completion_request(&mut cx, None).await;
13440 apply_additional_edits.await.unwrap();
13441 }
13442 }
13443}
13444
13445#[gpui::test]
13446async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13447 init_test(cx, |_| {});
13448 let mut cx = EditorLspTestContext::new_rust(
13449 lsp::ServerCapabilities {
13450 completion_provider: Some(lsp::CompletionOptions {
13451 resolve_provider: Some(true),
13452 ..Default::default()
13453 }),
13454 ..Default::default()
13455 },
13456 cx,
13457 )
13458 .await;
13459
13460 let initial_state = "SubˇError";
13461 let buffer_marked_text = "<Sub|Error>";
13462 let completion_text = "SubscriptionError";
13463 let expected_with_insert_mode = "SubscriptionErrorˇError";
13464 let expected_with_replace_mode = "SubscriptionErrorˇ";
13465
13466 update_test_language_settings(&mut cx, |settings| {
13467 settings.defaults.completions = Some(CompletionSettings {
13468 words: WordsCompletionMode::Disabled,
13469 words_min_length: 0,
13470 // set the opposite here to ensure that the action is overriding the default behavior
13471 lsp_insert_mode: LspInsertMode::Insert,
13472 lsp: true,
13473 lsp_fetch_timeout_ms: 0,
13474 });
13475 });
13476
13477 cx.set_state(initial_state);
13478 cx.update_editor(|editor, window, cx| {
13479 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13480 });
13481
13482 let counter = Arc::new(AtomicUsize::new(0));
13483 handle_completion_request_with_insert_and_replace(
13484 &mut cx,
13485 buffer_marked_text,
13486 vec![(completion_text, completion_text)],
13487 counter.clone(),
13488 )
13489 .await;
13490 cx.condition(|editor, _| editor.context_menu_visible())
13491 .await;
13492 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13493
13494 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13495 editor
13496 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13497 .unwrap()
13498 });
13499 cx.assert_editor_state(expected_with_replace_mode);
13500 handle_resolve_completion_request(&mut cx, None).await;
13501 apply_additional_edits.await.unwrap();
13502
13503 update_test_language_settings(&mut cx, |settings| {
13504 settings.defaults.completions = Some(CompletionSettings {
13505 words: WordsCompletionMode::Disabled,
13506 words_min_length: 0,
13507 // set the opposite here to ensure that the action is overriding the default behavior
13508 lsp_insert_mode: LspInsertMode::Replace,
13509 lsp: true,
13510 lsp_fetch_timeout_ms: 0,
13511 });
13512 });
13513
13514 cx.set_state(initial_state);
13515 cx.update_editor(|editor, window, cx| {
13516 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13517 });
13518 handle_completion_request_with_insert_and_replace(
13519 &mut cx,
13520 buffer_marked_text,
13521 vec![(completion_text, completion_text)],
13522 counter.clone(),
13523 )
13524 .await;
13525 cx.condition(|editor, _| editor.context_menu_visible())
13526 .await;
13527 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13528
13529 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13530 editor
13531 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13532 .unwrap()
13533 });
13534 cx.assert_editor_state(expected_with_insert_mode);
13535 handle_resolve_completion_request(&mut cx, None).await;
13536 apply_additional_edits.await.unwrap();
13537}
13538
13539#[gpui::test]
13540async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13541 init_test(cx, |_| {});
13542 let mut cx = EditorLspTestContext::new_rust(
13543 lsp::ServerCapabilities {
13544 completion_provider: Some(lsp::CompletionOptions {
13545 resolve_provider: Some(true),
13546 ..Default::default()
13547 }),
13548 ..Default::default()
13549 },
13550 cx,
13551 )
13552 .await;
13553
13554 // scenario: surrounding text matches completion text
13555 let completion_text = "to_offset";
13556 let initial_state = indoc! {"
13557 1. buf.to_offˇsuffix
13558 2. buf.to_offˇsuf
13559 3. buf.to_offˇfix
13560 4. buf.to_offˇ
13561 5. into_offˇensive
13562 6. ˇsuffix
13563 7. let ˇ //
13564 8. aaˇzz
13565 9. buf.to_off«zzzzzˇ»suffix
13566 10. buf.«ˇzzzzz»suffix
13567 11. to_off«ˇzzzzz»
13568
13569 buf.to_offˇsuffix // newest cursor
13570 "};
13571 let completion_marked_buffer = indoc! {"
13572 1. buf.to_offsuffix
13573 2. buf.to_offsuf
13574 3. buf.to_offfix
13575 4. buf.to_off
13576 5. into_offensive
13577 6. suffix
13578 7. let //
13579 8. aazz
13580 9. buf.to_offzzzzzsuffix
13581 10. buf.zzzzzsuffix
13582 11. to_offzzzzz
13583
13584 buf.<to_off|suffix> // newest cursor
13585 "};
13586 let expected = indoc! {"
13587 1. buf.to_offsetˇ
13588 2. buf.to_offsetˇsuf
13589 3. buf.to_offsetˇfix
13590 4. buf.to_offsetˇ
13591 5. into_offsetˇensive
13592 6. to_offsetˇsuffix
13593 7. let to_offsetˇ //
13594 8. aato_offsetˇzz
13595 9. buf.to_offsetˇ
13596 10. buf.to_offsetˇsuffix
13597 11. to_offsetˇ
13598
13599 buf.to_offsetˇ // newest cursor
13600 "};
13601 cx.set_state(initial_state);
13602 cx.update_editor(|editor, window, cx| {
13603 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13604 });
13605 handle_completion_request_with_insert_and_replace(
13606 &mut cx,
13607 completion_marked_buffer,
13608 vec![(completion_text, completion_text)],
13609 Arc::new(AtomicUsize::new(0)),
13610 )
13611 .await;
13612 cx.condition(|editor, _| editor.context_menu_visible())
13613 .await;
13614 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13615 editor
13616 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13617 .unwrap()
13618 });
13619 cx.assert_editor_state(expected);
13620 handle_resolve_completion_request(&mut cx, None).await;
13621 apply_additional_edits.await.unwrap();
13622
13623 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13624 let completion_text = "foo_and_bar";
13625 let initial_state = indoc! {"
13626 1. ooanbˇ
13627 2. zooanbˇ
13628 3. ooanbˇz
13629 4. zooanbˇz
13630 5. ooanˇ
13631 6. oanbˇ
13632
13633 ooanbˇ
13634 "};
13635 let completion_marked_buffer = indoc! {"
13636 1. ooanb
13637 2. zooanb
13638 3. ooanbz
13639 4. zooanbz
13640 5. ooan
13641 6. oanb
13642
13643 <ooanb|>
13644 "};
13645 let expected = indoc! {"
13646 1. foo_and_barˇ
13647 2. zfoo_and_barˇ
13648 3. foo_and_barˇz
13649 4. zfoo_and_barˇz
13650 5. ooanfoo_and_barˇ
13651 6. oanbfoo_and_barˇ
13652
13653 foo_and_barˇ
13654 "};
13655 cx.set_state(initial_state);
13656 cx.update_editor(|editor, window, cx| {
13657 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13658 });
13659 handle_completion_request_with_insert_and_replace(
13660 &mut cx,
13661 completion_marked_buffer,
13662 vec![(completion_text, completion_text)],
13663 Arc::new(AtomicUsize::new(0)),
13664 )
13665 .await;
13666 cx.condition(|editor, _| editor.context_menu_visible())
13667 .await;
13668 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13669 editor
13670 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13671 .unwrap()
13672 });
13673 cx.assert_editor_state(expected);
13674 handle_resolve_completion_request(&mut cx, None).await;
13675 apply_additional_edits.await.unwrap();
13676
13677 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13678 // (expects the same as if it was inserted at the end)
13679 let completion_text = "foo_and_bar";
13680 let initial_state = indoc! {"
13681 1. ooˇanb
13682 2. zooˇanb
13683 3. ooˇanbz
13684 4. zooˇanbz
13685
13686 ooˇanb
13687 "};
13688 let completion_marked_buffer = indoc! {"
13689 1. ooanb
13690 2. zooanb
13691 3. ooanbz
13692 4. zooanbz
13693
13694 <oo|anb>
13695 "};
13696 let expected = indoc! {"
13697 1. foo_and_barˇ
13698 2. zfoo_and_barˇ
13699 3. foo_and_barˇz
13700 4. zfoo_and_barˇz
13701
13702 foo_and_barˇ
13703 "};
13704 cx.set_state(initial_state);
13705 cx.update_editor(|editor, window, cx| {
13706 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13707 });
13708 handle_completion_request_with_insert_and_replace(
13709 &mut cx,
13710 completion_marked_buffer,
13711 vec![(completion_text, completion_text)],
13712 Arc::new(AtomicUsize::new(0)),
13713 )
13714 .await;
13715 cx.condition(|editor, _| editor.context_menu_visible())
13716 .await;
13717 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13718 editor
13719 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13720 .unwrap()
13721 });
13722 cx.assert_editor_state(expected);
13723 handle_resolve_completion_request(&mut cx, None).await;
13724 apply_additional_edits.await.unwrap();
13725}
13726
13727// This used to crash
13728#[gpui::test]
13729async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13730 init_test(cx, |_| {});
13731
13732 let buffer_text = indoc! {"
13733 fn main() {
13734 10.satu;
13735
13736 //
13737 // separate cursors so they open in different excerpts (manually reproducible)
13738 //
13739
13740 10.satu20;
13741 }
13742 "};
13743 let multibuffer_text_with_selections = indoc! {"
13744 fn main() {
13745 10.satuˇ;
13746
13747 //
13748
13749 //
13750
13751 10.satuˇ20;
13752 }
13753 "};
13754 let expected_multibuffer = indoc! {"
13755 fn main() {
13756 10.saturating_sub()ˇ;
13757
13758 //
13759
13760 //
13761
13762 10.saturating_sub()ˇ;
13763 }
13764 "};
13765
13766 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13767 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13768
13769 let fs = FakeFs::new(cx.executor());
13770 fs.insert_tree(
13771 path!("/a"),
13772 json!({
13773 "main.rs": buffer_text,
13774 }),
13775 )
13776 .await;
13777
13778 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13779 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13780 language_registry.add(rust_lang());
13781 let mut fake_servers = language_registry.register_fake_lsp(
13782 "Rust",
13783 FakeLspAdapter {
13784 capabilities: lsp::ServerCapabilities {
13785 completion_provider: Some(lsp::CompletionOptions {
13786 resolve_provider: None,
13787 ..lsp::CompletionOptions::default()
13788 }),
13789 ..lsp::ServerCapabilities::default()
13790 },
13791 ..FakeLspAdapter::default()
13792 },
13793 );
13794 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13795 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13796 let buffer = project
13797 .update(cx, |project, cx| {
13798 project.open_local_buffer(path!("/a/main.rs"), cx)
13799 })
13800 .await
13801 .unwrap();
13802
13803 let multi_buffer = cx.new(|cx| {
13804 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13805 multi_buffer.push_excerpts(
13806 buffer.clone(),
13807 [ExcerptRange::new(0..first_excerpt_end)],
13808 cx,
13809 );
13810 multi_buffer.push_excerpts(
13811 buffer.clone(),
13812 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13813 cx,
13814 );
13815 multi_buffer
13816 });
13817
13818 let editor = workspace
13819 .update(cx, |_, window, cx| {
13820 cx.new(|cx| {
13821 Editor::new(
13822 EditorMode::Full {
13823 scale_ui_elements_with_buffer_font_size: false,
13824 show_active_line_background: false,
13825 sized_by_content: false,
13826 },
13827 multi_buffer.clone(),
13828 Some(project.clone()),
13829 window,
13830 cx,
13831 )
13832 })
13833 })
13834 .unwrap();
13835
13836 let pane = workspace
13837 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13838 .unwrap();
13839 pane.update_in(cx, |pane, window, cx| {
13840 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13841 });
13842
13843 let fake_server = fake_servers.next().await.unwrap();
13844
13845 editor.update_in(cx, |editor, window, cx| {
13846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13847 s.select_ranges([
13848 Point::new(1, 11)..Point::new(1, 11),
13849 Point::new(7, 11)..Point::new(7, 11),
13850 ])
13851 });
13852
13853 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13854 });
13855
13856 editor.update_in(cx, |editor, window, cx| {
13857 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13858 });
13859
13860 fake_server
13861 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13862 let completion_item = lsp::CompletionItem {
13863 label: "saturating_sub()".into(),
13864 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13865 lsp::InsertReplaceEdit {
13866 new_text: "saturating_sub()".to_owned(),
13867 insert: lsp::Range::new(
13868 lsp::Position::new(7, 7),
13869 lsp::Position::new(7, 11),
13870 ),
13871 replace: lsp::Range::new(
13872 lsp::Position::new(7, 7),
13873 lsp::Position::new(7, 13),
13874 ),
13875 },
13876 )),
13877 ..lsp::CompletionItem::default()
13878 };
13879
13880 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13881 })
13882 .next()
13883 .await
13884 .unwrap();
13885
13886 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13887 .await;
13888
13889 editor
13890 .update_in(cx, |editor, window, cx| {
13891 editor
13892 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13893 .unwrap()
13894 })
13895 .await
13896 .unwrap();
13897
13898 editor.update(cx, |editor, cx| {
13899 assert_text_with_selections(editor, expected_multibuffer, cx);
13900 })
13901}
13902
13903#[gpui::test]
13904async fn test_completion(cx: &mut TestAppContext) {
13905 init_test(cx, |_| {});
13906
13907 let mut cx = EditorLspTestContext::new_rust(
13908 lsp::ServerCapabilities {
13909 completion_provider: Some(lsp::CompletionOptions {
13910 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13911 resolve_provider: Some(true),
13912 ..Default::default()
13913 }),
13914 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13915 ..Default::default()
13916 },
13917 cx,
13918 )
13919 .await;
13920 let counter = Arc::new(AtomicUsize::new(0));
13921
13922 cx.set_state(indoc! {"
13923 oneˇ
13924 two
13925 three
13926 "});
13927 cx.simulate_keystroke(".");
13928 handle_completion_request(
13929 indoc! {"
13930 one.|<>
13931 two
13932 three
13933 "},
13934 vec!["first_completion", "second_completion"],
13935 true,
13936 counter.clone(),
13937 &mut cx,
13938 )
13939 .await;
13940 cx.condition(|editor, _| editor.context_menu_visible())
13941 .await;
13942 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13943
13944 let _handler = handle_signature_help_request(
13945 &mut cx,
13946 lsp::SignatureHelp {
13947 signatures: vec![lsp::SignatureInformation {
13948 label: "test signature".to_string(),
13949 documentation: None,
13950 parameters: Some(vec![lsp::ParameterInformation {
13951 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13952 documentation: None,
13953 }]),
13954 active_parameter: None,
13955 }],
13956 active_signature: None,
13957 active_parameter: None,
13958 },
13959 );
13960 cx.update_editor(|editor, window, cx| {
13961 assert!(
13962 !editor.signature_help_state.is_shown(),
13963 "No signature help was called for"
13964 );
13965 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13966 });
13967 cx.run_until_parked();
13968 cx.update_editor(|editor, _, _| {
13969 assert!(
13970 !editor.signature_help_state.is_shown(),
13971 "No signature help should be shown when completions menu is open"
13972 );
13973 });
13974
13975 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13976 editor.context_menu_next(&Default::default(), window, cx);
13977 editor
13978 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13979 .unwrap()
13980 });
13981 cx.assert_editor_state(indoc! {"
13982 one.second_completionˇ
13983 two
13984 three
13985 "});
13986
13987 handle_resolve_completion_request(
13988 &mut cx,
13989 Some(vec![
13990 (
13991 //This overlaps with the primary completion edit which is
13992 //misbehavior from the LSP spec, test that we filter it out
13993 indoc! {"
13994 one.second_ˇcompletion
13995 two
13996 threeˇ
13997 "},
13998 "overlapping additional edit",
13999 ),
14000 (
14001 indoc! {"
14002 one.second_completion
14003 two
14004 threeˇ
14005 "},
14006 "\nadditional edit",
14007 ),
14008 ]),
14009 )
14010 .await;
14011 apply_additional_edits.await.unwrap();
14012 cx.assert_editor_state(indoc! {"
14013 one.second_completionˇ
14014 two
14015 three
14016 additional edit
14017 "});
14018
14019 cx.set_state(indoc! {"
14020 one.second_completion
14021 twoˇ
14022 threeˇ
14023 additional edit
14024 "});
14025 cx.simulate_keystroke(" ");
14026 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14027 cx.simulate_keystroke("s");
14028 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14029
14030 cx.assert_editor_state(indoc! {"
14031 one.second_completion
14032 two sˇ
14033 three sˇ
14034 additional edit
14035 "});
14036 handle_completion_request(
14037 indoc! {"
14038 one.second_completion
14039 two s
14040 three <s|>
14041 additional edit
14042 "},
14043 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14044 true,
14045 counter.clone(),
14046 &mut cx,
14047 )
14048 .await;
14049 cx.condition(|editor, _| editor.context_menu_visible())
14050 .await;
14051 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14052
14053 cx.simulate_keystroke("i");
14054
14055 handle_completion_request(
14056 indoc! {"
14057 one.second_completion
14058 two si
14059 three <si|>
14060 additional edit
14061 "},
14062 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14063 true,
14064 counter.clone(),
14065 &mut cx,
14066 )
14067 .await;
14068 cx.condition(|editor, _| editor.context_menu_visible())
14069 .await;
14070 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14071
14072 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14073 editor
14074 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14075 .unwrap()
14076 });
14077 cx.assert_editor_state(indoc! {"
14078 one.second_completion
14079 two sixth_completionˇ
14080 three sixth_completionˇ
14081 additional edit
14082 "});
14083
14084 apply_additional_edits.await.unwrap();
14085
14086 update_test_language_settings(&mut cx, |settings| {
14087 settings.defaults.show_completions_on_input = Some(false);
14088 });
14089 cx.set_state("editorˇ");
14090 cx.simulate_keystroke(".");
14091 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14092 cx.simulate_keystrokes("c l o");
14093 cx.assert_editor_state("editor.cloˇ");
14094 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14095 cx.update_editor(|editor, window, cx| {
14096 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14097 });
14098 handle_completion_request(
14099 "editor.<clo|>",
14100 vec!["close", "clobber"],
14101 true,
14102 counter.clone(),
14103 &mut cx,
14104 )
14105 .await;
14106 cx.condition(|editor, _| editor.context_menu_visible())
14107 .await;
14108 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14109
14110 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14111 editor
14112 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14113 .unwrap()
14114 });
14115 cx.assert_editor_state("editor.clobberˇ");
14116 handle_resolve_completion_request(&mut cx, None).await;
14117 apply_additional_edits.await.unwrap();
14118}
14119
14120#[gpui::test]
14121async fn test_completion_reuse(cx: &mut TestAppContext) {
14122 init_test(cx, |_| {});
14123
14124 let mut cx = EditorLspTestContext::new_rust(
14125 lsp::ServerCapabilities {
14126 completion_provider: Some(lsp::CompletionOptions {
14127 trigger_characters: Some(vec![".".to_string()]),
14128 ..Default::default()
14129 }),
14130 ..Default::default()
14131 },
14132 cx,
14133 )
14134 .await;
14135
14136 let counter = Arc::new(AtomicUsize::new(0));
14137 cx.set_state("objˇ");
14138 cx.simulate_keystroke(".");
14139
14140 // Initial completion request returns complete results
14141 let is_incomplete = false;
14142 handle_completion_request(
14143 "obj.|<>",
14144 vec!["a", "ab", "abc"],
14145 is_incomplete,
14146 counter.clone(),
14147 &mut cx,
14148 )
14149 .await;
14150 cx.run_until_parked();
14151 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14152 cx.assert_editor_state("obj.ˇ");
14153 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14154
14155 // Type "a" - filters existing completions
14156 cx.simulate_keystroke("a");
14157 cx.run_until_parked();
14158 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14159 cx.assert_editor_state("obj.aˇ");
14160 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14161
14162 // Type "b" - filters existing completions
14163 cx.simulate_keystroke("b");
14164 cx.run_until_parked();
14165 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14166 cx.assert_editor_state("obj.abˇ");
14167 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14168
14169 // Type "c" - filters existing completions
14170 cx.simulate_keystroke("c");
14171 cx.run_until_parked();
14172 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14173 cx.assert_editor_state("obj.abcˇ");
14174 check_displayed_completions(vec!["abc"], &mut cx);
14175
14176 // Backspace to delete "c" - filters existing completions
14177 cx.update_editor(|editor, window, cx| {
14178 editor.backspace(&Backspace, window, cx);
14179 });
14180 cx.run_until_parked();
14181 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14182 cx.assert_editor_state("obj.abˇ");
14183 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14184
14185 // Moving cursor to the left dismisses menu.
14186 cx.update_editor(|editor, window, cx| {
14187 editor.move_left(&MoveLeft, window, cx);
14188 });
14189 cx.run_until_parked();
14190 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14191 cx.assert_editor_state("obj.aˇb");
14192 cx.update_editor(|editor, _, _| {
14193 assert_eq!(editor.context_menu_visible(), false);
14194 });
14195
14196 // Type "b" - new request
14197 cx.simulate_keystroke("b");
14198 let is_incomplete = false;
14199 handle_completion_request(
14200 "obj.<ab|>a",
14201 vec!["ab", "abc"],
14202 is_incomplete,
14203 counter.clone(),
14204 &mut cx,
14205 )
14206 .await;
14207 cx.run_until_parked();
14208 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14209 cx.assert_editor_state("obj.abˇb");
14210 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14211
14212 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14213 cx.update_editor(|editor, window, cx| {
14214 editor.backspace(&Backspace, window, cx);
14215 });
14216 let is_incomplete = false;
14217 handle_completion_request(
14218 "obj.<a|>b",
14219 vec!["a", "ab", "abc"],
14220 is_incomplete,
14221 counter.clone(),
14222 &mut cx,
14223 )
14224 .await;
14225 cx.run_until_parked();
14226 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14227 cx.assert_editor_state("obj.aˇb");
14228 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14229
14230 // Backspace to delete "a" - dismisses menu.
14231 cx.update_editor(|editor, window, cx| {
14232 editor.backspace(&Backspace, window, cx);
14233 });
14234 cx.run_until_parked();
14235 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14236 cx.assert_editor_state("obj.ˇb");
14237 cx.update_editor(|editor, _, _| {
14238 assert_eq!(editor.context_menu_visible(), false);
14239 });
14240}
14241
14242#[gpui::test]
14243async fn test_word_completion(cx: &mut TestAppContext) {
14244 let lsp_fetch_timeout_ms = 10;
14245 init_test(cx, |language_settings| {
14246 language_settings.defaults.completions = Some(CompletionSettings {
14247 words: WordsCompletionMode::Fallback,
14248 words_min_length: 0,
14249 lsp: true,
14250 lsp_fetch_timeout_ms: 10,
14251 lsp_insert_mode: LspInsertMode::Insert,
14252 });
14253 });
14254
14255 let mut cx = EditorLspTestContext::new_rust(
14256 lsp::ServerCapabilities {
14257 completion_provider: Some(lsp::CompletionOptions {
14258 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14259 ..lsp::CompletionOptions::default()
14260 }),
14261 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14262 ..lsp::ServerCapabilities::default()
14263 },
14264 cx,
14265 )
14266 .await;
14267
14268 let throttle_completions = Arc::new(AtomicBool::new(false));
14269
14270 let lsp_throttle_completions = throttle_completions.clone();
14271 let _completion_requests_handler =
14272 cx.lsp
14273 .server
14274 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14275 let lsp_throttle_completions = lsp_throttle_completions.clone();
14276 let cx = cx.clone();
14277 async move {
14278 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14279 cx.background_executor()
14280 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14281 .await;
14282 }
14283 Ok(Some(lsp::CompletionResponse::Array(vec![
14284 lsp::CompletionItem {
14285 label: "first".into(),
14286 ..lsp::CompletionItem::default()
14287 },
14288 lsp::CompletionItem {
14289 label: "last".into(),
14290 ..lsp::CompletionItem::default()
14291 },
14292 ])))
14293 }
14294 });
14295
14296 cx.set_state(indoc! {"
14297 oneˇ
14298 two
14299 three
14300 "});
14301 cx.simulate_keystroke(".");
14302 cx.executor().run_until_parked();
14303 cx.condition(|editor, _| editor.context_menu_visible())
14304 .await;
14305 cx.update_editor(|editor, window, cx| {
14306 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14307 {
14308 assert_eq!(
14309 completion_menu_entries(menu),
14310 &["first", "last"],
14311 "When LSP server is fast to reply, no fallback word completions are used"
14312 );
14313 } else {
14314 panic!("expected completion menu to be open");
14315 }
14316 editor.cancel(&Cancel, window, cx);
14317 });
14318 cx.executor().run_until_parked();
14319 cx.condition(|editor, _| !editor.context_menu_visible())
14320 .await;
14321
14322 throttle_completions.store(true, atomic::Ordering::Release);
14323 cx.simulate_keystroke(".");
14324 cx.executor()
14325 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14326 cx.executor().run_until_parked();
14327 cx.condition(|editor, _| editor.context_menu_visible())
14328 .await;
14329 cx.update_editor(|editor, _, _| {
14330 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14331 {
14332 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14333 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14334 } else {
14335 panic!("expected completion menu to be open");
14336 }
14337 });
14338}
14339
14340#[gpui::test]
14341async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14342 init_test(cx, |language_settings| {
14343 language_settings.defaults.completions = Some(CompletionSettings {
14344 words: WordsCompletionMode::Enabled,
14345 words_min_length: 0,
14346 lsp: true,
14347 lsp_fetch_timeout_ms: 0,
14348 lsp_insert_mode: LspInsertMode::Insert,
14349 });
14350 });
14351
14352 let mut cx = EditorLspTestContext::new_rust(
14353 lsp::ServerCapabilities {
14354 completion_provider: Some(lsp::CompletionOptions {
14355 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14356 ..lsp::CompletionOptions::default()
14357 }),
14358 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14359 ..lsp::ServerCapabilities::default()
14360 },
14361 cx,
14362 )
14363 .await;
14364
14365 let _completion_requests_handler =
14366 cx.lsp
14367 .server
14368 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14369 Ok(Some(lsp::CompletionResponse::Array(vec![
14370 lsp::CompletionItem {
14371 label: "first".into(),
14372 ..lsp::CompletionItem::default()
14373 },
14374 lsp::CompletionItem {
14375 label: "last".into(),
14376 ..lsp::CompletionItem::default()
14377 },
14378 ])))
14379 });
14380
14381 cx.set_state(indoc! {"ˇ
14382 first
14383 last
14384 second
14385 "});
14386 cx.simulate_keystroke(".");
14387 cx.executor().run_until_parked();
14388 cx.condition(|editor, _| editor.context_menu_visible())
14389 .await;
14390 cx.update_editor(|editor, _, _| {
14391 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14392 {
14393 assert_eq!(
14394 completion_menu_entries(menu),
14395 &["first", "last", "second"],
14396 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14397 );
14398 } else {
14399 panic!("expected completion menu to be open");
14400 }
14401 });
14402}
14403
14404#[gpui::test]
14405async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14406 init_test(cx, |language_settings| {
14407 language_settings.defaults.completions = Some(CompletionSettings {
14408 words: WordsCompletionMode::Disabled,
14409 words_min_length: 0,
14410 lsp: true,
14411 lsp_fetch_timeout_ms: 0,
14412 lsp_insert_mode: LspInsertMode::Insert,
14413 });
14414 });
14415
14416 let mut cx = EditorLspTestContext::new_rust(
14417 lsp::ServerCapabilities {
14418 completion_provider: Some(lsp::CompletionOptions {
14419 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14420 ..lsp::CompletionOptions::default()
14421 }),
14422 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14423 ..lsp::ServerCapabilities::default()
14424 },
14425 cx,
14426 )
14427 .await;
14428
14429 let _completion_requests_handler =
14430 cx.lsp
14431 .server
14432 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14433 panic!("LSP completions should not be queried when dealing with word completions")
14434 });
14435
14436 cx.set_state(indoc! {"ˇ
14437 first
14438 last
14439 second
14440 "});
14441 cx.update_editor(|editor, window, cx| {
14442 editor.show_word_completions(&ShowWordCompletions, window, cx);
14443 });
14444 cx.executor().run_until_parked();
14445 cx.condition(|editor, _| editor.context_menu_visible())
14446 .await;
14447 cx.update_editor(|editor, _, _| {
14448 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14449 {
14450 assert_eq!(
14451 completion_menu_entries(menu),
14452 &["first", "last", "second"],
14453 "`ShowWordCompletions` action should show word completions"
14454 );
14455 } else {
14456 panic!("expected completion menu to be open");
14457 }
14458 });
14459
14460 cx.simulate_keystroke("l");
14461 cx.executor().run_until_parked();
14462 cx.condition(|editor, _| editor.context_menu_visible())
14463 .await;
14464 cx.update_editor(|editor, _, _| {
14465 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14466 {
14467 assert_eq!(
14468 completion_menu_entries(menu),
14469 &["last"],
14470 "After showing word completions, further editing should filter them and not query the LSP"
14471 );
14472 } else {
14473 panic!("expected completion menu to be open");
14474 }
14475 });
14476}
14477
14478#[gpui::test]
14479async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14480 init_test(cx, |language_settings| {
14481 language_settings.defaults.completions = Some(CompletionSettings {
14482 words: WordsCompletionMode::Fallback,
14483 words_min_length: 0,
14484 lsp: false,
14485 lsp_fetch_timeout_ms: 0,
14486 lsp_insert_mode: LspInsertMode::Insert,
14487 });
14488 });
14489
14490 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14491
14492 cx.set_state(indoc! {"ˇ
14493 0_usize
14494 let
14495 33
14496 4.5f32
14497 "});
14498 cx.update_editor(|editor, window, cx| {
14499 editor.show_completions(&ShowCompletions::default(), window, cx);
14500 });
14501 cx.executor().run_until_parked();
14502 cx.condition(|editor, _| editor.context_menu_visible())
14503 .await;
14504 cx.update_editor(|editor, window, cx| {
14505 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14506 {
14507 assert_eq!(
14508 completion_menu_entries(menu),
14509 &["let"],
14510 "With no digits in the completion query, no digits should be in the word completions"
14511 );
14512 } else {
14513 panic!("expected completion menu to be open");
14514 }
14515 editor.cancel(&Cancel, window, cx);
14516 });
14517
14518 cx.set_state(indoc! {"3ˇ
14519 0_usize
14520 let
14521 3
14522 33.35f32
14523 "});
14524 cx.update_editor(|editor, window, cx| {
14525 editor.show_completions(&ShowCompletions::default(), window, cx);
14526 });
14527 cx.executor().run_until_parked();
14528 cx.condition(|editor, _| editor.context_menu_visible())
14529 .await;
14530 cx.update_editor(|editor, _, _| {
14531 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14532 {
14533 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14534 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14535 } else {
14536 panic!("expected completion menu to be open");
14537 }
14538 });
14539}
14540
14541#[gpui::test]
14542async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14543 init_test(cx, |language_settings| {
14544 language_settings.defaults.completions = Some(CompletionSettings {
14545 words: WordsCompletionMode::Enabled,
14546 words_min_length: 3,
14547 lsp: true,
14548 lsp_fetch_timeout_ms: 0,
14549 lsp_insert_mode: LspInsertMode::Insert,
14550 });
14551 });
14552
14553 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14554 cx.set_state(indoc! {"ˇ
14555 wow
14556 wowen
14557 wowser
14558 "});
14559 cx.simulate_keystroke("w");
14560 cx.executor().run_until_parked();
14561 cx.update_editor(|editor, _, _| {
14562 if editor.context_menu.borrow_mut().is_some() {
14563 panic!(
14564 "expected completion menu to be hidden, as words completion threshold is not met"
14565 );
14566 }
14567 });
14568
14569 cx.update_editor(|editor, window, cx| {
14570 editor.show_word_completions(&ShowWordCompletions, window, cx);
14571 });
14572 cx.executor().run_until_parked();
14573 cx.update_editor(|editor, window, cx| {
14574 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14575 {
14576 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14577 } else {
14578 panic!("expected completion menu to be open after the word completions are called with an action");
14579 }
14580
14581 editor.cancel(&Cancel, window, cx);
14582 });
14583 cx.update_editor(|editor, _, _| {
14584 if editor.context_menu.borrow_mut().is_some() {
14585 panic!("expected completion menu to be hidden after canceling");
14586 }
14587 });
14588
14589 cx.simulate_keystroke("o");
14590 cx.executor().run_until_parked();
14591 cx.update_editor(|editor, _, _| {
14592 if editor.context_menu.borrow_mut().is_some() {
14593 panic!(
14594 "expected completion menu to be hidden, as words completion threshold is not met still"
14595 );
14596 }
14597 });
14598
14599 cx.simulate_keystroke("w");
14600 cx.executor().run_until_parked();
14601 cx.update_editor(|editor, _, _| {
14602 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14603 {
14604 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14605 } else {
14606 panic!("expected completion menu to be open after the word completions threshold is met");
14607 }
14608 });
14609}
14610
14611#[gpui::test]
14612async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14613 init_test(cx, |language_settings| {
14614 language_settings.defaults.completions = Some(CompletionSettings {
14615 words: WordsCompletionMode::Enabled,
14616 words_min_length: 0,
14617 lsp: true,
14618 lsp_fetch_timeout_ms: 0,
14619 lsp_insert_mode: LspInsertMode::Insert,
14620 });
14621 });
14622
14623 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14624 cx.update_editor(|editor, _, _| {
14625 editor.disable_word_completions();
14626 });
14627 cx.set_state(indoc! {"ˇ
14628 wow
14629 wowen
14630 wowser
14631 "});
14632 cx.simulate_keystroke("w");
14633 cx.executor().run_until_parked();
14634 cx.update_editor(|editor, _, _| {
14635 if editor.context_menu.borrow_mut().is_some() {
14636 panic!(
14637 "expected completion menu to be hidden, as words completion are disabled for this editor"
14638 );
14639 }
14640 });
14641
14642 cx.update_editor(|editor, window, cx| {
14643 editor.show_word_completions(&ShowWordCompletions, window, cx);
14644 });
14645 cx.executor().run_until_parked();
14646 cx.update_editor(|editor, _, _| {
14647 if editor.context_menu.borrow_mut().is_some() {
14648 panic!(
14649 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14650 );
14651 }
14652 });
14653}
14654
14655fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14656 let position = || lsp::Position {
14657 line: params.text_document_position.position.line,
14658 character: params.text_document_position.position.character,
14659 };
14660 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14661 range: lsp::Range {
14662 start: position(),
14663 end: position(),
14664 },
14665 new_text: text.to_string(),
14666 }))
14667}
14668
14669#[gpui::test]
14670async fn test_multiline_completion(cx: &mut TestAppContext) {
14671 init_test(cx, |_| {});
14672
14673 let fs = FakeFs::new(cx.executor());
14674 fs.insert_tree(
14675 path!("/a"),
14676 json!({
14677 "main.ts": "a",
14678 }),
14679 )
14680 .await;
14681
14682 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14683 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14684 let typescript_language = Arc::new(Language::new(
14685 LanguageConfig {
14686 name: "TypeScript".into(),
14687 matcher: LanguageMatcher {
14688 path_suffixes: vec!["ts".to_string()],
14689 ..LanguageMatcher::default()
14690 },
14691 line_comments: vec!["// ".into()],
14692 ..LanguageConfig::default()
14693 },
14694 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14695 ));
14696 language_registry.add(typescript_language.clone());
14697 let mut fake_servers = language_registry.register_fake_lsp(
14698 "TypeScript",
14699 FakeLspAdapter {
14700 capabilities: lsp::ServerCapabilities {
14701 completion_provider: Some(lsp::CompletionOptions {
14702 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14703 ..lsp::CompletionOptions::default()
14704 }),
14705 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14706 ..lsp::ServerCapabilities::default()
14707 },
14708 // Emulate vtsls label generation
14709 label_for_completion: Some(Box::new(|item, _| {
14710 let text = if let Some(description) = item
14711 .label_details
14712 .as_ref()
14713 .and_then(|label_details| label_details.description.as_ref())
14714 {
14715 format!("{} {}", item.label, description)
14716 } else if let Some(detail) = &item.detail {
14717 format!("{} {}", item.label, detail)
14718 } else {
14719 item.label.clone()
14720 };
14721 let len = text.len();
14722 Some(language::CodeLabel {
14723 text,
14724 runs: Vec::new(),
14725 filter_range: 0..len,
14726 })
14727 })),
14728 ..FakeLspAdapter::default()
14729 },
14730 );
14731 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14732 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14733 let worktree_id = workspace
14734 .update(cx, |workspace, _window, cx| {
14735 workspace.project().update(cx, |project, cx| {
14736 project.worktrees(cx).next().unwrap().read(cx).id()
14737 })
14738 })
14739 .unwrap();
14740 let _buffer = project
14741 .update(cx, |project, cx| {
14742 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14743 })
14744 .await
14745 .unwrap();
14746 let editor = workspace
14747 .update(cx, |workspace, window, cx| {
14748 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14749 })
14750 .unwrap()
14751 .await
14752 .unwrap()
14753 .downcast::<Editor>()
14754 .unwrap();
14755 let fake_server = fake_servers.next().await.unwrap();
14756
14757 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14758 let multiline_label_2 = "a\nb\nc\n";
14759 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14760 let multiline_description = "d\ne\nf\n";
14761 let multiline_detail_2 = "g\nh\ni\n";
14762
14763 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14764 move |params, _| async move {
14765 Ok(Some(lsp::CompletionResponse::Array(vec![
14766 lsp::CompletionItem {
14767 label: multiline_label.to_string(),
14768 text_edit: gen_text_edit(¶ms, "new_text_1"),
14769 ..lsp::CompletionItem::default()
14770 },
14771 lsp::CompletionItem {
14772 label: "single line label 1".to_string(),
14773 detail: Some(multiline_detail.to_string()),
14774 text_edit: gen_text_edit(¶ms, "new_text_2"),
14775 ..lsp::CompletionItem::default()
14776 },
14777 lsp::CompletionItem {
14778 label: "single line label 2".to_string(),
14779 label_details: Some(lsp::CompletionItemLabelDetails {
14780 description: Some(multiline_description.to_string()),
14781 detail: None,
14782 }),
14783 text_edit: gen_text_edit(¶ms, "new_text_2"),
14784 ..lsp::CompletionItem::default()
14785 },
14786 lsp::CompletionItem {
14787 label: multiline_label_2.to_string(),
14788 detail: Some(multiline_detail_2.to_string()),
14789 text_edit: gen_text_edit(¶ms, "new_text_3"),
14790 ..lsp::CompletionItem::default()
14791 },
14792 lsp::CompletionItem {
14793 label: "Label with many spaces and \t but without newlines".to_string(),
14794 detail: Some(
14795 "Details with many spaces and \t but without newlines".to_string(),
14796 ),
14797 text_edit: gen_text_edit(¶ms, "new_text_4"),
14798 ..lsp::CompletionItem::default()
14799 },
14800 ])))
14801 },
14802 );
14803
14804 editor.update_in(cx, |editor, window, cx| {
14805 cx.focus_self(window);
14806 editor.move_to_end(&MoveToEnd, window, cx);
14807 editor.handle_input(".", window, cx);
14808 });
14809 cx.run_until_parked();
14810 completion_handle.next().await.unwrap();
14811
14812 editor.update(cx, |editor, _| {
14813 assert!(editor.context_menu_visible());
14814 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14815 {
14816 let completion_labels = menu
14817 .completions
14818 .borrow()
14819 .iter()
14820 .map(|c| c.label.text.clone())
14821 .collect::<Vec<_>>();
14822 assert_eq!(
14823 completion_labels,
14824 &[
14825 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14826 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14827 "single line label 2 d e f ",
14828 "a b c g h i ",
14829 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14830 ],
14831 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14832 );
14833
14834 for completion in menu
14835 .completions
14836 .borrow()
14837 .iter() {
14838 assert_eq!(
14839 completion.label.filter_range,
14840 0..completion.label.text.len(),
14841 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14842 );
14843 }
14844 } else {
14845 panic!("expected completion menu to be open");
14846 }
14847 });
14848}
14849
14850#[gpui::test]
14851async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14852 init_test(cx, |_| {});
14853 let mut cx = EditorLspTestContext::new_rust(
14854 lsp::ServerCapabilities {
14855 completion_provider: Some(lsp::CompletionOptions {
14856 trigger_characters: Some(vec![".".to_string()]),
14857 ..Default::default()
14858 }),
14859 ..Default::default()
14860 },
14861 cx,
14862 )
14863 .await;
14864 cx.lsp
14865 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14866 Ok(Some(lsp::CompletionResponse::Array(vec![
14867 lsp::CompletionItem {
14868 label: "first".into(),
14869 ..Default::default()
14870 },
14871 lsp::CompletionItem {
14872 label: "last".into(),
14873 ..Default::default()
14874 },
14875 ])))
14876 });
14877 cx.set_state("variableˇ");
14878 cx.simulate_keystroke(".");
14879 cx.executor().run_until_parked();
14880
14881 cx.update_editor(|editor, _, _| {
14882 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14883 {
14884 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14885 } else {
14886 panic!("expected completion menu to be open");
14887 }
14888 });
14889
14890 cx.update_editor(|editor, window, cx| {
14891 editor.move_page_down(&MovePageDown::default(), window, cx);
14892 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14893 {
14894 assert!(
14895 menu.selected_item == 1,
14896 "expected PageDown to select the last item from the context menu"
14897 );
14898 } else {
14899 panic!("expected completion menu to stay open after PageDown");
14900 }
14901 });
14902
14903 cx.update_editor(|editor, window, cx| {
14904 editor.move_page_up(&MovePageUp::default(), window, cx);
14905 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14906 {
14907 assert!(
14908 menu.selected_item == 0,
14909 "expected PageUp to select the first item from the context menu"
14910 );
14911 } else {
14912 panic!("expected completion menu to stay open after PageUp");
14913 }
14914 });
14915}
14916
14917#[gpui::test]
14918async fn test_as_is_completions(cx: &mut TestAppContext) {
14919 init_test(cx, |_| {});
14920 let mut cx = EditorLspTestContext::new_rust(
14921 lsp::ServerCapabilities {
14922 completion_provider: Some(lsp::CompletionOptions {
14923 ..Default::default()
14924 }),
14925 ..Default::default()
14926 },
14927 cx,
14928 )
14929 .await;
14930 cx.lsp
14931 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14932 Ok(Some(lsp::CompletionResponse::Array(vec![
14933 lsp::CompletionItem {
14934 label: "unsafe".into(),
14935 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14936 range: lsp::Range {
14937 start: lsp::Position {
14938 line: 1,
14939 character: 2,
14940 },
14941 end: lsp::Position {
14942 line: 1,
14943 character: 3,
14944 },
14945 },
14946 new_text: "unsafe".to_string(),
14947 })),
14948 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14949 ..Default::default()
14950 },
14951 ])))
14952 });
14953 cx.set_state("fn a() {}\n nˇ");
14954 cx.executor().run_until_parked();
14955 cx.update_editor(|editor, window, cx| {
14956 editor.show_completions(
14957 &ShowCompletions {
14958 trigger: Some("\n".into()),
14959 },
14960 window,
14961 cx,
14962 );
14963 });
14964 cx.executor().run_until_parked();
14965
14966 cx.update_editor(|editor, window, cx| {
14967 editor.confirm_completion(&Default::default(), window, cx)
14968 });
14969 cx.executor().run_until_parked();
14970 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14971}
14972
14973#[gpui::test]
14974async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14975 init_test(cx, |_| {});
14976 let language =
14977 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14978 let mut cx = EditorLspTestContext::new(
14979 language,
14980 lsp::ServerCapabilities {
14981 completion_provider: Some(lsp::CompletionOptions {
14982 ..lsp::CompletionOptions::default()
14983 }),
14984 ..lsp::ServerCapabilities::default()
14985 },
14986 cx,
14987 )
14988 .await;
14989
14990 cx.set_state(
14991 "#ifndef BAR_H
14992#define BAR_H
14993
14994#include <stdbool.h>
14995
14996int fn_branch(bool do_branch1, bool do_branch2);
14997
14998#endif // BAR_H
14999ˇ",
15000 );
15001 cx.executor().run_until_parked();
15002 cx.update_editor(|editor, window, cx| {
15003 editor.handle_input("#", window, cx);
15004 });
15005 cx.executor().run_until_parked();
15006 cx.update_editor(|editor, window, cx| {
15007 editor.handle_input("i", window, cx);
15008 });
15009 cx.executor().run_until_parked();
15010 cx.update_editor(|editor, window, cx| {
15011 editor.handle_input("n", window, cx);
15012 });
15013 cx.executor().run_until_parked();
15014 cx.assert_editor_state(
15015 "#ifndef BAR_H
15016#define BAR_H
15017
15018#include <stdbool.h>
15019
15020int fn_branch(bool do_branch1, bool do_branch2);
15021
15022#endif // BAR_H
15023#inˇ",
15024 );
15025
15026 cx.lsp
15027 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15028 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15029 is_incomplete: false,
15030 item_defaults: None,
15031 items: vec![lsp::CompletionItem {
15032 kind: Some(lsp::CompletionItemKind::SNIPPET),
15033 label_details: Some(lsp::CompletionItemLabelDetails {
15034 detail: Some("header".to_string()),
15035 description: None,
15036 }),
15037 label: " include".to_string(),
15038 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15039 range: lsp::Range {
15040 start: lsp::Position {
15041 line: 8,
15042 character: 1,
15043 },
15044 end: lsp::Position {
15045 line: 8,
15046 character: 1,
15047 },
15048 },
15049 new_text: "include \"$0\"".to_string(),
15050 })),
15051 sort_text: Some("40b67681include".to_string()),
15052 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15053 filter_text: Some("include".to_string()),
15054 insert_text: Some("include \"$0\"".to_string()),
15055 ..lsp::CompletionItem::default()
15056 }],
15057 })))
15058 });
15059 cx.update_editor(|editor, window, cx| {
15060 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15061 });
15062 cx.executor().run_until_parked();
15063 cx.update_editor(|editor, window, cx| {
15064 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15065 });
15066 cx.executor().run_until_parked();
15067 cx.assert_editor_state(
15068 "#ifndef BAR_H
15069#define BAR_H
15070
15071#include <stdbool.h>
15072
15073int fn_branch(bool do_branch1, bool do_branch2);
15074
15075#endif // BAR_H
15076#include \"ˇ\"",
15077 );
15078
15079 cx.lsp
15080 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15081 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15082 is_incomplete: true,
15083 item_defaults: None,
15084 items: vec![lsp::CompletionItem {
15085 kind: Some(lsp::CompletionItemKind::FILE),
15086 label: "AGL/".to_string(),
15087 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15088 range: lsp::Range {
15089 start: lsp::Position {
15090 line: 8,
15091 character: 10,
15092 },
15093 end: lsp::Position {
15094 line: 8,
15095 character: 11,
15096 },
15097 },
15098 new_text: "AGL/".to_string(),
15099 })),
15100 sort_text: Some("40b67681AGL/".to_string()),
15101 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15102 filter_text: Some("AGL/".to_string()),
15103 insert_text: Some("AGL/".to_string()),
15104 ..lsp::CompletionItem::default()
15105 }],
15106 })))
15107 });
15108 cx.update_editor(|editor, window, cx| {
15109 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15110 });
15111 cx.executor().run_until_parked();
15112 cx.update_editor(|editor, window, cx| {
15113 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15114 });
15115 cx.executor().run_until_parked();
15116 cx.assert_editor_state(
15117 r##"#ifndef BAR_H
15118#define BAR_H
15119
15120#include <stdbool.h>
15121
15122int fn_branch(bool do_branch1, bool do_branch2);
15123
15124#endif // BAR_H
15125#include "AGL/ˇ"##,
15126 );
15127
15128 cx.update_editor(|editor, window, cx| {
15129 editor.handle_input("\"", window, cx);
15130 });
15131 cx.executor().run_until_parked();
15132 cx.assert_editor_state(
15133 r##"#ifndef BAR_H
15134#define BAR_H
15135
15136#include <stdbool.h>
15137
15138int fn_branch(bool do_branch1, bool do_branch2);
15139
15140#endif // BAR_H
15141#include "AGL/"ˇ"##,
15142 );
15143}
15144
15145#[gpui::test]
15146async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15147 init_test(cx, |_| {});
15148
15149 let mut cx = EditorLspTestContext::new_rust(
15150 lsp::ServerCapabilities {
15151 completion_provider: Some(lsp::CompletionOptions {
15152 trigger_characters: Some(vec![".".to_string()]),
15153 resolve_provider: Some(true),
15154 ..Default::default()
15155 }),
15156 ..Default::default()
15157 },
15158 cx,
15159 )
15160 .await;
15161
15162 cx.set_state("fn main() { let a = 2ˇ; }");
15163 cx.simulate_keystroke(".");
15164 let completion_item = lsp::CompletionItem {
15165 label: "Some".into(),
15166 kind: Some(lsp::CompletionItemKind::SNIPPET),
15167 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15168 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15169 kind: lsp::MarkupKind::Markdown,
15170 value: "```rust\nSome(2)\n```".to_string(),
15171 })),
15172 deprecated: Some(false),
15173 sort_text: Some("Some".to_string()),
15174 filter_text: Some("Some".to_string()),
15175 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15176 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15177 range: lsp::Range {
15178 start: lsp::Position {
15179 line: 0,
15180 character: 22,
15181 },
15182 end: lsp::Position {
15183 line: 0,
15184 character: 22,
15185 },
15186 },
15187 new_text: "Some(2)".to_string(),
15188 })),
15189 additional_text_edits: Some(vec![lsp::TextEdit {
15190 range: lsp::Range {
15191 start: lsp::Position {
15192 line: 0,
15193 character: 20,
15194 },
15195 end: lsp::Position {
15196 line: 0,
15197 character: 22,
15198 },
15199 },
15200 new_text: "".to_string(),
15201 }]),
15202 ..Default::default()
15203 };
15204
15205 let closure_completion_item = completion_item.clone();
15206 let counter = Arc::new(AtomicUsize::new(0));
15207 let counter_clone = counter.clone();
15208 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15209 let task_completion_item = closure_completion_item.clone();
15210 counter_clone.fetch_add(1, atomic::Ordering::Release);
15211 async move {
15212 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15213 is_incomplete: true,
15214 item_defaults: None,
15215 items: vec![task_completion_item],
15216 })))
15217 }
15218 });
15219
15220 cx.condition(|editor, _| editor.context_menu_visible())
15221 .await;
15222 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15223 assert!(request.next().await.is_some());
15224 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15225
15226 cx.simulate_keystrokes("S o m");
15227 cx.condition(|editor, _| editor.context_menu_visible())
15228 .await;
15229 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15230 assert!(request.next().await.is_some());
15231 assert!(request.next().await.is_some());
15232 assert!(request.next().await.is_some());
15233 request.close();
15234 assert!(request.next().await.is_none());
15235 assert_eq!(
15236 counter.load(atomic::Ordering::Acquire),
15237 4,
15238 "With the completions menu open, only one LSP request should happen per input"
15239 );
15240}
15241
15242#[gpui::test]
15243async fn test_toggle_comment(cx: &mut TestAppContext) {
15244 init_test(cx, |_| {});
15245 let mut cx = EditorTestContext::new(cx).await;
15246 let language = Arc::new(Language::new(
15247 LanguageConfig {
15248 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15249 ..Default::default()
15250 },
15251 Some(tree_sitter_rust::LANGUAGE.into()),
15252 ));
15253 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15254
15255 // If multiple selections intersect a line, the line is only toggled once.
15256 cx.set_state(indoc! {"
15257 fn a() {
15258 «//b();
15259 ˇ»// «c();
15260 //ˇ» d();
15261 }
15262 "});
15263
15264 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15265
15266 cx.assert_editor_state(indoc! {"
15267 fn a() {
15268 «b();
15269 c();
15270 ˇ» d();
15271 }
15272 "});
15273
15274 // The comment prefix is inserted at the same column for every line in a
15275 // selection.
15276 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15277
15278 cx.assert_editor_state(indoc! {"
15279 fn a() {
15280 // «b();
15281 // c();
15282 ˇ»// d();
15283 }
15284 "});
15285
15286 // If a selection ends at the beginning of a line, that line is not toggled.
15287 cx.set_selections_state(indoc! {"
15288 fn a() {
15289 // b();
15290 «// c();
15291 ˇ» // d();
15292 }
15293 "});
15294
15295 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15296
15297 cx.assert_editor_state(indoc! {"
15298 fn a() {
15299 // b();
15300 «c();
15301 ˇ» // d();
15302 }
15303 "});
15304
15305 // If a selection span a single line and is empty, the line is toggled.
15306 cx.set_state(indoc! {"
15307 fn a() {
15308 a();
15309 b();
15310 ˇ
15311 }
15312 "});
15313
15314 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15315
15316 cx.assert_editor_state(indoc! {"
15317 fn a() {
15318 a();
15319 b();
15320 //•ˇ
15321 }
15322 "});
15323
15324 // If a selection span multiple lines, empty lines are not toggled.
15325 cx.set_state(indoc! {"
15326 fn a() {
15327 «a();
15328
15329 c();ˇ»
15330 }
15331 "});
15332
15333 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15334
15335 cx.assert_editor_state(indoc! {"
15336 fn a() {
15337 // «a();
15338
15339 // c();ˇ»
15340 }
15341 "});
15342
15343 // If a selection includes multiple comment prefixes, all lines are uncommented.
15344 cx.set_state(indoc! {"
15345 fn a() {
15346 «// a();
15347 /// b();
15348 //! c();ˇ»
15349 }
15350 "});
15351
15352 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15353
15354 cx.assert_editor_state(indoc! {"
15355 fn a() {
15356 «a();
15357 b();
15358 c();ˇ»
15359 }
15360 "});
15361}
15362
15363#[gpui::test]
15364async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15365 init_test(cx, |_| {});
15366 let mut cx = EditorTestContext::new(cx).await;
15367 let language = Arc::new(Language::new(
15368 LanguageConfig {
15369 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15370 ..Default::default()
15371 },
15372 Some(tree_sitter_rust::LANGUAGE.into()),
15373 ));
15374 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15375
15376 let toggle_comments = &ToggleComments {
15377 advance_downwards: false,
15378 ignore_indent: true,
15379 };
15380
15381 // If multiple selections intersect a line, the line is only toggled once.
15382 cx.set_state(indoc! {"
15383 fn a() {
15384 // «b();
15385 // c();
15386 // ˇ» d();
15387 }
15388 "});
15389
15390 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15391
15392 cx.assert_editor_state(indoc! {"
15393 fn a() {
15394 «b();
15395 c();
15396 ˇ» d();
15397 }
15398 "});
15399
15400 // The comment prefix is inserted at the beginning of each line
15401 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15402
15403 cx.assert_editor_state(indoc! {"
15404 fn a() {
15405 // «b();
15406 // c();
15407 // ˇ» d();
15408 }
15409 "});
15410
15411 // If a selection ends at the beginning of a line, that line is not toggled.
15412 cx.set_selections_state(indoc! {"
15413 fn a() {
15414 // b();
15415 // «c();
15416 ˇ»// d();
15417 }
15418 "});
15419
15420 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15421
15422 cx.assert_editor_state(indoc! {"
15423 fn a() {
15424 // b();
15425 «c();
15426 ˇ»// d();
15427 }
15428 "});
15429
15430 // If a selection span a single line and is empty, the line is toggled.
15431 cx.set_state(indoc! {"
15432 fn a() {
15433 a();
15434 b();
15435 ˇ
15436 }
15437 "});
15438
15439 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15440
15441 cx.assert_editor_state(indoc! {"
15442 fn a() {
15443 a();
15444 b();
15445 //ˇ
15446 }
15447 "});
15448
15449 // If a selection span multiple lines, empty lines are not toggled.
15450 cx.set_state(indoc! {"
15451 fn a() {
15452 «a();
15453
15454 c();ˇ»
15455 }
15456 "});
15457
15458 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15459
15460 cx.assert_editor_state(indoc! {"
15461 fn a() {
15462 // «a();
15463
15464 // c();ˇ»
15465 }
15466 "});
15467
15468 // If a selection includes multiple comment prefixes, all lines are uncommented.
15469 cx.set_state(indoc! {"
15470 fn a() {
15471 // «a();
15472 /// b();
15473 //! c();ˇ»
15474 }
15475 "});
15476
15477 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15478
15479 cx.assert_editor_state(indoc! {"
15480 fn a() {
15481 «a();
15482 b();
15483 c();ˇ»
15484 }
15485 "});
15486}
15487
15488#[gpui::test]
15489async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15490 init_test(cx, |_| {});
15491
15492 let language = Arc::new(Language::new(
15493 LanguageConfig {
15494 line_comments: vec!["// ".into()],
15495 ..Default::default()
15496 },
15497 Some(tree_sitter_rust::LANGUAGE.into()),
15498 ));
15499
15500 let mut cx = EditorTestContext::new(cx).await;
15501
15502 cx.language_registry().add(language.clone());
15503 cx.update_buffer(|buffer, cx| {
15504 buffer.set_language(Some(language), cx);
15505 });
15506
15507 let toggle_comments = &ToggleComments {
15508 advance_downwards: true,
15509 ignore_indent: false,
15510 };
15511
15512 // Single cursor on one line -> advance
15513 // Cursor moves horizontally 3 characters as well on non-blank line
15514 cx.set_state(indoc!(
15515 "fn a() {
15516 ˇdog();
15517 cat();
15518 }"
15519 ));
15520 cx.update_editor(|editor, window, cx| {
15521 editor.toggle_comments(toggle_comments, window, cx);
15522 });
15523 cx.assert_editor_state(indoc!(
15524 "fn a() {
15525 // dog();
15526 catˇ();
15527 }"
15528 ));
15529
15530 // Single selection on one line -> don't advance
15531 cx.set_state(indoc!(
15532 "fn a() {
15533 «dog()ˇ»;
15534 cat();
15535 }"
15536 ));
15537 cx.update_editor(|editor, window, cx| {
15538 editor.toggle_comments(toggle_comments, window, cx);
15539 });
15540 cx.assert_editor_state(indoc!(
15541 "fn a() {
15542 // «dog()ˇ»;
15543 cat();
15544 }"
15545 ));
15546
15547 // Multiple cursors on one line -> advance
15548 cx.set_state(indoc!(
15549 "fn a() {
15550 ˇdˇog();
15551 cat();
15552 }"
15553 ));
15554 cx.update_editor(|editor, window, cx| {
15555 editor.toggle_comments(toggle_comments, window, cx);
15556 });
15557 cx.assert_editor_state(indoc!(
15558 "fn a() {
15559 // dog();
15560 catˇ(ˇ);
15561 }"
15562 ));
15563
15564 // Multiple cursors on one line, with selection -> don't advance
15565 cx.set_state(indoc!(
15566 "fn a() {
15567 ˇdˇog«()ˇ»;
15568 cat();
15569 }"
15570 ));
15571 cx.update_editor(|editor, window, cx| {
15572 editor.toggle_comments(toggle_comments, window, cx);
15573 });
15574 cx.assert_editor_state(indoc!(
15575 "fn a() {
15576 // ˇdˇog«()ˇ»;
15577 cat();
15578 }"
15579 ));
15580
15581 // Single cursor on one line -> advance
15582 // Cursor moves to column 0 on blank line
15583 cx.set_state(indoc!(
15584 "fn a() {
15585 ˇdog();
15586
15587 cat();
15588 }"
15589 ));
15590 cx.update_editor(|editor, window, cx| {
15591 editor.toggle_comments(toggle_comments, window, cx);
15592 });
15593 cx.assert_editor_state(indoc!(
15594 "fn a() {
15595 // dog();
15596 ˇ
15597 cat();
15598 }"
15599 ));
15600
15601 // Single cursor on one line -> advance
15602 // Cursor starts and ends at column 0
15603 cx.set_state(indoc!(
15604 "fn a() {
15605 ˇ dog();
15606 cat();
15607 }"
15608 ));
15609 cx.update_editor(|editor, window, cx| {
15610 editor.toggle_comments(toggle_comments, window, cx);
15611 });
15612 cx.assert_editor_state(indoc!(
15613 "fn a() {
15614 // dog();
15615 ˇ cat();
15616 }"
15617 ));
15618}
15619
15620#[gpui::test]
15621async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15622 init_test(cx, |_| {});
15623
15624 let mut cx = EditorTestContext::new(cx).await;
15625
15626 let html_language = Arc::new(
15627 Language::new(
15628 LanguageConfig {
15629 name: "HTML".into(),
15630 block_comment: Some(BlockCommentConfig {
15631 start: "<!-- ".into(),
15632 prefix: "".into(),
15633 end: " -->".into(),
15634 tab_size: 0,
15635 }),
15636 ..Default::default()
15637 },
15638 Some(tree_sitter_html::LANGUAGE.into()),
15639 )
15640 .with_injection_query(
15641 r#"
15642 (script_element
15643 (raw_text) @injection.content
15644 (#set! injection.language "javascript"))
15645 "#,
15646 )
15647 .unwrap(),
15648 );
15649
15650 let javascript_language = Arc::new(Language::new(
15651 LanguageConfig {
15652 name: "JavaScript".into(),
15653 line_comments: vec!["// ".into()],
15654 ..Default::default()
15655 },
15656 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15657 ));
15658
15659 cx.language_registry().add(html_language.clone());
15660 cx.language_registry().add(javascript_language);
15661 cx.update_buffer(|buffer, cx| {
15662 buffer.set_language(Some(html_language), cx);
15663 });
15664
15665 // Toggle comments for empty selections
15666 cx.set_state(
15667 &r#"
15668 <p>A</p>ˇ
15669 <p>B</p>ˇ
15670 <p>C</p>ˇ
15671 "#
15672 .unindent(),
15673 );
15674 cx.update_editor(|editor, window, cx| {
15675 editor.toggle_comments(&ToggleComments::default(), window, cx)
15676 });
15677 cx.assert_editor_state(
15678 &r#"
15679 <!-- <p>A</p>ˇ -->
15680 <!-- <p>B</p>ˇ -->
15681 <!-- <p>C</p>ˇ -->
15682 "#
15683 .unindent(),
15684 );
15685 cx.update_editor(|editor, window, cx| {
15686 editor.toggle_comments(&ToggleComments::default(), window, cx)
15687 });
15688 cx.assert_editor_state(
15689 &r#"
15690 <p>A</p>ˇ
15691 <p>B</p>ˇ
15692 <p>C</p>ˇ
15693 "#
15694 .unindent(),
15695 );
15696
15697 // Toggle comments for mixture of empty and non-empty selections, where
15698 // multiple selections occupy a given line.
15699 cx.set_state(
15700 &r#"
15701 <p>A«</p>
15702 <p>ˇ»B</p>ˇ
15703 <p>C«</p>
15704 <p>ˇ»D</p>ˇ
15705 "#
15706 .unindent(),
15707 );
15708
15709 cx.update_editor(|editor, window, cx| {
15710 editor.toggle_comments(&ToggleComments::default(), window, cx)
15711 });
15712 cx.assert_editor_state(
15713 &r#"
15714 <!-- <p>A«</p>
15715 <p>ˇ»B</p>ˇ -->
15716 <!-- <p>C«</p>
15717 <p>ˇ»D</p>ˇ -->
15718 "#
15719 .unindent(),
15720 );
15721 cx.update_editor(|editor, window, cx| {
15722 editor.toggle_comments(&ToggleComments::default(), window, cx)
15723 });
15724 cx.assert_editor_state(
15725 &r#"
15726 <p>A«</p>
15727 <p>ˇ»B</p>ˇ
15728 <p>C«</p>
15729 <p>ˇ»D</p>ˇ
15730 "#
15731 .unindent(),
15732 );
15733
15734 // Toggle comments when different languages are active for different
15735 // selections.
15736 cx.set_state(
15737 &r#"
15738 ˇ<script>
15739 ˇvar x = new Y();
15740 ˇ</script>
15741 "#
15742 .unindent(),
15743 );
15744 cx.executor().run_until_parked();
15745 cx.update_editor(|editor, window, cx| {
15746 editor.toggle_comments(&ToggleComments::default(), window, cx)
15747 });
15748 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15749 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15750 cx.assert_editor_state(
15751 &r#"
15752 <!-- ˇ<script> -->
15753 // ˇvar x = new Y();
15754 <!-- ˇ</script> -->
15755 "#
15756 .unindent(),
15757 );
15758}
15759
15760#[gpui::test]
15761fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15762 init_test(cx, |_| {});
15763
15764 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15765 let multibuffer = cx.new(|cx| {
15766 let mut multibuffer = MultiBuffer::new(ReadWrite);
15767 multibuffer.push_excerpts(
15768 buffer.clone(),
15769 [
15770 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15771 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15772 ],
15773 cx,
15774 );
15775 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15776 multibuffer
15777 });
15778
15779 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15780 editor.update_in(cx, |editor, window, cx| {
15781 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15782 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15783 s.select_ranges([
15784 Point::new(0, 0)..Point::new(0, 0),
15785 Point::new(1, 0)..Point::new(1, 0),
15786 ])
15787 });
15788
15789 editor.handle_input("X", window, cx);
15790 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15791 assert_eq!(
15792 editor.selections.ranges(cx),
15793 [
15794 Point::new(0, 1)..Point::new(0, 1),
15795 Point::new(1, 1)..Point::new(1, 1),
15796 ]
15797 );
15798
15799 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15801 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15802 });
15803 editor.backspace(&Default::default(), window, cx);
15804 assert_eq!(editor.text(cx), "Xa\nbbb");
15805 assert_eq!(
15806 editor.selections.ranges(cx),
15807 [Point::new(1, 0)..Point::new(1, 0)]
15808 );
15809
15810 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15811 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15812 });
15813 editor.backspace(&Default::default(), window, cx);
15814 assert_eq!(editor.text(cx), "X\nbb");
15815 assert_eq!(
15816 editor.selections.ranges(cx),
15817 [Point::new(0, 1)..Point::new(0, 1)]
15818 );
15819 });
15820}
15821
15822#[gpui::test]
15823fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15824 init_test(cx, |_| {});
15825
15826 let markers = vec![('[', ']').into(), ('(', ')').into()];
15827 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15828 indoc! {"
15829 [aaaa
15830 (bbbb]
15831 cccc)",
15832 },
15833 markers.clone(),
15834 );
15835 let excerpt_ranges = markers.into_iter().map(|marker| {
15836 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15837 ExcerptRange::new(context)
15838 });
15839 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15840 let multibuffer = cx.new(|cx| {
15841 let mut multibuffer = MultiBuffer::new(ReadWrite);
15842 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15843 multibuffer
15844 });
15845
15846 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15847 editor.update_in(cx, |editor, window, cx| {
15848 let (expected_text, selection_ranges) = marked_text_ranges(
15849 indoc! {"
15850 aaaa
15851 bˇbbb
15852 bˇbbˇb
15853 cccc"
15854 },
15855 true,
15856 );
15857 assert_eq!(editor.text(cx), expected_text);
15858 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15859 s.select_ranges(selection_ranges)
15860 });
15861
15862 editor.handle_input("X", window, cx);
15863
15864 let (expected_text, expected_selections) = marked_text_ranges(
15865 indoc! {"
15866 aaaa
15867 bXˇbbXb
15868 bXˇbbXˇb
15869 cccc"
15870 },
15871 false,
15872 );
15873 assert_eq!(editor.text(cx), expected_text);
15874 assert_eq!(editor.selections.ranges(cx), expected_selections);
15875
15876 editor.newline(&Newline, window, cx);
15877 let (expected_text, expected_selections) = marked_text_ranges(
15878 indoc! {"
15879 aaaa
15880 bX
15881 ˇbbX
15882 b
15883 bX
15884 ˇbbX
15885 ˇb
15886 cccc"
15887 },
15888 false,
15889 );
15890 assert_eq!(editor.text(cx), expected_text);
15891 assert_eq!(editor.selections.ranges(cx), expected_selections);
15892 });
15893}
15894
15895#[gpui::test]
15896fn test_refresh_selections(cx: &mut TestAppContext) {
15897 init_test(cx, |_| {});
15898
15899 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15900 let mut excerpt1_id = None;
15901 let multibuffer = cx.new(|cx| {
15902 let mut multibuffer = MultiBuffer::new(ReadWrite);
15903 excerpt1_id = multibuffer
15904 .push_excerpts(
15905 buffer.clone(),
15906 [
15907 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15908 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15909 ],
15910 cx,
15911 )
15912 .into_iter()
15913 .next();
15914 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15915 multibuffer
15916 });
15917
15918 let editor = cx.add_window(|window, cx| {
15919 let mut editor = build_editor(multibuffer.clone(), window, cx);
15920 let snapshot = editor.snapshot(window, cx);
15921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15922 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15923 });
15924 editor.begin_selection(
15925 Point::new(2, 1).to_display_point(&snapshot),
15926 true,
15927 1,
15928 window,
15929 cx,
15930 );
15931 assert_eq!(
15932 editor.selections.ranges(cx),
15933 [
15934 Point::new(1, 3)..Point::new(1, 3),
15935 Point::new(2, 1)..Point::new(2, 1),
15936 ]
15937 );
15938 editor
15939 });
15940
15941 // Refreshing selections is a no-op when excerpts haven't changed.
15942 _ = editor.update(cx, |editor, window, cx| {
15943 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15944 assert_eq!(
15945 editor.selections.ranges(cx),
15946 [
15947 Point::new(1, 3)..Point::new(1, 3),
15948 Point::new(2, 1)..Point::new(2, 1),
15949 ]
15950 );
15951 });
15952
15953 multibuffer.update(cx, |multibuffer, cx| {
15954 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15955 });
15956 _ = editor.update(cx, |editor, window, cx| {
15957 // Removing an excerpt causes the first selection to become degenerate.
15958 assert_eq!(
15959 editor.selections.ranges(cx),
15960 [
15961 Point::new(0, 0)..Point::new(0, 0),
15962 Point::new(0, 1)..Point::new(0, 1)
15963 ]
15964 );
15965
15966 // Refreshing selections will relocate the first selection to the original buffer
15967 // location.
15968 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15969 assert_eq!(
15970 editor.selections.ranges(cx),
15971 [
15972 Point::new(0, 1)..Point::new(0, 1),
15973 Point::new(0, 3)..Point::new(0, 3)
15974 ]
15975 );
15976 assert!(editor.selections.pending_anchor().is_some());
15977 });
15978}
15979
15980#[gpui::test]
15981fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15982 init_test(cx, |_| {});
15983
15984 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15985 let mut excerpt1_id = None;
15986 let multibuffer = cx.new(|cx| {
15987 let mut multibuffer = MultiBuffer::new(ReadWrite);
15988 excerpt1_id = multibuffer
15989 .push_excerpts(
15990 buffer.clone(),
15991 [
15992 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15993 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15994 ],
15995 cx,
15996 )
15997 .into_iter()
15998 .next();
15999 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16000 multibuffer
16001 });
16002
16003 let editor = cx.add_window(|window, cx| {
16004 let mut editor = build_editor(multibuffer.clone(), window, cx);
16005 let snapshot = editor.snapshot(window, cx);
16006 editor.begin_selection(
16007 Point::new(1, 3).to_display_point(&snapshot),
16008 false,
16009 1,
16010 window,
16011 cx,
16012 );
16013 assert_eq!(
16014 editor.selections.ranges(cx),
16015 [Point::new(1, 3)..Point::new(1, 3)]
16016 );
16017 editor
16018 });
16019
16020 multibuffer.update(cx, |multibuffer, cx| {
16021 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16022 });
16023 _ = editor.update(cx, |editor, window, cx| {
16024 assert_eq!(
16025 editor.selections.ranges(cx),
16026 [Point::new(0, 0)..Point::new(0, 0)]
16027 );
16028
16029 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16031 assert_eq!(
16032 editor.selections.ranges(cx),
16033 [Point::new(0, 3)..Point::new(0, 3)]
16034 );
16035 assert!(editor.selections.pending_anchor().is_some());
16036 });
16037}
16038
16039#[gpui::test]
16040async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16041 init_test(cx, |_| {});
16042
16043 let language = Arc::new(
16044 Language::new(
16045 LanguageConfig {
16046 brackets: BracketPairConfig {
16047 pairs: vec![
16048 BracketPair {
16049 start: "{".to_string(),
16050 end: "}".to_string(),
16051 close: true,
16052 surround: true,
16053 newline: true,
16054 },
16055 BracketPair {
16056 start: "/* ".to_string(),
16057 end: " */".to_string(),
16058 close: true,
16059 surround: true,
16060 newline: true,
16061 },
16062 ],
16063 ..Default::default()
16064 },
16065 ..Default::default()
16066 },
16067 Some(tree_sitter_rust::LANGUAGE.into()),
16068 )
16069 .with_indents_query("")
16070 .unwrap(),
16071 );
16072
16073 let text = concat!(
16074 "{ }\n", //
16075 " x\n", //
16076 " /* */\n", //
16077 "x\n", //
16078 "{{} }\n", //
16079 );
16080
16081 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16082 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16083 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16084 editor
16085 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16086 .await;
16087
16088 editor.update_in(cx, |editor, window, cx| {
16089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16090 s.select_display_ranges([
16091 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16092 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16093 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16094 ])
16095 });
16096 editor.newline(&Newline, window, cx);
16097
16098 assert_eq!(
16099 editor.buffer().read(cx).read(cx).text(),
16100 concat!(
16101 "{ \n", // Suppress rustfmt
16102 "\n", //
16103 "}\n", //
16104 " x\n", //
16105 " /* \n", //
16106 " \n", //
16107 " */\n", //
16108 "x\n", //
16109 "{{} \n", //
16110 "}\n", //
16111 )
16112 );
16113 });
16114}
16115
16116#[gpui::test]
16117fn test_highlighted_ranges(cx: &mut TestAppContext) {
16118 init_test(cx, |_| {});
16119
16120 let editor = cx.add_window(|window, cx| {
16121 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16122 build_editor(buffer, window, cx)
16123 });
16124
16125 _ = editor.update(cx, |editor, window, cx| {
16126 struct Type1;
16127 struct Type2;
16128
16129 let buffer = editor.buffer.read(cx).snapshot(cx);
16130
16131 let anchor_range =
16132 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16133
16134 editor.highlight_background::<Type1>(
16135 &[
16136 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16137 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16138 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16139 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16140 ],
16141 |_| Hsla::red(),
16142 cx,
16143 );
16144 editor.highlight_background::<Type2>(
16145 &[
16146 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16147 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16148 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16149 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16150 ],
16151 |_| Hsla::green(),
16152 cx,
16153 );
16154
16155 let snapshot = editor.snapshot(window, cx);
16156 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16157 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16158 &snapshot,
16159 cx.theme(),
16160 );
16161 assert_eq!(
16162 highlighted_ranges,
16163 &[
16164 (
16165 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16166 Hsla::green(),
16167 ),
16168 (
16169 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16170 Hsla::red(),
16171 ),
16172 (
16173 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16174 Hsla::green(),
16175 ),
16176 (
16177 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16178 Hsla::red(),
16179 ),
16180 ]
16181 );
16182 assert_eq!(
16183 editor.sorted_background_highlights_in_range(
16184 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16185 &snapshot,
16186 cx.theme(),
16187 ),
16188 &[(
16189 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16190 Hsla::red(),
16191 )]
16192 );
16193 });
16194}
16195
16196#[gpui::test]
16197async fn test_following(cx: &mut TestAppContext) {
16198 init_test(cx, |_| {});
16199
16200 let fs = FakeFs::new(cx.executor());
16201 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16202
16203 let buffer = project.update(cx, |project, cx| {
16204 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16205 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16206 });
16207 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16208 let follower = cx.update(|cx| {
16209 cx.open_window(
16210 WindowOptions {
16211 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16212 gpui::Point::new(px(0.), px(0.)),
16213 gpui::Point::new(px(10.), px(80.)),
16214 ))),
16215 ..Default::default()
16216 },
16217 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16218 )
16219 .unwrap()
16220 });
16221
16222 let is_still_following = Rc::new(RefCell::new(true));
16223 let follower_edit_event_count = Rc::new(RefCell::new(0));
16224 let pending_update = Rc::new(RefCell::new(None));
16225 let leader_entity = leader.root(cx).unwrap();
16226 let follower_entity = follower.root(cx).unwrap();
16227 _ = follower.update(cx, {
16228 let update = pending_update.clone();
16229 let is_still_following = is_still_following.clone();
16230 let follower_edit_event_count = follower_edit_event_count.clone();
16231 |_, window, cx| {
16232 cx.subscribe_in(
16233 &leader_entity,
16234 window,
16235 move |_, leader, event, window, cx| {
16236 leader.read(cx).add_event_to_update_proto(
16237 event,
16238 &mut update.borrow_mut(),
16239 window,
16240 cx,
16241 );
16242 },
16243 )
16244 .detach();
16245
16246 cx.subscribe_in(
16247 &follower_entity,
16248 window,
16249 move |_, _, event: &EditorEvent, _window, _cx| {
16250 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16251 *is_still_following.borrow_mut() = false;
16252 }
16253
16254 if let EditorEvent::BufferEdited = event {
16255 *follower_edit_event_count.borrow_mut() += 1;
16256 }
16257 },
16258 )
16259 .detach();
16260 }
16261 });
16262
16263 // Update the selections only
16264 _ = leader.update(cx, |leader, window, cx| {
16265 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16266 s.select_ranges([1..1])
16267 });
16268 });
16269 follower
16270 .update(cx, |follower, window, cx| {
16271 follower.apply_update_proto(
16272 &project,
16273 pending_update.borrow_mut().take().unwrap(),
16274 window,
16275 cx,
16276 )
16277 })
16278 .unwrap()
16279 .await
16280 .unwrap();
16281 _ = follower.update(cx, |follower, _, cx| {
16282 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16283 });
16284 assert!(*is_still_following.borrow());
16285 assert_eq!(*follower_edit_event_count.borrow(), 0);
16286
16287 // Update the scroll position only
16288 _ = leader.update(cx, |leader, window, cx| {
16289 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16290 });
16291 follower
16292 .update(cx, |follower, window, cx| {
16293 follower.apply_update_proto(
16294 &project,
16295 pending_update.borrow_mut().take().unwrap(),
16296 window,
16297 cx,
16298 )
16299 })
16300 .unwrap()
16301 .await
16302 .unwrap();
16303 assert_eq!(
16304 follower
16305 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16306 .unwrap(),
16307 gpui::Point::new(1.5, 3.5)
16308 );
16309 assert!(*is_still_following.borrow());
16310 assert_eq!(*follower_edit_event_count.borrow(), 0);
16311
16312 // Update the selections and scroll position. The follower's scroll position is updated
16313 // via autoscroll, not via the leader's exact scroll position.
16314 _ = leader.update(cx, |leader, window, cx| {
16315 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16316 s.select_ranges([0..0])
16317 });
16318 leader.request_autoscroll(Autoscroll::newest(), cx);
16319 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16320 });
16321 follower
16322 .update(cx, |follower, window, cx| {
16323 follower.apply_update_proto(
16324 &project,
16325 pending_update.borrow_mut().take().unwrap(),
16326 window,
16327 cx,
16328 )
16329 })
16330 .unwrap()
16331 .await
16332 .unwrap();
16333 _ = follower.update(cx, |follower, _, cx| {
16334 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16335 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16336 });
16337 assert!(*is_still_following.borrow());
16338
16339 // Creating a pending selection that precedes another selection
16340 _ = leader.update(cx, |leader, window, cx| {
16341 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16342 s.select_ranges([1..1])
16343 });
16344 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16345 });
16346 follower
16347 .update(cx, |follower, window, cx| {
16348 follower.apply_update_proto(
16349 &project,
16350 pending_update.borrow_mut().take().unwrap(),
16351 window,
16352 cx,
16353 )
16354 })
16355 .unwrap()
16356 .await
16357 .unwrap();
16358 _ = follower.update(cx, |follower, _, cx| {
16359 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16360 });
16361 assert!(*is_still_following.borrow());
16362
16363 // Extend the pending selection so that it surrounds another selection
16364 _ = leader.update(cx, |leader, window, cx| {
16365 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16366 });
16367 follower
16368 .update(cx, |follower, window, cx| {
16369 follower.apply_update_proto(
16370 &project,
16371 pending_update.borrow_mut().take().unwrap(),
16372 window,
16373 cx,
16374 )
16375 })
16376 .unwrap()
16377 .await
16378 .unwrap();
16379 _ = follower.update(cx, |follower, _, cx| {
16380 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16381 });
16382
16383 // Scrolling locally breaks the follow
16384 _ = follower.update(cx, |follower, window, cx| {
16385 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16386 follower.set_scroll_anchor(
16387 ScrollAnchor {
16388 anchor: top_anchor,
16389 offset: gpui::Point::new(0.0, 0.5),
16390 },
16391 window,
16392 cx,
16393 );
16394 });
16395 assert!(!(*is_still_following.borrow()));
16396}
16397
16398#[gpui::test]
16399async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16400 init_test(cx, |_| {});
16401
16402 let fs = FakeFs::new(cx.executor());
16403 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16404 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16405 let pane = workspace
16406 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16407 .unwrap();
16408
16409 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16410
16411 let leader = pane.update_in(cx, |_, window, cx| {
16412 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16413 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16414 });
16415
16416 // Start following the editor when it has no excerpts.
16417 let mut state_message =
16418 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16419 let workspace_entity = workspace.root(cx).unwrap();
16420 let follower_1 = cx
16421 .update_window(*workspace.deref(), |_, window, cx| {
16422 Editor::from_state_proto(
16423 workspace_entity,
16424 ViewId {
16425 creator: CollaboratorId::PeerId(PeerId::default()),
16426 id: 0,
16427 },
16428 &mut state_message,
16429 window,
16430 cx,
16431 )
16432 })
16433 .unwrap()
16434 .unwrap()
16435 .await
16436 .unwrap();
16437
16438 let update_message = Rc::new(RefCell::new(None));
16439 follower_1.update_in(cx, {
16440 let update = update_message.clone();
16441 |_, window, cx| {
16442 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16443 leader.read(cx).add_event_to_update_proto(
16444 event,
16445 &mut update.borrow_mut(),
16446 window,
16447 cx,
16448 );
16449 })
16450 .detach();
16451 }
16452 });
16453
16454 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16455 (
16456 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16457 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16458 )
16459 });
16460
16461 // Insert some excerpts.
16462 leader.update(cx, |leader, cx| {
16463 leader.buffer.update(cx, |multibuffer, cx| {
16464 multibuffer.set_excerpts_for_path(
16465 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16466 buffer_1.clone(),
16467 vec![
16468 Point::row_range(0..3),
16469 Point::row_range(1..6),
16470 Point::row_range(12..15),
16471 ],
16472 0,
16473 cx,
16474 );
16475 multibuffer.set_excerpts_for_path(
16476 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16477 buffer_2.clone(),
16478 vec![Point::row_range(0..6), Point::row_range(8..12)],
16479 0,
16480 cx,
16481 );
16482 });
16483 });
16484
16485 // Apply the update of adding the excerpts.
16486 follower_1
16487 .update_in(cx, |follower, window, cx| {
16488 follower.apply_update_proto(
16489 &project,
16490 update_message.borrow().clone().unwrap(),
16491 window,
16492 cx,
16493 )
16494 })
16495 .await
16496 .unwrap();
16497 assert_eq!(
16498 follower_1.update(cx, |editor, cx| editor.text(cx)),
16499 leader.update(cx, |editor, cx| editor.text(cx))
16500 );
16501 update_message.borrow_mut().take();
16502
16503 // Start following separately after it already has excerpts.
16504 let mut state_message =
16505 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16506 let workspace_entity = workspace.root(cx).unwrap();
16507 let follower_2 = cx
16508 .update_window(*workspace.deref(), |_, window, cx| {
16509 Editor::from_state_proto(
16510 workspace_entity,
16511 ViewId {
16512 creator: CollaboratorId::PeerId(PeerId::default()),
16513 id: 0,
16514 },
16515 &mut state_message,
16516 window,
16517 cx,
16518 )
16519 })
16520 .unwrap()
16521 .unwrap()
16522 .await
16523 .unwrap();
16524 assert_eq!(
16525 follower_2.update(cx, |editor, cx| editor.text(cx)),
16526 leader.update(cx, |editor, cx| editor.text(cx))
16527 );
16528
16529 // Remove some excerpts.
16530 leader.update(cx, |leader, cx| {
16531 leader.buffer.update(cx, |multibuffer, cx| {
16532 let excerpt_ids = multibuffer.excerpt_ids();
16533 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16534 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16535 });
16536 });
16537
16538 // Apply the update of removing the excerpts.
16539 follower_1
16540 .update_in(cx, |follower, window, cx| {
16541 follower.apply_update_proto(
16542 &project,
16543 update_message.borrow().clone().unwrap(),
16544 window,
16545 cx,
16546 )
16547 })
16548 .await
16549 .unwrap();
16550 follower_2
16551 .update_in(cx, |follower, window, cx| {
16552 follower.apply_update_proto(
16553 &project,
16554 update_message.borrow().clone().unwrap(),
16555 window,
16556 cx,
16557 )
16558 })
16559 .await
16560 .unwrap();
16561 update_message.borrow_mut().take();
16562 assert_eq!(
16563 follower_1.update(cx, |editor, cx| editor.text(cx)),
16564 leader.update(cx, |editor, cx| editor.text(cx))
16565 );
16566}
16567
16568#[gpui::test]
16569async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16570 init_test(cx, |_| {});
16571
16572 let mut cx = EditorTestContext::new(cx).await;
16573 let lsp_store =
16574 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16575
16576 cx.set_state(indoc! {"
16577 ˇfn func(abc def: i32) -> u32 {
16578 }
16579 "});
16580
16581 cx.update(|_, cx| {
16582 lsp_store.update(cx, |lsp_store, cx| {
16583 lsp_store
16584 .update_diagnostics(
16585 LanguageServerId(0),
16586 lsp::PublishDiagnosticsParams {
16587 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16588 version: None,
16589 diagnostics: vec![
16590 lsp::Diagnostic {
16591 range: lsp::Range::new(
16592 lsp::Position::new(0, 11),
16593 lsp::Position::new(0, 12),
16594 ),
16595 severity: Some(lsp::DiagnosticSeverity::ERROR),
16596 ..Default::default()
16597 },
16598 lsp::Diagnostic {
16599 range: lsp::Range::new(
16600 lsp::Position::new(0, 12),
16601 lsp::Position::new(0, 15),
16602 ),
16603 severity: Some(lsp::DiagnosticSeverity::ERROR),
16604 ..Default::default()
16605 },
16606 lsp::Diagnostic {
16607 range: lsp::Range::new(
16608 lsp::Position::new(0, 25),
16609 lsp::Position::new(0, 28),
16610 ),
16611 severity: Some(lsp::DiagnosticSeverity::ERROR),
16612 ..Default::default()
16613 },
16614 ],
16615 },
16616 None,
16617 DiagnosticSourceKind::Pushed,
16618 &[],
16619 cx,
16620 )
16621 .unwrap()
16622 });
16623 });
16624
16625 executor.run_until_parked();
16626
16627 cx.update_editor(|editor, window, cx| {
16628 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16629 });
16630
16631 cx.assert_editor_state(indoc! {"
16632 fn func(abc def: i32) -> ˇu32 {
16633 }
16634 "});
16635
16636 cx.update_editor(|editor, window, cx| {
16637 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16638 });
16639
16640 cx.assert_editor_state(indoc! {"
16641 fn func(abc ˇdef: i32) -> u32 {
16642 }
16643 "});
16644
16645 cx.update_editor(|editor, window, cx| {
16646 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16647 });
16648
16649 cx.assert_editor_state(indoc! {"
16650 fn func(abcˇ def: i32) -> u32 {
16651 }
16652 "});
16653
16654 cx.update_editor(|editor, window, cx| {
16655 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16656 });
16657
16658 cx.assert_editor_state(indoc! {"
16659 fn func(abc def: i32) -> ˇu32 {
16660 }
16661 "});
16662}
16663
16664#[gpui::test]
16665async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16666 init_test(cx, |_| {});
16667
16668 let mut cx = EditorTestContext::new(cx).await;
16669
16670 let diff_base = r#"
16671 use some::mod;
16672
16673 const A: u32 = 42;
16674
16675 fn main() {
16676 println!("hello");
16677
16678 println!("world");
16679 }
16680 "#
16681 .unindent();
16682
16683 // Edits are modified, removed, modified, added
16684 cx.set_state(
16685 &r#"
16686 use some::modified;
16687
16688 ˇ
16689 fn main() {
16690 println!("hello there");
16691
16692 println!("around the");
16693 println!("world");
16694 }
16695 "#
16696 .unindent(),
16697 );
16698
16699 cx.set_head_text(&diff_base);
16700 executor.run_until_parked();
16701
16702 cx.update_editor(|editor, window, cx| {
16703 //Wrap around the bottom of the buffer
16704 for _ in 0..3 {
16705 editor.go_to_next_hunk(&GoToHunk, window, cx);
16706 }
16707 });
16708
16709 cx.assert_editor_state(
16710 &r#"
16711 ˇuse some::modified;
16712
16713
16714 fn main() {
16715 println!("hello there");
16716
16717 println!("around the");
16718 println!("world");
16719 }
16720 "#
16721 .unindent(),
16722 );
16723
16724 cx.update_editor(|editor, window, cx| {
16725 //Wrap around the top of the buffer
16726 for _ in 0..2 {
16727 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16728 }
16729 });
16730
16731 cx.assert_editor_state(
16732 &r#"
16733 use some::modified;
16734
16735
16736 fn main() {
16737 ˇ println!("hello there");
16738
16739 println!("around the");
16740 println!("world");
16741 }
16742 "#
16743 .unindent(),
16744 );
16745
16746 cx.update_editor(|editor, window, cx| {
16747 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16748 });
16749
16750 cx.assert_editor_state(
16751 &r#"
16752 use some::modified;
16753
16754 ˇ
16755 fn main() {
16756 println!("hello there");
16757
16758 println!("around the");
16759 println!("world");
16760 }
16761 "#
16762 .unindent(),
16763 );
16764
16765 cx.update_editor(|editor, window, cx| {
16766 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16767 });
16768
16769 cx.assert_editor_state(
16770 &r#"
16771 ˇuse some::modified;
16772
16773
16774 fn main() {
16775 println!("hello there");
16776
16777 println!("around the");
16778 println!("world");
16779 }
16780 "#
16781 .unindent(),
16782 );
16783
16784 cx.update_editor(|editor, window, cx| {
16785 for _ in 0..2 {
16786 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16787 }
16788 });
16789
16790 cx.assert_editor_state(
16791 &r#"
16792 use some::modified;
16793
16794
16795 fn main() {
16796 ˇ println!("hello there");
16797
16798 println!("around the");
16799 println!("world");
16800 }
16801 "#
16802 .unindent(),
16803 );
16804
16805 cx.update_editor(|editor, window, cx| {
16806 editor.fold(&Fold, window, cx);
16807 });
16808
16809 cx.update_editor(|editor, window, cx| {
16810 editor.go_to_next_hunk(&GoToHunk, window, cx);
16811 });
16812
16813 cx.assert_editor_state(
16814 &r#"
16815 ˇuse some::modified;
16816
16817
16818 fn main() {
16819 println!("hello there");
16820
16821 println!("around the");
16822 println!("world");
16823 }
16824 "#
16825 .unindent(),
16826 );
16827}
16828
16829#[test]
16830fn test_split_words() {
16831 fn split(text: &str) -> Vec<&str> {
16832 split_words(text).collect()
16833 }
16834
16835 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16836 assert_eq!(split("hello_world"), &["hello_", "world"]);
16837 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16838 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16839 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16840 assert_eq!(split("helloworld"), &["helloworld"]);
16841
16842 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16843}
16844
16845#[gpui::test]
16846async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16847 init_test(cx, |_| {});
16848
16849 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16850 let mut assert = |before, after| {
16851 let _state_context = cx.set_state(before);
16852 cx.run_until_parked();
16853 cx.update_editor(|editor, window, cx| {
16854 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16855 });
16856 cx.run_until_parked();
16857 cx.assert_editor_state(after);
16858 };
16859
16860 // Outside bracket jumps to outside of matching bracket
16861 assert("console.logˇ(var);", "console.log(var)ˇ;");
16862 assert("console.log(var)ˇ;", "console.logˇ(var);");
16863
16864 // Inside bracket jumps to inside of matching bracket
16865 assert("console.log(ˇvar);", "console.log(varˇ);");
16866 assert("console.log(varˇ);", "console.log(ˇvar);");
16867
16868 // When outside a bracket and inside, favor jumping to the inside bracket
16869 assert(
16870 "console.log('foo', [1, 2, 3]ˇ);",
16871 "console.log(ˇ'foo', [1, 2, 3]);",
16872 );
16873 assert(
16874 "console.log(ˇ'foo', [1, 2, 3]);",
16875 "console.log('foo', [1, 2, 3]ˇ);",
16876 );
16877
16878 // Bias forward if two options are equally likely
16879 assert(
16880 "let result = curried_fun()ˇ();",
16881 "let result = curried_fun()()ˇ;",
16882 );
16883
16884 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16885 assert(
16886 indoc! {"
16887 function test() {
16888 console.log('test')ˇ
16889 }"},
16890 indoc! {"
16891 function test() {
16892 console.logˇ('test')
16893 }"},
16894 );
16895}
16896
16897#[gpui::test]
16898async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16899 init_test(cx, |_| {});
16900
16901 let fs = FakeFs::new(cx.executor());
16902 fs.insert_tree(
16903 path!("/a"),
16904 json!({
16905 "main.rs": "fn main() { let a = 5; }",
16906 "other.rs": "// Test file",
16907 }),
16908 )
16909 .await;
16910 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16911
16912 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16913 language_registry.add(Arc::new(Language::new(
16914 LanguageConfig {
16915 name: "Rust".into(),
16916 matcher: LanguageMatcher {
16917 path_suffixes: vec!["rs".to_string()],
16918 ..Default::default()
16919 },
16920 brackets: BracketPairConfig {
16921 pairs: vec![BracketPair {
16922 start: "{".to_string(),
16923 end: "}".to_string(),
16924 close: true,
16925 surround: true,
16926 newline: true,
16927 }],
16928 disabled_scopes_by_bracket_ix: Vec::new(),
16929 },
16930 ..Default::default()
16931 },
16932 Some(tree_sitter_rust::LANGUAGE.into()),
16933 )));
16934 let mut fake_servers = language_registry.register_fake_lsp(
16935 "Rust",
16936 FakeLspAdapter {
16937 capabilities: lsp::ServerCapabilities {
16938 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16939 first_trigger_character: "{".to_string(),
16940 more_trigger_character: None,
16941 }),
16942 ..Default::default()
16943 },
16944 ..Default::default()
16945 },
16946 );
16947
16948 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16949
16950 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16951
16952 let worktree_id = workspace
16953 .update(cx, |workspace, _, cx| {
16954 workspace.project().update(cx, |project, cx| {
16955 project.worktrees(cx).next().unwrap().read(cx).id()
16956 })
16957 })
16958 .unwrap();
16959
16960 let buffer = project
16961 .update(cx, |project, cx| {
16962 project.open_local_buffer(path!("/a/main.rs"), cx)
16963 })
16964 .await
16965 .unwrap();
16966 let editor_handle = workspace
16967 .update(cx, |workspace, window, cx| {
16968 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16969 })
16970 .unwrap()
16971 .await
16972 .unwrap()
16973 .downcast::<Editor>()
16974 .unwrap();
16975
16976 cx.executor().start_waiting();
16977 let fake_server = fake_servers.next().await.unwrap();
16978
16979 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16980 |params, _| async move {
16981 assert_eq!(
16982 params.text_document_position.text_document.uri,
16983 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16984 );
16985 assert_eq!(
16986 params.text_document_position.position,
16987 lsp::Position::new(0, 21),
16988 );
16989
16990 Ok(Some(vec![lsp::TextEdit {
16991 new_text: "]".to_string(),
16992 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16993 }]))
16994 },
16995 );
16996
16997 editor_handle.update_in(cx, |editor, window, cx| {
16998 window.focus(&editor.focus_handle(cx));
16999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17000 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17001 });
17002 editor.handle_input("{", window, cx);
17003 });
17004
17005 cx.executor().run_until_parked();
17006
17007 buffer.update(cx, |buffer, _| {
17008 assert_eq!(
17009 buffer.text(),
17010 "fn main() { let a = {5}; }",
17011 "No extra braces from on type formatting should appear in the buffer"
17012 )
17013 });
17014}
17015
17016#[gpui::test(iterations = 20, seeds(31))]
17017async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17018 init_test(cx, |_| {});
17019
17020 let mut cx = EditorLspTestContext::new_rust(
17021 lsp::ServerCapabilities {
17022 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17023 first_trigger_character: ".".to_string(),
17024 more_trigger_character: None,
17025 }),
17026 ..Default::default()
17027 },
17028 cx,
17029 )
17030 .await;
17031
17032 cx.update_buffer(|buffer, _| {
17033 // This causes autoindent to be async.
17034 buffer.set_sync_parse_timeout(Duration::ZERO)
17035 });
17036
17037 cx.set_state("fn c() {\n d()ˇ\n}\n");
17038 cx.simulate_keystroke("\n");
17039 cx.run_until_parked();
17040
17041 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17042 let mut request =
17043 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17044 let buffer_cloned = buffer_cloned.clone();
17045 async move {
17046 buffer_cloned.update(&mut cx, |buffer, _| {
17047 assert_eq!(
17048 buffer.text(),
17049 "fn c() {\n d()\n .\n}\n",
17050 "OnTypeFormatting should triggered after autoindent applied"
17051 )
17052 })?;
17053
17054 Ok(Some(vec![]))
17055 }
17056 });
17057
17058 cx.simulate_keystroke(".");
17059 cx.run_until_parked();
17060
17061 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17062 assert!(request.next().await.is_some());
17063 request.close();
17064 assert!(request.next().await.is_none());
17065}
17066
17067#[gpui::test]
17068async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17069 init_test(cx, |_| {});
17070
17071 let fs = FakeFs::new(cx.executor());
17072 fs.insert_tree(
17073 path!("/a"),
17074 json!({
17075 "main.rs": "fn main() { let a = 5; }",
17076 "other.rs": "// Test file",
17077 }),
17078 )
17079 .await;
17080
17081 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17082
17083 let server_restarts = Arc::new(AtomicUsize::new(0));
17084 let closure_restarts = Arc::clone(&server_restarts);
17085 let language_server_name = "test language server";
17086 let language_name: LanguageName = "Rust".into();
17087
17088 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17089 language_registry.add(Arc::new(Language::new(
17090 LanguageConfig {
17091 name: language_name.clone(),
17092 matcher: LanguageMatcher {
17093 path_suffixes: vec!["rs".to_string()],
17094 ..Default::default()
17095 },
17096 ..Default::default()
17097 },
17098 Some(tree_sitter_rust::LANGUAGE.into()),
17099 )));
17100 let mut fake_servers = language_registry.register_fake_lsp(
17101 "Rust",
17102 FakeLspAdapter {
17103 name: language_server_name,
17104 initialization_options: Some(json!({
17105 "testOptionValue": true
17106 })),
17107 initializer: Some(Box::new(move |fake_server| {
17108 let task_restarts = Arc::clone(&closure_restarts);
17109 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17110 task_restarts.fetch_add(1, atomic::Ordering::Release);
17111 futures::future::ready(Ok(()))
17112 });
17113 })),
17114 ..Default::default()
17115 },
17116 );
17117
17118 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17119 let _buffer = project
17120 .update(cx, |project, cx| {
17121 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17122 })
17123 .await
17124 .unwrap();
17125 let _fake_server = fake_servers.next().await.unwrap();
17126 update_test_language_settings(cx, |language_settings| {
17127 language_settings.languages.0.insert(
17128 language_name.clone(),
17129 LanguageSettingsContent {
17130 tab_size: NonZeroU32::new(8),
17131 ..Default::default()
17132 },
17133 );
17134 });
17135 cx.executor().run_until_parked();
17136 assert_eq!(
17137 server_restarts.load(atomic::Ordering::Acquire),
17138 0,
17139 "Should not restart LSP server on an unrelated change"
17140 );
17141
17142 update_test_project_settings(cx, |project_settings| {
17143 project_settings.lsp.insert(
17144 "Some other server name".into(),
17145 LspSettings {
17146 binary: None,
17147 settings: None,
17148 initialization_options: Some(json!({
17149 "some other init value": false
17150 })),
17151 enable_lsp_tasks: false,
17152 fetch: None,
17153 },
17154 );
17155 });
17156 cx.executor().run_until_parked();
17157 assert_eq!(
17158 server_restarts.load(atomic::Ordering::Acquire),
17159 0,
17160 "Should not restart LSP server on an unrelated LSP settings change"
17161 );
17162
17163 update_test_project_settings(cx, |project_settings| {
17164 project_settings.lsp.insert(
17165 language_server_name.into(),
17166 LspSettings {
17167 binary: None,
17168 settings: None,
17169 initialization_options: Some(json!({
17170 "anotherInitValue": false
17171 })),
17172 enable_lsp_tasks: false,
17173 fetch: None,
17174 },
17175 );
17176 });
17177 cx.executor().run_until_parked();
17178 assert_eq!(
17179 server_restarts.load(atomic::Ordering::Acquire),
17180 1,
17181 "Should restart LSP server on a related LSP settings change"
17182 );
17183
17184 update_test_project_settings(cx, |project_settings| {
17185 project_settings.lsp.insert(
17186 language_server_name.into(),
17187 LspSettings {
17188 binary: None,
17189 settings: None,
17190 initialization_options: Some(json!({
17191 "anotherInitValue": false
17192 })),
17193 enable_lsp_tasks: false,
17194 fetch: None,
17195 },
17196 );
17197 });
17198 cx.executor().run_until_parked();
17199 assert_eq!(
17200 server_restarts.load(atomic::Ordering::Acquire),
17201 1,
17202 "Should not restart LSP server on a related LSP settings change that is the same"
17203 );
17204
17205 update_test_project_settings(cx, |project_settings| {
17206 project_settings.lsp.insert(
17207 language_server_name.into(),
17208 LspSettings {
17209 binary: None,
17210 settings: None,
17211 initialization_options: None,
17212 enable_lsp_tasks: false,
17213 fetch: None,
17214 },
17215 );
17216 });
17217 cx.executor().run_until_parked();
17218 assert_eq!(
17219 server_restarts.load(atomic::Ordering::Acquire),
17220 2,
17221 "Should restart LSP server on another related LSP settings change"
17222 );
17223}
17224
17225#[gpui::test]
17226async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17227 init_test(cx, |_| {});
17228
17229 let mut cx = EditorLspTestContext::new_rust(
17230 lsp::ServerCapabilities {
17231 completion_provider: Some(lsp::CompletionOptions {
17232 trigger_characters: Some(vec![".".to_string()]),
17233 resolve_provider: Some(true),
17234 ..Default::default()
17235 }),
17236 ..Default::default()
17237 },
17238 cx,
17239 )
17240 .await;
17241
17242 cx.set_state("fn main() { let a = 2ˇ; }");
17243 cx.simulate_keystroke(".");
17244 let completion_item = lsp::CompletionItem {
17245 label: "some".into(),
17246 kind: Some(lsp::CompletionItemKind::SNIPPET),
17247 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17248 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17249 kind: lsp::MarkupKind::Markdown,
17250 value: "```rust\nSome(2)\n```".to_string(),
17251 })),
17252 deprecated: Some(false),
17253 sort_text: Some("fffffff2".to_string()),
17254 filter_text: Some("some".to_string()),
17255 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17256 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17257 range: lsp::Range {
17258 start: lsp::Position {
17259 line: 0,
17260 character: 22,
17261 },
17262 end: lsp::Position {
17263 line: 0,
17264 character: 22,
17265 },
17266 },
17267 new_text: "Some(2)".to_string(),
17268 })),
17269 additional_text_edits: Some(vec![lsp::TextEdit {
17270 range: lsp::Range {
17271 start: lsp::Position {
17272 line: 0,
17273 character: 20,
17274 },
17275 end: lsp::Position {
17276 line: 0,
17277 character: 22,
17278 },
17279 },
17280 new_text: "".to_string(),
17281 }]),
17282 ..Default::default()
17283 };
17284
17285 let closure_completion_item = completion_item.clone();
17286 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17287 let task_completion_item = closure_completion_item.clone();
17288 async move {
17289 Ok(Some(lsp::CompletionResponse::Array(vec![
17290 task_completion_item,
17291 ])))
17292 }
17293 });
17294
17295 request.next().await;
17296
17297 cx.condition(|editor, _| editor.context_menu_visible())
17298 .await;
17299 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17300 editor
17301 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17302 .unwrap()
17303 });
17304 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17305
17306 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17307 let task_completion_item = completion_item.clone();
17308 async move { Ok(task_completion_item) }
17309 })
17310 .next()
17311 .await
17312 .unwrap();
17313 apply_additional_edits.await.unwrap();
17314 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17315}
17316
17317#[gpui::test]
17318async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17319 init_test(cx, |_| {});
17320
17321 let mut cx = EditorLspTestContext::new_rust(
17322 lsp::ServerCapabilities {
17323 completion_provider: Some(lsp::CompletionOptions {
17324 trigger_characters: Some(vec![".".to_string()]),
17325 resolve_provider: Some(true),
17326 ..Default::default()
17327 }),
17328 ..Default::default()
17329 },
17330 cx,
17331 )
17332 .await;
17333
17334 cx.set_state("fn main() { let a = 2ˇ; }");
17335 cx.simulate_keystroke(".");
17336
17337 let item1 = lsp::CompletionItem {
17338 label: "method id()".to_string(),
17339 filter_text: Some("id".to_string()),
17340 detail: None,
17341 documentation: None,
17342 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17343 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17344 new_text: ".id".to_string(),
17345 })),
17346 ..lsp::CompletionItem::default()
17347 };
17348
17349 let item2 = lsp::CompletionItem {
17350 label: "other".to_string(),
17351 filter_text: Some("other".to_string()),
17352 detail: None,
17353 documentation: None,
17354 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17355 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17356 new_text: ".other".to_string(),
17357 })),
17358 ..lsp::CompletionItem::default()
17359 };
17360
17361 let item1 = item1.clone();
17362 cx.set_request_handler::<lsp::request::Completion, _, _>({
17363 let item1 = item1.clone();
17364 move |_, _, _| {
17365 let item1 = item1.clone();
17366 let item2 = item2.clone();
17367 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17368 }
17369 })
17370 .next()
17371 .await;
17372
17373 cx.condition(|editor, _| editor.context_menu_visible())
17374 .await;
17375 cx.update_editor(|editor, _, _| {
17376 let context_menu = editor.context_menu.borrow_mut();
17377 let context_menu = context_menu
17378 .as_ref()
17379 .expect("Should have the context menu deployed");
17380 match context_menu {
17381 CodeContextMenu::Completions(completions_menu) => {
17382 let completions = completions_menu.completions.borrow_mut();
17383 assert_eq!(
17384 completions
17385 .iter()
17386 .map(|completion| &completion.label.text)
17387 .collect::<Vec<_>>(),
17388 vec!["method id()", "other"]
17389 )
17390 }
17391 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17392 }
17393 });
17394
17395 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17396 let item1 = item1.clone();
17397 move |_, item_to_resolve, _| {
17398 let item1 = item1.clone();
17399 async move {
17400 if item1 == item_to_resolve {
17401 Ok(lsp::CompletionItem {
17402 label: "method id()".to_string(),
17403 filter_text: Some("id".to_string()),
17404 detail: Some("Now resolved!".to_string()),
17405 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17406 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17407 range: lsp::Range::new(
17408 lsp::Position::new(0, 22),
17409 lsp::Position::new(0, 22),
17410 ),
17411 new_text: ".id".to_string(),
17412 })),
17413 ..lsp::CompletionItem::default()
17414 })
17415 } else {
17416 Ok(item_to_resolve)
17417 }
17418 }
17419 }
17420 })
17421 .next()
17422 .await
17423 .unwrap();
17424 cx.run_until_parked();
17425
17426 cx.update_editor(|editor, window, cx| {
17427 editor.context_menu_next(&Default::default(), window, cx);
17428 });
17429
17430 cx.update_editor(|editor, _, _| {
17431 let context_menu = editor.context_menu.borrow_mut();
17432 let context_menu = context_menu
17433 .as_ref()
17434 .expect("Should have the context menu deployed");
17435 match context_menu {
17436 CodeContextMenu::Completions(completions_menu) => {
17437 let completions = completions_menu.completions.borrow_mut();
17438 assert_eq!(
17439 completions
17440 .iter()
17441 .map(|completion| &completion.label.text)
17442 .collect::<Vec<_>>(),
17443 vec!["method id() Now resolved!", "other"],
17444 "Should update first completion label, but not second as the filter text did not match."
17445 );
17446 }
17447 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17448 }
17449 });
17450}
17451
17452#[gpui::test]
17453async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17454 init_test(cx, |_| {});
17455 let mut cx = EditorLspTestContext::new_rust(
17456 lsp::ServerCapabilities {
17457 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17458 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17459 completion_provider: Some(lsp::CompletionOptions {
17460 resolve_provider: Some(true),
17461 ..Default::default()
17462 }),
17463 ..Default::default()
17464 },
17465 cx,
17466 )
17467 .await;
17468 cx.set_state(indoc! {"
17469 struct TestStruct {
17470 field: i32
17471 }
17472
17473 fn mainˇ() {
17474 let unused_var = 42;
17475 let test_struct = TestStruct { field: 42 };
17476 }
17477 "});
17478 let symbol_range = cx.lsp_range(indoc! {"
17479 struct TestStruct {
17480 field: i32
17481 }
17482
17483 «fn main»() {
17484 let unused_var = 42;
17485 let test_struct = TestStruct { field: 42 };
17486 }
17487 "});
17488 let mut hover_requests =
17489 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17490 Ok(Some(lsp::Hover {
17491 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17492 kind: lsp::MarkupKind::Markdown,
17493 value: "Function documentation".to_string(),
17494 }),
17495 range: Some(symbol_range),
17496 }))
17497 });
17498
17499 // Case 1: Test that code action menu hide hover popover
17500 cx.dispatch_action(Hover);
17501 hover_requests.next().await;
17502 cx.condition(|editor, _| editor.hover_state.visible()).await;
17503 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17504 move |_, _, _| async move {
17505 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17506 lsp::CodeAction {
17507 title: "Remove unused variable".to_string(),
17508 kind: Some(CodeActionKind::QUICKFIX),
17509 edit: Some(lsp::WorkspaceEdit {
17510 changes: Some(
17511 [(
17512 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17513 vec![lsp::TextEdit {
17514 range: lsp::Range::new(
17515 lsp::Position::new(5, 4),
17516 lsp::Position::new(5, 27),
17517 ),
17518 new_text: "".to_string(),
17519 }],
17520 )]
17521 .into_iter()
17522 .collect(),
17523 ),
17524 ..Default::default()
17525 }),
17526 ..Default::default()
17527 },
17528 )]))
17529 },
17530 );
17531 cx.update_editor(|editor, window, cx| {
17532 editor.toggle_code_actions(
17533 &ToggleCodeActions {
17534 deployed_from: None,
17535 quick_launch: false,
17536 },
17537 window,
17538 cx,
17539 );
17540 });
17541 code_action_requests.next().await;
17542 cx.run_until_parked();
17543 cx.condition(|editor, _| editor.context_menu_visible())
17544 .await;
17545 cx.update_editor(|editor, _, _| {
17546 assert!(
17547 !editor.hover_state.visible(),
17548 "Hover popover should be hidden when code action menu is shown"
17549 );
17550 // Hide code actions
17551 editor.context_menu.take();
17552 });
17553
17554 // Case 2: Test that code completions hide hover popover
17555 cx.dispatch_action(Hover);
17556 hover_requests.next().await;
17557 cx.condition(|editor, _| editor.hover_state.visible()).await;
17558 let counter = Arc::new(AtomicUsize::new(0));
17559 let mut completion_requests =
17560 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17561 let counter = counter.clone();
17562 async move {
17563 counter.fetch_add(1, atomic::Ordering::Release);
17564 Ok(Some(lsp::CompletionResponse::Array(vec![
17565 lsp::CompletionItem {
17566 label: "main".into(),
17567 kind: Some(lsp::CompletionItemKind::FUNCTION),
17568 detail: Some("() -> ()".to_string()),
17569 ..Default::default()
17570 },
17571 lsp::CompletionItem {
17572 label: "TestStruct".into(),
17573 kind: Some(lsp::CompletionItemKind::STRUCT),
17574 detail: Some("struct TestStruct".to_string()),
17575 ..Default::default()
17576 },
17577 ])))
17578 }
17579 });
17580 cx.update_editor(|editor, window, cx| {
17581 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17582 });
17583 completion_requests.next().await;
17584 cx.condition(|editor, _| editor.context_menu_visible())
17585 .await;
17586 cx.update_editor(|editor, _, _| {
17587 assert!(
17588 !editor.hover_state.visible(),
17589 "Hover popover should be hidden when completion menu is shown"
17590 );
17591 });
17592}
17593
17594#[gpui::test]
17595async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17596 init_test(cx, |_| {});
17597
17598 let mut cx = EditorLspTestContext::new_rust(
17599 lsp::ServerCapabilities {
17600 completion_provider: Some(lsp::CompletionOptions {
17601 trigger_characters: Some(vec![".".to_string()]),
17602 resolve_provider: Some(true),
17603 ..Default::default()
17604 }),
17605 ..Default::default()
17606 },
17607 cx,
17608 )
17609 .await;
17610
17611 cx.set_state("fn main() { let a = 2ˇ; }");
17612 cx.simulate_keystroke(".");
17613
17614 let unresolved_item_1 = lsp::CompletionItem {
17615 label: "id".to_string(),
17616 filter_text: Some("id".to_string()),
17617 detail: None,
17618 documentation: None,
17619 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17620 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17621 new_text: ".id".to_string(),
17622 })),
17623 ..lsp::CompletionItem::default()
17624 };
17625 let resolved_item_1 = lsp::CompletionItem {
17626 additional_text_edits: Some(vec![lsp::TextEdit {
17627 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17628 new_text: "!!".to_string(),
17629 }]),
17630 ..unresolved_item_1.clone()
17631 };
17632 let unresolved_item_2 = lsp::CompletionItem {
17633 label: "other".to_string(),
17634 filter_text: Some("other".to_string()),
17635 detail: None,
17636 documentation: None,
17637 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17638 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17639 new_text: ".other".to_string(),
17640 })),
17641 ..lsp::CompletionItem::default()
17642 };
17643 let resolved_item_2 = lsp::CompletionItem {
17644 additional_text_edits: Some(vec![lsp::TextEdit {
17645 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17646 new_text: "??".to_string(),
17647 }]),
17648 ..unresolved_item_2.clone()
17649 };
17650
17651 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17652 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17653 cx.lsp
17654 .server
17655 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17656 let unresolved_item_1 = unresolved_item_1.clone();
17657 let resolved_item_1 = resolved_item_1.clone();
17658 let unresolved_item_2 = unresolved_item_2.clone();
17659 let resolved_item_2 = resolved_item_2.clone();
17660 let resolve_requests_1 = resolve_requests_1.clone();
17661 let resolve_requests_2 = resolve_requests_2.clone();
17662 move |unresolved_request, _| {
17663 let unresolved_item_1 = unresolved_item_1.clone();
17664 let resolved_item_1 = resolved_item_1.clone();
17665 let unresolved_item_2 = unresolved_item_2.clone();
17666 let resolved_item_2 = resolved_item_2.clone();
17667 let resolve_requests_1 = resolve_requests_1.clone();
17668 let resolve_requests_2 = resolve_requests_2.clone();
17669 async move {
17670 if unresolved_request == unresolved_item_1 {
17671 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17672 Ok(resolved_item_1.clone())
17673 } else if unresolved_request == unresolved_item_2 {
17674 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17675 Ok(resolved_item_2.clone())
17676 } else {
17677 panic!("Unexpected completion item {unresolved_request:?}")
17678 }
17679 }
17680 }
17681 })
17682 .detach();
17683
17684 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17685 let unresolved_item_1 = unresolved_item_1.clone();
17686 let unresolved_item_2 = unresolved_item_2.clone();
17687 async move {
17688 Ok(Some(lsp::CompletionResponse::Array(vec![
17689 unresolved_item_1,
17690 unresolved_item_2,
17691 ])))
17692 }
17693 })
17694 .next()
17695 .await;
17696
17697 cx.condition(|editor, _| editor.context_menu_visible())
17698 .await;
17699 cx.update_editor(|editor, _, _| {
17700 let context_menu = editor.context_menu.borrow_mut();
17701 let context_menu = context_menu
17702 .as_ref()
17703 .expect("Should have the context menu deployed");
17704 match context_menu {
17705 CodeContextMenu::Completions(completions_menu) => {
17706 let completions = completions_menu.completions.borrow_mut();
17707 assert_eq!(
17708 completions
17709 .iter()
17710 .map(|completion| &completion.label.text)
17711 .collect::<Vec<_>>(),
17712 vec!["id", "other"]
17713 )
17714 }
17715 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17716 }
17717 });
17718 cx.run_until_parked();
17719
17720 cx.update_editor(|editor, window, cx| {
17721 editor.context_menu_next(&ContextMenuNext, window, cx);
17722 });
17723 cx.run_until_parked();
17724 cx.update_editor(|editor, window, cx| {
17725 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17726 });
17727 cx.run_until_parked();
17728 cx.update_editor(|editor, window, cx| {
17729 editor.context_menu_next(&ContextMenuNext, window, cx);
17730 });
17731 cx.run_until_parked();
17732 cx.update_editor(|editor, window, cx| {
17733 editor
17734 .compose_completion(&ComposeCompletion::default(), window, cx)
17735 .expect("No task returned")
17736 })
17737 .await
17738 .expect("Completion failed");
17739 cx.run_until_parked();
17740
17741 cx.update_editor(|editor, _, cx| {
17742 assert_eq!(
17743 resolve_requests_1.load(atomic::Ordering::Acquire),
17744 1,
17745 "Should always resolve once despite multiple selections"
17746 );
17747 assert_eq!(
17748 resolve_requests_2.load(atomic::Ordering::Acquire),
17749 1,
17750 "Should always resolve once after multiple selections and applying the completion"
17751 );
17752 assert_eq!(
17753 editor.text(cx),
17754 "fn main() { let a = ??.other; }",
17755 "Should use resolved data when applying the completion"
17756 );
17757 });
17758}
17759
17760#[gpui::test]
17761async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17762 init_test(cx, |_| {});
17763
17764 let item_0 = lsp::CompletionItem {
17765 label: "abs".into(),
17766 insert_text: Some("abs".into()),
17767 data: Some(json!({ "very": "special"})),
17768 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17769 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17770 lsp::InsertReplaceEdit {
17771 new_text: "abs".to_string(),
17772 insert: lsp::Range::default(),
17773 replace: lsp::Range::default(),
17774 },
17775 )),
17776 ..lsp::CompletionItem::default()
17777 };
17778 let items = iter::once(item_0.clone())
17779 .chain((11..51).map(|i| lsp::CompletionItem {
17780 label: format!("item_{}", i),
17781 insert_text: Some(format!("item_{}", i)),
17782 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17783 ..lsp::CompletionItem::default()
17784 }))
17785 .collect::<Vec<_>>();
17786
17787 let default_commit_characters = vec!["?".to_string()];
17788 let default_data = json!({ "default": "data"});
17789 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17790 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17791 let default_edit_range = lsp::Range {
17792 start: lsp::Position {
17793 line: 0,
17794 character: 5,
17795 },
17796 end: lsp::Position {
17797 line: 0,
17798 character: 5,
17799 },
17800 };
17801
17802 let mut cx = EditorLspTestContext::new_rust(
17803 lsp::ServerCapabilities {
17804 completion_provider: Some(lsp::CompletionOptions {
17805 trigger_characters: Some(vec![".".to_string()]),
17806 resolve_provider: Some(true),
17807 ..Default::default()
17808 }),
17809 ..Default::default()
17810 },
17811 cx,
17812 )
17813 .await;
17814
17815 cx.set_state("fn main() { let a = 2ˇ; }");
17816 cx.simulate_keystroke(".");
17817
17818 let completion_data = default_data.clone();
17819 let completion_characters = default_commit_characters.clone();
17820 let completion_items = items.clone();
17821 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17822 let default_data = completion_data.clone();
17823 let default_commit_characters = completion_characters.clone();
17824 let items = completion_items.clone();
17825 async move {
17826 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17827 items,
17828 item_defaults: Some(lsp::CompletionListItemDefaults {
17829 data: Some(default_data.clone()),
17830 commit_characters: Some(default_commit_characters.clone()),
17831 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17832 default_edit_range,
17833 )),
17834 insert_text_format: Some(default_insert_text_format),
17835 insert_text_mode: Some(default_insert_text_mode),
17836 }),
17837 ..lsp::CompletionList::default()
17838 })))
17839 }
17840 })
17841 .next()
17842 .await;
17843
17844 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17845 cx.lsp
17846 .server
17847 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17848 let closure_resolved_items = resolved_items.clone();
17849 move |item_to_resolve, _| {
17850 let closure_resolved_items = closure_resolved_items.clone();
17851 async move {
17852 closure_resolved_items.lock().push(item_to_resolve.clone());
17853 Ok(item_to_resolve)
17854 }
17855 }
17856 })
17857 .detach();
17858
17859 cx.condition(|editor, _| editor.context_menu_visible())
17860 .await;
17861 cx.run_until_parked();
17862 cx.update_editor(|editor, _, _| {
17863 let menu = editor.context_menu.borrow_mut();
17864 match menu.as_ref().expect("should have the completions menu") {
17865 CodeContextMenu::Completions(completions_menu) => {
17866 assert_eq!(
17867 completions_menu
17868 .entries
17869 .borrow()
17870 .iter()
17871 .map(|mat| mat.string.clone())
17872 .collect::<Vec<String>>(),
17873 items
17874 .iter()
17875 .map(|completion| completion.label.clone())
17876 .collect::<Vec<String>>()
17877 );
17878 }
17879 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17880 }
17881 });
17882 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17883 // with 4 from the end.
17884 assert_eq!(
17885 *resolved_items.lock(),
17886 [&items[0..16], &items[items.len() - 4..items.len()]]
17887 .concat()
17888 .iter()
17889 .cloned()
17890 .map(|mut item| {
17891 if item.data.is_none() {
17892 item.data = Some(default_data.clone());
17893 }
17894 item
17895 })
17896 .collect::<Vec<lsp::CompletionItem>>(),
17897 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17898 );
17899 resolved_items.lock().clear();
17900
17901 cx.update_editor(|editor, window, cx| {
17902 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17903 });
17904 cx.run_until_parked();
17905 // Completions that have already been resolved are skipped.
17906 assert_eq!(
17907 *resolved_items.lock(),
17908 items[items.len() - 17..items.len() - 4]
17909 .iter()
17910 .cloned()
17911 .map(|mut item| {
17912 if item.data.is_none() {
17913 item.data = Some(default_data.clone());
17914 }
17915 item
17916 })
17917 .collect::<Vec<lsp::CompletionItem>>()
17918 );
17919 resolved_items.lock().clear();
17920}
17921
17922#[gpui::test]
17923async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17924 init_test(cx, |_| {});
17925
17926 let mut cx = EditorLspTestContext::new(
17927 Language::new(
17928 LanguageConfig {
17929 matcher: LanguageMatcher {
17930 path_suffixes: vec!["jsx".into()],
17931 ..Default::default()
17932 },
17933 overrides: [(
17934 "element".into(),
17935 LanguageConfigOverride {
17936 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17937 ..Default::default()
17938 },
17939 )]
17940 .into_iter()
17941 .collect(),
17942 ..Default::default()
17943 },
17944 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17945 )
17946 .with_override_query("(jsx_self_closing_element) @element")
17947 .unwrap(),
17948 lsp::ServerCapabilities {
17949 completion_provider: Some(lsp::CompletionOptions {
17950 trigger_characters: Some(vec![":".to_string()]),
17951 ..Default::default()
17952 }),
17953 ..Default::default()
17954 },
17955 cx,
17956 )
17957 .await;
17958
17959 cx.lsp
17960 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17961 Ok(Some(lsp::CompletionResponse::Array(vec![
17962 lsp::CompletionItem {
17963 label: "bg-blue".into(),
17964 ..Default::default()
17965 },
17966 lsp::CompletionItem {
17967 label: "bg-red".into(),
17968 ..Default::default()
17969 },
17970 lsp::CompletionItem {
17971 label: "bg-yellow".into(),
17972 ..Default::default()
17973 },
17974 ])))
17975 });
17976
17977 cx.set_state(r#"<p class="bgˇ" />"#);
17978
17979 // Trigger completion when typing a dash, because the dash is an extra
17980 // word character in the 'element' scope, which contains the cursor.
17981 cx.simulate_keystroke("-");
17982 cx.executor().run_until_parked();
17983 cx.update_editor(|editor, _, _| {
17984 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17985 {
17986 assert_eq!(
17987 completion_menu_entries(menu),
17988 &["bg-blue", "bg-red", "bg-yellow"]
17989 );
17990 } else {
17991 panic!("expected completion menu to be open");
17992 }
17993 });
17994
17995 cx.simulate_keystroke("l");
17996 cx.executor().run_until_parked();
17997 cx.update_editor(|editor, _, _| {
17998 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17999 {
18000 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18001 } else {
18002 panic!("expected completion menu to be open");
18003 }
18004 });
18005
18006 // When filtering completions, consider the character after the '-' to
18007 // be the start of a subword.
18008 cx.set_state(r#"<p class="yelˇ" />"#);
18009 cx.simulate_keystroke("l");
18010 cx.executor().run_until_parked();
18011 cx.update_editor(|editor, _, _| {
18012 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18013 {
18014 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18015 } else {
18016 panic!("expected completion menu to be open");
18017 }
18018 });
18019}
18020
18021fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18022 let entries = menu.entries.borrow();
18023 entries.iter().map(|mat| mat.string.clone()).collect()
18024}
18025
18026#[gpui::test]
18027async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18028 init_test(cx, |settings| {
18029 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18030 Formatter::Prettier,
18031 )))
18032 });
18033
18034 let fs = FakeFs::new(cx.executor());
18035 fs.insert_file(path!("/file.ts"), Default::default()).await;
18036
18037 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18038 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18039
18040 language_registry.add(Arc::new(Language::new(
18041 LanguageConfig {
18042 name: "TypeScript".into(),
18043 matcher: LanguageMatcher {
18044 path_suffixes: vec!["ts".to_string()],
18045 ..Default::default()
18046 },
18047 ..Default::default()
18048 },
18049 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18050 )));
18051 update_test_language_settings(cx, |settings| {
18052 settings.defaults.prettier = Some(PrettierSettings {
18053 allowed: true,
18054 ..PrettierSettings::default()
18055 });
18056 });
18057
18058 let test_plugin = "test_plugin";
18059 let _ = language_registry.register_fake_lsp(
18060 "TypeScript",
18061 FakeLspAdapter {
18062 prettier_plugins: vec![test_plugin],
18063 ..Default::default()
18064 },
18065 );
18066
18067 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18068 let buffer = project
18069 .update(cx, |project, cx| {
18070 project.open_local_buffer(path!("/file.ts"), cx)
18071 })
18072 .await
18073 .unwrap();
18074
18075 let buffer_text = "one\ntwo\nthree\n";
18076 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18077 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18078 editor.update_in(cx, |editor, window, cx| {
18079 editor.set_text(buffer_text, window, cx)
18080 });
18081
18082 editor
18083 .update_in(cx, |editor, window, cx| {
18084 editor.perform_format(
18085 project.clone(),
18086 FormatTrigger::Manual,
18087 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18088 window,
18089 cx,
18090 )
18091 })
18092 .unwrap()
18093 .await;
18094 assert_eq!(
18095 editor.update(cx, |editor, cx| editor.text(cx)),
18096 buffer_text.to_string() + prettier_format_suffix,
18097 "Test prettier formatting was not applied to the original buffer text",
18098 );
18099
18100 update_test_language_settings(cx, |settings| {
18101 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18102 });
18103 let format = editor.update_in(cx, |editor, window, cx| {
18104 editor.perform_format(
18105 project.clone(),
18106 FormatTrigger::Manual,
18107 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18108 window,
18109 cx,
18110 )
18111 });
18112 format.await.unwrap();
18113 assert_eq!(
18114 editor.update(cx, |editor, cx| editor.text(cx)),
18115 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18116 "Autoformatting (via test prettier) was not applied to the original buffer text",
18117 );
18118}
18119
18120#[gpui::test]
18121async fn test_addition_reverts(cx: &mut TestAppContext) {
18122 init_test(cx, |_| {});
18123 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18124 let base_text = indoc! {r#"
18125 struct Row;
18126 struct Row1;
18127 struct Row2;
18128
18129 struct Row4;
18130 struct Row5;
18131 struct Row6;
18132
18133 struct Row8;
18134 struct Row9;
18135 struct Row10;"#};
18136
18137 // When addition hunks are not adjacent to carets, no hunk revert is performed
18138 assert_hunk_revert(
18139 indoc! {r#"struct Row;
18140 struct Row1;
18141 struct Row1.1;
18142 struct Row1.2;
18143 struct Row2;ˇ
18144
18145 struct Row4;
18146 struct Row5;
18147 struct Row6;
18148
18149 struct Row8;
18150 ˇstruct Row9;
18151 struct Row9.1;
18152 struct Row9.2;
18153 struct Row9.3;
18154 struct Row10;"#},
18155 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18156 indoc! {r#"struct Row;
18157 struct Row1;
18158 struct Row1.1;
18159 struct Row1.2;
18160 struct Row2;ˇ
18161
18162 struct Row4;
18163 struct Row5;
18164 struct Row6;
18165
18166 struct Row8;
18167 ˇstruct Row9;
18168 struct Row9.1;
18169 struct Row9.2;
18170 struct Row9.3;
18171 struct Row10;"#},
18172 base_text,
18173 &mut cx,
18174 );
18175 // Same for selections
18176 assert_hunk_revert(
18177 indoc! {r#"struct Row;
18178 struct Row1;
18179 struct Row2;
18180 struct Row2.1;
18181 struct Row2.2;
18182 «ˇ
18183 struct Row4;
18184 struct» Row5;
18185 «struct Row6;
18186 ˇ»
18187 struct Row9.1;
18188 struct Row9.2;
18189 struct Row9.3;
18190 struct Row8;
18191 struct Row9;
18192 struct Row10;"#},
18193 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18194 indoc! {r#"struct Row;
18195 struct Row1;
18196 struct Row2;
18197 struct Row2.1;
18198 struct Row2.2;
18199 «ˇ
18200 struct Row4;
18201 struct» Row5;
18202 «struct Row6;
18203 ˇ»
18204 struct Row9.1;
18205 struct Row9.2;
18206 struct Row9.3;
18207 struct Row8;
18208 struct Row9;
18209 struct Row10;"#},
18210 base_text,
18211 &mut cx,
18212 );
18213
18214 // When carets and selections intersect the addition hunks, those are reverted.
18215 // Adjacent carets got merged.
18216 assert_hunk_revert(
18217 indoc! {r#"struct Row;
18218 ˇ// something on the top
18219 struct Row1;
18220 struct Row2;
18221 struct Roˇw3.1;
18222 struct Row2.2;
18223 struct Row2.3;ˇ
18224
18225 struct Row4;
18226 struct ˇRow5.1;
18227 struct Row5.2;
18228 struct «Rowˇ»5.3;
18229 struct Row5;
18230 struct Row6;
18231 ˇ
18232 struct Row9.1;
18233 struct «Rowˇ»9.2;
18234 struct «ˇRow»9.3;
18235 struct Row8;
18236 struct Row9;
18237 «ˇ// something on bottom»
18238 struct Row10;"#},
18239 vec![
18240 DiffHunkStatusKind::Added,
18241 DiffHunkStatusKind::Added,
18242 DiffHunkStatusKind::Added,
18243 DiffHunkStatusKind::Added,
18244 DiffHunkStatusKind::Added,
18245 ],
18246 indoc! {r#"struct Row;
18247 ˇstruct Row1;
18248 struct Row2;
18249 ˇ
18250 struct Row4;
18251 ˇstruct Row5;
18252 struct Row6;
18253 ˇ
18254 ˇstruct Row8;
18255 struct Row9;
18256 ˇstruct Row10;"#},
18257 base_text,
18258 &mut cx,
18259 );
18260}
18261
18262#[gpui::test]
18263async fn test_modification_reverts(cx: &mut TestAppContext) {
18264 init_test(cx, |_| {});
18265 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18266 let base_text = indoc! {r#"
18267 struct Row;
18268 struct Row1;
18269 struct Row2;
18270
18271 struct Row4;
18272 struct Row5;
18273 struct Row6;
18274
18275 struct Row8;
18276 struct Row9;
18277 struct Row10;"#};
18278
18279 // Modification hunks behave the same as the addition ones.
18280 assert_hunk_revert(
18281 indoc! {r#"struct Row;
18282 struct Row1;
18283 struct Row33;
18284 ˇ
18285 struct Row4;
18286 struct Row5;
18287 struct Row6;
18288 ˇ
18289 struct Row99;
18290 struct Row9;
18291 struct Row10;"#},
18292 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18293 indoc! {r#"struct Row;
18294 struct Row1;
18295 struct Row33;
18296 ˇ
18297 struct Row4;
18298 struct Row5;
18299 struct Row6;
18300 ˇ
18301 struct Row99;
18302 struct Row9;
18303 struct Row10;"#},
18304 base_text,
18305 &mut cx,
18306 );
18307 assert_hunk_revert(
18308 indoc! {r#"struct Row;
18309 struct Row1;
18310 struct Row33;
18311 «ˇ
18312 struct Row4;
18313 struct» Row5;
18314 «struct Row6;
18315 ˇ»
18316 struct Row99;
18317 struct Row9;
18318 struct Row10;"#},
18319 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18320 indoc! {r#"struct Row;
18321 struct Row1;
18322 struct Row33;
18323 «ˇ
18324 struct Row4;
18325 struct» Row5;
18326 «struct Row6;
18327 ˇ»
18328 struct Row99;
18329 struct Row9;
18330 struct Row10;"#},
18331 base_text,
18332 &mut cx,
18333 );
18334
18335 assert_hunk_revert(
18336 indoc! {r#"ˇstruct Row1.1;
18337 struct Row1;
18338 «ˇstr»uct Row22;
18339
18340 struct ˇRow44;
18341 struct Row5;
18342 struct «Rˇ»ow66;ˇ
18343
18344 «struˇ»ct Row88;
18345 struct Row9;
18346 struct Row1011;ˇ"#},
18347 vec![
18348 DiffHunkStatusKind::Modified,
18349 DiffHunkStatusKind::Modified,
18350 DiffHunkStatusKind::Modified,
18351 DiffHunkStatusKind::Modified,
18352 DiffHunkStatusKind::Modified,
18353 DiffHunkStatusKind::Modified,
18354 ],
18355 indoc! {r#"struct Row;
18356 ˇstruct Row1;
18357 struct Row2;
18358 ˇ
18359 struct Row4;
18360 ˇstruct Row5;
18361 struct Row6;
18362 ˇ
18363 struct Row8;
18364 ˇstruct Row9;
18365 struct Row10;ˇ"#},
18366 base_text,
18367 &mut cx,
18368 );
18369}
18370
18371#[gpui::test]
18372async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18373 init_test(cx, |_| {});
18374 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18375 let base_text = indoc! {r#"
18376 one
18377
18378 two
18379 three
18380 "#};
18381
18382 cx.set_head_text(base_text);
18383 cx.set_state("\nˇ\n");
18384 cx.executor().run_until_parked();
18385 cx.update_editor(|editor, _window, cx| {
18386 editor.expand_selected_diff_hunks(cx);
18387 });
18388 cx.executor().run_until_parked();
18389 cx.update_editor(|editor, window, cx| {
18390 editor.backspace(&Default::default(), window, cx);
18391 });
18392 cx.run_until_parked();
18393 cx.assert_state_with_diff(
18394 indoc! {r#"
18395
18396 - two
18397 - threeˇ
18398 +
18399 "#}
18400 .to_string(),
18401 );
18402}
18403
18404#[gpui::test]
18405async fn test_deletion_reverts(cx: &mut TestAppContext) {
18406 init_test(cx, |_| {});
18407 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18408 let base_text = indoc! {r#"struct Row;
18409struct Row1;
18410struct Row2;
18411
18412struct Row4;
18413struct Row5;
18414struct Row6;
18415
18416struct Row8;
18417struct Row9;
18418struct Row10;"#};
18419
18420 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18421 assert_hunk_revert(
18422 indoc! {r#"struct Row;
18423 struct Row2;
18424
18425 ˇstruct Row4;
18426 struct Row5;
18427 struct Row6;
18428 ˇ
18429 struct Row8;
18430 struct Row10;"#},
18431 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18432 indoc! {r#"struct Row;
18433 struct Row2;
18434
18435 ˇstruct Row4;
18436 struct Row5;
18437 struct Row6;
18438 ˇ
18439 struct Row8;
18440 struct Row10;"#},
18441 base_text,
18442 &mut cx,
18443 );
18444 assert_hunk_revert(
18445 indoc! {r#"struct Row;
18446 struct Row2;
18447
18448 «ˇstruct Row4;
18449 struct» Row5;
18450 «struct Row6;
18451 ˇ»
18452 struct Row8;
18453 struct Row10;"#},
18454 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18455 indoc! {r#"struct Row;
18456 struct Row2;
18457
18458 «ˇstruct Row4;
18459 struct» Row5;
18460 «struct Row6;
18461 ˇ»
18462 struct Row8;
18463 struct Row10;"#},
18464 base_text,
18465 &mut cx,
18466 );
18467
18468 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18469 assert_hunk_revert(
18470 indoc! {r#"struct Row;
18471 ˇstruct Row2;
18472
18473 struct Row4;
18474 struct Row5;
18475 struct Row6;
18476
18477 struct Row8;ˇ
18478 struct Row10;"#},
18479 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18480 indoc! {r#"struct Row;
18481 struct Row1;
18482 ˇstruct Row2;
18483
18484 struct Row4;
18485 struct Row5;
18486 struct Row6;
18487
18488 struct Row8;ˇ
18489 struct Row9;
18490 struct Row10;"#},
18491 base_text,
18492 &mut cx,
18493 );
18494 assert_hunk_revert(
18495 indoc! {r#"struct Row;
18496 struct Row2«ˇ;
18497 struct Row4;
18498 struct» Row5;
18499 «struct Row6;
18500
18501 struct Row8;ˇ»
18502 struct Row10;"#},
18503 vec![
18504 DiffHunkStatusKind::Deleted,
18505 DiffHunkStatusKind::Deleted,
18506 DiffHunkStatusKind::Deleted,
18507 ],
18508 indoc! {r#"struct Row;
18509 struct Row1;
18510 struct Row2«ˇ;
18511
18512 struct Row4;
18513 struct» Row5;
18514 «struct Row6;
18515
18516 struct Row8;ˇ»
18517 struct Row9;
18518 struct Row10;"#},
18519 base_text,
18520 &mut cx,
18521 );
18522}
18523
18524#[gpui::test]
18525async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18526 init_test(cx, |_| {});
18527
18528 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18529 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18530 let base_text_3 =
18531 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18532
18533 let text_1 = edit_first_char_of_every_line(base_text_1);
18534 let text_2 = edit_first_char_of_every_line(base_text_2);
18535 let text_3 = edit_first_char_of_every_line(base_text_3);
18536
18537 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18538 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18539 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18540
18541 let multibuffer = cx.new(|cx| {
18542 let mut multibuffer = MultiBuffer::new(ReadWrite);
18543 multibuffer.push_excerpts(
18544 buffer_1.clone(),
18545 [
18546 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18547 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18548 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18549 ],
18550 cx,
18551 );
18552 multibuffer.push_excerpts(
18553 buffer_2.clone(),
18554 [
18555 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18556 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18557 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18558 ],
18559 cx,
18560 );
18561 multibuffer.push_excerpts(
18562 buffer_3.clone(),
18563 [
18564 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18565 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18566 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18567 ],
18568 cx,
18569 );
18570 multibuffer
18571 });
18572
18573 let fs = FakeFs::new(cx.executor());
18574 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18575 let (editor, cx) = cx
18576 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18577 editor.update_in(cx, |editor, _window, cx| {
18578 for (buffer, diff_base) in [
18579 (buffer_1.clone(), base_text_1),
18580 (buffer_2.clone(), base_text_2),
18581 (buffer_3.clone(), base_text_3),
18582 ] {
18583 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18584 editor
18585 .buffer
18586 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18587 }
18588 });
18589 cx.executor().run_until_parked();
18590
18591 editor.update_in(cx, |editor, window, cx| {
18592 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}");
18593 editor.select_all(&SelectAll, window, cx);
18594 editor.git_restore(&Default::default(), window, cx);
18595 });
18596 cx.executor().run_until_parked();
18597
18598 // When all ranges are selected, all buffer hunks are reverted.
18599 editor.update(cx, |editor, cx| {
18600 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");
18601 });
18602 buffer_1.update(cx, |buffer, _| {
18603 assert_eq!(buffer.text(), base_text_1);
18604 });
18605 buffer_2.update(cx, |buffer, _| {
18606 assert_eq!(buffer.text(), base_text_2);
18607 });
18608 buffer_3.update(cx, |buffer, _| {
18609 assert_eq!(buffer.text(), base_text_3);
18610 });
18611
18612 editor.update_in(cx, |editor, window, cx| {
18613 editor.undo(&Default::default(), window, cx);
18614 });
18615
18616 editor.update_in(cx, |editor, window, cx| {
18617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18618 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18619 });
18620 editor.git_restore(&Default::default(), window, cx);
18621 });
18622
18623 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18624 // but not affect buffer_2 and its related excerpts.
18625 editor.update(cx, |editor, cx| {
18626 assert_eq!(
18627 editor.text(cx),
18628 "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}"
18629 );
18630 });
18631 buffer_1.update(cx, |buffer, _| {
18632 assert_eq!(buffer.text(), base_text_1);
18633 });
18634 buffer_2.update(cx, |buffer, _| {
18635 assert_eq!(
18636 buffer.text(),
18637 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18638 );
18639 });
18640 buffer_3.update(cx, |buffer, _| {
18641 assert_eq!(
18642 buffer.text(),
18643 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18644 );
18645 });
18646
18647 fn edit_first_char_of_every_line(text: &str) -> String {
18648 text.split('\n')
18649 .map(|line| format!("X{}", &line[1..]))
18650 .collect::<Vec<_>>()
18651 .join("\n")
18652 }
18653}
18654
18655#[gpui::test]
18656async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18657 init_test(cx, |_| {});
18658
18659 let cols = 4;
18660 let rows = 10;
18661 let sample_text_1 = sample_text(rows, cols, 'a');
18662 assert_eq!(
18663 sample_text_1,
18664 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18665 );
18666 let sample_text_2 = sample_text(rows, cols, 'l');
18667 assert_eq!(
18668 sample_text_2,
18669 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18670 );
18671 let sample_text_3 = sample_text(rows, cols, 'v');
18672 assert_eq!(
18673 sample_text_3,
18674 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18675 );
18676
18677 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18678 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18679 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18680
18681 let multi_buffer = cx.new(|cx| {
18682 let mut multibuffer = MultiBuffer::new(ReadWrite);
18683 multibuffer.push_excerpts(
18684 buffer_1.clone(),
18685 [
18686 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18687 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18688 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18689 ],
18690 cx,
18691 );
18692 multibuffer.push_excerpts(
18693 buffer_2.clone(),
18694 [
18695 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18696 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18697 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18698 ],
18699 cx,
18700 );
18701 multibuffer.push_excerpts(
18702 buffer_3.clone(),
18703 [
18704 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18705 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18706 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18707 ],
18708 cx,
18709 );
18710 multibuffer
18711 });
18712
18713 let fs = FakeFs::new(cx.executor());
18714 fs.insert_tree(
18715 "/a",
18716 json!({
18717 "main.rs": sample_text_1,
18718 "other.rs": sample_text_2,
18719 "lib.rs": sample_text_3,
18720 }),
18721 )
18722 .await;
18723 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18725 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18726 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18727 Editor::new(
18728 EditorMode::full(),
18729 multi_buffer,
18730 Some(project.clone()),
18731 window,
18732 cx,
18733 )
18734 });
18735 let multibuffer_item_id = workspace
18736 .update(cx, |workspace, window, cx| {
18737 assert!(
18738 workspace.active_item(cx).is_none(),
18739 "active item should be None before the first item is added"
18740 );
18741 workspace.add_item_to_active_pane(
18742 Box::new(multi_buffer_editor.clone()),
18743 None,
18744 true,
18745 window,
18746 cx,
18747 );
18748 let active_item = workspace
18749 .active_item(cx)
18750 .expect("should have an active item after adding the multi buffer");
18751 assert!(
18752 !active_item.is_singleton(cx),
18753 "A multi buffer was expected to active after adding"
18754 );
18755 active_item.item_id()
18756 })
18757 .unwrap();
18758 cx.executor().run_until_parked();
18759
18760 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18761 editor.change_selections(
18762 SelectionEffects::scroll(Autoscroll::Next),
18763 window,
18764 cx,
18765 |s| s.select_ranges(Some(1..2)),
18766 );
18767 editor.open_excerpts(&OpenExcerpts, window, cx);
18768 });
18769 cx.executor().run_until_parked();
18770 let first_item_id = workspace
18771 .update(cx, |workspace, window, cx| {
18772 let active_item = workspace
18773 .active_item(cx)
18774 .expect("should have an active item after navigating into the 1st buffer");
18775 let first_item_id = active_item.item_id();
18776 assert_ne!(
18777 first_item_id, multibuffer_item_id,
18778 "Should navigate into the 1st buffer and activate it"
18779 );
18780 assert!(
18781 active_item.is_singleton(cx),
18782 "New active item should be a singleton buffer"
18783 );
18784 assert_eq!(
18785 active_item
18786 .act_as::<Editor>(cx)
18787 .expect("should have navigated into an editor for the 1st buffer")
18788 .read(cx)
18789 .text(cx),
18790 sample_text_1
18791 );
18792
18793 workspace
18794 .go_back(workspace.active_pane().downgrade(), window, cx)
18795 .detach_and_log_err(cx);
18796
18797 first_item_id
18798 })
18799 .unwrap();
18800 cx.executor().run_until_parked();
18801 workspace
18802 .update(cx, |workspace, _, cx| {
18803 let active_item = workspace
18804 .active_item(cx)
18805 .expect("should have an active item after navigating back");
18806 assert_eq!(
18807 active_item.item_id(),
18808 multibuffer_item_id,
18809 "Should navigate back to the multi buffer"
18810 );
18811 assert!(!active_item.is_singleton(cx));
18812 })
18813 .unwrap();
18814
18815 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18816 editor.change_selections(
18817 SelectionEffects::scroll(Autoscroll::Next),
18818 window,
18819 cx,
18820 |s| s.select_ranges(Some(39..40)),
18821 );
18822 editor.open_excerpts(&OpenExcerpts, window, cx);
18823 });
18824 cx.executor().run_until_parked();
18825 let second_item_id = workspace
18826 .update(cx, |workspace, window, cx| {
18827 let active_item = workspace
18828 .active_item(cx)
18829 .expect("should have an active item after navigating into the 2nd buffer");
18830 let second_item_id = active_item.item_id();
18831 assert_ne!(
18832 second_item_id, multibuffer_item_id,
18833 "Should navigate away from the multibuffer"
18834 );
18835 assert_ne!(
18836 second_item_id, first_item_id,
18837 "Should navigate into the 2nd buffer and activate it"
18838 );
18839 assert!(
18840 active_item.is_singleton(cx),
18841 "New active item should be a singleton buffer"
18842 );
18843 assert_eq!(
18844 active_item
18845 .act_as::<Editor>(cx)
18846 .expect("should have navigated into an editor")
18847 .read(cx)
18848 .text(cx),
18849 sample_text_2
18850 );
18851
18852 workspace
18853 .go_back(workspace.active_pane().downgrade(), window, cx)
18854 .detach_and_log_err(cx);
18855
18856 second_item_id
18857 })
18858 .unwrap();
18859 cx.executor().run_until_parked();
18860 workspace
18861 .update(cx, |workspace, _, cx| {
18862 let active_item = workspace
18863 .active_item(cx)
18864 .expect("should have an active item after navigating back from the 2nd buffer");
18865 assert_eq!(
18866 active_item.item_id(),
18867 multibuffer_item_id,
18868 "Should navigate back from the 2nd buffer to the multi buffer"
18869 );
18870 assert!(!active_item.is_singleton(cx));
18871 })
18872 .unwrap();
18873
18874 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18875 editor.change_selections(
18876 SelectionEffects::scroll(Autoscroll::Next),
18877 window,
18878 cx,
18879 |s| s.select_ranges(Some(70..70)),
18880 );
18881 editor.open_excerpts(&OpenExcerpts, window, cx);
18882 });
18883 cx.executor().run_until_parked();
18884 workspace
18885 .update(cx, |workspace, window, cx| {
18886 let active_item = workspace
18887 .active_item(cx)
18888 .expect("should have an active item after navigating into the 3rd buffer");
18889 let third_item_id = active_item.item_id();
18890 assert_ne!(
18891 third_item_id, multibuffer_item_id,
18892 "Should navigate into the 3rd buffer and activate it"
18893 );
18894 assert_ne!(third_item_id, first_item_id);
18895 assert_ne!(third_item_id, second_item_id);
18896 assert!(
18897 active_item.is_singleton(cx),
18898 "New active item should be a singleton buffer"
18899 );
18900 assert_eq!(
18901 active_item
18902 .act_as::<Editor>(cx)
18903 .expect("should have navigated into an editor")
18904 .read(cx)
18905 .text(cx),
18906 sample_text_3
18907 );
18908
18909 workspace
18910 .go_back(workspace.active_pane().downgrade(), window, cx)
18911 .detach_and_log_err(cx);
18912 })
18913 .unwrap();
18914 cx.executor().run_until_parked();
18915 workspace
18916 .update(cx, |workspace, _, cx| {
18917 let active_item = workspace
18918 .active_item(cx)
18919 .expect("should have an active item after navigating back from the 3rd buffer");
18920 assert_eq!(
18921 active_item.item_id(),
18922 multibuffer_item_id,
18923 "Should navigate back from the 3rd buffer to the multi buffer"
18924 );
18925 assert!(!active_item.is_singleton(cx));
18926 })
18927 .unwrap();
18928}
18929
18930#[gpui::test]
18931async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18932 init_test(cx, |_| {});
18933
18934 let mut cx = EditorTestContext::new(cx).await;
18935
18936 let diff_base = r#"
18937 use some::mod;
18938
18939 const A: u32 = 42;
18940
18941 fn main() {
18942 println!("hello");
18943
18944 println!("world");
18945 }
18946 "#
18947 .unindent();
18948
18949 cx.set_state(
18950 &r#"
18951 use some::modified;
18952
18953 ˇ
18954 fn main() {
18955 println!("hello there");
18956
18957 println!("around the");
18958 println!("world");
18959 }
18960 "#
18961 .unindent(),
18962 );
18963
18964 cx.set_head_text(&diff_base);
18965 executor.run_until_parked();
18966
18967 cx.update_editor(|editor, window, cx| {
18968 editor.go_to_next_hunk(&GoToHunk, window, cx);
18969 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18970 });
18971 executor.run_until_parked();
18972 cx.assert_state_with_diff(
18973 r#"
18974 use some::modified;
18975
18976
18977 fn main() {
18978 - println!("hello");
18979 + ˇ println!("hello there");
18980
18981 println!("around the");
18982 println!("world");
18983 }
18984 "#
18985 .unindent(),
18986 );
18987
18988 cx.update_editor(|editor, window, cx| {
18989 for _ in 0..2 {
18990 editor.go_to_next_hunk(&GoToHunk, window, cx);
18991 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18992 }
18993 });
18994 executor.run_until_parked();
18995 cx.assert_state_with_diff(
18996 r#"
18997 - use some::mod;
18998 + ˇuse some::modified;
18999
19000
19001 fn main() {
19002 - println!("hello");
19003 + println!("hello there");
19004
19005 + println!("around the");
19006 println!("world");
19007 }
19008 "#
19009 .unindent(),
19010 );
19011
19012 cx.update_editor(|editor, window, cx| {
19013 editor.go_to_next_hunk(&GoToHunk, window, cx);
19014 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19015 });
19016 executor.run_until_parked();
19017 cx.assert_state_with_diff(
19018 r#"
19019 - use some::mod;
19020 + use some::modified;
19021
19022 - const A: u32 = 42;
19023 ˇ
19024 fn main() {
19025 - println!("hello");
19026 + println!("hello there");
19027
19028 + println!("around the");
19029 println!("world");
19030 }
19031 "#
19032 .unindent(),
19033 );
19034
19035 cx.update_editor(|editor, window, cx| {
19036 editor.cancel(&Cancel, window, cx);
19037 });
19038
19039 cx.assert_state_with_diff(
19040 r#"
19041 use some::modified;
19042
19043 ˇ
19044 fn main() {
19045 println!("hello there");
19046
19047 println!("around the");
19048 println!("world");
19049 }
19050 "#
19051 .unindent(),
19052 );
19053}
19054
19055#[gpui::test]
19056async fn test_diff_base_change_with_expanded_diff_hunks(
19057 executor: BackgroundExecutor,
19058 cx: &mut TestAppContext,
19059) {
19060 init_test(cx, |_| {});
19061
19062 let mut cx = EditorTestContext::new(cx).await;
19063
19064 let diff_base = r#"
19065 use some::mod1;
19066 use some::mod2;
19067
19068 const A: u32 = 42;
19069 const B: u32 = 42;
19070 const C: u32 = 42;
19071
19072 fn main() {
19073 println!("hello");
19074
19075 println!("world");
19076 }
19077 "#
19078 .unindent();
19079
19080 cx.set_state(
19081 &r#"
19082 use some::mod2;
19083
19084 const A: u32 = 42;
19085 const C: u32 = 42;
19086
19087 fn main(ˇ) {
19088 //println!("hello");
19089
19090 println!("world");
19091 //
19092 //
19093 }
19094 "#
19095 .unindent(),
19096 );
19097
19098 cx.set_head_text(&diff_base);
19099 executor.run_until_parked();
19100
19101 cx.update_editor(|editor, window, cx| {
19102 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19103 });
19104 executor.run_until_parked();
19105 cx.assert_state_with_diff(
19106 r#"
19107 - use some::mod1;
19108 use some::mod2;
19109
19110 const A: u32 = 42;
19111 - const B: u32 = 42;
19112 const C: u32 = 42;
19113
19114 fn main(ˇ) {
19115 - println!("hello");
19116 + //println!("hello");
19117
19118 println!("world");
19119 + //
19120 + //
19121 }
19122 "#
19123 .unindent(),
19124 );
19125
19126 cx.set_head_text("new diff base!");
19127 executor.run_until_parked();
19128 cx.assert_state_with_diff(
19129 r#"
19130 - new diff base!
19131 + use some::mod2;
19132 +
19133 + const A: u32 = 42;
19134 + const C: u32 = 42;
19135 +
19136 + fn main(ˇ) {
19137 + //println!("hello");
19138 +
19139 + println!("world");
19140 + //
19141 + //
19142 + }
19143 "#
19144 .unindent(),
19145 );
19146}
19147
19148#[gpui::test]
19149async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19150 init_test(cx, |_| {});
19151
19152 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19153 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19154 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19155 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19156 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19157 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19158
19159 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19160 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19161 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19162
19163 let multi_buffer = cx.new(|cx| {
19164 let mut multibuffer = MultiBuffer::new(ReadWrite);
19165 multibuffer.push_excerpts(
19166 buffer_1.clone(),
19167 [
19168 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19169 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19170 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19171 ],
19172 cx,
19173 );
19174 multibuffer.push_excerpts(
19175 buffer_2.clone(),
19176 [
19177 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19178 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19179 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19180 ],
19181 cx,
19182 );
19183 multibuffer.push_excerpts(
19184 buffer_3.clone(),
19185 [
19186 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19187 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19188 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19189 ],
19190 cx,
19191 );
19192 multibuffer
19193 });
19194
19195 let editor =
19196 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19197 editor
19198 .update(cx, |editor, _window, cx| {
19199 for (buffer, diff_base) in [
19200 (buffer_1.clone(), file_1_old),
19201 (buffer_2.clone(), file_2_old),
19202 (buffer_3.clone(), file_3_old),
19203 ] {
19204 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19205 editor
19206 .buffer
19207 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19208 }
19209 })
19210 .unwrap();
19211
19212 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19213 cx.run_until_parked();
19214
19215 cx.assert_editor_state(
19216 &"
19217 ˇaaa
19218 ccc
19219 ddd
19220
19221 ggg
19222 hhh
19223
19224
19225 lll
19226 mmm
19227 NNN
19228
19229 qqq
19230 rrr
19231
19232 uuu
19233 111
19234 222
19235 333
19236
19237 666
19238 777
19239
19240 000
19241 !!!"
19242 .unindent(),
19243 );
19244
19245 cx.update_editor(|editor, window, cx| {
19246 editor.select_all(&SelectAll, window, cx);
19247 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19248 });
19249 cx.executor().run_until_parked();
19250
19251 cx.assert_state_with_diff(
19252 "
19253 «aaa
19254 - bbb
19255 ccc
19256 ddd
19257
19258 ggg
19259 hhh
19260
19261
19262 lll
19263 mmm
19264 - nnn
19265 + NNN
19266
19267 qqq
19268 rrr
19269
19270 uuu
19271 111
19272 222
19273 333
19274
19275 + 666
19276 777
19277
19278 000
19279 !!!ˇ»"
19280 .unindent(),
19281 );
19282}
19283
19284#[gpui::test]
19285async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19286 init_test(cx, |_| {});
19287
19288 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19289 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19290
19291 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19292 let multi_buffer = cx.new(|cx| {
19293 let mut multibuffer = MultiBuffer::new(ReadWrite);
19294 multibuffer.push_excerpts(
19295 buffer.clone(),
19296 [
19297 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19298 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19299 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19300 ],
19301 cx,
19302 );
19303 multibuffer
19304 });
19305
19306 let editor =
19307 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19308 editor
19309 .update(cx, |editor, _window, cx| {
19310 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19311 editor
19312 .buffer
19313 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19314 })
19315 .unwrap();
19316
19317 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19318 cx.run_until_parked();
19319
19320 cx.update_editor(|editor, window, cx| {
19321 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19322 });
19323 cx.executor().run_until_parked();
19324
19325 // When the start of a hunk coincides with the start of its excerpt,
19326 // the hunk is expanded. When the start of a hunk is earlier than
19327 // the start of its excerpt, the hunk is not expanded.
19328 cx.assert_state_with_diff(
19329 "
19330 ˇaaa
19331 - bbb
19332 + BBB
19333
19334 - ddd
19335 - eee
19336 + DDD
19337 + EEE
19338 fff
19339
19340 iii
19341 "
19342 .unindent(),
19343 );
19344}
19345
19346#[gpui::test]
19347async fn test_edits_around_expanded_insertion_hunks(
19348 executor: BackgroundExecutor,
19349 cx: &mut TestAppContext,
19350) {
19351 init_test(cx, |_| {});
19352
19353 let mut cx = EditorTestContext::new(cx).await;
19354
19355 let diff_base = r#"
19356 use some::mod1;
19357 use some::mod2;
19358
19359 const A: u32 = 42;
19360
19361 fn main() {
19362 println!("hello");
19363
19364 println!("world");
19365 }
19366 "#
19367 .unindent();
19368 executor.run_until_parked();
19369 cx.set_state(
19370 &r#"
19371 use some::mod1;
19372 use some::mod2;
19373
19374 const A: u32 = 42;
19375 const B: u32 = 42;
19376 const C: u32 = 42;
19377 ˇ
19378
19379 fn main() {
19380 println!("hello");
19381
19382 println!("world");
19383 }
19384 "#
19385 .unindent(),
19386 );
19387
19388 cx.set_head_text(&diff_base);
19389 executor.run_until_parked();
19390
19391 cx.update_editor(|editor, window, cx| {
19392 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19393 });
19394 executor.run_until_parked();
19395
19396 cx.assert_state_with_diff(
19397 r#"
19398 use some::mod1;
19399 use some::mod2;
19400
19401 const A: u32 = 42;
19402 + const B: u32 = 42;
19403 + const C: u32 = 42;
19404 + ˇ
19405
19406 fn main() {
19407 println!("hello");
19408
19409 println!("world");
19410 }
19411 "#
19412 .unindent(),
19413 );
19414
19415 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19416 executor.run_until_parked();
19417
19418 cx.assert_state_with_diff(
19419 r#"
19420 use some::mod1;
19421 use some::mod2;
19422
19423 const A: u32 = 42;
19424 + const B: u32 = 42;
19425 + const C: u32 = 42;
19426 + const D: u32 = 42;
19427 + ˇ
19428
19429 fn main() {
19430 println!("hello");
19431
19432 println!("world");
19433 }
19434 "#
19435 .unindent(),
19436 );
19437
19438 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19439 executor.run_until_parked();
19440
19441 cx.assert_state_with_diff(
19442 r#"
19443 use some::mod1;
19444 use some::mod2;
19445
19446 const A: u32 = 42;
19447 + const B: u32 = 42;
19448 + const C: u32 = 42;
19449 + const D: u32 = 42;
19450 + const E: u32 = 42;
19451 + ˇ
19452
19453 fn main() {
19454 println!("hello");
19455
19456 println!("world");
19457 }
19458 "#
19459 .unindent(),
19460 );
19461
19462 cx.update_editor(|editor, window, cx| {
19463 editor.delete_line(&DeleteLine, window, cx);
19464 });
19465 executor.run_until_parked();
19466
19467 cx.assert_state_with_diff(
19468 r#"
19469 use some::mod1;
19470 use some::mod2;
19471
19472 const A: u32 = 42;
19473 + const B: u32 = 42;
19474 + const C: u32 = 42;
19475 + const D: u32 = 42;
19476 + const E: u32 = 42;
19477 ˇ
19478 fn main() {
19479 println!("hello");
19480
19481 println!("world");
19482 }
19483 "#
19484 .unindent(),
19485 );
19486
19487 cx.update_editor(|editor, window, cx| {
19488 editor.move_up(&MoveUp, window, cx);
19489 editor.delete_line(&DeleteLine, window, cx);
19490 editor.move_up(&MoveUp, window, cx);
19491 editor.delete_line(&DeleteLine, window, cx);
19492 editor.move_up(&MoveUp, window, cx);
19493 editor.delete_line(&DeleteLine, window, cx);
19494 });
19495 executor.run_until_parked();
19496 cx.assert_state_with_diff(
19497 r#"
19498 use some::mod1;
19499 use some::mod2;
19500
19501 const A: u32 = 42;
19502 + const B: u32 = 42;
19503 ˇ
19504 fn main() {
19505 println!("hello");
19506
19507 println!("world");
19508 }
19509 "#
19510 .unindent(),
19511 );
19512
19513 cx.update_editor(|editor, window, cx| {
19514 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19515 editor.delete_line(&DeleteLine, window, cx);
19516 });
19517 executor.run_until_parked();
19518 cx.assert_state_with_diff(
19519 r#"
19520 ˇ
19521 fn main() {
19522 println!("hello");
19523
19524 println!("world");
19525 }
19526 "#
19527 .unindent(),
19528 );
19529}
19530
19531#[gpui::test]
19532async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19533 init_test(cx, |_| {});
19534
19535 let mut cx = EditorTestContext::new(cx).await;
19536 cx.set_head_text(indoc! { "
19537 one
19538 two
19539 three
19540 four
19541 five
19542 "
19543 });
19544 cx.set_state(indoc! { "
19545 one
19546 ˇthree
19547 five
19548 "});
19549 cx.run_until_parked();
19550 cx.update_editor(|editor, window, cx| {
19551 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19552 });
19553 cx.assert_state_with_diff(
19554 indoc! { "
19555 one
19556 - two
19557 ˇthree
19558 - four
19559 five
19560 "}
19561 .to_string(),
19562 );
19563 cx.update_editor(|editor, window, cx| {
19564 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19565 });
19566
19567 cx.assert_state_with_diff(
19568 indoc! { "
19569 one
19570 ˇthree
19571 five
19572 "}
19573 .to_string(),
19574 );
19575
19576 cx.set_state(indoc! { "
19577 one
19578 ˇTWO
19579 three
19580 four
19581 five
19582 "});
19583 cx.run_until_parked();
19584 cx.update_editor(|editor, window, cx| {
19585 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19586 });
19587
19588 cx.assert_state_with_diff(
19589 indoc! { "
19590 one
19591 - two
19592 + ˇTWO
19593 three
19594 four
19595 five
19596 "}
19597 .to_string(),
19598 );
19599 cx.update_editor(|editor, window, cx| {
19600 editor.move_up(&Default::default(), window, cx);
19601 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19602 });
19603 cx.assert_state_with_diff(
19604 indoc! { "
19605 one
19606 ˇTWO
19607 three
19608 four
19609 five
19610 "}
19611 .to_string(),
19612 );
19613}
19614
19615#[gpui::test]
19616async fn test_edits_around_expanded_deletion_hunks(
19617 executor: BackgroundExecutor,
19618 cx: &mut TestAppContext,
19619) {
19620 init_test(cx, |_| {});
19621
19622 let mut cx = EditorTestContext::new(cx).await;
19623
19624 let diff_base = r#"
19625 use some::mod1;
19626 use some::mod2;
19627
19628 const A: u32 = 42;
19629 const B: u32 = 42;
19630 const C: u32 = 42;
19631
19632
19633 fn main() {
19634 println!("hello");
19635
19636 println!("world");
19637 }
19638 "#
19639 .unindent();
19640 executor.run_until_parked();
19641 cx.set_state(
19642 &r#"
19643 use some::mod1;
19644 use some::mod2;
19645
19646 ˇconst B: u32 = 42;
19647 const C: u32 = 42;
19648
19649
19650 fn main() {
19651 println!("hello");
19652
19653 println!("world");
19654 }
19655 "#
19656 .unindent(),
19657 );
19658
19659 cx.set_head_text(&diff_base);
19660 executor.run_until_parked();
19661
19662 cx.update_editor(|editor, window, cx| {
19663 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19664 });
19665 executor.run_until_parked();
19666
19667 cx.assert_state_with_diff(
19668 r#"
19669 use some::mod1;
19670 use some::mod2;
19671
19672 - const A: u32 = 42;
19673 ˇconst B: u32 = 42;
19674 const C: u32 = 42;
19675
19676
19677 fn main() {
19678 println!("hello");
19679
19680 println!("world");
19681 }
19682 "#
19683 .unindent(),
19684 );
19685
19686 cx.update_editor(|editor, window, cx| {
19687 editor.delete_line(&DeleteLine, window, cx);
19688 });
19689 executor.run_until_parked();
19690 cx.assert_state_with_diff(
19691 r#"
19692 use some::mod1;
19693 use some::mod2;
19694
19695 - const A: u32 = 42;
19696 - const B: u32 = 42;
19697 ˇconst C: u32 = 42;
19698
19699
19700 fn main() {
19701 println!("hello");
19702
19703 println!("world");
19704 }
19705 "#
19706 .unindent(),
19707 );
19708
19709 cx.update_editor(|editor, window, cx| {
19710 editor.delete_line(&DeleteLine, window, cx);
19711 });
19712 executor.run_until_parked();
19713 cx.assert_state_with_diff(
19714 r#"
19715 use some::mod1;
19716 use some::mod2;
19717
19718 - const A: u32 = 42;
19719 - const B: u32 = 42;
19720 - const C: u32 = 42;
19721 ˇ
19722
19723 fn main() {
19724 println!("hello");
19725
19726 println!("world");
19727 }
19728 "#
19729 .unindent(),
19730 );
19731
19732 cx.update_editor(|editor, window, cx| {
19733 editor.handle_input("replacement", window, cx);
19734 });
19735 executor.run_until_parked();
19736 cx.assert_state_with_diff(
19737 r#"
19738 use some::mod1;
19739 use some::mod2;
19740
19741 - const A: u32 = 42;
19742 - const B: u32 = 42;
19743 - const C: u32 = 42;
19744 -
19745 + replacementˇ
19746
19747 fn main() {
19748 println!("hello");
19749
19750 println!("world");
19751 }
19752 "#
19753 .unindent(),
19754 );
19755}
19756
19757#[gpui::test]
19758async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19759 init_test(cx, |_| {});
19760
19761 let mut cx = EditorTestContext::new(cx).await;
19762
19763 let base_text = r#"
19764 one
19765 two
19766 three
19767 four
19768 five
19769 "#
19770 .unindent();
19771 executor.run_until_parked();
19772 cx.set_state(
19773 &r#"
19774 one
19775 two
19776 fˇour
19777 five
19778 "#
19779 .unindent(),
19780 );
19781
19782 cx.set_head_text(&base_text);
19783 executor.run_until_parked();
19784
19785 cx.update_editor(|editor, window, cx| {
19786 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19787 });
19788 executor.run_until_parked();
19789
19790 cx.assert_state_with_diff(
19791 r#"
19792 one
19793 two
19794 - three
19795 fˇour
19796 five
19797 "#
19798 .unindent(),
19799 );
19800
19801 cx.update_editor(|editor, window, cx| {
19802 editor.backspace(&Backspace, window, cx);
19803 editor.backspace(&Backspace, window, cx);
19804 });
19805 executor.run_until_parked();
19806 cx.assert_state_with_diff(
19807 r#"
19808 one
19809 two
19810 - threeˇ
19811 - four
19812 + our
19813 five
19814 "#
19815 .unindent(),
19816 );
19817}
19818
19819#[gpui::test]
19820async fn test_edit_after_expanded_modification_hunk(
19821 executor: BackgroundExecutor,
19822 cx: &mut TestAppContext,
19823) {
19824 init_test(cx, |_| {});
19825
19826 let mut cx = EditorTestContext::new(cx).await;
19827
19828 let diff_base = r#"
19829 use some::mod1;
19830 use some::mod2;
19831
19832 const A: u32 = 42;
19833 const B: u32 = 42;
19834 const C: u32 = 42;
19835 const D: u32 = 42;
19836
19837
19838 fn main() {
19839 println!("hello");
19840
19841 println!("world");
19842 }"#
19843 .unindent();
19844
19845 cx.set_state(
19846 &r#"
19847 use some::mod1;
19848 use some::mod2;
19849
19850 const A: u32 = 42;
19851 const B: u32 = 42;
19852 const C: u32 = 43ˇ
19853 const D: u32 = 42;
19854
19855
19856 fn main() {
19857 println!("hello");
19858
19859 println!("world");
19860 }"#
19861 .unindent(),
19862 );
19863
19864 cx.set_head_text(&diff_base);
19865 executor.run_until_parked();
19866 cx.update_editor(|editor, window, cx| {
19867 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19868 });
19869 executor.run_until_parked();
19870
19871 cx.assert_state_with_diff(
19872 r#"
19873 use some::mod1;
19874 use some::mod2;
19875
19876 const A: u32 = 42;
19877 const B: u32 = 42;
19878 - const C: u32 = 42;
19879 + const C: u32 = 43ˇ
19880 const D: u32 = 42;
19881
19882
19883 fn main() {
19884 println!("hello");
19885
19886 println!("world");
19887 }"#
19888 .unindent(),
19889 );
19890
19891 cx.update_editor(|editor, window, cx| {
19892 editor.handle_input("\nnew_line\n", window, cx);
19893 });
19894 executor.run_until_parked();
19895
19896 cx.assert_state_with_diff(
19897 r#"
19898 use some::mod1;
19899 use some::mod2;
19900
19901 const A: u32 = 42;
19902 const B: u32 = 42;
19903 - const C: u32 = 42;
19904 + const C: u32 = 43
19905 + new_line
19906 + ˇ
19907 const D: u32 = 42;
19908
19909
19910 fn main() {
19911 println!("hello");
19912
19913 println!("world");
19914 }"#
19915 .unindent(),
19916 );
19917}
19918
19919#[gpui::test]
19920async fn test_stage_and_unstage_added_file_hunk(
19921 executor: BackgroundExecutor,
19922 cx: &mut TestAppContext,
19923) {
19924 init_test(cx, |_| {});
19925
19926 let mut cx = EditorTestContext::new(cx).await;
19927 cx.update_editor(|editor, _, cx| {
19928 editor.set_expand_all_diff_hunks(cx);
19929 });
19930
19931 let working_copy = r#"
19932 ˇfn main() {
19933 println!("hello, world!");
19934 }
19935 "#
19936 .unindent();
19937
19938 cx.set_state(&working_copy);
19939 executor.run_until_parked();
19940
19941 cx.assert_state_with_diff(
19942 r#"
19943 + ˇfn main() {
19944 + println!("hello, world!");
19945 + }
19946 "#
19947 .unindent(),
19948 );
19949 cx.assert_index_text(None);
19950
19951 cx.update_editor(|editor, window, cx| {
19952 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19953 });
19954 executor.run_until_parked();
19955 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19956 cx.assert_state_with_diff(
19957 r#"
19958 + ˇfn main() {
19959 + println!("hello, world!");
19960 + }
19961 "#
19962 .unindent(),
19963 );
19964
19965 cx.update_editor(|editor, window, cx| {
19966 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19967 });
19968 executor.run_until_parked();
19969 cx.assert_index_text(None);
19970}
19971
19972async fn setup_indent_guides_editor(
19973 text: &str,
19974 cx: &mut TestAppContext,
19975) -> (BufferId, EditorTestContext) {
19976 init_test(cx, |_| {});
19977
19978 let mut cx = EditorTestContext::new(cx).await;
19979
19980 let buffer_id = cx.update_editor(|editor, window, cx| {
19981 editor.set_text(text, window, cx);
19982 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19983
19984 buffer_ids[0]
19985 });
19986
19987 (buffer_id, cx)
19988}
19989
19990fn assert_indent_guides(
19991 range: Range<u32>,
19992 expected: Vec<IndentGuide>,
19993 active_indices: Option<Vec<usize>>,
19994 cx: &mut EditorTestContext,
19995) {
19996 let indent_guides = cx.update_editor(|editor, window, cx| {
19997 let snapshot = editor.snapshot(window, cx).display_snapshot;
19998 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19999 editor,
20000 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20001 true,
20002 &snapshot,
20003 cx,
20004 );
20005
20006 indent_guides.sort_by(|a, b| {
20007 a.depth.cmp(&b.depth).then(
20008 a.start_row
20009 .cmp(&b.start_row)
20010 .then(a.end_row.cmp(&b.end_row)),
20011 )
20012 });
20013 indent_guides
20014 });
20015
20016 if let Some(expected) = active_indices {
20017 let active_indices = cx.update_editor(|editor, window, cx| {
20018 let snapshot = editor.snapshot(window, cx).display_snapshot;
20019 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20020 });
20021
20022 assert_eq!(
20023 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20024 expected,
20025 "Active indent guide indices do not match"
20026 );
20027 }
20028
20029 assert_eq!(indent_guides, expected, "Indent guides do not match");
20030}
20031
20032fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20033 IndentGuide {
20034 buffer_id,
20035 start_row: MultiBufferRow(start_row),
20036 end_row: MultiBufferRow(end_row),
20037 depth,
20038 tab_size: 4,
20039 settings: IndentGuideSettings {
20040 enabled: true,
20041 line_width: 1,
20042 active_line_width: 1,
20043 ..Default::default()
20044 },
20045 }
20046}
20047
20048#[gpui::test]
20049async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20050 let (buffer_id, mut cx) = setup_indent_guides_editor(
20051 &"
20052 fn main() {
20053 let a = 1;
20054 }"
20055 .unindent(),
20056 cx,
20057 )
20058 .await;
20059
20060 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20061}
20062
20063#[gpui::test]
20064async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20065 let (buffer_id, mut cx) = setup_indent_guides_editor(
20066 &"
20067 fn main() {
20068 let a = 1;
20069 let b = 2;
20070 }"
20071 .unindent(),
20072 cx,
20073 )
20074 .await;
20075
20076 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20077}
20078
20079#[gpui::test]
20080async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20081 let (buffer_id, mut cx) = setup_indent_guides_editor(
20082 &"
20083 fn main() {
20084 let a = 1;
20085 if a == 3 {
20086 let b = 2;
20087 } else {
20088 let c = 3;
20089 }
20090 }"
20091 .unindent(),
20092 cx,
20093 )
20094 .await;
20095
20096 assert_indent_guides(
20097 0..8,
20098 vec![
20099 indent_guide(buffer_id, 1, 6, 0),
20100 indent_guide(buffer_id, 3, 3, 1),
20101 indent_guide(buffer_id, 5, 5, 1),
20102 ],
20103 None,
20104 &mut cx,
20105 );
20106}
20107
20108#[gpui::test]
20109async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20110 let (buffer_id, mut cx) = setup_indent_guides_editor(
20111 &"
20112 fn main() {
20113 let a = 1;
20114 let b = 2;
20115 let c = 3;
20116 }"
20117 .unindent(),
20118 cx,
20119 )
20120 .await;
20121
20122 assert_indent_guides(
20123 0..5,
20124 vec![
20125 indent_guide(buffer_id, 1, 3, 0),
20126 indent_guide(buffer_id, 2, 2, 1),
20127 ],
20128 None,
20129 &mut cx,
20130 );
20131}
20132
20133#[gpui::test]
20134async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20135 let (buffer_id, mut cx) = setup_indent_guides_editor(
20136 &"
20137 fn main() {
20138 let a = 1;
20139
20140 let c = 3;
20141 }"
20142 .unindent(),
20143 cx,
20144 )
20145 .await;
20146
20147 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20148}
20149
20150#[gpui::test]
20151async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20152 let (buffer_id, mut cx) = setup_indent_guides_editor(
20153 &"
20154 fn main() {
20155 let a = 1;
20156
20157 let c = 3;
20158
20159 if a == 3 {
20160 let b = 2;
20161 } else {
20162 let c = 3;
20163 }
20164 }"
20165 .unindent(),
20166 cx,
20167 )
20168 .await;
20169
20170 assert_indent_guides(
20171 0..11,
20172 vec![
20173 indent_guide(buffer_id, 1, 9, 0),
20174 indent_guide(buffer_id, 6, 6, 1),
20175 indent_guide(buffer_id, 8, 8, 1),
20176 ],
20177 None,
20178 &mut cx,
20179 );
20180}
20181
20182#[gpui::test]
20183async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20184 let (buffer_id, mut cx) = setup_indent_guides_editor(
20185 &"
20186 fn main() {
20187 let a = 1;
20188
20189 let c = 3;
20190
20191 if a == 3 {
20192 let b = 2;
20193 } else {
20194 let c = 3;
20195 }
20196 }"
20197 .unindent(),
20198 cx,
20199 )
20200 .await;
20201
20202 assert_indent_guides(
20203 1..11,
20204 vec![
20205 indent_guide(buffer_id, 1, 9, 0),
20206 indent_guide(buffer_id, 6, 6, 1),
20207 indent_guide(buffer_id, 8, 8, 1),
20208 ],
20209 None,
20210 &mut cx,
20211 );
20212}
20213
20214#[gpui::test]
20215async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20216 let (buffer_id, mut cx) = setup_indent_guides_editor(
20217 &"
20218 fn main() {
20219 let a = 1;
20220
20221 let c = 3;
20222
20223 if a == 3 {
20224 let b = 2;
20225 } else {
20226 let c = 3;
20227 }
20228 }"
20229 .unindent(),
20230 cx,
20231 )
20232 .await;
20233
20234 assert_indent_guides(
20235 1..10,
20236 vec![
20237 indent_guide(buffer_id, 1, 9, 0),
20238 indent_guide(buffer_id, 6, 6, 1),
20239 indent_guide(buffer_id, 8, 8, 1),
20240 ],
20241 None,
20242 &mut cx,
20243 );
20244}
20245
20246#[gpui::test]
20247async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20248 let (buffer_id, mut cx) = setup_indent_guides_editor(
20249 &"
20250 fn main() {
20251 if a {
20252 b(
20253 c,
20254 d,
20255 )
20256 } else {
20257 e(
20258 f
20259 )
20260 }
20261 }"
20262 .unindent(),
20263 cx,
20264 )
20265 .await;
20266
20267 assert_indent_guides(
20268 0..11,
20269 vec![
20270 indent_guide(buffer_id, 1, 10, 0),
20271 indent_guide(buffer_id, 2, 5, 1),
20272 indent_guide(buffer_id, 7, 9, 1),
20273 indent_guide(buffer_id, 3, 4, 2),
20274 indent_guide(buffer_id, 8, 8, 2),
20275 ],
20276 None,
20277 &mut cx,
20278 );
20279
20280 cx.update_editor(|editor, window, cx| {
20281 editor.fold_at(MultiBufferRow(2), window, cx);
20282 assert_eq!(
20283 editor.display_text(cx),
20284 "
20285 fn main() {
20286 if a {
20287 b(⋯
20288 )
20289 } else {
20290 e(
20291 f
20292 )
20293 }
20294 }"
20295 .unindent()
20296 );
20297 });
20298
20299 assert_indent_guides(
20300 0..11,
20301 vec![
20302 indent_guide(buffer_id, 1, 10, 0),
20303 indent_guide(buffer_id, 2, 5, 1),
20304 indent_guide(buffer_id, 7, 9, 1),
20305 indent_guide(buffer_id, 8, 8, 2),
20306 ],
20307 None,
20308 &mut cx,
20309 );
20310}
20311
20312#[gpui::test]
20313async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20314 let (buffer_id, mut cx) = setup_indent_guides_editor(
20315 &"
20316 block1
20317 block2
20318 block3
20319 block4
20320 block2
20321 block1
20322 block1"
20323 .unindent(),
20324 cx,
20325 )
20326 .await;
20327
20328 assert_indent_guides(
20329 1..10,
20330 vec![
20331 indent_guide(buffer_id, 1, 4, 0),
20332 indent_guide(buffer_id, 2, 3, 1),
20333 indent_guide(buffer_id, 3, 3, 2),
20334 ],
20335 None,
20336 &mut cx,
20337 );
20338}
20339
20340#[gpui::test]
20341async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20342 let (buffer_id, mut cx) = setup_indent_guides_editor(
20343 &"
20344 block1
20345 block2
20346 block3
20347
20348 block1
20349 block1"
20350 .unindent(),
20351 cx,
20352 )
20353 .await;
20354
20355 assert_indent_guides(
20356 0..6,
20357 vec![
20358 indent_guide(buffer_id, 1, 2, 0),
20359 indent_guide(buffer_id, 2, 2, 1),
20360 ],
20361 None,
20362 &mut cx,
20363 );
20364}
20365
20366#[gpui::test]
20367async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20368 let (buffer_id, mut cx) = setup_indent_guides_editor(
20369 &"
20370 function component() {
20371 \treturn (
20372 \t\t\t
20373 \t\t<div>
20374 \t\t\t<abc></abc>
20375 \t\t</div>
20376 \t)
20377 }"
20378 .unindent(),
20379 cx,
20380 )
20381 .await;
20382
20383 assert_indent_guides(
20384 0..8,
20385 vec![
20386 indent_guide(buffer_id, 1, 6, 0),
20387 indent_guide(buffer_id, 2, 5, 1),
20388 indent_guide(buffer_id, 4, 4, 2),
20389 ],
20390 None,
20391 &mut cx,
20392 );
20393}
20394
20395#[gpui::test]
20396async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20397 let (buffer_id, mut cx) = setup_indent_guides_editor(
20398 &"
20399 function component() {
20400 \treturn (
20401 \t
20402 \t\t<div>
20403 \t\t\t<abc></abc>
20404 \t\t</div>
20405 \t)
20406 }"
20407 .unindent(),
20408 cx,
20409 )
20410 .await;
20411
20412 assert_indent_guides(
20413 0..8,
20414 vec![
20415 indent_guide(buffer_id, 1, 6, 0),
20416 indent_guide(buffer_id, 2, 5, 1),
20417 indent_guide(buffer_id, 4, 4, 2),
20418 ],
20419 None,
20420 &mut cx,
20421 );
20422}
20423
20424#[gpui::test]
20425async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20426 let (buffer_id, mut cx) = setup_indent_guides_editor(
20427 &"
20428 block1
20429
20430
20431
20432 block2
20433 "
20434 .unindent(),
20435 cx,
20436 )
20437 .await;
20438
20439 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20440}
20441
20442#[gpui::test]
20443async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20444 let (buffer_id, mut cx) = setup_indent_guides_editor(
20445 &"
20446 def a:
20447 \tb = 3
20448 \tif True:
20449 \t\tc = 4
20450 \t\td = 5
20451 \tprint(b)
20452 "
20453 .unindent(),
20454 cx,
20455 )
20456 .await;
20457
20458 assert_indent_guides(
20459 0..6,
20460 vec![
20461 indent_guide(buffer_id, 1, 5, 0),
20462 indent_guide(buffer_id, 3, 4, 1),
20463 ],
20464 None,
20465 &mut cx,
20466 );
20467}
20468
20469#[gpui::test]
20470async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20471 let (buffer_id, mut cx) = setup_indent_guides_editor(
20472 &"
20473 fn main() {
20474 let a = 1;
20475 }"
20476 .unindent(),
20477 cx,
20478 )
20479 .await;
20480
20481 cx.update_editor(|editor, window, cx| {
20482 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20483 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20484 });
20485 });
20486
20487 assert_indent_guides(
20488 0..3,
20489 vec![indent_guide(buffer_id, 1, 1, 0)],
20490 Some(vec![0]),
20491 &mut cx,
20492 );
20493}
20494
20495#[gpui::test]
20496async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20497 let (buffer_id, mut cx) = setup_indent_guides_editor(
20498 &"
20499 fn main() {
20500 if 1 == 2 {
20501 let a = 1;
20502 }
20503 }"
20504 .unindent(),
20505 cx,
20506 )
20507 .await;
20508
20509 cx.update_editor(|editor, window, cx| {
20510 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20511 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20512 });
20513 });
20514
20515 assert_indent_guides(
20516 0..4,
20517 vec![
20518 indent_guide(buffer_id, 1, 3, 0),
20519 indent_guide(buffer_id, 2, 2, 1),
20520 ],
20521 Some(vec![1]),
20522 &mut cx,
20523 );
20524
20525 cx.update_editor(|editor, window, cx| {
20526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20527 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20528 });
20529 });
20530
20531 assert_indent_guides(
20532 0..4,
20533 vec![
20534 indent_guide(buffer_id, 1, 3, 0),
20535 indent_guide(buffer_id, 2, 2, 1),
20536 ],
20537 Some(vec![1]),
20538 &mut cx,
20539 );
20540
20541 cx.update_editor(|editor, window, cx| {
20542 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20543 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20544 });
20545 });
20546
20547 assert_indent_guides(
20548 0..4,
20549 vec![
20550 indent_guide(buffer_id, 1, 3, 0),
20551 indent_guide(buffer_id, 2, 2, 1),
20552 ],
20553 Some(vec![0]),
20554 &mut cx,
20555 );
20556}
20557
20558#[gpui::test]
20559async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20560 let (buffer_id, mut cx) = setup_indent_guides_editor(
20561 &"
20562 fn main() {
20563 let a = 1;
20564
20565 let b = 2;
20566 }"
20567 .unindent(),
20568 cx,
20569 )
20570 .await;
20571
20572 cx.update_editor(|editor, window, cx| {
20573 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20574 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20575 });
20576 });
20577
20578 assert_indent_guides(
20579 0..5,
20580 vec![indent_guide(buffer_id, 1, 3, 0)],
20581 Some(vec![0]),
20582 &mut cx,
20583 );
20584}
20585
20586#[gpui::test]
20587async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20588 let (buffer_id, mut cx) = setup_indent_guides_editor(
20589 &"
20590 def m:
20591 a = 1
20592 pass"
20593 .unindent(),
20594 cx,
20595 )
20596 .await;
20597
20598 cx.update_editor(|editor, window, cx| {
20599 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20600 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20601 });
20602 });
20603
20604 assert_indent_guides(
20605 0..3,
20606 vec![indent_guide(buffer_id, 1, 2, 0)],
20607 Some(vec![0]),
20608 &mut cx,
20609 );
20610}
20611
20612#[gpui::test]
20613async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20614 init_test(cx, |_| {});
20615 let mut cx = EditorTestContext::new(cx).await;
20616 let text = indoc! {
20617 "
20618 impl A {
20619 fn b() {
20620 0;
20621 3;
20622 5;
20623 6;
20624 7;
20625 }
20626 }
20627 "
20628 };
20629 let base_text = indoc! {
20630 "
20631 impl A {
20632 fn b() {
20633 0;
20634 1;
20635 2;
20636 3;
20637 4;
20638 }
20639 fn c() {
20640 5;
20641 6;
20642 7;
20643 }
20644 }
20645 "
20646 };
20647
20648 cx.update_editor(|editor, window, cx| {
20649 editor.set_text(text, window, cx);
20650
20651 editor.buffer().update(cx, |multibuffer, cx| {
20652 let buffer = multibuffer.as_singleton().unwrap();
20653 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20654
20655 multibuffer.set_all_diff_hunks_expanded(cx);
20656 multibuffer.add_diff(diff, cx);
20657
20658 buffer.read(cx).remote_id()
20659 })
20660 });
20661 cx.run_until_parked();
20662
20663 cx.assert_state_with_diff(
20664 indoc! { "
20665 impl A {
20666 fn b() {
20667 0;
20668 - 1;
20669 - 2;
20670 3;
20671 - 4;
20672 - }
20673 - fn c() {
20674 5;
20675 6;
20676 7;
20677 }
20678 }
20679 ˇ"
20680 }
20681 .to_string(),
20682 );
20683
20684 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20685 editor
20686 .snapshot(window, cx)
20687 .buffer_snapshot
20688 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20689 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20690 .collect::<Vec<_>>()
20691 });
20692 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20693 assert_eq!(
20694 actual_guides,
20695 vec![
20696 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20697 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20698 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20699 ]
20700 );
20701}
20702
20703#[gpui::test]
20704async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20705 init_test(cx, |_| {});
20706 let mut cx = EditorTestContext::new(cx).await;
20707
20708 let diff_base = r#"
20709 a
20710 b
20711 c
20712 "#
20713 .unindent();
20714
20715 cx.set_state(
20716 &r#"
20717 ˇA
20718 b
20719 C
20720 "#
20721 .unindent(),
20722 );
20723 cx.set_head_text(&diff_base);
20724 cx.update_editor(|editor, window, cx| {
20725 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20726 });
20727 executor.run_until_parked();
20728
20729 let both_hunks_expanded = r#"
20730 - a
20731 + ˇA
20732 b
20733 - c
20734 + C
20735 "#
20736 .unindent();
20737
20738 cx.assert_state_with_diff(both_hunks_expanded.clone());
20739
20740 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20741 let snapshot = editor.snapshot(window, cx);
20742 let hunks = editor
20743 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20744 .collect::<Vec<_>>();
20745 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20746 let buffer_id = hunks[0].buffer_id;
20747 hunks
20748 .into_iter()
20749 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20750 .collect::<Vec<_>>()
20751 });
20752 assert_eq!(hunk_ranges.len(), 2);
20753
20754 cx.update_editor(|editor, _, cx| {
20755 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20756 });
20757 executor.run_until_parked();
20758
20759 let second_hunk_expanded = r#"
20760 ˇA
20761 b
20762 - c
20763 + C
20764 "#
20765 .unindent();
20766
20767 cx.assert_state_with_diff(second_hunk_expanded);
20768
20769 cx.update_editor(|editor, _, cx| {
20770 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20771 });
20772 executor.run_until_parked();
20773
20774 cx.assert_state_with_diff(both_hunks_expanded.clone());
20775
20776 cx.update_editor(|editor, _, cx| {
20777 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20778 });
20779 executor.run_until_parked();
20780
20781 let first_hunk_expanded = r#"
20782 - a
20783 + ˇA
20784 b
20785 C
20786 "#
20787 .unindent();
20788
20789 cx.assert_state_with_diff(first_hunk_expanded);
20790
20791 cx.update_editor(|editor, _, cx| {
20792 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20793 });
20794 executor.run_until_parked();
20795
20796 cx.assert_state_with_diff(both_hunks_expanded);
20797
20798 cx.set_state(
20799 &r#"
20800 ˇA
20801 b
20802 "#
20803 .unindent(),
20804 );
20805 cx.run_until_parked();
20806
20807 // TODO this cursor position seems bad
20808 cx.assert_state_with_diff(
20809 r#"
20810 - ˇa
20811 + A
20812 b
20813 "#
20814 .unindent(),
20815 );
20816
20817 cx.update_editor(|editor, window, cx| {
20818 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20819 });
20820
20821 cx.assert_state_with_diff(
20822 r#"
20823 - ˇa
20824 + A
20825 b
20826 - c
20827 "#
20828 .unindent(),
20829 );
20830
20831 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20832 let snapshot = editor.snapshot(window, cx);
20833 let hunks = editor
20834 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20835 .collect::<Vec<_>>();
20836 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20837 let buffer_id = hunks[0].buffer_id;
20838 hunks
20839 .into_iter()
20840 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20841 .collect::<Vec<_>>()
20842 });
20843 assert_eq!(hunk_ranges.len(), 2);
20844
20845 cx.update_editor(|editor, _, cx| {
20846 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20847 });
20848 executor.run_until_parked();
20849
20850 cx.assert_state_with_diff(
20851 r#"
20852 - ˇa
20853 + A
20854 b
20855 "#
20856 .unindent(),
20857 );
20858}
20859
20860#[gpui::test]
20861async fn test_toggle_deletion_hunk_at_start_of_file(
20862 executor: BackgroundExecutor,
20863 cx: &mut TestAppContext,
20864) {
20865 init_test(cx, |_| {});
20866 let mut cx = EditorTestContext::new(cx).await;
20867
20868 let diff_base = r#"
20869 a
20870 b
20871 c
20872 "#
20873 .unindent();
20874
20875 cx.set_state(
20876 &r#"
20877 ˇb
20878 c
20879 "#
20880 .unindent(),
20881 );
20882 cx.set_head_text(&diff_base);
20883 cx.update_editor(|editor, window, cx| {
20884 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20885 });
20886 executor.run_until_parked();
20887
20888 let hunk_expanded = r#"
20889 - a
20890 ˇb
20891 c
20892 "#
20893 .unindent();
20894
20895 cx.assert_state_with_diff(hunk_expanded.clone());
20896
20897 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20898 let snapshot = editor.snapshot(window, cx);
20899 let hunks = editor
20900 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20901 .collect::<Vec<_>>();
20902 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20903 let buffer_id = hunks[0].buffer_id;
20904 hunks
20905 .into_iter()
20906 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20907 .collect::<Vec<_>>()
20908 });
20909 assert_eq!(hunk_ranges.len(), 1);
20910
20911 cx.update_editor(|editor, _, cx| {
20912 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20913 });
20914 executor.run_until_parked();
20915
20916 let hunk_collapsed = r#"
20917 ˇb
20918 c
20919 "#
20920 .unindent();
20921
20922 cx.assert_state_with_diff(hunk_collapsed);
20923
20924 cx.update_editor(|editor, _, cx| {
20925 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20926 });
20927 executor.run_until_parked();
20928
20929 cx.assert_state_with_diff(hunk_expanded);
20930}
20931
20932#[gpui::test]
20933async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20934 init_test(cx, |_| {});
20935
20936 let fs = FakeFs::new(cx.executor());
20937 fs.insert_tree(
20938 path!("/test"),
20939 json!({
20940 ".git": {},
20941 "file-1": "ONE\n",
20942 "file-2": "TWO\n",
20943 "file-3": "THREE\n",
20944 }),
20945 )
20946 .await;
20947
20948 fs.set_head_for_repo(
20949 path!("/test/.git").as_ref(),
20950 &[
20951 ("file-1".into(), "one\n".into()),
20952 ("file-2".into(), "two\n".into()),
20953 ("file-3".into(), "three\n".into()),
20954 ],
20955 "deadbeef",
20956 );
20957
20958 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20959 let mut buffers = vec![];
20960 for i in 1..=3 {
20961 let buffer = project
20962 .update(cx, |project, cx| {
20963 let path = format!(path!("/test/file-{}"), i);
20964 project.open_local_buffer(path, cx)
20965 })
20966 .await
20967 .unwrap();
20968 buffers.push(buffer);
20969 }
20970
20971 let multibuffer = cx.new(|cx| {
20972 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20973 multibuffer.set_all_diff_hunks_expanded(cx);
20974 for buffer in &buffers {
20975 let snapshot = buffer.read(cx).snapshot();
20976 multibuffer.set_excerpts_for_path(
20977 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20978 buffer.clone(),
20979 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20980 2,
20981 cx,
20982 );
20983 }
20984 multibuffer
20985 });
20986
20987 let editor = cx.add_window(|window, cx| {
20988 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20989 });
20990 cx.run_until_parked();
20991
20992 let snapshot = editor
20993 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20994 .unwrap();
20995 let hunks = snapshot
20996 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20997 .map(|hunk| match hunk {
20998 DisplayDiffHunk::Unfolded {
20999 display_row_range, ..
21000 } => display_row_range,
21001 DisplayDiffHunk::Folded { .. } => unreachable!(),
21002 })
21003 .collect::<Vec<_>>();
21004 assert_eq!(
21005 hunks,
21006 [
21007 DisplayRow(2)..DisplayRow(4),
21008 DisplayRow(7)..DisplayRow(9),
21009 DisplayRow(12)..DisplayRow(14),
21010 ]
21011 );
21012}
21013
21014#[gpui::test]
21015async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21016 init_test(cx, |_| {});
21017
21018 let mut cx = EditorTestContext::new(cx).await;
21019 cx.set_head_text(indoc! { "
21020 one
21021 two
21022 three
21023 four
21024 five
21025 "
21026 });
21027 cx.set_index_text(indoc! { "
21028 one
21029 two
21030 three
21031 four
21032 five
21033 "
21034 });
21035 cx.set_state(indoc! {"
21036 one
21037 TWO
21038 ˇTHREE
21039 FOUR
21040 five
21041 "});
21042 cx.run_until_parked();
21043 cx.update_editor(|editor, window, cx| {
21044 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21045 });
21046 cx.run_until_parked();
21047 cx.assert_index_text(Some(indoc! {"
21048 one
21049 TWO
21050 THREE
21051 FOUR
21052 five
21053 "}));
21054 cx.set_state(indoc! { "
21055 one
21056 TWO
21057 ˇTHREE-HUNDRED
21058 FOUR
21059 five
21060 "});
21061 cx.run_until_parked();
21062 cx.update_editor(|editor, window, cx| {
21063 let snapshot = editor.snapshot(window, cx);
21064 let hunks = editor
21065 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21066 .collect::<Vec<_>>();
21067 assert_eq!(hunks.len(), 1);
21068 assert_eq!(
21069 hunks[0].status(),
21070 DiffHunkStatus {
21071 kind: DiffHunkStatusKind::Modified,
21072 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21073 }
21074 );
21075
21076 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21077 });
21078 cx.run_until_parked();
21079 cx.assert_index_text(Some(indoc! {"
21080 one
21081 TWO
21082 THREE-HUNDRED
21083 FOUR
21084 five
21085 "}));
21086}
21087
21088#[gpui::test]
21089fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21090 init_test(cx, |_| {});
21091
21092 let editor = cx.add_window(|window, cx| {
21093 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21094 build_editor(buffer, window, cx)
21095 });
21096
21097 let render_args = Arc::new(Mutex::new(None));
21098 let snapshot = editor
21099 .update(cx, |editor, window, cx| {
21100 let snapshot = editor.buffer().read(cx).snapshot(cx);
21101 let range =
21102 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21103
21104 struct RenderArgs {
21105 row: MultiBufferRow,
21106 folded: bool,
21107 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21108 }
21109
21110 let crease = Crease::inline(
21111 range,
21112 FoldPlaceholder::test(),
21113 {
21114 let toggle_callback = render_args.clone();
21115 move |row, folded, callback, _window, _cx| {
21116 *toggle_callback.lock() = Some(RenderArgs {
21117 row,
21118 folded,
21119 callback,
21120 });
21121 div()
21122 }
21123 },
21124 |_row, _folded, _window, _cx| div(),
21125 );
21126
21127 editor.insert_creases(Some(crease), cx);
21128 let snapshot = editor.snapshot(window, cx);
21129 let _div =
21130 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21131 snapshot
21132 })
21133 .unwrap();
21134
21135 let render_args = render_args.lock().take().unwrap();
21136 assert_eq!(render_args.row, MultiBufferRow(1));
21137 assert!(!render_args.folded);
21138 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21139
21140 cx.update_window(*editor, |_, window, cx| {
21141 (render_args.callback)(true, window, cx)
21142 })
21143 .unwrap();
21144 let snapshot = editor
21145 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21146 .unwrap();
21147 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21148
21149 cx.update_window(*editor, |_, window, cx| {
21150 (render_args.callback)(false, window, cx)
21151 })
21152 .unwrap();
21153 let snapshot = editor
21154 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21155 .unwrap();
21156 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21157}
21158
21159#[gpui::test]
21160async fn test_input_text(cx: &mut TestAppContext) {
21161 init_test(cx, |_| {});
21162 let mut cx = EditorTestContext::new(cx).await;
21163
21164 cx.set_state(
21165 &r#"ˇone
21166 two
21167
21168 three
21169 fourˇ
21170 five
21171
21172 siˇx"#
21173 .unindent(),
21174 );
21175
21176 cx.dispatch_action(HandleInput(String::new()));
21177 cx.assert_editor_state(
21178 &r#"ˇone
21179 two
21180
21181 three
21182 fourˇ
21183 five
21184
21185 siˇx"#
21186 .unindent(),
21187 );
21188
21189 cx.dispatch_action(HandleInput("AAAA".to_string()));
21190 cx.assert_editor_state(
21191 &r#"AAAAˇone
21192 two
21193
21194 three
21195 fourAAAAˇ
21196 five
21197
21198 siAAAAˇx"#
21199 .unindent(),
21200 );
21201}
21202
21203#[gpui::test]
21204async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21205 init_test(cx, |_| {});
21206
21207 let mut cx = EditorTestContext::new(cx).await;
21208 cx.set_state(
21209 r#"let foo = 1;
21210let foo = 2;
21211let foo = 3;
21212let fooˇ = 4;
21213let foo = 5;
21214let foo = 6;
21215let foo = 7;
21216let foo = 8;
21217let foo = 9;
21218let foo = 10;
21219let foo = 11;
21220let foo = 12;
21221let foo = 13;
21222let foo = 14;
21223let foo = 15;"#,
21224 );
21225
21226 cx.update_editor(|e, window, cx| {
21227 assert_eq!(
21228 e.next_scroll_position,
21229 NextScrollCursorCenterTopBottom::Center,
21230 "Default next scroll direction is center",
21231 );
21232
21233 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21234 assert_eq!(
21235 e.next_scroll_position,
21236 NextScrollCursorCenterTopBottom::Top,
21237 "After center, next scroll direction should be top",
21238 );
21239
21240 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21241 assert_eq!(
21242 e.next_scroll_position,
21243 NextScrollCursorCenterTopBottom::Bottom,
21244 "After top, next scroll direction should be bottom",
21245 );
21246
21247 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21248 assert_eq!(
21249 e.next_scroll_position,
21250 NextScrollCursorCenterTopBottom::Center,
21251 "After bottom, scrolling should start over",
21252 );
21253
21254 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21255 assert_eq!(
21256 e.next_scroll_position,
21257 NextScrollCursorCenterTopBottom::Top,
21258 "Scrolling continues if retriggered fast enough"
21259 );
21260 });
21261
21262 cx.executor()
21263 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21264 cx.executor().run_until_parked();
21265 cx.update_editor(|e, _, _| {
21266 assert_eq!(
21267 e.next_scroll_position,
21268 NextScrollCursorCenterTopBottom::Center,
21269 "If scrolling is not triggered fast enough, it should reset"
21270 );
21271 });
21272}
21273
21274#[gpui::test]
21275async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21276 init_test(cx, |_| {});
21277 let mut cx = EditorLspTestContext::new_rust(
21278 lsp::ServerCapabilities {
21279 definition_provider: Some(lsp::OneOf::Left(true)),
21280 references_provider: Some(lsp::OneOf::Left(true)),
21281 ..lsp::ServerCapabilities::default()
21282 },
21283 cx,
21284 )
21285 .await;
21286
21287 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21288 let go_to_definition = cx
21289 .lsp
21290 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21291 move |params, _| async move {
21292 if empty_go_to_definition {
21293 Ok(None)
21294 } else {
21295 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21296 uri: params.text_document_position_params.text_document.uri,
21297 range: lsp::Range::new(
21298 lsp::Position::new(4, 3),
21299 lsp::Position::new(4, 6),
21300 ),
21301 })))
21302 }
21303 },
21304 );
21305 let references = cx
21306 .lsp
21307 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21308 Ok(Some(vec![lsp::Location {
21309 uri: params.text_document_position.text_document.uri,
21310 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21311 }]))
21312 });
21313 (go_to_definition, references)
21314 };
21315
21316 cx.set_state(
21317 &r#"fn one() {
21318 let mut a = ˇtwo();
21319 }
21320
21321 fn two() {}"#
21322 .unindent(),
21323 );
21324 set_up_lsp_handlers(false, &mut cx);
21325 let navigated = cx
21326 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21327 .await
21328 .expect("Failed to navigate to definition");
21329 assert_eq!(
21330 navigated,
21331 Navigated::Yes,
21332 "Should have navigated to definition from the GetDefinition response"
21333 );
21334 cx.assert_editor_state(
21335 &r#"fn one() {
21336 let mut a = two();
21337 }
21338
21339 fn «twoˇ»() {}"#
21340 .unindent(),
21341 );
21342
21343 let editors = cx.update_workspace(|workspace, _, cx| {
21344 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21345 });
21346 cx.update_editor(|_, _, test_editor_cx| {
21347 assert_eq!(
21348 editors.len(),
21349 1,
21350 "Initially, only one, test, editor should be open in the workspace"
21351 );
21352 assert_eq!(
21353 test_editor_cx.entity(),
21354 editors.last().expect("Asserted len is 1").clone()
21355 );
21356 });
21357
21358 set_up_lsp_handlers(true, &mut cx);
21359 let navigated = cx
21360 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21361 .await
21362 .expect("Failed to navigate to lookup references");
21363 assert_eq!(
21364 navigated,
21365 Navigated::Yes,
21366 "Should have navigated to references as a fallback after empty GoToDefinition response"
21367 );
21368 // We should not change the selections in the existing file,
21369 // if opening another milti buffer with the references
21370 cx.assert_editor_state(
21371 &r#"fn one() {
21372 let mut a = two();
21373 }
21374
21375 fn «twoˇ»() {}"#
21376 .unindent(),
21377 );
21378 let editors = cx.update_workspace(|workspace, _, cx| {
21379 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21380 });
21381 cx.update_editor(|_, _, test_editor_cx| {
21382 assert_eq!(
21383 editors.len(),
21384 2,
21385 "After falling back to references search, we open a new editor with the results"
21386 );
21387 let references_fallback_text = editors
21388 .into_iter()
21389 .find(|new_editor| *new_editor != test_editor_cx.entity())
21390 .expect("Should have one non-test editor now")
21391 .read(test_editor_cx)
21392 .text(test_editor_cx);
21393 assert_eq!(
21394 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21395 "Should use the range from the references response and not the GoToDefinition one"
21396 );
21397 });
21398}
21399
21400#[gpui::test]
21401async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21402 init_test(cx, |_| {});
21403 cx.update(|cx| {
21404 let mut editor_settings = EditorSettings::get_global(cx).clone();
21405 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21406 EditorSettings::override_global(editor_settings, cx);
21407 });
21408 let mut cx = EditorLspTestContext::new_rust(
21409 lsp::ServerCapabilities {
21410 definition_provider: Some(lsp::OneOf::Left(true)),
21411 references_provider: Some(lsp::OneOf::Left(true)),
21412 ..lsp::ServerCapabilities::default()
21413 },
21414 cx,
21415 )
21416 .await;
21417 let original_state = r#"fn one() {
21418 let mut a = ˇtwo();
21419 }
21420
21421 fn two() {}"#
21422 .unindent();
21423 cx.set_state(&original_state);
21424
21425 let mut go_to_definition = cx
21426 .lsp
21427 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21428 move |_, _| async move { Ok(None) },
21429 );
21430 let _references = cx
21431 .lsp
21432 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21433 panic!("Should not call for references with no go to definition fallback")
21434 });
21435
21436 let navigated = cx
21437 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21438 .await
21439 .expect("Failed to navigate to lookup references");
21440 go_to_definition
21441 .next()
21442 .await
21443 .expect("Should have called the go_to_definition handler");
21444
21445 assert_eq!(
21446 navigated,
21447 Navigated::No,
21448 "Should have navigated to references as a fallback after empty GoToDefinition response"
21449 );
21450 cx.assert_editor_state(&original_state);
21451 let editors = cx.update_workspace(|workspace, _, cx| {
21452 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21453 });
21454 cx.update_editor(|_, _, _| {
21455 assert_eq!(
21456 editors.len(),
21457 1,
21458 "After unsuccessful fallback, no other editor should have been opened"
21459 );
21460 });
21461}
21462
21463#[gpui::test]
21464async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21465 init_test(cx, |_| {});
21466 let mut cx = EditorLspTestContext::new_rust(
21467 lsp::ServerCapabilities {
21468 references_provider: Some(lsp::OneOf::Left(true)),
21469 ..lsp::ServerCapabilities::default()
21470 },
21471 cx,
21472 )
21473 .await;
21474
21475 cx.set_state(
21476 &r#"
21477 fn one() {
21478 let mut a = two();
21479 }
21480
21481 fn ˇtwo() {}"#
21482 .unindent(),
21483 );
21484 cx.lsp
21485 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21486 Ok(Some(vec![
21487 lsp::Location {
21488 uri: params.text_document_position.text_document.uri.clone(),
21489 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21490 },
21491 lsp::Location {
21492 uri: params.text_document_position.text_document.uri,
21493 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21494 },
21495 ]))
21496 });
21497 let navigated = cx
21498 .update_editor(|editor, window, cx| {
21499 editor.find_all_references(&FindAllReferences, window, cx)
21500 })
21501 .unwrap()
21502 .await
21503 .expect("Failed to navigate to references");
21504 assert_eq!(
21505 navigated,
21506 Navigated::Yes,
21507 "Should have navigated to references from the FindAllReferences response"
21508 );
21509 cx.assert_editor_state(
21510 &r#"fn one() {
21511 let mut a = two();
21512 }
21513
21514 fn ˇtwo() {}"#
21515 .unindent(),
21516 );
21517
21518 let editors = cx.update_workspace(|workspace, _, cx| {
21519 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21520 });
21521 cx.update_editor(|_, _, _| {
21522 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21523 });
21524
21525 cx.set_state(
21526 &r#"fn one() {
21527 let mut a = ˇtwo();
21528 }
21529
21530 fn two() {}"#
21531 .unindent(),
21532 );
21533 let navigated = cx
21534 .update_editor(|editor, window, cx| {
21535 editor.find_all_references(&FindAllReferences, window, cx)
21536 })
21537 .unwrap()
21538 .await
21539 .expect("Failed to navigate to references");
21540 assert_eq!(
21541 navigated,
21542 Navigated::Yes,
21543 "Should have navigated to references from the FindAllReferences response"
21544 );
21545 cx.assert_editor_state(
21546 &r#"fn one() {
21547 let mut a = ˇtwo();
21548 }
21549
21550 fn two() {}"#
21551 .unindent(),
21552 );
21553 let editors = cx.update_workspace(|workspace, _, cx| {
21554 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21555 });
21556 cx.update_editor(|_, _, _| {
21557 assert_eq!(
21558 editors.len(),
21559 2,
21560 "should have re-used the previous multibuffer"
21561 );
21562 });
21563
21564 cx.set_state(
21565 &r#"fn one() {
21566 let mut a = ˇtwo();
21567 }
21568 fn three() {}
21569 fn two() {}"#
21570 .unindent(),
21571 );
21572 cx.lsp
21573 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21574 Ok(Some(vec![
21575 lsp::Location {
21576 uri: params.text_document_position.text_document.uri.clone(),
21577 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21578 },
21579 lsp::Location {
21580 uri: params.text_document_position.text_document.uri,
21581 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21582 },
21583 ]))
21584 });
21585 let navigated = cx
21586 .update_editor(|editor, window, cx| {
21587 editor.find_all_references(&FindAllReferences, window, cx)
21588 })
21589 .unwrap()
21590 .await
21591 .expect("Failed to navigate to references");
21592 assert_eq!(
21593 navigated,
21594 Navigated::Yes,
21595 "Should have navigated to references from the FindAllReferences response"
21596 );
21597 cx.assert_editor_state(
21598 &r#"fn one() {
21599 let mut a = ˇtwo();
21600 }
21601 fn three() {}
21602 fn two() {}"#
21603 .unindent(),
21604 );
21605 let editors = cx.update_workspace(|workspace, _, cx| {
21606 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21607 });
21608 cx.update_editor(|_, _, _| {
21609 assert_eq!(
21610 editors.len(),
21611 3,
21612 "should have used a new multibuffer as offsets changed"
21613 );
21614 });
21615}
21616#[gpui::test]
21617async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21618 init_test(cx, |_| {});
21619
21620 let language = Arc::new(Language::new(
21621 LanguageConfig::default(),
21622 Some(tree_sitter_rust::LANGUAGE.into()),
21623 ));
21624
21625 let text = r#"
21626 #[cfg(test)]
21627 mod tests() {
21628 #[test]
21629 fn runnable_1() {
21630 let a = 1;
21631 }
21632
21633 #[test]
21634 fn runnable_2() {
21635 let a = 1;
21636 let b = 2;
21637 }
21638 }
21639 "#
21640 .unindent();
21641
21642 let fs = FakeFs::new(cx.executor());
21643 fs.insert_file("/file.rs", Default::default()).await;
21644
21645 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21646 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21647 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21648 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21649 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21650
21651 let editor = cx.new_window_entity(|window, cx| {
21652 Editor::new(
21653 EditorMode::full(),
21654 multi_buffer,
21655 Some(project.clone()),
21656 window,
21657 cx,
21658 )
21659 });
21660
21661 editor.update_in(cx, |editor, window, cx| {
21662 let snapshot = editor.buffer().read(cx).snapshot(cx);
21663 editor.tasks.insert(
21664 (buffer.read(cx).remote_id(), 3),
21665 RunnableTasks {
21666 templates: vec![],
21667 offset: snapshot.anchor_before(43),
21668 column: 0,
21669 extra_variables: HashMap::default(),
21670 context_range: BufferOffset(43)..BufferOffset(85),
21671 },
21672 );
21673 editor.tasks.insert(
21674 (buffer.read(cx).remote_id(), 8),
21675 RunnableTasks {
21676 templates: vec![],
21677 offset: snapshot.anchor_before(86),
21678 column: 0,
21679 extra_variables: HashMap::default(),
21680 context_range: BufferOffset(86)..BufferOffset(191),
21681 },
21682 );
21683
21684 // Test finding task when cursor is inside function body
21685 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21686 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21687 });
21688 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21689 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21690
21691 // Test finding task when cursor is on function name
21692 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21693 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21694 });
21695 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21696 assert_eq!(row, 8, "Should find task when cursor is on function name");
21697 });
21698}
21699
21700#[gpui::test]
21701async fn test_folding_buffers(cx: &mut TestAppContext) {
21702 init_test(cx, |_| {});
21703
21704 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21705 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21706 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21707
21708 let fs = FakeFs::new(cx.executor());
21709 fs.insert_tree(
21710 path!("/a"),
21711 json!({
21712 "first.rs": sample_text_1,
21713 "second.rs": sample_text_2,
21714 "third.rs": sample_text_3,
21715 }),
21716 )
21717 .await;
21718 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21719 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21720 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21721 let worktree = project.update(cx, |project, cx| {
21722 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21723 assert_eq!(worktrees.len(), 1);
21724 worktrees.pop().unwrap()
21725 });
21726 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21727
21728 let buffer_1 = project
21729 .update(cx, |project, cx| {
21730 project.open_buffer((worktree_id, "first.rs"), cx)
21731 })
21732 .await
21733 .unwrap();
21734 let buffer_2 = project
21735 .update(cx, |project, cx| {
21736 project.open_buffer((worktree_id, "second.rs"), cx)
21737 })
21738 .await
21739 .unwrap();
21740 let buffer_3 = project
21741 .update(cx, |project, cx| {
21742 project.open_buffer((worktree_id, "third.rs"), cx)
21743 })
21744 .await
21745 .unwrap();
21746
21747 let multi_buffer = cx.new(|cx| {
21748 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21749 multi_buffer.push_excerpts(
21750 buffer_1.clone(),
21751 [
21752 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21753 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21754 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21755 ],
21756 cx,
21757 );
21758 multi_buffer.push_excerpts(
21759 buffer_2.clone(),
21760 [
21761 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21762 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21763 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21764 ],
21765 cx,
21766 );
21767 multi_buffer.push_excerpts(
21768 buffer_3.clone(),
21769 [
21770 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21771 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21772 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21773 ],
21774 cx,
21775 );
21776 multi_buffer
21777 });
21778 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21779 Editor::new(
21780 EditorMode::full(),
21781 multi_buffer.clone(),
21782 Some(project.clone()),
21783 window,
21784 cx,
21785 )
21786 });
21787
21788 assert_eq!(
21789 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21790 "\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",
21791 );
21792
21793 multi_buffer_editor.update(cx, |editor, cx| {
21794 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21795 });
21796 assert_eq!(
21797 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21798 "\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",
21799 "After folding the first buffer, its text should not be displayed"
21800 );
21801
21802 multi_buffer_editor.update(cx, |editor, cx| {
21803 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21804 });
21805 assert_eq!(
21806 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21807 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21808 "After folding the second buffer, its text should not be displayed"
21809 );
21810
21811 multi_buffer_editor.update(cx, |editor, cx| {
21812 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21813 });
21814 assert_eq!(
21815 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21816 "\n\n\n\n\n",
21817 "After folding the third buffer, its text should not be displayed"
21818 );
21819
21820 // Emulate selection inside the fold logic, that should work
21821 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21822 editor
21823 .snapshot(window, cx)
21824 .next_line_boundary(Point::new(0, 4));
21825 });
21826
21827 multi_buffer_editor.update(cx, |editor, cx| {
21828 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21829 });
21830 assert_eq!(
21831 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21832 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21833 "After unfolding the second buffer, its text should be displayed"
21834 );
21835
21836 // Typing inside of buffer 1 causes that buffer to be unfolded.
21837 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21838 assert_eq!(
21839 multi_buffer
21840 .read(cx)
21841 .snapshot(cx)
21842 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21843 .collect::<String>(),
21844 "bbbb"
21845 );
21846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21847 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21848 });
21849 editor.handle_input("B", window, cx);
21850 });
21851
21852 assert_eq!(
21853 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21854 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21855 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21856 );
21857
21858 multi_buffer_editor.update(cx, |editor, cx| {
21859 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21860 });
21861 assert_eq!(
21862 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21863 "\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",
21864 "After unfolding the all buffers, all original text should be displayed"
21865 );
21866}
21867
21868#[gpui::test]
21869async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21870 init_test(cx, |_| {});
21871
21872 let sample_text_1 = "1111\n2222\n3333".to_string();
21873 let sample_text_2 = "4444\n5555\n6666".to_string();
21874 let sample_text_3 = "7777\n8888\n9999".to_string();
21875
21876 let fs = FakeFs::new(cx.executor());
21877 fs.insert_tree(
21878 path!("/a"),
21879 json!({
21880 "first.rs": sample_text_1,
21881 "second.rs": sample_text_2,
21882 "third.rs": sample_text_3,
21883 }),
21884 )
21885 .await;
21886 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21887 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21888 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21889 let worktree = project.update(cx, |project, cx| {
21890 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21891 assert_eq!(worktrees.len(), 1);
21892 worktrees.pop().unwrap()
21893 });
21894 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21895
21896 let buffer_1 = project
21897 .update(cx, |project, cx| {
21898 project.open_buffer((worktree_id, "first.rs"), cx)
21899 })
21900 .await
21901 .unwrap();
21902 let buffer_2 = project
21903 .update(cx, |project, cx| {
21904 project.open_buffer((worktree_id, "second.rs"), cx)
21905 })
21906 .await
21907 .unwrap();
21908 let buffer_3 = project
21909 .update(cx, |project, cx| {
21910 project.open_buffer((worktree_id, "third.rs"), cx)
21911 })
21912 .await
21913 .unwrap();
21914
21915 let multi_buffer = cx.new(|cx| {
21916 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21917 multi_buffer.push_excerpts(
21918 buffer_1.clone(),
21919 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21920 cx,
21921 );
21922 multi_buffer.push_excerpts(
21923 buffer_2.clone(),
21924 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21925 cx,
21926 );
21927 multi_buffer.push_excerpts(
21928 buffer_3.clone(),
21929 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21930 cx,
21931 );
21932 multi_buffer
21933 });
21934
21935 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21936 Editor::new(
21937 EditorMode::full(),
21938 multi_buffer,
21939 Some(project.clone()),
21940 window,
21941 cx,
21942 )
21943 });
21944
21945 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21946 assert_eq!(
21947 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21948 full_text,
21949 );
21950
21951 multi_buffer_editor.update(cx, |editor, cx| {
21952 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21953 });
21954 assert_eq!(
21955 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21956 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21957 "After folding the first buffer, its text should not be displayed"
21958 );
21959
21960 multi_buffer_editor.update(cx, |editor, cx| {
21961 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21962 });
21963
21964 assert_eq!(
21965 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21966 "\n\n\n\n\n\n7777\n8888\n9999",
21967 "After folding the second buffer, its text should not be displayed"
21968 );
21969
21970 multi_buffer_editor.update(cx, |editor, cx| {
21971 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21972 });
21973 assert_eq!(
21974 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21975 "\n\n\n\n\n",
21976 "After folding the third buffer, its text should not be displayed"
21977 );
21978
21979 multi_buffer_editor.update(cx, |editor, cx| {
21980 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21981 });
21982 assert_eq!(
21983 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21984 "\n\n\n\n4444\n5555\n6666\n\n",
21985 "After unfolding the second buffer, its text should be displayed"
21986 );
21987
21988 multi_buffer_editor.update(cx, |editor, cx| {
21989 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21990 });
21991 assert_eq!(
21992 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21993 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21994 "After unfolding the first buffer, its text should be displayed"
21995 );
21996
21997 multi_buffer_editor.update(cx, |editor, cx| {
21998 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21999 });
22000 assert_eq!(
22001 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22002 full_text,
22003 "After unfolding all buffers, all original text should be displayed"
22004 );
22005}
22006
22007#[gpui::test]
22008async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22009 init_test(cx, |_| {});
22010
22011 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22012
22013 let fs = FakeFs::new(cx.executor());
22014 fs.insert_tree(
22015 path!("/a"),
22016 json!({
22017 "main.rs": sample_text,
22018 }),
22019 )
22020 .await;
22021 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22022 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22023 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22024 let worktree = project.update(cx, |project, cx| {
22025 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22026 assert_eq!(worktrees.len(), 1);
22027 worktrees.pop().unwrap()
22028 });
22029 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22030
22031 let buffer_1 = project
22032 .update(cx, |project, cx| {
22033 project.open_buffer((worktree_id, "main.rs"), cx)
22034 })
22035 .await
22036 .unwrap();
22037
22038 let multi_buffer = cx.new(|cx| {
22039 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22040 multi_buffer.push_excerpts(
22041 buffer_1.clone(),
22042 [ExcerptRange::new(
22043 Point::new(0, 0)
22044 ..Point::new(
22045 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22046 0,
22047 ),
22048 )],
22049 cx,
22050 );
22051 multi_buffer
22052 });
22053 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22054 Editor::new(
22055 EditorMode::full(),
22056 multi_buffer,
22057 Some(project.clone()),
22058 window,
22059 cx,
22060 )
22061 });
22062
22063 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22064 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22065 enum TestHighlight {}
22066 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22067 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22068 editor.highlight_text::<TestHighlight>(
22069 vec![highlight_range.clone()],
22070 HighlightStyle::color(Hsla::green()),
22071 cx,
22072 );
22073 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22074 s.select_ranges(Some(highlight_range))
22075 });
22076 });
22077
22078 let full_text = format!("\n\n{sample_text}");
22079 assert_eq!(
22080 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22081 full_text,
22082 );
22083}
22084
22085#[gpui::test]
22086async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22087 init_test(cx, |_| {});
22088 cx.update(|cx| {
22089 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22090 "keymaps/default-linux.json",
22091 cx,
22092 )
22093 .unwrap();
22094 cx.bind_keys(default_key_bindings);
22095 });
22096
22097 let (editor, cx) = cx.add_window_view(|window, cx| {
22098 let multi_buffer = MultiBuffer::build_multi(
22099 [
22100 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22101 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22102 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22103 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22104 ],
22105 cx,
22106 );
22107 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22108
22109 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22110 // fold all but the second buffer, so that we test navigating between two
22111 // adjacent folded buffers, as well as folded buffers at the start and
22112 // end the multibuffer
22113 editor.fold_buffer(buffer_ids[0], cx);
22114 editor.fold_buffer(buffer_ids[2], cx);
22115 editor.fold_buffer(buffer_ids[3], cx);
22116
22117 editor
22118 });
22119 cx.simulate_resize(size(px(1000.), px(1000.)));
22120
22121 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22122 cx.assert_excerpts_with_selections(indoc! {"
22123 [EXCERPT]
22124 ˇ[FOLDED]
22125 [EXCERPT]
22126 a1
22127 b1
22128 [EXCERPT]
22129 [FOLDED]
22130 [EXCERPT]
22131 [FOLDED]
22132 "
22133 });
22134 cx.simulate_keystroke("down");
22135 cx.assert_excerpts_with_selections(indoc! {"
22136 [EXCERPT]
22137 [FOLDED]
22138 [EXCERPT]
22139 ˇa1
22140 b1
22141 [EXCERPT]
22142 [FOLDED]
22143 [EXCERPT]
22144 [FOLDED]
22145 "
22146 });
22147 cx.simulate_keystroke("down");
22148 cx.assert_excerpts_with_selections(indoc! {"
22149 [EXCERPT]
22150 [FOLDED]
22151 [EXCERPT]
22152 a1
22153 ˇb1
22154 [EXCERPT]
22155 [FOLDED]
22156 [EXCERPT]
22157 [FOLDED]
22158 "
22159 });
22160 cx.simulate_keystroke("down");
22161 cx.assert_excerpts_with_selections(indoc! {"
22162 [EXCERPT]
22163 [FOLDED]
22164 [EXCERPT]
22165 a1
22166 b1
22167 ˇ[EXCERPT]
22168 [FOLDED]
22169 [EXCERPT]
22170 [FOLDED]
22171 "
22172 });
22173 cx.simulate_keystroke("down");
22174 cx.assert_excerpts_with_selections(indoc! {"
22175 [EXCERPT]
22176 [FOLDED]
22177 [EXCERPT]
22178 a1
22179 b1
22180 [EXCERPT]
22181 ˇ[FOLDED]
22182 [EXCERPT]
22183 [FOLDED]
22184 "
22185 });
22186 for _ in 0..5 {
22187 cx.simulate_keystroke("down");
22188 cx.assert_excerpts_with_selections(indoc! {"
22189 [EXCERPT]
22190 [FOLDED]
22191 [EXCERPT]
22192 a1
22193 b1
22194 [EXCERPT]
22195 [FOLDED]
22196 [EXCERPT]
22197 ˇ[FOLDED]
22198 "
22199 });
22200 }
22201
22202 cx.simulate_keystroke("up");
22203 cx.assert_excerpts_with_selections(indoc! {"
22204 [EXCERPT]
22205 [FOLDED]
22206 [EXCERPT]
22207 a1
22208 b1
22209 [EXCERPT]
22210 ˇ[FOLDED]
22211 [EXCERPT]
22212 [FOLDED]
22213 "
22214 });
22215 cx.simulate_keystroke("up");
22216 cx.assert_excerpts_with_selections(indoc! {"
22217 [EXCERPT]
22218 [FOLDED]
22219 [EXCERPT]
22220 a1
22221 b1
22222 ˇ[EXCERPT]
22223 [FOLDED]
22224 [EXCERPT]
22225 [FOLDED]
22226 "
22227 });
22228 cx.simulate_keystroke("up");
22229 cx.assert_excerpts_with_selections(indoc! {"
22230 [EXCERPT]
22231 [FOLDED]
22232 [EXCERPT]
22233 a1
22234 ˇb1
22235 [EXCERPT]
22236 [FOLDED]
22237 [EXCERPT]
22238 [FOLDED]
22239 "
22240 });
22241 cx.simulate_keystroke("up");
22242 cx.assert_excerpts_with_selections(indoc! {"
22243 [EXCERPT]
22244 [FOLDED]
22245 [EXCERPT]
22246 ˇa1
22247 b1
22248 [EXCERPT]
22249 [FOLDED]
22250 [EXCERPT]
22251 [FOLDED]
22252 "
22253 });
22254 for _ in 0..5 {
22255 cx.simulate_keystroke("up");
22256 cx.assert_excerpts_with_selections(indoc! {"
22257 [EXCERPT]
22258 ˇ[FOLDED]
22259 [EXCERPT]
22260 a1
22261 b1
22262 [EXCERPT]
22263 [FOLDED]
22264 [EXCERPT]
22265 [FOLDED]
22266 "
22267 });
22268 }
22269}
22270
22271#[gpui::test]
22272async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22273 init_test(cx, |_| {});
22274
22275 // Simple insertion
22276 assert_highlighted_edits(
22277 "Hello, world!",
22278 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22279 true,
22280 cx,
22281 |highlighted_edits, cx| {
22282 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22283 assert_eq!(highlighted_edits.highlights.len(), 1);
22284 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22285 assert_eq!(
22286 highlighted_edits.highlights[0].1.background_color,
22287 Some(cx.theme().status().created_background)
22288 );
22289 },
22290 )
22291 .await;
22292
22293 // Replacement
22294 assert_highlighted_edits(
22295 "This is a test.",
22296 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22297 false,
22298 cx,
22299 |highlighted_edits, cx| {
22300 assert_eq!(highlighted_edits.text, "That is a test.");
22301 assert_eq!(highlighted_edits.highlights.len(), 1);
22302 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22303 assert_eq!(
22304 highlighted_edits.highlights[0].1.background_color,
22305 Some(cx.theme().status().created_background)
22306 );
22307 },
22308 )
22309 .await;
22310
22311 // Multiple edits
22312 assert_highlighted_edits(
22313 "Hello, world!",
22314 vec![
22315 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22316 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22317 ],
22318 false,
22319 cx,
22320 |highlighted_edits, cx| {
22321 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22322 assert_eq!(highlighted_edits.highlights.len(), 2);
22323 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22324 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22325 assert_eq!(
22326 highlighted_edits.highlights[0].1.background_color,
22327 Some(cx.theme().status().created_background)
22328 );
22329 assert_eq!(
22330 highlighted_edits.highlights[1].1.background_color,
22331 Some(cx.theme().status().created_background)
22332 );
22333 },
22334 )
22335 .await;
22336
22337 // Multiple lines with edits
22338 assert_highlighted_edits(
22339 "First line\nSecond line\nThird line\nFourth line",
22340 vec![
22341 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22342 (
22343 Point::new(2, 0)..Point::new(2, 10),
22344 "New third line".to_string(),
22345 ),
22346 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22347 ],
22348 false,
22349 cx,
22350 |highlighted_edits, cx| {
22351 assert_eq!(
22352 highlighted_edits.text,
22353 "Second modified\nNew third line\nFourth updated line"
22354 );
22355 assert_eq!(highlighted_edits.highlights.len(), 3);
22356 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22357 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22358 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22359 for highlight in &highlighted_edits.highlights {
22360 assert_eq!(
22361 highlight.1.background_color,
22362 Some(cx.theme().status().created_background)
22363 );
22364 }
22365 },
22366 )
22367 .await;
22368}
22369
22370#[gpui::test]
22371async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22372 init_test(cx, |_| {});
22373
22374 // Deletion
22375 assert_highlighted_edits(
22376 "Hello, world!",
22377 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22378 true,
22379 cx,
22380 |highlighted_edits, cx| {
22381 assert_eq!(highlighted_edits.text, "Hello, world!");
22382 assert_eq!(highlighted_edits.highlights.len(), 1);
22383 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22384 assert_eq!(
22385 highlighted_edits.highlights[0].1.background_color,
22386 Some(cx.theme().status().deleted_background)
22387 );
22388 },
22389 )
22390 .await;
22391
22392 // Insertion
22393 assert_highlighted_edits(
22394 "Hello, world!",
22395 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22396 true,
22397 cx,
22398 |highlighted_edits, cx| {
22399 assert_eq!(highlighted_edits.highlights.len(), 1);
22400 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22401 assert_eq!(
22402 highlighted_edits.highlights[0].1.background_color,
22403 Some(cx.theme().status().created_background)
22404 );
22405 },
22406 )
22407 .await;
22408}
22409
22410async fn assert_highlighted_edits(
22411 text: &str,
22412 edits: Vec<(Range<Point>, String)>,
22413 include_deletions: bool,
22414 cx: &mut TestAppContext,
22415 assertion_fn: impl Fn(HighlightedText, &App),
22416) {
22417 let window = cx.add_window(|window, cx| {
22418 let buffer = MultiBuffer::build_simple(text, cx);
22419 Editor::new(EditorMode::full(), buffer, None, window, cx)
22420 });
22421 let cx = &mut VisualTestContext::from_window(*window, cx);
22422
22423 let (buffer, snapshot) = window
22424 .update(cx, |editor, _window, cx| {
22425 (
22426 editor.buffer().clone(),
22427 editor.buffer().read(cx).snapshot(cx),
22428 )
22429 })
22430 .unwrap();
22431
22432 let edits = edits
22433 .into_iter()
22434 .map(|(range, edit)| {
22435 (
22436 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22437 edit,
22438 )
22439 })
22440 .collect::<Vec<_>>();
22441
22442 let text_anchor_edits = edits
22443 .clone()
22444 .into_iter()
22445 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22446 .collect::<Vec<_>>();
22447
22448 let edit_preview = window
22449 .update(cx, |_, _window, cx| {
22450 buffer
22451 .read(cx)
22452 .as_singleton()
22453 .unwrap()
22454 .read(cx)
22455 .preview_edits(text_anchor_edits.into(), cx)
22456 })
22457 .unwrap()
22458 .await;
22459
22460 cx.update(|_window, cx| {
22461 let highlighted_edits = edit_prediction_edit_text(
22462 snapshot.as_singleton().unwrap().2,
22463 &edits,
22464 &edit_preview,
22465 include_deletions,
22466 cx,
22467 );
22468 assertion_fn(highlighted_edits, cx)
22469 });
22470}
22471
22472#[track_caller]
22473fn assert_breakpoint(
22474 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22475 path: &Arc<Path>,
22476 expected: Vec<(u32, Breakpoint)>,
22477) {
22478 if expected.is_empty() {
22479 assert!(!breakpoints.contains_key(path), "{}", path.display());
22480 } else {
22481 let mut breakpoint = breakpoints
22482 .get(path)
22483 .unwrap()
22484 .iter()
22485 .map(|breakpoint| {
22486 (
22487 breakpoint.row,
22488 Breakpoint {
22489 message: breakpoint.message.clone(),
22490 state: breakpoint.state,
22491 condition: breakpoint.condition.clone(),
22492 hit_condition: breakpoint.hit_condition.clone(),
22493 },
22494 )
22495 })
22496 .collect::<Vec<_>>();
22497
22498 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22499
22500 assert_eq!(expected, breakpoint);
22501 }
22502}
22503
22504fn add_log_breakpoint_at_cursor(
22505 editor: &mut Editor,
22506 log_message: &str,
22507 window: &mut Window,
22508 cx: &mut Context<Editor>,
22509) {
22510 let (anchor, bp) = editor
22511 .breakpoints_at_cursors(window, cx)
22512 .first()
22513 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22514 .unwrap_or_else(|| {
22515 let cursor_position: Point = editor.selections.newest(cx).head();
22516
22517 let breakpoint_position = editor
22518 .snapshot(window, cx)
22519 .display_snapshot
22520 .buffer_snapshot
22521 .anchor_before(Point::new(cursor_position.row, 0));
22522
22523 (breakpoint_position, Breakpoint::new_log(log_message))
22524 });
22525
22526 editor.edit_breakpoint_at_anchor(
22527 anchor,
22528 bp,
22529 BreakpointEditAction::EditLogMessage(log_message.into()),
22530 cx,
22531 );
22532}
22533
22534#[gpui::test]
22535async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22536 init_test(cx, |_| {});
22537
22538 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22539 let fs = FakeFs::new(cx.executor());
22540 fs.insert_tree(
22541 path!("/a"),
22542 json!({
22543 "main.rs": sample_text,
22544 }),
22545 )
22546 .await;
22547 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22548 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22549 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22550
22551 let fs = FakeFs::new(cx.executor());
22552 fs.insert_tree(
22553 path!("/a"),
22554 json!({
22555 "main.rs": sample_text,
22556 }),
22557 )
22558 .await;
22559 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22560 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22561 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22562 let worktree_id = workspace
22563 .update(cx, |workspace, _window, cx| {
22564 workspace.project().update(cx, |project, cx| {
22565 project.worktrees(cx).next().unwrap().read(cx).id()
22566 })
22567 })
22568 .unwrap();
22569
22570 let buffer = project
22571 .update(cx, |project, cx| {
22572 project.open_buffer((worktree_id, "main.rs"), cx)
22573 })
22574 .await
22575 .unwrap();
22576
22577 let (editor, cx) = cx.add_window_view(|window, cx| {
22578 Editor::new(
22579 EditorMode::full(),
22580 MultiBuffer::build_from_buffer(buffer, cx),
22581 Some(project.clone()),
22582 window,
22583 cx,
22584 )
22585 });
22586
22587 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22588 let abs_path = project.read_with(cx, |project, cx| {
22589 project
22590 .absolute_path(&project_path, cx)
22591 .map(Arc::from)
22592 .unwrap()
22593 });
22594
22595 // assert we can add breakpoint on the first line
22596 editor.update_in(cx, |editor, window, cx| {
22597 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22598 editor.move_to_end(&MoveToEnd, window, cx);
22599 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22600 });
22601
22602 let breakpoints = editor.update(cx, |editor, cx| {
22603 editor
22604 .breakpoint_store()
22605 .as_ref()
22606 .unwrap()
22607 .read(cx)
22608 .all_source_breakpoints(cx)
22609 });
22610
22611 assert_eq!(1, breakpoints.len());
22612 assert_breakpoint(
22613 &breakpoints,
22614 &abs_path,
22615 vec![
22616 (0, Breakpoint::new_standard()),
22617 (3, Breakpoint::new_standard()),
22618 ],
22619 );
22620
22621 editor.update_in(cx, |editor, window, cx| {
22622 editor.move_to_beginning(&MoveToBeginning, window, cx);
22623 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22624 });
22625
22626 let breakpoints = editor.update(cx, |editor, cx| {
22627 editor
22628 .breakpoint_store()
22629 .as_ref()
22630 .unwrap()
22631 .read(cx)
22632 .all_source_breakpoints(cx)
22633 });
22634
22635 assert_eq!(1, breakpoints.len());
22636 assert_breakpoint(
22637 &breakpoints,
22638 &abs_path,
22639 vec![(3, Breakpoint::new_standard())],
22640 );
22641
22642 editor.update_in(cx, |editor, window, cx| {
22643 editor.move_to_end(&MoveToEnd, window, cx);
22644 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22645 });
22646
22647 let breakpoints = editor.update(cx, |editor, cx| {
22648 editor
22649 .breakpoint_store()
22650 .as_ref()
22651 .unwrap()
22652 .read(cx)
22653 .all_source_breakpoints(cx)
22654 });
22655
22656 assert_eq!(0, breakpoints.len());
22657 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22658}
22659
22660#[gpui::test]
22661async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22662 init_test(cx, |_| {});
22663
22664 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22665
22666 let fs = FakeFs::new(cx.executor());
22667 fs.insert_tree(
22668 path!("/a"),
22669 json!({
22670 "main.rs": sample_text,
22671 }),
22672 )
22673 .await;
22674 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22675 let (workspace, cx) =
22676 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22677
22678 let worktree_id = workspace.update(cx, |workspace, cx| {
22679 workspace.project().update(cx, |project, cx| {
22680 project.worktrees(cx).next().unwrap().read(cx).id()
22681 })
22682 });
22683
22684 let buffer = project
22685 .update(cx, |project, cx| {
22686 project.open_buffer((worktree_id, "main.rs"), cx)
22687 })
22688 .await
22689 .unwrap();
22690
22691 let (editor, cx) = cx.add_window_view(|window, cx| {
22692 Editor::new(
22693 EditorMode::full(),
22694 MultiBuffer::build_from_buffer(buffer, cx),
22695 Some(project.clone()),
22696 window,
22697 cx,
22698 )
22699 });
22700
22701 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22702 let abs_path = project.read_with(cx, |project, cx| {
22703 project
22704 .absolute_path(&project_path, cx)
22705 .map(Arc::from)
22706 .unwrap()
22707 });
22708
22709 editor.update_in(cx, |editor, window, cx| {
22710 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22711 });
22712
22713 let breakpoints = editor.update(cx, |editor, cx| {
22714 editor
22715 .breakpoint_store()
22716 .as_ref()
22717 .unwrap()
22718 .read(cx)
22719 .all_source_breakpoints(cx)
22720 });
22721
22722 assert_breakpoint(
22723 &breakpoints,
22724 &abs_path,
22725 vec![(0, Breakpoint::new_log("hello world"))],
22726 );
22727
22728 // Removing a log message from a log breakpoint should remove it
22729 editor.update_in(cx, |editor, window, cx| {
22730 add_log_breakpoint_at_cursor(editor, "", window, cx);
22731 });
22732
22733 let breakpoints = editor.update(cx, |editor, cx| {
22734 editor
22735 .breakpoint_store()
22736 .as_ref()
22737 .unwrap()
22738 .read(cx)
22739 .all_source_breakpoints(cx)
22740 });
22741
22742 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22743
22744 editor.update_in(cx, |editor, window, cx| {
22745 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22746 editor.move_to_end(&MoveToEnd, window, cx);
22747 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22748 // Not adding a log message to a standard breakpoint shouldn't remove it
22749 add_log_breakpoint_at_cursor(editor, "", window, cx);
22750 });
22751
22752 let breakpoints = editor.update(cx, |editor, cx| {
22753 editor
22754 .breakpoint_store()
22755 .as_ref()
22756 .unwrap()
22757 .read(cx)
22758 .all_source_breakpoints(cx)
22759 });
22760
22761 assert_breakpoint(
22762 &breakpoints,
22763 &abs_path,
22764 vec![
22765 (0, Breakpoint::new_standard()),
22766 (3, Breakpoint::new_standard()),
22767 ],
22768 );
22769
22770 editor.update_in(cx, |editor, window, cx| {
22771 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22772 });
22773
22774 let breakpoints = editor.update(cx, |editor, cx| {
22775 editor
22776 .breakpoint_store()
22777 .as_ref()
22778 .unwrap()
22779 .read(cx)
22780 .all_source_breakpoints(cx)
22781 });
22782
22783 assert_breakpoint(
22784 &breakpoints,
22785 &abs_path,
22786 vec![
22787 (0, Breakpoint::new_standard()),
22788 (3, Breakpoint::new_log("hello world")),
22789 ],
22790 );
22791
22792 editor.update_in(cx, |editor, window, cx| {
22793 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22794 });
22795
22796 let breakpoints = editor.update(cx, |editor, cx| {
22797 editor
22798 .breakpoint_store()
22799 .as_ref()
22800 .unwrap()
22801 .read(cx)
22802 .all_source_breakpoints(cx)
22803 });
22804
22805 assert_breakpoint(
22806 &breakpoints,
22807 &abs_path,
22808 vec![
22809 (0, Breakpoint::new_standard()),
22810 (3, Breakpoint::new_log("hello Earth!!")),
22811 ],
22812 );
22813}
22814
22815/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22816/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22817/// or when breakpoints were placed out of order. This tests for a regression too
22818#[gpui::test]
22819async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22820 init_test(cx, |_| {});
22821
22822 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22823 let fs = FakeFs::new(cx.executor());
22824 fs.insert_tree(
22825 path!("/a"),
22826 json!({
22827 "main.rs": sample_text,
22828 }),
22829 )
22830 .await;
22831 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22832 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22833 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22834
22835 let fs = FakeFs::new(cx.executor());
22836 fs.insert_tree(
22837 path!("/a"),
22838 json!({
22839 "main.rs": sample_text,
22840 }),
22841 )
22842 .await;
22843 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22844 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22845 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22846 let worktree_id = workspace
22847 .update(cx, |workspace, _window, cx| {
22848 workspace.project().update(cx, |project, cx| {
22849 project.worktrees(cx).next().unwrap().read(cx).id()
22850 })
22851 })
22852 .unwrap();
22853
22854 let buffer = project
22855 .update(cx, |project, cx| {
22856 project.open_buffer((worktree_id, "main.rs"), cx)
22857 })
22858 .await
22859 .unwrap();
22860
22861 let (editor, cx) = cx.add_window_view(|window, cx| {
22862 Editor::new(
22863 EditorMode::full(),
22864 MultiBuffer::build_from_buffer(buffer, cx),
22865 Some(project.clone()),
22866 window,
22867 cx,
22868 )
22869 });
22870
22871 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22872 let abs_path = project.read_with(cx, |project, cx| {
22873 project
22874 .absolute_path(&project_path, cx)
22875 .map(Arc::from)
22876 .unwrap()
22877 });
22878
22879 // assert we can add breakpoint on the first line
22880 editor.update_in(cx, |editor, window, cx| {
22881 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22882 editor.move_to_end(&MoveToEnd, window, cx);
22883 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22884 editor.move_up(&MoveUp, window, cx);
22885 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22886 });
22887
22888 let breakpoints = editor.update(cx, |editor, cx| {
22889 editor
22890 .breakpoint_store()
22891 .as_ref()
22892 .unwrap()
22893 .read(cx)
22894 .all_source_breakpoints(cx)
22895 });
22896
22897 assert_eq!(1, breakpoints.len());
22898 assert_breakpoint(
22899 &breakpoints,
22900 &abs_path,
22901 vec![
22902 (0, Breakpoint::new_standard()),
22903 (2, Breakpoint::new_standard()),
22904 (3, Breakpoint::new_standard()),
22905 ],
22906 );
22907
22908 editor.update_in(cx, |editor, window, cx| {
22909 editor.move_to_beginning(&MoveToBeginning, window, cx);
22910 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22911 editor.move_to_end(&MoveToEnd, window, cx);
22912 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22913 // Disabling a breakpoint that doesn't exist should do nothing
22914 editor.move_up(&MoveUp, window, cx);
22915 editor.move_up(&MoveUp, window, cx);
22916 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22917 });
22918
22919 let breakpoints = editor.update(cx, |editor, cx| {
22920 editor
22921 .breakpoint_store()
22922 .as_ref()
22923 .unwrap()
22924 .read(cx)
22925 .all_source_breakpoints(cx)
22926 });
22927
22928 let disable_breakpoint = {
22929 let mut bp = Breakpoint::new_standard();
22930 bp.state = BreakpointState::Disabled;
22931 bp
22932 };
22933
22934 assert_eq!(1, breakpoints.len());
22935 assert_breakpoint(
22936 &breakpoints,
22937 &abs_path,
22938 vec![
22939 (0, disable_breakpoint.clone()),
22940 (2, Breakpoint::new_standard()),
22941 (3, disable_breakpoint.clone()),
22942 ],
22943 );
22944
22945 editor.update_in(cx, |editor, window, cx| {
22946 editor.move_to_beginning(&MoveToBeginning, window, cx);
22947 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22948 editor.move_to_end(&MoveToEnd, window, cx);
22949 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22950 editor.move_up(&MoveUp, window, cx);
22951 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22952 });
22953
22954 let breakpoints = editor.update(cx, |editor, cx| {
22955 editor
22956 .breakpoint_store()
22957 .as_ref()
22958 .unwrap()
22959 .read(cx)
22960 .all_source_breakpoints(cx)
22961 });
22962
22963 assert_eq!(1, breakpoints.len());
22964 assert_breakpoint(
22965 &breakpoints,
22966 &abs_path,
22967 vec![
22968 (0, Breakpoint::new_standard()),
22969 (2, disable_breakpoint),
22970 (3, Breakpoint::new_standard()),
22971 ],
22972 );
22973}
22974
22975#[gpui::test]
22976async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22977 init_test(cx, |_| {});
22978 let capabilities = lsp::ServerCapabilities {
22979 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22980 prepare_provider: Some(true),
22981 work_done_progress_options: Default::default(),
22982 })),
22983 ..Default::default()
22984 };
22985 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22986
22987 cx.set_state(indoc! {"
22988 struct Fˇoo {}
22989 "});
22990
22991 cx.update_editor(|editor, _, cx| {
22992 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22993 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22994 editor.highlight_background::<DocumentHighlightRead>(
22995 &[highlight_range],
22996 |theme| theme.colors().editor_document_highlight_read_background,
22997 cx,
22998 );
22999 });
23000
23001 let mut prepare_rename_handler = cx
23002 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23003 move |_, _, _| async move {
23004 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23005 start: lsp::Position {
23006 line: 0,
23007 character: 7,
23008 },
23009 end: lsp::Position {
23010 line: 0,
23011 character: 10,
23012 },
23013 })))
23014 },
23015 );
23016 let prepare_rename_task = cx
23017 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23018 .expect("Prepare rename was not started");
23019 prepare_rename_handler.next().await.unwrap();
23020 prepare_rename_task.await.expect("Prepare rename failed");
23021
23022 let mut rename_handler =
23023 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23024 let edit = lsp::TextEdit {
23025 range: lsp::Range {
23026 start: lsp::Position {
23027 line: 0,
23028 character: 7,
23029 },
23030 end: lsp::Position {
23031 line: 0,
23032 character: 10,
23033 },
23034 },
23035 new_text: "FooRenamed".to_string(),
23036 };
23037 Ok(Some(lsp::WorkspaceEdit::new(
23038 // Specify the same edit twice
23039 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23040 )))
23041 });
23042 let rename_task = cx
23043 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23044 .expect("Confirm rename was not started");
23045 rename_handler.next().await.unwrap();
23046 rename_task.await.expect("Confirm rename failed");
23047 cx.run_until_parked();
23048
23049 // Despite two edits, only one is actually applied as those are identical
23050 cx.assert_editor_state(indoc! {"
23051 struct FooRenamedˇ {}
23052 "});
23053}
23054
23055#[gpui::test]
23056async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23057 init_test(cx, |_| {});
23058 // These capabilities indicate that the server does not support prepare rename.
23059 let capabilities = lsp::ServerCapabilities {
23060 rename_provider: Some(lsp::OneOf::Left(true)),
23061 ..Default::default()
23062 };
23063 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23064
23065 cx.set_state(indoc! {"
23066 struct Fˇoo {}
23067 "});
23068
23069 cx.update_editor(|editor, _window, cx| {
23070 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23071 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23072 editor.highlight_background::<DocumentHighlightRead>(
23073 &[highlight_range],
23074 |theme| theme.colors().editor_document_highlight_read_background,
23075 cx,
23076 );
23077 });
23078
23079 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23080 .expect("Prepare rename was not started")
23081 .await
23082 .expect("Prepare rename failed");
23083
23084 let mut rename_handler =
23085 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23086 let edit = lsp::TextEdit {
23087 range: lsp::Range {
23088 start: lsp::Position {
23089 line: 0,
23090 character: 7,
23091 },
23092 end: lsp::Position {
23093 line: 0,
23094 character: 10,
23095 },
23096 },
23097 new_text: "FooRenamed".to_string(),
23098 };
23099 Ok(Some(lsp::WorkspaceEdit::new(
23100 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23101 )))
23102 });
23103 let rename_task = cx
23104 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23105 .expect("Confirm rename was not started");
23106 rename_handler.next().await.unwrap();
23107 rename_task.await.expect("Confirm rename failed");
23108 cx.run_until_parked();
23109
23110 // Correct range is renamed, as `surrounding_word` is used to find it.
23111 cx.assert_editor_state(indoc! {"
23112 struct FooRenamedˇ {}
23113 "});
23114}
23115
23116#[gpui::test]
23117async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23118 init_test(cx, |_| {});
23119 let mut cx = EditorTestContext::new(cx).await;
23120
23121 let language = Arc::new(
23122 Language::new(
23123 LanguageConfig::default(),
23124 Some(tree_sitter_html::LANGUAGE.into()),
23125 )
23126 .with_brackets_query(
23127 r#"
23128 ("<" @open "/>" @close)
23129 ("</" @open ">" @close)
23130 ("<" @open ">" @close)
23131 ("\"" @open "\"" @close)
23132 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23133 "#,
23134 )
23135 .unwrap(),
23136 );
23137 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23138
23139 cx.set_state(indoc! {"
23140 <span>ˇ</span>
23141 "});
23142 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23143 cx.assert_editor_state(indoc! {"
23144 <span>
23145 ˇ
23146 </span>
23147 "});
23148
23149 cx.set_state(indoc! {"
23150 <span><span></span>ˇ</span>
23151 "});
23152 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23153 cx.assert_editor_state(indoc! {"
23154 <span><span></span>
23155 ˇ</span>
23156 "});
23157
23158 cx.set_state(indoc! {"
23159 <span>ˇ
23160 </span>
23161 "});
23162 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23163 cx.assert_editor_state(indoc! {"
23164 <span>
23165 ˇ
23166 </span>
23167 "});
23168}
23169
23170#[gpui::test(iterations = 10)]
23171async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23172 init_test(cx, |_| {});
23173
23174 let fs = FakeFs::new(cx.executor());
23175 fs.insert_tree(
23176 path!("/dir"),
23177 json!({
23178 "a.ts": "a",
23179 }),
23180 )
23181 .await;
23182
23183 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23184 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23185 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23186
23187 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23188 language_registry.add(Arc::new(Language::new(
23189 LanguageConfig {
23190 name: "TypeScript".into(),
23191 matcher: LanguageMatcher {
23192 path_suffixes: vec!["ts".to_string()],
23193 ..Default::default()
23194 },
23195 ..Default::default()
23196 },
23197 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23198 )));
23199 let mut fake_language_servers = language_registry.register_fake_lsp(
23200 "TypeScript",
23201 FakeLspAdapter {
23202 capabilities: lsp::ServerCapabilities {
23203 code_lens_provider: Some(lsp::CodeLensOptions {
23204 resolve_provider: Some(true),
23205 }),
23206 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23207 commands: vec!["_the/command".to_string()],
23208 ..lsp::ExecuteCommandOptions::default()
23209 }),
23210 ..lsp::ServerCapabilities::default()
23211 },
23212 ..FakeLspAdapter::default()
23213 },
23214 );
23215
23216 let editor = workspace
23217 .update(cx, |workspace, window, cx| {
23218 workspace.open_abs_path(
23219 PathBuf::from(path!("/dir/a.ts")),
23220 OpenOptions::default(),
23221 window,
23222 cx,
23223 )
23224 })
23225 .unwrap()
23226 .await
23227 .unwrap()
23228 .downcast::<Editor>()
23229 .unwrap();
23230 cx.executor().run_until_parked();
23231
23232 let fake_server = fake_language_servers.next().await.unwrap();
23233
23234 let buffer = editor.update(cx, |editor, cx| {
23235 editor
23236 .buffer()
23237 .read(cx)
23238 .as_singleton()
23239 .expect("have opened a single file by path")
23240 });
23241
23242 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23243 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23244 drop(buffer_snapshot);
23245 let actions = cx
23246 .update_window(*workspace, |_, window, cx| {
23247 project.code_actions(&buffer, anchor..anchor, window, cx)
23248 })
23249 .unwrap();
23250
23251 fake_server
23252 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23253 Ok(Some(vec![
23254 lsp::CodeLens {
23255 range: lsp::Range::default(),
23256 command: Some(lsp::Command {
23257 title: "Code lens command".to_owned(),
23258 command: "_the/command".to_owned(),
23259 arguments: None,
23260 }),
23261 data: None,
23262 },
23263 lsp::CodeLens {
23264 range: lsp::Range::default(),
23265 command: Some(lsp::Command {
23266 title: "Command not in capabilities".to_owned(),
23267 command: "not in capabilities".to_owned(),
23268 arguments: None,
23269 }),
23270 data: None,
23271 },
23272 lsp::CodeLens {
23273 range: lsp::Range {
23274 start: lsp::Position {
23275 line: 1,
23276 character: 1,
23277 },
23278 end: lsp::Position {
23279 line: 1,
23280 character: 1,
23281 },
23282 },
23283 command: Some(lsp::Command {
23284 title: "Command not in range".to_owned(),
23285 command: "_the/command".to_owned(),
23286 arguments: None,
23287 }),
23288 data: None,
23289 },
23290 ]))
23291 })
23292 .next()
23293 .await;
23294
23295 let actions = actions.await.unwrap();
23296 assert_eq!(
23297 actions.len(),
23298 1,
23299 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23300 );
23301 let action = actions[0].clone();
23302 let apply = project.update(cx, |project, cx| {
23303 project.apply_code_action(buffer.clone(), action, true, cx)
23304 });
23305
23306 // Resolving the code action does not populate its edits. In absence of
23307 // edits, we must execute the given command.
23308 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23309 |mut lens, _| async move {
23310 let lens_command = lens.command.as_mut().expect("should have a command");
23311 assert_eq!(lens_command.title, "Code lens command");
23312 lens_command.arguments = Some(vec![json!("the-argument")]);
23313 Ok(lens)
23314 },
23315 );
23316
23317 // While executing the command, the language server sends the editor
23318 // a `workspaceEdit` request.
23319 fake_server
23320 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23321 let fake = fake_server.clone();
23322 move |params, _| {
23323 assert_eq!(params.command, "_the/command");
23324 let fake = fake.clone();
23325 async move {
23326 fake.server
23327 .request::<lsp::request::ApplyWorkspaceEdit>(
23328 lsp::ApplyWorkspaceEditParams {
23329 label: None,
23330 edit: lsp::WorkspaceEdit {
23331 changes: Some(
23332 [(
23333 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23334 vec![lsp::TextEdit {
23335 range: lsp::Range::new(
23336 lsp::Position::new(0, 0),
23337 lsp::Position::new(0, 0),
23338 ),
23339 new_text: "X".into(),
23340 }],
23341 )]
23342 .into_iter()
23343 .collect(),
23344 ),
23345 ..lsp::WorkspaceEdit::default()
23346 },
23347 },
23348 )
23349 .await
23350 .into_response()
23351 .unwrap();
23352 Ok(Some(json!(null)))
23353 }
23354 }
23355 })
23356 .next()
23357 .await;
23358
23359 // Applying the code lens command returns a project transaction containing the edits
23360 // sent by the language server in its `workspaceEdit` request.
23361 let transaction = apply.await.unwrap();
23362 assert!(transaction.0.contains_key(&buffer));
23363 buffer.update(cx, |buffer, cx| {
23364 assert_eq!(buffer.text(), "Xa");
23365 buffer.undo(cx);
23366 assert_eq!(buffer.text(), "a");
23367 });
23368
23369 let actions_after_edits = cx
23370 .update_window(*workspace, |_, window, cx| {
23371 project.code_actions(&buffer, anchor..anchor, window, cx)
23372 })
23373 .unwrap()
23374 .await
23375 .unwrap();
23376 assert_eq!(
23377 actions, actions_after_edits,
23378 "For the same selection, same code lens actions should be returned"
23379 );
23380
23381 let _responses =
23382 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23383 panic!("No more code lens requests are expected");
23384 });
23385 editor.update_in(cx, |editor, window, cx| {
23386 editor.select_all(&SelectAll, window, cx);
23387 });
23388 cx.executor().run_until_parked();
23389 let new_actions = cx
23390 .update_window(*workspace, |_, window, cx| {
23391 project.code_actions(&buffer, anchor..anchor, window, cx)
23392 })
23393 .unwrap()
23394 .await
23395 .unwrap();
23396 assert_eq!(
23397 actions, new_actions,
23398 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23399 );
23400}
23401
23402#[gpui::test]
23403async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23404 init_test(cx, |_| {});
23405
23406 let fs = FakeFs::new(cx.executor());
23407 let main_text = r#"fn main() {
23408println!("1");
23409println!("2");
23410println!("3");
23411println!("4");
23412println!("5");
23413}"#;
23414 let lib_text = "mod foo {}";
23415 fs.insert_tree(
23416 path!("/a"),
23417 json!({
23418 "lib.rs": lib_text,
23419 "main.rs": main_text,
23420 }),
23421 )
23422 .await;
23423
23424 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23425 let (workspace, cx) =
23426 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23427 let worktree_id = workspace.update(cx, |workspace, cx| {
23428 workspace.project().update(cx, |project, cx| {
23429 project.worktrees(cx).next().unwrap().read(cx).id()
23430 })
23431 });
23432
23433 let expected_ranges = vec![
23434 Point::new(0, 0)..Point::new(0, 0),
23435 Point::new(1, 0)..Point::new(1, 1),
23436 Point::new(2, 0)..Point::new(2, 2),
23437 Point::new(3, 0)..Point::new(3, 3),
23438 ];
23439
23440 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23441 let editor_1 = workspace
23442 .update_in(cx, |workspace, window, cx| {
23443 workspace.open_path(
23444 (worktree_id, "main.rs"),
23445 Some(pane_1.downgrade()),
23446 true,
23447 window,
23448 cx,
23449 )
23450 })
23451 .unwrap()
23452 .await
23453 .downcast::<Editor>()
23454 .unwrap();
23455 pane_1.update(cx, |pane, cx| {
23456 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23457 open_editor.update(cx, |editor, cx| {
23458 assert_eq!(
23459 editor.display_text(cx),
23460 main_text,
23461 "Original main.rs text on initial open",
23462 );
23463 assert_eq!(
23464 editor
23465 .selections
23466 .all::<Point>(cx)
23467 .into_iter()
23468 .map(|s| s.range())
23469 .collect::<Vec<_>>(),
23470 vec![Point::zero()..Point::zero()],
23471 "Default selections on initial open",
23472 );
23473 })
23474 });
23475 editor_1.update_in(cx, |editor, window, cx| {
23476 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23477 s.select_ranges(expected_ranges.clone());
23478 });
23479 });
23480
23481 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23482 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23483 });
23484 let editor_2 = workspace
23485 .update_in(cx, |workspace, window, cx| {
23486 workspace.open_path(
23487 (worktree_id, "main.rs"),
23488 Some(pane_2.downgrade()),
23489 true,
23490 window,
23491 cx,
23492 )
23493 })
23494 .unwrap()
23495 .await
23496 .downcast::<Editor>()
23497 .unwrap();
23498 pane_2.update(cx, |pane, cx| {
23499 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23500 open_editor.update(cx, |editor, cx| {
23501 assert_eq!(
23502 editor.display_text(cx),
23503 main_text,
23504 "Original main.rs text on initial open in another panel",
23505 );
23506 assert_eq!(
23507 editor
23508 .selections
23509 .all::<Point>(cx)
23510 .into_iter()
23511 .map(|s| s.range())
23512 .collect::<Vec<_>>(),
23513 vec![Point::zero()..Point::zero()],
23514 "Default selections on initial open in another panel",
23515 );
23516 })
23517 });
23518
23519 editor_2.update_in(cx, |editor, window, cx| {
23520 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23521 });
23522
23523 let _other_editor_1 = workspace
23524 .update_in(cx, |workspace, window, cx| {
23525 workspace.open_path(
23526 (worktree_id, "lib.rs"),
23527 Some(pane_1.downgrade()),
23528 true,
23529 window,
23530 cx,
23531 )
23532 })
23533 .unwrap()
23534 .await
23535 .downcast::<Editor>()
23536 .unwrap();
23537 pane_1
23538 .update_in(cx, |pane, window, cx| {
23539 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23540 })
23541 .await
23542 .unwrap();
23543 drop(editor_1);
23544 pane_1.update(cx, |pane, cx| {
23545 pane.active_item()
23546 .unwrap()
23547 .downcast::<Editor>()
23548 .unwrap()
23549 .update(cx, |editor, cx| {
23550 assert_eq!(
23551 editor.display_text(cx),
23552 lib_text,
23553 "Other file should be open and active",
23554 );
23555 });
23556 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23557 });
23558
23559 let _other_editor_2 = workspace
23560 .update_in(cx, |workspace, window, cx| {
23561 workspace.open_path(
23562 (worktree_id, "lib.rs"),
23563 Some(pane_2.downgrade()),
23564 true,
23565 window,
23566 cx,
23567 )
23568 })
23569 .unwrap()
23570 .await
23571 .downcast::<Editor>()
23572 .unwrap();
23573 pane_2
23574 .update_in(cx, |pane, window, cx| {
23575 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23576 })
23577 .await
23578 .unwrap();
23579 drop(editor_2);
23580 pane_2.update(cx, |pane, cx| {
23581 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23582 open_editor.update(cx, |editor, cx| {
23583 assert_eq!(
23584 editor.display_text(cx),
23585 lib_text,
23586 "Other file should be open and active in another panel too",
23587 );
23588 });
23589 assert_eq!(
23590 pane.items().count(),
23591 1,
23592 "No other editors should be open in another pane",
23593 );
23594 });
23595
23596 let _editor_1_reopened = workspace
23597 .update_in(cx, |workspace, window, cx| {
23598 workspace.open_path(
23599 (worktree_id, "main.rs"),
23600 Some(pane_1.downgrade()),
23601 true,
23602 window,
23603 cx,
23604 )
23605 })
23606 .unwrap()
23607 .await
23608 .downcast::<Editor>()
23609 .unwrap();
23610 let _editor_2_reopened = workspace
23611 .update_in(cx, |workspace, window, cx| {
23612 workspace.open_path(
23613 (worktree_id, "main.rs"),
23614 Some(pane_2.downgrade()),
23615 true,
23616 window,
23617 cx,
23618 )
23619 })
23620 .unwrap()
23621 .await
23622 .downcast::<Editor>()
23623 .unwrap();
23624 pane_1.update(cx, |pane, cx| {
23625 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23626 open_editor.update(cx, |editor, cx| {
23627 assert_eq!(
23628 editor.display_text(cx),
23629 main_text,
23630 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23631 );
23632 assert_eq!(
23633 editor
23634 .selections
23635 .all::<Point>(cx)
23636 .into_iter()
23637 .map(|s| s.range())
23638 .collect::<Vec<_>>(),
23639 expected_ranges,
23640 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23641 );
23642 })
23643 });
23644 pane_2.update(cx, |pane, cx| {
23645 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23646 open_editor.update(cx, |editor, cx| {
23647 assert_eq!(
23648 editor.display_text(cx),
23649 r#"fn main() {
23650⋯rintln!("1");
23651⋯intln!("2");
23652⋯ntln!("3");
23653println!("4");
23654println!("5");
23655}"#,
23656 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23657 );
23658 assert_eq!(
23659 editor
23660 .selections
23661 .all::<Point>(cx)
23662 .into_iter()
23663 .map(|s| s.range())
23664 .collect::<Vec<_>>(),
23665 vec![Point::zero()..Point::zero()],
23666 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23667 );
23668 })
23669 });
23670}
23671
23672#[gpui::test]
23673async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23674 init_test(cx, |_| {});
23675
23676 let fs = FakeFs::new(cx.executor());
23677 let main_text = r#"fn main() {
23678println!("1");
23679println!("2");
23680println!("3");
23681println!("4");
23682println!("5");
23683}"#;
23684 let lib_text = "mod foo {}";
23685 fs.insert_tree(
23686 path!("/a"),
23687 json!({
23688 "lib.rs": lib_text,
23689 "main.rs": main_text,
23690 }),
23691 )
23692 .await;
23693
23694 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23695 let (workspace, cx) =
23696 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23697 let worktree_id = workspace.update(cx, |workspace, cx| {
23698 workspace.project().update(cx, |project, cx| {
23699 project.worktrees(cx).next().unwrap().read(cx).id()
23700 })
23701 });
23702
23703 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23704 let editor = workspace
23705 .update_in(cx, |workspace, window, cx| {
23706 workspace.open_path(
23707 (worktree_id, "main.rs"),
23708 Some(pane.downgrade()),
23709 true,
23710 window,
23711 cx,
23712 )
23713 })
23714 .unwrap()
23715 .await
23716 .downcast::<Editor>()
23717 .unwrap();
23718 pane.update(cx, |pane, cx| {
23719 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23720 open_editor.update(cx, |editor, cx| {
23721 assert_eq!(
23722 editor.display_text(cx),
23723 main_text,
23724 "Original main.rs text on initial open",
23725 );
23726 })
23727 });
23728 editor.update_in(cx, |editor, window, cx| {
23729 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23730 });
23731
23732 cx.update_global(|store: &mut SettingsStore, cx| {
23733 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23734 s.restore_on_file_reopen = Some(false);
23735 });
23736 });
23737 editor.update_in(cx, |editor, window, cx| {
23738 editor.fold_ranges(
23739 vec![
23740 Point::new(1, 0)..Point::new(1, 1),
23741 Point::new(2, 0)..Point::new(2, 2),
23742 Point::new(3, 0)..Point::new(3, 3),
23743 ],
23744 false,
23745 window,
23746 cx,
23747 );
23748 });
23749 pane.update_in(cx, |pane, window, cx| {
23750 pane.close_all_items(&CloseAllItems::default(), window, cx)
23751 })
23752 .await
23753 .unwrap();
23754 pane.update(cx, |pane, _| {
23755 assert!(pane.active_item().is_none());
23756 });
23757 cx.update_global(|store: &mut SettingsStore, cx| {
23758 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23759 s.restore_on_file_reopen = Some(true);
23760 });
23761 });
23762
23763 let _editor_reopened = workspace
23764 .update_in(cx, |workspace, window, cx| {
23765 workspace.open_path(
23766 (worktree_id, "main.rs"),
23767 Some(pane.downgrade()),
23768 true,
23769 window,
23770 cx,
23771 )
23772 })
23773 .unwrap()
23774 .await
23775 .downcast::<Editor>()
23776 .unwrap();
23777 pane.update(cx, |pane, cx| {
23778 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23779 open_editor.update(cx, |editor, cx| {
23780 assert_eq!(
23781 editor.display_text(cx),
23782 main_text,
23783 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23784 );
23785 })
23786 });
23787}
23788
23789#[gpui::test]
23790async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23791 struct EmptyModalView {
23792 focus_handle: gpui::FocusHandle,
23793 }
23794 impl EventEmitter<DismissEvent> for EmptyModalView {}
23795 impl Render for EmptyModalView {
23796 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23797 div()
23798 }
23799 }
23800 impl Focusable for EmptyModalView {
23801 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23802 self.focus_handle.clone()
23803 }
23804 }
23805 impl workspace::ModalView for EmptyModalView {}
23806 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23807 EmptyModalView {
23808 focus_handle: cx.focus_handle(),
23809 }
23810 }
23811
23812 init_test(cx, |_| {});
23813
23814 let fs = FakeFs::new(cx.executor());
23815 let project = Project::test(fs, [], cx).await;
23816 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23817 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23818 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23819 let editor = cx.new_window_entity(|window, cx| {
23820 Editor::new(
23821 EditorMode::full(),
23822 buffer,
23823 Some(project.clone()),
23824 window,
23825 cx,
23826 )
23827 });
23828 workspace
23829 .update(cx, |workspace, window, cx| {
23830 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23831 })
23832 .unwrap();
23833 editor.update_in(cx, |editor, window, cx| {
23834 editor.open_context_menu(&OpenContextMenu, window, cx);
23835 assert!(editor.mouse_context_menu.is_some());
23836 });
23837 workspace
23838 .update(cx, |workspace, window, cx| {
23839 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23840 })
23841 .unwrap();
23842 cx.read(|cx| {
23843 assert!(editor.read(cx).mouse_context_menu.is_none());
23844 });
23845}
23846
23847#[gpui::test]
23848async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23849 init_test(cx, |_| {});
23850
23851 let fs = FakeFs::new(cx.executor());
23852 fs.insert_file(path!("/file.html"), Default::default())
23853 .await;
23854
23855 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23856
23857 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23858 let html_language = Arc::new(Language::new(
23859 LanguageConfig {
23860 name: "HTML".into(),
23861 matcher: LanguageMatcher {
23862 path_suffixes: vec!["html".to_string()],
23863 ..LanguageMatcher::default()
23864 },
23865 brackets: BracketPairConfig {
23866 pairs: vec![BracketPair {
23867 start: "<".into(),
23868 end: ">".into(),
23869 close: true,
23870 ..Default::default()
23871 }],
23872 ..Default::default()
23873 },
23874 ..Default::default()
23875 },
23876 Some(tree_sitter_html::LANGUAGE.into()),
23877 ));
23878 language_registry.add(html_language);
23879 let mut fake_servers = language_registry.register_fake_lsp(
23880 "HTML",
23881 FakeLspAdapter {
23882 capabilities: lsp::ServerCapabilities {
23883 completion_provider: Some(lsp::CompletionOptions {
23884 resolve_provider: Some(true),
23885 ..Default::default()
23886 }),
23887 ..Default::default()
23888 },
23889 ..Default::default()
23890 },
23891 );
23892
23893 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23894 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23895
23896 let worktree_id = workspace
23897 .update(cx, |workspace, _window, cx| {
23898 workspace.project().update(cx, |project, cx| {
23899 project.worktrees(cx).next().unwrap().read(cx).id()
23900 })
23901 })
23902 .unwrap();
23903 project
23904 .update(cx, |project, cx| {
23905 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23906 })
23907 .await
23908 .unwrap();
23909 let editor = workspace
23910 .update(cx, |workspace, window, cx| {
23911 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23912 })
23913 .unwrap()
23914 .await
23915 .unwrap()
23916 .downcast::<Editor>()
23917 .unwrap();
23918
23919 let fake_server = fake_servers.next().await.unwrap();
23920 editor.update_in(cx, |editor, window, cx| {
23921 editor.set_text("<ad></ad>", window, cx);
23922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23923 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23924 });
23925 let Some((buffer, _)) = editor
23926 .buffer
23927 .read(cx)
23928 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23929 else {
23930 panic!("Failed to get buffer for selection position");
23931 };
23932 let buffer = buffer.read(cx);
23933 let buffer_id = buffer.remote_id();
23934 let opening_range =
23935 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23936 let closing_range =
23937 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23938 let mut linked_ranges = HashMap::default();
23939 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23940 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23941 });
23942 let mut completion_handle =
23943 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23944 Ok(Some(lsp::CompletionResponse::Array(vec![
23945 lsp::CompletionItem {
23946 label: "head".to_string(),
23947 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23948 lsp::InsertReplaceEdit {
23949 new_text: "head".to_string(),
23950 insert: lsp::Range::new(
23951 lsp::Position::new(0, 1),
23952 lsp::Position::new(0, 3),
23953 ),
23954 replace: lsp::Range::new(
23955 lsp::Position::new(0, 1),
23956 lsp::Position::new(0, 3),
23957 ),
23958 },
23959 )),
23960 ..Default::default()
23961 },
23962 ])))
23963 });
23964 editor.update_in(cx, |editor, window, cx| {
23965 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23966 });
23967 cx.run_until_parked();
23968 completion_handle.next().await.unwrap();
23969 editor.update(cx, |editor, _| {
23970 assert!(
23971 editor.context_menu_visible(),
23972 "Completion menu should be visible"
23973 );
23974 });
23975 editor.update_in(cx, |editor, window, cx| {
23976 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23977 });
23978 cx.executor().run_until_parked();
23979 editor.update(cx, |editor, cx| {
23980 assert_eq!(editor.text(cx), "<head></head>");
23981 });
23982}
23983
23984#[gpui::test]
23985async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23986 init_test(cx, |_| {});
23987
23988 let fs = FakeFs::new(cx.executor());
23989 fs.insert_tree(
23990 path!("/root"),
23991 json!({
23992 "a": {
23993 "main.rs": "fn main() {}",
23994 },
23995 "foo": {
23996 "bar": {
23997 "external_file.rs": "pub mod external {}",
23998 }
23999 }
24000 }),
24001 )
24002 .await;
24003
24004 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24005 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24006 language_registry.add(rust_lang());
24007 let _fake_servers = language_registry.register_fake_lsp(
24008 "Rust",
24009 FakeLspAdapter {
24010 ..FakeLspAdapter::default()
24011 },
24012 );
24013 let (workspace, cx) =
24014 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24015 let worktree_id = workspace.update(cx, |workspace, cx| {
24016 workspace.project().update(cx, |project, cx| {
24017 project.worktrees(cx).next().unwrap().read(cx).id()
24018 })
24019 });
24020
24021 let assert_language_servers_count =
24022 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24023 project.update(cx, |project, cx| {
24024 let current = project
24025 .lsp_store()
24026 .read(cx)
24027 .as_local()
24028 .unwrap()
24029 .language_servers
24030 .len();
24031 assert_eq!(expected, current, "{context}");
24032 });
24033 };
24034
24035 assert_language_servers_count(
24036 0,
24037 "No servers should be running before any file is open",
24038 cx,
24039 );
24040 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24041 let main_editor = workspace
24042 .update_in(cx, |workspace, window, cx| {
24043 workspace.open_path(
24044 (worktree_id, "main.rs"),
24045 Some(pane.downgrade()),
24046 true,
24047 window,
24048 cx,
24049 )
24050 })
24051 .unwrap()
24052 .await
24053 .downcast::<Editor>()
24054 .unwrap();
24055 pane.update(cx, |pane, cx| {
24056 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24057 open_editor.update(cx, |editor, cx| {
24058 assert_eq!(
24059 editor.display_text(cx),
24060 "fn main() {}",
24061 "Original main.rs text on initial open",
24062 );
24063 });
24064 assert_eq!(open_editor, main_editor);
24065 });
24066 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24067
24068 let external_editor = workspace
24069 .update_in(cx, |workspace, window, cx| {
24070 workspace.open_abs_path(
24071 PathBuf::from("/root/foo/bar/external_file.rs"),
24072 OpenOptions::default(),
24073 window,
24074 cx,
24075 )
24076 })
24077 .await
24078 .expect("opening external file")
24079 .downcast::<Editor>()
24080 .expect("downcasted external file's open element to editor");
24081 pane.update(cx, |pane, cx| {
24082 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24083 open_editor.update(cx, |editor, cx| {
24084 assert_eq!(
24085 editor.display_text(cx),
24086 "pub mod external {}",
24087 "External file is open now",
24088 );
24089 });
24090 assert_eq!(open_editor, external_editor);
24091 });
24092 assert_language_servers_count(
24093 1,
24094 "Second, external, *.rs file should join the existing server",
24095 cx,
24096 );
24097
24098 pane.update_in(cx, |pane, window, cx| {
24099 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24100 })
24101 .await
24102 .unwrap();
24103 pane.update_in(cx, |pane, window, cx| {
24104 pane.navigate_backward(&Default::default(), window, cx);
24105 });
24106 cx.run_until_parked();
24107 pane.update(cx, |pane, cx| {
24108 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24109 open_editor.update(cx, |editor, cx| {
24110 assert_eq!(
24111 editor.display_text(cx),
24112 "pub mod external {}",
24113 "External file is open now",
24114 );
24115 });
24116 });
24117 assert_language_servers_count(
24118 1,
24119 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24120 cx,
24121 );
24122
24123 cx.update(|_, cx| {
24124 workspace::reload(cx);
24125 });
24126 assert_language_servers_count(
24127 1,
24128 "After reloading the worktree with local and external files opened, only one project should be started",
24129 cx,
24130 );
24131}
24132
24133#[gpui::test]
24134async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24135 init_test(cx, |_| {});
24136
24137 let mut cx = EditorTestContext::new(cx).await;
24138 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24139 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24140
24141 // test cursor move to start of each line on tab
24142 // for `if`, `elif`, `else`, `while`, `with` and `for`
24143 cx.set_state(indoc! {"
24144 def main():
24145 ˇ for item in items:
24146 ˇ while item.active:
24147 ˇ if item.value > 10:
24148 ˇ continue
24149 ˇ elif item.value < 0:
24150 ˇ break
24151 ˇ else:
24152 ˇ with item.context() as ctx:
24153 ˇ yield count
24154 ˇ else:
24155 ˇ log('while else')
24156 ˇ else:
24157 ˇ log('for else')
24158 "});
24159 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24160 cx.assert_editor_state(indoc! {"
24161 def main():
24162 ˇfor item in items:
24163 ˇwhile item.active:
24164 ˇif item.value > 10:
24165 ˇcontinue
24166 ˇelif item.value < 0:
24167 ˇbreak
24168 ˇelse:
24169 ˇwith item.context() as ctx:
24170 ˇyield count
24171 ˇelse:
24172 ˇlog('while else')
24173 ˇelse:
24174 ˇlog('for else')
24175 "});
24176 // test relative indent is preserved when tab
24177 // for `if`, `elif`, `else`, `while`, `with` and `for`
24178 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24179 cx.assert_editor_state(indoc! {"
24180 def main():
24181 ˇfor item in items:
24182 ˇwhile item.active:
24183 ˇif item.value > 10:
24184 ˇcontinue
24185 ˇelif item.value < 0:
24186 ˇbreak
24187 ˇelse:
24188 ˇwith item.context() as ctx:
24189 ˇyield count
24190 ˇelse:
24191 ˇlog('while else')
24192 ˇelse:
24193 ˇlog('for else')
24194 "});
24195
24196 // test cursor move to start of each line on tab
24197 // for `try`, `except`, `else`, `finally`, `match` and `def`
24198 cx.set_state(indoc! {"
24199 def main():
24200 ˇ try:
24201 ˇ fetch()
24202 ˇ except ValueError:
24203 ˇ handle_error()
24204 ˇ else:
24205 ˇ match value:
24206 ˇ case _:
24207 ˇ finally:
24208 ˇ def status():
24209 ˇ return 0
24210 "});
24211 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24212 cx.assert_editor_state(indoc! {"
24213 def main():
24214 ˇtry:
24215 ˇfetch()
24216 ˇexcept ValueError:
24217 ˇhandle_error()
24218 ˇelse:
24219 ˇmatch value:
24220 ˇcase _:
24221 ˇfinally:
24222 ˇdef status():
24223 ˇreturn 0
24224 "});
24225 // test relative indent is preserved when tab
24226 // for `try`, `except`, `else`, `finally`, `match` and `def`
24227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24228 cx.assert_editor_state(indoc! {"
24229 def main():
24230 ˇtry:
24231 ˇfetch()
24232 ˇexcept ValueError:
24233 ˇhandle_error()
24234 ˇelse:
24235 ˇmatch value:
24236 ˇcase _:
24237 ˇfinally:
24238 ˇdef status():
24239 ˇreturn 0
24240 "});
24241}
24242
24243#[gpui::test]
24244async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24245 init_test(cx, |_| {});
24246
24247 let mut cx = EditorTestContext::new(cx).await;
24248 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24249 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24250
24251 // test `else` auto outdents when typed inside `if` block
24252 cx.set_state(indoc! {"
24253 def main():
24254 if i == 2:
24255 return
24256 ˇ
24257 "});
24258 cx.update_editor(|editor, window, cx| {
24259 editor.handle_input("else:", window, cx);
24260 });
24261 cx.assert_editor_state(indoc! {"
24262 def main():
24263 if i == 2:
24264 return
24265 else:ˇ
24266 "});
24267
24268 // test `except` auto outdents when typed inside `try` block
24269 cx.set_state(indoc! {"
24270 def main():
24271 try:
24272 i = 2
24273 ˇ
24274 "});
24275 cx.update_editor(|editor, window, cx| {
24276 editor.handle_input("except:", window, cx);
24277 });
24278 cx.assert_editor_state(indoc! {"
24279 def main():
24280 try:
24281 i = 2
24282 except:ˇ
24283 "});
24284
24285 // test `else` auto outdents when typed inside `except` block
24286 cx.set_state(indoc! {"
24287 def main():
24288 try:
24289 i = 2
24290 except:
24291 j = 2
24292 ˇ
24293 "});
24294 cx.update_editor(|editor, window, cx| {
24295 editor.handle_input("else:", window, cx);
24296 });
24297 cx.assert_editor_state(indoc! {"
24298 def main():
24299 try:
24300 i = 2
24301 except:
24302 j = 2
24303 else:ˇ
24304 "});
24305
24306 // test `finally` auto outdents when typed inside `else` block
24307 cx.set_state(indoc! {"
24308 def main():
24309 try:
24310 i = 2
24311 except:
24312 j = 2
24313 else:
24314 k = 2
24315 ˇ
24316 "});
24317 cx.update_editor(|editor, window, cx| {
24318 editor.handle_input("finally:", window, cx);
24319 });
24320 cx.assert_editor_state(indoc! {"
24321 def main():
24322 try:
24323 i = 2
24324 except:
24325 j = 2
24326 else:
24327 k = 2
24328 finally:ˇ
24329 "});
24330
24331 // test `else` does not outdents when typed inside `except` block right after for block
24332 cx.set_state(indoc! {"
24333 def main():
24334 try:
24335 i = 2
24336 except:
24337 for i in range(n):
24338 pass
24339 ˇ
24340 "});
24341 cx.update_editor(|editor, window, cx| {
24342 editor.handle_input("else:", window, cx);
24343 });
24344 cx.assert_editor_state(indoc! {"
24345 def main():
24346 try:
24347 i = 2
24348 except:
24349 for i in range(n):
24350 pass
24351 else:ˇ
24352 "});
24353
24354 // test `finally` auto outdents when typed inside `else` block right after for block
24355 cx.set_state(indoc! {"
24356 def main():
24357 try:
24358 i = 2
24359 except:
24360 j = 2
24361 else:
24362 for i in range(n):
24363 pass
24364 ˇ
24365 "});
24366 cx.update_editor(|editor, window, cx| {
24367 editor.handle_input("finally:", window, cx);
24368 });
24369 cx.assert_editor_state(indoc! {"
24370 def main():
24371 try:
24372 i = 2
24373 except:
24374 j = 2
24375 else:
24376 for i in range(n):
24377 pass
24378 finally:ˇ
24379 "});
24380
24381 // test `except` outdents to inner "try" block
24382 cx.set_state(indoc! {"
24383 def main():
24384 try:
24385 i = 2
24386 if i == 2:
24387 try:
24388 i = 3
24389 ˇ
24390 "});
24391 cx.update_editor(|editor, window, cx| {
24392 editor.handle_input("except:", window, cx);
24393 });
24394 cx.assert_editor_state(indoc! {"
24395 def main():
24396 try:
24397 i = 2
24398 if i == 2:
24399 try:
24400 i = 3
24401 except:ˇ
24402 "});
24403
24404 // test `except` outdents to outer "try" block
24405 cx.set_state(indoc! {"
24406 def main():
24407 try:
24408 i = 2
24409 if i == 2:
24410 try:
24411 i = 3
24412 ˇ
24413 "});
24414 cx.update_editor(|editor, window, cx| {
24415 editor.handle_input("except:", window, cx);
24416 });
24417 cx.assert_editor_state(indoc! {"
24418 def main():
24419 try:
24420 i = 2
24421 if i == 2:
24422 try:
24423 i = 3
24424 except:ˇ
24425 "});
24426
24427 // test `else` stays at correct indent when typed after `for` block
24428 cx.set_state(indoc! {"
24429 def main():
24430 for i in range(10):
24431 if i == 3:
24432 break
24433 ˇ
24434 "});
24435 cx.update_editor(|editor, window, cx| {
24436 editor.handle_input("else:", window, cx);
24437 });
24438 cx.assert_editor_state(indoc! {"
24439 def main():
24440 for i in range(10):
24441 if i == 3:
24442 break
24443 else:ˇ
24444 "});
24445
24446 // test does not outdent on typing after line with square brackets
24447 cx.set_state(indoc! {"
24448 def f() -> list[str]:
24449 ˇ
24450 "});
24451 cx.update_editor(|editor, window, cx| {
24452 editor.handle_input("a", window, cx);
24453 });
24454 cx.assert_editor_state(indoc! {"
24455 def f() -> list[str]:
24456 aˇ
24457 "});
24458
24459 // test does not outdent on typing : after case keyword
24460 cx.set_state(indoc! {"
24461 match 1:
24462 caseˇ
24463 "});
24464 cx.update_editor(|editor, window, cx| {
24465 editor.handle_input(":", window, cx);
24466 });
24467 cx.assert_editor_state(indoc! {"
24468 match 1:
24469 case:ˇ
24470 "});
24471}
24472
24473#[gpui::test]
24474async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24475 init_test(cx, |_| {});
24476 update_test_language_settings(cx, |settings| {
24477 settings.defaults.extend_comment_on_newline = Some(false);
24478 });
24479 let mut cx = EditorTestContext::new(cx).await;
24480 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24481 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24482
24483 // test correct indent after newline on comment
24484 cx.set_state(indoc! {"
24485 # COMMENT:ˇ
24486 "});
24487 cx.update_editor(|editor, window, cx| {
24488 editor.newline(&Newline, window, cx);
24489 });
24490 cx.assert_editor_state(indoc! {"
24491 # COMMENT:
24492 ˇ
24493 "});
24494
24495 // test correct indent after newline in brackets
24496 cx.set_state(indoc! {"
24497 {ˇ}
24498 "});
24499 cx.update_editor(|editor, window, cx| {
24500 editor.newline(&Newline, window, cx);
24501 });
24502 cx.run_until_parked();
24503 cx.assert_editor_state(indoc! {"
24504 {
24505 ˇ
24506 }
24507 "});
24508
24509 cx.set_state(indoc! {"
24510 (ˇ)
24511 "});
24512 cx.update_editor(|editor, window, cx| {
24513 editor.newline(&Newline, window, cx);
24514 });
24515 cx.run_until_parked();
24516 cx.assert_editor_state(indoc! {"
24517 (
24518 ˇ
24519 )
24520 "});
24521
24522 // do not indent after empty lists or dictionaries
24523 cx.set_state(indoc! {"
24524 a = []ˇ
24525 "});
24526 cx.update_editor(|editor, window, cx| {
24527 editor.newline(&Newline, window, cx);
24528 });
24529 cx.run_until_parked();
24530 cx.assert_editor_state(indoc! {"
24531 a = []
24532 ˇ
24533 "});
24534}
24535
24536#[gpui::test]
24537async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24538 init_test(cx, |_| {});
24539
24540 let mut cx = EditorTestContext::new(cx).await;
24541 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24542 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24543
24544 // test cursor move to start of each line on tab
24545 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24546 cx.set_state(indoc! {"
24547 function main() {
24548 ˇ for item in $items; do
24549 ˇ while [ -n \"$item\" ]; do
24550 ˇ if [ \"$value\" -gt 10 ]; then
24551 ˇ continue
24552 ˇ elif [ \"$value\" -lt 0 ]; then
24553 ˇ break
24554 ˇ else
24555 ˇ echo \"$item\"
24556 ˇ fi
24557 ˇ done
24558 ˇ done
24559 ˇ}
24560 "});
24561 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24562 cx.assert_editor_state(indoc! {"
24563 function main() {
24564 ˇfor item in $items; do
24565 ˇwhile [ -n \"$item\" ]; do
24566 ˇif [ \"$value\" -gt 10 ]; then
24567 ˇcontinue
24568 ˇelif [ \"$value\" -lt 0 ]; then
24569 ˇbreak
24570 ˇelse
24571 ˇecho \"$item\"
24572 ˇfi
24573 ˇdone
24574 ˇdone
24575 ˇ}
24576 "});
24577 // test relative indent is preserved when tab
24578 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24579 cx.assert_editor_state(indoc! {"
24580 function main() {
24581 ˇfor item in $items; do
24582 ˇwhile [ -n \"$item\" ]; do
24583 ˇif [ \"$value\" -gt 10 ]; then
24584 ˇcontinue
24585 ˇelif [ \"$value\" -lt 0 ]; then
24586 ˇbreak
24587 ˇelse
24588 ˇecho \"$item\"
24589 ˇfi
24590 ˇdone
24591 ˇdone
24592 ˇ}
24593 "});
24594
24595 // test cursor move to start of each line on tab
24596 // for `case` statement with patterns
24597 cx.set_state(indoc! {"
24598 function handle() {
24599 ˇ case \"$1\" in
24600 ˇ start)
24601 ˇ echo \"a\"
24602 ˇ ;;
24603 ˇ stop)
24604 ˇ echo \"b\"
24605 ˇ ;;
24606 ˇ *)
24607 ˇ echo \"c\"
24608 ˇ ;;
24609 ˇ esac
24610 ˇ}
24611 "});
24612 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24613 cx.assert_editor_state(indoc! {"
24614 function handle() {
24615 ˇcase \"$1\" in
24616 ˇstart)
24617 ˇecho \"a\"
24618 ˇ;;
24619 ˇstop)
24620 ˇecho \"b\"
24621 ˇ;;
24622 ˇ*)
24623 ˇecho \"c\"
24624 ˇ;;
24625 ˇesac
24626 ˇ}
24627 "});
24628}
24629
24630#[gpui::test]
24631async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24632 init_test(cx, |_| {});
24633
24634 let mut cx = EditorTestContext::new(cx).await;
24635 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24636 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24637
24638 // test indents on comment insert
24639 cx.set_state(indoc! {"
24640 function main() {
24641 ˇ for item in $items; do
24642 ˇ while [ -n \"$item\" ]; do
24643 ˇ if [ \"$value\" -gt 10 ]; then
24644 ˇ continue
24645 ˇ elif [ \"$value\" -lt 0 ]; then
24646 ˇ break
24647 ˇ else
24648 ˇ echo \"$item\"
24649 ˇ fi
24650 ˇ done
24651 ˇ done
24652 ˇ}
24653 "});
24654 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24655 cx.assert_editor_state(indoc! {"
24656 function main() {
24657 #ˇ for item in $items; do
24658 #ˇ while [ -n \"$item\" ]; do
24659 #ˇ if [ \"$value\" -gt 10 ]; then
24660 #ˇ continue
24661 #ˇ elif [ \"$value\" -lt 0 ]; then
24662 #ˇ break
24663 #ˇ else
24664 #ˇ echo \"$item\"
24665 #ˇ fi
24666 #ˇ done
24667 #ˇ done
24668 #ˇ}
24669 "});
24670}
24671
24672#[gpui::test]
24673async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24674 init_test(cx, |_| {});
24675
24676 let mut cx = EditorTestContext::new(cx).await;
24677 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24678 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24679
24680 // test `else` auto outdents when typed inside `if` block
24681 cx.set_state(indoc! {"
24682 if [ \"$1\" = \"test\" ]; then
24683 echo \"foo bar\"
24684 ˇ
24685 "});
24686 cx.update_editor(|editor, window, cx| {
24687 editor.handle_input("else", window, cx);
24688 });
24689 cx.assert_editor_state(indoc! {"
24690 if [ \"$1\" = \"test\" ]; then
24691 echo \"foo bar\"
24692 elseˇ
24693 "});
24694
24695 // test `elif` auto outdents when typed inside `if` block
24696 cx.set_state(indoc! {"
24697 if [ \"$1\" = \"test\" ]; then
24698 echo \"foo bar\"
24699 ˇ
24700 "});
24701 cx.update_editor(|editor, window, cx| {
24702 editor.handle_input("elif", window, cx);
24703 });
24704 cx.assert_editor_state(indoc! {"
24705 if [ \"$1\" = \"test\" ]; then
24706 echo \"foo bar\"
24707 elifˇ
24708 "});
24709
24710 // test `fi` auto outdents when typed inside `else` block
24711 cx.set_state(indoc! {"
24712 if [ \"$1\" = \"test\" ]; then
24713 echo \"foo bar\"
24714 else
24715 echo \"bar baz\"
24716 ˇ
24717 "});
24718 cx.update_editor(|editor, window, cx| {
24719 editor.handle_input("fi", window, cx);
24720 });
24721 cx.assert_editor_state(indoc! {"
24722 if [ \"$1\" = \"test\" ]; then
24723 echo \"foo bar\"
24724 else
24725 echo \"bar baz\"
24726 fiˇ
24727 "});
24728
24729 // test `done` auto outdents when typed inside `while` block
24730 cx.set_state(indoc! {"
24731 while read line; do
24732 echo \"$line\"
24733 ˇ
24734 "});
24735 cx.update_editor(|editor, window, cx| {
24736 editor.handle_input("done", window, cx);
24737 });
24738 cx.assert_editor_state(indoc! {"
24739 while read line; do
24740 echo \"$line\"
24741 doneˇ
24742 "});
24743
24744 // test `done` auto outdents when typed inside `for` block
24745 cx.set_state(indoc! {"
24746 for file in *.txt; do
24747 cat \"$file\"
24748 ˇ
24749 "});
24750 cx.update_editor(|editor, window, cx| {
24751 editor.handle_input("done", window, cx);
24752 });
24753 cx.assert_editor_state(indoc! {"
24754 for file in *.txt; do
24755 cat \"$file\"
24756 doneˇ
24757 "});
24758
24759 // test `esac` auto outdents when typed inside `case` block
24760 cx.set_state(indoc! {"
24761 case \"$1\" in
24762 start)
24763 echo \"foo bar\"
24764 ;;
24765 stop)
24766 echo \"bar baz\"
24767 ;;
24768 ˇ
24769 "});
24770 cx.update_editor(|editor, window, cx| {
24771 editor.handle_input("esac", window, cx);
24772 });
24773 cx.assert_editor_state(indoc! {"
24774 case \"$1\" in
24775 start)
24776 echo \"foo bar\"
24777 ;;
24778 stop)
24779 echo \"bar baz\"
24780 ;;
24781 esacˇ
24782 "});
24783
24784 // test `*)` auto outdents when typed inside `case` block
24785 cx.set_state(indoc! {"
24786 case \"$1\" in
24787 start)
24788 echo \"foo bar\"
24789 ;;
24790 ˇ
24791 "});
24792 cx.update_editor(|editor, window, cx| {
24793 editor.handle_input("*)", window, cx);
24794 });
24795 cx.assert_editor_state(indoc! {"
24796 case \"$1\" in
24797 start)
24798 echo \"foo bar\"
24799 ;;
24800 *)ˇ
24801 "});
24802
24803 // test `fi` outdents to correct level with nested if blocks
24804 cx.set_state(indoc! {"
24805 if [ \"$1\" = \"test\" ]; then
24806 echo \"outer if\"
24807 if [ \"$2\" = \"debug\" ]; then
24808 echo \"inner if\"
24809 ˇ
24810 "});
24811 cx.update_editor(|editor, window, cx| {
24812 editor.handle_input("fi", window, cx);
24813 });
24814 cx.assert_editor_state(indoc! {"
24815 if [ \"$1\" = \"test\" ]; then
24816 echo \"outer if\"
24817 if [ \"$2\" = \"debug\" ]; then
24818 echo \"inner if\"
24819 fiˇ
24820 "});
24821}
24822
24823#[gpui::test]
24824async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24825 init_test(cx, |_| {});
24826 update_test_language_settings(cx, |settings| {
24827 settings.defaults.extend_comment_on_newline = Some(false);
24828 });
24829 let mut cx = EditorTestContext::new(cx).await;
24830 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24831 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24832
24833 // test correct indent after newline on comment
24834 cx.set_state(indoc! {"
24835 # COMMENT:ˇ
24836 "});
24837 cx.update_editor(|editor, window, cx| {
24838 editor.newline(&Newline, window, cx);
24839 });
24840 cx.assert_editor_state(indoc! {"
24841 # COMMENT:
24842 ˇ
24843 "});
24844
24845 // test correct indent after newline after `then`
24846 cx.set_state(indoc! {"
24847
24848 if [ \"$1\" = \"test\" ]; thenˇ
24849 "});
24850 cx.update_editor(|editor, window, cx| {
24851 editor.newline(&Newline, window, cx);
24852 });
24853 cx.run_until_parked();
24854 cx.assert_editor_state(indoc! {"
24855
24856 if [ \"$1\" = \"test\" ]; then
24857 ˇ
24858 "});
24859
24860 // test correct indent after newline after `else`
24861 cx.set_state(indoc! {"
24862 if [ \"$1\" = \"test\" ]; then
24863 elseˇ
24864 "});
24865 cx.update_editor(|editor, window, cx| {
24866 editor.newline(&Newline, window, cx);
24867 });
24868 cx.run_until_parked();
24869 cx.assert_editor_state(indoc! {"
24870 if [ \"$1\" = \"test\" ]; then
24871 else
24872 ˇ
24873 "});
24874
24875 // test correct indent after newline after `elif`
24876 cx.set_state(indoc! {"
24877 if [ \"$1\" = \"test\" ]; then
24878 elifˇ
24879 "});
24880 cx.update_editor(|editor, window, cx| {
24881 editor.newline(&Newline, window, cx);
24882 });
24883 cx.run_until_parked();
24884 cx.assert_editor_state(indoc! {"
24885 if [ \"$1\" = \"test\" ]; then
24886 elif
24887 ˇ
24888 "});
24889
24890 // test correct indent after newline after `do`
24891 cx.set_state(indoc! {"
24892 for file in *.txt; doˇ
24893 "});
24894 cx.update_editor(|editor, window, cx| {
24895 editor.newline(&Newline, window, cx);
24896 });
24897 cx.run_until_parked();
24898 cx.assert_editor_state(indoc! {"
24899 for file in *.txt; do
24900 ˇ
24901 "});
24902
24903 // test correct indent after newline after case pattern
24904 cx.set_state(indoc! {"
24905 case \"$1\" in
24906 start)ˇ
24907 "});
24908 cx.update_editor(|editor, window, cx| {
24909 editor.newline(&Newline, window, cx);
24910 });
24911 cx.run_until_parked();
24912 cx.assert_editor_state(indoc! {"
24913 case \"$1\" in
24914 start)
24915 ˇ
24916 "});
24917
24918 // test correct indent after newline after case pattern
24919 cx.set_state(indoc! {"
24920 case \"$1\" in
24921 start)
24922 ;;
24923 *)ˇ
24924 "});
24925 cx.update_editor(|editor, window, cx| {
24926 editor.newline(&Newline, window, cx);
24927 });
24928 cx.run_until_parked();
24929 cx.assert_editor_state(indoc! {"
24930 case \"$1\" in
24931 start)
24932 ;;
24933 *)
24934 ˇ
24935 "});
24936
24937 // test correct indent after newline after function opening brace
24938 cx.set_state(indoc! {"
24939 function test() {ˇ}
24940 "});
24941 cx.update_editor(|editor, window, cx| {
24942 editor.newline(&Newline, window, cx);
24943 });
24944 cx.run_until_parked();
24945 cx.assert_editor_state(indoc! {"
24946 function test() {
24947 ˇ
24948 }
24949 "});
24950
24951 // test no extra indent after semicolon on same line
24952 cx.set_state(indoc! {"
24953 echo \"test\";ˇ
24954 "});
24955 cx.update_editor(|editor, window, cx| {
24956 editor.newline(&Newline, window, cx);
24957 });
24958 cx.run_until_parked();
24959 cx.assert_editor_state(indoc! {"
24960 echo \"test\";
24961 ˇ
24962 "});
24963}
24964
24965fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24966 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24967 point..point
24968}
24969
24970#[track_caller]
24971fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24972 let (text, ranges) = marked_text_ranges(marked_text, true);
24973 assert_eq!(editor.text(cx), text);
24974 assert_eq!(
24975 editor.selections.ranges(cx),
24976 ranges,
24977 "Assert selections are {}",
24978 marked_text
24979 );
24980}
24981
24982pub fn handle_signature_help_request(
24983 cx: &mut EditorLspTestContext,
24984 mocked_response: lsp::SignatureHelp,
24985) -> impl Future<Output = ()> + use<> {
24986 let mut request =
24987 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24988 let mocked_response = mocked_response.clone();
24989 async move { Ok(Some(mocked_response)) }
24990 });
24991
24992 async move {
24993 request.next().await;
24994 }
24995}
24996
24997#[track_caller]
24998pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24999 cx.update_editor(|editor, _, _| {
25000 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25001 let entries = menu.entries.borrow();
25002 let entries = entries
25003 .iter()
25004 .map(|entry| entry.string.as_str())
25005 .collect::<Vec<_>>();
25006 assert_eq!(entries, expected);
25007 } else {
25008 panic!("Expected completions menu");
25009 }
25010 });
25011}
25012
25013/// Handle completion request passing a marked string specifying where the completion
25014/// should be triggered from using '|' character, what range should be replaced, and what completions
25015/// should be returned using '<' and '>' to delimit the range.
25016///
25017/// Also see `handle_completion_request_with_insert_and_replace`.
25018#[track_caller]
25019pub fn handle_completion_request(
25020 marked_string: &str,
25021 completions: Vec<&'static str>,
25022 is_incomplete: bool,
25023 counter: Arc<AtomicUsize>,
25024 cx: &mut EditorLspTestContext,
25025) -> impl Future<Output = ()> {
25026 let complete_from_marker: TextRangeMarker = '|'.into();
25027 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25028 let (_, mut marked_ranges) = marked_text_ranges_by(
25029 marked_string,
25030 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25031 );
25032
25033 let complete_from_position =
25034 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25035 let replace_range =
25036 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25037
25038 let mut request =
25039 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25040 let completions = completions.clone();
25041 counter.fetch_add(1, atomic::Ordering::Release);
25042 async move {
25043 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25044 assert_eq!(
25045 params.text_document_position.position,
25046 complete_from_position
25047 );
25048 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25049 is_incomplete,
25050 item_defaults: None,
25051 items: completions
25052 .iter()
25053 .map(|completion_text| lsp::CompletionItem {
25054 label: completion_text.to_string(),
25055 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25056 range: replace_range,
25057 new_text: completion_text.to_string(),
25058 })),
25059 ..Default::default()
25060 })
25061 .collect(),
25062 })))
25063 }
25064 });
25065
25066 async move {
25067 request.next().await;
25068 }
25069}
25070
25071/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25072/// given instead, which also contains an `insert` range.
25073///
25074/// This function uses markers to define ranges:
25075/// - `|` marks the cursor position
25076/// - `<>` marks the replace range
25077/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25078pub fn handle_completion_request_with_insert_and_replace(
25079 cx: &mut EditorLspTestContext,
25080 marked_string: &str,
25081 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25082 counter: Arc<AtomicUsize>,
25083) -> impl Future<Output = ()> {
25084 let complete_from_marker: TextRangeMarker = '|'.into();
25085 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25086 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25087
25088 let (_, mut marked_ranges) = marked_text_ranges_by(
25089 marked_string,
25090 vec![
25091 complete_from_marker.clone(),
25092 replace_range_marker.clone(),
25093 insert_range_marker.clone(),
25094 ],
25095 );
25096
25097 let complete_from_position =
25098 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25099 let replace_range =
25100 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25101
25102 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25103 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25104 _ => lsp::Range {
25105 start: replace_range.start,
25106 end: complete_from_position,
25107 },
25108 };
25109
25110 let mut request =
25111 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25112 let completions = completions.clone();
25113 counter.fetch_add(1, atomic::Ordering::Release);
25114 async move {
25115 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25116 assert_eq!(
25117 params.text_document_position.position, complete_from_position,
25118 "marker `|` position doesn't match",
25119 );
25120 Ok(Some(lsp::CompletionResponse::Array(
25121 completions
25122 .iter()
25123 .map(|(label, new_text)| lsp::CompletionItem {
25124 label: label.to_string(),
25125 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25126 lsp::InsertReplaceEdit {
25127 insert: insert_range,
25128 replace: replace_range,
25129 new_text: new_text.to_string(),
25130 },
25131 )),
25132 ..Default::default()
25133 })
25134 .collect(),
25135 )))
25136 }
25137 });
25138
25139 async move {
25140 request.next().await;
25141 }
25142}
25143
25144fn handle_resolve_completion_request(
25145 cx: &mut EditorLspTestContext,
25146 edits: Option<Vec<(&'static str, &'static str)>>,
25147) -> impl Future<Output = ()> {
25148 let edits = edits.map(|edits| {
25149 edits
25150 .iter()
25151 .map(|(marked_string, new_text)| {
25152 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25153 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25154 lsp::TextEdit::new(replace_range, new_text.to_string())
25155 })
25156 .collect::<Vec<_>>()
25157 });
25158
25159 let mut request =
25160 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25161 let edits = edits.clone();
25162 async move {
25163 Ok(lsp::CompletionItem {
25164 additional_text_edits: edits,
25165 ..Default::default()
25166 })
25167 }
25168 });
25169
25170 async move {
25171 request.next().await;
25172 }
25173}
25174
25175pub(crate) fn update_test_language_settings(
25176 cx: &mut TestAppContext,
25177 f: impl Fn(&mut AllLanguageSettingsContent),
25178) {
25179 cx.update(|cx| {
25180 SettingsStore::update_global(cx, |store, cx| {
25181 store.update_user_settings::<AllLanguageSettings>(cx, f);
25182 });
25183 });
25184}
25185
25186pub(crate) fn update_test_project_settings(
25187 cx: &mut TestAppContext,
25188 f: impl Fn(&mut ProjectSettings),
25189) {
25190 cx.update(|cx| {
25191 SettingsStore::update_global(cx, |store, cx| {
25192 store.update_user_settings::<ProjectSettings>(cx, f);
25193 });
25194 });
25195}
25196
25197pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25198 cx.update(|cx| {
25199 assets::Assets.load_test_fonts(cx);
25200 let store = SettingsStore::test(cx);
25201 cx.set_global(store);
25202 theme::init(theme::LoadThemes::JustBase, cx);
25203 release_channel::init(SemanticVersion::default(), cx);
25204 client::init_settings(cx);
25205 language::init(cx);
25206 Project::init_settings(cx);
25207 workspace::init_settings(cx);
25208 crate::init(cx);
25209 });
25210 zlog::init_test();
25211 update_test_language_settings(cx, f);
25212}
25213
25214#[track_caller]
25215fn assert_hunk_revert(
25216 not_reverted_text_with_selections: &str,
25217 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25218 expected_reverted_text_with_selections: &str,
25219 base_text: &str,
25220 cx: &mut EditorLspTestContext,
25221) {
25222 cx.set_state(not_reverted_text_with_selections);
25223 cx.set_head_text(base_text);
25224 cx.executor().run_until_parked();
25225
25226 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25227 let snapshot = editor.snapshot(window, cx);
25228 let reverted_hunk_statuses = snapshot
25229 .buffer_snapshot
25230 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25231 .map(|hunk| hunk.status().kind)
25232 .collect::<Vec<_>>();
25233
25234 editor.git_restore(&Default::default(), window, cx);
25235 reverted_hunk_statuses
25236 });
25237 cx.executor().run_until_parked();
25238 cx.assert_editor_state(expected_reverted_text_with_selections);
25239 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25240}
25241
25242#[gpui::test(iterations = 10)]
25243async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25244 init_test(cx, |_| {});
25245
25246 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25247 let counter = diagnostic_requests.clone();
25248
25249 let fs = FakeFs::new(cx.executor());
25250 fs.insert_tree(
25251 path!("/a"),
25252 json!({
25253 "first.rs": "fn main() { let a = 5; }",
25254 "second.rs": "// Test file",
25255 }),
25256 )
25257 .await;
25258
25259 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25260 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25261 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25262
25263 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25264 language_registry.add(rust_lang());
25265 let mut fake_servers = language_registry.register_fake_lsp(
25266 "Rust",
25267 FakeLspAdapter {
25268 capabilities: lsp::ServerCapabilities {
25269 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25270 lsp::DiagnosticOptions {
25271 identifier: None,
25272 inter_file_dependencies: true,
25273 workspace_diagnostics: true,
25274 work_done_progress_options: Default::default(),
25275 },
25276 )),
25277 ..Default::default()
25278 },
25279 ..Default::default()
25280 },
25281 );
25282
25283 let editor = workspace
25284 .update(cx, |workspace, window, cx| {
25285 workspace.open_abs_path(
25286 PathBuf::from(path!("/a/first.rs")),
25287 OpenOptions::default(),
25288 window,
25289 cx,
25290 )
25291 })
25292 .unwrap()
25293 .await
25294 .unwrap()
25295 .downcast::<Editor>()
25296 .unwrap();
25297 let fake_server = fake_servers.next().await.unwrap();
25298 let server_id = fake_server.server.server_id();
25299 let mut first_request = fake_server
25300 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25301 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25302 let result_id = Some(new_result_id.to_string());
25303 assert_eq!(
25304 params.text_document.uri,
25305 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25306 );
25307 async move {
25308 Ok(lsp::DocumentDiagnosticReportResult::Report(
25309 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25310 related_documents: None,
25311 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25312 items: Vec::new(),
25313 result_id,
25314 },
25315 }),
25316 ))
25317 }
25318 });
25319
25320 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25321 project.update(cx, |project, cx| {
25322 let buffer_id = editor
25323 .read(cx)
25324 .buffer()
25325 .read(cx)
25326 .as_singleton()
25327 .expect("created a singleton buffer")
25328 .read(cx)
25329 .remote_id();
25330 let buffer_result_id = project
25331 .lsp_store()
25332 .read(cx)
25333 .result_id(server_id, buffer_id, cx);
25334 assert_eq!(expected, buffer_result_id);
25335 });
25336 };
25337
25338 ensure_result_id(None, cx);
25339 cx.executor().advance_clock(Duration::from_millis(60));
25340 cx.executor().run_until_parked();
25341 assert_eq!(
25342 diagnostic_requests.load(atomic::Ordering::Acquire),
25343 1,
25344 "Opening file should trigger diagnostic request"
25345 );
25346 first_request
25347 .next()
25348 .await
25349 .expect("should have sent the first diagnostics pull request");
25350 ensure_result_id(Some("1".to_string()), cx);
25351
25352 // Editing should trigger diagnostics
25353 editor.update_in(cx, |editor, window, cx| {
25354 editor.handle_input("2", window, cx)
25355 });
25356 cx.executor().advance_clock(Duration::from_millis(60));
25357 cx.executor().run_until_parked();
25358 assert_eq!(
25359 diagnostic_requests.load(atomic::Ordering::Acquire),
25360 2,
25361 "Editing should trigger diagnostic request"
25362 );
25363 ensure_result_id(Some("2".to_string()), cx);
25364
25365 // Moving cursor should not trigger diagnostic request
25366 editor.update_in(cx, |editor, window, cx| {
25367 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25368 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25369 });
25370 });
25371 cx.executor().advance_clock(Duration::from_millis(60));
25372 cx.executor().run_until_parked();
25373 assert_eq!(
25374 diagnostic_requests.load(atomic::Ordering::Acquire),
25375 2,
25376 "Cursor movement should not trigger diagnostic request"
25377 );
25378 ensure_result_id(Some("2".to_string()), cx);
25379 // Multiple rapid edits should be debounced
25380 for _ in 0..5 {
25381 editor.update_in(cx, |editor, window, cx| {
25382 editor.handle_input("x", window, cx)
25383 });
25384 }
25385 cx.executor().advance_clock(Duration::from_millis(60));
25386 cx.executor().run_until_parked();
25387
25388 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25389 assert!(
25390 final_requests <= 4,
25391 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25392 );
25393 ensure_result_id(Some(final_requests.to_string()), cx);
25394}
25395
25396#[gpui::test]
25397async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25398 // Regression test for issue #11671
25399 // Previously, adding a cursor after moving multiple cursors would reset
25400 // the cursor count instead of adding to the existing cursors.
25401 init_test(cx, |_| {});
25402 let mut cx = EditorTestContext::new(cx).await;
25403
25404 // Create a simple buffer with cursor at start
25405 cx.set_state(indoc! {"
25406 ˇaaaa
25407 bbbb
25408 cccc
25409 dddd
25410 eeee
25411 ffff
25412 gggg
25413 hhhh"});
25414
25415 // Add 2 cursors below (so we have 3 total)
25416 cx.update_editor(|editor, window, cx| {
25417 editor.add_selection_below(&Default::default(), window, cx);
25418 editor.add_selection_below(&Default::default(), window, cx);
25419 });
25420
25421 // Verify we have 3 cursors
25422 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25423 assert_eq!(
25424 initial_count, 3,
25425 "Should have 3 cursors after adding 2 below"
25426 );
25427
25428 // Move down one line
25429 cx.update_editor(|editor, window, cx| {
25430 editor.move_down(&MoveDown, window, cx);
25431 });
25432
25433 // Add another cursor below
25434 cx.update_editor(|editor, window, cx| {
25435 editor.add_selection_below(&Default::default(), window, cx);
25436 });
25437
25438 // Should now have 4 cursors (3 original + 1 new)
25439 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25440 assert_eq!(
25441 final_count, 4,
25442 "Should have 4 cursors after moving and adding another"
25443 );
25444}
25445
25446#[gpui::test(iterations = 10)]
25447async fn test_document_colors(cx: &mut TestAppContext) {
25448 let expected_color = Rgba {
25449 r: 0.33,
25450 g: 0.33,
25451 b: 0.33,
25452 a: 0.33,
25453 };
25454
25455 init_test(cx, |_| {});
25456
25457 let fs = FakeFs::new(cx.executor());
25458 fs.insert_tree(
25459 path!("/a"),
25460 json!({
25461 "first.rs": "fn main() { let a = 5; }",
25462 }),
25463 )
25464 .await;
25465
25466 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25467 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25468 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25469
25470 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25471 language_registry.add(rust_lang());
25472 let mut fake_servers = language_registry.register_fake_lsp(
25473 "Rust",
25474 FakeLspAdapter {
25475 capabilities: lsp::ServerCapabilities {
25476 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25477 ..lsp::ServerCapabilities::default()
25478 },
25479 name: "rust-analyzer",
25480 ..FakeLspAdapter::default()
25481 },
25482 );
25483 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25484 "Rust",
25485 FakeLspAdapter {
25486 capabilities: lsp::ServerCapabilities {
25487 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25488 ..lsp::ServerCapabilities::default()
25489 },
25490 name: "not-rust-analyzer",
25491 ..FakeLspAdapter::default()
25492 },
25493 );
25494
25495 let editor = workspace
25496 .update(cx, |workspace, window, cx| {
25497 workspace.open_abs_path(
25498 PathBuf::from(path!("/a/first.rs")),
25499 OpenOptions::default(),
25500 window,
25501 cx,
25502 )
25503 })
25504 .unwrap()
25505 .await
25506 .unwrap()
25507 .downcast::<Editor>()
25508 .unwrap();
25509 let fake_language_server = fake_servers.next().await.unwrap();
25510 let fake_language_server_without_capabilities =
25511 fake_servers_without_capabilities.next().await.unwrap();
25512 let requests_made = Arc::new(AtomicUsize::new(0));
25513 let closure_requests_made = Arc::clone(&requests_made);
25514 let mut color_request_handle = fake_language_server
25515 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25516 let requests_made = Arc::clone(&closure_requests_made);
25517 async move {
25518 assert_eq!(
25519 params.text_document.uri,
25520 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25521 );
25522 requests_made.fetch_add(1, atomic::Ordering::Release);
25523 Ok(vec![
25524 lsp::ColorInformation {
25525 range: lsp::Range {
25526 start: lsp::Position {
25527 line: 0,
25528 character: 0,
25529 },
25530 end: lsp::Position {
25531 line: 0,
25532 character: 1,
25533 },
25534 },
25535 color: lsp::Color {
25536 red: 0.33,
25537 green: 0.33,
25538 blue: 0.33,
25539 alpha: 0.33,
25540 },
25541 },
25542 lsp::ColorInformation {
25543 range: lsp::Range {
25544 start: lsp::Position {
25545 line: 0,
25546 character: 0,
25547 },
25548 end: lsp::Position {
25549 line: 0,
25550 character: 1,
25551 },
25552 },
25553 color: lsp::Color {
25554 red: 0.33,
25555 green: 0.33,
25556 blue: 0.33,
25557 alpha: 0.33,
25558 },
25559 },
25560 ])
25561 }
25562 });
25563
25564 let _handle = fake_language_server_without_capabilities
25565 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25566 panic!("Should not be called");
25567 });
25568 cx.executor().advance_clock(Duration::from_millis(100));
25569 color_request_handle.next().await.unwrap();
25570 cx.run_until_parked();
25571 assert_eq!(
25572 1,
25573 requests_made.load(atomic::Ordering::Acquire),
25574 "Should query for colors once per editor open"
25575 );
25576 editor.update_in(cx, |editor, _, cx| {
25577 assert_eq!(
25578 vec![expected_color],
25579 extract_color_inlays(editor, cx),
25580 "Should have an initial inlay"
25581 );
25582 });
25583
25584 // opening another file in a split should not influence the LSP query counter
25585 workspace
25586 .update(cx, |workspace, window, cx| {
25587 assert_eq!(
25588 workspace.panes().len(),
25589 1,
25590 "Should have one pane with one editor"
25591 );
25592 workspace.move_item_to_pane_in_direction(
25593 &MoveItemToPaneInDirection {
25594 direction: SplitDirection::Right,
25595 focus: false,
25596 clone: true,
25597 },
25598 window,
25599 cx,
25600 );
25601 })
25602 .unwrap();
25603 cx.run_until_parked();
25604 workspace
25605 .update(cx, |workspace, _, cx| {
25606 let panes = workspace.panes();
25607 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25608 for pane in panes {
25609 let editor = pane
25610 .read(cx)
25611 .active_item()
25612 .and_then(|item| item.downcast::<Editor>())
25613 .expect("Should have opened an editor in each split");
25614 let editor_file = editor
25615 .read(cx)
25616 .buffer()
25617 .read(cx)
25618 .as_singleton()
25619 .expect("test deals with singleton buffers")
25620 .read(cx)
25621 .file()
25622 .expect("test buffese should have a file")
25623 .path();
25624 assert_eq!(
25625 editor_file.as_ref(),
25626 Path::new("first.rs"),
25627 "Both editors should be opened for the same file"
25628 )
25629 }
25630 })
25631 .unwrap();
25632
25633 cx.executor().advance_clock(Duration::from_millis(500));
25634 let save = editor.update_in(cx, |editor, window, cx| {
25635 editor.move_to_end(&MoveToEnd, window, cx);
25636 editor.handle_input("dirty", window, cx);
25637 editor.save(
25638 SaveOptions {
25639 format: true,
25640 autosave: true,
25641 },
25642 project.clone(),
25643 window,
25644 cx,
25645 )
25646 });
25647 save.await.unwrap();
25648
25649 color_request_handle.next().await.unwrap();
25650 cx.run_until_parked();
25651 assert_eq!(
25652 3,
25653 requests_made.load(atomic::Ordering::Acquire),
25654 "Should query for colors once per save and once per formatting after save"
25655 );
25656
25657 drop(editor);
25658 let close = workspace
25659 .update(cx, |workspace, window, cx| {
25660 workspace.active_pane().update(cx, |pane, cx| {
25661 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25662 })
25663 })
25664 .unwrap();
25665 close.await.unwrap();
25666 let close = workspace
25667 .update(cx, |workspace, window, cx| {
25668 workspace.active_pane().update(cx, |pane, cx| {
25669 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25670 })
25671 })
25672 .unwrap();
25673 close.await.unwrap();
25674 assert_eq!(
25675 3,
25676 requests_made.load(atomic::Ordering::Acquire),
25677 "After saving and closing all editors, no extra requests should be made"
25678 );
25679 workspace
25680 .update(cx, |workspace, _, cx| {
25681 assert!(
25682 workspace.active_item(cx).is_none(),
25683 "Should close all editors"
25684 )
25685 })
25686 .unwrap();
25687
25688 workspace
25689 .update(cx, |workspace, window, cx| {
25690 workspace.active_pane().update(cx, |pane, cx| {
25691 pane.navigate_backward(&workspace::GoBack, window, cx);
25692 })
25693 })
25694 .unwrap();
25695 cx.executor().advance_clock(Duration::from_millis(100));
25696 cx.run_until_parked();
25697 let editor = workspace
25698 .update(cx, |workspace, _, cx| {
25699 workspace
25700 .active_item(cx)
25701 .expect("Should have reopened the editor again after navigating back")
25702 .downcast::<Editor>()
25703 .expect("Should be an editor")
25704 })
25705 .unwrap();
25706 color_request_handle.next().await.unwrap();
25707 assert_eq!(
25708 3,
25709 requests_made.load(atomic::Ordering::Acquire),
25710 "Cache should be reused on buffer close and reopen"
25711 );
25712 editor.update(cx, |editor, cx| {
25713 assert_eq!(
25714 vec![expected_color],
25715 extract_color_inlays(editor, cx),
25716 "Should have an initial inlay"
25717 );
25718 });
25719
25720 drop(color_request_handle);
25721 let closure_requests_made = Arc::clone(&requests_made);
25722 let mut empty_color_request_handle = fake_language_server
25723 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25724 let requests_made = Arc::clone(&closure_requests_made);
25725 async move {
25726 assert_eq!(
25727 params.text_document.uri,
25728 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25729 );
25730 requests_made.fetch_add(1, atomic::Ordering::Release);
25731 Ok(Vec::new())
25732 }
25733 });
25734 let save = editor.update_in(cx, |editor, window, cx| {
25735 editor.move_to_end(&MoveToEnd, window, cx);
25736 editor.handle_input("dirty_again", window, cx);
25737 editor.save(
25738 SaveOptions {
25739 format: false,
25740 autosave: true,
25741 },
25742 project.clone(),
25743 window,
25744 cx,
25745 )
25746 });
25747 save.await.unwrap();
25748
25749 empty_color_request_handle.next().await.unwrap();
25750 cx.run_until_parked();
25751 assert_eq!(
25752 4,
25753 requests_made.load(atomic::Ordering::Acquire),
25754 "Should query for colors once per save only, as formatting was not requested"
25755 );
25756 editor.update(cx, |editor, cx| {
25757 assert_eq!(
25758 Vec::<Rgba>::new(),
25759 extract_color_inlays(editor, cx),
25760 "Should clear all colors when the server returns an empty response"
25761 );
25762 });
25763}
25764
25765#[gpui::test]
25766async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25767 init_test(cx, |_| {});
25768 let (editor, cx) = cx.add_window_view(Editor::single_line);
25769 editor.update_in(cx, |editor, window, cx| {
25770 editor.set_text("oops\n\nwow\n", window, cx)
25771 });
25772 cx.run_until_parked();
25773 editor.update(cx, |editor, cx| {
25774 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25775 });
25776 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25777 cx.run_until_parked();
25778 editor.update(cx, |editor, cx| {
25779 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25780 });
25781}
25782
25783#[gpui::test]
25784async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25785 init_test(cx, |_| {});
25786
25787 cx.update(|cx| {
25788 register_project_item::<Editor>(cx);
25789 });
25790
25791 let fs = FakeFs::new(cx.executor());
25792 fs.insert_tree("/root1", json!({})).await;
25793 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25794 .await;
25795
25796 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25797 let (workspace, cx) =
25798 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25799
25800 let worktree_id = project.update(cx, |project, cx| {
25801 project.worktrees(cx).next().unwrap().read(cx).id()
25802 });
25803
25804 let handle = workspace
25805 .update_in(cx, |workspace, window, cx| {
25806 let project_path = (worktree_id, "one.pdf");
25807 workspace.open_path(project_path, None, true, window, cx)
25808 })
25809 .await
25810 .unwrap();
25811
25812 assert_eq!(
25813 handle.to_any().entity_type(),
25814 TypeId::of::<InvalidBufferView>()
25815 );
25816}
25817
25818#[gpui::test]
25819async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25820 init_test(cx, |_| {});
25821
25822 let language = Arc::new(Language::new(
25823 LanguageConfig::default(),
25824 Some(tree_sitter_rust::LANGUAGE.into()),
25825 ));
25826
25827 // Test hierarchical sibling navigation
25828 let text = r#"
25829 fn outer() {
25830 if condition {
25831 let a = 1;
25832 }
25833 let b = 2;
25834 }
25835
25836 fn another() {
25837 let c = 3;
25838 }
25839 "#;
25840
25841 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25842 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25843 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25844
25845 // Wait for parsing to complete
25846 editor
25847 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25848 .await;
25849
25850 editor.update_in(cx, |editor, window, cx| {
25851 // Start by selecting "let a = 1;" inside the if block
25852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25853 s.select_display_ranges([
25854 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25855 ]);
25856 });
25857
25858 let initial_selection = editor.selections.display_ranges(cx);
25859 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25860
25861 // Test select next sibling - should move up levels to find the next sibling
25862 // Since "let a = 1;" has no siblings in the if block, it should move up
25863 // to find "let b = 2;" which is a sibling of the if block
25864 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25865 let next_selection = editor.selections.display_ranges(cx);
25866
25867 // Should have a selection and it should be different from the initial
25868 assert_eq!(
25869 next_selection.len(),
25870 1,
25871 "Should have one selection after next"
25872 );
25873 assert_ne!(
25874 next_selection[0], initial_selection[0],
25875 "Next sibling selection should be different"
25876 );
25877
25878 // Test hierarchical navigation by going to the end of the current function
25879 // and trying to navigate to the next function
25880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25881 s.select_display_ranges([
25882 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25883 ]);
25884 });
25885
25886 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25887 let function_next_selection = editor.selections.display_ranges(cx);
25888
25889 // Should move to the next function
25890 assert_eq!(
25891 function_next_selection.len(),
25892 1,
25893 "Should have one selection after function next"
25894 );
25895
25896 // Test select previous sibling navigation
25897 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25898 let prev_selection = editor.selections.display_ranges(cx);
25899
25900 // Should have a selection and it should be different
25901 assert_eq!(
25902 prev_selection.len(),
25903 1,
25904 "Should have one selection after prev"
25905 );
25906 assert_ne!(
25907 prev_selection[0], function_next_selection[0],
25908 "Previous sibling selection should be different from next"
25909 );
25910 });
25911}
25912
25913#[gpui::test]
25914async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25915 init_test(cx, |_| {});
25916
25917 let mut cx = EditorTestContext::new(cx).await;
25918 cx.set_state(
25919 "let ˇvariable = 42;
25920let another = variable + 1;
25921let result = variable * 2;",
25922 );
25923
25924 // Set up document highlights manually (simulating LSP response)
25925 cx.update_editor(|editor, _window, cx| {
25926 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25927
25928 // Create highlights for "variable" occurrences
25929 let highlight_ranges = [
25930 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25931 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25932 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25933 ];
25934
25935 let anchor_ranges: Vec<_> = highlight_ranges
25936 .iter()
25937 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25938 .collect();
25939
25940 editor.highlight_background::<DocumentHighlightRead>(
25941 &anchor_ranges,
25942 |theme| theme.colors().editor_document_highlight_read_background,
25943 cx,
25944 );
25945 });
25946
25947 // Go to next highlight - should move to second "variable"
25948 cx.update_editor(|editor, window, cx| {
25949 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25950 });
25951 cx.assert_editor_state(
25952 "let variable = 42;
25953let another = ˇvariable + 1;
25954let result = variable * 2;",
25955 );
25956
25957 // Go to next highlight - should move to third "variable"
25958 cx.update_editor(|editor, window, cx| {
25959 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25960 });
25961 cx.assert_editor_state(
25962 "let variable = 42;
25963let another = variable + 1;
25964let result = ˇvariable * 2;",
25965 );
25966
25967 // Go to next highlight - should stay at third "variable" (no wrap-around)
25968 cx.update_editor(|editor, window, cx| {
25969 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25970 });
25971 cx.assert_editor_state(
25972 "let variable = 42;
25973let another = variable + 1;
25974let result = ˇvariable * 2;",
25975 );
25976
25977 // Now test going backwards from third position
25978 cx.update_editor(|editor, window, cx| {
25979 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25980 });
25981 cx.assert_editor_state(
25982 "let variable = 42;
25983let another = ˇvariable + 1;
25984let result = variable * 2;",
25985 );
25986
25987 // Go to previous highlight - should move to first "variable"
25988 cx.update_editor(|editor, window, cx| {
25989 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25990 });
25991 cx.assert_editor_state(
25992 "let ˇvariable = 42;
25993let another = variable + 1;
25994let result = variable * 2;",
25995 );
25996
25997 // Go to previous highlight - should stay on first "variable"
25998 cx.update_editor(|editor, window, cx| {
25999 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26000 });
26001 cx.assert_editor_state(
26002 "let ˇvariable = 42;
26003let another = variable + 1;
26004let result = variable * 2;",
26005 );
26006}
26007
26008#[track_caller]
26009fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26010 editor
26011 .all_inlays(cx)
26012 .into_iter()
26013 .filter_map(|inlay| inlay.get_color())
26014 .map(Rgba::from)
26015 .collect()
26016}