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 collections::HashMap;
17use futures::StreamExt;
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 SelectedFormatter,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
60 uri,
61};
62use workspace::{
63 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
64 OpenOptions, ViewId,
65 invalid_buffer_view::InvalidBufferView,
66 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
67 register_project_item,
68};
69
70#[gpui::test]
71fn test_edit_events(cx: &mut TestAppContext) {
72 init_test(cx, |_| {});
73
74 let buffer = cx.new(|cx| {
75 let mut buffer = language::Buffer::local("123456", cx);
76 buffer.set_group_interval(Duration::from_secs(1));
77 buffer
78 });
79
80 let events = Rc::new(RefCell::new(Vec::new()));
81 let editor1 = cx.add_window({
82 let events = events.clone();
83 |window, cx| {
84 let entity = cx.entity();
85 cx.subscribe_in(
86 &entity,
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor1", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 let editor2 = cx.add_window({
102 let events = events.clone();
103 |window, cx| {
104 cx.subscribe_in(
105 &cx.entity(),
106 window,
107 move |_, _, event: &EditorEvent, _, _| match event {
108 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
109 EditorEvent::BufferEdited => {
110 events.borrow_mut().push(("editor2", "buffer edited"))
111 }
112 _ => {}
113 },
114 )
115 .detach();
116 Editor::for_buffer(buffer.clone(), None, window, cx)
117 }
118 });
119
120 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
121
122 // Mutating editor 1 will emit an `Edited` event only for that editor.
123 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor1", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Mutating editor 2 will emit an `Edited` event only for that editor.
134 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Undoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Redoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Undoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Redoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // No event is emitted when the mutation is a no-op.
189 _ = editor2.update(cx, |editor, window, cx| {
190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
191 s.select_ranges([0..0])
192 });
193
194 editor.backspace(&Backspace, window, cx);
195 });
196 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
197}
198
199#[gpui::test]
200fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
201 init_test(cx, |_| {});
202
203 let mut now = Instant::now();
204 let group_interval = Duration::from_millis(1);
205 let buffer = cx.new(|cx| {
206 let mut buf = language::Buffer::local("123456", cx);
207 buf.set_group_interval(group_interval);
208 buf
209 });
210 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
211 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
212
213 _ = editor.update(cx, |editor, window, cx| {
214 editor.start_transaction_at(now, window, cx);
215 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
216 s.select_ranges([2..4])
217 });
218
219 editor.insert("cd", window, cx);
220 editor.end_transaction_at(now, cx);
221 assert_eq!(editor.text(cx), "12cd56");
222 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
223
224 editor.start_transaction_at(now, window, cx);
225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
226 s.select_ranges([4..5])
227 });
228 editor.insert("e", window, cx);
229 editor.end_transaction_at(now, cx);
230 assert_eq!(editor.text(cx), "12cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
232
233 now += group_interval + Duration::from_millis(1);
234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
235 s.select_ranges([2..2])
236 });
237
238 // Simulate an edit in another editor
239 buffer.update(cx, |buffer, cx| {
240 buffer.start_transaction_at(now, cx);
241 buffer.edit([(0..1, "a")], None, cx);
242 buffer.edit([(1..1, "b")], None, cx);
243 buffer.end_transaction_at(now, cx);
244 });
245
246 assert_eq!(editor.text(cx), "ab2cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
248
249 // Last transaction happened past the group interval in a different editor.
250 // Undo it individually and don't restore selections.
251 editor.undo(&Undo, window, cx);
252 assert_eq!(editor.text(cx), "12cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
254
255 // First two transactions happened within the group interval in this editor.
256 // Undo them together and restore selections.
257 editor.undo(&Undo, window, cx);
258 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
259 assert_eq!(editor.text(cx), "123456");
260 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
261
262 // Redo the first two transactions together.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "12cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
266
267 // Redo the last transaction on its own.
268 editor.redo(&Redo, window, cx);
269 assert_eq!(editor.text(cx), "ab2cde6");
270 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
271
272 // Test empty transactions.
273 editor.start_transaction_at(now, window, cx);
274 editor.end_transaction_at(now, cx);
275 editor.undo(&Undo, window, cx);
276 assert_eq!(editor.text(cx), "12cde6");
277 });
278}
279
280#[gpui::test]
281fn test_ime_composition(cx: &mut TestAppContext) {
282 init_test(cx, |_| {});
283
284 let buffer = cx.new(|cx| {
285 let mut buffer = language::Buffer::local("abcde", cx);
286 // Ensure automatic grouping doesn't occur.
287 buffer.set_group_interval(Duration::ZERO);
288 buffer
289 });
290
291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
292 cx.add_window(|window, cx| {
293 let mut editor = build_editor(buffer.clone(), window, cx);
294
295 // Start a new IME composition.
296 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
297 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
299 assert_eq!(editor.text(cx), "äbcde");
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Finalize IME composition.
306 editor.replace_text_in_range(None, "ā", window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // IME composition edits are grouped and are undone/redone at once.
311 editor.undo(&Default::default(), window, cx);
312 assert_eq!(editor.text(cx), "abcde");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314 editor.redo(&Default::default(), window, cx);
315 assert_eq!(editor.text(cx), "ābcde");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition.
319 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
320 assert_eq!(
321 editor.marked_text_ranges(cx),
322 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
323 );
324
325 // Undoing during an IME composition cancels it.
326 editor.undo(&Default::default(), window, cx);
327 assert_eq!(editor.text(cx), "ābcde");
328 assert_eq!(editor.marked_text_ranges(cx), None);
329
330 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
331 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
332 assert_eq!(editor.text(cx), "ābcdè");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
336 );
337
338 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
339 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
340 assert_eq!(editor.text(cx), "ābcdę");
341 assert_eq!(editor.marked_text_ranges(cx), None);
342
343 // Start a new IME composition with multiple cursors.
344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
345 s.select_ranges([
346 OffsetUtf16(1)..OffsetUtf16(1),
347 OffsetUtf16(3)..OffsetUtf16(3),
348 OffsetUtf16(5)..OffsetUtf16(5),
349 ])
350 });
351 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
352 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
353 assert_eq!(
354 editor.marked_text_ranges(cx),
355 Some(vec![
356 OffsetUtf16(0)..OffsetUtf16(3),
357 OffsetUtf16(4)..OffsetUtf16(7),
358 OffsetUtf16(8)..OffsetUtf16(11)
359 ])
360 );
361
362 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
363 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
364 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
365 assert_eq!(
366 editor.marked_text_ranges(cx),
367 Some(vec![
368 OffsetUtf16(1)..OffsetUtf16(2),
369 OffsetUtf16(5)..OffsetUtf16(6),
370 OffsetUtf16(9)..OffsetUtf16(10)
371 ])
372 );
373
374 // Finalize IME composition with multiple cursors.
375 editor.replace_text_in_range(Some(9..10), "2", window, cx);
376 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
377 assert_eq!(editor.marked_text_ranges(cx), None);
378
379 editor
380 });
381}
382
383#[gpui::test]
384fn test_selection_with_mouse(cx: &mut TestAppContext) {
385 init_test(cx, |_| {});
386
387 let editor = cx.add_window(|window, cx| {
388 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
389 build_editor(buffer, window, cx)
390 });
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
394 });
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(3), 3),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(1), 1),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.end_selection(window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(3), 3),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
452 );
453
454 _ = editor.update(cx, |editor, window, cx| {
455 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
456 editor.update_selection(
457 DisplayPoint::new(DisplayRow(0), 0),
458 0,
459 gpui::Point::<f32>::default(),
460 window,
461 cx,
462 );
463 });
464
465 assert_eq!(
466 editor
467 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
468 .unwrap(),
469 [
470 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
471 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
472 ]
473 );
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 assert_eq!(
480 editor
481 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
482 .unwrap(),
483 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
484 );
485}
486
487#[gpui::test]
488fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
489 init_test(cx, |_| {});
490
491 let editor = cx.add_window(|window, cx| {
492 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
493 build_editor(buffer, window, cx)
494 });
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.end_selection(window, cx);
502 });
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
518 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.end_selection(window, cx);
528 });
529
530 assert_eq!(
531 editor
532 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
533 .unwrap(),
534 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
535 );
536}
537
538#[gpui::test]
539fn test_canceling_pending_selection(cx: &mut TestAppContext) {
540 init_test(cx, |_| {});
541
542 let editor = cx.add_window(|window, cx| {
543 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
544 build_editor(buffer, window, cx)
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
549 assert_eq!(
550 editor.selections.display_ranges(cx),
551 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
552 );
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.update_selection(
557 DisplayPoint::new(DisplayRow(3), 3),
558 0,
559 gpui::Point::<f32>::default(),
560 window,
561 cx,
562 );
563 assert_eq!(
564 editor.selections.display_ranges(cx),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
566 );
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.cancel(&Cancel, window, cx);
571 editor.update_selection(
572 DisplayPoint::new(DisplayRow(1), 1),
573 0,
574 gpui::Point::<f32>::default(),
575 window,
576 cx,
577 );
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
581 );
582 });
583}
584
585#[gpui::test]
586fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600
601 editor.move_down(&Default::default(), window, cx);
602 assert_eq!(
603 editor.selections.display_ranges(cx),
604 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
605 );
606
607 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
608 assert_eq!(
609 editor.selections.display_ranges(cx),
610 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
611 );
612
613 editor.move_up(&Default::default(), window, cx);
614 assert_eq!(
615 editor.selections.display_ranges(cx),
616 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
617 );
618 });
619}
620
621#[gpui::test]
622fn test_clone(cx: &mut TestAppContext) {
623 init_test(cx, |_| {});
624
625 let (text, selection_ranges) = marked_text_ranges(
626 indoc! {"
627 one
628 two
629 threeˇ
630 four
631 fiveˇ
632 "},
633 true,
634 );
635
636 let editor = cx.add_window(|window, cx| {
637 let buffer = MultiBuffer::build_simple(&text, cx);
638 build_editor(buffer, window, cx)
639 });
640
641 _ = editor.update(cx, |editor, window, cx| {
642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
643 s.select_ranges(selection_ranges.clone())
644 });
645 editor.fold_creases(
646 vec![
647 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
648 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
649 ],
650 true,
651 window,
652 cx,
653 );
654 });
655
656 let cloned_editor = editor
657 .update(cx, |editor, _, cx| {
658 cx.open_window(Default::default(), |window, cx| {
659 cx.new(|cx| editor.clone(window, cx))
660 })
661 })
662 .unwrap()
663 .unwrap();
664
665 let snapshot = editor
666 .update(cx, |e, window, cx| e.snapshot(window, cx))
667 .unwrap();
668 let cloned_snapshot = cloned_editor
669 .update(cx, |e, window, cx| e.snapshot(window, cx))
670 .unwrap();
671
672 assert_eq!(
673 cloned_editor
674 .update(cx, |e, _, cx| e.display_text(cx))
675 .unwrap(),
676 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
677 );
678 assert_eq!(
679 cloned_snapshot
680 .folds_in_range(0..text.len())
681 .collect::<Vec<_>>(),
682 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
683 );
684 assert_set_eq!(
685 cloned_editor
686 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
687 .unwrap(),
688 editor
689 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
690 .unwrap()
691 );
692 assert_set_eq!(
693 cloned_editor
694 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
695 .unwrap(),
696 editor
697 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
698 .unwrap()
699 );
700}
701
702#[gpui::test]
703async fn test_navigation_history(cx: &mut TestAppContext) {
704 init_test(cx, |_| {});
705
706 use workspace::item::Item;
707
708 let fs = FakeFs::new(cx.executor());
709 let project = Project::test(fs, [], cx).await;
710 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
711 let pane = workspace
712 .update(cx, |workspace, _, _| workspace.active_pane().clone())
713 .unwrap();
714
715 _ = workspace.update(cx, |_v, window, cx| {
716 cx.new(|cx| {
717 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
718 let mut editor = build_editor(buffer, window, cx);
719 let handle = cx.entity();
720 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
721
722 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
723 editor.nav_history.as_mut().unwrap().pop_backward(cx)
724 }
725
726 // Move the cursor a small distance.
727 // Nothing is added to the navigation history.
728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
729 s.select_display_ranges([
730 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
731 ])
732 });
733 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
734 s.select_display_ranges([
735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
736 ])
737 });
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a large distance.
741 // The history can jump back to the previous position.
742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
743 s.select_display_ranges([
744 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
745 ])
746 });
747 let nav_entry = pop_history(&mut editor, cx).unwrap();
748 editor.navigate(nav_entry.data.unwrap(), window, cx);
749 assert_eq!(nav_entry.item.id(), cx.entity_id());
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
753 );
754 assert!(pop_history(&mut editor, cx).is_none());
755
756 // Move the cursor a small distance via the mouse.
757 // Nothing is added to the navigation history.
758 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
759 editor.end_selection(window, cx);
760 assert_eq!(
761 editor.selections.display_ranges(cx),
762 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
763 );
764 assert!(pop_history(&mut editor, cx).is_none());
765
766 // Move the cursor a large distance via the mouse.
767 // The history can jump back to the previous position.
768 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
769 editor.end_selection(window, cx);
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
773 );
774 let nav_entry = pop_history(&mut editor, cx).unwrap();
775 editor.navigate(nav_entry.data.unwrap(), window, cx);
776 assert_eq!(nav_entry.item.id(), cx.entity_id());
777 assert_eq!(
778 editor.selections.display_ranges(cx),
779 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
780 );
781 assert!(pop_history(&mut editor, cx).is_none());
782
783 // Set scroll position to check later
784 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
785 let original_scroll_position = editor.scroll_manager.anchor();
786
787 // Jump to the end of the document and adjust scroll
788 editor.move_to_end(&MoveToEnd, window, cx);
789 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
790 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
791
792 let nav_entry = pop_history(&mut editor, cx).unwrap();
793 editor.navigate(nav_entry.data.unwrap(), window, cx);
794 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
795
796 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
797 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
798 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
799 let invalid_point = Point::new(9999, 0);
800 editor.navigate(
801 Box::new(NavigationData {
802 cursor_anchor: invalid_anchor,
803 cursor_position: invalid_point,
804 scroll_anchor: ScrollAnchor {
805 anchor: invalid_anchor,
806 offset: Default::default(),
807 },
808 scroll_top_row: invalid_point.row,
809 }),
810 window,
811 cx,
812 );
813 assert_eq!(
814 editor.selections.display_ranges(cx),
815 &[editor.max_point(cx)..editor.max_point(cx)]
816 );
817 assert_eq!(
818 editor.scroll_position(cx),
819 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
820 );
821
822 editor
823 })
824 });
825}
826
827#[gpui::test]
828fn test_cancel(cx: &mut TestAppContext) {
829 init_test(cx, |_| {});
830
831 let editor = cx.add_window(|window, cx| {
832 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
833 build_editor(buffer, window, cx)
834 });
835
836 _ = editor.update(cx, |editor, window, cx| {
837 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
838 editor.update_selection(
839 DisplayPoint::new(DisplayRow(1), 1),
840 0,
841 gpui::Point::<f32>::default(),
842 window,
843 cx,
844 );
845 editor.end_selection(window, cx);
846
847 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
848 editor.update_selection(
849 DisplayPoint::new(DisplayRow(0), 3),
850 0,
851 gpui::Point::<f32>::default(),
852 window,
853 cx,
854 );
855 editor.end_selection(window, cx);
856 assert_eq!(
857 editor.selections.display_ranges(cx),
858 [
859 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
860 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
861 ]
862 );
863 });
864
865 _ = editor.update(cx, |editor, window, cx| {
866 editor.cancel(&Cancel, window, cx);
867 assert_eq!(
868 editor.selections.display_ranges(cx),
869 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
870 );
871 });
872
873 _ = editor.update(cx, |editor, window, cx| {
874 editor.cancel(&Cancel, window, cx);
875 assert_eq!(
876 editor.selections.display_ranges(cx),
877 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
878 );
879 });
880}
881
882#[gpui::test]
883fn test_fold_action(cx: &mut TestAppContext) {
884 init_test(cx, |_| {});
885
886 let editor = cx.add_window(|window, cx| {
887 let buffer = MultiBuffer::build_simple(
888 &"
889 impl Foo {
890 // Hello!
891
892 fn a() {
893 1
894 }
895
896 fn b() {
897 2
898 }
899
900 fn c() {
901 3
902 }
903 }
904 "
905 .unindent(),
906 cx,
907 );
908 build_editor(buffer, window, cx)
909 });
910
911 _ = editor.update(cx, |editor, window, cx| {
912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
913 s.select_display_ranges([
914 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
915 ]);
916 });
917 editor.fold(&Fold, window, cx);
918 assert_eq!(
919 editor.display_text(cx),
920 "
921 impl Foo {
922 // Hello!
923
924 fn a() {
925 1
926 }
927
928 fn b() {⋯
929 }
930
931 fn c() {⋯
932 }
933 }
934 "
935 .unindent(),
936 );
937
938 editor.fold(&Fold, window, cx);
939 assert_eq!(
940 editor.display_text(cx),
941 "
942 impl Foo {⋯
943 }
944 "
945 .unindent(),
946 );
947
948 editor.unfold_lines(&UnfoldLines, window, cx);
949 assert_eq!(
950 editor.display_text(cx),
951 "
952 impl Foo {
953 // Hello!
954
955 fn a() {
956 1
957 }
958
959 fn b() {⋯
960 }
961
962 fn c() {⋯
963 }
964 }
965 "
966 .unindent(),
967 );
968
969 editor.unfold_lines(&UnfoldLines, window, cx);
970 assert_eq!(
971 editor.display_text(cx),
972 editor.buffer.read(cx).read(cx).text()
973 );
974 });
975}
976
977#[gpui::test]
978fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
979 init_test(cx, |_| {});
980
981 let editor = cx.add_window(|window, cx| {
982 let buffer = MultiBuffer::build_simple(
983 &"
984 class Foo:
985 # Hello!
986
987 def a():
988 print(1)
989
990 def b():
991 print(2)
992
993 def c():
994 print(3)
995 "
996 .unindent(),
997 cx,
998 );
999 build_editor(buffer, window, cx)
1000 });
1001
1002 _ = editor.update(cx, |editor, window, cx| {
1003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1004 s.select_display_ranges([
1005 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1006 ]);
1007 });
1008 editor.fold(&Fold, window, cx);
1009 assert_eq!(
1010 editor.display_text(cx),
1011 "
1012 class Foo:
1013 # Hello!
1014
1015 def a():
1016 print(1)
1017
1018 def b():⋯
1019
1020 def c():⋯
1021 "
1022 .unindent(),
1023 );
1024
1025 editor.fold(&Fold, window, cx);
1026 assert_eq!(
1027 editor.display_text(cx),
1028 "
1029 class Foo:⋯
1030 "
1031 .unindent(),
1032 );
1033
1034 editor.unfold_lines(&UnfoldLines, window, cx);
1035 assert_eq!(
1036 editor.display_text(cx),
1037 "
1038 class Foo:
1039 # Hello!
1040
1041 def a():
1042 print(1)
1043
1044 def b():⋯
1045
1046 def c():⋯
1047 "
1048 .unindent(),
1049 );
1050
1051 editor.unfold_lines(&UnfoldLines, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 editor.buffer.read(cx).read(cx).text()
1055 );
1056 });
1057}
1058
1059#[gpui::test]
1060fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1061 init_test(cx, |_| {});
1062
1063 let editor = cx.add_window(|window, cx| {
1064 let buffer = MultiBuffer::build_simple(
1065 &"
1066 class Foo:
1067 # Hello!
1068
1069 def a():
1070 print(1)
1071
1072 def b():
1073 print(2)
1074
1075
1076 def c():
1077 print(3)
1078
1079
1080 "
1081 .unindent(),
1082 cx,
1083 );
1084 build_editor(buffer, window, cx)
1085 });
1086
1087 _ = editor.update(cx, |editor, window, cx| {
1088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1089 s.select_display_ranges([
1090 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1091 ]);
1092 });
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:
1098 # Hello!
1099
1100 def a():
1101 print(1)
1102
1103 def b():⋯
1104
1105
1106 def c():⋯
1107
1108
1109 "
1110 .unindent(),
1111 );
1112
1113 editor.fold(&Fold, window, cx);
1114 assert_eq!(
1115 editor.display_text(cx),
1116 "
1117 class Foo:⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 "
1128 class Foo:
1129 # Hello!
1130
1131 def a():
1132 print(1)
1133
1134 def b():⋯
1135
1136
1137 def c():⋯
1138
1139
1140 "
1141 .unindent(),
1142 );
1143
1144 editor.unfold_lines(&UnfoldLines, window, cx);
1145 assert_eq!(
1146 editor.display_text(cx),
1147 editor.buffer.read(cx).read(cx).text()
1148 );
1149 });
1150}
1151
1152#[gpui::test]
1153fn test_fold_at_level(cx: &mut TestAppContext) {
1154 init_test(cx, |_| {});
1155
1156 let editor = cx.add_window(|window, cx| {
1157 let buffer = MultiBuffer::build_simple(
1158 &"
1159 class Foo:
1160 # Hello!
1161
1162 def a():
1163 print(1)
1164
1165 def b():
1166 print(2)
1167
1168
1169 class Bar:
1170 # World!
1171
1172 def a():
1173 print(1)
1174
1175 def b():
1176 print(2)
1177
1178
1179 "
1180 .unindent(),
1181 cx,
1182 );
1183 build_editor(buffer, window, cx)
1184 });
1185
1186 _ = editor.update(cx, |editor, window, cx| {
1187 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1188 assert_eq!(
1189 editor.display_text(cx),
1190 "
1191 class Foo:
1192 # Hello!
1193
1194 def a():⋯
1195
1196 def b():⋯
1197
1198
1199 class Bar:
1200 # World!
1201
1202 def a():⋯
1203
1204 def b():⋯
1205
1206
1207 "
1208 .unindent(),
1209 );
1210
1211 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1212 assert_eq!(
1213 editor.display_text(cx),
1214 "
1215 class Foo:⋯
1216
1217
1218 class Bar:⋯
1219
1220
1221 "
1222 .unindent(),
1223 );
1224
1225 editor.unfold_all(&UnfoldAll, window, cx);
1226 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 "
1230 class Foo:
1231 # Hello!
1232
1233 def a():
1234 print(1)
1235
1236 def b():
1237 print(2)
1238
1239
1240 class Bar:
1241 # World!
1242
1243 def a():
1244 print(1)
1245
1246 def b():
1247 print(2)
1248
1249
1250 "
1251 .unindent(),
1252 );
1253
1254 assert_eq!(
1255 editor.display_text(cx),
1256 editor.buffer.read(cx).read(cx).text()
1257 );
1258 });
1259}
1260
1261#[gpui::test]
1262fn test_move_cursor(cx: &mut TestAppContext) {
1263 init_test(cx, |_| {});
1264
1265 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1266 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1267
1268 buffer.update(cx, |buffer, cx| {
1269 buffer.edit(
1270 vec![
1271 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1272 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1273 ],
1274 None,
1275 cx,
1276 );
1277 });
1278 _ = editor.update(cx, |editor, window, cx| {
1279 assert_eq!(
1280 editor.selections.display_ranges(cx),
1281 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1282 );
1283
1284 editor.move_down(&MoveDown, window, cx);
1285 assert_eq!(
1286 editor.selections.display_ranges(cx),
1287 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1288 );
1289
1290 editor.move_right(&MoveRight, window, cx);
1291 assert_eq!(
1292 editor.selections.display_ranges(cx),
1293 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1294 );
1295
1296 editor.move_left(&MoveLeft, window, cx);
1297 assert_eq!(
1298 editor.selections.display_ranges(cx),
1299 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1300 );
1301
1302 editor.move_up(&MoveUp, window, cx);
1303 assert_eq!(
1304 editor.selections.display_ranges(cx),
1305 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1306 );
1307
1308 editor.move_to_end(&MoveToEnd, window, cx);
1309 assert_eq!(
1310 editor.selections.display_ranges(cx),
1311 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1312 );
1313
1314 editor.move_to_beginning(&MoveToBeginning, window, cx);
1315 assert_eq!(
1316 editor.selections.display_ranges(cx),
1317 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1318 );
1319
1320 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1321 s.select_display_ranges([
1322 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1323 ]);
1324 });
1325 editor.select_to_beginning(&SelectToBeginning, window, cx);
1326 assert_eq!(
1327 editor.selections.display_ranges(cx),
1328 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1329 );
1330
1331 editor.select_to_end(&SelectToEnd, window, cx);
1332 assert_eq!(
1333 editor.selections.display_ranges(cx),
1334 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1335 );
1336 });
1337}
1338
1339#[gpui::test]
1340fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1341 init_test(cx, |_| {});
1342
1343 let editor = cx.add_window(|window, cx| {
1344 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1345 build_editor(buffer, window, cx)
1346 });
1347
1348 assert_eq!('🟥'.len_utf8(), 4);
1349 assert_eq!('α'.len_utf8(), 2);
1350
1351 _ = editor.update(cx, |editor, window, cx| {
1352 editor.fold_creases(
1353 vec![
1354 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1355 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1356 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1357 ],
1358 true,
1359 window,
1360 cx,
1361 );
1362 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
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 editor.move_right(&MoveRight, window, cx);
1375 assert_eq!(
1376 editor.selections.display_ranges(cx),
1377 &[empty_range(0, "🟥🟧⋯".len())]
1378 );
1379
1380 editor.move_down(&MoveDown, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(1, "ab⋯e".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, "ab".len())]
1394 );
1395 editor.move_left(&MoveLeft, window, cx);
1396 assert_eq!(
1397 editor.selections.display_ranges(cx),
1398 &[empty_range(1, "a".len())]
1399 );
1400
1401 editor.move_down(&MoveDown, 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 editor.move_right(&MoveRight, window, cx);
1417 assert_eq!(
1418 editor.selections.display_ranges(cx),
1419 &[empty_range(2, "αβ⋯ε".len())]
1420 );
1421
1422 editor.move_up(&MoveUp, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(1, "ab⋯e".len())]
1426 );
1427 editor.move_down(&MoveDown, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(2, "αβ⋯ε".len())]
1431 );
1432 editor.move_up(&MoveUp, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(1, "ab⋯e".len())]
1436 );
1437
1438 editor.move_up(&MoveUp, 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 editor.move_left(&MoveLeft, window, cx);
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[empty_range(0, "".len())]
1452 );
1453 });
1454}
1455
1456#[gpui::test]
1457fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1458 init_test(cx, |_| {});
1459
1460 let editor = cx.add_window(|window, cx| {
1461 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1462 build_editor(buffer, window, cx)
1463 });
1464 _ = editor.update(cx, |editor, window, cx| {
1465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1466 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1467 });
1468
1469 // moving above start of document should move selection to start of document,
1470 // but the next move down should still be at the original goal_x
1471 editor.move_up(&MoveUp, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(0, "".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(1, "abcd".len())]
1481 );
1482
1483 editor.move_down(&MoveDown, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[empty_range(2, "αβγ".len())]
1487 );
1488
1489 editor.move_down(&MoveDown, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(3, "abcd".len())]
1493 );
1494
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1499 );
1500
1501 // moving past end of document should not change goal_x
1502 editor.move_down(&MoveDown, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(5, "".len())]
1506 );
1507
1508 editor.move_down(&MoveDown, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(5, "".len())]
1512 );
1513
1514 editor.move_up(&MoveUp, window, cx);
1515 assert_eq!(
1516 editor.selections.display_ranges(cx),
1517 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1518 );
1519
1520 editor.move_up(&MoveUp, window, cx);
1521 assert_eq!(
1522 editor.selections.display_ranges(cx),
1523 &[empty_range(3, "abcd".len())]
1524 );
1525
1526 editor.move_up(&MoveUp, window, cx);
1527 assert_eq!(
1528 editor.selections.display_ranges(cx),
1529 &[empty_range(2, "αβγ".len())]
1530 );
1531 });
1532}
1533
1534#[gpui::test]
1535fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1536 init_test(cx, |_| {});
1537 let move_to_beg = MoveToBeginningOfLine {
1538 stop_at_soft_wraps: true,
1539 stop_at_indent: true,
1540 };
1541
1542 let delete_to_beg = DeleteToBeginningOfLine {
1543 stop_at_indent: false,
1544 };
1545
1546 let move_to_end = MoveToEndOfLine {
1547 stop_at_soft_wraps: true,
1548 };
1549
1550 let editor = cx.add_window(|window, cx| {
1551 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1552 build_editor(buffer, window, cx)
1553 });
1554 _ = editor.update(cx, |editor, window, cx| {
1555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1556 s.select_display_ranges([
1557 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1558 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1559 ]);
1560 });
1561 });
1562
1563 _ = editor.update(cx, |editor, window, cx| {
1564 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1565 assert_eq!(
1566 editor.selections.display_ranges(cx),
1567 &[
1568 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1569 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1570 ]
1571 );
1572 });
1573
1574 _ = editor.update(cx, |editor, window, cx| {
1575 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1576 assert_eq!(
1577 editor.selections.display_ranges(cx),
1578 &[
1579 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1580 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1581 ]
1582 );
1583 });
1584
1585 _ = editor.update(cx, |editor, window, cx| {
1586 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1587 assert_eq!(
1588 editor.selections.display_ranges(cx),
1589 &[
1590 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1591 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1592 ]
1593 );
1594 });
1595
1596 _ = editor.update(cx, |editor, window, cx| {
1597 editor.move_to_end_of_line(&move_to_end, window, cx);
1598 assert_eq!(
1599 editor.selections.display_ranges(cx),
1600 &[
1601 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1602 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1603 ]
1604 );
1605 });
1606
1607 // Moving to the end of line again is a no-op.
1608 _ = editor.update(cx, |editor, window, cx| {
1609 editor.move_to_end_of_line(&move_to_end, window, cx);
1610 assert_eq!(
1611 editor.selections.display_ranges(cx),
1612 &[
1613 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1614 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1615 ]
1616 );
1617 });
1618
1619 _ = editor.update(cx, |editor, window, cx| {
1620 editor.move_left(&MoveLeft, window, cx);
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_beginning_of_line(
1658 &SelectToBeginningOfLine {
1659 stop_at_soft_wraps: true,
1660 stop_at_indent: true,
1661 },
1662 window,
1663 cx,
1664 );
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[
1668 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1669 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1670 ]
1671 );
1672 });
1673
1674 _ = editor.update(cx, |editor, window, cx| {
1675 editor.select_to_end_of_line(
1676 &SelectToEndOfLine {
1677 stop_at_soft_wraps: true,
1678 },
1679 window,
1680 cx,
1681 );
1682 assert_eq!(
1683 editor.selections.display_ranges(cx),
1684 &[
1685 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1686 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1687 ]
1688 );
1689 });
1690
1691 _ = editor.update(cx, |editor, window, cx| {
1692 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1693 assert_eq!(editor.display_text(cx), "ab\n de");
1694 assert_eq!(
1695 editor.selections.display_ranges(cx),
1696 &[
1697 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1698 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1699 ]
1700 );
1701 });
1702
1703 _ = editor.update(cx, |editor, window, cx| {
1704 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1705 assert_eq!(editor.display_text(cx), "\n");
1706 assert_eq!(
1707 editor.selections.display_ranges(cx),
1708 &[
1709 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1711 ]
1712 );
1713 });
1714}
1715
1716#[gpui::test]
1717fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1718 init_test(cx, |_| {});
1719 let move_to_beg = MoveToBeginningOfLine {
1720 stop_at_soft_wraps: false,
1721 stop_at_indent: false,
1722 };
1723
1724 let move_to_end = MoveToEndOfLine {
1725 stop_at_soft_wraps: false,
1726 };
1727
1728 let editor = cx.add_window(|window, cx| {
1729 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1730 build_editor(buffer, window, cx)
1731 });
1732
1733 _ = editor.update(cx, |editor, window, cx| {
1734 editor.set_wrap_width(Some(140.0.into()), cx);
1735
1736 // We expect the following lines after wrapping
1737 // ```
1738 // thequickbrownfox
1739 // jumpedoverthelazydo
1740 // gs
1741 // ```
1742 // The final `gs` was soft-wrapped onto a new line.
1743 assert_eq!(
1744 "thequickbrownfox\njumpedoverthelaz\nydogs",
1745 editor.display_text(cx),
1746 );
1747
1748 // First, let's assert behavior on the first line, that was not soft-wrapped.
1749 // Start the cursor at the `k` on the first line
1750 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1751 s.select_display_ranges([
1752 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1753 ]);
1754 });
1755
1756 // Moving to the beginning of the line should put us at the beginning of the line.
1757 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Moving to the end of the line should put us at the end of the line.
1764 editor.move_to_end_of_line(&move_to_end, window, cx);
1765 assert_eq!(
1766 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1767 editor.selections.display_ranges(cx)
1768 );
1769
1770 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1771 // Start the cursor at the last line (`y` that was wrapped to a new line)
1772 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1773 s.select_display_ranges([
1774 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1775 ]);
1776 });
1777
1778 // Moving to the beginning of the line should put us at the start of the second line of
1779 // display text, i.e., the `j`.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the beginning of the line again should be a no-op.
1787 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1788 assert_eq!(
1789 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1790 editor.selections.display_ranges(cx)
1791 );
1792
1793 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1794 // next display line.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800
1801 // Moving to the end of the line again should be a no-op.
1802 editor.move_to_end_of_line(&move_to_end, window, cx);
1803 assert_eq!(
1804 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1805 editor.selections.display_ranges(cx)
1806 );
1807 });
1808}
1809
1810#[gpui::test]
1811fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1812 init_test(cx, |_| {});
1813
1814 let move_to_beg = MoveToBeginningOfLine {
1815 stop_at_soft_wraps: true,
1816 stop_at_indent: true,
1817 };
1818
1819 let select_to_beg = SelectToBeginningOfLine {
1820 stop_at_soft_wraps: true,
1821 stop_at_indent: true,
1822 };
1823
1824 let delete_to_beg = DeleteToBeginningOfLine {
1825 stop_at_indent: true,
1826 };
1827
1828 let move_to_end = MoveToEndOfLine {
1829 stop_at_soft_wraps: false,
1830 };
1831
1832 let editor = cx.add_window(|window, cx| {
1833 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1834 build_editor(buffer, window, cx)
1835 });
1836
1837 _ = editor.update(cx, |editor, window, cx| {
1838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1839 s.select_display_ranges([
1840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1841 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1842 ]);
1843 });
1844
1845 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1846 // and the second cursor at the first non-whitespace character in the line.
1847 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1848 assert_eq!(
1849 editor.selections.display_ranges(cx),
1850 &[
1851 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1852 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1853 ]
1854 );
1855
1856 // Moving to the beginning of the line again should be a no-op for the first cursor,
1857 // and should move the second cursor to the beginning of the line.
1858 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1859 assert_eq!(
1860 editor.selections.display_ranges(cx),
1861 &[
1862 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1863 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1864 ]
1865 );
1866
1867 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1868 // and should move the second cursor back to the first non-whitespace character in the line.
1869 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1870 assert_eq!(
1871 editor.selections.display_ranges(cx),
1872 &[
1873 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1874 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1875 ]
1876 );
1877
1878 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1879 // and to the first non-whitespace character in the line for the second cursor.
1880 editor.move_to_end_of_line(&move_to_end, window, cx);
1881 editor.move_left(&MoveLeft, window, cx);
1882 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1883 assert_eq!(
1884 editor.selections.display_ranges(cx),
1885 &[
1886 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1887 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1888 ]
1889 );
1890
1891 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1892 // and should select to the beginning of the line for the second cursor.
1893 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1894 assert_eq!(
1895 editor.selections.display_ranges(cx),
1896 &[
1897 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1898 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1899 ]
1900 );
1901
1902 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1903 // and should delete to the first non-whitespace character in the line for the second cursor.
1904 editor.move_to_end_of_line(&move_to_end, window, cx);
1905 editor.move_left(&MoveLeft, window, cx);
1906 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1907 assert_eq!(editor.text(cx), "c\n f");
1908 });
1909}
1910
1911#[gpui::test]
1912fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1913 init_test(cx, |_| {});
1914
1915 let move_to_beg = MoveToBeginningOfLine {
1916 stop_at_soft_wraps: true,
1917 stop_at_indent: true,
1918 };
1919
1920 let editor = cx.add_window(|window, cx| {
1921 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1922 build_editor(buffer, window, cx)
1923 });
1924
1925 _ = editor.update(cx, |editor, window, cx| {
1926 // test cursor between line_start and indent_start
1927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1928 s.select_display_ranges([
1929 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1930 ]);
1931 });
1932
1933 // cursor should move to line_start
1934 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1935 assert_eq!(
1936 editor.selections.display_ranges(cx),
1937 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1938 );
1939
1940 // cursor should move to indent_start
1941 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1942 assert_eq!(
1943 editor.selections.display_ranges(cx),
1944 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1945 );
1946
1947 // cursor should move to back to line_start
1948 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1949 assert_eq!(
1950 editor.selections.display_ranges(cx),
1951 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1952 );
1953 });
1954}
1955
1956#[gpui::test]
1957fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1958 init_test(cx, |_| {});
1959
1960 let editor = cx.add_window(|window, cx| {
1961 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1962 build_editor(buffer, window, cx)
1963 });
1964 _ = editor.update(cx, |editor, window, cx| {
1965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1966 s.select_display_ranges([
1967 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1968 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1969 ])
1970 });
1971 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1972 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1973
1974 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1975 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1976
1977 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1978 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1979
1980 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1981 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1982
1983 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1984 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1985
1986 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1987 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1991
1992 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1993 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1994
1995 editor.move_right(&MoveRight, window, cx);
1996 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1997 assert_selection_ranges(
1998 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1999 editor,
2000 cx,
2001 );
2002
2003 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2004 assert_selection_ranges(
2005 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2006 editor,
2007 cx,
2008 );
2009
2010 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2011 assert_selection_ranges(
2012 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2013 editor,
2014 cx,
2015 );
2016 });
2017}
2018
2019#[gpui::test]
2020fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2021 init_test(cx, |_| {});
2022
2023 let editor = cx.add_window(|window, cx| {
2024 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2025 build_editor(buffer, window, cx)
2026 });
2027
2028 _ = editor.update(cx, |editor, window, cx| {
2029 editor.set_wrap_width(Some(140.0.into()), cx);
2030 assert_eq!(
2031 editor.display_text(cx),
2032 "use one::{\n two::three::\n four::five\n};"
2033 );
2034
2035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2036 s.select_display_ranges([
2037 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2038 ]);
2039 });
2040
2041 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2042 assert_eq!(
2043 editor.selections.display_ranges(cx),
2044 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2045 );
2046
2047 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2048 assert_eq!(
2049 editor.selections.display_ranges(cx),
2050 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2051 );
2052
2053 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2054 assert_eq!(
2055 editor.selections.display_ranges(cx),
2056 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2057 );
2058
2059 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2060 assert_eq!(
2061 editor.selections.display_ranges(cx),
2062 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2063 );
2064
2065 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2066 assert_eq!(
2067 editor.selections.display_ranges(cx),
2068 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2069 );
2070
2071 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2072 assert_eq!(
2073 editor.selections.display_ranges(cx),
2074 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2075 );
2076 });
2077}
2078
2079#[gpui::test]
2080async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2081 init_test(cx, |_| {});
2082 let mut cx = EditorTestContext::new(cx).await;
2083
2084 let line_height = cx.editor(|editor, window, _| {
2085 editor
2086 .style()
2087 .unwrap()
2088 .text
2089 .line_height_in_pixels(window.rem_size())
2090 });
2091 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2092
2093 cx.set_state(
2094 &r#"ˇone
2095 two
2096
2097 three
2098 fourˇ
2099 five
2100
2101 six"#
2102 .unindent(),
2103 );
2104
2105 cx.update_editor(|editor, window, cx| {
2106 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2107 });
2108 cx.assert_editor_state(
2109 &r#"one
2110 two
2111 ˇ
2112 three
2113 four
2114 five
2115 ˇ
2116 six"#
2117 .unindent(),
2118 );
2119
2120 cx.update_editor(|editor, window, cx| {
2121 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2122 });
2123 cx.assert_editor_state(
2124 &r#"one
2125 two
2126
2127 three
2128 four
2129 five
2130 ˇ
2131 sixˇ"#
2132 .unindent(),
2133 );
2134
2135 cx.update_editor(|editor, window, cx| {
2136 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2137 });
2138 cx.assert_editor_state(
2139 &r#"one
2140 two
2141
2142 three
2143 four
2144 five
2145
2146 sixˇ"#
2147 .unindent(),
2148 );
2149
2150 cx.update_editor(|editor, window, cx| {
2151 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2152 });
2153 cx.assert_editor_state(
2154 &r#"one
2155 two
2156
2157 three
2158 four
2159 five
2160 ˇ
2161 six"#
2162 .unindent(),
2163 );
2164
2165 cx.update_editor(|editor, window, cx| {
2166 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2167 });
2168 cx.assert_editor_state(
2169 &r#"one
2170 two
2171 ˇ
2172 three
2173 four
2174 five
2175
2176 six"#
2177 .unindent(),
2178 );
2179
2180 cx.update_editor(|editor, window, cx| {
2181 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2182 });
2183 cx.assert_editor_state(
2184 &r#"ˇone
2185 two
2186
2187 three
2188 four
2189 five
2190
2191 six"#
2192 .unindent(),
2193 );
2194}
2195
2196#[gpui::test]
2197async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200 let line_height = cx.editor(|editor, window, _| {
2201 editor
2202 .style()
2203 .unwrap()
2204 .text
2205 .line_height_in_pixels(window.rem_size())
2206 });
2207 let window = cx.window;
2208 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2209
2210 cx.set_state(
2211 r#"ˇone
2212 two
2213 three
2214 four
2215 five
2216 six
2217 seven
2218 eight
2219 nine
2220 ten
2221 "#,
2222 );
2223
2224 cx.update_editor(|editor, window, cx| {
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 0.)
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., 3.)
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., 6.)
2238 );
2239 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2240 assert_eq!(
2241 editor.snapshot(window, cx).scroll_position(),
2242 gpui::Point::new(0., 3.)
2243 );
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., 1.)
2249 );
2250 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2251 assert_eq!(
2252 editor.snapshot(window, cx).scroll_position(),
2253 gpui::Point::new(0., 3.)
2254 );
2255 });
2256}
2257
2258#[gpui::test]
2259async fn test_autoscroll(cx: &mut TestAppContext) {
2260 init_test(cx, |_| {});
2261 let mut cx = EditorTestContext::new(cx).await;
2262
2263 let line_height = cx.update_editor(|editor, window, cx| {
2264 editor.set_vertical_scroll_margin(2, cx);
2265 editor
2266 .style()
2267 .unwrap()
2268 .text
2269 .line_height_in_pixels(window.rem_size())
2270 });
2271 let window = cx.window;
2272 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2273
2274 cx.set_state(
2275 r#"ˇone
2276 two
2277 three
2278 four
2279 five
2280 six
2281 seven
2282 eight
2283 nine
2284 ten
2285 "#,
2286 );
2287 cx.update_editor(|editor, window, cx| {
2288 assert_eq!(
2289 editor.snapshot(window, cx).scroll_position(),
2290 gpui::Point::new(0., 0.0)
2291 );
2292 });
2293
2294 // Add a cursor below the visible area. Since both cursors cannot fit
2295 // on screen, the editor autoscrolls to reveal the newest cursor, and
2296 // allows the vertical scroll margin below that cursor.
2297 cx.update_editor(|editor, window, cx| {
2298 editor.change_selections(Default::default(), window, cx, |selections| {
2299 selections.select_ranges([
2300 Point::new(0, 0)..Point::new(0, 0),
2301 Point::new(6, 0)..Point::new(6, 0),
2302 ]);
2303 })
2304 });
2305 cx.update_editor(|editor, window, cx| {
2306 assert_eq!(
2307 editor.snapshot(window, cx).scroll_position(),
2308 gpui::Point::new(0., 3.0)
2309 );
2310 });
2311
2312 // Move down. The editor cursor scrolls down to track the newest cursor.
2313 cx.update_editor(|editor, window, cx| {
2314 editor.move_down(&Default::default(), window, cx);
2315 });
2316 cx.update_editor(|editor, window, cx| {
2317 assert_eq!(
2318 editor.snapshot(window, cx).scroll_position(),
2319 gpui::Point::new(0., 4.0)
2320 );
2321 });
2322
2323 // Add a cursor above the visible area. Since both cursors fit on screen,
2324 // the editor scrolls to show both.
2325 cx.update_editor(|editor, window, cx| {
2326 editor.change_selections(Default::default(), window, cx, |selections| {
2327 selections.select_ranges([
2328 Point::new(1, 0)..Point::new(1, 0),
2329 Point::new(6, 0)..Point::new(6, 0),
2330 ]);
2331 })
2332 });
2333 cx.update_editor(|editor, window, cx| {
2334 assert_eq!(
2335 editor.snapshot(window, cx).scroll_position(),
2336 gpui::Point::new(0., 1.0)
2337 );
2338 });
2339}
2340
2341#[gpui::test]
2342async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2343 init_test(cx, |_| {});
2344 let mut cx = EditorTestContext::new(cx).await;
2345
2346 let line_height = cx.editor(|editor, window, _cx| {
2347 editor
2348 .style()
2349 .unwrap()
2350 .text
2351 .line_height_in_pixels(window.rem_size())
2352 });
2353 let window = cx.window;
2354 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2355 cx.set_state(
2356 &r#"
2357 ˇone
2358 two
2359 threeˇ
2360 four
2361 five
2362 six
2363 seven
2364 eight
2365 nine
2366 ten
2367 "#
2368 .unindent(),
2369 );
2370
2371 cx.update_editor(|editor, window, cx| {
2372 editor.move_page_down(&MovePageDown::default(), window, cx)
2373 });
2374 cx.assert_editor_state(
2375 &r#"
2376 one
2377 two
2378 three
2379 ˇfour
2380 five
2381 sixˇ
2382 seven
2383 eight
2384 nine
2385 ten
2386 "#
2387 .unindent(),
2388 );
2389
2390 cx.update_editor(|editor, window, cx| {
2391 editor.move_page_down(&MovePageDown::default(), window, cx)
2392 });
2393 cx.assert_editor_state(
2394 &r#"
2395 one
2396 two
2397 three
2398 four
2399 five
2400 six
2401 ˇseven
2402 eight
2403 nineˇ
2404 ten
2405 "#
2406 .unindent(),
2407 );
2408
2409 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2410 cx.assert_editor_state(
2411 &r#"
2412 one
2413 two
2414 three
2415 ˇfour
2416 five
2417 sixˇ
2418 seven
2419 eight
2420 nine
2421 ten
2422 "#
2423 .unindent(),
2424 );
2425
2426 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2427 cx.assert_editor_state(
2428 &r#"
2429 ˇone
2430 two
2431 threeˇ
2432 four
2433 five
2434 six
2435 seven
2436 eight
2437 nine
2438 ten
2439 "#
2440 .unindent(),
2441 );
2442
2443 // Test select collapsing
2444 cx.update_editor(|editor, window, cx| {
2445 editor.move_page_down(&MovePageDown::default(), window, cx);
2446 editor.move_page_down(&MovePageDown::default(), window, cx);
2447 editor.move_page_down(&MovePageDown::default(), window, cx);
2448 });
2449 cx.assert_editor_state(
2450 &r#"
2451 one
2452 two
2453 three
2454 four
2455 five
2456 six
2457 seven
2458 eight
2459 nine
2460 ˇten
2461 ˇ"#
2462 .unindent(),
2463 );
2464}
2465
2466#[gpui::test]
2467async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2468 init_test(cx, |_| {});
2469 let mut cx = EditorTestContext::new(cx).await;
2470 cx.set_state("one «two threeˇ» four");
2471 cx.update_editor(|editor, window, cx| {
2472 editor.delete_to_beginning_of_line(
2473 &DeleteToBeginningOfLine {
2474 stop_at_indent: false,
2475 },
2476 window,
2477 cx,
2478 );
2479 assert_eq!(editor.text(cx), " four");
2480 });
2481}
2482
2483#[gpui::test]
2484async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2485 init_test(cx, |_| {});
2486
2487 let mut cx = EditorTestContext::new(cx).await;
2488
2489 // For an empty selection, the preceding word fragment is deleted.
2490 // For non-empty selections, only selected characters are deleted.
2491 cx.set_state("onˇe two t«hreˇ»e four");
2492 cx.update_editor(|editor, window, cx| {
2493 editor.delete_to_previous_word_start(
2494 &DeleteToPreviousWordStart {
2495 ignore_newlines: false,
2496 ignore_brackets: false,
2497 },
2498 window,
2499 cx,
2500 );
2501 });
2502 cx.assert_editor_state("ˇe two tˇe four");
2503
2504 cx.set_state("e tˇwo te «fˇ»our");
2505 cx.update_editor(|editor, window, cx| {
2506 editor.delete_to_next_word_end(
2507 &DeleteToNextWordEnd {
2508 ignore_newlines: false,
2509 ignore_brackets: false,
2510 },
2511 window,
2512 cx,
2513 );
2514 });
2515 cx.assert_editor_state("e tˇ te ˇour");
2516}
2517
2518#[gpui::test]
2519async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2520 init_test(cx, |_| {});
2521
2522 let mut cx = EditorTestContext::new(cx).await;
2523
2524 cx.set_state("here is some text ˇwith a space");
2525 cx.update_editor(|editor, window, cx| {
2526 editor.delete_to_previous_word_start(
2527 &DeleteToPreviousWordStart {
2528 ignore_newlines: false,
2529 ignore_brackets: true,
2530 },
2531 window,
2532 cx,
2533 );
2534 });
2535 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2536 cx.assert_editor_state("here is some textˇwith a space");
2537
2538 cx.set_state("here is some text ˇwith a space");
2539 cx.update_editor(|editor, window, cx| {
2540 editor.delete_to_previous_word_start(
2541 &DeleteToPreviousWordStart {
2542 ignore_newlines: false,
2543 ignore_brackets: false,
2544 },
2545 window,
2546 cx,
2547 );
2548 });
2549 cx.assert_editor_state("here is some textˇwith a space");
2550
2551 cx.set_state("here is some textˇ with a space");
2552 cx.update_editor(|editor, window, cx| {
2553 editor.delete_to_next_word_end(
2554 &DeleteToNextWordEnd {
2555 ignore_newlines: false,
2556 ignore_brackets: true,
2557 },
2558 window,
2559 cx,
2560 );
2561 });
2562 // Same happens in the other direction.
2563 cx.assert_editor_state("here is some textˇwith a space");
2564
2565 cx.set_state("here is some textˇ with a space");
2566 cx.update_editor(|editor, window, cx| {
2567 editor.delete_to_next_word_end(
2568 &DeleteToNextWordEnd {
2569 ignore_newlines: false,
2570 ignore_brackets: false,
2571 },
2572 window,
2573 cx,
2574 );
2575 });
2576 cx.assert_editor_state("here is some textˇwith a space");
2577
2578 cx.set_state("here is some textˇ with a space");
2579 cx.update_editor(|editor, window, cx| {
2580 editor.delete_to_next_word_end(
2581 &DeleteToNextWordEnd {
2582 ignore_newlines: true,
2583 ignore_brackets: false,
2584 },
2585 window,
2586 cx,
2587 );
2588 });
2589 cx.assert_editor_state("here is some textˇwith a space");
2590 cx.update_editor(|editor, window, cx| {
2591 editor.delete_to_previous_word_start(
2592 &DeleteToPreviousWordStart {
2593 ignore_newlines: true,
2594 ignore_brackets: false,
2595 },
2596 window,
2597 cx,
2598 );
2599 });
2600 cx.assert_editor_state("here is some ˇwith a space");
2601 cx.update_editor(|editor, window, cx| {
2602 editor.delete_to_previous_word_start(
2603 &DeleteToPreviousWordStart {
2604 ignore_newlines: true,
2605 ignore_brackets: false,
2606 },
2607 window,
2608 cx,
2609 );
2610 });
2611 // Single whitespaces are removed with the word behind them.
2612 cx.assert_editor_state("here is ˇwith a space");
2613 cx.update_editor(|editor, window, cx| {
2614 editor.delete_to_previous_word_start(
2615 &DeleteToPreviousWordStart {
2616 ignore_newlines: true,
2617 ignore_brackets: false,
2618 },
2619 window,
2620 cx,
2621 );
2622 });
2623 cx.assert_editor_state("here ˇwith a space");
2624 cx.update_editor(|editor, window, cx| {
2625 editor.delete_to_previous_word_start(
2626 &DeleteToPreviousWordStart {
2627 ignore_newlines: true,
2628 ignore_brackets: false,
2629 },
2630 window,
2631 cx,
2632 );
2633 });
2634 cx.assert_editor_state("ˇwith a space");
2635 cx.update_editor(|editor, window, cx| {
2636 editor.delete_to_previous_word_start(
2637 &DeleteToPreviousWordStart {
2638 ignore_newlines: true,
2639 ignore_brackets: false,
2640 },
2641 window,
2642 cx,
2643 );
2644 });
2645 cx.assert_editor_state("ˇwith a space");
2646 cx.update_editor(|editor, window, cx| {
2647 editor.delete_to_next_word_end(
2648 &DeleteToNextWordEnd {
2649 ignore_newlines: true,
2650 ignore_brackets: false,
2651 },
2652 window,
2653 cx,
2654 );
2655 });
2656 // Same happens in the other direction.
2657 cx.assert_editor_state("ˇ a space");
2658 cx.update_editor(|editor, window, cx| {
2659 editor.delete_to_next_word_end(
2660 &DeleteToNextWordEnd {
2661 ignore_newlines: true,
2662 ignore_brackets: false,
2663 },
2664 window,
2665 cx,
2666 );
2667 });
2668 cx.assert_editor_state("ˇ space");
2669 cx.update_editor(|editor, window, cx| {
2670 editor.delete_to_next_word_end(
2671 &DeleteToNextWordEnd {
2672 ignore_newlines: true,
2673 ignore_brackets: false,
2674 },
2675 window,
2676 cx,
2677 );
2678 });
2679 cx.assert_editor_state("ˇ");
2680 cx.update_editor(|editor, window, cx| {
2681 editor.delete_to_next_word_end(
2682 &DeleteToNextWordEnd {
2683 ignore_newlines: true,
2684 ignore_brackets: false,
2685 },
2686 window,
2687 cx,
2688 );
2689 });
2690 cx.assert_editor_state("ˇ");
2691 cx.update_editor(|editor, window, cx| {
2692 editor.delete_to_previous_word_start(
2693 &DeleteToPreviousWordStart {
2694 ignore_newlines: true,
2695 ignore_brackets: false,
2696 },
2697 window,
2698 cx,
2699 );
2700 });
2701 cx.assert_editor_state("ˇ");
2702}
2703
2704#[gpui::test]
2705async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2706 init_test(cx, |_| {});
2707
2708 let language = Arc::new(
2709 Language::new(
2710 LanguageConfig {
2711 brackets: BracketPairConfig {
2712 pairs: vec![
2713 BracketPair {
2714 start: "\"".to_string(),
2715 end: "\"".to_string(),
2716 close: true,
2717 surround: true,
2718 newline: false,
2719 },
2720 BracketPair {
2721 start: "(".to_string(),
2722 end: ")".to_string(),
2723 close: true,
2724 surround: true,
2725 newline: true,
2726 },
2727 ],
2728 ..BracketPairConfig::default()
2729 },
2730 ..LanguageConfig::default()
2731 },
2732 Some(tree_sitter_rust::LANGUAGE.into()),
2733 )
2734 .with_brackets_query(
2735 r#"
2736 ("(" @open ")" @close)
2737 ("\"" @open "\"" @close)
2738 "#,
2739 )
2740 .unwrap(),
2741 );
2742
2743 let mut cx = EditorTestContext::new(cx).await;
2744 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2745
2746 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2747 cx.update_editor(|editor, window, cx| {
2748 editor.delete_to_previous_word_start(
2749 &DeleteToPreviousWordStart {
2750 ignore_newlines: true,
2751 ignore_brackets: false,
2752 },
2753 window,
2754 cx,
2755 );
2756 });
2757 // Deletion stops before brackets if asked to not ignore them.
2758 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2759 cx.update_editor(|editor, window, cx| {
2760 editor.delete_to_previous_word_start(
2761 &DeleteToPreviousWordStart {
2762 ignore_newlines: true,
2763 ignore_brackets: false,
2764 },
2765 window,
2766 cx,
2767 );
2768 });
2769 // Deletion has to remove a single bracket and then stop again.
2770 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2771
2772 cx.update_editor(|editor, window, cx| {
2773 editor.delete_to_previous_word_start(
2774 &DeleteToPreviousWordStart {
2775 ignore_newlines: true,
2776 ignore_brackets: false,
2777 },
2778 window,
2779 cx,
2780 );
2781 });
2782 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2783
2784 cx.update_editor(|editor, window, cx| {
2785 editor.delete_to_previous_word_start(
2786 &DeleteToPreviousWordStart {
2787 ignore_newlines: true,
2788 ignore_brackets: false,
2789 },
2790 window,
2791 cx,
2792 );
2793 });
2794 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2795
2796 cx.update_editor(|editor, window, cx| {
2797 editor.delete_to_previous_word_start(
2798 &DeleteToPreviousWordStart {
2799 ignore_newlines: true,
2800 ignore_brackets: false,
2801 },
2802 window,
2803 cx,
2804 );
2805 });
2806 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2807
2808 cx.update_editor(|editor, window, cx| {
2809 editor.delete_to_next_word_end(
2810 &DeleteToNextWordEnd {
2811 ignore_newlines: true,
2812 ignore_brackets: false,
2813 },
2814 window,
2815 cx,
2816 );
2817 });
2818 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2819 cx.assert_editor_state(r#"ˇ");"#);
2820
2821 cx.update_editor(|editor, window, cx| {
2822 editor.delete_to_next_word_end(
2823 &DeleteToNextWordEnd {
2824 ignore_newlines: true,
2825 ignore_brackets: false,
2826 },
2827 window,
2828 cx,
2829 );
2830 });
2831 cx.assert_editor_state(r#"ˇ"#);
2832
2833 cx.update_editor(|editor, window, cx| {
2834 editor.delete_to_next_word_end(
2835 &DeleteToNextWordEnd {
2836 ignore_newlines: true,
2837 ignore_brackets: false,
2838 },
2839 window,
2840 cx,
2841 );
2842 });
2843 cx.assert_editor_state(r#"ˇ"#);
2844
2845 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2846 cx.update_editor(|editor, window, cx| {
2847 editor.delete_to_previous_word_start(
2848 &DeleteToPreviousWordStart {
2849 ignore_newlines: true,
2850 ignore_brackets: true,
2851 },
2852 window,
2853 cx,
2854 );
2855 });
2856 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2857}
2858
2859#[gpui::test]
2860fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2861 init_test(cx, |_| {});
2862
2863 let editor = cx.add_window(|window, cx| {
2864 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2865 build_editor(buffer, window, cx)
2866 });
2867 let del_to_prev_word_start = DeleteToPreviousWordStart {
2868 ignore_newlines: false,
2869 ignore_brackets: false,
2870 };
2871 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2872 ignore_newlines: true,
2873 ignore_brackets: false,
2874 };
2875
2876 _ = editor.update(cx, |editor, window, cx| {
2877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2878 s.select_display_ranges([
2879 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2880 ])
2881 });
2882 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2883 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2884 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2885 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2886 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2887 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2888 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2889 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2890 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2891 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2892 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2893 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2894 });
2895}
2896
2897#[gpui::test]
2898fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2899 init_test(cx, |_| {});
2900
2901 let editor = cx.add_window(|window, cx| {
2902 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2903 build_editor(buffer, window, cx)
2904 });
2905 let del_to_next_word_end = DeleteToNextWordEnd {
2906 ignore_newlines: false,
2907 ignore_brackets: false,
2908 };
2909 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2910 ignore_newlines: true,
2911 ignore_brackets: false,
2912 };
2913
2914 _ = editor.update(cx, |editor, window, cx| {
2915 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2916 s.select_display_ranges([
2917 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2918 ])
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 "one\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 "\n two\nthree\n four"
2929 );
2930 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2931 assert_eq!(
2932 editor.buffer.read(cx).read(cx).text(),
2933 "two\nthree\n four"
2934 );
2935 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2936 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2937 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2938 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2939 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2940 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2941 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2942 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2943 });
2944}
2945
2946#[gpui::test]
2947fn test_newline(cx: &mut TestAppContext) {
2948 init_test(cx, |_| {});
2949
2950 let editor = cx.add_window(|window, cx| {
2951 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2952 build_editor(buffer, window, cx)
2953 });
2954
2955 _ = editor.update(cx, |editor, window, cx| {
2956 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2957 s.select_display_ranges([
2958 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2959 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2960 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2961 ])
2962 });
2963
2964 editor.newline(&Newline, window, cx);
2965 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2966 });
2967}
2968
2969#[gpui::test]
2970fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2971 init_test(cx, |_| {});
2972
2973 let editor = cx.add_window(|window, cx| {
2974 let buffer = MultiBuffer::build_simple(
2975 "
2976 a
2977 b(
2978 X
2979 )
2980 c(
2981 X
2982 )
2983 "
2984 .unindent()
2985 .as_str(),
2986 cx,
2987 );
2988 let mut editor = build_editor(buffer, window, cx);
2989 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2990 s.select_ranges([
2991 Point::new(2, 4)..Point::new(2, 5),
2992 Point::new(5, 4)..Point::new(5, 5),
2993 ])
2994 });
2995 editor
2996 });
2997
2998 _ = editor.update(cx, |editor, window, cx| {
2999 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3000 editor.buffer.update(cx, |buffer, cx| {
3001 buffer.edit(
3002 [
3003 (Point::new(1, 2)..Point::new(3, 0), ""),
3004 (Point::new(4, 2)..Point::new(6, 0), ""),
3005 ],
3006 None,
3007 cx,
3008 );
3009 assert_eq!(
3010 buffer.read(cx).text(),
3011 "
3012 a
3013 b()
3014 c()
3015 "
3016 .unindent()
3017 );
3018 });
3019 assert_eq!(
3020 editor.selections.ranges(cx),
3021 &[
3022 Point::new(1, 2)..Point::new(1, 2),
3023 Point::new(2, 2)..Point::new(2, 2),
3024 ],
3025 );
3026
3027 editor.newline(&Newline, window, cx);
3028 assert_eq!(
3029 editor.text(cx),
3030 "
3031 a
3032 b(
3033 )
3034 c(
3035 )
3036 "
3037 .unindent()
3038 );
3039
3040 // The selections are moved after the inserted newlines
3041 assert_eq!(
3042 editor.selections.ranges(cx),
3043 &[
3044 Point::new(2, 0)..Point::new(2, 0),
3045 Point::new(4, 0)..Point::new(4, 0),
3046 ],
3047 );
3048 });
3049}
3050
3051#[gpui::test]
3052async fn test_newline_above(cx: &mut TestAppContext) {
3053 init_test(cx, |settings| {
3054 settings.defaults.tab_size = NonZeroU32::new(4)
3055 });
3056
3057 let language = Arc::new(
3058 Language::new(
3059 LanguageConfig::default(),
3060 Some(tree_sitter_rust::LANGUAGE.into()),
3061 )
3062 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3063 .unwrap(),
3064 );
3065
3066 let mut cx = EditorTestContext::new(cx).await;
3067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3068 cx.set_state(indoc! {"
3069 const a: ˇA = (
3070 (ˇ
3071 «const_functionˇ»(ˇ),
3072 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3073 )ˇ
3074 ˇ);ˇ
3075 "});
3076
3077 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3078 cx.assert_editor_state(indoc! {"
3079 ˇ
3080 const a: A = (
3081 ˇ
3082 (
3083 ˇ
3084 ˇ
3085 const_function(),
3086 ˇ
3087 ˇ
3088 ˇ
3089 ˇ
3090 something_else,
3091 ˇ
3092 )
3093 ˇ
3094 ˇ
3095 );
3096 "});
3097}
3098
3099#[gpui::test]
3100async fn test_newline_below(cx: &mut TestAppContext) {
3101 init_test(cx, |settings| {
3102 settings.defaults.tab_size = NonZeroU32::new(4)
3103 });
3104
3105 let language = Arc::new(
3106 Language::new(
3107 LanguageConfig::default(),
3108 Some(tree_sitter_rust::LANGUAGE.into()),
3109 )
3110 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3111 .unwrap(),
3112 );
3113
3114 let mut cx = EditorTestContext::new(cx).await;
3115 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3116 cx.set_state(indoc! {"
3117 const a: ˇA = (
3118 (ˇ
3119 «const_functionˇ»(ˇ),
3120 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3121 )ˇ
3122 ˇ);ˇ
3123 "});
3124
3125 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3126 cx.assert_editor_state(indoc! {"
3127 const a: A = (
3128 ˇ
3129 (
3130 ˇ
3131 const_function(),
3132 ˇ
3133 ˇ
3134 something_else,
3135 ˇ
3136 ˇ
3137 ˇ
3138 ˇ
3139 )
3140 ˇ
3141 );
3142 ˇ
3143 ˇ
3144 "});
3145}
3146
3147#[gpui::test]
3148async fn test_newline_comments(cx: &mut TestAppContext) {
3149 init_test(cx, |settings| {
3150 settings.defaults.tab_size = NonZeroU32::new(4)
3151 });
3152
3153 let language = Arc::new(Language::new(
3154 LanguageConfig {
3155 line_comments: vec!["// ".into()],
3156 ..LanguageConfig::default()
3157 },
3158 None,
3159 ));
3160 {
3161 let mut cx = EditorTestContext::new(cx).await;
3162 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3163 cx.set_state(indoc! {"
3164 // Fooˇ
3165 "});
3166
3167 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3168 cx.assert_editor_state(indoc! {"
3169 // Foo
3170 // ˇ
3171 "});
3172 // Ensure that we add comment prefix when existing line contains space
3173 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3174 cx.assert_editor_state(
3175 indoc! {"
3176 // Foo
3177 //s
3178 // ˇ
3179 "}
3180 .replace("s", " ") // s is used as space placeholder to prevent format on save
3181 .as_str(),
3182 );
3183 // Ensure that we add comment prefix when existing line does not contain space
3184 cx.set_state(indoc! {"
3185 // Foo
3186 //ˇ
3187 "});
3188 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3189 cx.assert_editor_state(indoc! {"
3190 // Foo
3191 //
3192 // ˇ
3193 "});
3194 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3195 cx.set_state(indoc! {"
3196 ˇ// Foo
3197 "});
3198 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3199 cx.assert_editor_state(indoc! {"
3200
3201 ˇ// Foo
3202 "});
3203 }
3204 // Ensure that comment continuations can be disabled.
3205 update_test_language_settings(cx, |settings| {
3206 settings.defaults.extend_comment_on_newline = Some(false);
3207 });
3208 let mut cx = EditorTestContext::new(cx).await;
3209 cx.set_state(indoc! {"
3210 // Fooˇ
3211 "});
3212 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3213 cx.assert_editor_state(indoc! {"
3214 // Foo
3215 ˇ
3216 "});
3217}
3218
3219#[gpui::test]
3220async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3221 init_test(cx, |settings| {
3222 settings.defaults.tab_size = NonZeroU32::new(4)
3223 });
3224
3225 let language = Arc::new(Language::new(
3226 LanguageConfig {
3227 line_comments: vec!["// ".into(), "/// ".into()],
3228 ..LanguageConfig::default()
3229 },
3230 None,
3231 ));
3232 {
3233 let mut cx = EditorTestContext::new(cx).await;
3234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3235 cx.set_state(indoc! {"
3236 //ˇ
3237 "});
3238 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3239 cx.assert_editor_state(indoc! {"
3240 //
3241 // ˇ
3242 "});
3243
3244 cx.set_state(indoc! {"
3245 ///ˇ
3246 "});
3247 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3248 cx.assert_editor_state(indoc! {"
3249 ///
3250 /// ˇ
3251 "});
3252 }
3253}
3254
3255#[gpui::test]
3256async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3257 init_test(cx, |settings| {
3258 settings.defaults.tab_size = NonZeroU32::new(4)
3259 });
3260
3261 let language = Arc::new(
3262 Language::new(
3263 LanguageConfig {
3264 documentation_comment: Some(language::BlockCommentConfig {
3265 start: "/**".into(),
3266 end: "*/".into(),
3267 prefix: "* ".into(),
3268 tab_size: 1,
3269 }),
3270
3271 ..LanguageConfig::default()
3272 },
3273 Some(tree_sitter_rust::LANGUAGE.into()),
3274 )
3275 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3276 .unwrap(),
3277 );
3278
3279 {
3280 let mut cx = EditorTestContext::new(cx).await;
3281 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3282 cx.set_state(indoc! {"
3283 /**ˇ
3284 "});
3285
3286 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3287 cx.assert_editor_state(indoc! {"
3288 /**
3289 * ˇ
3290 "});
3291 // Ensure that if cursor is before the comment start,
3292 // we do not actually insert a comment prefix.
3293 cx.set_state(indoc! {"
3294 ˇ/**
3295 "});
3296 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3297 cx.assert_editor_state(indoc! {"
3298
3299 ˇ/**
3300 "});
3301 // Ensure that if cursor is between it doesn't add comment prefix.
3302 cx.set_state(indoc! {"
3303 /*ˇ*
3304 "});
3305 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3306 cx.assert_editor_state(indoc! {"
3307 /*
3308 ˇ*
3309 "});
3310 // Ensure that if suffix exists on same line after cursor it adds new line.
3311 cx.set_state(indoc! {"
3312 /**ˇ*/
3313 "});
3314 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3315 cx.assert_editor_state(indoc! {"
3316 /**
3317 * ˇ
3318 */
3319 "});
3320 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3321 cx.set_state(indoc! {"
3322 /**ˇ */
3323 "});
3324 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3325 cx.assert_editor_state(indoc! {"
3326 /**
3327 * ˇ
3328 */
3329 "});
3330 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3331 cx.set_state(indoc! {"
3332 /** ˇ*/
3333 "});
3334 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3335 cx.assert_editor_state(
3336 indoc! {"
3337 /**s
3338 * ˇ
3339 */
3340 "}
3341 .replace("s", " ") // s is used as space placeholder to prevent format on save
3342 .as_str(),
3343 );
3344 // Ensure that delimiter space is preserved when newline on already
3345 // spaced delimiter.
3346 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3347 cx.assert_editor_state(
3348 indoc! {"
3349 /**s
3350 *s
3351 * ˇ
3352 */
3353 "}
3354 .replace("s", " ") // s is used as space placeholder to prevent format on save
3355 .as_str(),
3356 );
3357 // Ensure that delimiter space is preserved when space is not
3358 // on existing delimiter.
3359 cx.set_state(indoc! {"
3360 /**
3361 *ˇ
3362 */
3363 "});
3364 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3365 cx.assert_editor_state(indoc! {"
3366 /**
3367 *
3368 * ˇ
3369 */
3370 "});
3371 // Ensure that if suffix exists on same line after cursor it
3372 // doesn't add extra new line if prefix is not on same line.
3373 cx.set_state(indoc! {"
3374 /**
3375 ˇ*/
3376 "});
3377 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3378 cx.assert_editor_state(indoc! {"
3379 /**
3380
3381 ˇ*/
3382 "});
3383 // Ensure that it detects suffix after existing prefix.
3384 cx.set_state(indoc! {"
3385 /**ˇ/
3386 "});
3387 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3388 cx.assert_editor_state(indoc! {"
3389 /**
3390 ˇ/
3391 "});
3392 // Ensure that if suffix exists on same line before
3393 // cursor it does not add comment prefix.
3394 cx.set_state(indoc! {"
3395 /** */ˇ
3396 "});
3397 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3398 cx.assert_editor_state(indoc! {"
3399 /** */
3400 ˇ
3401 "});
3402 // Ensure that if suffix exists on same line before
3403 // cursor it does not add comment prefix.
3404 cx.set_state(indoc! {"
3405 /**
3406 *
3407 */ˇ
3408 "});
3409 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3410 cx.assert_editor_state(indoc! {"
3411 /**
3412 *
3413 */
3414 ˇ
3415 "});
3416
3417 // Ensure that inline comment followed by code
3418 // doesn't add comment prefix on newline
3419 cx.set_state(indoc! {"
3420 /** */ textˇ
3421 "});
3422 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 /** */ text
3425 ˇ
3426 "});
3427
3428 // Ensure that text after comment end tag
3429 // doesn't add comment prefix on newline
3430 cx.set_state(indoc! {"
3431 /**
3432 *
3433 */ˇtext
3434 "});
3435 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3436 cx.assert_editor_state(indoc! {"
3437 /**
3438 *
3439 */
3440 ˇtext
3441 "});
3442
3443 // Ensure if not comment block it doesn't
3444 // add comment prefix on newline
3445 cx.set_state(indoc! {"
3446 * textˇ
3447 "});
3448 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3449 cx.assert_editor_state(indoc! {"
3450 * text
3451 ˇ
3452 "});
3453 }
3454 // Ensure that comment continuations can be disabled.
3455 update_test_language_settings(cx, |settings| {
3456 settings.defaults.extend_comment_on_newline = Some(false);
3457 });
3458 let mut cx = EditorTestContext::new(cx).await;
3459 cx.set_state(indoc! {"
3460 /**ˇ
3461 "});
3462 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 /**
3465 ˇ
3466 "});
3467}
3468
3469#[gpui::test]
3470async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3471 init_test(cx, |settings| {
3472 settings.defaults.tab_size = NonZeroU32::new(4)
3473 });
3474
3475 let lua_language = Arc::new(Language::new(
3476 LanguageConfig {
3477 line_comments: vec!["--".into()],
3478 block_comment: Some(language::BlockCommentConfig {
3479 start: "--[[".into(),
3480 prefix: "".into(),
3481 end: "]]".into(),
3482 tab_size: 0,
3483 }),
3484 ..LanguageConfig::default()
3485 },
3486 None,
3487 ));
3488
3489 let mut cx = EditorTestContext::new(cx).await;
3490 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3491
3492 // Line with line comment should extend
3493 cx.set_state(indoc! {"
3494 --ˇ
3495 "});
3496 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3497 cx.assert_editor_state(indoc! {"
3498 --
3499 --ˇ
3500 "});
3501
3502 // Line with block comment that matches line comment should not extend
3503 cx.set_state(indoc! {"
3504 --[[ˇ
3505 "});
3506 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3507 cx.assert_editor_state(indoc! {"
3508 --[[
3509 ˇ
3510 "});
3511}
3512
3513#[gpui::test]
3514fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3515 init_test(cx, |_| {});
3516
3517 let editor = cx.add_window(|window, cx| {
3518 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3519 let mut editor = build_editor(buffer, window, cx);
3520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3521 s.select_ranges([3..4, 11..12, 19..20])
3522 });
3523 editor
3524 });
3525
3526 _ = editor.update(cx, |editor, window, cx| {
3527 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3528 editor.buffer.update(cx, |buffer, cx| {
3529 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3530 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3531 });
3532 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3533
3534 editor.insert("Z", window, cx);
3535 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3536
3537 // The selections are moved after the inserted characters
3538 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3539 });
3540}
3541
3542#[gpui::test]
3543async fn test_tab(cx: &mut TestAppContext) {
3544 init_test(cx, |settings| {
3545 settings.defaults.tab_size = NonZeroU32::new(3)
3546 });
3547
3548 let mut cx = EditorTestContext::new(cx).await;
3549 cx.set_state(indoc! {"
3550 ˇabˇc
3551 ˇ🏀ˇ🏀ˇefg
3552 dˇ
3553 "});
3554 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3555 cx.assert_editor_state(indoc! {"
3556 ˇab ˇc
3557 ˇ🏀 ˇ🏀 ˇefg
3558 d ˇ
3559 "});
3560
3561 cx.set_state(indoc! {"
3562 a
3563 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3564 "});
3565 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3566 cx.assert_editor_state(indoc! {"
3567 a
3568 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3569 "});
3570}
3571
3572#[gpui::test]
3573async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3574 init_test(cx, |_| {});
3575
3576 let mut cx = EditorTestContext::new(cx).await;
3577 let language = Arc::new(
3578 Language::new(
3579 LanguageConfig::default(),
3580 Some(tree_sitter_rust::LANGUAGE.into()),
3581 )
3582 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3583 .unwrap(),
3584 );
3585 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3586
3587 // test when all cursors are not at suggested indent
3588 // then simply move to their suggested indent location
3589 cx.set_state(indoc! {"
3590 const a: B = (
3591 c(
3592 ˇ
3593 ˇ )
3594 );
3595 "});
3596 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3597 cx.assert_editor_state(indoc! {"
3598 const a: B = (
3599 c(
3600 ˇ
3601 ˇ)
3602 );
3603 "});
3604
3605 // test cursor already at suggested indent not moving when
3606 // other cursors are yet to reach their suggested indents
3607 cx.set_state(indoc! {"
3608 ˇ
3609 const a: B = (
3610 c(
3611 d(
3612 ˇ
3613 )
3614 ˇ
3615 ˇ )
3616 );
3617 "});
3618 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3619 cx.assert_editor_state(indoc! {"
3620 ˇ
3621 const a: B = (
3622 c(
3623 d(
3624 ˇ
3625 )
3626 ˇ
3627 ˇ)
3628 );
3629 "});
3630 // test when all cursors are at suggested indent then tab is inserted
3631 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3632 cx.assert_editor_state(indoc! {"
3633 ˇ
3634 const a: B = (
3635 c(
3636 d(
3637 ˇ
3638 )
3639 ˇ
3640 ˇ)
3641 );
3642 "});
3643
3644 // test when current indent is less than suggested indent,
3645 // we adjust line to match suggested indent and move cursor to it
3646 //
3647 // when no other cursor is at word boundary, all of them should move
3648 cx.set_state(indoc! {"
3649 const a: B = (
3650 c(
3651 d(
3652 ˇ
3653 ˇ )
3654 ˇ )
3655 );
3656 "});
3657 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3658 cx.assert_editor_state(indoc! {"
3659 const a: B = (
3660 c(
3661 d(
3662 ˇ
3663 ˇ)
3664 ˇ)
3665 );
3666 "});
3667
3668 // test when current indent is less than suggested indent,
3669 // we adjust line to match suggested indent and move cursor to it
3670 //
3671 // when some other cursor is at word boundary, it should not move
3672 cx.set_state(indoc! {"
3673 const a: B = (
3674 c(
3675 d(
3676 ˇ
3677 ˇ )
3678 ˇ)
3679 );
3680 "});
3681 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3682 cx.assert_editor_state(indoc! {"
3683 const a: B = (
3684 c(
3685 d(
3686 ˇ
3687 ˇ)
3688 ˇ)
3689 );
3690 "});
3691
3692 // test when current indent is more than suggested indent,
3693 // we just move cursor to current indent instead of suggested indent
3694 //
3695 // when no other cursor is at word boundary, all of them should move
3696 cx.set_state(indoc! {"
3697 const a: B = (
3698 c(
3699 d(
3700 ˇ
3701 ˇ )
3702 ˇ )
3703 );
3704 "});
3705 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3706 cx.assert_editor_state(indoc! {"
3707 const a: B = (
3708 c(
3709 d(
3710 ˇ
3711 ˇ)
3712 ˇ)
3713 );
3714 "});
3715 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3716 cx.assert_editor_state(indoc! {"
3717 const a: B = (
3718 c(
3719 d(
3720 ˇ
3721 ˇ)
3722 ˇ)
3723 );
3724 "});
3725
3726 // test when current indent is more than suggested indent,
3727 // we just move cursor to current indent instead of suggested indent
3728 //
3729 // when some other cursor is at word boundary, it doesn't move
3730 cx.set_state(indoc! {"
3731 const a: B = (
3732 c(
3733 d(
3734 ˇ
3735 ˇ )
3736 ˇ)
3737 );
3738 "});
3739 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3740 cx.assert_editor_state(indoc! {"
3741 const a: B = (
3742 c(
3743 d(
3744 ˇ
3745 ˇ)
3746 ˇ)
3747 );
3748 "});
3749
3750 // handle auto-indent when there are multiple cursors on the same line
3751 cx.set_state(indoc! {"
3752 const a: B = (
3753 c(
3754 ˇ ˇ
3755 ˇ )
3756 );
3757 "});
3758 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3759 cx.assert_editor_state(indoc! {"
3760 const a: B = (
3761 c(
3762 ˇ
3763 ˇ)
3764 );
3765 "});
3766}
3767
3768#[gpui::test]
3769async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3770 init_test(cx, |settings| {
3771 settings.defaults.tab_size = NonZeroU32::new(3)
3772 });
3773
3774 let mut cx = EditorTestContext::new(cx).await;
3775 cx.set_state(indoc! {"
3776 ˇ
3777 \t ˇ
3778 \t ˇ
3779 \t ˇ
3780 \t \t\t \t \t\t \t\t \t \t ˇ
3781 "});
3782
3783 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3784 cx.assert_editor_state(indoc! {"
3785 ˇ
3786 \t ˇ
3787 \t ˇ
3788 \t ˇ
3789 \t \t\t \t \t\t \t\t \t \t ˇ
3790 "});
3791}
3792
3793#[gpui::test]
3794async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3795 init_test(cx, |settings| {
3796 settings.defaults.tab_size = NonZeroU32::new(4)
3797 });
3798
3799 let language = Arc::new(
3800 Language::new(
3801 LanguageConfig::default(),
3802 Some(tree_sitter_rust::LANGUAGE.into()),
3803 )
3804 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3805 .unwrap(),
3806 );
3807
3808 let mut cx = EditorTestContext::new(cx).await;
3809 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3810 cx.set_state(indoc! {"
3811 fn a() {
3812 if b {
3813 \t ˇc
3814 }
3815 }
3816 "});
3817
3818 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 fn a() {
3821 if b {
3822 ˇc
3823 }
3824 }
3825 "});
3826}
3827
3828#[gpui::test]
3829async fn test_indent_outdent(cx: &mut TestAppContext) {
3830 init_test(cx, |settings| {
3831 settings.defaults.tab_size = NonZeroU32::new(4);
3832 });
3833
3834 let mut cx = EditorTestContext::new(cx).await;
3835
3836 cx.set_state(indoc! {"
3837 «oneˇ» «twoˇ»
3838 three
3839 four
3840 "});
3841 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3842 cx.assert_editor_state(indoc! {"
3843 «oneˇ» «twoˇ»
3844 three
3845 four
3846 "});
3847
3848 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3849 cx.assert_editor_state(indoc! {"
3850 «oneˇ» «twoˇ»
3851 three
3852 four
3853 "});
3854
3855 // select across line ending
3856 cx.set_state(indoc! {"
3857 one two
3858 t«hree
3859 ˇ» four
3860 "});
3861 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3862 cx.assert_editor_state(indoc! {"
3863 one two
3864 t«hree
3865 ˇ» four
3866 "});
3867
3868 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3869 cx.assert_editor_state(indoc! {"
3870 one two
3871 t«hree
3872 ˇ» four
3873 "});
3874
3875 // Ensure that indenting/outdenting works when the cursor is at column 0.
3876 cx.set_state(indoc! {"
3877 one two
3878 ˇthree
3879 four
3880 "});
3881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3882 cx.assert_editor_state(indoc! {"
3883 one two
3884 ˇthree
3885 four
3886 "});
3887
3888 cx.set_state(indoc! {"
3889 one two
3890 ˇ three
3891 four
3892 "});
3893 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3894 cx.assert_editor_state(indoc! {"
3895 one two
3896 ˇthree
3897 four
3898 "});
3899}
3900
3901#[gpui::test]
3902async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3903 // This is a regression test for issue #33761
3904 init_test(cx, |_| {});
3905
3906 let mut cx = EditorTestContext::new(cx).await;
3907 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3909
3910 cx.set_state(
3911 r#"ˇ# ingress:
3912ˇ# api:
3913ˇ# enabled: false
3914ˇ# pathType: Prefix
3915ˇ# console:
3916ˇ# enabled: false
3917ˇ# pathType: Prefix
3918"#,
3919 );
3920
3921 // Press tab to indent all lines
3922 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3923
3924 cx.assert_editor_state(
3925 r#" ˇ# ingress:
3926 ˇ# api:
3927 ˇ# enabled: false
3928 ˇ# pathType: Prefix
3929 ˇ# console:
3930 ˇ# enabled: false
3931 ˇ# pathType: Prefix
3932"#,
3933 );
3934}
3935
3936#[gpui::test]
3937async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3938 // This is a test to make sure our fix for issue #33761 didn't break anything
3939 init_test(cx, |_| {});
3940
3941 let mut cx = EditorTestContext::new(cx).await;
3942 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3943 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3944
3945 cx.set_state(
3946 r#"ˇingress:
3947ˇ api:
3948ˇ enabled: false
3949ˇ pathType: Prefix
3950"#,
3951 );
3952
3953 // Press tab to indent all lines
3954 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3955
3956 cx.assert_editor_state(
3957 r#"ˇingress:
3958 ˇapi:
3959 ˇenabled: false
3960 ˇpathType: Prefix
3961"#,
3962 );
3963}
3964
3965#[gpui::test]
3966async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3967 init_test(cx, |settings| {
3968 settings.defaults.hard_tabs = Some(true);
3969 });
3970
3971 let mut cx = EditorTestContext::new(cx).await;
3972
3973 // select two ranges on one line
3974 cx.set_state(indoc! {"
3975 «oneˇ» «twoˇ»
3976 three
3977 four
3978 "});
3979 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3980 cx.assert_editor_state(indoc! {"
3981 \t«oneˇ» «twoˇ»
3982 three
3983 four
3984 "});
3985 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3986 cx.assert_editor_state(indoc! {"
3987 \t\t«oneˇ» «twoˇ»
3988 three
3989 four
3990 "});
3991 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3992 cx.assert_editor_state(indoc! {"
3993 \t«oneˇ» «twoˇ»
3994 three
3995 four
3996 "});
3997 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3998 cx.assert_editor_state(indoc! {"
3999 «oneˇ» «twoˇ»
4000 three
4001 four
4002 "});
4003
4004 // select across a line ending
4005 cx.set_state(indoc! {"
4006 one two
4007 t«hree
4008 ˇ»four
4009 "});
4010 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4011 cx.assert_editor_state(indoc! {"
4012 one two
4013 \tt«hree
4014 ˇ»four
4015 "});
4016 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4017 cx.assert_editor_state(indoc! {"
4018 one two
4019 \t\tt«hree
4020 ˇ»four
4021 "});
4022 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4023 cx.assert_editor_state(indoc! {"
4024 one two
4025 \tt«hree
4026 ˇ»four
4027 "});
4028 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4029 cx.assert_editor_state(indoc! {"
4030 one two
4031 t«hree
4032 ˇ»four
4033 "});
4034
4035 // Ensure that indenting/outdenting works when the cursor is at column 0.
4036 cx.set_state(indoc! {"
4037 one two
4038 ˇthree
4039 four
4040 "});
4041 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4042 cx.assert_editor_state(indoc! {"
4043 one two
4044 ˇthree
4045 four
4046 "});
4047 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4048 cx.assert_editor_state(indoc! {"
4049 one two
4050 \tˇthree
4051 four
4052 "});
4053 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4054 cx.assert_editor_state(indoc! {"
4055 one two
4056 ˇthree
4057 four
4058 "});
4059}
4060
4061#[gpui::test]
4062fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4063 init_test(cx, |settings| {
4064 settings.languages.0.extend([
4065 (
4066 "TOML".into(),
4067 LanguageSettingsContent {
4068 tab_size: NonZeroU32::new(2),
4069 ..Default::default()
4070 },
4071 ),
4072 (
4073 "Rust".into(),
4074 LanguageSettingsContent {
4075 tab_size: NonZeroU32::new(4),
4076 ..Default::default()
4077 },
4078 ),
4079 ]);
4080 });
4081
4082 let toml_language = Arc::new(Language::new(
4083 LanguageConfig {
4084 name: "TOML".into(),
4085 ..Default::default()
4086 },
4087 None,
4088 ));
4089 let rust_language = Arc::new(Language::new(
4090 LanguageConfig {
4091 name: "Rust".into(),
4092 ..Default::default()
4093 },
4094 None,
4095 ));
4096
4097 let toml_buffer =
4098 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4099 let rust_buffer =
4100 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4101 let multibuffer = cx.new(|cx| {
4102 let mut multibuffer = MultiBuffer::new(ReadWrite);
4103 multibuffer.push_excerpts(
4104 toml_buffer.clone(),
4105 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4106 cx,
4107 );
4108 multibuffer.push_excerpts(
4109 rust_buffer.clone(),
4110 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4111 cx,
4112 );
4113 multibuffer
4114 });
4115
4116 cx.add_window(|window, cx| {
4117 let mut editor = build_editor(multibuffer, window, cx);
4118
4119 assert_eq!(
4120 editor.text(cx),
4121 indoc! {"
4122 a = 1
4123 b = 2
4124
4125 const c: usize = 3;
4126 "}
4127 );
4128
4129 select_ranges(
4130 &mut editor,
4131 indoc! {"
4132 «aˇ» = 1
4133 b = 2
4134
4135 «const c:ˇ» usize = 3;
4136 "},
4137 window,
4138 cx,
4139 );
4140
4141 editor.tab(&Tab, window, cx);
4142 assert_text_with_selections(
4143 &mut editor,
4144 indoc! {"
4145 «aˇ» = 1
4146 b = 2
4147
4148 «const c:ˇ» usize = 3;
4149 "},
4150 cx,
4151 );
4152 editor.backtab(&Backtab, window, cx);
4153 assert_text_with_selections(
4154 &mut editor,
4155 indoc! {"
4156 «aˇ» = 1
4157 b = 2
4158
4159 «const c:ˇ» usize = 3;
4160 "},
4161 cx,
4162 );
4163
4164 editor
4165 });
4166}
4167
4168#[gpui::test]
4169async fn test_backspace(cx: &mut TestAppContext) {
4170 init_test(cx, |_| {});
4171
4172 let mut cx = EditorTestContext::new(cx).await;
4173
4174 // Basic backspace
4175 cx.set_state(indoc! {"
4176 onˇe two three
4177 fou«rˇ» five six
4178 seven «ˇeight nine
4179 »ten
4180 "});
4181 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4182 cx.assert_editor_state(indoc! {"
4183 oˇe two three
4184 fouˇ five six
4185 seven ˇten
4186 "});
4187
4188 // Test backspace inside and around indents
4189 cx.set_state(indoc! {"
4190 zero
4191 ˇone
4192 ˇtwo
4193 ˇ ˇ ˇ three
4194 ˇ ˇ four
4195 "});
4196 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4197 cx.assert_editor_state(indoc! {"
4198 zero
4199 ˇone
4200 ˇtwo
4201 ˇ threeˇ four
4202 "});
4203}
4204
4205#[gpui::test]
4206async fn test_delete(cx: &mut TestAppContext) {
4207 init_test(cx, |_| {});
4208
4209 let mut cx = EditorTestContext::new(cx).await;
4210 cx.set_state(indoc! {"
4211 onˇe two three
4212 fou«rˇ» five six
4213 seven «ˇeight nine
4214 »ten
4215 "});
4216 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4217 cx.assert_editor_state(indoc! {"
4218 onˇ two three
4219 fouˇ five six
4220 seven ˇten
4221 "});
4222}
4223
4224#[gpui::test]
4225fn test_delete_line(cx: &mut TestAppContext) {
4226 init_test(cx, |_| {});
4227
4228 let editor = cx.add_window(|window, cx| {
4229 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4230 build_editor(buffer, window, cx)
4231 });
4232 _ = editor.update(cx, |editor, window, cx| {
4233 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4234 s.select_display_ranges([
4235 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4236 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4237 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4238 ])
4239 });
4240 editor.delete_line(&DeleteLine, window, cx);
4241 assert_eq!(editor.display_text(cx), "ghi");
4242 assert_eq!(
4243 editor.selections.display_ranges(cx),
4244 vec![
4245 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4246 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4247 ]
4248 );
4249 });
4250
4251 let editor = cx.add_window(|window, cx| {
4252 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4253 build_editor(buffer, window, cx)
4254 });
4255 _ = editor.update(cx, |editor, window, cx| {
4256 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4257 s.select_display_ranges([
4258 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4259 ])
4260 });
4261 editor.delete_line(&DeleteLine, window, cx);
4262 assert_eq!(editor.display_text(cx), "ghi\n");
4263 assert_eq!(
4264 editor.selections.display_ranges(cx),
4265 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4266 );
4267 });
4268}
4269
4270#[gpui::test]
4271fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4272 init_test(cx, |_| {});
4273
4274 cx.add_window(|window, cx| {
4275 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4276 let mut editor = build_editor(buffer.clone(), window, cx);
4277 let buffer = buffer.read(cx).as_singleton().unwrap();
4278
4279 assert_eq!(
4280 editor.selections.ranges::<Point>(cx),
4281 &[Point::new(0, 0)..Point::new(0, 0)]
4282 );
4283
4284 // When on single line, replace newline at end by space
4285 editor.join_lines(&JoinLines, window, cx);
4286 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4287 assert_eq!(
4288 editor.selections.ranges::<Point>(cx),
4289 &[Point::new(0, 3)..Point::new(0, 3)]
4290 );
4291
4292 // When multiple lines are selected, remove newlines that are spanned by the selection
4293 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4294 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4295 });
4296 editor.join_lines(&JoinLines, window, cx);
4297 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4298 assert_eq!(
4299 editor.selections.ranges::<Point>(cx),
4300 &[Point::new(0, 11)..Point::new(0, 11)]
4301 );
4302
4303 // Undo should be transactional
4304 editor.undo(&Undo, window, cx);
4305 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4306 assert_eq!(
4307 editor.selections.ranges::<Point>(cx),
4308 &[Point::new(0, 5)..Point::new(2, 2)]
4309 );
4310
4311 // When joining an empty line don't insert a space
4312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4313 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4314 });
4315 editor.join_lines(&JoinLines, window, cx);
4316 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4317 assert_eq!(
4318 editor.selections.ranges::<Point>(cx),
4319 [Point::new(2, 3)..Point::new(2, 3)]
4320 );
4321
4322 // We can remove trailing newlines
4323 editor.join_lines(&JoinLines, window, cx);
4324 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4325 assert_eq!(
4326 editor.selections.ranges::<Point>(cx),
4327 [Point::new(2, 3)..Point::new(2, 3)]
4328 );
4329
4330 // We don't blow up on the last line
4331 editor.join_lines(&JoinLines, window, cx);
4332 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4333 assert_eq!(
4334 editor.selections.ranges::<Point>(cx),
4335 [Point::new(2, 3)..Point::new(2, 3)]
4336 );
4337
4338 // reset to test indentation
4339 editor.buffer.update(cx, |buffer, cx| {
4340 buffer.edit(
4341 [
4342 (Point::new(1, 0)..Point::new(1, 2), " "),
4343 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4344 ],
4345 None,
4346 cx,
4347 )
4348 });
4349
4350 // We remove any leading spaces
4351 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4353 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4354 });
4355 editor.join_lines(&JoinLines, window, cx);
4356 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4357
4358 // We don't insert a space for a line containing only spaces
4359 editor.join_lines(&JoinLines, window, cx);
4360 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4361
4362 // We ignore any leading tabs
4363 editor.join_lines(&JoinLines, window, cx);
4364 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4365
4366 editor
4367 });
4368}
4369
4370#[gpui::test]
4371fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4372 init_test(cx, |_| {});
4373
4374 cx.add_window(|window, cx| {
4375 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4376 let mut editor = build_editor(buffer.clone(), window, cx);
4377 let buffer = buffer.read(cx).as_singleton().unwrap();
4378
4379 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4380 s.select_ranges([
4381 Point::new(0, 2)..Point::new(1, 1),
4382 Point::new(1, 2)..Point::new(1, 2),
4383 Point::new(3, 1)..Point::new(3, 2),
4384 ])
4385 });
4386
4387 editor.join_lines(&JoinLines, window, cx);
4388 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4389
4390 assert_eq!(
4391 editor.selections.ranges::<Point>(cx),
4392 [
4393 Point::new(0, 7)..Point::new(0, 7),
4394 Point::new(1, 3)..Point::new(1, 3)
4395 ]
4396 );
4397 editor
4398 });
4399}
4400
4401#[gpui::test]
4402async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4403 init_test(cx, |_| {});
4404
4405 let mut cx = EditorTestContext::new(cx).await;
4406
4407 let diff_base = r#"
4408 Line 0
4409 Line 1
4410 Line 2
4411 Line 3
4412 "#
4413 .unindent();
4414
4415 cx.set_state(
4416 &r#"
4417 ˇLine 0
4418 Line 1
4419 Line 2
4420 Line 3
4421 "#
4422 .unindent(),
4423 );
4424
4425 cx.set_head_text(&diff_base);
4426 executor.run_until_parked();
4427
4428 // Join lines
4429 cx.update_editor(|editor, window, cx| {
4430 editor.join_lines(&JoinLines, window, cx);
4431 });
4432 executor.run_until_parked();
4433
4434 cx.assert_editor_state(
4435 &r#"
4436 Line 0ˇ Line 1
4437 Line 2
4438 Line 3
4439 "#
4440 .unindent(),
4441 );
4442 // Join again
4443 cx.update_editor(|editor, window, cx| {
4444 editor.join_lines(&JoinLines, window, cx);
4445 });
4446 executor.run_until_parked();
4447
4448 cx.assert_editor_state(
4449 &r#"
4450 Line 0 Line 1ˇ Line 2
4451 Line 3
4452 "#
4453 .unindent(),
4454 );
4455}
4456
4457#[gpui::test]
4458async fn test_custom_newlines_cause_no_false_positive_diffs(
4459 executor: BackgroundExecutor,
4460 cx: &mut TestAppContext,
4461) {
4462 init_test(cx, |_| {});
4463 let mut cx = EditorTestContext::new(cx).await;
4464 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4465 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4466 executor.run_until_parked();
4467
4468 cx.update_editor(|editor, window, cx| {
4469 let snapshot = editor.snapshot(window, cx);
4470 assert_eq!(
4471 snapshot
4472 .buffer_snapshot
4473 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4474 .collect::<Vec<_>>(),
4475 Vec::new(),
4476 "Should not have any diffs for files with custom newlines"
4477 );
4478 });
4479}
4480
4481#[gpui::test]
4482async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4483 init_test(cx, |_| {});
4484
4485 let mut cx = EditorTestContext::new(cx).await;
4486
4487 // Test sort_lines_case_insensitive()
4488 cx.set_state(indoc! {"
4489 «z
4490 y
4491 x
4492 Z
4493 Y
4494 Xˇ»
4495 "});
4496 cx.update_editor(|e, window, cx| {
4497 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4498 });
4499 cx.assert_editor_state(indoc! {"
4500 «x
4501 X
4502 y
4503 Y
4504 z
4505 Zˇ»
4506 "});
4507
4508 // Test sort_lines_by_length()
4509 //
4510 // Demonstrates:
4511 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4512 // - sort is stable
4513 cx.set_state(indoc! {"
4514 «123
4515 æ
4516 12
4517 ∞
4518 1
4519 æˇ»
4520 "});
4521 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4522 cx.assert_editor_state(indoc! {"
4523 «æ
4524 ∞
4525 1
4526 æ
4527 12
4528 123ˇ»
4529 "});
4530
4531 // Test reverse_lines()
4532 cx.set_state(indoc! {"
4533 «5
4534 4
4535 3
4536 2
4537 1ˇ»
4538 "});
4539 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4540 cx.assert_editor_state(indoc! {"
4541 «1
4542 2
4543 3
4544 4
4545 5ˇ»
4546 "});
4547
4548 // Skip testing shuffle_line()
4549
4550 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4551 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4552
4553 // Don't manipulate when cursor is on single line, but expand the selection
4554 cx.set_state(indoc! {"
4555 ddˇdd
4556 ccc
4557 bb
4558 a
4559 "});
4560 cx.update_editor(|e, window, cx| {
4561 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4562 });
4563 cx.assert_editor_state(indoc! {"
4564 «ddddˇ»
4565 ccc
4566 bb
4567 a
4568 "});
4569
4570 // Basic manipulate case
4571 // Start selection moves to column 0
4572 // End of selection shrinks to fit shorter line
4573 cx.set_state(indoc! {"
4574 dd«d
4575 ccc
4576 bb
4577 aaaaaˇ»
4578 "});
4579 cx.update_editor(|e, window, cx| {
4580 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4581 });
4582 cx.assert_editor_state(indoc! {"
4583 «aaaaa
4584 bb
4585 ccc
4586 dddˇ»
4587 "});
4588
4589 // Manipulate case with newlines
4590 cx.set_state(indoc! {"
4591 dd«d
4592 ccc
4593
4594 bb
4595 aaaaa
4596
4597 ˇ»
4598 "});
4599 cx.update_editor(|e, window, cx| {
4600 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4601 });
4602 cx.assert_editor_state(indoc! {"
4603 «
4604
4605 aaaaa
4606 bb
4607 ccc
4608 dddˇ»
4609
4610 "});
4611
4612 // Adding new line
4613 cx.set_state(indoc! {"
4614 aa«a
4615 bbˇ»b
4616 "});
4617 cx.update_editor(|e, window, cx| {
4618 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4619 });
4620 cx.assert_editor_state(indoc! {"
4621 «aaa
4622 bbb
4623 added_lineˇ»
4624 "});
4625
4626 // Removing line
4627 cx.set_state(indoc! {"
4628 aa«a
4629 bbbˇ»
4630 "});
4631 cx.update_editor(|e, window, cx| {
4632 e.manipulate_immutable_lines(window, cx, |lines| {
4633 lines.pop();
4634 })
4635 });
4636 cx.assert_editor_state(indoc! {"
4637 «aaaˇ»
4638 "});
4639
4640 // Removing all lines
4641 cx.set_state(indoc! {"
4642 aa«a
4643 bbbˇ»
4644 "});
4645 cx.update_editor(|e, window, cx| {
4646 e.manipulate_immutable_lines(window, cx, |lines| {
4647 lines.drain(..);
4648 })
4649 });
4650 cx.assert_editor_state(indoc! {"
4651 ˇ
4652 "});
4653}
4654
4655#[gpui::test]
4656async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4657 init_test(cx, |_| {});
4658
4659 let mut cx = EditorTestContext::new(cx).await;
4660
4661 // Consider continuous selection as single selection
4662 cx.set_state(indoc! {"
4663 Aaa«aa
4664 cˇ»c«c
4665 bb
4666 aaaˇ»aa
4667 "});
4668 cx.update_editor(|e, window, cx| {
4669 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4670 });
4671 cx.assert_editor_state(indoc! {"
4672 «Aaaaa
4673 ccc
4674 bb
4675 aaaaaˇ»
4676 "});
4677
4678 cx.set_state(indoc! {"
4679 Aaa«aa
4680 cˇ»c«c
4681 bb
4682 aaaˇ»aa
4683 "});
4684 cx.update_editor(|e, window, cx| {
4685 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4686 });
4687 cx.assert_editor_state(indoc! {"
4688 «Aaaaa
4689 ccc
4690 bbˇ»
4691 "});
4692
4693 // Consider non continuous selection as distinct dedup operations
4694 cx.set_state(indoc! {"
4695 «aaaaa
4696 bb
4697 aaaaa
4698 aaaaaˇ»
4699
4700 aaa«aaˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| {
4703 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4704 });
4705 cx.assert_editor_state(indoc! {"
4706 «aaaaa
4707 bbˇ»
4708
4709 «aaaaaˇ»
4710 "});
4711}
4712
4713#[gpui::test]
4714async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4715 init_test(cx, |_| {});
4716
4717 let mut cx = EditorTestContext::new(cx).await;
4718
4719 cx.set_state(indoc! {"
4720 «Aaa
4721 aAa
4722 Aaaˇ»
4723 "});
4724 cx.update_editor(|e, window, cx| {
4725 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4726 });
4727 cx.assert_editor_state(indoc! {"
4728 «Aaa
4729 aAaˇ»
4730 "});
4731
4732 cx.set_state(indoc! {"
4733 «Aaa
4734 aAa
4735 aaAˇ»
4736 "});
4737 cx.update_editor(|e, window, cx| {
4738 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4739 });
4740 cx.assert_editor_state(indoc! {"
4741 «Aaaˇ»
4742 "});
4743}
4744
4745#[gpui::test]
4746async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4747 init_test(cx, |_| {});
4748
4749 let mut cx = EditorTestContext::new(cx).await;
4750
4751 let js_language = Arc::new(Language::new(
4752 LanguageConfig {
4753 name: "JavaScript".into(),
4754 wrap_characters: Some(language::WrapCharactersConfig {
4755 start_prefix: "<".into(),
4756 start_suffix: ">".into(),
4757 end_prefix: "</".into(),
4758 end_suffix: ">".into(),
4759 }),
4760 ..LanguageConfig::default()
4761 },
4762 None,
4763 ));
4764
4765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4766
4767 cx.set_state(indoc! {"
4768 «testˇ»
4769 "});
4770 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4771 cx.assert_editor_state(indoc! {"
4772 <«ˇ»>test</«ˇ»>
4773 "});
4774
4775 cx.set_state(indoc! {"
4776 «test
4777 testˇ»
4778 "});
4779 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4780 cx.assert_editor_state(indoc! {"
4781 <«ˇ»>test
4782 test</«ˇ»>
4783 "});
4784
4785 cx.set_state(indoc! {"
4786 teˇst
4787 "});
4788 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4789 cx.assert_editor_state(indoc! {"
4790 te<«ˇ»></«ˇ»>st
4791 "});
4792}
4793
4794#[gpui::test]
4795async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4796 init_test(cx, |_| {});
4797
4798 let mut cx = EditorTestContext::new(cx).await;
4799
4800 let js_language = Arc::new(Language::new(
4801 LanguageConfig {
4802 name: "JavaScript".into(),
4803 wrap_characters: Some(language::WrapCharactersConfig {
4804 start_prefix: "<".into(),
4805 start_suffix: ">".into(),
4806 end_prefix: "</".into(),
4807 end_suffix: ">".into(),
4808 }),
4809 ..LanguageConfig::default()
4810 },
4811 None,
4812 ));
4813
4814 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4815
4816 cx.set_state(indoc! {"
4817 «testˇ»
4818 «testˇ» «testˇ»
4819 «testˇ»
4820 "});
4821 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4822 cx.assert_editor_state(indoc! {"
4823 <«ˇ»>test</«ˇ»>
4824 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4825 <«ˇ»>test</«ˇ»>
4826 "});
4827
4828 cx.set_state(indoc! {"
4829 «test
4830 testˇ»
4831 «test
4832 testˇ»
4833 "});
4834 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4835 cx.assert_editor_state(indoc! {"
4836 <«ˇ»>test
4837 test</«ˇ»>
4838 <«ˇ»>test
4839 test</«ˇ»>
4840 "});
4841}
4842
4843#[gpui::test]
4844async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4845 init_test(cx, |_| {});
4846
4847 let mut cx = EditorTestContext::new(cx).await;
4848
4849 let plaintext_language = Arc::new(Language::new(
4850 LanguageConfig {
4851 name: "Plain Text".into(),
4852 ..LanguageConfig::default()
4853 },
4854 None,
4855 ));
4856
4857 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4858
4859 cx.set_state(indoc! {"
4860 «testˇ»
4861 "});
4862 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4863 cx.assert_editor_state(indoc! {"
4864 «testˇ»
4865 "});
4866}
4867
4868#[gpui::test]
4869async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4870 init_test(cx, |_| {});
4871
4872 let mut cx = EditorTestContext::new(cx).await;
4873
4874 // Manipulate with multiple selections on a single line
4875 cx.set_state(indoc! {"
4876 dd«dd
4877 cˇ»c«c
4878 bb
4879 aaaˇ»aa
4880 "});
4881 cx.update_editor(|e, window, cx| {
4882 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4883 });
4884 cx.assert_editor_state(indoc! {"
4885 «aaaaa
4886 bb
4887 ccc
4888 ddddˇ»
4889 "});
4890
4891 // Manipulate with multiple disjoin selections
4892 cx.set_state(indoc! {"
4893 5«
4894 4
4895 3
4896 2
4897 1ˇ»
4898
4899 dd«dd
4900 ccc
4901 bb
4902 aaaˇ»aa
4903 "});
4904 cx.update_editor(|e, window, cx| {
4905 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4906 });
4907 cx.assert_editor_state(indoc! {"
4908 «1
4909 2
4910 3
4911 4
4912 5ˇ»
4913
4914 «aaaaa
4915 bb
4916 ccc
4917 ddddˇ»
4918 "});
4919
4920 // Adding lines on each selection
4921 cx.set_state(indoc! {"
4922 2«
4923 1ˇ»
4924
4925 bb«bb
4926 aaaˇ»aa
4927 "});
4928 cx.update_editor(|e, window, cx| {
4929 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4930 });
4931 cx.assert_editor_state(indoc! {"
4932 «2
4933 1
4934 added lineˇ»
4935
4936 «bbbb
4937 aaaaa
4938 added lineˇ»
4939 "});
4940
4941 // Removing lines on each selection
4942 cx.set_state(indoc! {"
4943 2«
4944 1ˇ»
4945
4946 bb«bb
4947 aaaˇ»aa
4948 "});
4949 cx.update_editor(|e, window, cx| {
4950 e.manipulate_immutable_lines(window, cx, |lines| {
4951 lines.pop();
4952 })
4953 });
4954 cx.assert_editor_state(indoc! {"
4955 «2ˇ»
4956
4957 «bbbbˇ»
4958 "});
4959}
4960
4961#[gpui::test]
4962async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4963 init_test(cx, |settings| {
4964 settings.defaults.tab_size = NonZeroU32::new(3)
4965 });
4966
4967 let mut cx = EditorTestContext::new(cx).await;
4968
4969 // MULTI SELECTION
4970 // Ln.1 "«" tests empty lines
4971 // Ln.9 tests just leading whitespace
4972 cx.set_state(indoc! {"
4973 «
4974 abc // No indentationˇ»
4975 «\tabc // 1 tabˇ»
4976 \t\tabc « ˇ» // 2 tabs
4977 \t ab«c // Tab followed by space
4978 \tabc // Space followed by tab (3 spaces should be the result)
4979 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4980 abˇ»ˇc ˇ ˇ // Already space indented«
4981 \t
4982 \tabc\tdef // Only the leading tab is manipulatedˇ»
4983 "});
4984 cx.update_editor(|e, window, cx| {
4985 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4986 });
4987 cx.assert_editor_state(
4988 indoc! {"
4989 «
4990 abc // No indentation
4991 abc // 1 tab
4992 abc // 2 tabs
4993 abc // Tab followed by space
4994 abc // Space followed by tab (3 spaces should be the result)
4995 abc // Mixed indentation (tab conversion depends on the column)
4996 abc // Already space indented
4997 ·
4998 abc\tdef // Only the leading tab is manipulatedˇ»
4999 "}
5000 .replace("·", "")
5001 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5002 );
5003
5004 // Test on just a few lines, the others should remain unchanged
5005 // Only lines (3, 5, 10, 11) should change
5006 cx.set_state(
5007 indoc! {"
5008 ·
5009 abc // No indentation
5010 \tabcˇ // 1 tab
5011 \t\tabc // 2 tabs
5012 \t abcˇ // Tab followed by space
5013 \tabc // Space followed by tab (3 spaces should be the result)
5014 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5015 abc // Already space indented
5016 «\t
5017 \tabc\tdef // Only the leading tab is manipulatedˇ»
5018 "}
5019 .replace("·", "")
5020 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5021 );
5022 cx.update_editor(|e, window, cx| {
5023 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5024 });
5025 cx.assert_editor_state(
5026 indoc! {"
5027 ·
5028 abc // No indentation
5029 « abc // 1 tabˇ»
5030 \t\tabc // 2 tabs
5031 « abc // Tab followed by spaceˇ»
5032 \tabc // Space followed by tab (3 spaces should be the result)
5033 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5034 abc // Already space indented
5035 « ·
5036 abc\tdef // Only the leading tab is manipulatedˇ»
5037 "}
5038 .replace("·", "")
5039 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5040 );
5041
5042 // SINGLE SELECTION
5043 // Ln.1 "«" tests empty lines
5044 // Ln.9 tests just leading whitespace
5045 cx.set_state(indoc! {"
5046 «
5047 abc // No indentation
5048 \tabc // 1 tab
5049 \t\tabc // 2 tabs
5050 \t abc // Tab followed by space
5051 \tabc // Space followed by tab (3 spaces should be the result)
5052 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5053 abc // Already space indented
5054 \t
5055 \tabc\tdef // Only the leading tab is manipulatedˇ»
5056 "});
5057 cx.update_editor(|e, window, cx| {
5058 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5059 });
5060 cx.assert_editor_state(
5061 indoc! {"
5062 «
5063 abc // No indentation
5064 abc // 1 tab
5065 abc // 2 tabs
5066 abc // Tab followed by space
5067 abc // Space followed by tab (3 spaces should be the result)
5068 abc // Mixed indentation (tab conversion depends on the column)
5069 abc // Already space indented
5070 ·
5071 abc\tdef // Only the leading tab is manipulatedˇ»
5072 "}
5073 .replace("·", "")
5074 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5075 );
5076}
5077
5078#[gpui::test]
5079async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5080 init_test(cx, |settings| {
5081 settings.defaults.tab_size = NonZeroU32::new(3)
5082 });
5083
5084 let mut cx = EditorTestContext::new(cx).await;
5085
5086 // MULTI SELECTION
5087 // Ln.1 "«" tests empty lines
5088 // Ln.11 tests just leading whitespace
5089 cx.set_state(indoc! {"
5090 «
5091 abˇ»ˇc // No indentation
5092 abc ˇ ˇ // 1 space (< 3 so dont convert)
5093 abc « // 2 spaces (< 3 so dont convert)
5094 abc // 3 spaces (convert)
5095 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5096 «\tˇ»\t«\tˇ»abc // Already tab indented
5097 «\t abc // Tab followed by space
5098 \tabc // Space followed by tab (should be consumed due to tab)
5099 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5100 \tˇ» «\t
5101 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5102 "});
5103 cx.update_editor(|e, window, cx| {
5104 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5105 });
5106 cx.assert_editor_state(indoc! {"
5107 «
5108 abc // No indentation
5109 abc // 1 space (< 3 so dont convert)
5110 abc // 2 spaces (< 3 so dont convert)
5111 \tabc // 3 spaces (convert)
5112 \t abc // 5 spaces (1 tab + 2 spaces)
5113 \t\t\tabc // Already tab indented
5114 \t abc // Tab followed by space
5115 \tabc // Space followed by tab (should be consumed due to tab)
5116 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5117 \t\t\t
5118 \tabc \t // Only the leading spaces should be convertedˇ»
5119 "});
5120
5121 // Test on just a few lines, the other should remain unchanged
5122 // Only lines (4, 8, 11, 12) should change
5123 cx.set_state(
5124 indoc! {"
5125 ·
5126 abc // No indentation
5127 abc // 1 space (< 3 so dont convert)
5128 abc // 2 spaces (< 3 so dont convert)
5129 « abc // 3 spaces (convert)ˇ»
5130 abc // 5 spaces (1 tab + 2 spaces)
5131 \t\t\tabc // Already tab indented
5132 \t abc // Tab followed by space
5133 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5134 \t\t \tabc // Mixed indentation
5135 \t \t \t \tabc // Mixed indentation
5136 \t \tˇ
5137 « abc \t // Only the leading spaces should be convertedˇ»
5138 "}
5139 .replace("·", "")
5140 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5141 );
5142 cx.update_editor(|e, window, cx| {
5143 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5144 });
5145 cx.assert_editor_state(
5146 indoc! {"
5147 ·
5148 abc // No indentation
5149 abc // 1 space (< 3 so dont convert)
5150 abc // 2 spaces (< 3 so dont convert)
5151 «\tabc // 3 spaces (convert)ˇ»
5152 abc // 5 spaces (1 tab + 2 spaces)
5153 \t\t\tabc // Already tab indented
5154 \t abc // Tab followed by space
5155 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5156 \t\t \tabc // Mixed indentation
5157 \t \t \t \tabc // Mixed indentation
5158 «\t\t\t
5159 \tabc \t // Only the leading spaces should be convertedˇ»
5160 "}
5161 .replace("·", "")
5162 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5163 );
5164
5165 // SINGLE SELECTION
5166 // Ln.1 "«" tests empty lines
5167 // Ln.11 tests just leading whitespace
5168 cx.set_state(indoc! {"
5169 «
5170 abc // No indentation
5171 abc // 1 space (< 3 so dont convert)
5172 abc // 2 spaces (< 3 so dont convert)
5173 abc // 3 spaces (convert)
5174 abc // 5 spaces (1 tab + 2 spaces)
5175 \t\t\tabc // Already tab indented
5176 \t abc // Tab followed by space
5177 \tabc // Space followed by tab (should be consumed due to tab)
5178 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5179 \t \t
5180 abc \t // Only the leading spaces should be convertedˇ»
5181 "});
5182 cx.update_editor(|e, window, cx| {
5183 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5184 });
5185 cx.assert_editor_state(indoc! {"
5186 «
5187 abc // No indentation
5188 abc // 1 space (< 3 so dont convert)
5189 abc // 2 spaces (< 3 so dont convert)
5190 \tabc // 3 spaces (convert)
5191 \t abc // 5 spaces (1 tab + 2 spaces)
5192 \t\t\tabc // Already tab indented
5193 \t abc // Tab followed by space
5194 \tabc // Space followed by tab (should be consumed due to tab)
5195 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5196 \t\t\t
5197 \tabc \t // Only the leading spaces should be convertedˇ»
5198 "});
5199}
5200
5201#[gpui::test]
5202async fn test_toggle_case(cx: &mut TestAppContext) {
5203 init_test(cx, |_| {});
5204
5205 let mut cx = EditorTestContext::new(cx).await;
5206
5207 // If all lower case -> upper case
5208 cx.set_state(indoc! {"
5209 «hello worldˇ»
5210 "});
5211 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5212 cx.assert_editor_state(indoc! {"
5213 «HELLO WORLDˇ»
5214 "});
5215
5216 // If all upper case -> lower case
5217 cx.set_state(indoc! {"
5218 «HELLO WORLDˇ»
5219 "});
5220 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5221 cx.assert_editor_state(indoc! {"
5222 «hello worldˇ»
5223 "});
5224
5225 // If any upper case characters are identified -> lower case
5226 // This matches JetBrains IDEs
5227 cx.set_state(indoc! {"
5228 «hEllo worldˇ»
5229 "});
5230 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5231 cx.assert_editor_state(indoc! {"
5232 «hello worldˇ»
5233 "});
5234}
5235
5236#[gpui::test]
5237async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5238 init_test(cx, |_| {});
5239
5240 let mut cx = EditorTestContext::new(cx).await;
5241
5242 cx.set_state(indoc! {"
5243 «implement-windows-supportˇ»
5244 "});
5245 cx.update_editor(|e, window, cx| {
5246 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5247 });
5248 cx.assert_editor_state(indoc! {"
5249 «Implement windows supportˇ»
5250 "});
5251}
5252
5253#[gpui::test]
5254async fn test_manipulate_text(cx: &mut TestAppContext) {
5255 init_test(cx, |_| {});
5256
5257 let mut cx = EditorTestContext::new(cx).await;
5258
5259 // Test convert_to_upper_case()
5260 cx.set_state(indoc! {"
5261 «hello worldˇ»
5262 "});
5263 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5264 cx.assert_editor_state(indoc! {"
5265 «HELLO WORLDˇ»
5266 "});
5267
5268 // Test convert_to_lower_case()
5269 cx.set_state(indoc! {"
5270 «HELLO WORLDˇ»
5271 "});
5272 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5273 cx.assert_editor_state(indoc! {"
5274 «hello worldˇ»
5275 "});
5276
5277 // Test multiple line, single selection case
5278 cx.set_state(indoc! {"
5279 «The quick brown
5280 fox jumps over
5281 the lazy dogˇ»
5282 "});
5283 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5284 cx.assert_editor_state(indoc! {"
5285 «The Quick Brown
5286 Fox Jumps Over
5287 The Lazy Dogˇ»
5288 "});
5289
5290 // Test multiple line, single selection case
5291 cx.set_state(indoc! {"
5292 «The quick brown
5293 fox jumps over
5294 the lazy dogˇ»
5295 "});
5296 cx.update_editor(|e, window, cx| {
5297 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5298 });
5299 cx.assert_editor_state(indoc! {"
5300 «TheQuickBrown
5301 FoxJumpsOver
5302 TheLazyDogˇ»
5303 "});
5304
5305 // From here on out, test more complex cases of manipulate_text()
5306
5307 // Test no selection case - should affect words cursors are in
5308 // Cursor at beginning, middle, and end of word
5309 cx.set_state(indoc! {"
5310 ˇhello big beauˇtiful worldˇ
5311 "});
5312 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5313 cx.assert_editor_state(indoc! {"
5314 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5315 "});
5316
5317 // Test multiple selections on a single line and across multiple lines
5318 cx.set_state(indoc! {"
5319 «Theˇ» quick «brown
5320 foxˇ» jumps «overˇ»
5321 the «lazyˇ» dog
5322 "});
5323 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5324 cx.assert_editor_state(indoc! {"
5325 «THEˇ» quick «BROWN
5326 FOXˇ» jumps «OVERˇ»
5327 the «LAZYˇ» dog
5328 "});
5329
5330 // Test case where text length grows
5331 cx.set_state(indoc! {"
5332 «tschüߡ»
5333 "});
5334 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5335 cx.assert_editor_state(indoc! {"
5336 «TSCHÜSSˇ»
5337 "});
5338
5339 // Test to make sure we don't crash when text shrinks
5340 cx.set_state(indoc! {"
5341 aaa_bbbˇ
5342 "});
5343 cx.update_editor(|e, window, cx| {
5344 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5345 });
5346 cx.assert_editor_state(indoc! {"
5347 «aaaBbbˇ»
5348 "});
5349
5350 // Test to make sure we all aware of the fact that each word can grow and shrink
5351 // Final selections should be aware of this fact
5352 cx.set_state(indoc! {"
5353 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5354 "});
5355 cx.update_editor(|e, window, cx| {
5356 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5357 });
5358 cx.assert_editor_state(indoc! {"
5359 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5360 "});
5361
5362 cx.set_state(indoc! {"
5363 «hElLo, WoRld!ˇ»
5364 "});
5365 cx.update_editor(|e, window, cx| {
5366 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5367 });
5368 cx.assert_editor_state(indoc! {"
5369 «HeLlO, wOrLD!ˇ»
5370 "});
5371
5372 // Test selections with `line_mode = true`.
5373 cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
5374 cx.set_state(indoc! {"
5375 «The quick brown
5376 fox jumps over
5377 tˇ»he lazy dog
5378 "});
5379 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5380 cx.assert_editor_state(indoc! {"
5381 «THE QUICK BROWN
5382 FOX JUMPS OVER
5383 THE LAZY DOGˇ»
5384 "});
5385}
5386
5387#[gpui::test]
5388fn test_duplicate_line(cx: &mut TestAppContext) {
5389 init_test(cx, |_| {});
5390
5391 let editor = cx.add_window(|window, cx| {
5392 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5393 build_editor(buffer, window, cx)
5394 });
5395 _ = editor.update(cx, |editor, window, cx| {
5396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5397 s.select_display_ranges([
5398 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5399 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5400 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5401 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5402 ])
5403 });
5404 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5405 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5406 assert_eq!(
5407 editor.selections.display_ranges(cx),
5408 vec![
5409 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5410 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5411 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5412 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5413 ]
5414 );
5415 });
5416
5417 let editor = cx.add_window(|window, cx| {
5418 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5419 build_editor(buffer, window, cx)
5420 });
5421 _ = editor.update(cx, |editor, window, cx| {
5422 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5423 s.select_display_ranges([
5424 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5425 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5426 ])
5427 });
5428 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5429 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5430 assert_eq!(
5431 editor.selections.display_ranges(cx),
5432 vec![
5433 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5434 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5435 ]
5436 );
5437 });
5438
5439 // With `move_upwards` the selections stay in place, except for
5440 // the lines inserted above them
5441 let editor = cx.add_window(|window, cx| {
5442 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5443 build_editor(buffer, window, cx)
5444 });
5445 _ = editor.update(cx, |editor, window, cx| {
5446 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5447 s.select_display_ranges([
5448 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5449 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5450 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5451 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5452 ])
5453 });
5454 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5455 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5456 assert_eq!(
5457 editor.selections.display_ranges(cx),
5458 vec![
5459 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5460 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5461 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5462 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5463 ]
5464 );
5465 });
5466
5467 let editor = cx.add_window(|window, cx| {
5468 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5469 build_editor(buffer, window, cx)
5470 });
5471 _ = editor.update(cx, |editor, window, cx| {
5472 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5473 s.select_display_ranges([
5474 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5475 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5476 ])
5477 });
5478 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5479 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5480 assert_eq!(
5481 editor.selections.display_ranges(cx),
5482 vec![
5483 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5484 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5485 ]
5486 );
5487 });
5488
5489 let editor = cx.add_window(|window, cx| {
5490 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5491 build_editor(buffer, window, cx)
5492 });
5493 _ = editor.update(cx, |editor, window, cx| {
5494 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5495 s.select_display_ranges([
5496 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5497 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5498 ])
5499 });
5500 editor.duplicate_selection(&DuplicateSelection, window, cx);
5501 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5502 assert_eq!(
5503 editor.selections.display_ranges(cx),
5504 vec![
5505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5506 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5507 ]
5508 );
5509 });
5510}
5511
5512#[gpui::test]
5513fn test_move_line_up_down(cx: &mut TestAppContext) {
5514 init_test(cx, |_| {});
5515
5516 let editor = cx.add_window(|window, cx| {
5517 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5518 build_editor(buffer, window, cx)
5519 });
5520 _ = editor.update(cx, |editor, window, cx| {
5521 editor.fold_creases(
5522 vec![
5523 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5524 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5525 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5526 ],
5527 true,
5528 window,
5529 cx,
5530 );
5531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5532 s.select_display_ranges([
5533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5534 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5535 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5536 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5537 ])
5538 });
5539 assert_eq!(
5540 editor.display_text(cx),
5541 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5542 );
5543
5544 editor.move_line_up(&MoveLineUp, window, cx);
5545 assert_eq!(
5546 editor.display_text(cx),
5547 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5548 );
5549 assert_eq!(
5550 editor.selections.display_ranges(cx),
5551 vec![
5552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5553 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5554 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5555 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5556 ]
5557 );
5558 });
5559
5560 _ = editor.update(cx, |editor, window, cx| {
5561 editor.move_line_down(&MoveLineDown, window, cx);
5562 assert_eq!(
5563 editor.display_text(cx),
5564 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5565 );
5566 assert_eq!(
5567 editor.selections.display_ranges(cx),
5568 vec![
5569 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5570 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5571 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5572 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5573 ]
5574 );
5575 });
5576
5577 _ = editor.update(cx, |editor, window, cx| {
5578 editor.move_line_down(&MoveLineDown, window, cx);
5579 assert_eq!(
5580 editor.display_text(cx),
5581 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5582 );
5583 assert_eq!(
5584 editor.selections.display_ranges(cx),
5585 vec![
5586 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5587 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5588 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5589 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5590 ]
5591 );
5592 });
5593
5594 _ = editor.update(cx, |editor, window, cx| {
5595 editor.move_line_up(&MoveLineUp, window, cx);
5596 assert_eq!(
5597 editor.display_text(cx),
5598 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5599 );
5600 assert_eq!(
5601 editor.selections.display_ranges(cx),
5602 vec![
5603 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5604 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5605 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5606 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5607 ]
5608 );
5609 });
5610}
5611
5612#[gpui::test]
5613fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5614 init_test(cx, |_| {});
5615 let editor = cx.add_window(|window, cx| {
5616 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5617 build_editor(buffer, window, cx)
5618 });
5619 _ = editor.update(cx, |editor, window, cx| {
5620 editor.fold_creases(
5621 vec![Crease::simple(
5622 Point::new(6, 4)..Point::new(7, 4),
5623 FoldPlaceholder::test(),
5624 )],
5625 true,
5626 window,
5627 cx,
5628 );
5629 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5630 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5631 });
5632 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5633 editor.move_line_up(&MoveLineUp, window, cx);
5634 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5635 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5636 });
5637}
5638
5639#[gpui::test]
5640fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5641 init_test(cx, |_| {});
5642
5643 let editor = cx.add_window(|window, cx| {
5644 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5645 build_editor(buffer, window, cx)
5646 });
5647 _ = editor.update(cx, |editor, window, cx| {
5648 let snapshot = editor.buffer.read(cx).snapshot(cx);
5649 editor.insert_blocks(
5650 [BlockProperties {
5651 style: BlockStyle::Fixed,
5652 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5653 height: Some(1),
5654 render: Arc::new(|_| div().into_any()),
5655 priority: 0,
5656 }],
5657 Some(Autoscroll::fit()),
5658 cx,
5659 );
5660 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5661 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5662 });
5663 editor.move_line_down(&MoveLineDown, window, cx);
5664 });
5665}
5666
5667#[gpui::test]
5668async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5669 init_test(cx, |_| {});
5670
5671 let mut cx = EditorTestContext::new(cx).await;
5672 cx.set_state(
5673 &"
5674 ˇzero
5675 one
5676 two
5677 three
5678 four
5679 five
5680 "
5681 .unindent(),
5682 );
5683
5684 // Create a four-line block that replaces three lines of text.
5685 cx.update_editor(|editor, window, cx| {
5686 let snapshot = editor.snapshot(window, cx);
5687 let snapshot = &snapshot.buffer_snapshot;
5688 let placement = BlockPlacement::Replace(
5689 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5690 );
5691 editor.insert_blocks(
5692 [BlockProperties {
5693 placement,
5694 height: Some(4),
5695 style: BlockStyle::Sticky,
5696 render: Arc::new(|_| gpui::div().into_any_element()),
5697 priority: 0,
5698 }],
5699 None,
5700 cx,
5701 );
5702 });
5703
5704 // Move down so that the cursor touches the block.
5705 cx.update_editor(|editor, window, cx| {
5706 editor.move_down(&Default::default(), window, cx);
5707 });
5708 cx.assert_editor_state(
5709 &"
5710 zero
5711 «one
5712 two
5713 threeˇ»
5714 four
5715 five
5716 "
5717 .unindent(),
5718 );
5719
5720 // Move down past the block.
5721 cx.update_editor(|editor, window, cx| {
5722 editor.move_down(&Default::default(), window, cx);
5723 });
5724 cx.assert_editor_state(
5725 &"
5726 zero
5727 one
5728 two
5729 three
5730 ˇfour
5731 five
5732 "
5733 .unindent(),
5734 );
5735}
5736
5737#[gpui::test]
5738fn test_transpose(cx: &mut TestAppContext) {
5739 init_test(cx, |_| {});
5740
5741 _ = cx.add_window(|window, cx| {
5742 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5743 editor.set_style(EditorStyle::default(), window, cx);
5744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5745 s.select_ranges([1..1])
5746 });
5747 editor.transpose(&Default::default(), window, cx);
5748 assert_eq!(editor.text(cx), "bac");
5749 assert_eq!(editor.selections.ranges(cx), [2..2]);
5750
5751 editor.transpose(&Default::default(), window, cx);
5752 assert_eq!(editor.text(cx), "bca");
5753 assert_eq!(editor.selections.ranges(cx), [3..3]);
5754
5755 editor.transpose(&Default::default(), window, cx);
5756 assert_eq!(editor.text(cx), "bac");
5757 assert_eq!(editor.selections.ranges(cx), [3..3]);
5758
5759 editor
5760 });
5761
5762 _ = cx.add_window(|window, cx| {
5763 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5764 editor.set_style(EditorStyle::default(), window, cx);
5765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5766 s.select_ranges([3..3])
5767 });
5768 editor.transpose(&Default::default(), window, cx);
5769 assert_eq!(editor.text(cx), "acb\nde");
5770 assert_eq!(editor.selections.ranges(cx), [3..3]);
5771
5772 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5773 s.select_ranges([4..4])
5774 });
5775 editor.transpose(&Default::default(), window, cx);
5776 assert_eq!(editor.text(cx), "acbd\ne");
5777 assert_eq!(editor.selections.ranges(cx), [5..5]);
5778
5779 editor.transpose(&Default::default(), window, cx);
5780 assert_eq!(editor.text(cx), "acbde\n");
5781 assert_eq!(editor.selections.ranges(cx), [6..6]);
5782
5783 editor.transpose(&Default::default(), window, cx);
5784 assert_eq!(editor.text(cx), "acbd\ne");
5785 assert_eq!(editor.selections.ranges(cx), [6..6]);
5786
5787 editor
5788 });
5789
5790 _ = cx.add_window(|window, cx| {
5791 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5792 editor.set_style(EditorStyle::default(), window, cx);
5793 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5794 s.select_ranges([1..1, 2..2, 4..4])
5795 });
5796 editor.transpose(&Default::default(), window, cx);
5797 assert_eq!(editor.text(cx), "bacd\ne");
5798 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5799
5800 editor.transpose(&Default::default(), window, cx);
5801 assert_eq!(editor.text(cx), "bcade\n");
5802 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5803
5804 editor.transpose(&Default::default(), window, cx);
5805 assert_eq!(editor.text(cx), "bcda\ne");
5806 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5807
5808 editor.transpose(&Default::default(), window, cx);
5809 assert_eq!(editor.text(cx), "bcade\n");
5810 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5811
5812 editor.transpose(&Default::default(), window, cx);
5813 assert_eq!(editor.text(cx), "bcaed\n");
5814 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5815
5816 editor
5817 });
5818
5819 _ = cx.add_window(|window, cx| {
5820 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5821 editor.set_style(EditorStyle::default(), window, cx);
5822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5823 s.select_ranges([4..4])
5824 });
5825 editor.transpose(&Default::default(), window, cx);
5826 assert_eq!(editor.text(cx), "🏀🍐✋");
5827 assert_eq!(editor.selections.ranges(cx), [8..8]);
5828
5829 editor.transpose(&Default::default(), window, cx);
5830 assert_eq!(editor.text(cx), "🏀✋🍐");
5831 assert_eq!(editor.selections.ranges(cx), [11..11]);
5832
5833 editor.transpose(&Default::default(), window, cx);
5834 assert_eq!(editor.text(cx), "🏀🍐✋");
5835 assert_eq!(editor.selections.ranges(cx), [11..11]);
5836
5837 editor
5838 });
5839}
5840
5841#[gpui::test]
5842async fn test_rewrap(cx: &mut TestAppContext) {
5843 init_test(cx, |settings| {
5844 settings.languages.0.extend([
5845 (
5846 "Markdown".into(),
5847 LanguageSettingsContent {
5848 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5849 preferred_line_length: Some(40),
5850 ..Default::default()
5851 },
5852 ),
5853 (
5854 "Plain Text".into(),
5855 LanguageSettingsContent {
5856 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5857 preferred_line_length: Some(40),
5858 ..Default::default()
5859 },
5860 ),
5861 (
5862 "C++".into(),
5863 LanguageSettingsContent {
5864 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5865 preferred_line_length: Some(40),
5866 ..Default::default()
5867 },
5868 ),
5869 (
5870 "Python".into(),
5871 LanguageSettingsContent {
5872 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5873 preferred_line_length: Some(40),
5874 ..Default::default()
5875 },
5876 ),
5877 (
5878 "Rust".into(),
5879 LanguageSettingsContent {
5880 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5881 preferred_line_length: Some(40),
5882 ..Default::default()
5883 },
5884 ),
5885 ])
5886 });
5887
5888 let mut cx = EditorTestContext::new(cx).await;
5889
5890 let cpp_language = Arc::new(Language::new(
5891 LanguageConfig {
5892 name: "C++".into(),
5893 line_comments: vec!["// ".into()],
5894 ..LanguageConfig::default()
5895 },
5896 None,
5897 ));
5898 let python_language = Arc::new(Language::new(
5899 LanguageConfig {
5900 name: "Python".into(),
5901 line_comments: vec!["# ".into()],
5902 ..LanguageConfig::default()
5903 },
5904 None,
5905 ));
5906 let markdown_language = Arc::new(Language::new(
5907 LanguageConfig {
5908 name: "Markdown".into(),
5909 rewrap_prefixes: vec![
5910 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5911 regex::Regex::new("[-*+]\\s+").unwrap(),
5912 ],
5913 ..LanguageConfig::default()
5914 },
5915 None,
5916 ));
5917 let rust_language = Arc::new(
5918 Language::new(
5919 LanguageConfig {
5920 name: "Rust".into(),
5921 line_comments: vec!["// ".into(), "/// ".into()],
5922 ..LanguageConfig::default()
5923 },
5924 Some(tree_sitter_rust::LANGUAGE.into()),
5925 )
5926 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5927 .unwrap(),
5928 );
5929
5930 let plaintext_language = Arc::new(Language::new(
5931 LanguageConfig {
5932 name: "Plain Text".into(),
5933 ..LanguageConfig::default()
5934 },
5935 None,
5936 ));
5937
5938 // Test basic rewrapping of a long line with a cursor
5939 assert_rewrap(
5940 indoc! {"
5941 // ˇThis is a long comment that needs to be wrapped.
5942 "},
5943 indoc! {"
5944 // ˇThis is a long comment that needs to
5945 // be wrapped.
5946 "},
5947 cpp_language.clone(),
5948 &mut cx,
5949 );
5950
5951 // Test rewrapping a full selection
5952 assert_rewrap(
5953 indoc! {"
5954 «// This selected long comment needs to be wrapped.ˇ»"
5955 },
5956 indoc! {"
5957 «// This selected long comment needs to
5958 // be wrapped.ˇ»"
5959 },
5960 cpp_language.clone(),
5961 &mut cx,
5962 );
5963
5964 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5965 assert_rewrap(
5966 indoc! {"
5967 // ˇThis is the first line.
5968 // Thisˇ is the second line.
5969 // This is the thirdˇ line, all part of one paragraph.
5970 "},
5971 indoc! {"
5972 // ˇThis is the first line. Thisˇ is the
5973 // second line. This is the thirdˇ line,
5974 // all part of one paragraph.
5975 "},
5976 cpp_language.clone(),
5977 &mut cx,
5978 );
5979
5980 // Test multiple cursors in different paragraphs trigger separate rewraps
5981 assert_rewrap(
5982 indoc! {"
5983 // ˇThis is the first paragraph, first line.
5984 // ˇThis is the first paragraph, second line.
5985
5986 // ˇThis is the second paragraph, first line.
5987 // ˇThis is the second paragraph, second line.
5988 "},
5989 indoc! {"
5990 // ˇThis is the first paragraph, first
5991 // line. ˇThis is the first paragraph,
5992 // second line.
5993
5994 // ˇThis is the second paragraph, first
5995 // line. ˇThis is the second paragraph,
5996 // second line.
5997 "},
5998 cpp_language.clone(),
5999 &mut cx,
6000 );
6001
6002 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6003 assert_rewrap(
6004 indoc! {"
6005 «// A regular long long comment to be wrapped.
6006 /// A documentation long comment to be wrapped.ˇ»
6007 "},
6008 indoc! {"
6009 «// A regular long long comment to be
6010 // wrapped.
6011 /// A documentation long comment to be
6012 /// wrapped.ˇ»
6013 "},
6014 rust_language.clone(),
6015 &mut cx,
6016 );
6017
6018 // Test that change in indentation level trigger seperate rewraps
6019 assert_rewrap(
6020 indoc! {"
6021 fn foo() {
6022 «// This is a long comment at the base indent.
6023 // This is a long comment at the next indent.ˇ»
6024 }
6025 "},
6026 indoc! {"
6027 fn foo() {
6028 «// This is a long comment at the
6029 // base indent.
6030 // This is a long comment at the
6031 // next indent.ˇ»
6032 }
6033 "},
6034 rust_language.clone(),
6035 &mut cx,
6036 );
6037
6038 // Test that different comment prefix characters (e.g., '#') are handled correctly
6039 assert_rewrap(
6040 indoc! {"
6041 # ˇThis is a long comment using a pound sign.
6042 "},
6043 indoc! {"
6044 # ˇThis is a long comment using a pound
6045 # sign.
6046 "},
6047 python_language,
6048 &mut cx,
6049 );
6050
6051 // Test rewrapping only affects comments, not code even when selected
6052 assert_rewrap(
6053 indoc! {"
6054 «/// This doc comment is long and should be wrapped.
6055 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6056 "},
6057 indoc! {"
6058 «/// This doc comment is long and should
6059 /// be wrapped.
6060 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6061 "},
6062 rust_language.clone(),
6063 &mut cx,
6064 );
6065
6066 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6067 assert_rewrap(
6068 indoc! {"
6069 # Header
6070
6071 A long long long line of markdown text to wrap.ˇ
6072 "},
6073 indoc! {"
6074 # Header
6075
6076 A long long long line of markdown text
6077 to wrap.ˇ
6078 "},
6079 markdown_language.clone(),
6080 &mut cx,
6081 );
6082
6083 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6084 assert_rewrap(
6085 indoc! {"
6086 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6087 2. This is a numbered list item that is very long and needs to be wrapped properly.
6088 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6089 "},
6090 indoc! {"
6091 «1. This is a numbered list item that is
6092 very long and needs to be wrapped
6093 properly.
6094 2. This is a numbered list item that is
6095 very long and needs to be wrapped
6096 properly.
6097 - This is an unordered list item that is
6098 also very long and should not merge
6099 with the numbered item.ˇ»
6100 "},
6101 markdown_language.clone(),
6102 &mut cx,
6103 );
6104
6105 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6106 assert_rewrap(
6107 indoc! {"
6108 «1. This is a numbered list item that is
6109 very long and needs to be wrapped
6110 properly.
6111 2. This is a numbered list item that is
6112 very long and needs to be wrapped
6113 properly.
6114 - This is an unordered list item that is
6115 also very long and should not merge with
6116 the numbered item.ˇ»
6117 "},
6118 indoc! {"
6119 «1. This is a numbered list item that is
6120 very long and needs to be wrapped
6121 properly.
6122 2. This is a numbered list item that is
6123 very long and needs to be wrapped
6124 properly.
6125 - This is an unordered list item that is
6126 also very long and should not merge
6127 with the numbered item.ˇ»
6128 "},
6129 markdown_language.clone(),
6130 &mut cx,
6131 );
6132
6133 // Test that rewrapping maintain indents even when they already exists.
6134 assert_rewrap(
6135 indoc! {"
6136 «1. This is a numbered list
6137 item that is very long and needs to be wrapped properly.
6138 2. This is a numbered list
6139 item that is very long and needs to be wrapped properly.
6140 - This is an unordered list item that is also very long and
6141 should not merge with the numbered item.ˇ»
6142 "},
6143 indoc! {"
6144 «1. This is a numbered list item that is
6145 very long and needs to be wrapped
6146 properly.
6147 2. This is a numbered list item that is
6148 very long and needs to be wrapped
6149 properly.
6150 - This is an unordered list item that is
6151 also very long and should not merge
6152 with the numbered item.ˇ»
6153 "},
6154 markdown_language,
6155 &mut cx,
6156 );
6157
6158 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6159 assert_rewrap(
6160 indoc! {"
6161 ˇThis is a very long line of plain text that will be wrapped.
6162 "},
6163 indoc! {"
6164 ˇThis is a very long line of plain text
6165 that will be wrapped.
6166 "},
6167 plaintext_language.clone(),
6168 &mut cx,
6169 );
6170
6171 // Test that non-commented code acts as a paragraph boundary within a selection
6172 assert_rewrap(
6173 indoc! {"
6174 «// This is the first long comment block to be wrapped.
6175 fn my_func(a: u32);
6176 // This is the second long comment block to be wrapped.ˇ»
6177 "},
6178 indoc! {"
6179 «// This is the first long comment block
6180 // to be wrapped.
6181 fn my_func(a: u32);
6182 // This is the second long comment block
6183 // to be wrapped.ˇ»
6184 "},
6185 rust_language,
6186 &mut cx,
6187 );
6188
6189 // Test rewrapping multiple selections, including ones with blank lines or tabs
6190 assert_rewrap(
6191 indoc! {"
6192 «ˇThis is a very long line that will be wrapped.
6193
6194 This is another paragraph in the same selection.»
6195
6196 «\tThis is a very long indented line that will be wrapped.ˇ»
6197 "},
6198 indoc! {"
6199 «ˇThis is a very long line that will be
6200 wrapped.
6201
6202 This is another paragraph in the same
6203 selection.»
6204
6205 «\tThis is a very long indented line
6206 \tthat will be wrapped.ˇ»
6207 "},
6208 plaintext_language,
6209 &mut cx,
6210 );
6211
6212 // Test that an empty comment line acts as a paragraph boundary
6213 assert_rewrap(
6214 indoc! {"
6215 // ˇThis is a long comment that will be wrapped.
6216 //
6217 // And this is another long comment that will also be wrapped.ˇ
6218 "},
6219 indoc! {"
6220 // ˇThis is a long comment that will be
6221 // wrapped.
6222 //
6223 // And this is another long comment that
6224 // will also be wrapped.ˇ
6225 "},
6226 cpp_language,
6227 &mut cx,
6228 );
6229
6230 #[track_caller]
6231 fn assert_rewrap(
6232 unwrapped_text: &str,
6233 wrapped_text: &str,
6234 language: Arc<Language>,
6235 cx: &mut EditorTestContext,
6236 ) {
6237 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6238 cx.set_state(unwrapped_text);
6239 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6240 cx.assert_editor_state(wrapped_text);
6241 }
6242}
6243
6244#[gpui::test]
6245async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6246 init_test(cx, |settings| {
6247 settings.languages.0.extend([(
6248 "Rust".into(),
6249 LanguageSettingsContent {
6250 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6251 preferred_line_length: Some(40),
6252 ..Default::default()
6253 },
6254 )])
6255 });
6256
6257 let mut cx = EditorTestContext::new(cx).await;
6258
6259 let rust_lang = Arc::new(
6260 Language::new(
6261 LanguageConfig {
6262 name: "Rust".into(),
6263 line_comments: vec!["// ".into()],
6264 block_comment: Some(BlockCommentConfig {
6265 start: "/*".into(),
6266 end: "*/".into(),
6267 prefix: "* ".into(),
6268 tab_size: 1,
6269 }),
6270 documentation_comment: Some(BlockCommentConfig {
6271 start: "/**".into(),
6272 end: "*/".into(),
6273 prefix: "* ".into(),
6274 tab_size: 1,
6275 }),
6276
6277 ..LanguageConfig::default()
6278 },
6279 Some(tree_sitter_rust::LANGUAGE.into()),
6280 )
6281 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6282 .unwrap(),
6283 );
6284
6285 // regular block comment
6286 assert_rewrap(
6287 indoc! {"
6288 /*
6289 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6290 */
6291 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6292 "},
6293 indoc! {"
6294 /*
6295 *ˇ Lorem ipsum dolor sit amet,
6296 * consectetur adipiscing elit.
6297 */
6298 /*
6299 *ˇ Lorem ipsum dolor sit amet,
6300 * consectetur adipiscing elit.
6301 */
6302 "},
6303 rust_lang.clone(),
6304 &mut cx,
6305 );
6306
6307 // indent is respected
6308 assert_rewrap(
6309 indoc! {"
6310 {}
6311 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6312 "},
6313 indoc! {"
6314 {}
6315 /*
6316 *ˇ Lorem ipsum dolor sit amet,
6317 * consectetur adipiscing elit.
6318 */
6319 "},
6320 rust_lang.clone(),
6321 &mut cx,
6322 );
6323
6324 // short block comments with inline delimiters
6325 assert_rewrap(
6326 indoc! {"
6327 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6328 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6329 */
6330 /*
6331 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6332 "},
6333 indoc! {"
6334 /*
6335 *ˇ Lorem ipsum dolor sit amet,
6336 * consectetur adipiscing elit.
6337 */
6338 /*
6339 *ˇ Lorem ipsum dolor sit amet,
6340 * consectetur adipiscing elit.
6341 */
6342 /*
6343 *ˇ Lorem ipsum dolor sit amet,
6344 * consectetur adipiscing elit.
6345 */
6346 "},
6347 rust_lang.clone(),
6348 &mut cx,
6349 );
6350
6351 // multiline block comment with inline start/end delimiters
6352 assert_rewrap(
6353 indoc! {"
6354 /*ˇ Lorem ipsum dolor sit amet,
6355 * consectetur adipiscing elit. */
6356 "},
6357 indoc! {"
6358 /*
6359 *ˇ Lorem ipsum dolor sit amet,
6360 * consectetur adipiscing elit.
6361 */
6362 "},
6363 rust_lang.clone(),
6364 &mut cx,
6365 );
6366
6367 // block comment rewrap still respects paragraph bounds
6368 assert_rewrap(
6369 indoc! {"
6370 /*
6371 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6372 *
6373 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6374 */
6375 "},
6376 indoc! {"
6377 /*
6378 *ˇ Lorem ipsum dolor sit amet,
6379 * consectetur adipiscing elit.
6380 *
6381 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6382 */
6383 "},
6384 rust_lang.clone(),
6385 &mut cx,
6386 );
6387
6388 // documentation comments
6389 assert_rewrap(
6390 indoc! {"
6391 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6392 /**
6393 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6394 */
6395 "},
6396 indoc! {"
6397 /**
6398 *ˇ Lorem ipsum dolor sit amet,
6399 * consectetur adipiscing elit.
6400 */
6401 /**
6402 *ˇ Lorem ipsum dolor sit amet,
6403 * consectetur adipiscing elit.
6404 */
6405 "},
6406 rust_lang.clone(),
6407 &mut cx,
6408 );
6409
6410 // different, adjacent comments
6411 assert_rewrap(
6412 indoc! {"
6413 /**
6414 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6415 */
6416 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6417 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6418 "},
6419 indoc! {"
6420 /**
6421 *ˇ Lorem ipsum dolor sit amet,
6422 * consectetur adipiscing elit.
6423 */
6424 /*
6425 *ˇ Lorem ipsum dolor sit amet,
6426 * consectetur adipiscing elit.
6427 */
6428 //ˇ Lorem ipsum dolor sit amet,
6429 // consectetur adipiscing elit.
6430 "},
6431 rust_lang.clone(),
6432 &mut cx,
6433 );
6434
6435 // selection w/ single short block comment
6436 assert_rewrap(
6437 indoc! {"
6438 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6439 "},
6440 indoc! {"
6441 «/*
6442 * Lorem ipsum dolor sit amet,
6443 * consectetur adipiscing elit.
6444 */ˇ»
6445 "},
6446 rust_lang.clone(),
6447 &mut cx,
6448 );
6449
6450 // rewrapping a single comment w/ abutting comments
6451 assert_rewrap(
6452 indoc! {"
6453 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6454 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6455 "},
6456 indoc! {"
6457 /*
6458 * ˇLorem ipsum dolor sit amet,
6459 * consectetur adipiscing elit.
6460 */
6461 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6462 "},
6463 rust_lang.clone(),
6464 &mut cx,
6465 );
6466
6467 // selection w/ non-abutting short block comments
6468 assert_rewrap(
6469 indoc! {"
6470 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6471
6472 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6473 "},
6474 indoc! {"
6475 «/*
6476 * Lorem ipsum dolor sit amet,
6477 * consectetur adipiscing elit.
6478 */
6479
6480 /*
6481 * Lorem ipsum dolor sit amet,
6482 * consectetur adipiscing elit.
6483 */ˇ»
6484 "},
6485 rust_lang.clone(),
6486 &mut cx,
6487 );
6488
6489 // selection of multiline block comments
6490 assert_rewrap(
6491 indoc! {"
6492 «/* Lorem ipsum dolor sit amet,
6493 * consectetur adipiscing elit. */ˇ»
6494 "},
6495 indoc! {"
6496 «/*
6497 * Lorem ipsum dolor sit amet,
6498 * consectetur adipiscing elit.
6499 */ˇ»
6500 "},
6501 rust_lang.clone(),
6502 &mut cx,
6503 );
6504
6505 // partial selection of multiline block comments
6506 assert_rewrap(
6507 indoc! {"
6508 «/* Lorem ipsum dolor sit amet,ˇ»
6509 * consectetur adipiscing elit. */
6510 /* Lorem ipsum dolor sit amet,
6511 «* consectetur adipiscing elit. */ˇ»
6512 "},
6513 indoc! {"
6514 «/*
6515 * Lorem ipsum dolor sit amet,ˇ»
6516 * consectetur adipiscing elit. */
6517 /* Lorem ipsum dolor sit amet,
6518 «* consectetur adipiscing elit.
6519 */ˇ»
6520 "},
6521 rust_lang.clone(),
6522 &mut cx,
6523 );
6524
6525 // selection w/ abutting short block comments
6526 // TODO: should not be combined; should rewrap as 2 comments
6527 assert_rewrap(
6528 indoc! {"
6529 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6530 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6531 "},
6532 // desired behavior:
6533 // indoc! {"
6534 // «/*
6535 // * Lorem ipsum dolor sit amet,
6536 // * consectetur adipiscing elit.
6537 // */
6538 // /*
6539 // * Lorem ipsum dolor sit amet,
6540 // * consectetur adipiscing elit.
6541 // */ˇ»
6542 // "},
6543 // actual behaviour:
6544 indoc! {"
6545 «/*
6546 * Lorem ipsum dolor sit amet,
6547 * consectetur adipiscing elit. Lorem
6548 * ipsum dolor sit amet, consectetur
6549 * adipiscing elit.
6550 */ˇ»
6551 "},
6552 rust_lang.clone(),
6553 &mut cx,
6554 );
6555
6556 // TODO: same as above, but with delimiters on separate line
6557 // assert_rewrap(
6558 // indoc! {"
6559 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6560 // */
6561 // /*
6562 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6563 // "},
6564 // // desired:
6565 // // indoc! {"
6566 // // «/*
6567 // // * Lorem ipsum dolor sit amet,
6568 // // * consectetur adipiscing elit.
6569 // // */
6570 // // /*
6571 // // * Lorem ipsum dolor sit amet,
6572 // // * consectetur adipiscing elit.
6573 // // */ˇ»
6574 // // "},
6575 // // actual: (but with trailing w/s on the empty lines)
6576 // indoc! {"
6577 // «/*
6578 // * Lorem ipsum dolor sit amet,
6579 // * consectetur adipiscing elit.
6580 // *
6581 // */
6582 // /*
6583 // *
6584 // * Lorem ipsum dolor sit amet,
6585 // * consectetur adipiscing elit.
6586 // */ˇ»
6587 // "},
6588 // rust_lang.clone(),
6589 // &mut cx,
6590 // );
6591
6592 // TODO these are unhandled edge cases; not correct, just documenting known issues
6593 assert_rewrap(
6594 indoc! {"
6595 /*
6596 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6597 */
6598 /*
6599 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6600 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6601 "},
6602 // desired:
6603 // indoc! {"
6604 // /*
6605 // *ˇ Lorem ipsum dolor sit amet,
6606 // * consectetur adipiscing elit.
6607 // */
6608 // /*
6609 // *ˇ Lorem ipsum dolor sit amet,
6610 // * consectetur adipiscing elit.
6611 // */
6612 // /*
6613 // *ˇ Lorem ipsum dolor sit amet
6614 // */ /* consectetur adipiscing elit. */
6615 // "},
6616 // actual:
6617 indoc! {"
6618 /*
6619 //ˇ Lorem ipsum dolor sit amet,
6620 // consectetur adipiscing elit.
6621 */
6622 /*
6623 * //ˇ Lorem ipsum dolor sit amet,
6624 * consectetur adipiscing elit.
6625 */
6626 /*
6627 *ˇ Lorem ipsum dolor sit amet */ /*
6628 * consectetur adipiscing elit.
6629 */
6630 "},
6631 rust_lang,
6632 &mut cx,
6633 );
6634
6635 #[track_caller]
6636 fn assert_rewrap(
6637 unwrapped_text: &str,
6638 wrapped_text: &str,
6639 language: Arc<Language>,
6640 cx: &mut EditorTestContext,
6641 ) {
6642 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6643 cx.set_state(unwrapped_text);
6644 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6645 cx.assert_editor_state(wrapped_text);
6646 }
6647}
6648
6649#[gpui::test]
6650async fn test_hard_wrap(cx: &mut TestAppContext) {
6651 init_test(cx, |_| {});
6652 let mut cx = EditorTestContext::new(cx).await;
6653
6654 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6655 cx.update_editor(|editor, _, cx| {
6656 editor.set_hard_wrap(Some(14), cx);
6657 });
6658
6659 cx.set_state(indoc!(
6660 "
6661 one two three ˇ
6662 "
6663 ));
6664 cx.simulate_input("four");
6665 cx.run_until_parked();
6666
6667 cx.assert_editor_state(indoc!(
6668 "
6669 one two three
6670 fourˇ
6671 "
6672 ));
6673
6674 cx.update_editor(|editor, window, cx| {
6675 editor.newline(&Default::default(), window, cx);
6676 });
6677 cx.run_until_parked();
6678 cx.assert_editor_state(indoc!(
6679 "
6680 one two three
6681 four
6682 ˇ
6683 "
6684 ));
6685
6686 cx.simulate_input("five");
6687 cx.run_until_parked();
6688 cx.assert_editor_state(indoc!(
6689 "
6690 one two three
6691 four
6692 fiveˇ
6693 "
6694 ));
6695
6696 cx.update_editor(|editor, window, cx| {
6697 editor.newline(&Default::default(), window, cx);
6698 });
6699 cx.run_until_parked();
6700 cx.simulate_input("# ");
6701 cx.run_until_parked();
6702 cx.assert_editor_state(indoc!(
6703 "
6704 one two three
6705 four
6706 five
6707 # ˇ
6708 "
6709 ));
6710
6711 cx.update_editor(|editor, window, cx| {
6712 editor.newline(&Default::default(), window, cx);
6713 });
6714 cx.run_until_parked();
6715 cx.assert_editor_state(indoc!(
6716 "
6717 one two three
6718 four
6719 five
6720 #\x20
6721 #ˇ
6722 "
6723 ));
6724
6725 cx.simulate_input(" 6");
6726 cx.run_until_parked();
6727 cx.assert_editor_state(indoc!(
6728 "
6729 one two three
6730 four
6731 five
6732 #
6733 # 6ˇ
6734 "
6735 ));
6736}
6737
6738#[gpui::test]
6739async fn test_cut_line_ends(cx: &mut TestAppContext) {
6740 init_test(cx, |_| {});
6741
6742 let mut cx = EditorTestContext::new(cx).await;
6743
6744 cx.set_state(indoc! {"
6745 The quick« brownˇ»
6746 fox jumps overˇ
6747 the lazy dog"});
6748 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6749 cx.assert_editor_state(indoc! {"
6750 The quickˇ
6751 ˇthe lazy dog"});
6752
6753 cx.set_state(indoc! {"
6754 The quick« brownˇ»
6755 fox jumps overˇ
6756 the lazy dog"});
6757 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6758 cx.assert_editor_state(indoc! {"
6759 The quickˇ
6760 fox jumps overˇthe lazy dog"});
6761
6762 cx.set_state(indoc! {"
6763 The quick« brownˇ»
6764 fox jumps overˇ
6765 the lazy dog"});
6766 cx.update_editor(|e, window, cx| {
6767 e.cut_to_end_of_line(
6768 &CutToEndOfLine {
6769 stop_at_newlines: true,
6770 },
6771 window,
6772 cx,
6773 )
6774 });
6775 cx.assert_editor_state(indoc! {"
6776 The quickˇ
6777 fox jumps overˇ
6778 the lazy dog"});
6779
6780 cx.set_state(indoc! {"
6781 The quick« brownˇ»
6782 fox jumps overˇ
6783 the lazy dog"});
6784 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6785 cx.assert_editor_state(indoc! {"
6786 The quickˇ
6787 fox jumps overˇthe lazy dog"});
6788}
6789
6790#[gpui::test]
6791async fn test_clipboard(cx: &mut TestAppContext) {
6792 init_test(cx, |_| {});
6793
6794 let mut cx = EditorTestContext::new(cx).await;
6795
6796 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6797 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6798 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6799
6800 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6801 cx.set_state("two ˇfour ˇsix ˇ");
6802 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6803 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6804
6805 // Paste again but with only two cursors. Since the number of cursors doesn't
6806 // match the number of slices in the clipboard, the entire clipboard text
6807 // is pasted at each cursor.
6808 cx.set_state("ˇtwo one✅ four three six five ˇ");
6809 cx.update_editor(|e, window, cx| {
6810 e.handle_input("( ", window, cx);
6811 e.paste(&Paste, window, cx);
6812 e.handle_input(") ", window, cx);
6813 });
6814 cx.assert_editor_state(
6815 &([
6816 "( one✅ ",
6817 "three ",
6818 "five ) ˇtwo one✅ four three six five ( one✅ ",
6819 "three ",
6820 "five ) ˇ",
6821 ]
6822 .join("\n")),
6823 );
6824
6825 // Cut with three selections, one of which is full-line.
6826 cx.set_state(indoc! {"
6827 1«2ˇ»3
6828 4ˇ567
6829 «8ˇ»9"});
6830 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6831 cx.assert_editor_state(indoc! {"
6832 1ˇ3
6833 ˇ9"});
6834
6835 // Paste with three selections, noticing how the copied selection that was full-line
6836 // gets inserted before the second cursor.
6837 cx.set_state(indoc! {"
6838 1ˇ3
6839 9ˇ
6840 «oˇ»ne"});
6841 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6842 cx.assert_editor_state(indoc! {"
6843 12ˇ3
6844 4567
6845 9ˇ
6846 8ˇne"});
6847
6848 // Copy with a single cursor only, which writes the whole line into the clipboard.
6849 cx.set_state(indoc! {"
6850 The quick brown
6851 fox juˇmps over
6852 the lazy dog"});
6853 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6854 assert_eq!(
6855 cx.read_from_clipboard()
6856 .and_then(|item| item.text().as_deref().map(str::to_string)),
6857 Some("fox jumps over\n".to_string())
6858 );
6859
6860 // Paste with three selections, noticing how the copied full-line selection is inserted
6861 // before the empty selections but replaces the selection that is non-empty.
6862 cx.set_state(indoc! {"
6863 Tˇhe quick brown
6864 «foˇ»x jumps over
6865 tˇhe lazy dog"});
6866 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6867 cx.assert_editor_state(indoc! {"
6868 fox jumps over
6869 Tˇhe quick brown
6870 fox jumps over
6871 ˇx jumps over
6872 fox jumps over
6873 tˇhe lazy dog"});
6874}
6875
6876#[gpui::test]
6877async fn test_copy_trim(cx: &mut TestAppContext) {
6878 init_test(cx, |_| {});
6879
6880 let mut cx = EditorTestContext::new(cx).await;
6881 cx.set_state(
6882 r#" «for selection in selections.iter() {
6883 let mut start = selection.start;
6884 let mut end = selection.end;
6885 let is_entire_line = selection.is_empty();
6886 if is_entire_line {
6887 start = Point::new(start.row, 0);ˇ»
6888 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6889 }
6890 "#,
6891 );
6892 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6893 assert_eq!(
6894 cx.read_from_clipboard()
6895 .and_then(|item| item.text().as_deref().map(str::to_string)),
6896 Some(
6897 "for selection in selections.iter() {
6898 let mut start = selection.start;
6899 let mut end = selection.end;
6900 let is_entire_line = selection.is_empty();
6901 if is_entire_line {
6902 start = Point::new(start.row, 0);"
6903 .to_string()
6904 ),
6905 "Regular copying preserves all indentation selected",
6906 );
6907 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6908 assert_eq!(
6909 cx.read_from_clipboard()
6910 .and_then(|item| item.text().as_deref().map(str::to_string)),
6911 Some(
6912 "for selection in selections.iter() {
6913let mut start = selection.start;
6914let mut end = selection.end;
6915let is_entire_line = selection.is_empty();
6916if is_entire_line {
6917 start = Point::new(start.row, 0);"
6918 .to_string()
6919 ),
6920 "Copying with stripping should strip all leading whitespaces"
6921 );
6922
6923 cx.set_state(
6924 r#" « for selection in selections.iter() {
6925 let mut start = selection.start;
6926 let mut end = selection.end;
6927 let is_entire_line = selection.is_empty();
6928 if is_entire_line {
6929 start = Point::new(start.row, 0);ˇ»
6930 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6931 }
6932 "#,
6933 );
6934 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6935 assert_eq!(
6936 cx.read_from_clipboard()
6937 .and_then(|item| item.text().as_deref().map(str::to_string)),
6938 Some(
6939 " for selection in selections.iter() {
6940 let mut start = selection.start;
6941 let mut end = selection.end;
6942 let is_entire_line = selection.is_empty();
6943 if is_entire_line {
6944 start = Point::new(start.row, 0);"
6945 .to_string()
6946 ),
6947 "Regular copying preserves all indentation selected",
6948 );
6949 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6950 assert_eq!(
6951 cx.read_from_clipboard()
6952 .and_then(|item| item.text().as_deref().map(str::to_string)),
6953 Some(
6954 "for selection in selections.iter() {
6955let mut start = selection.start;
6956let mut end = selection.end;
6957let is_entire_line = selection.is_empty();
6958if is_entire_line {
6959 start = Point::new(start.row, 0);"
6960 .to_string()
6961 ),
6962 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6963 );
6964
6965 cx.set_state(
6966 r#" «ˇ for selection in selections.iter() {
6967 let mut start = selection.start;
6968 let mut end = selection.end;
6969 let is_entire_line = selection.is_empty();
6970 if is_entire_line {
6971 start = Point::new(start.row, 0);»
6972 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6973 }
6974 "#,
6975 );
6976 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6977 assert_eq!(
6978 cx.read_from_clipboard()
6979 .and_then(|item| item.text().as_deref().map(str::to_string)),
6980 Some(
6981 " for selection in selections.iter() {
6982 let mut start = selection.start;
6983 let mut end = selection.end;
6984 let is_entire_line = selection.is_empty();
6985 if is_entire_line {
6986 start = Point::new(start.row, 0);"
6987 .to_string()
6988 ),
6989 "Regular copying for reverse selection works the same",
6990 );
6991 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6992 assert_eq!(
6993 cx.read_from_clipboard()
6994 .and_then(|item| item.text().as_deref().map(str::to_string)),
6995 Some(
6996 "for selection in selections.iter() {
6997let mut start = selection.start;
6998let mut end = selection.end;
6999let is_entire_line = selection.is_empty();
7000if is_entire_line {
7001 start = Point::new(start.row, 0);"
7002 .to_string()
7003 ),
7004 "Copying with stripping for reverse selection works the same"
7005 );
7006
7007 cx.set_state(
7008 r#" for selection «in selections.iter() {
7009 let mut start = selection.start;
7010 let mut end = selection.end;
7011 let is_entire_line = selection.is_empty();
7012 if is_entire_line {
7013 start = Point::new(start.row, 0);ˇ»
7014 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7015 }
7016 "#,
7017 );
7018 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7019 assert_eq!(
7020 cx.read_from_clipboard()
7021 .and_then(|item| item.text().as_deref().map(str::to_string)),
7022 Some(
7023 "in selections.iter() {
7024 let mut start = selection.start;
7025 let mut end = selection.end;
7026 let is_entire_line = selection.is_empty();
7027 if is_entire_line {
7028 start = Point::new(start.row, 0);"
7029 .to_string()
7030 ),
7031 "When selecting past the indent, the copying works as usual",
7032 );
7033 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7034 assert_eq!(
7035 cx.read_from_clipboard()
7036 .and_then(|item| item.text().as_deref().map(str::to_string)),
7037 Some(
7038 "in selections.iter() {
7039 let mut start = selection.start;
7040 let mut end = selection.end;
7041 let is_entire_line = selection.is_empty();
7042 if is_entire_line {
7043 start = Point::new(start.row, 0);"
7044 .to_string()
7045 ),
7046 "When selecting past the indent, nothing is trimmed"
7047 );
7048
7049 cx.set_state(
7050 r#" «for selection in selections.iter() {
7051 let mut start = selection.start;
7052
7053 let mut end = selection.end;
7054 let is_entire_line = selection.is_empty();
7055 if is_entire_line {
7056 start = Point::new(start.row, 0);
7057ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7058 }
7059 "#,
7060 );
7061 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7062 assert_eq!(
7063 cx.read_from_clipboard()
7064 .and_then(|item| item.text().as_deref().map(str::to_string)),
7065 Some(
7066 "for selection in selections.iter() {
7067let mut start = selection.start;
7068
7069let mut end = selection.end;
7070let is_entire_line = selection.is_empty();
7071if is_entire_line {
7072 start = Point::new(start.row, 0);
7073"
7074 .to_string()
7075 ),
7076 "Copying with stripping should ignore empty lines"
7077 );
7078}
7079
7080#[gpui::test]
7081async fn test_paste_multiline(cx: &mut TestAppContext) {
7082 init_test(cx, |_| {});
7083
7084 let mut cx = EditorTestContext::new(cx).await;
7085 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7086
7087 // Cut an indented block, without the leading whitespace.
7088 cx.set_state(indoc! {"
7089 const a: B = (
7090 c(),
7091 «d(
7092 e,
7093 f
7094 )ˇ»
7095 );
7096 "});
7097 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7098 cx.assert_editor_state(indoc! {"
7099 const a: B = (
7100 c(),
7101 ˇ
7102 );
7103 "});
7104
7105 // Paste it at the same position.
7106 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7107 cx.assert_editor_state(indoc! {"
7108 const a: B = (
7109 c(),
7110 d(
7111 e,
7112 f
7113 )ˇ
7114 );
7115 "});
7116
7117 // Paste it at a line with a lower indent level.
7118 cx.set_state(indoc! {"
7119 ˇ
7120 const a: B = (
7121 c(),
7122 );
7123 "});
7124 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7125 cx.assert_editor_state(indoc! {"
7126 d(
7127 e,
7128 f
7129 )ˇ
7130 const a: B = (
7131 c(),
7132 );
7133 "});
7134
7135 // Cut an indented block, with the leading whitespace.
7136 cx.set_state(indoc! {"
7137 const a: B = (
7138 c(),
7139 « d(
7140 e,
7141 f
7142 )
7143 ˇ»);
7144 "});
7145 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7146 cx.assert_editor_state(indoc! {"
7147 const a: B = (
7148 c(),
7149 ˇ);
7150 "});
7151
7152 // Paste it at the same position.
7153 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7154 cx.assert_editor_state(indoc! {"
7155 const a: B = (
7156 c(),
7157 d(
7158 e,
7159 f
7160 )
7161 ˇ);
7162 "});
7163
7164 // Paste it at a line with a higher indent level.
7165 cx.set_state(indoc! {"
7166 const a: B = (
7167 c(),
7168 d(
7169 e,
7170 fˇ
7171 )
7172 );
7173 "});
7174 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7175 cx.assert_editor_state(indoc! {"
7176 const a: B = (
7177 c(),
7178 d(
7179 e,
7180 f d(
7181 e,
7182 f
7183 )
7184 ˇ
7185 )
7186 );
7187 "});
7188
7189 // Copy an indented block, starting mid-line
7190 cx.set_state(indoc! {"
7191 const a: B = (
7192 c(),
7193 somethin«g(
7194 e,
7195 f
7196 )ˇ»
7197 );
7198 "});
7199 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7200
7201 // Paste it on a line with a lower indent level
7202 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7203 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7204 cx.assert_editor_state(indoc! {"
7205 const a: B = (
7206 c(),
7207 something(
7208 e,
7209 f
7210 )
7211 );
7212 g(
7213 e,
7214 f
7215 )ˇ"});
7216}
7217
7218#[gpui::test]
7219async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7220 init_test(cx, |_| {});
7221
7222 cx.write_to_clipboard(ClipboardItem::new_string(
7223 " d(\n e\n );\n".into(),
7224 ));
7225
7226 let mut cx = EditorTestContext::new(cx).await;
7227 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7228
7229 cx.set_state(indoc! {"
7230 fn a() {
7231 b();
7232 if c() {
7233 ˇ
7234 }
7235 }
7236 "});
7237
7238 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7239 cx.assert_editor_state(indoc! {"
7240 fn a() {
7241 b();
7242 if c() {
7243 d(
7244 e
7245 );
7246 ˇ
7247 }
7248 }
7249 "});
7250
7251 cx.set_state(indoc! {"
7252 fn a() {
7253 b();
7254 ˇ
7255 }
7256 "});
7257
7258 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7259 cx.assert_editor_state(indoc! {"
7260 fn a() {
7261 b();
7262 d(
7263 e
7264 );
7265 ˇ
7266 }
7267 "});
7268}
7269
7270#[gpui::test]
7271fn test_select_all(cx: &mut TestAppContext) {
7272 init_test(cx, |_| {});
7273
7274 let editor = cx.add_window(|window, cx| {
7275 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7276 build_editor(buffer, window, cx)
7277 });
7278 _ = editor.update(cx, |editor, window, cx| {
7279 editor.select_all(&SelectAll, window, cx);
7280 assert_eq!(
7281 editor.selections.display_ranges(cx),
7282 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7283 );
7284 });
7285}
7286
7287#[gpui::test]
7288fn test_select_line(cx: &mut TestAppContext) {
7289 init_test(cx, |_| {});
7290
7291 let editor = cx.add_window(|window, cx| {
7292 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7293 build_editor(buffer, window, cx)
7294 });
7295 _ = editor.update(cx, |editor, window, cx| {
7296 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7297 s.select_display_ranges([
7298 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7299 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7300 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7301 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7302 ])
7303 });
7304 editor.select_line(&SelectLine, window, cx);
7305 assert_eq!(
7306 editor.selections.display_ranges(cx),
7307 vec![
7308 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7309 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7310 ]
7311 );
7312 });
7313
7314 _ = editor.update(cx, |editor, window, cx| {
7315 editor.select_line(&SelectLine, window, cx);
7316 assert_eq!(
7317 editor.selections.display_ranges(cx),
7318 vec![
7319 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7320 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7321 ]
7322 );
7323 });
7324
7325 _ = editor.update(cx, |editor, window, cx| {
7326 editor.select_line(&SelectLine, window, cx);
7327 assert_eq!(
7328 editor.selections.display_ranges(cx),
7329 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7330 );
7331 });
7332}
7333
7334#[gpui::test]
7335async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7336 init_test(cx, |_| {});
7337 let mut cx = EditorTestContext::new(cx).await;
7338
7339 #[track_caller]
7340 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7341 cx.set_state(initial_state);
7342 cx.update_editor(|e, window, cx| {
7343 e.split_selection_into_lines(&Default::default(), window, cx)
7344 });
7345 cx.assert_editor_state(expected_state);
7346 }
7347
7348 // Selection starts and ends at the middle of lines, left-to-right
7349 test(
7350 &mut cx,
7351 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7352 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7353 );
7354 // Same thing, right-to-left
7355 test(
7356 &mut cx,
7357 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7358 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7359 );
7360
7361 // Whole buffer, left-to-right, last line *doesn't* end with newline
7362 test(
7363 &mut cx,
7364 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7365 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7366 );
7367 // Same thing, right-to-left
7368 test(
7369 &mut cx,
7370 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7371 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7372 );
7373
7374 // Whole buffer, left-to-right, last line ends with newline
7375 test(
7376 &mut cx,
7377 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7378 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7379 );
7380 // Same thing, right-to-left
7381 test(
7382 &mut cx,
7383 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7384 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7385 );
7386
7387 // Starts at the end of a line, ends at the start of another
7388 test(
7389 &mut cx,
7390 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7391 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7392 );
7393}
7394
7395#[gpui::test]
7396async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7397 init_test(cx, |_| {});
7398
7399 let editor = cx.add_window(|window, cx| {
7400 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7401 build_editor(buffer, window, cx)
7402 });
7403
7404 // setup
7405 _ = editor.update(cx, |editor, window, cx| {
7406 editor.fold_creases(
7407 vec![
7408 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7409 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7410 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7411 ],
7412 true,
7413 window,
7414 cx,
7415 );
7416 assert_eq!(
7417 editor.display_text(cx),
7418 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7419 );
7420 });
7421
7422 _ = editor.update(cx, |editor, window, cx| {
7423 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7424 s.select_display_ranges([
7425 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7426 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7427 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7428 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7429 ])
7430 });
7431 editor.split_selection_into_lines(&Default::default(), window, cx);
7432 assert_eq!(
7433 editor.display_text(cx),
7434 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7435 );
7436 });
7437 EditorTestContext::for_editor(editor, cx)
7438 .await
7439 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7440
7441 _ = editor.update(cx, |editor, window, cx| {
7442 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7443 s.select_display_ranges([
7444 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7445 ])
7446 });
7447 editor.split_selection_into_lines(&Default::default(), window, cx);
7448 assert_eq!(
7449 editor.display_text(cx),
7450 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7451 );
7452 assert_eq!(
7453 editor.selections.display_ranges(cx),
7454 [
7455 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7456 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7457 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7458 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7459 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7460 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7461 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7462 ]
7463 );
7464 });
7465 EditorTestContext::for_editor(editor, cx)
7466 .await
7467 .assert_editor_state(
7468 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7469 );
7470}
7471
7472#[gpui::test]
7473async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7474 init_test(cx, |_| {});
7475
7476 let mut cx = EditorTestContext::new(cx).await;
7477
7478 cx.set_state(indoc!(
7479 r#"abc
7480 defˇghi
7481
7482 jk
7483 nlmo
7484 "#
7485 ));
7486
7487 cx.update_editor(|editor, window, cx| {
7488 editor.add_selection_above(&Default::default(), window, cx);
7489 });
7490
7491 cx.assert_editor_state(indoc!(
7492 r#"abcˇ
7493 defˇghi
7494
7495 jk
7496 nlmo
7497 "#
7498 ));
7499
7500 cx.update_editor(|editor, window, cx| {
7501 editor.add_selection_above(&Default::default(), window, cx);
7502 });
7503
7504 cx.assert_editor_state(indoc!(
7505 r#"abcˇ
7506 defˇghi
7507
7508 jk
7509 nlmo
7510 "#
7511 ));
7512
7513 cx.update_editor(|editor, window, cx| {
7514 editor.add_selection_below(&Default::default(), window, cx);
7515 });
7516
7517 cx.assert_editor_state(indoc!(
7518 r#"abc
7519 defˇghi
7520
7521 jk
7522 nlmo
7523 "#
7524 ));
7525
7526 cx.update_editor(|editor, window, cx| {
7527 editor.undo_selection(&Default::default(), window, cx);
7528 });
7529
7530 cx.assert_editor_state(indoc!(
7531 r#"abcˇ
7532 defˇghi
7533
7534 jk
7535 nlmo
7536 "#
7537 ));
7538
7539 cx.update_editor(|editor, window, cx| {
7540 editor.redo_selection(&Default::default(), window, cx);
7541 });
7542
7543 cx.assert_editor_state(indoc!(
7544 r#"abc
7545 defˇghi
7546
7547 jk
7548 nlmo
7549 "#
7550 ));
7551
7552 cx.update_editor(|editor, window, cx| {
7553 editor.add_selection_below(&Default::default(), window, cx);
7554 });
7555
7556 cx.assert_editor_state(indoc!(
7557 r#"abc
7558 defˇghi
7559 ˇ
7560 jk
7561 nlmo
7562 "#
7563 ));
7564
7565 cx.update_editor(|editor, window, cx| {
7566 editor.add_selection_below(&Default::default(), window, cx);
7567 });
7568
7569 cx.assert_editor_state(indoc!(
7570 r#"abc
7571 defˇghi
7572 ˇ
7573 jkˇ
7574 nlmo
7575 "#
7576 ));
7577
7578 cx.update_editor(|editor, window, cx| {
7579 editor.add_selection_below(&Default::default(), window, cx);
7580 });
7581
7582 cx.assert_editor_state(indoc!(
7583 r#"abc
7584 defˇghi
7585 ˇ
7586 jkˇ
7587 nlmˇo
7588 "#
7589 ));
7590
7591 cx.update_editor(|editor, window, cx| {
7592 editor.add_selection_below(&Default::default(), window, cx);
7593 });
7594
7595 cx.assert_editor_state(indoc!(
7596 r#"abc
7597 defˇghi
7598 ˇ
7599 jkˇ
7600 nlmˇo
7601 ˇ"#
7602 ));
7603
7604 // change selections
7605 cx.set_state(indoc!(
7606 r#"abc
7607 def«ˇg»hi
7608
7609 jk
7610 nlmo
7611 "#
7612 ));
7613
7614 cx.update_editor(|editor, window, cx| {
7615 editor.add_selection_below(&Default::default(), window, cx);
7616 });
7617
7618 cx.assert_editor_state(indoc!(
7619 r#"abc
7620 def«ˇg»hi
7621
7622 jk
7623 nlm«ˇo»
7624 "#
7625 ));
7626
7627 cx.update_editor(|editor, window, cx| {
7628 editor.add_selection_below(&Default::default(), window, cx);
7629 });
7630
7631 cx.assert_editor_state(indoc!(
7632 r#"abc
7633 def«ˇg»hi
7634
7635 jk
7636 nlm«ˇo»
7637 "#
7638 ));
7639
7640 cx.update_editor(|editor, window, cx| {
7641 editor.add_selection_above(&Default::default(), window, cx);
7642 });
7643
7644 cx.assert_editor_state(indoc!(
7645 r#"abc
7646 def«ˇg»hi
7647
7648 jk
7649 nlmo
7650 "#
7651 ));
7652
7653 cx.update_editor(|editor, window, cx| {
7654 editor.add_selection_above(&Default::default(), window, cx);
7655 });
7656
7657 cx.assert_editor_state(indoc!(
7658 r#"abc
7659 def«ˇg»hi
7660
7661 jk
7662 nlmo
7663 "#
7664 ));
7665
7666 // Change selections again
7667 cx.set_state(indoc!(
7668 r#"a«bc
7669 defgˇ»hi
7670
7671 jk
7672 nlmo
7673 "#
7674 ));
7675
7676 cx.update_editor(|editor, window, cx| {
7677 editor.add_selection_below(&Default::default(), window, cx);
7678 });
7679
7680 cx.assert_editor_state(indoc!(
7681 r#"a«bcˇ»
7682 d«efgˇ»hi
7683
7684 j«kˇ»
7685 nlmo
7686 "#
7687 ));
7688
7689 cx.update_editor(|editor, window, cx| {
7690 editor.add_selection_below(&Default::default(), window, cx);
7691 });
7692 cx.assert_editor_state(indoc!(
7693 r#"a«bcˇ»
7694 d«efgˇ»hi
7695
7696 j«kˇ»
7697 n«lmoˇ»
7698 "#
7699 ));
7700 cx.update_editor(|editor, window, cx| {
7701 editor.add_selection_above(&Default::default(), window, cx);
7702 });
7703
7704 cx.assert_editor_state(indoc!(
7705 r#"a«bcˇ»
7706 d«efgˇ»hi
7707
7708 j«kˇ»
7709 nlmo
7710 "#
7711 ));
7712
7713 // Change selections again
7714 cx.set_state(indoc!(
7715 r#"abc
7716 d«ˇefghi
7717
7718 jk
7719 nlm»o
7720 "#
7721 ));
7722
7723 cx.update_editor(|editor, window, cx| {
7724 editor.add_selection_above(&Default::default(), window, cx);
7725 });
7726
7727 cx.assert_editor_state(indoc!(
7728 r#"a«ˇbc»
7729 d«ˇef»ghi
7730
7731 j«ˇk»
7732 n«ˇlm»o
7733 "#
7734 ));
7735
7736 cx.update_editor(|editor, window, cx| {
7737 editor.add_selection_below(&Default::default(), window, cx);
7738 });
7739
7740 cx.assert_editor_state(indoc!(
7741 r#"abc
7742 d«ˇef»ghi
7743
7744 j«ˇk»
7745 n«ˇlm»o
7746 "#
7747 ));
7748}
7749
7750#[gpui::test]
7751async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7752 init_test(cx, |_| {});
7753 let mut cx = EditorTestContext::new(cx).await;
7754
7755 cx.set_state(indoc!(
7756 r#"line onˇe
7757 liˇne two
7758 line three
7759 line four"#
7760 ));
7761
7762 cx.update_editor(|editor, window, cx| {
7763 editor.add_selection_below(&Default::default(), window, cx);
7764 });
7765
7766 // test multiple cursors expand in the same direction
7767 cx.assert_editor_state(indoc!(
7768 r#"line onˇe
7769 liˇne twˇo
7770 liˇne three
7771 line four"#
7772 ));
7773
7774 cx.update_editor(|editor, window, cx| {
7775 editor.add_selection_below(&Default::default(), window, cx);
7776 });
7777
7778 cx.update_editor(|editor, window, cx| {
7779 editor.add_selection_below(&Default::default(), window, cx);
7780 });
7781
7782 // test multiple cursors expand below overflow
7783 cx.assert_editor_state(indoc!(
7784 r#"line onˇe
7785 liˇne twˇo
7786 liˇne thˇree
7787 liˇne foˇur"#
7788 ));
7789
7790 cx.update_editor(|editor, window, cx| {
7791 editor.add_selection_above(&Default::default(), window, cx);
7792 });
7793
7794 // test multiple cursors retrieves back correctly
7795 cx.assert_editor_state(indoc!(
7796 r#"line onˇe
7797 liˇne twˇo
7798 liˇne thˇree
7799 line four"#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.add_selection_above(&Default::default(), window, cx);
7804 });
7805
7806 cx.update_editor(|editor, window, cx| {
7807 editor.add_selection_above(&Default::default(), window, cx);
7808 });
7809
7810 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7811 cx.assert_editor_state(indoc!(
7812 r#"liˇne onˇe
7813 liˇne two
7814 line three
7815 line four"#
7816 ));
7817
7818 cx.update_editor(|editor, window, cx| {
7819 editor.undo_selection(&Default::default(), window, cx);
7820 });
7821
7822 // test undo
7823 cx.assert_editor_state(indoc!(
7824 r#"line onˇe
7825 liˇne twˇo
7826 line three
7827 line four"#
7828 ));
7829
7830 cx.update_editor(|editor, window, cx| {
7831 editor.redo_selection(&Default::default(), window, cx);
7832 });
7833
7834 // test redo
7835 cx.assert_editor_state(indoc!(
7836 r#"liˇne onˇe
7837 liˇne two
7838 line three
7839 line four"#
7840 ));
7841
7842 cx.set_state(indoc!(
7843 r#"abcd
7844 ef«ghˇ»
7845 ijkl
7846 «mˇ»nop"#
7847 ));
7848
7849 cx.update_editor(|editor, window, cx| {
7850 editor.add_selection_above(&Default::default(), window, cx);
7851 });
7852
7853 // test multiple selections expand in the same direction
7854 cx.assert_editor_state(indoc!(
7855 r#"ab«cdˇ»
7856 ef«ghˇ»
7857 «iˇ»jkl
7858 «mˇ»nop"#
7859 ));
7860
7861 cx.update_editor(|editor, window, cx| {
7862 editor.add_selection_above(&Default::default(), window, cx);
7863 });
7864
7865 // test multiple selection upward overflow
7866 cx.assert_editor_state(indoc!(
7867 r#"ab«cdˇ»
7868 «eˇ»f«ghˇ»
7869 «iˇ»jkl
7870 «mˇ»nop"#
7871 ));
7872
7873 cx.update_editor(|editor, window, cx| {
7874 editor.add_selection_below(&Default::default(), window, cx);
7875 });
7876
7877 // test multiple selection retrieves back correctly
7878 cx.assert_editor_state(indoc!(
7879 r#"abcd
7880 ef«ghˇ»
7881 «iˇ»jkl
7882 «mˇ»nop"#
7883 ));
7884
7885 cx.update_editor(|editor, window, cx| {
7886 editor.add_selection_below(&Default::default(), window, cx);
7887 });
7888
7889 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7890 cx.assert_editor_state(indoc!(
7891 r#"abcd
7892 ef«ghˇ»
7893 ij«klˇ»
7894 «mˇ»nop"#
7895 ));
7896
7897 cx.update_editor(|editor, window, cx| {
7898 editor.undo_selection(&Default::default(), window, cx);
7899 });
7900
7901 // test undo
7902 cx.assert_editor_state(indoc!(
7903 r#"abcd
7904 ef«ghˇ»
7905 «iˇ»jkl
7906 «mˇ»nop"#
7907 ));
7908
7909 cx.update_editor(|editor, window, cx| {
7910 editor.redo_selection(&Default::default(), window, cx);
7911 });
7912
7913 // test redo
7914 cx.assert_editor_state(indoc!(
7915 r#"abcd
7916 ef«ghˇ»
7917 ij«klˇ»
7918 «mˇ»nop"#
7919 ));
7920}
7921
7922#[gpui::test]
7923async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7924 init_test(cx, |_| {});
7925 let mut cx = EditorTestContext::new(cx).await;
7926
7927 cx.set_state(indoc!(
7928 r#"line onˇe
7929 liˇne two
7930 line three
7931 line four"#
7932 ));
7933
7934 cx.update_editor(|editor, window, cx| {
7935 editor.add_selection_below(&Default::default(), window, cx);
7936 editor.add_selection_below(&Default::default(), window, cx);
7937 editor.add_selection_below(&Default::default(), window, cx);
7938 });
7939
7940 // initial state with two multi cursor groups
7941 cx.assert_editor_state(indoc!(
7942 r#"line onˇe
7943 liˇne twˇo
7944 liˇne thˇree
7945 liˇne foˇur"#
7946 ));
7947
7948 // add single cursor in middle - simulate opt click
7949 cx.update_editor(|editor, window, cx| {
7950 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7951 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7952 editor.end_selection(window, cx);
7953 });
7954
7955 cx.assert_editor_state(indoc!(
7956 r#"line onˇe
7957 liˇne twˇo
7958 liˇneˇ thˇree
7959 liˇne foˇur"#
7960 ));
7961
7962 cx.update_editor(|editor, window, cx| {
7963 editor.add_selection_above(&Default::default(), window, cx);
7964 });
7965
7966 // test new added selection expands above and existing selection shrinks
7967 cx.assert_editor_state(indoc!(
7968 r#"line onˇe
7969 liˇneˇ twˇo
7970 liˇneˇ thˇree
7971 line four"#
7972 ));
7973
7974 cx.update_editor(|editor, window, cx| {
7975 editor.add_selection_above(&Default::default(), window, cx);
7976 });
7977
7978 // test new added selection expands above and existing selection shrinks
7979 cx.assert_editor_state(indoc!(
7980 r#"lineˇ onˇe
7981 liˇneˇ twˇo
7982 lineˇ three
7983 line four"#
7984 ));
7985
7986 // intial state with two selection groups
7987 cx.set_state(indoc!(
7988 r#"abcd
7989 ef«ghˇ»
7990 ijkl
7991 «mˇ»nop"#
7992 ));
7993
7994 cx.update_editor(|editor, window, cx| {
7995 editor.add_selection_above(&Default::default(), window, cx);
7996 editor.add_selection_above(&Default::default(), window, cx);
7997 });
7998
7999 cx.assert_editor_state(indoc!(
8000 r#"ab«cdˇ»
8001 «eˇ»f«ghˇ»
8002 «iˇ»jkl
8003 «mˇ»nop"#
8004 ));
8005
8006 // add single selection in middle - simulate opt drag
8007 cx.update_editor(|editor, window, cx| {
8008 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8009 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8010 editor.update_selection(
8011 DisplayPoint::new(DisplayRow(2), 4),
8012 0,
8013 gpui::Point::<f32>::default(),
8014 window,
8015 cx,
8016 );
8017 editor.end_selection(window, cx);
8018 });
8019
8020 cx.assert_editor_state(indoc!(
8021 r#"ab«cdˇ»
8022 «eˇ»f«ghˇ»
8023 «iˇ»jk«lˇ»
8024 «mˇ»nop"#
8025 ));
8026
8027 cx.update_editor(|editor, window, cx| {
8028 editor.add_selection_below(&Default::default(), window, cx);
8029 });
8030
8031 // test new added selection expands below, others shrinks from above
8032 cx.assert_editor_state(indoc!(
8033 r#"abcd
8034 ef«ghˇ»
8035 «iˇ»jk«lˇ»
8036 «mˇ»no«pˇ»"#
8037 ));
8038}
8039
8040#[gpui::test]
8041async fn test_select_next(cx: &mut TestAppContext) {
8042 init_test(cx, |_| {});
8043
8044 let mut cx = EditorTestContext::new(cx).await;
8045 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8046
8047 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8048 .unwrap();
8049 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8050
8051 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8052 .unwrap();
8053 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8054
8055 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8056 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8057
8058 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8059 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8060
8061 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8062 .unwrap();
8063 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8064
8065 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8066 .unwrap();
8067 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8068
8069 // Test selection direction should be preserved
8070 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8071
8072 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8073 .unwrap();
8074 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8075}
8076
8077#[gpui::test]
8078async fn test_select_all_matches(cx: &mut TestAppContext) {
8079 init_test(cx, |_| {});
8080
8081 let mut cx = EditorTestContext::new(cx).await;
8082
8083 // Test caret-only selections
8084 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8085 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8086 .unwrap();
8087 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8088
8089 // Test left-to-right selections
8090 cx.set_state("abc\n«abcˇ»\nabc");
8091 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8092 .unwrap();
8093 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8094
8095 // Test right-to-left selections
8096 cx.set_state("abc\n«ˇabc»\nabc");
8097 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8098 .unwrap();
8099 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8100
8101 // Test selecting whitespace with caret selection
8102 cx.set_state("abc\nˇ abc\nabc");
8103 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8104 .unwrap();
8105 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8106
8107 // Test selecting whitespace with left-to-right selection
8108 cx.set_state("abc\n«ˇ »abc\nabc");
8109 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8110 .unwrap();
8111 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8112
8113 // Test no matches with right-to-left selection
8114 cx.set_state("abc\n« ˇ»abc\nabc");
8115 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8116 .unwrap();
8117 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8118
8119 // Test with a single word and clip_at_line_ends=true (#29823)
8120 cx.set_state("aˇbc");
8121 cx.update_editor(|e, window, cx| {
8122 e.set_clip_at_line_ends(true, cx);
8123 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8124 e.set_clip_at_line_ends(false, cx);
8125 });
8126 cx.assert_editor_state("«abcˇ»");
8127}
8128
8129#[gpui::test]
8130async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8131 init_test(cx, |_| {});
8132
8133 let mut cx = EditorTestContext::new(cx).await;
8134
8135 let large_body_1 = "\nd".repeat(200);
8136 let large_body_2 = "\ne".repeat(200);
8137
8138 cx.set_state(&format!(
8139 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8140 ));
8141 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8142 let scroll_position = editor.scroll_position(cx);
8143 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8144 scroll_position
8145 });
8146
8147 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8148 .unwrap();
8149 cx.assert_editor_state(&format!(
8150 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8151 ));
8152 let scroll_position_after_selection =
8153 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8154 assert_eq!(
8155 initial_scroll_position, scroll_position_after_selection,
8156 "Scroll position should not change after selecting all matches"
8157 );
8158}
8159
8160#[gpui::test]
8161async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8162 init_test(cx, |_| {});
8163
8164 let mut cx = EditorLspTestContext::new_rust(
8165 lsp::ServerCapabilities {
8166 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8167 ..Default::default()
8168 },
8169 cx,
8170 )
8171 .await;
8172
8173 cx.set_state(indoc! {"
8174 line 1
8175 line 2
8176 linˇe 3
8177 line 4
8178 line 5
8179 "});
8180
8181 // Make an edit
8182 cx.update_editor(|editor, window, cx| {
8183 editor.handle_input("X", window, cx);
8184 });
8185
8186 // Move cursor to a different position
8187 cx.update_editor(|editor, window, cx| {
8188 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8189 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8190 });
8191 });
8192
8193 cx.assert_editor_state(indoc! {"
8194 line 1
8195 line 2
8196 linXe 3
8197 line 4
8198 liˇne 5
8199 "});
8200
8201 cx.lsp
8202 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8203 Ok(Some(vec![lsp::TextEdit::new(
8204 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8205 "PREFIX ".to_string(),
8206 )]))
8207 });
8208
8209 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8210 .unwrap()
8211 .await
8212 .unwrap();
8213
8214 cx.assert_editor_state(indoc! {"
8215 PREFIX line 1
8216 line 2
8217 linXe 3
8218 line 4
8219 liˇne 5
8220 "});
8221
8222 // Undo formatting
8223 cx.update_editor(|editor, window, cx| {
8224 editor.undo(&Default::default(), window, cx);
8225 });
8226
8227 // Verify cursor moved back to position after edit
8228 cx.assert_editor_state(indoc! {"
8229 line 1
8230 line 2
8231 linXˇe 3
8232 line 4
8233 line 5
8234 "});
8235}
8236
8237#[gpui::test]
8238async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8239 init_test(cx, |_| {});
8240
8241 let mut cx = EditorTestContext::new(cx).await;
8242
8243 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8244 cx.update_editor(|editor, window, cx| {
8245 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8246 });
8247
8248 cx.set_state(indoc! {"
8249 line 1
8250 line 2
8251 linˇe 3
8252 line 4
8253 line 5
8254 line 6
8255 line 7
8256 line 8
8257 line 9
8258 line 10
8259 "});
8260
8261 let snapshot = cx.buffer_snapshot();
8262 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8263
8264 cx.update(|_, cx| {
8265 provider.update(cx, |provider, _| {
8266 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8267 id: None,
8268 edits: vec![(edit_position..edit_position, "X".into())],
8269 edit_preview: None,
8270 }))
8271 })
8272 });
8273
8274 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8275 cx.update_editor(|editor, window, cx| {
8276 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8277 });
8278
8279 cx.assert_editor_state(indoc! {"
8280 line 1
8281 line 2
8282 lineXˇ 3
8283 line 4
8284 line 5
8285 line 6
8286 line 7
8287 line 8
8288 line 9
8289 line 10
8290 "});
8291
8292 cx.update_editor(|editor, window, cx| {
8293 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8294 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8295 });
8296 });
8297
8298 cx.assert_editor_state(indoc! {"
8299 line 1
8300 line 2
8301 lineX 3
8302 line 4
8303 line 5
8304 line 6
8305 line 7
8306 line 8
8307 line 9
8308 liˇne 10
8309 "});
8310
8311 cx.update_editor(|editor, window, cx| {
8312 editor.undo(&Default::default(), window, cx);
8313 });
8314
8315 cx.assert_editor_state(indoc! {"
8316 line 1
8317 line 2
8318 lineˇ 3
8319 line 4
8320 line 5
8321 line 6
8322 line 7
8323 line 8
8324 line 9
8325 line 10
8326 "});
8327}
8328
8329#[gpui::test]
8330async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8331 init_test(cx, |_| {});
8332
8333 let mut cx = EditorTestContext::new(cx).await;
8334 cx.set_state(
8335 r#"let foo = 2;
8336lˇet foo = 2;
8337let fooˇ = 2;
8338let foo = 2;
8339let foo = ˇ2;"#,
8340 );
8341
8342 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8343 .unwrap();
8344 cx.assert_editor_state(
8345 r#"let foo = 2;
8346«letˇ» foo = 2;
8347let «fooˇ» = 2;
8348let foo = 2;
8349let foo = «2ˇ»;"#,
8350 );
8351
8352 // noop for multiple selections with different contents
8353 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8354 .unwrap();
8355 cx.assert_editor_state(
8356 r#"let foo = 2;
8357«letˇ» foo = 2;
8358let «fooˇ» = 2;
8359let foo = 2;
8360let foo = «2ˇ»;"#,
8361 );
8362
8363 // Test last selection direction should be preserved
8364 cx.set_state(
8365 r#"let foo = 2;
8366let foo = 2;
8367let «fooˇ» = 2;
8368let «ˇfoo» = 2;
8369let foo = 2;"#,
8370 );
8371
8372 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8373 .unwrap();
8374 cx.assert_editor_state(
8375 r#"let foo = 2;
8376let foo = 2;
8377let «fooˇ» = 2;
8378let «ˇfoo» = 2;
8379let «ˇfoo» = 2;"#,
8380 );
8381}
8382
8383#[gpui::test]
8384async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8385 init_test(cx, |_| {});
8386
8387 let mut cx =
8388 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8389
8390 cx.assert_editor_state(indoc! {"
8391 ˇbbb
8392 ccc
8393
8394 bbb
8395 ccc
8396 "});
8397 cx.dispatch_action(SelectPrevious::default());
8398 cx.assert_editor_state(indoc! {"
8399 «bbbˇ»
8400 ccc
8401
8402 bbb
8403 ccc
8404 "});
8405 cx.dispatch_action(SelectPrevious::default());
8406 cx.assert_editor_state(indoc! {"
8407 «bbbˇ»
8408 ccc
8409
8410 «bbbˇ»
8411 ccc
8412 "});
8413}
8414
8415#[gpui::test]
8416async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8417 init_test(cx, |_| {});
8418
8419 let mut cx = EditorTestContext::new(cx).await;
8420 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8421
8422 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8423 .unwrap();
8424 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8425
8426 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8427 .unwrap();
8428 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8429
8430 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8431 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8432
8433 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8434 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8435
8436 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8437 .unwrap();
8438 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8439
8440 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8441 .unwrap();
8442 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8443}
8444
8445#[gpui::test]
8446async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8447 init_test(cx, |_| {});
8448
8449 let mut cx = EditorTestContext::new(cx).await;
8450 cx.set_state("aˇ");
8451
8452 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8453 .unwrap();
8454 cx.assert_editor_state("«aˇ»");
8455 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8456 .unwrap();
8457 cx.assert_editor_state("«aˇ»");
8458}
8459
8460#[gpui::test]
8461async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8462 init_test(cx, |_| {});
8463
8464 let mut cx = EditorTestContext::new(cx).await;
8465 cx.set_state(
8466 r#"let foo = 2;
8467lˇet foo = 2;
8468let fooˇ = 2;
8469let foo = 2;
8470let foo = ˇ2;"#,
8471 );
8472
8473 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8474 .unwrap();
8475 cx.assert_editor_state(
8476 r#"let foo = 2;
8477«letˇ» foo = 2;
8478let «fooˇ» = 2;
8479let foo = 2;
8480let foo = «2ˇ»;"#,
8481 );
8482
8483 // noop for multiple selections with different contents
8484 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8485 .unwrap();
8486 cx.assert_editor_state(
8487 r#"let foo = 2;
8488«letˇ» foo = 2;
8489let «fooˇ» = 2;
8490let foo = 2;
8491let foo = «2ˇ»;"#,
8492 );
8493}
8494
8495#[gpui::test]
8496async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8497 init_test(cx, |_| {});
8498
8499 let mut cx = EditorTestContext::new(cx).await;
8500 cx.set_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 // selection direction is preserved
8505 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8506
8507 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8508 .unwrap();
8509 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8510
8511 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8512 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8513
8514 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8515 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8516
8517 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8518 .unwrap();
8519 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8520
8521 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8522 .unwrap();
8523 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8524}
8525
8526#[gpui::test]
8527async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8528 init_test(cx, |_| {});
8529
8530 let language = Arc::new(Language::new(
8531 LanguageConfig::default(),
8532 Some(tree_sitter_rust::LANGUAGE.into()),
8533 ));
8534
8535 let text = r#"
8536 use mod1::mod2::{mod3, mod4};
8537
8538 fn fn_1(param1: bool, param2: &str) {
8539 let var1 = "text";
8540 }
8541 "#
8542 .unindent();
8543
8544 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8545 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8546 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8547
8548 editor
8549 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8550 .await;
8551
8552 editor.update_in(cx, |editor, window, cx| {
8553 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8554 s.select_display_ranges([
8555 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8556 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8557 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8558 ]);
8559 });
8560 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8561 });
8562 editor.update(cx, |editor, cx| {
8563 assert_text_with_selections(
8564 editor,
8565 indoc! {r#"
8566 use mod1::mod2::{mod3, «mod4ˇ»};
8567
8568 fn fn_1«ˇ(param1: bool, param2: &str)» {
8569 let var1 = "«ˇtext»";
8570 }
8571 "#},
8572 cx,
8573 );
8574 });
8575
8576 editor.update_in(cx, |editor, window, cx| {
8577 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8578 });
8579 editor.update(cx, |editor, cx| {
8580 assert_text_with_selections(
8581 editor,
8582 indoc! {r#"
8583 use mod1::mod2::«{mod3, mod4}ˇ»;
8584
8585 «ˇfn fn_1(param1: bool, param2: &str) {
8586 let var1 = "text";
8587 }»
8588 "#},
8589 cx,
8590 );
8591 });
8592
8593 editor.update_in(cx, |editor, window, cx| {
8594 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8595 });
8596 assert_eq!(
8597 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8598 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8599 );
8600
8601 // Trying to expand the selected syntax node one more time has no effect.
8602 editor.update_in(cx, |editor, window, cx| {
8603 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8604 });
8605 assert_eq!(
8606 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8607 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8608 );
8609
8610 editor.update_in(cx, |editor, window, cx| {
8611 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8612 });
8613 editor.update(cx, |editor, cx| {
8614 assert_text_with_selections(
8615 editor,
8616 indoc! {r#"
8617 use mod1::mod2::«{mod3, mod4}ˇ»;
8618
8619 «ˇfn fn_1(param1: bool, param2: &str) {
8620 let var1 = "text";
8621 }»
8622 "#},
8623 cx,
8624 );
8625 });
8626
8627 editor.update_in(cx, |editor, window, cx| {
8628 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8629 });
8630 editor.update(cx, |editor, cx| {
8631 assert_text_with_selections(
8632 editor,
8633 indoc! {r#"
8634 use mod1::mod2::{mod3, «mod4ˇ»};
8635
8636 fn fn_1«ˇ(param1: bool, param2: &str)» {
8637 let var1 = "«ˇtext»";
8638 }
8639 "#},
8640 cx,
8641 );
8642 });
8643
8644 editor.update_in(cx, |editor, window, cx| {
8645 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8646 });
8647 editor.update(cx, |editor, cx| {
8648 assert_text_with_selections(
8649 editor,
8650 indoc! {r#"
8651 use mod1::mod2::{mod3, moˇd4};
8652
8653 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8654 let var1 = "teˇxt";
8655 }
8656 "#},
8657 cx,
8658 );
8659 });
8660
8661 // Trying to shrink the selected syntax node one more time has no effect.
8662 editor.update_in(cx, |editor, window, cx| {
8663 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8664 });
8665 editor.update_in(cx, |editor, _, cx| {
8666 assert_text_with_selections(
8667 editor,
8668 indoc! {r#"
8669 use mod1::mod2::{mod3, moˇd4};
8670
8671 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8672 let var1 = "teˇxt";
8673 }
8674 "#},
8675 cx,
8676 );
8677 });
8678
8679 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8680 // a fold.
8681 editor.update_in(cx, |editor, window, cx| {
8682 editor.fold_creases(
8683 vec![
8684 Crease::simple(
8685 Point::new(0, 21)..Point::new(0, 24),
8686 FoldPlaceholder::test(),
8687 ),
8688 Crease::simple(
8689 Point::new(3, 20)..Point::new(3, 22),
8690 FoldPlaceholder::test(),
8691 ),
8692 ],
8693 true,
8694 window,
8695 cx,
8696 );
8697 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8698 });
8699 editor.update(cx, |editor, cx| {
8700 assert_text_with_selections(
8701 editor,
8702 indoc! {r#"
8703 use mod1::mod2::«{mod3, mod4}ˇ»;
8704
8705 fn fn_1«ˇ(param1: bool, param2: &str)» {
8706 let var1 = "«ˇtext»";
8707 }
8708 "#},
8709 cx,
8710 );
8711 });
8712}
8713
8714#[gpui::test]
8715async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8716 init_test(cx, |_| {});
8717
8718 let language = Arc::new(Language::new(
8719 LanguageConfig::default(),
8720 Some(tree_sitter_rust::LANGUAGE.into()),
8721 ));
8722
8723 let text = "let a = 2;";
8724
8725 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8726 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8727 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8728
8729 editor
8730 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8731 .await;
8732
8733 // Test case 1: Cursor at end of word
8734 editor.update_in(cx, |editor, window, cx| {
8735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8736 s.select_display_ranges([
8737 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8738 ]);
8739 });
8740 });
8741 editor.update(cx, |editor, cx| {
8742 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8743 });
8744 editor.update_in(cx, |editor, window, cx| {
8745 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8746 });
8747 editor.update(cx, |editor, cx| {
8748 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8749 });
8750 editor.update_in(cx, |editor, window, cx| {
8751 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8752 });
8753 editor.update(cx, |editor, cx| {
8754 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8755 });
8756
8757 // Test case 2: Cursor at end of statement
8758 editor.update_in(cx, |editor, window, cx| {
8759 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8760 s.select_display_ranges([
8761 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8762 ]);
8763 });
8764 });
8765 editor.update(cx, |editor, cx| {
8766 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8767 });
8768 editor.update_in(cx, |editor, window, cx| {
8769 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8770 });
8771 editor.update(cx, |editor, cx| {
8772 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8773 });
8774}
8775
8776#[gpui::test]
8777async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8778 init_test(cx, |_| {});
8779
8780 let language = Arc::new(Language::new(
8781 LanguageConfig {
8782 name: "JavaScript".into(),
8783 ..Default::default()
8784 },
8785 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8786 ));
8787
8788 let text = r#"
8789 let a = {
8790 key: "value",
8791 };
8792 "#
8793 .unindent();
8794
8795 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8796 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8797 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8798
8799 editor
8800 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8801 .await;
8802
8803 // Test case 1: Cursor after '{'
8804 editor.update_in(cx, |editor, window, cx| {
8805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8806 s.select_display_ranges([
8807 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8808 ]);
8809 });
8810 });
8811 editor.update(cx, |editor, cx| {
8812 assert_text_with_selections(
8813 editor,
8814 indoc! {r#"
8815 let a = {ˇ
8816 key: "value",
8817 };
8818 "#},
8819 cx,
8820 );
8821 });
8822 editor.update_in(cx, |editor, window, cx| {
8823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8824 });
8825 editor.update(cx, |editor, cx| {
8826 assert_text_with_selections(
8827 editor,
8828 indoc! {r#"
8829 let a = «ˇ{
8830 key: "value",
8831 }»;
8832 "#},
8833 cx,
8834 );
8835 });
8836
8837 // Test case 2: Cursor after ':'
8838 editor.update_in(cx, |editor, window, cx| {
8839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8840 s.select_display_ranges([
8841 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8842 ]);
8843 });
8844 });
8845 editor.update(cx, |editor, cx| {
8846 assert_text_with_selections(
8847 editor,
8848 indoc! {r#"
8849 let a = {
8850 key:ˇ "value",
8851 };
8852 "#},
8853 cx,
8854 );
8855 });
8856 editor.update_in(cx, |editor, window, cx| {
8857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8858 });
8859 editor.update(cx, |editor, cx| {
8860 assert_text_with_selections(
8861 editor,
8862 indoc! {r#"
8863 let a = {
8864 «ˇkey: "value"»,
8865 };
8866 "#},
8867 cx,
8868 );
8869 });
8870 editor.update_in(cx, |editor, window, cx| {
8871 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8872 });
8873 editor.update(cx, |editor, cx| {
8874 assert_text_with_selections(
8875 editor,
8876 indoc! {r#"
8877 let a = «ˇ{
8878 key: "value",
8879 }»;
8880 "#},
8881 cx,
8882 );
8883 });
8884
8885 // Test case 3: Cursor after ','
8886 editor.update_in(cx, |editor, window, cx| {
8887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8888 s.select_display_ranges([
8889 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8890 ]);
8891 });
8892 });
8893 editor.update(cx, |editor, cx| {
8894 assert_text_with_selections(
8895 editor,
8896 indoc! {r#"
8897 let a = {
8898 key: "value",ˇ
8899 };
8900 "#},
8901 cx,
8902 );
8903 });
8904 editor.update_in(cx, |editor, window, cx| {
8905 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8906 });
8907 editor.update(cx, |editor, cx| {
8908 assert_text_with_selections(
8909 editor,
8910 indoc! {r#"
8911 let a = «ˇ{
8912 key: "value",
8913 }»;
8914 "#},
8915 cx,
8916 );
8917 });
8918
8919 // Test case 4: Cursor after ';'
8920 editor.update_in(cx, |editor, window, cx| {
8921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8922 s.select_display_ranges([
8923 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8924 ]);
8925 });
8926 });
8927 editor.update(cx, |editor, cx| {
8928 assert_text_with_selections(
8929 editor,
8930 indoc! {r#"
8931 let a = {
8932 key: "value",
8933 };ˇ
8934 "#},
8935 cx,
8936 );
8937 });
8938 editor.update_in(cx, |editor, window, cx| {
8939 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8940 });
8941 editor.update(cx, |editor, cx| {
8942 assert_text_with_selections(
8943 editor,
8944 indoc! {r#"
8945 «ˇlet a = {
8946 key: "value",
8947 };
8948 »"#},
8949 cx,
8950 );
8951 });
8952}
8953
8954#[gpui::test]
8955async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8956 init_test(cx, |_| {});
8957
8958 let language = Arc::new(Language::new(
8959 LanguageConfig::default(),
8960 Some(tree_sitter_rust::LANGUAGE.into()),
8961 ));
8962
8963 let text = r#"
8964 use mod1::mod2::{mod3, mod4};
8965
8966 fn fn_1(param1: bool, param2: &str) {
8967 let var1 = "hello world";
8968 }
8969 "#
8970 .unindent();
8971
8972 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8973 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8974 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8975
8976 editor
8977 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8978 .await;
8979
8980 // Test 1: Cursor on a letter of a string word
8981 editor.update_in(cx, |editor, window, cx| {
8982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8983 s.select_display_ranges([
8984 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8985 ]);
8986 });
8987 });
8988 editor.update_in(cx, |editor, window, cx| {
8989 assert_text_with_selections(
8990 editor,
8991 indoc! {r#"
8992 use mod1::mod2::{mod3, mod4};
8993
8994 fn fn_1(param1: bool, param2: &str) {
8995 let var1 = "hˇello world";
8996 }
8997 "#},
8998 cx,
8999 );
9000 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9001 assert_text_with_selections(
9002 editor,
9003 indoc! {r#"
9004 use mod1::mod2::{mod3, mod4};
9005
9006 fn fn_1(param1: bool, param2: &str) {
9007 let var1 = "«ˇhello» world";
9008 }
9009 "#},
9010 cx,
9011 );
9012 });
9013
9014 // Test 2: Partial selection within a word
9015 editor.update_in(cx, |editor, window, cx| {
9016 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9017 s.select_display_ranges([
9018 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9019 ]);
9020 });
9021 });
9022 editor.update_in(cx, |editor, window, cx| {
9023 assert_text_with_selections(
9024 editor,
9025 indoc! {r#"
9026 use mod1::mod2::{mod3, mod4};
9027
9028 fn fn_1(param1: bool, param2: &str) {
9029 let var1 = "h«elˇ»lo world";
9030 }
9031 "#},
9032 cx,
9033 );
9034 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9035 assert_text_with_selections(
9036 editor,
9037 indoc! {r#"
9038 use mod1::mod2::{mod3, mod4};
9039
9040 fn fn_1(param1: bool, param2: &str) {
9041 let var1 = "«ˇhello» world";
9042 }
9043 "#},
9044 cx,
9045 );
9046 });
9047
9048 // Test 3: Complete word already selected
9049 editor.update_in(cx, |editor, window, cx| {
9050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9051 s.select_display_ranges([
9052 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9053 ]);
9054 });
9055 });
9056 editor.update_in(cx, |editor, window, cx| {
9057 assert_text_with_selections(
9058 editor,
9059 indoc! {r#"
9060 use mod1::mod2::{mod3, mod4};
9061
9062 fn fn_1(param1: bool, param2: &str) {
9063 let var1 = "«helloˇ» world";
9064 }
9065 "#},
9066 cx,
9067 );
9068 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9069 assert_text_with_selections(
9070 editor,
9071 indoc! {r#"
9072 use mod1::mod2::{mod3, mod4};
9073
9074 fn fn_1(param1: bool, param2: &str) {
9075 let var1 = "«hello worldˇ»";
9076 }
9077 "#},
9078 cx,
9079 );
9080 });
9081
9082 // Test 4: Selection spanning across words
9083 editor.update_in(cx, |editor, window, cx| {
9084 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9085 s.select_display_ranges([
9086 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9087 ]);
9088 });
9089 });
9090 editor.update_in(cx, |editor, window, cx| {
9091 assert_text_with_selections(
9092 editor,
9093 indoc! {r#"
9094 use mod1::mod2::{mod3, mod4};
9095
9096 fn fn_1(param1: bool, param2: &str) {
9097 let var1 = "hel«lo woˇ»rld";
9098 }
9099 "#},
9100 cx,
9101 );
9102 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9103 assert_text_with_selections(
9104 editor,
9105 indoc! {r#"
9106 use mod1::mod2::{mod3, mod4};
9107
9108 fn fn_1(param1: bool, param2: &str) {
9109 let var1 = "«ˇhello world»";
9110 }
9111 "#},
9112 cx,
9113 );
9114 });
9115
9116 // Test 5: Expansion beyond string
9117 editor.update_in(cx, |editor, window, cx| {
9118 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9119 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9120 assert_text_with_selections(
9121 editor,
9122 indoc! {r#"
9123 use mod1::mod2::{mod3, mod4};
9124
9125 fn fn_1(param1: bool, param2: &str) {
9126 «ˇlet var1 = "hello world";»
9127 }
9128 "#},
9129 cx,
9130 );
9131 });
9132}
9133
9134#[gpui::test]
9135async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9136 init_test(cx, |_| {});
9137
9138 let mut cx = EditorTestContext::new(cx).await;
9139
9140 let language = Arc::new(Language::new(
9141 LanguageConfig::default(),
9142 Some(tree_sitter_rust::LANGUAGE.into()),
9143 ));
9144
9145 cx.update_buffer(|buffer, cx| {
9146 buffer.set_language(Some(language), cx);
9147 });
9148
9149 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9150 cx.update_editor(|editor, window, cx| {
9151 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9152 });
9153
9154 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9155}
9156
9157#[gpui::test]
9158async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9159 init_test(cx, |_| {});
9160
9161 let base_text = r#"
9162 impl A {
9163 // this is an uncommitted comment
9164
9165 fn b() {
9166 c();
9167 }
9168
9169 // this is another uncommitted comment
9170
9171 fn d() {
9172 // e
9173 // f
9174 }
9175 }
9176
9177 fn g() {
9178 // h
9179 }
9180 "#
9181 .unindent();
9182
9183 let text = r#"
9184 ˇimpl A {
9185
9186 fn b() {
9187 c();
9188 }
9189
9190 fn d() {
9191 // e
9192 // f
9193 }
9194 }
9195
9196 fn g() {
9197 // h
9198 }
9199 "#
9200 .unindent();
9201
9202 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9203 cx.set_state(&text);
9204 cx.set_head_text(&base_text);
9205 cx.update_editor(|editor, window, cx| {
9206 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9207 });
9208
9209 cx.assert_state_with_diff(
9210 "
9211 ˇimpl A {
9212 - // this is an uncommitted comment
9213
9214 fn b() {
9215 c();
9216 }
9217
9218 - // this is another uncommitted comment
9219 -
9220 fn d() {
9221 // e
9222 // f
9223 }
9224 }
9225
9226 fn g() {
9227 // h
9228 }
9229 "
9230 .unindent(),
9231 );
9232
9233 let expected_display_text = "
9234 impl A {
9235 // this is an uncommitted comment
9236
9237 fn b() {
9238 ⋯
9239 }
9240
9241 // this is another uncommitted comment
9242
9243 fn d() {
9244 ⋯
9245 }
9246 }
9247
9248 fn g() {
9249 ⋯
9250 }
9251 "
9252 .unindent();
9253
9254 cx.update_editor(|editor, window, cx| {
9255 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9256 assert_eq!(editor.display_text(cx), expected_display_text);
9257 });
9258}
9259
9260#[gpui::test]
9261async fn test_autoindent(cx: &mut TestAppContext) {
9262 init_test(cx, |_| {});
9263
9264 let language = Arc::new(
9265 Language::new(
9266 LanguageConfig {
9267 brackets: BracketPairConfig {
9268 pairs: vec![
9269 BracketPair {
9270 start: "{".to_string(),
9271 end: "}".to_string(),
9272 close: false,
9273 surround: false,
9274 newline: true,
9275 },
9276 BracketPair {
9277 start: "(".to_string(),
9278 end: ")".to_string(),
9279 close: false,
9280 surround: false,
9281 newline: true,
9282 },
9283 ],
9284 ..Default::default()
9285 },
9286 ..Default::default()
9287 },
9288 Some(tree_sitter_rust::LANGUAGE.into()),
9289 )
9290 .with_indents_query(
9291 r#"
9292 (_ "(" ")" @end) @indent
9293 (_ "{" "}" @end) @indent
9294 "#,
9295 )
9296 .unwrap(),
9297 );
9298
9299 let text = "fn a() {}";
9300
9301 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9302 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9303 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9304 editor
9305 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9306 .await;
9307
9308 editor.update_in(cx, |editor, window, cx| {
9309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9310 s.select_ranges([5..5, 8..8, 9..9])
9311 });
9312 editor.newline(&Newline, window, cx);
9313 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9314 assert_eq!(
9315 editor.selections.ranges(cx),
9316 &[
9317 Point::new(1, 4)..Point::new(1, 4),
9318 Point::new(3, 4)..Point::new(3, 4),
9319 Point::new(5, 0)..Point::new(5, 0)
9320 ]
9321 );
9322 });
9323}
9324
9325#[gpui::test]
9326async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9327 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9328
9329 let language = Arc::new(
9330 Language::new(
9331 LanguageConfig {
9332 brackets: BracketPairConfig {
9333 pairs: vec![
9334 BracketPair {
9335 start: "{".to_string(),
9336 end: "}".to_string(),
9337 close: false,
9338 surround: false,
9339 newline: true,
9340 },
9341 BracketPair {
9342 start: "(".to_string(),
9343 end: ")".to_string(),
9344 close: false,
9345 surround: false,
9346 newline: true,
9347 },
9348 ],
9349 ..Default::default()
9350 },
9351 ..Default::default()
9352 },
9353 Some(tree_sitter_rust::LANGUAGE.into()),
9354 )
9355 .with_indents_query(
9356 r#"
9357 (_ "(" ")" @end) @indent
9358 (_ "{" "}" @end) @indent
9359 "#,
9360 )
9361 .unwrap(),
9362 );
9363
9364 let text = "fn a() {}";
9365
9366 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9367 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9368 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9369 editor
9370 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9371 .await;
9372
9373 editor.update_in(cx, |editor, window, cx| {
9374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9375 s.select_ranges([5..5, 8..8, 9..9])
9376 });
9377 editor.newline(&Newline, window, cx);
9378 assert_eq!(
9379 editor.text(cx),
9380 indoc!(
9381 "
9382 fn a(
9383
9384 ) {
9385
9386 }
9387 "
9388 )
9389 );
9390 assert_eq!(
9391 editor.selections.ranges(cx),
9392 &[
9393 Point::new(1, 0)..Point::new(1, 0),
9394 Point::new(3, 0)..Point::new(3, 0),
9395 Point::new(5, 0)..Point::new(5, 0)
9396 ]
9397 );
9398 });
9399}
9400
9401#[gpui::test]
9402async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9403 init_test(cx, |settings| {
9404 settings.defaults.auto_indent = Some(true);
9405 settings.languages.0.insert(
9406 "python".into(),
9407 LanguageSettingsContent {
9408 auto_indent: Some(false),
9409 ..Default::default()
9410 },
9411 );
9412 });
9413
9414 let mut cx = EditorTestContext::new(cx).await;
9415
9416 let injected_language = Arc::new(
9417 Language::new(
9418 LanguageConfig {
9419 brackets: BracketPairConfig {
9420 pairs: vec![
9421 BracketPair {
9422 start: "{".to_string(),
9423 end: "}".to_string(),
9424 close: false,
9425 surround: false,
9426 newline: true,
9427 },
9428 BracketPair {
9429 start: "(".to_string(),
9430 end: ")".to_string(),
9431 close: true,
9432 surround: false,
9433 newline: true,
9434 },
9435 ],
9436 ..Default::default()
9437 },
9438 name: "python".into(),
9439 ..Default::default()
9440 },
9441 Some(tree_sitter_python::LANGUAGE.into()),
9442 )
9443 .with_indents_query(
9444 r#"
9445 (_ "(" ")" @end) @indent
9446 (_ "{" "}" @end) @indent
9447 "#,
9448 )
9449 .unwrap(),
9450 );
9451
9452 let language = Arc::new(
9453 Language::new(
9454 LanguageConfig {
9455 brackets: BracketPairConfig {
9456 pairs: vec![
9457 BracketPair {
9458 start: "{".to_string(),
9459 end: "}".to_string(),
9460 close: false,
9461 surround: false,
9462 newline: true,
9463 },
9464 BracketPair {
9465 start: "(".to_string(),
9466 end: ")".to_string(),
9467 close: true,
9468 surround: false,
9469 newline: true,
9470 },
9471 ],
9472 ..Default::default()
9473 },
9474 name: LanguageName::new("rust"),
9475 ..Default::default()
9476 },
9477 Some(tree_sitter_rust::LANGUAGE.into()),
9478 )
9479 .with_indents_query(
9480 r#"
9481 (_ "(" ")" @end) @indent
9482 (_ "{" "}" @end) @indent
9483 "#,
9484 )
9485 .unwrap()
9486 .with_injection_query(
9487 r#"
9488 (macro_invocation
9489 macro: (identifier) @_macro_name
9490 (token_tree) @injection.content
9491 (#set! injection.language "python"))
9492 "#,
9493 )
9494 .unwrap(),
9495 );
9496
9497 cx.language_registry().add(injected_language);
9498 cx.language_registry().add(language.clone());
9499
9500 cx.update_buffer(|buffer, cx| {
9501 buffer.set_language(Some(language), cx);
9502 });
9503
9504 cx.set_state(r#"struct A {ˇ}"#);
9505
9506 cx.update_editor(|editor, window, cx| {
9507 editor.newline(&Default::default(), window, cx);
9508 });
9509
9510 cx.assert_editor_state(indoc!(
9511 "struct A {
9512 ˇ
9513 }"
9514 ));
9515
9516 cx.set_state(r#"select_biased!(ˇ)"#);
9517
9518 cx.update_editor(|editor, window, cx| {
9519 editor.newline(&Default::default(), window, cx);
9520 editor.handle_input("def ", window, cx);
9521 editor.handle_input("(", window, cx);
9522 editor.newline(&Default::default(), window, cx);
9523 editor.handle_input("a", window, cx);
9524 });
9525
9526 cx.assert_editor_state(indoc!(
9527 "select_biased!(
9528 def (
9529 aˇ
9530 )
9531 )"
9532 ));
9533}
9534
9535#[gpui::test]
9536async fn test_autoindent_selections(cx: &mut TestAppContext) {
9537 init_test(cx, |_| {});
9538
9539 {
9540 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9541 cx.set_state(indoc! {"
9542 impl A {
9543
9544 fn b() {}
9545
9546 «fn c() {
9547
9548 }ˇ»
9549 }
9550 "});
9551
9552 cx.update_editor(|editor, window, cx| {
9553 editor.autoindent(&Default::default(), window, cx);
9554 });
9555
9556 cx.assert_editor_state(indoc! {"
9557 impl A {
9558
9559 fn b() {}
9560
9561 «fn c() {
9562
9563 }ˇ»
9564 }
9565 "});
9566 }
9567
9568 {
9569 let mut cx = EditorTestContext::new_multibuffer(
9570 cx,
9571 [indoc! { "
9572 impl A {
9573 «
9574 // a
9575 fn b(){}
9576 »
9577 «
9578 }
9579 fn c(){}
9580 »
9581 "}],
9582 );
9583
9584 let buffer = cx.update_editor(|editor, _, cx| {
9585 let buffer = editor.buffer().update(cx, |buffer, _| {
9586 buffer.all_buffers().iter().next().unwrap().clone()
9587 });
9588 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9589 buffer
9590 });
9591
9592 cx.run_until_parked();
9593 cx.update_editor(|editor, window, cx| {
9594 editor.select_all(&Default::default(), window, cx);
9595 editor.autoindent(&Default::default(), window, cx)
9596 });
9597 cx.run_until_parked();
9598
9599 cx.update(|_, cx| {
9600 assert_eq!(
9601 buffer.read(cx).text(),
9602 indoc! { "
9603 impl A {
9604
9605 // a
9606 fn b(){}
9607
9608
9609 }
9610 fn c(){}
9611
9612 " }
9613 )
9614 });
9615 }
9616}
9617
9618#[gpui::test]
9619async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9620 init_test(cx, |_| {});
9621
9622 let mut cx = EditorTestContext::new(cx).await;
9623
9624 let language = Arc::new(Language::new(
9625 LanguageConfig {
9626 brackets: BracketPairConfig {
9627 pairs: vec![
9628 BracketPair {
9629 start: "{".to_string(),
9630 end: "}".to_string(),
9631 close: true,
9632 surround: true,
9633 newline: true,
9634 },
9635 BracketPair {
9636 start: "(".to_string(),
9637 end: ")".to_string(),
9638 close: true,
9639 surround: true,
9640 newline: true,
9641 },
9642 BracketPair {
9643 start: "/*".to_string(),
9644 end: " */".to_string(),
9645 close: true,
9646 surround: true,
9647 newline: true,
9648 },
9649 BracketPair {
9650 start: "[".to_string(),
9651 end: "]".to_string(),
9652 close: false,
9653 surround: false,
9654 newline: true,
9655 },
9656 BracketPair {
9657 start: "\"".to_string(),
9658 end: "\"".to_string(),
9659 close: true,
9660 surround: true,
9661 newline: false,
9662 },
9663 BracketPair {
9664 start: "<".to_string(),
9665 end: ">".to_string(),
9666 close: false,
9667 surround: true,
9668 newline: true,
9669 },
9670 ],
9671 ..Default::default()
9672 },
9673 autoclose_before: "})]".to_string(),
9674 ..Default::default()
9675 },
9676 Some(tree_sitter_rust::LANGUAGE.into()),
9677 ));
9678
9679 cx.language_registry().add(language.clone());
9680 cx.update_buffer(|buffer, cx| {
9681 buffer.set_language(Some(language), cx);
9682 });
9683
9684 cx.set_state(
9685 &r#"
9686 🏀ˇ
9687 εˇ
9688 ❤️ˇ
9689 "#
9690 .unindent(),
9691 );
9692
9693 // autoclose multiple nested brackets at multiple cursors
9694 cx.update_editor(|editor, window, cx| {
9695 editor.handle_input("{", window, cx);
9696 editor.handle_input("{", window, cx);
9697 editor.handle_input("{", window, cx);
9698 });
9699 cx.assert_editor_state(
9700 &"
9701 🏀{{{ˇ}}}
9702 ε{{{ˇ}}}
9703 ❤️{{{ˇ}}}
9704 "
9705 .unindent(),
9706 );
9707
9708 // insert a different closing bracket
9709 cx.update_editor(|editor, window, cx| {
9710 editor.handle_input(")", window, cx);
9711 });
9712 cx.assert_editor_state(
9713 &"
9714 🏀{{{)ˇ}}}
9715 ε{{{)ˇ}}}
9716 ❤️{{{)ˇ}}}
9717 "
9718 .unindent(),
9719 );
9720
9721 // skip over the auto-closed brackets when typing a closing bracket
9722 cx.update_editor(|editor, window, cx| {
9723 editor.move_right(&MoveRight, window, cx);
9724 editor.handle_input("}", window, cx);
9725 editor.handle_input("}", window, cx);
9726 editor.handle_input("}", window, cx);
9727 });
9728 cx.assert_editor_state(
9729 &"
9730 🏀{{{)}}}}ˇ
9731 ε{{{)}}}}ˇ
9732 ❤️{{{)}}}}ˇ
9733 "
9734 .unindent(),
9735 );
9736
9737 // autoclose multi-character pairs
9738 cx.set_state(
9739 &"
9740 ˇ
9741 ˇ
9742 "
9743 .unindent(),
9744 );
9745 cx.update_editor(|editor, window, cx| {
9746 editor.handle_input("/", window, cx);
9747 editor.handle_input("*", window, cx);
9748 });
9749 cx.assert_editor_state(
9750 &"
9751 /*ˇ */
9752 /*ˇ */
9753 "
9754 .unindent(),
9755 );
9756
9757 // one cursor autocloses a multi-character pair, one cursor
9758 // does not autoclose.
9759 cx.set_state(
9760 &"
9761 /ˇ
9762 ˇ
9763 "
9764 .unindent(),
9765 );
9766 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9767 cx.assert_editor_state(
9768 &"
9769 /*ˇ */
9770 *ˇ
9771 "
9772 .unindent(),
9773 );
9774
9775 // Don't autoclose if the next character isn't whitespace and isn't
9776 // listed in the language's "autoclose_before" section.
9777 cx.set_state("ˇa b");
9778 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9779 cx.assert_editor_state("{ˇa b");
9780
9781 // Don't autoclose if `close` is false for the bracket pair
9782 cx.set_state("ˇ");
9783 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9784 cx.assert_editor_state("[ˇ");
9785
9786 // Surround with brackets if text is selected
9787 cx.set_state("«aˇ» b");
9788 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9789 cx.assert_editor_state("{«aˇ»} b");
9790
9791 // Autoclose when not immediately after a word character
9792 cx.set_state("a ˇ");
9793 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9794 cx.assert_editor_state("a \"ˇ\"");
9795
9796 // Autoclose pair where the start and end characters are the same
9797 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9798 cx.assert_editor_state("a \"\"ˇ");
9799
9800 // Don't autoclose when immediately after a word character
9801 cx.set_state("aˇ");
9802 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9803 cx.assert_editor_state("a\"ˇ");
9804
9805 // Do autoclose when after a non-word character
9806 cx.set_state("{ˇ");
9807 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9808 cx.assert_editor_state("{\"ˇ\"");
9809
9810 // Non identical pairs autoclose regardless of preceding character
9811 cx.set_state("aˇ");
9812 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9813 cx.assert_editor_state("a{ˇ}");
9814
9815 // Don't autoclose pair if autoclose is disabled
9816 cx.set_state("ˇ");
9817 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9818 cx.assert_editor_state("<ˇ");
9819
9820 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9821 cx.set_state("«aˇ» b");
9822 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9823 cx.assert_editor_state("<«aˇ»> b");
9824}
9825
9826#[gpui::test]
9827async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9828 init_test(cx, |settings| {
9829 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9830 });
9831
9832 let mut cx = EditorTestContext::new(cx).await;
9833
9834 let language = Arc::new(Language::new(
9835 LanguageConfig {
9836 brackets: BracketPairConfig {
9837 pairs: vec![
9838 BracketPair {
9839 start: "{".to_string(),
9840 end: "}".to_string(),
9841 close: true,
9842 surround: true,
9843 newline: true,
9844 },
9845 BracketPair {
9846 start: "(".to_string(),
9847 end: ")".to_string(),
9848 close: true,
9849 surround: true,
9850 newline: true,
9851 },
9852 BracketPair {
9853 start: "[".to_string(),
9854 end: "]".to_string(),
9855 close: false,
9856 surround: false,
9857 newline: true,
9858 },
9859 ],
9860 ..Default::default()
9861 },
9862 autoclose_before: "})]".to_string(),
9863 ..Default::default()
9864 },
9865 Some(tree_sitter_rust::LANGUAGE.into()),
9866 ));
9867
9868 cx.language_registry().add(language.clone());
9869 cx.update_buffer(|buffer, cx| {
9870 buffer.set_language(Some(language), cx);
9871 });
9872
9873 cx.set_state(
9874 &"
9875 ˇ
9876 ˇ
9877 ˇ
9878 "
9879 .unindent(),
9880 );
9881
9882 // ensure only matching closing brackets are skipped over
9883 cx.update_editor(|editor, window, cx| {
9884 editor.handle_input("}", window, cx);
9885 editor.move_left(&MoveLeft, window, cx);
9886 editor.handle_input(")", window, cx);
9887 editor.move_left(&MoveLeft, window, cx);
9888 });
9889 cx.assert_editor_state(
9890 &"
9891 ˇ)}
9892 ˇ)}
9893 ˇ)}
9894 "
9895 .unindent(),
9896 );
9897
9898 // skip-over closing brackets at multiple cursors
9899 cx.update_editor(|editor, window, cx| {
9900 editor.handle_input(")", window, cx);
9901 editor.handle_input("}", window, cx);
9902 });
9903 cx.assert_editor_state(
9904 &"
9905 )}ˇ
9906 )}ˇ
9907 )}ˇ
9908 "
9909 .unindent(),
9910 );
9911
9912 // ignore non-close brackets
9913 cx.update_editor(|editor, window, cx| {
9914 editor.handle_input("]", window, cx);
9915 editor.move_left(&MoveLeft, window, cx);
9916 editor.handle_input("]", window, cx);
9917 });
9918 cx.assert_editor_state(
9919 &"
9920 )}]ˇ]
9921 )}]ˇ]
9922 )}]ˇ]
9923 "
9924 .unindent(),
9925 );
9926}
9927
9928#[gpui::test]
9929async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9930 init_test(cx, |_| {});
9931
9932 let mut cx = EditorTestContext::new(cx).await;
9933
9934 let html_language = Arc::new(
9935 Language::new(
9936 LanguageConfig {
9937 name: "HTML".into(),
9938 brackets: BracketPairConfig {
9939 pairs: vec![
9940 BracketPair {
9941 start: "<".into(),
9942 end: ">".into(),
9943 close: true,
9944 ..Default::default()
9945 },
9946 BracketPair {
9947 start: "{".into(),
9948 end: "}".into(),
9949 close: true,
9950 ..Default::default()
9951 },
9952 BracketPair {
9953 start: "(".into(),
9954 end: ")".into(),
9955 close: true,
9956 ..Default::default()
9957 },
9958 ],
9959 ..Default::default()
9960 },
9961 autoclose_before: "})]>".into(),
9962 ..Default::default()
9963 },
9964 Some(tree_sitter_html::LANGUAGE.into()),
9965 )
9966 .with_injection_query(
9967 r#"
9968 (script_element
9969 (raw_text) @injection.content
9970 (#set! injection.language "javascript"))
9971 "#,
9972 )
9973 .unwrap(),
9974 );
9975
9976 let javascript_language = Arc::new(Language::new(
9977 LanguageConfig {
9978 name: "JavaScript".into(),
9979 brackets: BracketPairConfig {
9980 pairs: vec![
9981 BracketPair {
9982 start: "/*".into(),
9983 end: " */".into(),
9984 close: true,
9985 ..Default::default()
9986 },
9987 BracketPair {
9988 start: "{".into(),
9989 end: "}".into(),
9990 close: true,
9991 ..Default::default()
9992 },
9993 BracketPair {
9994 start: "(".into(),
9995 end: ")".into(),
9996 close: true,
9997 ..Default::default()
9998 },
9999 ],
10000 ..Default::default()
10001 },
10002 autoclose_before: "})]>".into(),
10003 ..Default::default()
10004 },
10005 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10006 ));
10007
10008 cx.language_registry().add(html_language.clone());
10009 cx.language_registry().add(javascript_language);
10010 cx.executor().run_until_parked();
10011
10012 cx.update_buffer(|buffer, cx| {
10013 buffer.set_language(Some(html_language), cx);
10014 });
10015
10016 cx.set_state(
10017 &r#"
10018 <body>ˇ
10019 <script>
10020 var x = 1;ˇ
10021 </script>
10022 </body>ˇ
10023 "#
10024 .unindent(),
10025 );
10026
10027 // Precondition: different languages are active at different locations.
10028 cx.update_editor(|editor, window, cx| {
10029 let snapshot = editor.snapshot(window, cx);
10030 let cursors = editor.selections.ranges::<usize>(cx);
10031 let languages = cursors
10032 .iter()
10033 .map(|c| snapshot.language_at(c.start).unwrap().name())
10034 .collect::<Vec<_>>();
10035 assert_eq!(
10036 languages,
10037 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10038 );
10039 });
10040
10041 // Angle brackets autoclose in HTML, but not JavaScript.
10042 cx.update_editor(|editor, window, cx| {
10043 editor.handle_input("<", window, cx);
10044 editor.handle_input("a", window, cx);
10045 });
10046 cx.assert_editor_state(
10047 &r#"
10048 <body><aˇ>
10049 <script>
10050 var x = 1;<aˇ
10051 </script>
10052 </body><aˇ>
10053 "#
10054 .unindent(),
10055 );
10056
10057 // Curly braces and parens autoclose in both HTML and JavaScript.
10058 cx.update_editor(|editor, window, cx| {
10059 editor.handle_input(" b=", window, cx);
10060 editor.handle_input("{", window, cx);
10061 editor.handle_input("c", window, cx);
10062 editor.handle_input("(", window, cx);
10063 });
10064 cx.assert_editor_state(
10065 &r#"
10066 <body><a b={c(ˇ)}>
10067 <script>
10068 var x = 1;<a b={c(ˇ)}
10069 </script>
10070 </body><a b={c(ˇ)}>
10071 "#
10072 .unindent(),
10073 );
10074
10075 // Brackets that were already autoclosed are skipped.
10076 cx.update_editor(|editor, window, cx| {
10077 editor.handle_input(")", window, cx);
10078 editor.handle_input("d", window, cx);
10079 editor.handle_input("}", window, cx);
10080 });
10081 cx.assert_editor_state(
10082 &r#"
10083 <body><a b={c()d}ˇ>
10084 <script>
10085 var x = 1;<a b={c()d}ˇ
10086 </script>
10087 </body><a b={c()d}ˇ>
10088 "#
10089 .unindent(),
10090 );
10091 cx.update_editor(|editor, window, cx| {
10092 editor.handle_input(">", window, cx);
10093 });
10094 cx.assert_editor_state(
10095 &r#"
10096 <body><a b={c()d}>ˇ
10097 <script>
10098 var x = 1;<a b={c()d}>ˇ
10099 </script>
10100 </body><a b={c()d}>ˇ
10101 "#
10102 .unindent(),
10103 );
10104
10105 // Reset
10106 cx.set_state(
10107 &r#"
10108 <body>ˇ
10109 <script>
10110 var x = 1;ˇ
10111 </script>
10112 </body>ˇ
10113 "#
10114 .unindent(),
10115 );
10116
10117 cx.update_editor(|editor, window, cx| {
10118 editor.handle_input("<", window, cx);
10119 });
10120 cx.assert_editor_state(
10121 &r#"
10122 <body><ˇ>
10123 <script>
10124 var x = 1;<ˇ
10125 </script>
10126 </body><ˇ>
10127 "#
10128 .unindent(),
10129 );
10130
10131 // When backspacing, the closing angle brackets are removed.
10132 cx.update_editor(|editor, window, cx| {
10133 editor.backspace(&Backspace, window, cx);
10134 });
10135 cx.assert_editor_state(
10136 &r#"
10137 <body>ˇ
10138 <script>
10139 var x = 1;ˇ
10140 </script>
10141 </body>ˇ
10142 "#
10143 .unindent(),
10144 );
10145
10146 // Block comments autoclose in JavaScript, but not HTML.
10147 cx.update_editor(|editor, window, cx| {
10148 editor.handle_input("/", window, cx);
10149 editor.handle_input("*", window, cx);
10150 });
10151 cx.assert_editor_state(
10152 &r#"
10153 <body>/*ˇ
10154 <script>
10155 var x = 1;/*ˇ */
10156 </script>
10157 </body>/*ˇ
10158 "#
10159 .unindent(),
10160 );
10161}
10162
10163#[gpui::test]
10164async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10165 init_test(cx, |_| {});
10166
10167 let mut cx = EditorTestContext::new(cx).await;
10168
10169 let rust_language = Arc::new(
10170 Language::new(
10171 LanguageConfig {
10172 name: "Rust".into(),
10173 brackets: serde_json::from_value(json!([
10174 { "start": "{", "end": "}", "close": true, "newline": true },
10175 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10176 ]))
10177 .unwrap(),
10178 autoclose_before: "})]>".into(),
10179 ..Default::default()
10180 },
10181 Some(tree_sitter_rust::LANGUAGE.into()),
10182 )
10183 .with_override_query("(string_literal) @string")
10184 .unwrap(),
10185 );
10186
10187 cx.language_registry().add(rust_language.clone());
10188 cx.update_buffer(|buffer, cx| {
10189 buffer.set_language(Some(rust_language), cx);
10190 });
10191
10192 cx.set_state(
10193 &r#"
10194 let x = ˇ
10195 "#
10196 .unindent(),
10197 );
10198
10199 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10200 cx.update_editor(|editor, window, cx| {
10201 editor.handle_input("\"", window, cx);
10202 });
10203 cx.assert_editor_state(
10204 &r#"
10205 let x = "ˇ"
10206 "#
10207 .unindent(),
10208 );
10209
10210 // Inserting another quotation mark. The cursor moves across the existing
10211 // automatically-inserted quotation mark.
10212 cx.update_editor(|editor, window, cx| {
10213 editor.handle_input("\"", window, cx);
10214 });
10215 cx.assert_editor_state(
10216 &r#"
10217 let x = ""ˇ
10218 "#
10219 .unindent(),
10220 );
10221
10222 // Reset
10223 cx.set_state(
10224 &r#"
10225 let x = ˇ
10226 "#
10227 .unindent(),
10228 );
10229
10230 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10231 cx.update_editor(|editor, window, cx| {
10232 editor.handle_input("\"", window, cx);
10233 editor.handle_input(" ", window, cx);
10234 editor.move_left(&Default::default(), window, cx);
10235 editor.handle_input("\\", window, cx);
10236 editor.handle_input("\"", window, cx);
10237 });
10238 cx.assert_editor_state(
10239 &r#"
10240 let x = "\"ˇ "
10241 "#
10242 .unindent(),
10243 );
10244
10245 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10246 // mark. Nothing is inserted.
10247 cx.update_editor(|editor, window, cx| {
10248 editor.move_right(&Default::default(), window, cx);
10249 editor.handle_input("\"", window, cx);
10250 });
10251 cx.assert_editor_state(
10252 &r#"
10253 let x = "\" "ˇ
10254 "#
10255 .unindent(),
10256 );
10257}
10258
10259#[gpui::test]
10260async fn test_surround_with_pair(cx: &mut TestAppContext) {
10261 init_test(cx, |_| {});
10262
10263 let language = Arc::new(Language::new(
10264 LanguageConfig {
10265 brackets: BracketPairConfig {
10266 pairs: vec![
10267 BracketPair {
10268 start: "{".to_string(),
10269 end: "}".to_string(),
10270 close: true,
10271 surround: true,
10272 newline: true,
10273 },
10274 BracketPair {
10275 start: "/* ".to_string(),
10276 end: "*/".to_string(),
10277 close: true,
10278 surround: true,
10279 ..Default::default()
10280 },
10281 ],
10282 ..Default::default()
10283 },
10284 ..Default::default()
10285 },
10286 Some(tree_sitter_rust::LANGUAGE.into()),
10287 ));
10288
10289 let text = r#"
10290 a
10291 b
10292 c
10293 "#
10294 .unindent();
10295
10296 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10297 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10298 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10299 editor
10300 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10301 .await;
10302
10303 editor.update_in(cx, |editor, window, cx| {
10304 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10305 s.select_display_ranges([
10306 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10307 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10308 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10309 ])
10310 });
10311
10312 editor.handle_input("{", window, cx);
10313 editor.handle_input("{", window, cx);
10314 editor.handle_input("{", window, cx);
10315 assert_eq!(
10316 editor.text(cx),
10317 "
10318 {{{a}}}
10319 {{{b}}}
10320 {{{c}}}
10321 "
10322 .unindent()
10323 );
10324 assert_eq!(
10325 editor.selections.display_ranges(cx),
10326 [
10327 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10328 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10329 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10330 ]
10331 );
10332
10333 editor.undo(&Undo, window, cx);
10334 editor.undo(&Undo, window, cx);
10335 editor.undo(&Undo, window, cx);
10336 assert_eq!(
10337 editor.text(cx),
10338 "
10339 a
10340 b
10341 c
10342 "
10343 .unindent()
10344 );
10345 assert_eq!(
10346 editor.selections.display_ranges(cx),
10347 [
10348 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10349 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10350 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10351 ]
10352 );
10353
10354 // Ensure inserting the first character of a multi-byte bracket pair
10355 // doesn't surround the selections with the bracket.
10356 editor.handle_input("/", window, cx);
10357 assert_eq!(
10358 editor.text(cx),
10359 "
10360 /
10361 /
10362 /
10363 "
10364 .unindent()
10365 );
10366 assert_eq!(
10367 editor.selections.display_ranges(cx),
10368 [
10369 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10370 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10371 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10372 ]
10373 );
10374
10375 editor.undo(&Undo, window, cx);
10376 assert_eq!(
10377 editor.text(cx),
10378 "
10379 a
10380 b
10381 c
10382 "
10383 .unindent()
10384 );
10385 assert_eq!(
10386 editor.selections.display_ranges(cx),
10387 [
10388 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10389 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10390 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10391 ]
10392 );
10393
10394 // Ensure inserting the last character of a multi-byte bracket pair
10395 // doesn't surround the selections with the bracket.
10396 editor.handle_input("*", window, cx);
10397 assert_eq!(
10398 editor.text(cx),
10399 "
10400 *
10401 *
10402 *
10403 "
10404 .unindent()
10405 );
10406 assert_eq!(
10407 editor.selections.display_ranges(cx),
10408 [
10409 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10410 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10411 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10412 ]
10413 );
10414 });
10415}
10416
10417#[gpui::test]
10418async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10419 init_test(cx, |_| {});
10420
10421 let language = Arc::new(Language::new(
10422 LanguageConfig {
10423 brackets: BracketPairConfig {
10424 pairs: vec![BracketPair {
10425 start: "{".to_string(),
10426 end: "}".to_string(),
10427 close: true,
10428 surround: true,
10429 newline: true,
10430 }],
10431 ..Default::default()
10432 },
10433 autoclose_before: "}".to_string(),
10434 ..Default::default()
10435 },
10436 Some(tree_sitter_rust::LANGUAGE.into()),
10437 ));
10438
10439 let text = r#"
10440 a
10441 b
10442 c
10443 "#
10444 .unindent();
10445
10446 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10447 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10448 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10449 editor
10450 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10451 .await;
10452
10453 editor.update_in(cx, |editor, window, cx| {
10454 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10455 s.select_ranges([
10456 Point::new(0, 1)..Point::new(0, 1),
10457 Point::new(1, 1)..Point::new(1, 1),
10458 Point::new(2, 1)..Point::new(2, 1),
10459 ])
10460 });
10461
10462 editor.handle_input("{", window, cx);
10463 editor.handle_input("{", window, cx);
10464 editor.handle_input("_", window, cx);
10465 assert_eq!(
10466 editor.text(cx),
10467 "
10468 a{{_}}
10469 b{{_}}
10470 c{{_}}
10471 "
10472 .unindent()
10473 );
10474 assert_eq!(
10475 editor.selections.ranges::<Point>(cx),
10476 [
10477 Point::new(0, 4)..Point::new(0, 4),
10478 Point::new(1, 4)..Point::new(1, 4),
10479 Point::new(2, 4)..Point::new(2, 4)
10480 ]
10481 );
10482
10483 editor.backspace(&Default::default(), window, cx);
10484 editor.backspace(&Default::default(), window, cx);
10485 assert_eq!(
10486 editor.text(cx),
10487 "
10488 a{}
10489 b{}
10490 c{}
10491 "
10492 .unindent()
10493 );
10494 assert_eq!(
10495 editor.selections.ranges::<Point>(cx),
10496 [
10497 Point::new(0, 2)..Point::new(0, 2),
10498 Point::new(1, 2)..Point::new(1, 2),
10499 Point::new(2, 2)..Point::new(2, 2)
10500 ]
10501 );
10502
10503 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10504 assert_eq!(
10505 editor.text(cx),
10506 "
10507 a
10508 b
10509 c
10510 "
10511 .unindent()
10512 );
10513 assert_eq!(
10514 editor.selections.ranges::<Point>(cx),
10515 [
10516 Point::new(0, 1)..Point::new(0, 1),
10517 Point::new(1, 1)..Point::new(1, 1),
10518 Point::new(2, 1)..Point::new(2, 1)
10519 ]
10520 );
10521 });
10522}
10523
10524#[gpui::test]
10525async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10526 init_test(cx, |settings| {
10527 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10528 });
10529
10530 let mut cx = EditorTestContext::new(cx).await;
10531
10532 let language = Arc::new(Language::new(
10533 LanguageConfig {
10534 brackets: BracketPairConfig {
10535 pairs: vec![
10536 BracketPair {
10537 start: "{".to_string(),
10538 end: "}".to_string(),
10539 close: true,
10540 surround: true,
10541 newline: true,
10542 },
10543 BracketPair {
10544 start: "(".to_string(),
10545 end: ")".to_string(),
10546 close: true,
10547 surround: true,
10548 newline: true,
10549 },
10550 BracketPair {
10551 start: "[".to_string(),
10552 end: "]".to_string(),
10553 close: false,
10554 surround: true,
10555 newline: true,
10556 },
10557 ],
10558 ..Default::default()
10559 },
10560 autoclose_before: "})]".to_string(),
10561 ..Default::default()
10562 },
10563 Some(tree_sitter_rust::LANGUAGE.into()),
10564 ));
10565
10566 cx.language_registry().add(language.clone());
10567 cx.update_buffer(|buffer, cx| {
10568 buffer.set_language(Some(language), cx);
10569 });
10570
10571 cx.set_state(
10572 &"
10573 {(ˇ)}
10574 [[ˇ]]
10575 {(ˇ)}
10576 "
10577 .unindent(),
10578 );
10579
10580 cx.update_editor(|editor, window, cx| {
10581 editor.backspace(&Default::default(), window, cx);
10582 editor.backspace(&Default::default(), window, cx);
10583 });
10584
10585 cx.assert_editor_state(
10586 &"
10587 ˇ
10588 ˇ]]
10589 ˇ
10590 "
10591 .unindent(),
10592 );
10593
10594 cx.update_editor(|editor, window, cx| {
10595 editor.handle_input("{", window, cx);
10596 editor.handle_input("{", window, cx);
10597 editor.move_right(&MoveRight, window, cx);
10598 editor.move_right(&MoveRight, window, cx);
10599 editor.move_left(&MoveLeft, window, cx);
10600 editor.move_left(&MoveLeft, window, cx);
10601 editor.backspace(&Default::default(), window, cx);
10602 });
10603
10604 cx.assert_editor_state(
10605 &"
10606 {ˇ}
10607 {ˇ}]]
10608 {ˇ}
10609 "
10610 .unindent(),
10611 );
10612
10613 cx.update_editor(|editor, window, cx| {
10614 editor.backspace(&Default::default(), window, cx);
10615 });
10616
10617 cx.assert_editor_state(
10618 &"
10619 ˇ
10620 ˇ]]
10621 ˇ
10622 "
10623 .unindent(),
10624 );
10625}
10626
10627#[gpui::test]
10628async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10629 init_test(cx, |_| {});
10630
10631 let language = Arc::new(Language::new(
10632 LanguageConfig::default(),
10633 Some(tree_sitter_rust::LANGUAGE.into()),
10634 ));
10635
10636 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10637 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10638 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10639 editor
10640 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10641 .await;
10642
10643 editor.update_in(cx, |editor, window, cx| {
10644 editor.set_auto_replace_emoji_shortcode(true);
10645
10646 editor.handle_input("Hello ", window, cx);
10647 editor.handle_input(":wave", window, cx);
10648 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10649
10650 editor.handle_input(":", window, cx);
10651 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10652
10653 editor.handle_input(" :smile", window, cx);
10654 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10655
10656 editor.handle_input(":", window, cx);
10657 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10658
10659 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10660 editor.handle_input(":wave", window, cx);
10661 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10662
10663 editor.handle_input(":", window, cx);
10664 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10665
10666 editor.handle_input(":1", window, cx);
10667 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10668
10669 editor.handle_input(":", window, cx);
10670 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10671
10672 // Ensure shortcode does not get replaced when it is part of a word
10673 editor.handle_input(" Test:wave", window, cx);
10674 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10675
10676 editor.handle_input(":", window, cx);
10677 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10678
10679 editor.set_auto_replace_emoji_shortcode(false);
10680
10681 // Ensure shortcode does not get replaced when auto replace is off
10682 editor.handle_input(" :wave", window, cx);
10683 assert_eq!(
10684 editor.text(cx),
10685 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10686 );
10687
10688 editor.handle_input(":", window, cx);
10689 assert_eq!(
10690 editor.text(cx),
10691 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10692 );
10693 });
10694}
10695
10696#[gpui::test]
10697async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10698 init_test(cx, |_| {});
10699
10700 let (text, insertion_ranges) = marked_text_ranges(
10701 indoc! {"
10702 ˇ
10703 "},
10704 false,
10705 );
10706
10707 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10708 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10709
10710 _ = editor.update_in(cx, |editor, window, cx| {
10711 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10712
10713 editor
10714 .insert_snippet(&insertion_ranges, snippet, window, cx)
10715 .unwrap();
10716
10717 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10718 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10719 assert_eq!(editor.text(cx), expected_text);
10720 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10721 }
10722
10723 assert(
10724 editor,
10725 cx,
10726 indoc! {"
10727 type «» =•
10728 "},
10729 );
10730
10731 assert!(editor.context_menu_visible(), "There should be a matches");
10732 });
10733}
10734
10735#[gpui::test]
10736async fn test_snippets(cx: &mut TestAppContext) {
10737 init_test(cx, |_| {});
10738
10739 let mut cx = EditorTestContext::new(cx).await;
10740
10741 cx.set_state(indoc! {"
10742 a.ˇ b
10743 a.ˇ b
10744 a.ˇ b
10745 "});
10746
10747 cx.update_editor(|editor, window, cx| {
10748 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10749 let insertion_ranges = editor
10750 .selections
10751 .all(cx)
10752 .iter()
10753 .map(|s| s.range())
10754 .collect::<Vec<_>>();
10755 editor
10756 .insert_snippet(&insertion_ranges, snippet, window, cx)
10757 .unwrap();
10758 });
10759
10760 cx.assert_editor_state(indoc! {"
10761 a.f(«oneˇ», two, «threeˇ») b
10762 a.f(«oneˇ», two, «threeˇ») b
10763 a.f(«oneˇ», two, «threeˇ») b
10764 "});
10765
10766 // Can't move earlier than the first tab stop
10767 cx.update_editor(|editor, window, cx| {
10768 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10769 });
10770 cx.assert_editor_state(indoc! {"
10771 a.f(«oneˇ», two, «threeˇ») b
10772 a.f(«oneˇ», two, «threeˇ») b
10773 a.f(«oneˇ», two, «threeˇ») b
10774 "});
10775
10776 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10777 cx.assert_editor_state(indoc! {"
10778 a.f(one, «twoˇ», three) b
10779 a.f(one, «twoˇ», three) b
10780 a.f(one, «twoˇ», three) b
10781 "});
10782
10783 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10784 cx.assert_editor_state(indoc! {"
10785 a.f(«oneˇ», two, «threeˇ») b
10786 a.f(«oneˇ», two, «threeˇ») b
10787 a.f(«oneˇ», two, «threeˇ») b
10788 "});
10789
10790 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10791 cx.assert_editor_state(indoc! {"
10792 a.f(one, «twoˇ», three) b
10793 a.f(one, «twoˇ», three) b
10794 a.f(one, «twoˇ», three) b
10795 "});
10796 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10797 cx.assert_editor_state(indoc! {"
10798 a.f(one, two, three)ˇ b
10799 a.f(one, two, three)ˇ b
10800 a.f(one, two, three)ˇ b
10801 "});
10802
10803 // As soon as the last tab stop is reached, snippet state is gone
10804 cx.update_editor(|editor, window, cx| {
10805 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10806 });
10807 cx.assert_editor_state(indoc! {"
10808 a.f(one, two, three)ˇ b
10809 a.f(one, two, three)ˇ b
10810 a.f(one, two, three)ˇ b
10811 "});
10812}
10813
10814#[gpui::test]
10815async fn test_snippet_indentation(cx: &mut TestAppContext) {
10816 init_test(cx, |_| {});
10817
10818 let mut cx = EditorTestContext::new(cx).await;
10819
10820 cx.update_editor(|editor, window, cx| {
10821 let snippet = Snippet::parse(indoc! {"
10822 /*
10823 * Multiline comment with leading indentation
10824 *
10825 * $1
10826 */
10827 $0"})
10828 .unwrap();
10829 let insertion_ranges = editor
10830 .selections
10831 .all(cx)
10832 .iter()
10833 .map(|s| s.range())
10834 .collect::<Vec<_>>();
10835 editor
10836 .insert_snippet(&insertion_ranges, snippet, window, cx)
10837 .unwrap();
10838 });
10839
10840 cx.assert_editor_state(indoc! {"
10841 /*
10842 * Multiline comment with leading indentation
10843 *
10844 * ˇ
10845 */
10846 "});
10847
10848 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10849 cx.assert_editor_state(indoc! {"
10850 /*
10851 * Multiline comment with leading indentation
10852 *
10853 *•
10854 */
10855 ˇ"});
10856}
10857
10858#[gpui::test]
10859async fn test_document_format_during_save(cx: &mut TestAppContext) {
10860 init_test(cx, |_| {});
10861
10862 let fs = FakeFs::new(cx.executor());
10863 fs.insert_file(path!("/file.rs"), Default::default()).await;
10864
10865 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10866
10867 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10868 language_registry.add(rust_lang());
10869 let mut fake_servers = language_registry.register_fake_lsp(
10870 "Rust",
10871 FakeLspAdapter {
10872 capabilities: lsp::ServerCapabilities {
10873 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10874 ..Default::default()
10875 },
10876 ..Default::default()
10877 },
10878 );
10879
10880 let buffer = project
10881 .update(cx, |project, cx| {
10882 project.open_local_buffer(path!("/file.rs"), cx)
10883 })
10884 .await
10885 .unwrap();
10886
10887 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10888 let (editor, cx) = cx.add_window_view(|window, cx| {
10889 build_editor_with_project(project.clone(), buffer, window, cx)
10890 });
10891 editor.update_in(cx, |editor, window, cx| {
10892 editor.set_text("one\ntwo\nthree\n", window, cx)
10893 });
10894 assert!(cx.read(|cx| editor.is_dirty(cx)));
10895
10896 cx.executor().start_waiting();
10897 let fake_server = fake_servers.next().await.unwrap();
10898
10899 {
10900 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10901 move |params, _| async move {
10902 assert_eq!(
10903 params.text_document.uri,
10904 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10905 );
10906 assert_eq!(params.options.tab_size, 4);
10907 Ok(Some(vec![lsp::TextEdit::new(
10908 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10909 ", ".to_string(),
10910 )]))
10911 },
10912 );
10913 let save = editor
10914 .update_in(cx, |editor, window, cx| {
10915 editor.save(
10916 SaveOptions {
10917 format: true,
10918 autosave: false,
10919 },
10920 project.clone(),
10921 window,
10922 cx,
10923 )
10924 })
10925 .unwrap();
10926 cx.executor().start_waiting();
10927 save.await;
10928
10929 assert_eq!(
10930 editor.update(cx, |editor, cx| editor.text(cx)),
10931 "one, two\nthree\n"
10932 );
10933 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10934 }
10935
10936 {
10937 editor.update_in(cx, |editor, window, cx| {
10938 editor.set_text("one\ntwo\nthree\n", window, cx)
10939 });
10940 assert!(cx.read(|cx| editor.is_dirty(cx)));
10941
10942 // Ensure we can still save even if formatting hangs.
10943 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10944 move |params, _| async move {
10945 assert_eq!(
10946 params.text_document.uri,
10947 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10948 );
10949 futures::future::pending::<()>().await;
10950 unreachable!()
10951 },
10952 );
10953 let save = editor
10954 .update_in(cx, |editor, window, cx| {
10955 editor.save(
10956 SaveOptions {
10957 format: true,
10958 autosave: false,
10959 },
10960 project.clone(),
10961 window,
10962 cx,
10963 )
10964 })
10965 .unwrap();
10966 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10967 cx.executor().start_waiting();
10968 save.await;
10969 assert_eq!(
10970 editor.update(cx, |editor, cx| editor.text(cx)),
10971 "one\ntwo\nthree\n"
10972 );
10973 }
10974
10975 // Set rust language override and assert overridden tabsize is sent to language server
10976 update_test_language_settings(cx, |settings| {
10977 settings.languages.0.insert(
10978 "Rust".into(),
10979 LanguageSettingsContent {
10980 tab_size: NonZeroU32::new(8),
10981 ..Default::default()
10982 },
10983 );
10984 });
10985
10986 {
10987 editor.update_in(cx, |editor, window, cx| {
10988 editor.set_text("somehting_new\n", window, cx)
10989 });
10990 assert!(cx.read(|cx| editor.is_dirty(cx)));
10991 let _formatting_request_signal = fake_server
10992 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10993 assert_eq!(
10994 params.text_document.uri,
10995 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10996 );
10997 assert_eq!(params.options.tab_size, 8);
10998 Ok(Some(vec![]))
10999 });
11000 let save = editor
11001 .update_in(cx, |editor, window, cx| {
11002 editor.save(
11003 SaveOptions {
11004 format: true,
11005 autosave: false,
11006 },
11007 project.clone(),
11008 window,
11009 cx,
11010 )
11011 })
11012 .unwrap();
11013 cx.executor().start_waiting();
11014 save.await;
11015 }
11016}
11017
11018#[gpui::test]
11019async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11020 init_test(cx, |settings| {
11021 settings.defaults.ensure_final_newline_on_save = Some(false);
11022 });
11023
11024 let fs = FakeFs::new(cx.executor());
11025 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11026
11027 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11028
11029 let buffer = project
11030 .update(cx, |project, cx| {
11031 project.open_local_buffer(path!("/file.txt"), cx)
11032 })
11033 .await
11034 .unwrap();
11035
11036 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11037 let (editor, cx) = cx.add_window_view(|window, cx| {
11038 build_editor_with_project(project.clone(), buffer, window, cx)
11039 });
11040 editor.update_in(cx, |editor, window, cx| {
11041 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11042 s.select_ranges([0..0])
11043 });
11044 });
11045 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11046
11047 editor.update_in(cx, |editor, window, cx| {
11048 editor.handle_input("\n", window, cx)
11049 });
11050 cx.run_until_parked();
11051 save(&editor, &project, cx).await;
11052 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11053
11054 editor.update_in(cx, |editor, window, cx| {
11055 editor.undo(&Default::default(), window, cx);
11056 });
11057 save(&editor, &project, cx).await;
11058 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11059
11060 editor.update_in(cx, |editor, window, cx| {
11061 editor.redo(&Default::default(), window, cx);
11062 });
11063 cx.run_until_parked();
11064 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11065
11066 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11067 let save = editor
11068 .update_in(cx, |editor, window, cx| {
11069 editor.save(
11070 SaveOptions {
11071 format: true,
11072 autosave: false,
11073 },
11074 project.clone(),
11075 window,
11076 cx,
11077 )
11078 })
11079 .unwrap();
11080 cx.executor().start_waiting();
11081 save.await;
11082 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11083 }
11084}
11085
11086#[gpui::test]
11087async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11088 init_test(cx, |_| {});
11089
11090 let cols = 4;
11091 let rows = 10;
11092 let sample_text_1 = sample_text(rows, cols, 'a');
11093 assert_eq!(
11094 sample_text_1,
11095 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11096 );
11097 let sample_text_2 = sample_text(rows, cols, 'l');
11098 assert_eq!(
11099 sample_text_2,
11100 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11101 );
11102 let sample_text_3 = sample_text(rows, cols, 'v');
11103 assert_eq!(
11104 sample_text_3,
11105 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11106 );
11107
11108 let fs = FakeFs::new(cx.executor());
11109 fs.insert_tree(
11110 path!("/a"),
11111 json!({
11112 "main.rs": sample_text_1,
11113 "other.rs": sample_text_2,
11114 "lib.rs": sample_text_3,
11115 }),
11116 )
11117 .await;
11118
11119 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11120 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11121 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11122
11123 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11124 language_registry.add(rust_lang());
11125 let mut fake_servers = language_registry.register_fake_lsp(
11126 "Rust",
11127 FakeLspAdapter {
11128 capabilities: lsp::ServerCapabilities {
11129 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11130 ..Default::default()
11131 },
11132 ..Default::default()
11133 },
11134 );
11135
11136 let worktree = project.update(cx, |project, cx| {
11137 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11138 assert_eq!(worktrees.len(), 1);
11139 worktrees.pop().unwrap()
11140 });
11141 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11142
11143 let buffer_1 = project
11144 .update(cx, |project, cx| {
11145 project.open_buffer((worktree_id, "main.rs"), cx)
11146 })
11147 .await
11148 .unwrap();
11149 let buffer_2 = project
11150 .update(cx, |project, cx| {
11151 project.open_buffer((worktree_id, "other.rs"), cx)
11152 })
11153 .await
11154 .unwrap();
11155 let buffer_3 = project
11156 .update(cx, |project, cx| {
11157 project.open_buffer((worktree_id, "lib.rs"), cx)
11158 })
11159 .await
11160 .unwrap();
11161
11162 let multi_buffer = cx.new(|cx| {
11163 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11164 multi_buffer.push_excerpts(
11165 buffer_1.clone(),
11166 [
11167 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11168 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11169 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11170 ],
11171 cx,
11172 );
11173 multi_buffer.push_excerpts(
11174 buffer_2.clone(),
11175 [
11176 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11177 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11178 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11179 ],
11180 cx,
11181 );
11182 multi_buffer.push_excerpts(
11183 buffer_3.clone(),
11184 [
11185 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11186 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11187 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11188 ],
11189 cx,
11190 );
11191 multi_buffer
11192 });
11193 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11194 Editor::new(
11195 EditorMode::full(),
11196 multi_buffer,
11197 Some(project.clone()),
11198 window,
11199 cx,
11200 )
11201 });
11202
11203 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11204 editor.change_selections(
11205 SelectionEffects::scroll(Autoscroll::Next),
11206 window,
11207 cx,
11208 |s| s.select_ranges(Some(1..2)),
11209 );
11210 editor.insert("|one|two|three|", window, cx);
11211 });
11212 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11213 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11214 editor.change_selections(
11215 SelectionEffects::scroll(Autoscroll::Next),
11216 window,
11217 cx,
11218 |s| s.select_ranges(Some(60..70)),
11219 );
11220 editor.insert("|four|five|six|", window, cx);
11221 });
11222 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11223
11224 // First two buffers should be edited, but not the third one.
11225 assert_eq!(
11226 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11227 "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}",
11228 );
11229 buffer_1.update(cx, |buffer, _| {
11230 assert!(buffer.is_dirty());
11231 assert_eq!(
11232 buffer.text(),
11233 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11234 )
11235 });
11236 buffer_2.update(cx, |buffer, _| {
11237 assert!(buffer.is_dirty());
11238 assert_eq!(
11239 buffer.text(),
11240 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11241 )
11242 });
11243 buffer_3.update(cx, |buffer, _| {
11244 assert!(!buffer.is_dirty());
11245 assert_eq!(buffer.text(), sample_text_3,)
11246 });
11247 cx.executor().run_until_parked();
11248
11249 cx.executor().start_waiting();
11250 let save = multi_buffer_editor
11251 .update_in(cx, |editor, window, cx| {
11252 editor.save(
11253 SaveOptions {
11254 format: true,
11255 autosave: false,
11256 },
11257 project.clone(),
11258 window,
11259 cx,
11260 )
11261 })
11262 .unwrap();
11263
11264 let fake_server = fake_servers.next().await.unwrap();
11265 fake_server
11266 .server
11267 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11268 Ok(Some(vec![lsp::TextEdit::new(
11269 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11270 format!("[{} formatted]", params.text_document.uri),
11271 )]))
11272 })
11273 .detach();
11274 save.await;
11275
11276 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11277 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11278 assert_eq!(
11279 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11280 uri!(
11281 "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}"
11282 ),
11283 );
11284 buffer_1.update(cx, |buffer, _| {
11285 assert!(!buffer.is_dirty());
11286 assert_eq!(
11287 buffer.text(),
11288 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11289 )
11290 });
11291 buffer_2.update(cx, |buffer, _| {
11292 assert!(!buffer.is_dirty());
11293 assert_eq!(
11294 buffer.text(),
11295 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11296 )
11297 });
11298 buffer_3.update(cx, |buffer, _| {
11299 assert!(!buffer.is_dirty());
11300 assert_eq!(buffer.text(), sample_text_3,)
11301 });
11302}
11303
11304#[gpui::test]
11305async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11306 init_test(cx, |_| {});
11307
11308 let fs = FakeFs::new(cx.executor());
11309 fs.insert_tree(
11310 path!("/dir"),
11311 json!({
11312 "file1.rs": "fn main() { println!(\"hello\"); }",
11313 "file2.rs": "fn test() { println!(\"test\"); }",
11314 "file3.rs": "fn other() { println!(\"other\"); }\n",
11315 }),
11316 )
11317 .await;
11318
11319 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11320 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11321 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11322
11323 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11324 language_registry.add(rust_lang());
11325
11326 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11327 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11328
11329 // Open three buffers
11330 let buffer_1 = project
11331 .update(cx, |project, cx| {
11332 project.open_buffer((worktree_id, "file1.rs"), cx)
11333 })
11334 .await
11335 .unwrap();
11336 let buffer_2 = project
11337 .update(cx, |project, cx| {
11338 project.open_buffer((worktree_id, "file2.rs"), cx)
11339 })
11340 .await
11341 .unwrap();
11342 let buffer_3 = project
11343 .update(cx, |project, cx| {
11344 project.open_buffer((worktree_id, "file3.rs"), cx)
11345 })
11346 .await
11347 .unwrap();
11348
11349 // Create a multi-buffer with all three buffers
11350 let multi_buffer = cx.new(|cx| {
11351 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11352 multi_buffer.push_excerpts(
11353 buffer_1.clone(),
11354 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11355 cx,
11356 );
11357 multi_buffer.push_excerpts(
11358 buffer_2.clone(),
11359 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11360 cx,
11361 );
11362 multi_buffer.push_excerpts(
11363 buffer_3.clone(),
11364 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11365 cx,
11366 );
11367 multi_buffer
11368 });
11369
11370 let editor = cx.new_window_entity(|window, cx| {
11371 Editor::new(
11372 EditorMode::full(),
11373 multi_buffer,
11374 Some(project.clone()),
11375 window,
11376 cx,
11377 )
11378 });
11379
11380 // Edit only the first buffer
11381 editor.update_in(cx, |editor, window, cx| {
11382 editor.change_selections(
11383 SelectionEffects::scroll(Autoscroll::Next),
11384 window,
11385 cx,
11386 |s| s.select_ranges(Some(10..10)),
11387 );
11388 editor.insert("// edited", window, cx);
11389 });
11390
11391 // Verify that only buffer 1 is dirty
11392 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11393 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11394 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11395
11396 // Get write counts after file creation (files were created with initial content)
11397 // We expect each file to have been written once during creation
11398 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11399 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11400 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11401
11402 // Perform autosave
11403 let save_task = editor.update_in(cx, |editor, window, cx| {
11404 editor.save(
11405 SaveOptions {
11406 format: true,
11407 autosave: true,
11408 },
11409 project.clone(),
11410 window,
11411 cx,
11412 )
11413 });
11414 save_task.await.unwrap();
11415
11416 // Only the dirty buffer should have been saved
11417 assert_eq!(
11418 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11419 1,
11420 "Buffer 1 was dirty, so it should have been written once during autosave"
11421 );
11422 assert_eq!(
11423 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11424 0,
11425 "Buffer 2 was clean, so it should not have been written during autosave"
11426 );
11427 assert_eq!(
11428 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11429 0,
11430 "Buffer 3 was clean, so it should not have been written during autosave"
11431 );
11432
11433 // Verify buffer states after autosave
11434 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11435 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11436 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11437
11438 // Now perform a manual save (format = true)
11439 let save_task = editor.update_in(cx, |editor, window, cx| {
11440 editor.save(
11441 SaveOptions {
11442 format: true,
11443 autosave: false,
11444 },
11445 project.clone(),
11446 window,
11447 cx,
11448 )
11449 });
11450 save_task.await.unwrap();
11451
11452 // During manual save, clean buffers don't get written to disk
11453 // They just get did_save called for language server notifications
11454 assert_eq!(
11455 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11456 1,
11457 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11458 );
11459 assert_eq!(
11460 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11461 0,
11462 "Buffer 2 should not have been written at all"
11463 );
11464 assert_eq!(
11465 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11466 0,
11467 "Buffer 3 should not have been written at all"
11468 );
11469}
11470
11471async fn setup_range_format_test(
11472 cx: &mut TestAppContext,
11473) -> (
11474 Entity<Project>,
11475 Entity<Editor>,
11476 &mut gpui::VisualTestContext,
11477 lsp::FakeLanguageServer,
11478) {
11479 init_test(cx, |_| {});
11480
11481 let fs = FakeFs::new(cx.executor());
11482 fs.insert_file(path!("/file.rs"), Default::default()).await;
11483
11484 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11485
11486 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11487 language_registry.add(rust_lang());
11488 let mut fake_servers = language_registry.register_fake_lsp(
11489 "Rust",
11490 FakeLspAdapter {
11491 capabilities: lsp::ServerCapabilities {
11492 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11493 ..lsp::ServerCapabilities::default()
11494 },
11495 ..FakeLspAdapter::default()
11496 },
11497 );
11498
11499 let buffer = project
11500 .update(cx, |project, cx| {
11501 project.open_local_buffer(path!("/file.rs"), cx)
11502 })
11503 .await
11504 .unwrap();
11505
11506 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11507 let (editor, cx) = cx.add_window_view(|window, cx| {
11508 build_editor_with_project(project.clone(), buffer, window, cx)
11509 });
11510
11511 cx.executor().start_waiting();
11512 let fake_server = fake_servers.next().await.unwrap();
11513
11514 (project, editor, cx, fake_server)
11515}
11516
11517#[gpui::test]
11518async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11519 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11520
11521 editor.update_in(cx, |editor, window, cx| {
11522 editor.set_text("one\ntwo\nthree\n", window, cx)
11523 });
11524 assert!(cx.read(|cx| editor.is_dirty(cx)));
11525
11526 let save = editor
11527 .update_in(cx, |editor, window, cx| {
11528 editor.save(
11529 SaveOptions {
11530 format: true,
11531 autosave: false,
11532 },
11533 project.clone(),
11534 window,
11535 cx,
11536 )
11537 })
11538 .unwrap();
11539 fake_server
11540 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11541 assert_eq!(
11542 params.text_document.uri,
11543 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11544 );
11545 assert_eq!(params.options.tab_size, 4);
11546 Ok(Some(vec![lsp::TextEdit::new(
11547 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11548 ", ".to_string(),
11549 )]))
11550 })
11551 .next()
11552 .await;
11553 cx.executor().start_waiting();
11554 save.await;
11555 assert_eq!(
11556 editor.update(cx, |editor, cx| editor.text(cx)),
11557 "one, two\nthree\n"
11558 );
11559 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11560}
11561
11562#[gpui::test]
11563async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11564 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11565
11566 editor.update_in(cx, |editor, window, cx| {
11567 editor.set_text("one\ntwo\nthree\n", window, cx)
11568 });
11569 assert!(cx.read(|cx| editor.is_dirty(cx)));
11570
11571 // Test that save still works when formatting hangs
11572 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11573 move |params, _| async move {
11574 assert_eq!(
11575 params.text_document.uri,
11576 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11577 );
11578 futures::future::pending::<()>().await;
11579 unreachable!()
11580 },
11581 );
11582 let save = editor
11583 .update_in(cx, |editor, window, cx| {
11584 editor.save(
11585 SaveOptions {
11586 format: true,
11587 autosave: false,
11588 },
11589 project.clone(),
11590 window,
11591 cx,
11592 )
11593 })
11594 .unwrap();
11595 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11596 cx.executor().start_waiting();
11597 save.await;
11598 assert_eq!(
11599 editor.update(cx, |editor, cx| editor.text(cx)),
11600 "one\ntwo\nthree\n"
11601 );
11602 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11603}
11604
11605#[gpui::test]
11606async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11607 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11608
11609 // Buffer starts clean, no formatting should be requested
11610 let save = editor
11611 .update_in(cx, |editor, window, cx| {
11612 editor.save(
11613 SaveOptions {
11614 format: false,
11615 autosave: false,
11616 },
11617 project.clone(),
11618 window,
11619 cx,
11620 )
11621 })
11622 .unwrap();
11623 let _pending_format_request = fake_server
11624 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11625 panic!("Should not be invoked");
11626 })
11627 .next();
11628 cx.executor().start_waiting();
11629 save.await;
11630 cx.run_until_parked();
11631}
11632
11633#[gpui::test]
11634async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11635 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11636
11637 // Set Rust language override and assert overridden tabsize is sent to language server
11638 update_test_language_settings(cx, |settings| {
11639 settings.languages.0.insert(
11640 "Rust".into(),
11641 LanguageSettingsContent {
11642 tab_size: NonZeroU32::new(8),
11643 ..Default::default()
11644 },
11645 );
11646 });
11647
11648 editor.update_in(cx, |editor, window, cx| {
11649 editor.set_text("something_new\n", window, cx)
11650 });
11651 assert!(cx.read(|cx| editor.is_dirty(cx)));
11652 let save = editor
11653 .update_in(cx, |editor, window, cx| {
11654 editor.save(
11655 SaveOptions {
11656 format: true,
11657 autosave: false,
11658 },
11659 project.clone(),
11660 window,
11661 cx,
11662 )
11663 })
11664 .unwrap();
11665 fake_server
11666 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11667 assert_eq!(
11668 params.text_document.uri,
11669 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11670 );
11671 assert_eq!(params.options.tab_size, 8);
11672 Ok(Some(Vec::new()))
11673 })
11674 .next()
11675 .await;
11676 save.await;
11677}
11678
11679#[gpui::test]
11680async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11681 init_test(cx, |settings| {
11682 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11683 Formatter::LanguageServer { name: None },
11684 )))
11685 });
11686
11687 let fs = FakeFs::new(cx.executor());
11688 fs.insert_file(path!("/file.rs"), Default::default()).await;
11689
11690 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11691
11692 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11693 language_registry.add(Arc::new(Language::new(
11694 LanguageConfig {
11695 name: "Rust".into(),
11696 matcher: LanguageMatcher {
11697 path_suffixes: vec!["rs".to_string()],
11698 ..Default::default()
11699 },
11700 ..LanguageConfig::default()
11701 },
11702 Some(tree_sitter_rust::LANGUAGE.into()),
11703 )));
11704 update_test_language_settings(cx, |settings| {
11705 // Enable Prettier formatting for the same buffer, and ensure
11706 // LSP is called instead of Prettier.
11707 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11708 });
11709 let mut fake_servers = language_registry.register_fake_lsp(
11710 "Rust",
11711 FakeLspAdapter {
11712 capabilities: lsp::ServerCapabilities {
11713 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11714 ..Default::default()
11715 },
11716 ..Default::default()
11717 },
11718 );
11719
11720 let buffer = project
11721 .update(cx, |project, cx| {
11722 project.open_local_buffer(path!("/file.rs"), cx)
11723 })
11724 .await
11725 .unwrap();
11726
11727 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11728 let (editor, cx) = cx.add_window_view(|window, cx| {
11729 build_editor_with_project(project.clone(), buffer, window, cx)
11730 });
11731 editor.update_in(cx, |editor, window, cx| {
11732 editor.set_text("one\ntwo\nthree\n", window, cx)
11733 });
11734
11735 cx.executor().start_waiting();
11736 let fake_server = fake_servers.next().await.unwrap();
11737
11738 let format = editor
11739 .update_in(cx, |editor, window, cx| {
11740 editor.perform_format(
11741 project.clone(),
11742 FormatTrigger::Manual,
11743 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11744 window,
11745 cx,
11746 )
11747 })
11748 .unwrap();
11749 fake_server
11750 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11751 assert_eq!(
11752 params.text_document.uri,
11753 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11754 );
11755 assert_eq!(params.options.tab_size, 4);
11756 Ok(Some(vec![lsp::TextEdit::new(
11757 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11758 ", ".to_string(),
11759 )]))
11760 })
11761 .next()
11762 .await;
11763 cx.executor().start_waiting();
11764 format.await;
11765 assert_eq!(
11766 editor.update(cx, |editor, cx| editor.text(cx)),
11767 "one, two\nthree\n"
11768 );
11769
11770 editor.update_in(cx, |editor, window, cx| {
11771 editor.set_text("one\ntwo\nthree\n", window, cx)
11772 });
11773 // Ensure we don't lock if formatting hangs.
11774 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11775 move |params, _| async move {
11776 assert_eq!(
11777 params.text_document.uri,
11778 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11779 );
11780 futures::future::pending::<()>().await;
11781 unreachable!()
11782 },
11783 );
11784 let format = editor
11785 .update_in(cx, |editor, window, cx| {
11786 editor.perform_format(
11787 project,
11788 FormatTrigger::Manual,
11789 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11790 window,
11791 cx,
11792 )
11793 })
11794 .unwrap();
11795 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11796 cx.executor().start_waiting();
11797 format.await;
11798 assert_eq!(
11799 editor.update(cx, |editor, cx| editor.text(cx)),
11800 "one\ntwo\nthree\n"
11801 );
11802}
11803
11804#[gpui::test]
11805async fn test_multiple_formatters(cx: &mut TestAppContext) {
11806 init_test(cx, |settings| {
11807 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11808 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11809 Formatter::LanguageServer { name: None },
11810 Formatter::CodeActions(
11811 [
11812 ("code-action-1".into(), true),
11813 ("code-action-2".into(), true),
11814 ]
11815 .into_iter()
11816 .collect(),
11817 ),
11818 ])))
11819 });
11820
11821 let fs = FakeFs::new(cx.executor());
11822 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11823 .await;
11824
11825 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11826 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11827 language_registry.add(rust_lang());
11828
11829 let mut fake_servers = language_registry.register_fake_lsp(
11830 "Rust",
11831 FakeLspAdapter {
11832 capabilities: lsp::ServerCapabilities {
11833 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11834 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11835 commands: vec!["the-command-for-code-action-1".into()],
11836 ..Default::default()
11837 }),
11838 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11839 ..Default::default()
11840 },
11841 ..Default::default()
11842 },
11843 );
11844
11845 let buffer = project
11846 .update(cx, |project, cx| {
11847 project.open_local_buffer(path!("/file.rs"), cx)
11848 })
11849 .await
11850 .unwrap();
11851
11852 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11853 let (editor, cx) = cx.add_window_view(|window, cx| {
11854 build_editor_with_project(project.clone(), buffer, window, cx)
11855 });
11856
11857 cx.executor().start_waiting();
11858
11859 let fake_server = fake_servers.next().await.unwrap();
11860 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11861 move |_params, _| async move {
11862 Ok(Some(vec![lsp::TextEdit::new(
11863 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11864 "applied-formatting\n".to_string(),
11865 )]))
11866 },
11867 );
11868 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11869 move |params, _| async move {
11870 assert_eq!(
11871 params.context.only,
11872 Some(vec!["code-action-1".into(), "code-action-2".into()])
11873 );
11874 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11875 Ok(Some(vec![
11876 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11877 kind: Some("code-action-1".into()),
11878 edit: Some(lsp::WorkspaceEdit::new(
11879 [(
11880 uri.clone(),
11881 vec![lsp::TextEdit::new(
11882 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11883 "applied-code-action-1-edit\n".to_string(),
11884 )],
11885 )]
11886 .into_iter()
11887 .collect(),
11888 )),
11889 command: Some(lsp::Command {
11890 command: "the-command-for-code-action-1".into(),
11891 ..Default::default()
11892 }),
11893 ..Default::default()
11894 }),
11895 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11896 kind: Some("code-action-2".into()),
11897 edit: Some(lsp::WorkspaceEdit::new(
11898 [(
11899 uri,
11900 vec![lsp::TextEdit::new(
11901 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11902 "applied-code-action-2-edit\n".to_string(),
11903 )],
11904 )]
11905 .into_iter()
11906 .collect(),
11907 )),
11908 ..Default::default()
11909 }),
11910 ]))
11911 },
11912 );
11913
11914 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11915 move |params, _| async move { Ok(params) }
11916 });
11917
11918 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11919 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11920 let fake = fake_server.clone();
11921 let lock = command_lock.clone();
11922 move |params, _| {
11923 assert_eq!(params.command, "the-command-for-code-action-1");
11924 let fake = fake.clone();
11925 let lock = lock.clone();
11926 async move {
11927 lock.lock().await;
11928 fake.server
11929 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11930 label: None,
11931 edit: lsp::WorkspaceEdit {
11932 changes: Some(
11933 [(
11934 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11935 vec![lsp::TextEdit {
11936 range: lsp::Range::new(
11937 lsp::Position::new(0, 0),
11938 lsp::Position::new(0, 0),
11939 ),
11940 new_text: "applied-code-action-1-command\n".into(),
11941 }],
11942 )]
11943 .into_iter()
11944 .collect(),
11945 ),
11946 ..Default::default()
11947 },
11948 })
11949 .await
11950 .into_response()
11951 .unwrap();
11952 Ok(Some(json!(null)))
11953 }
11954 }
11955 });
11956
11957 cx.executor().start_waiting();
11958 editor
11959 .update_in(cx, |editor, window, cx| {
11960 editor.perform_format(
11961 project.clone(),
11962 FormatTrigger::Manual,
11963 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11964 window,
11965 cx,
11966 )
11967 })
11968 .unwrap()
11969 .await;
11970 editor.update(cx, |editor, cx| {
11971 assert_eq!(
11972 editor.text(cx),
11973 r#"
11974 applied-code-action-2-edit
11975 applied-code-action-1-command
11976 applied-code-action-1-edit
11977 applied-formatting
11978 one
11979 two
11980 three
11981 "#
11982 .unindent()
11983 );
11984 });
11985
11986 editor.update_in(cx, |editor, window, cx| {
11987 editor.undo(&Default::default(), window, cx);
11988 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11989 });
11990
11991 // Perform a manual edit while waiting for an LSP command
11992 // that's being run as part of a formatting code action.
11993 let lock_guard = command_lock.lock().await;
11994 let format = editor
11995 .update_in(cx, |editor, window, cx| {
11996 editor.perform_format(
11997 project.clone(),
11998 FormatTrigger::Manual,
11999 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12000 window,
12001 cx,
12002 )
12003 })
12004 .unwrap();
12005 cx.run_until_parked();
12006 editor.update(cx, |editor, cx| {
12007 assert_eq!(
12008 editor.text(cx),
12009 r#"
12010 applied-code-action-1-edit
12011 applied-formatting
12012 one
12013 two
12014 three
12015 "#
12016 .unindent()
12017 );
12018
12019 editor.buffer.update(cx, |buffer, cx| {
12020 let ix = buffer.len(cx);
12021 buffer.edit([(ix..ix, "edited\n")], None, cx);
12022 });
12023 });
12024
12025 // Allow the LSP command to proceed. Because the buffer was edited,
12026 // the second code action will not be run.
12027 drop(lock_guard);
12028 format.await;
12029 editor.update_in(cx, |editor, window, cx| {
12030 assert_eq!(
12031 editor.text(cx),
12032 r#"
12033 applied-code-action-1-command
12034 applied-code-action-1-edit
12035 applied-formatting
12036 one
12037 two
12038 three
12039 edited
12040 "#
12041 .unindent()
12042 );
12043
12044 // The manual edit is undone first, because it is the last thing the user did
12045 // (even though the command completed afterwards).
12046 editor.undo(&Default::default(), window, cx);
12047 assert_eq!(
12048 editor.text(cx),
12049 r#"
12050 applied-code-action-1-command
12051 applied-code-action-1-edit
12052 applied-formatting
12053 one
12054 two
12055 three
12056 "#
12057 .unindent()
12058 );
12059
12060 // All the formatting (including the command, which completed after the manual edit)
12061 // is undone together.
12062 editor.undo(&Default::default(), window, cx);
12063 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12064 });
12065}
12066
12067#[gpui::test]
12068async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12069 init_test(cx, |settings| {
12070 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12071 Formatter::LanguageServer { name: None },
12072 ])))
12073 });
12074
12075 let fs = FakeFs::new(cx.executor());
12076 fs.insert_file(path!("/file.ts"), Default::default()).await;
12077
12078 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12079
12080 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12081 language_registry.add(Arc::new(Language::new(
12082 LanguageConfig {
12083 name: "TypeScript".into(),
12084 matcher: LanguageMatcher {
12085 path_suffixes: vec!["ts".to_string()],
12086 ..Default::default()
12087 },
12088 ..LanguageConfig::default()
12089 },
12090 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12091 )));
12092 update_test_language_settings(cx, |settings| {
12093 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12094 });
12095 let mut fake_servers = language_registry.register_fake_lsp(
12096 "TypeScript",
12097 FakeLspAdapter {
12098 capabilities: lsp::ServerCapabilities {
12099 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12100 ..Default::default()
12101 },
12102 ..Default::default()
12103 },
12104 );
12105
12106 let buffer = project
12107 .update(cx, |project, cx| {
12108 project.open_local_buffer(path!("/file.ts"), cx)
12109 })
12110 .await
12111 .unwrap();
12112
12113 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12114 let (editor, cx) = cx.add_window_view(|window, cx| {
12115 build_editor_with_project(project.clone(), buffer, window, cx)
12116 });
12117 editor.update_in(cx, |editor, window, cx| {
12118 editor.set_text(
12119 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12120 window,
12121 cx,
12122 )
12123 });
12124
12125 cx.executor().start_waiting();
12126 let fake_server = fake_servers.next().await.unwrap();
12127
12128 let format = editor
12129 .update_in(cx, |editor, window, cx| {
12130 editor.perform_code_action_kind(
12131 project.clone(),
12132 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12133 window,
12134 cx,
12135 )
12136 })
12137 .unwrap();
12138 fake_server
12139 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12140 assert_eq!(
12141 params.text_document.uri,
12142 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12143 );
12144 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12145 lsp::CodeAction {
12146 title: "Organize Imports".to_string(),
12147 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12148 edit: Some(lsp::WorkspaceEdit {
12149 changes: Some(
12150 [(
12151 params.text_document.uri.clone(),
12152 vec![lsp::TextEdit::new(
12153 lsp::Range::new(
12154 lsp::Position::new(1, 0),
12155 lsp::Position::new(2, 0),
12156 ),
12157 "".to_string(),
12158 )],
12159 )]
12160 .into_iter()
12161 .collect(),
12162 ),
12163 ..Default::default()
12164 }),
12165 ..Default::default()
12166 },
12167 )]))
12168 })
12169 .next()
12170 .await;
12171 cx.executor().start_waiting();
12172 format.await;
12173 assert_eq!(
12174 editor.update(cx, |editor, cx| editor.text(cx)),
12175 "import { a } from 'module';\n\nconst x = a;\n"
12176 );
12177
12178 editor.update_in(cx, |editor, window, cx| {
12179 editor.set_text(
12180 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12181 window,
12182 cx,
12183 )
12184 });
12185 // Ensure we don't lock if code action hangs.
12186 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12187 move |params, _| async move {
12188 assert_eq!(
12189 params.text_document.uri,
12190 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12191 );
12192 futures::future::pending::<()>().await;
12193 unreachable!()
12194 },
12195 );
12196 let format = editor
12197 .update_in(cx, |editor, window, cx| {
12198 editor.perform_code_action_kind(
12199 project,
12200 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12201 window,
12202 cx,
12203 )
12204 })
12205 .unwrap();
12206 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12207 cx.executor().start_waiting();
12208 format.await;
12209 assert_eq!(
12210 editor.update(cx, |editor, cx| editor.text(cx)),
12211 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12212 );
12213}
12214
12215#[gpui::test]
12216async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12217 init_test(cx, |_| {});
12218
12219 let mut cx = EditorLspTestContext::new_rust(
12220 lsp::ServerCapabilities {
12221 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12222 ..Default::default()
12223 },
12224 cx,
12225 )
12226 .await;
12227
12228 cx.set_state(indoc! {"
12229 one.twoˇ
12230 "});
12231
12232 // The format request takes a long time. When it completes, it inserts
12233 // a newline and an indent before the `.`
12234 cx.lsp
12235 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12236 let executor = cx.background_executor().clone();
12237 async move {
12238 executor.timer(Duration::from_millis(100)).await;
12239 Ok(Some(vec![lsp::TextEdit {
12240 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12241 new_text: "\n ".into(),
12242 }]))
12243 }
12244 });
12245
12246 // Submit a format request.
12247 let format_1 = cx
12248 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12249 .unwrap();
12250 cx.executor().run_until_parked();
12251
12252 // Submit a second format request.
12253 let format_2 = cx
12254 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12255 .unwrap();
12256 cx.executor().run_until_parked();
12257
12258 // Wait for both format requests to complete
12259 cx.executor().advance_clock(Duration::from_millis(200));
12260 cx.executor().start_waiting();
12261 format_1.await.unwrap();
12262 cx.executor().start_waiting();
12263 format_2.await.unwrap();
12264
12265 // The formatting edits only happens once.
12266 cx.assert_editor_state(indoc! {"
12267 one
12268 .twoˇ
12269 "});
12270}
12271
12272#[gpui::test]
12273async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12274 init_test(cx, |settings| {
12275 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12276 });
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 // Set up a buffer white some trailing whitespace and no trailing newline.
12288 cx.set_state(
12289 &[
12290 "one ", //
12291 "twoˇ", //
12292 "three ", //
12293 "four", //
12294 ]
12295 .join("\n"),
12296 );
12297
12298 // Submit a format request.
12299 let format = cx
12300 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12301 .unwrap();
12302
12303 // Record which buffer changes have been sent to the language server
12304 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12305 cx.lsp
12306 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12307 let buffer_changes = buffer_changes.clone();
12308 move |params, _| {
12309 buffer_changes.lock().extend(
12310 params
12311 .content_changes
12312 .into_iter()
12313 .map(|e| (e.range.unwrap(), e.text)),
12314 );
12315 }
12316 });
12317
12318 // Handle formatting requests to the language server.
12319 cx.lsp
12320 .set_request_handler::<lsp::request::Formatting, _, _>({
12321 let buffer_changes = buffer_changes.clone();
12322 move |_, _| {
12323 // When formatting is requested, trailing whitespace has already been stripped,
12324 // and the trailing newline has already been added.
12325 assert_eq!(
12326 &buffer_changes.lock()[1..],
12327 &[
12328 (
12329 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12330 "".into()
12331 ),
12332 (
12333 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12334 "".into()
12335 ),
12336 (
12337 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12338 "\n".into()
12339 ),
12340 ]
12341 );
12342
12343 // Insert blank lines between each line of the buffer.
12344 async move {
12345 Ok(Some(vec![
12346 lsp::TextEdit {
12347 range: lsp::Range::new(
12348 lsp::Position::new(1, 0),
12349 lsp::Position::new(1, 0),
12350 ),
12351 new_text: "\n".into(),
12352 },
12353 lsp::TextEdit {
12354 range: lsp::Range::new(
12355 lsp::Position::new(2, 0),
12356 lsp::Position::new(2, 0),
12357 ),
12358 new_text: "\n".into(),
12359 },
12360 ]))
12361 }
12362 }
12363 });
12364
12365 // After formatting the buffer, the trailing whitespace is stripped,
12366 // a newline is appended, and the edits provided by the language server
12367 // have been applied.
12368 format.await.unwrap();
12369 cx.assert_editor_state(
12370 &[
12371 "one", //
12372 "", //
12373 "twoˇ", //
12374 "", //
12375 "three", //
12376 "four", //
12377 "", //
12378 ]
12379 .join("\n"),
12380 );
12381
12382 // Undoing the formatting undoes the trailing whitespace removal, the
12383 // trailing newline, and the LSP edits.
12384 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12385 cx.assert_editor_state(
12386 &[
12387 "one ", //
12388 "twoˇ", //
12389 "three ", //
12390 "four", //
12391 ]
12392 .join("\n"),
12393 );
12394}
12395
12396#[gpui::test]
12397async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12398 cx: &mut TestAppContext,
12399) {
12400 init_test(cx, |_| {});
12401
12402 cx.update(|cx| {
12403 cx.update_global::<SettingsStore, _>(|settings, cx| {
12404 settings.update_user_settings(cx, |settings| {
12405 settings.editor.auto_signature_help = Some(true);
12406 });
12407 });
12408 });
12409
12410 let mut cx = EditorLspTestContext::new_rust(
12411 lsp::ServerCapabilities {
12412 signature_help_provider: Some(lsp::SignatureHelpOptions {
12413 ..Default::default()
12414 }),
12415 ..Default::default()
12416 },
12417 cx,
12418 )
12419 .await;
12420
12421 let language = Language::new(
12422 LanguageConfig {
12423 name: "Rust".into(),
12424 brackets: BracketPairConfig {
12425 pairs: vec![
12426 BracketPair {
12427 start: "{".to_string(),
12428 end: "}".to_string(),
12429 close: true,
12430 surround: true,
12431 newline: true,
12432 },
12433 BracketPair {
12434 start: "(".to_string(),
12435 end: ")".to_string(),
12436 close: true,
12437 surround: true,
12438 newline: true,
12439 },
12440 BracketPair {
12441 start: "/*".to_string(),
12442 end: " */".to_string(),
12443 close: true,
12444 surround: true,
12445 newline: true,
12446 },
12447 BracketPair {
12448 start: "[".to_string(),
12449 end: "]".to_string(),
12450 close: false,
12451 surround: false,
12452 newline: true,
12453 },
12454 BracketPair {
12455 start: "\"".to_string(),
12456 end: "\"".to_string(),
12457 close: true,
12458 surround: true,
12459 newline: false,
12460 },
12461 BracketPair {
12462 start: "<".to_string(),
12463 end: ">".to_string(),
12464 close: false,
12465 surround: true,
12466 newline: true,
12467 },
12468 ],
12469 ..Default::default()
12470 },
12471 autoclose_before: "})]".to_string(),
12472 ..Default::default()
12473 },
12474 Some(tree_sitter_rust::LANGUAGE.into()),
12475 );
12476 let language = Arc::new(language);
12477
12478 cx.language_registry().add(language.clone());
12479 cx.update_buffer(|buffer, cx| {
12480 buffer.set_language(Some(language), cx);
12481 });
12482
12483 cx.set_state(
12484 &r#"
12485 fn main() {
12486 sampleˇ
12487 }
12488 "#
12489 .unindent(),
12490 );
12491
12492 cx.update_editor(|editor, window, cx| {
12493 editor.handle_input("(", window, cx);
12494 });
12495 cx.assert_editor_state(
12496 &"
12497 fn main() {
12498 sample(ˇ)
12499 }
12500 "
12501 .unindent(),
12502 );
12503
12504 let mocked_response = lsp::SignatureHelp {
12505 signatures: vec![lsp::SignatureInformation {
12506 label: "fn sample(param1: u8, param2: u8)".to_string(),
12507 documentation: None,
12508 parameters: Some(vec![
12509 lsp::ParameterInformation {
12510 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12511 documentation: None,
12512 },
12513 lsp::ParameterInformation {
12514 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12515 documentation: None,
12516 },
12517 ]),
12518 active_parameter: None,
12519 }],
12520 active_signature: Some(0),
12521 active_parameter: Some(0),
12522 };
12523 handle_signature_help_request(&mut cx, mocked_response).await;
12524
12525 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12526 .await;
12527
12528 cx.editor(|editor, _, _| {
12529 let signature_help_state = editor.signature_help_state.popover().cloned();
12530 let signature = signature_help_state.unwrap();
12531 assert_eq!(
12532 signature.signatures[signature.current_signature].label,
12533 "fn sample(param1: u8, param2: u8)"
12534 );
12535 });
12536}
12537
12538#[gpui::test]
12539async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12540 init_test(cx, |_| {});
12541
12542 cx.update(|cx| {
12543 cx.update_global::<SettingsStore, _>(|settings, cx| {
12544 settings.update_user_settings(cx, |settings| {
12545 settings.editor.auto_signature_help = Some(false);
12546 settings.editor.show_signature_help_after_edits = Some(false);
12547 });
12548 });
12549 });
12550
12551 let mut cx = EditorLspTestContext::new_rust(
12552 lsp::ServerCapabilities {
12553 signature_help_provider: Some(lsp::SignatureHelpOptions {
12554 ..Default::default()
12555 }),
12556 ..Default::default()
12557 },
12558 cx,
12559 )
12560 .await;
12561
12562 let language = Language::new(
12563 LanguageConfig {
12564 name: "Rust".into(),
12565 brackets: BracketPairConfig {
12566 pairs: vec![
12567 BracketPair {
12568 start: "{".to_string(),
12569 end: "}".to_string(),
12570 close: true,
12571 surround: true,
12572 newline: true,
12573 },
12574 BracketPair {
12575 start: "(".to_string(),
12576 end: ")".to_string(),
12577 close: true,
12578 surround: true,
12579 newline: true,
12580 },
12581 BracketPair {
12582 start: "/*".to_string(),
12583 end: " */".to_string(),
12584 close: true,
12585 surround: true,
12586 newline: true,
12587 },
12588 BracketPair {
12589 start: "[".to_string(),
12590 end: "]".to_string(),
12591 close: false,
12592 surround: false,
12593 newline: true,
12594 },
12595 BracketPair {
12596 start: "\"".to_string(),
12597 end: "\"".to_string(),
12598 close: true,
12599 surround: true,
12600 newline: false,
12601 },
12602 BracketPair {
12603 start: "<".to_string(),
12604 end: ">".to_string(),
12605 close: false,
12606 surround: true,
12607 newline: true,
12608 },
12609 ],
12610 ..Default::default()
12611 },
12612 autoclose_before: "})]".to_string(),
12613 ..Default::default()
12614 },
12615 Some(tree_sitter_rust::LANGUAGE.into()),
12616 );
12617 let language = Arc::new(language);
12618
12619 cx.language_registry().add(language.clone());
12620 cx.update_buffer(|buffer, cx| {
12621 buffer.set_language(Some(language), cx);
12622 });
12623
12624 // Ensure that signature_help is not called when no signature help is enabled.
12625 cx.set_state(
12626 &r#"
12627 fn main() {
12628 sampleˇ
12629 }
12630 "#
12631 .unindent(),
12632 );
12633 cx.update_editor(|editor, window, cx| {
12634 editor.handle_input("(", window, cx);
12635 });
12636 cx.assert_editor_state(
12637 &"
12638 fn main() {
12639 sample(ˇ)
12640 }
12641 "
12642 .unindent(),
12643 );
12644 cx.editor(|editor, _, _| {
12645 assert!(editor.signature_help_state.task().is_none());
12646 });
12647
12648 let mocked_response = lsp::SignatureHelp {
12649 signatures: vec![lsp::SignatureInformation {
12650 label: "fn sample(param1: u8, param2: u8)".to_string(),
12651 documentation: None,
12652 parameters: Some(vec![
12653 lsp::ParameterInformation {
12654 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12655 documentation: None,
12656 },
12657 lsp::ParameterInformation {
12658 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12659 documentation: None,
12660 },
12661 ]),
12662 active_parameter: None,
12663 }],
12664 active_signature: Some(0),
12665 active_parameter: Some(0),
12666 };
12667
12668 // Ensure that signature_help is called when enabled afte edits
12669 cx.update(|_, cx| {
12670 cx.update_global::<SettingsStore, _>(|settings, cx| {
12671 settings.update_user_settings(cx, |settings| {
12672 settings.editor.auto_signature_help = Some(false);
12673 settings.editor.show_signature_help_after_edits = Some(true);
12674 });
12675 });
12676 });
12677 cx.set_state(
12678 &r#"
12679 fn main() {
12680 sampleˇ
12681 }
12682 "#
12683 .unindent(),
12684 );
12685 cx.update_editor(|editor, window, cx| {
12686 editor.handle_input("(", window, cx);
12687 });
12688 cx.assert_editor_state(
12689 &"
12690 fn main() {
12691 sample(ˇ)
12692 }
12693 "
12694 .unindent(),
12695 );
12696 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12697 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12698 .await;
12699 cx.update_editor(|editor, _, _| {
12700 let signature_help_state = editor.signature_help_state.popover().cloned();
12701 assert!(signature_help_state.is_some());
12702 let signature = signature_help_state.unwrap();
12703 assert_eq!(
12704 signature.signatures[signature.current_signature].label,
12705 "fn sample(param1: u8, param2: u8)"
12706 );
12707 editor.signature_help_state = SignatureHelpState::default();
12708 });
12709
12710 // Ensure that signature_help is called when auto signature help override is enabled
12711 cx.update(|_, cx| {
12712 cx.update_global::<SettingsStore, _>(|settings, cx| {
12713 settings.update_user_settings(cx, |settings| {
12714 settings.editor.auto_signature_help = Some(true);
12715 settings.editor.show_signature_help_after_edits = Some(false);
12716 });
12717 });
12718 });
12719 cx.set_state(
12720 &r#"
12721 fn main() {
12722 sampleˇ
12723 }
12724 "#
12725 .unindent(),
12726 );
12727 cx.update_editor(|editor, window, cx| {
12728 editor.handle_input("(", window, cx);
12729 });
12730 cx.assert_editor_state(
12731 &"
12732 fn main() {
12733 sample(ˇ)
12734 }
12735 "
12736 .unindent(),
12737 );
12738 handle_signature_help_request(&mut cx, mocked_response).await;
12739 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12740 .await;
12741 cx.editor(|editor, _, _| {
12742 let signature_help_state = editor.signature_help_state.popover().cloned();
12743 assert!(signature_help_state.is_some());
12744 let signature = signature_help_state.unwrap();
12745 assert_eq!(
12746 signature.signatures[signature.current_signature].label,
12747 "fn sample(param1: u8, param2: u8)"
12748 );
12749 });
12750}
12751
12752#[gpui::test]
12753async fn test_signature_help(cx: &mut TestAppContext) {
12754 init_test(cx, |_| {});
12755 cx.update(|cx| {
12756 cx.update_global::<SettingsStore, _>(|settings, cx| {
12757 settings.update_user_settings(cx, |settings| {
12758 settings.editor.auto_signature_help = Some(true);
12759 });
12760 });
12761 });
12762
12763 let mut cx = EditorLspTestContext::new_rust(
12764 lsp::ServerCapabilities {
12765 signature_help_provider: Some(lsp::SignatureHelpOptions {
12766 ..Default::default()
12767 }),
12768 ..Default::default()
12769 },
12770 cx,
12771 )
12772 .await;
12773
12774 // A test that directly calls `show_signature_help`
12775 cx.update_editor(|editor, window, cx| {
12776 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12777 });
12778
12779 let mocked_response = lsp::SignatureHelp {
12780 signatures: vec![lsp::SignatureInformation {
12781 label: "fn sample(param1: u8, param2: u8)".to_string(),
12782 documentation: None,
12783 parameters: Some(vec![
12784 lsp::ParameterInformation {
12785 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12786 documentation: None,
12787 },
12788 lsp::ParameterInformation {
12789 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12790 documentation: None,
12791 },
12792 ]),
12793 active_parameter: None,
12794 }],
12795 active_signature: Some(0),
12796 active_parameter: Some(0),
12797 };
12798 handle_signature_help_request(&mut cx, mocked_response).await;
12799
12800 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12801 .await;
12802
12803 cx.editor(|editor, _, _| {
12804 let signature_help_state = editor.signature_help_state.popover().cloned();
12805 assert!(signature_help_state.is_some());
12806 let signature = signature_help_state.unwrap();
12807 assert_eq!(
12808 signature.signatures[signature.current_signature].label,
12809 "fn sample(param1: u8, param2: u8)"
12810 );
12811 });
12812
12813 // When exiting outside from inside the brackets, `signature_help` is closed.
12814 cx.set_state(indoc! {"
12815 fn main() {
12816 sample(ˇ);
12817 }
12818
12819 fn sample(param1: u8, param2: u8) {}
12820 "});
12821
12822 cx.update_editor(|editor, window, cx| {
12823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12824 s.select_ranges([0..0])
12825 });
12826 });
12827
12828 let mocked_response = lsp::SignatureHelp {
12829 signatures: Vec::new(),
12830 active_signature: None,
12831 active_parameter: None,
12832 };
12833 handle_signature_help_request(&mut cx, mocked_response).await;
12834
12835 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12836 .await;
12837
12838 cx.editor(|editor, _, _| {
12839 assert!(!editor.signature_help_state.is_shown());
12840 });
12841
12842 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12843 cx.set_state(indoc! {"
12844 fn main() {
12845 sample(ˇ);
12846 }
12847
12848 fn sample(param1: u8, param2: u8) {}
12849 "});
12850
12851 let mocked_response = lsp::SignatureHelp {
12852 signatures: vec![lsp::SignatureInformation {
12853 label: "fn sample(param1: u8, param2: u8)".to_string(),
12854 documentation: None,
12855 parameters: Some(vec![
12856 lsp::ParameterInformation {
12857 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12858 documentation: None,
12859 },
12860 lsp::ParameterInformation {
12861 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12862 documentation: None,
12863 },
12864 ]),
12865 active_parameter: None,
12866 }],
12867 active_signature: Some(0),
12868 active_parameter: Some(0),
12869 };
12870 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12871 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12872 .await;
12873 cx.editor(|editor, _, _| {
12874 assert!(editor.signature_help_state.is_shown());
12875 });
12876
12877 // Restore the popover with more parameter input
12878 cx.set_state(indoc! {"
12879 fn main() {
12880 sample(param1, param2ˇ);
12881 }
12882
12883 fn sample(param1: u8, param2: u8) {}
12884 "});
12885
12886 let mocked_response = lsp::SignatureHelp {
12887 signatures: vec![lsp::SignatureInformation {
12888 label: "fn sample(param1: u8, param2: u8)".to_string(),
12889 documentation: None,
12890 parameters: Some(vec![
12891 lsp::ParameterInformation {
12892 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12893 documentation: None,
12894 },
12895 lsp::ParameterInformation {
12896 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12897 documentation: None,
12898 },
12899 ]),
12900 active_parameter: None,
12901 }],
12902 active_signature: Some(0),
12903 active_parameter: Some(1),
12904 };
12905 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12906 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12907 .await;
12908
12909 // When selecting a range, the popover is gone.
12910 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12911 cx.update_editor(|editor, window, cx| {
12912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12913 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12914 })
12915 });
12916 cx.assert_editor_state(indoc! {"
12917 fn main() {
12918 sample(param1, «ˇparam2»);
12919 }
12920
12921 fn sample(param1: u8, param2: u8) {}
12922 "});
12923 cx.editor(|editor, _, _| {
12924 assert!(!editor.signature_help_state.is_shown());
12925 });
12926
12927 // When unselecting again, the popover is back if within the brackets.
12928 cx.update_editor(|editor, window, cx| {
12929 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12930 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12931 })
12932 });
12933 cx.assert_editor_state(indoc! {"
12934 fn main() {
12935 sample(param1, ˇparam2);
12936 }
12937
12938 fn sample(param1: u8, param2: u8) {}
12939 "});
12940 handle_signature_help_request(&mut cx, mocked_response).await;
12941 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12942 .await;
12943 cx.editor(|editor, _, _| {
12944 assert!(editor.signature_help_state.is_shown());
12945 });
12946
12947 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12948 cx.update_editor(|editor, window, cx| {
12949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12950 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12951 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12952 })
12953 });
12954 cx.assert_editor_state(indoc! {"
12955 fn main() {
12956 sample(param1, ˇparam2);
12957 }
12958
12959 fn sample(param1: u8, param2: u8) {}
12960 "});
12961
12962 let mocked_response = lsp::SignatureHelp {
12963 signatures: vec![lsp::SignatureInformation {
12964 label: "fn sample(param1: u8, param2: u8)".to_string(),
12965 documentation: None,
12966 parameters: Some(vec![
12967 lsp::ParameterInformation {
12968 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12969 documentation: None,
12970 },
12971 lsp::ParameterInformation {
12972 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12973 documentation: None,
12974 },
12975 ]),
12976 active_parameter: None,
12977 }],
12978 active_signature: Some(0),
12979 active_parameter: Some(1),
12980 };
12981 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12982 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12983 .await;
12984 cx.update_editor(|editor, _, cx| {
12985 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12986 });
12987 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12988 .await;
12989 cx.update_editor(|editor, window, cx| {
12990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12991 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12992 })
12993 });
12994 cx.assert_editor_state(indoc! {"
12995 fn main() {
12996 sample(param1, «ˇparam2»);
12997 }
12998
12999 fn sample(param1: u8, param2: u8) {}
13000 "});
13001 cx.update_editor(|editor, window, cx| {
13002 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13003 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13004 })
13005 });
13006 cx.assert_editor_state(indoc! {"
13007 fn main() {
13008 sample(param1, ˇparam2);
13009 }
13010
13011 fn sample(param1: u8, param2: u8) {}
13012 "});
13013 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13014 .await;
13015}
13016
13017#[gpui::test]
13018async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13019 init_test(cx, |_| {});
13020
13021 let mut cx = EditorLspTestContext::new_rust(
13022 lsp::ServerCapabilities {
13023 signature_help_provider: Some(lsp::SignatureHelpOptions {
13024 ..Default::default()
13025 }),
13026 ..Default::default()
13027 },
13028 cx,
13029 )
13030 .await;
13031
13032 cx.set_state(indoc! {"
13033 fn main() {
13034 overloadedˇ
13035 }
13036 "});
13037
13038 cx.update_editor(|editor, window, cx| {
13039 editor.handle_input("(", window, cx);
13040 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13041 });
13042
13043 // Mock response with 3 signatures
13044 let mocked_response = lsp::SignatureHelp {
13045 signatures: vec![
13046 lsp::SignatureInformation {
13047 label: "fn overloaded(x: i32)".to_string(),
13048 documentation: None,
13049 parameters: Some(vec![lsp::ParameterInformation {
13050 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13051 documentation: None,
13052 }]),
13053 active_parameter: None,
13054 },
13055 lsp::SignatureInformation {
13056 label: "fn overloaded(x: i32, y: i32)".to_string(),
13057 documentation: None,
13058 parameters: Some(vec![
13059 lsp::ParameterInformation {
13060 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13061 documentation: None,
13062 },
13063 lsp::ParameterInformation {
13064 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13065 documentation: None,
13066 },
13067 ]),
13068 active_parameter: None,
13069 },
13070 lsp::SignatureInformation {
13071 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13072 documentation: None,
13073 parameters: Some(vec![
13074 lsp::ParameterInformation {
13075 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13076 documentation: None,
13077 },
13078 lsp::ParameterInformation {
13079 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13080 documentation: None,
13081 },
13082 lsp::ParameterInformation {
13083 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13084 documentation: None,
13085 },
13086 ]),
13087 active_parameter: None,
13088 },
13089 ],
13090 active_signature: Some(1),
13091 active_parameter: Some(0),
13092 };
13093 handle_signature_help_request(&mut cx, mocked_response).await;
13094
13095 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13096 .await;
13097
13098 // Verify we have multiple signatures and the right one is selected
13099 cx.editor(|editor, _, _| {
13100 let popover = editor.signature_help_state.popover().cloned().unwrap();
13101 assert_eq!(popover.signatures.len(), 3);
13102 // active_signature was 1, so that should be the current
13103 assert_eq!(popover.current_signature, 1);
13104 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13105 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13106 assert_eq!(
13107 popover.signatures[2].label,
13108 "fn overloaded(x: i32, y: i32, z: i32)"
13109 );
13110 });
13111
13112 // Test navigation functionality
13113 cx.update_editor(|editor, window, cx| {
13114 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13115 });
13116
13117 cx.editor(|editor, _, _| {
13118 let popover = editor.signature_help_state.popover().cloned().unwrap();
13119 assert_eq!(popover.current_signature, 2);
13120 });
13121
13122 // Test wrap around
13123 cx.update_editor(|editor, window, cx| {
13124 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13125 });
13126
13127 cx.editor(|editor, _, _| {
13128 let popover = editor.signature_help_state.popover().cloned().unwrap();
13129 assert_eq!(popover.current_signature, 0);
13130 });
13131
13132 // Test previous navigation
13133 cx.update_editor(|editor, window, cx| {
13134 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13135 });
13136
13137 cx.editor(|editor, _, _| {
13138 let popover = editor.signature_help_state.popover().cloned().unwrap();
13139 assert_eq!(popover.current_signature, 2);
13140 });
13141}
13142
13143#[gpui::test]
13144async fn test_completion_mode(cx: &mut TestAppContext) {
13145 init_test(cx, |_| {});
13146 let mut cx = EditorLspTestContext::new_rust(
13147 lsp::ServerCapabilities {
13148 completion_provider: Some(lsp::CompletionOptions {
13149 resolve_provider: Some(true),
13150 ..Default::default()
13151 }),
13152 ..Default::default()
13153 },
13154 cx,
13155 )
13156 .await;
13157
13158 struct Run {
13159 run_description: &'static str,
13160 initial_state: String,
13161 buffer_marked_text: String,
13162 completion_label: &'static str,
13163 completion_text: &'static str,
13164 expected_with_insert_mode: String,
13165 expected_with_replace_mode: String,
13166 expected_with_replace_subsequence_mode: String,
13167 expected_with_replace_suffix_mode: String,
13168 }
13169
13170 let runs = [
13171 Run {
13172 run_description: "Start of word matches completion text",
13173 initial_state: "before ediˇ after".into(),
13174 buffer_marked_text: "before <edi|> after".into(),
13175 completion_label: "editor",
13176 completion_text: "editor",
13177 expected_with_insert_mode: "before editorˇ after".into(),
13178 expected_with_replace_mode: "before editorˇ after".into(),
13179 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13180 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13181 },
13182 Run {
13183 run_description: "Accept same text at the middle of the word",
13184 initial_state: "before ediˇtor after".into(),
13185 buffer_marked_text: "before <edi|tor> after".into(),
13186 completion_label: "editor",
13187 completion_text: "editor",
13188 expected_with_insert_mode: "before editorˇtor after".into(),
13189 expected_with_replace_mode: "before editorˇ after".into(),
13190 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13191 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13192 },
13193 Run {
13194 run_description: "End of word matches completion text -- cursor at end",
13195 initial_state: "before torˇ after".into(),
13196 buffer_marked_text: "before <tor|> after".into(),
13197 completion_label: "editor",
13198 completion_text: "editor",
13199 expected_with_insert_mode: "before editorˇ after".into(),
13200 expected_with_replace_mode: "before editorˇ after".into(),
13201 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13202 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13203 },
13204 Run {
13205 run_description: "End of word matches completion text -- cursor at start",
13206 initial_state: "before ˇtor after".into(),
13207 buffer_marked_text: "before <|tor> after".into(),
13208 completion_label: "editor",
13209 completion_text: "editor",
13210 expected_with_insert_mode: "before editorˇtor after".into(),
13211 expected_with_replace_mode: "before editorˇ after".into(),
13212 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13213 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13214 },
13215 Run {
13216 run_description: "Prepend text containing whitespace",
13217 initial_state: "pˇfield: bool".into(),
13218 buffer_marked_text: "<p|field>: bool".into(),
13219 completion_label: "pub ",
13220 completion_text: "pub ",
13221 expected_with_insert_mode: "pub ˇfield: bool".into(),
13222 expected_with_replace_mode: "pub ˇ: bool".into(),
13223 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13224 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13225 },
13226 Run {
13227 run_description: "Add element to start of list",
13228 initial_state: "[element_ˇelement_2]".into(),
13229 buffer_marked_text: "[<element_|element_2>]".into(),
13230 completion_label: "element_1",
13231 completion_text: "element_1",
13232 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13233 expected_with_replace_mode: "[element_1ˇ]".into(),
13234 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13235 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13236 },
13237 Run {
13238 run_description: "Add element to start of list -- first and second elements are equal",
13239 initial_state: "[elˇelement]".into(),
13240 buffer_marked_text: "[<el|element>]".into(),
13241 completion_label: "element",
13242 completion_text: "element",
13243 expected_with_insert_mode: "[elementˇelement]".into(),
13244 expected_with_replace_mode: "[elementˇ]".into(),
13245 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13246 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13247 },
13248 Run {
13249 run_description: "Ends with matching suffix",
13250 initial_state: "SubˇError".into(),
13251 buffer_marked_text: "<Sub|Error>".into(),
13252 completion_label: "SubscriptionError",
13253 completion_text: "SubscriptionError",
13254 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13255 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13256 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13257 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13258 },
13259 Run {
13260 run_description: "Suffix is a subsequence -- contiguous",
13261 initial_state: "SubˇErr".into(),
13262 buffer_marked_text: "<Sub|Err>".into(),
13263 completion_label: "SubscriptionError",
13264 completion_text: "SubscriptionError",
13265 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13266 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13267 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13268 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13269 },
13270 Run {
13271 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13272 initial_state: "Suˇscrirr".into(),
13273 buffer_marked_text: "<Su|scrirr>".into(),
13274 completion_label: "SubscriptionError",
13275 completion_text: "SubscriptionError",
13276 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13277 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13278 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13279 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13280 },
13281 Run {
13282 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13283 initial_state: "foo(indˇix)".into(),
13284 buffer_marked_text: "foo(<ind|ix>)".into(),
13285 completion_label: "node_index",
13286 completion_text: "node_index",
13287 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13288 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13289 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13290 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13291 },
13292 Run {
13293 run_description: "Replace range ends before cursor - should extend to cursor",
13294 initial_state: "before editˇo after".into(),
13295 buffer_marked_text: "before <{ed}>it|o after".into(),
13296 completion_label: "editor",
13297 completion_text: "editor",
13298 expected_with_insert_mode: "before editorˇo after".into(),
13299 expected_with_replace_mode: "before editorˇo after".into(),
13300 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13301 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13302 },
13303 Run {
13304 run_description: "Uses label for suffix matching",
13305 initial_state: "before ediˇtor after".into(),
13306 buffer_marked_text: "before <edi|tor> after".into(),
13307 completion_label: "editor",
13308 completion_text: "editor()",
13309 expected_with_insert_mode: "before editor()ˇtor after".into(),
13310 expected_with_replace_mode: "before editor()ˇ after".into(),
13311 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13312 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13313 },
13314 Run {
13315 run_description: "Case insensitive subsequence and suffix matching",
13316 initial_state: "before EDiˇtoR after".into(),
13317 buffer_marked_text: "before <EDi|toR> after".into(),
13318 completion_label: "editor",
13319 completion_text: "editor",
13320 expected_with_insert_mode: "before editorˇtoR after".into(),
13321 expected_with_replace_mode: "before editorˇ after".into(),
13322 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13323 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13324 },
13325 ];
13326
13327 for run in runs {
13328 let run_variations = [
13329 (LspInsertMode::Insert, run.expected_with_insert_mode),
13330 (LspInsertMode::Replace, run.expected_with_replace_mode),
13331 (
13332 LspInsertMode::ReplaceSubsequence,
13333 run.expected_with_replace_subsequence_mode,
13334 ),
13335 (
13336 LspInsertMode::ReplaceSuffix,
13337 run.expected_with_replace_suffix_mode,
13338 ),
13339 ];
13340
13341 for (lsp_insert_mode, expected_text) in run_variations {
13342 eprintln!(
13343 "run = {:?}, mode = {lsp_insert_mode:.?}",
13344 run.run_description,
13345 );
13346
13347 update_test_language_settings(&mut cx, |settings| {
13348 settings.defaults.completions = Some(CompletionSettingsContent {
13349 lsp_insert_mode: Some(lsp_insert_mode),
13350 words: Some(WordsCompletionMode::Disabled),
13351 words_min_length: Some(0),
13352 ..Default::default()
13353 });
13354 });
13355
13356 cx.set_state(&run.initial_state);
13357 cx.update_editor(|editor, window, cx| {
13358 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13359 });
13360
13361 let counter = Arc::new(AtomicUsize::new(0));
13362 handle_completion_request_with_insert_and_replace(
13363 &mut cx,
13364 &run.buffer_marked_text,
13365 vec![(run.completion_label, run.completion_text)],
13366 counter.clone(),
13367 )
13368 .await;
13369 cx.condition(|editor, _| editor.context_menu_visible())
13370 .await;
13371 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13372
13373 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13374 editor
13375 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13376 .unwrap()
13377 });
13378 cx.assert_editor_state(&expected_text);
13379 handle_resolve_completion_request(&mut cx, None).await;
13380 apply_additional_edits.await.unwrap();
13381 }
13382 }
13383}
13384
13385#[gpui::test]
13386async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13387 init_test(cx, |_| {});
13388 let mut cx = EditorLspTestContext::new_rust(
13389 lsp::ServerCapabilities {
13390 completion_provider: Some(lsp::CompletionOptions {
13391 resolve_provider: Some(true),
13392 ..Default::default()
13393 }),
13394 ..Default::default()
13395 },
13396 cx,
13397 )
13398 .await;
13399
13400 let initial_state = "SubˇError";
13401 let buffer_marked_text = "<Sub|Error>";
13402 let completion_text = "SubscriptionError";
13403 let expected_with_insert_mode = "SubscriptionErrorˇError";
13404 let expected_with_replace_mode = "SubscriptionErrorˇ";
13405
13406 update_test_language_settings(&mut cx, |settings| {
13407 settings.defaults.completions = Some(CompletionSettingsContent {
13408 words: Some(WordsCompletionMode::Disabled),
13409 words_min_length: Some(0),
13410 // set the opposite here to ensure that the action is overriding the default behavior
13411 lsp_insert_mode: Some(LspInsertMode::Insert),
13412 ..Default::default()
13413 });
13414 });
13415
13416 cx.set_state(initial_state);
13417 cx.update_editor(|editor, window, cx| {
13418 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13419 });
13420
13421 let counter = Arc::new(AtomicUsize::new(0));
13422 handle_completion_request_with_insert_and_replace(
13423 &mut cx,
13424 buffer_marked_text,
13425 vec![(completion_text, 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_replace(&ConfirmCompletionReplace, window, cx)
13436 .unwrap()
13437 });
13438 cx.assert_editor_state(expected_with_replace_mode);
13439 handle_resolve_completion_request(&mut cx, None).await;
13440 apply_additional_edits.await.unwrap();
13441
13442 update_test_language_settings(&mut cx, |settings| {
13443 settings.defaults.completions = Some(CompletionSettingsContent {
13444 words: Some(WordsCompletionMode::Disabled),
13445 words_min_length: Some(0),
13446 // set the opposite here to ensure that the action is overriding the default behavior
13447 lsp_insert_mode: Some(LspInsertMode::Replace),
13448 ..Default::default()
13449 });
13450 });
13451
13452 cx.set_state(initial_state);
13453 cx.update_editor(|editor, window, cx| {
13454 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13455 });
13456 handle_completion_request_with_insert_and_replace(
13457 &mut cx,
13458 buffer_marked_text,
13459 vec![(completion_text, completion_text)],
13460 counter.clone(),
13461 )
13462 .await;
13463 cx.condition(|editor, _| editor.context_menu_visible())
13464 .await;
13465 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13466
13467 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13468 editor
13469 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13470 .unwrap()
13471 });
13472 cx.assert_editor_state(expected_with_insert_mode);
13473 handle_resolve_completion_request(&mut cx, None).await;
13474 apply_additional_edits.await.unwrap();
13475}
13476
13477#[gpui::test]
13478async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13479 init_test(cx, |_| {});
13480 let mut cx = EditorLspTestContext::new_rust(
13481 lsp::ServerCapabilities {
13482 completion_provider: Some(lsp::CompletionOptions {
13483 resolve_provider: Some(true),
13484 ..Default::default()
13485 }),
13486 ..Default::default()
13487 },
13488 cx,
13489 )
13490 .await;
13491
13492 // scenario: surrounding text matches completion text
13493 let completion_text = "to_offset";
13494 let initial_state = indoc! {"
13495 1. buf.to_offˇsuffix
13496 2. buf.to_offˇsuf
13497 3. buf.to_offˇfix
13498 4. buf.to_offˇ
13499 5. into_offˇensive
13500 6. ˇsuffix
13501 7. let ˇ //
13502 8. aaˇzz
13503 9. buf.to_off«zzzzzˇ»suffix
13504 10. buf.«ˇzzzzz»suffix
13505 11. to_off«ˇzzzzz»
13506
13507 buf.to_offˇsuffix // newest cursor
13508 "};
13509 let completion_marked_buffer = indoc! {"
13510 1. buf.to_offsuffix
13511 2. buf.to_offsuf
13512 3. buf.to_offfix
13513 4. buf.to_off
13514 5. into_offensive
13515 6. suffix
13516 7. let //
13517 8. aazz
13518 9. buf.to_offzzzzzsuffix
13519 10. buf.zzzzzsuffix
13520 11. to_offzzzzz
13521
13522 buf.<to_off|suffix> // newest cursor
13523 "};
13524 let expected = indoc! {"
13525 1. buf.to_offsetˇ
13526 2. buf.to_offsetˇsuf
13527 3. buf.to_offsetˇfix
13528 4. buf.to_offsetˇ
13529 5. into_offsetˇensive
13530 6. to_offsetˇsuffix
13531 7. let to_offsetˇ //
13532 8. aato_offsetˇzz
13533 9. buf.to_offsetˇ
13534 10. buf.to_offsetˇsuffix
13535 11. to_offsetˇ
13536
13537 buf.to_offsetˇ // newest cursor
13538 "};
13539 cx.set_state(initial_state);
13540 cx.update_editor(|editor, window, cx| {
13541 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13542 });
13543 handle_completion_request_with_insert_and_replace(
13544 &mut cx,
13545 completion_marked_buffer,
13546 vec![(completion_text, completion_text)],
13547 Arc::new(AtomicUsize::new(0)),
13548 )
13549 .await;
13550 cx.condition(|editor, _| editor.context_menu_visible())
13551 .await;
13552 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13553 editor
13554 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13555 .unwrap()
13556 });
13557 cx.assert_editor_state(expected);
13558 handle_resolve_completion_request(&mut cx, None).await;
13559 apply_additional_edits.await.unwrap();
13560
13561 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13562 let completion_text = "foo_and_bar";
13563 let initial_state = indoc! {"
13564 1. ooanbˇ
13565 2. zooanbˇ
13566 3. ooanbˇz
13567 4. zooanbˇz
13568 5. ooanˇ
13569 6. oanbˇ
13570
13571 ooanbˇ
13572 "};
13573 let completion_marked_buffer = indoc! {"
13574 1. ooanb
13575 2. zooanb
13576 3. ooanbz
13577 4. zooanbz
13578 5. ooan
13579 6. oanb
13580
13581 <ooanb|>
13582 "};
13583 let expected = indoc! {"
13584 1. foo_and_barˇ
13585 2. zfoo_and_barˇ
13586 3. foo_and_barˇz
13587 4. zfoo_and_barˇz
13588 5. ooanfoo_and_barˇ
13589 6. oanbfoo_and_barˇ
13590
13591 foo_and_barˇ
13592 "};
13593 cx.set_state(initial_state);
13594 cx.update_editor(|editor, window, cx| {
13595 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13596 });
13597 handle_completion_request_with_insert_and_replace(
13598 &mut cx,
13599 completion_marked_buffer,
13600 vec![(completion_text, completion_text)],
13601 Arc::new(AtomicUsize::new(0)),
13602 )
13603 .await;
13604 cx.condition(|editor, _| editor.context_menu_visible())
13605 .await;
13606 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13607 editor
13608 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13609 .unwrap()
13610 });
13611 cx.assert_editor_state(expected);
13612 handle_resolve_completion_request(&mut cx, None).await;
13613 apply_additional_edits.await.unwrap();
13614
13615 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13616 // (expects the same as if it was inserted at the end)
13617 let completion_text = "foo_and_bar";
13618 let initial_state = indoc! {"
13619 1. ooˇanb
13620 2. zooˇanb
13621 3. ooˇanbz
13622 4. zooˇanbz
13623
13624 ooˇanb
13625 "};
13626 let completion_marked_buffer = indoc! {"
13627 1. ooanb
13628 2. zooanb
13629 3. ooanbz
13630 4. zooanbz
13631
13632 <oo|anb>
13633 "};
13634 let expected = indoc! {"
13635 1. foo_and_barˇ
13636 2. zfoo_and_barˇ
13637 3. foo_and_barˇz
13638 4. zfoo_and_barˇz
13639
13640 foo_and_barˇ
13641 "};
13642 cx.set_state(initial_state);
13643 cx.update_editor(|editor, window, cx| {
13644 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13645 });
13646 handle_completion_request_with_insert_and_replace(
13647 &mut cx,
13648 completion_marked_buffer,
13649 vec![(completion_text, completion_text)],
13650 Arc::new(AtomicUsize::new(0)),
13651 )
13652 .await;
13653 cx.condition(|editor, _| editor.context_menu_visible())
13654 .await;
13655 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13656 editor
13657 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13658 .unwrap()
13659 });
13660 cx.assert_editor_state(expected);
13661 handle_resolve_completion_request(&mut cx, None).await;
13662 apply_additional_edits.await.unwrap();
13663}
13664
13665// This used to crash
13666#[gpui::test]
13667async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13668 init_test(cx, |_| {});
13669
13670 let buffer_text = indoc! {"
13671 fn main() {
13672 10.satu;
13673
13674 //
13675 // separate cursors so they open in different excerpts (manually reproducible)
13676 //
13677
13678 10.satu20;
13679 }
13680 "};
13681 let multibuffer_text_with_selections = indoc! {"
13682 fn main() {
13683 10.satuˇ;
13684
13685 //
13686
13687 //
13688
13689 10.satuˇ20;
13690 }
13691 "};
13692 let expected_multibuffer = indoc! {"
13693 fn main() {
13694 10.saturating_sub()ˇ;
13695
13696 //
13697
13698 //
13699
13700 10.saturating_sub()ˇ;
13701 }
13702 "};
13703
13704 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13705 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13706
13707 let fs = FakeFs::new(cx.executor());
13708 fs.insert_tree(
13709 path!("/a"),
13710 json!({
13711 "main.rs": buffer_text,
13712 }),
13713 )
13714 .await;
13715
13716 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13717 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13718 language_registry.add(rust_lang());
13719 let mut fake_servers = language_registry.register_fake_lsp(
13720 "Rust",
13721 FakeLspAdapter {
13722 capabilities: lsp::ServerCapabilities {
13723 completion_provider: Some(lsp::CompletionOptions {
13724 resolve_provider: None,
13725 ..lsp::CompletionOptions::default()
13726 }),
13727 ..lsp::ServerCapabilities::default()
13728 },
13729 ..FakeLspAdapter::default()
13730 },
13731 );
13732 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13733 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13734 let buffer = project
13735 .update(cx, |project, cx| {
13736 project.open_local_buffer(path!("/a/main.rs"), cx)
13737 })
13738 .await
13739 .unwrap();
13740
13741 let multi_buffer = cx.new(|cx| {
13742 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13743 multi_buffer.push_excerpts(
13744 buffer.clone(),
13745 [ExcerptRange::new(0..first_excerpt_end)],
13746 cx,
13747 );
13748 multi_buffer.push_excerpts(
13749 buffer.clone(),
13750 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13751 cx,
13752 );
13753 multi_buffer
13754 });
13755
13756 let editor = workspace
13757 .update(cx, |_, window, cx| {
13758 cx.new(|cx| {
13759 Editor::new(
13760 EditorMode::Full {
13761 scale_ui_elements_with_buffer_font_size: false,
13762 show_active_line_background: false,
13763 sized_by_content: false,
13764 },
13765 multi_buffer.clone(),
13766 Some(project.clone()),
13767 window,
13768 cx,
13769 )
13770 })
13771 })
13772 .unwrap();
13773
13774 let pane = workspace
13775 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13776 .unwrap();
13777 pane.update_in(cx, |pane, window, cx| {
13778 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13779 });
13780
13781 let fake_server = fake_servers.next().await.unwrap();
13782
13783 editor.update_in(cx, |editor, window, cx| {
13784 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13785 s.select_ranges([
13786 Point::new(1, 11)..Point::new(1, 11),
13787 Point::new(7, 11)..Point::new(7, 11),
13788 ])
13789 });
13790
13791 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13792 });
13793
13794 editor.update_in(cx, |editor, window, cx| {
13795 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13796 });
13797
13798 fake_server
13799 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13800 let completion_item = lsp::CompletionItem {
13801 label: "saturating_sub()".into(),
13802 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13803 lsp::InsertReplaceEdit {
13804 new_text: "saturating_sub()".to_owned(),
13805 insert: lsp::Range::new(
13806 lsp::Position::new(7, 7),
13807 lsp::Position::new(7, 11),
13808 ),
13809 replace: lsp::Range::new(
13810 lsp::Position::new(7, 7),
13811 lsp::Position::new(7, 13),
13812 ),
13813 },
13814 )),
13815 ..lsp::CompletionItem::default()
13816 };
13817
13818 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13819 })
13820 .next()
13821 .await
13822 .unwrap();
13823
13824 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13825 .await;
13826
13827 editor
13828 .update_in(cx, |editor, window, cx| {
13829 editor
13830 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13831 .unwrap()
13832 })
13833 .await
13834 .unwrap();
13835
13836 editor.update(cx, |editor, cx| {
13837 assert_text_with_selections(editor, expected_multibuffer, cx);
13838 })
13839}
13840
13841#[gpui::test]
13842async fn test_completion(cx: &mut TestAppContext) {
13843 init_test(cx, |_| {});
13844
13845 let mut cx = EditorLspTestContext::new_rust(
13846 lsp::ServerCapabilities {
13847 completion_provider: Some(lsp::CompletionOptions {
13848 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13849 resolve_provider: Some(true),
13850 ..Default::default()
13851 }),
13852 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13853 ..Default::default()
13854 },
13855 cx,
13856 )
13857 .await;
13858 let counter = Arc::new(AtomicUsize::new(0));
13859
13860 cx.set_state(indoc! {"
13861 oneˇ
13862 two
13863 three
13864 "});
13865 cx.simulate_keystroke(".");
13866 handle_completion_request(
13867 indoc! {"
13868 one.|<>
13869 two
13870 three
13871 "},
13872 vec!["first_completion", "second_completion"],
13873 true,
13874 counter.clone(),
13875 &mut cx,
13876 )
13877 .await;
13878 cx.condition(|editor, _| editor.context_menu_visible())
13879 .await;
13880 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13881
13882 let _handler = handle_signature_help_request(
13883 &mut cx,
13884 lsp::SignatureHelp {
13885 signatures: vec![lsp::SignatureInformation {
13886 label: "test signature".to_string(),
13887 documentation: None,
13888 parameters: Some(vec![lsp::ParameterInformation {
13889 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13890 documentation: None,
13891 }]),
13892 active_parameter: None,
13893 }],
13894 active_signature: None,
13895 active_parameter: None,
13896 },
13897 );
13898 cx.update_editor(|editor, window, cx| {
13899 assert!(
13900 !editor.signature_help_state.is_shown(),
13901 "No signature help was called for"
13902 );
13903 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13904 });
13905 cx.run_until_parked();
13906 cx.update_editor(|editor, _, _| {
13907 assert!(
13908 !editor.signature_help_state.is_shown(),
13909 "No signature help should be shown when completions menu is open"
13910 );
13911 });
13912
13913 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13914 editor.context_menu_next(&Default::default(), window, cx);
13915 editor
13916 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13917 .unwrap()
13918 });
13919 cx.assert_editor_state(indoc! {"
13920 one.second_completionˇ
13921 two
13922 three
13923 "});
13924
13925 handle_resolve_completion_request(
13926 &mut cx,
13927 Some(vec![
13928 (
13929 //This overlaps with the primary completion edit which is
13930 //misbehavior from the LSP spec, test that we filter it out
13931 indoc! {"
13932 one.second_ˇcompletion
13933 two
13934 threeˇ
13935 "},
13936 "overlapping additional edit",
13937 ),
13938 (
13939 indoc! {"
13940 one.second_completion
13941 two
13942 threeˇ
13943 "},
13944 "\nadditional edit",
13945 ),
13946 ]),
13947 )
13948 .await;
13949 apply_additional_edits.await.unwrap();
13950 cx.assert_editor_state(indoc! {"
13951 one.second_completionˇ
13952 two
13953 three
13954 additional edit
13955 "});
13956
13957 cx.set_state(indoc! {"
13958 one.second_completion
13959 twoˇ
13960 threeˇ
13961 additional edit
13962 "});
13963 cx.simulate_keystroke(" ");
13964 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13965 cx.simulate_keystroke("s");
13966 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13967
13968 cx.assert_editor_state(indoc! {"
13969 one.second_completion
13970 two sˇ
13971 three sˇ
13972 additional edit
13973 "});
13974 handle_completion_request(
13975 indoc! {"
13976 one.second_completion
13977 two s
13978 three <s|>
13979 additional edit
13980 "},
13981 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13982 true,
13983 counter.clone(),
13984 &mut cx,
13985 )
13986 .await;
13987 cx.condition(|editor, _| editor.context_menu_visible())
13988 .await;
13989 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13990
13991 cx.simulate_keystroke("i");
13992
13993 handle_completion_request(
13994 indoc! {"
13995 one.second_completion
13996 two si
13997 three <si|>
13998 additional edit
13999 "},
14000 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14001 true,
14002 counter.clone(),
14003 &mut cx,
14004 )
14005 .await;
14006 cx.condition(|editor, _| editor.context_menu_visible())
14007 .await;
14008 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14009
14010 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14011 editor
14012 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14013 .unwrap()
14014 });
14015 cx.assert_editor_state(indoc! {"
14016 one.second_completion
14017 two sixth_completionˇ
14018 three sixth_completionˇ
14019 additional edit
14020 "});
14021
14022 apply_additional_edits.await.unwrap();
14023
14024 update_test_language_settings(&mut cx, |settings| {
14025 settings.defaults.show_completions_on_input = Some(false);
14026 });
14027 cx.set_state("editorˇ");
14028 cx.simulate_keystroke(".");
14029 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14030 cx.simulate_keystrokes("c l o");
14031 cx.assert_editor_state("editor.cloˇ");
14032 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14033 cx.update_editor(|editor, window, cx| {
14034 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14035 });
14036 handle_completion_request(
14037 "editor.<clo|>",
14038 vec!["close", "clobber"],
14039 true,
14040 counter.clone(),
14041 &mut cx,
14042 )
14043 .await;
14044 cx.condition(|editor, _| editor.context_menu_visible())
14045 .await;
14046 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14047
14048 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14049 editor
14050 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14051 .unwrap()
14052 });
14053 cx.assert_editor_state("editor.clobberˇ");
14054 handle_resolve_completion_request(&mut cx, None).await;
14055 apply_additional_edits.await.unwrap();
14056}
14057
14058#[gpui::test]
14059async fn test_completion_reuse(cx: &mut TestAppContext) {
14060 init_test(cx, |_| {});
14061
14062 let mut cx = EditorLspTestContext::new_rust(
14063 lsp::ServerCapabilities {
14064 completion_provider: Some(lsp::CompletionOptions {
14065 trigger_characters: Some(vec![".".to_string()]),
14066 ..Default::default()
14067 }),
14068 ..Default::default()
14069 },
14070 cx,
14071 )
14072 .await;
14073
14074 let counter = Arc::new(AtomicUsize::new(0));
14075 cx.set_state("objˇ");
14076 cx.simulate_keystroke(".");
14077
14078 // Initial completion request returns complete results
14079 let is_incomplete = false;
14080 handle_completion_request(
14081 "obj.|<>",
14082 vec!["a", "ab", "abc"],
14083 is_incomplete,
14084 counter.clone(),
14085 &mut cx,
14086 )
14087 .await;
14088 cx.run_until_parked();
14089 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14090 cx.assert_editor_state("obj.ˇ");
14091 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14092
14093 // Type "a" - filters existing completions
14094 cx.simulate_keystroke("a");
14095 cx.run_until_parked();
14096 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14097 cx.assert_editor_state("obj.aˇ");
14098 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14099
14100 // Type "b" - filters existing completions
14101 cx.simulate_keystroke("b");
14102 cx.run_until_parked();
14103 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14104 cx.assert_editor_state("obj.abˇ");
14105 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14106
14107 // Type "c" - filters existing completions
14108 cx.simulate_keystroke("c");
14109 cx.run_until_parked();
14110 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14111 cx.assert_editor_state("obj.abcˇ");
14112 check_displayed_completions(vec!["abc"], &mut cx);
14113
14114 // Backspace to delete "c" - filters existing completions
14115 cx.update_editor(|editor, window, cx| {
14116 editor.backspace(&Backspace, window, cx);
14117 });
14118 cx.run_until_parked();
14119 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14120 cx.assert_editor_state("obj.abˇ");
14121 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14122
14123 // Moving cursor to the left dismisses menu.
14124 cx.update_editor(|editor, window, cx| {
14125 editor.move_left(&MoveLeft, window, cx);
14126 });
14127 cx.run_until_parked();
14128 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14129 cx.assert_editor_state("obj.aˇb");
14130 cx.update_editor(|editor, _, _| {
14131 assert_eq!(editor.context_menu_visible(), false);
14132 });
14133
14134 // Type "b" - new request
14135 cx.simulate_keystroke("b");
14136 let is_incomplete = false;
14137 handle_completion_request(
14138 "obj.<ab|>a",
14139 vec!["ab", "abc"],
14140 is_incomplete,
14141 counter.clone(),
14142 &mut cx,
14143 )
14144 .await;
14145 cx.run_until_parked();
14146 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14147 cx.assert_editor_state("obj.abˇb");
14148 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14149
14150 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14151 cx.update_editor(|editor, window, cx| {
14152 editor.backspace(&Backspace, window, cx);
14153 });
14154 let is_incomplete = false;
14155 handle_completion_request(
14156 "obj.<a|>b",
14157 vec!["a", "ab", "abc"],
14158 is_incomplete,
14159 counter.clone(),
14160 &mut cx,
14161 )
14162 .await;
14163 cx.run_until_parked();
14164 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14165 cx.assert_editor_state("obj.aˇb");
14166 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14167
14168 // Backspace to delete "a" - dismisses menu.
14169 cx.update_editor(|editor, window, cx| {
14170 editor.backspace(&Backspace, window, cx);
14171 });
14172 cx.run_until_parked();
14173 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14174 cx.assert_editor_state("obj.ˇb");
14175 cx.update_editor(|editor, _, _| {
14176 assert_eq!(editor.context_menu_visible(), false);
14177 });
14178}
14179
14180#[gpui::test]
14181async fn test_word_completion(cx: &mut TestAppContext) {
14182 let lsp_fetch_timeout_ms = 10;
14183 init_test(cx, |language_settings| {
14184 language_settings.defaults.completions = Some(CompletionSettingsContent {
14185 words_min_length: Some(0),
14186 lsp_fetch_timeout_ms: Some(10),
14187 lsp_insert_mode: Some(LspInsertMode::Insert),
14188 ..Default::default()
14189 });
14190 });
14191
14192 let mut cx = EditorLspTestContext::new_rust(
14193 lsp::ServerCapabilities {
14194 completion_provider: Some(lsp::CompletionOptions {
14195 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14196 ..lsp::CompletionOptions::default()
14197 }),
14198 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14199 ..lsp::ServerCapabilities::default()
14200 },
14201 cx,
14202 )
14203 .await;
14204
14205 let throttle_completions = Arc::new(AtomicBool::new(false));
14206
14207 let lsp_throttle_completions = throttle_completions.clone();
14208 let _completion_requests_handler =
14209 cx.lsp
14210 .server
14211 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14212 let lsp_throttle_completions = lsp_throttle_completions.clone();
14213 let cx = cx.clone();
14214 async move {
14215 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14216 cx.background_executor()
14217 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14218 .await;
14219 }
14220 Ok(Some(lsp::CompletionResponse::Array(vec![
14221 lsp::CompletionItem {
14222 label: "first".into(),
14223 ..lsp::CompletionItem::default()
14224 },
14225 lsp::CompletionItem {
14226 label: "last".into(),
14227 ..lsp::CompletionItem::default()
14228 },
14229 ])))
14230 }
14231 });
14232
14233 cx.set_state(indoc! {"
14234 oneˇ
14235 two
14236 three
14237 "});
14238 cx.simulate_keystroke(".");
14239 cx.executor().run_until_parked();
14240 cx.condition(|editor, _| editor.context_menu_visible())
14241 .await;
14242 cx.update_editor(|editor, window, cx| {
14243 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14244 {
14245 assert_eq!(
14246 completion_menu_entries(menu),
14247 &["first", "last"],
14248 "When LSP server is fast to reply, no fallback word completions are used"
14249 );
14250 } else {
14251 panic!("expected completion menu to be open");
14252 }
14253 editor.cancel(&Cancel, window, cx);
14254 });
14255 cx.executor().run_until_parked();
14256 cx.condition(|editor, _| !editor.context_menu_visible())
14257 .await;
14258
14259 throttle_completions.store(true, atomic::Ordering::Release);
14260 cx.simulate_keystroke(".");
14261 cx.executor()
14262 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14263 cx.executor().run_until_parked();
14264 cx.condition(|editor, _| editor.context_menu_visible())
14265 .await;
14266 cx.update_editor(|editor, _, _| {
14267 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14268 {
14269 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14270 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14271 } else {
14272 panic!("expected completion menu to be open");
14273 }
14274 });
14275}
14276
14277#[gpui::test]
14278async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14279 init_test(cx, |language_settings| {
14280 language_settings.defaults.completions = Some(CompletionSettingsContent {
14281 words: Some(WordsCompletionMode::Enabled),
14282 words_min_length: Some(0),
14283 lsp_insert_mode: Some(LspInsertMode::Insert),
14284 ..Default::default()
14285 });
14286 });
14287
14288 let mut cx = EditorLspTestContext::new_rust(
14289 lsp::ServerCapabilities {
14290 completion_provider: Some(lsp::CompletionOptions {
14291 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14292 ..lsp::CompletionOptions::default()
14293 }),
14294 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14295 ..lsp::ServerCapabilities::default()
14296 },
14297 cx,
14298 )
14299 .await;
14300
14301 let _completion_requests_handler =
14302 cx.lsp
14303 .server
14304 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14305 Ok(Some(lsp::CompletionResponse::Array(vec![
14306 lsp::CompletionItem {
14307 label: "first".into(),
14308 ..lsp::CompletionItem::default()
14309 },
14310 lsp::CompletionItem {
14311 label: "last".into(),
14312 ..lsp::CompletionItem::default()
14313 },
14314 ])))
14315 });
14316
14317 cx.set_state(indoc! {"ˇ
14318 first
14319 last
14320 second
14321 "});
14322 cx.simulate_keystroke(".");
14323 cx.executor().run_until_parked();
14324 cx.condition(|editor, _| editor.context_menu_visible())
14325 .await;
14326 cx.update_editor(|editor, _, _| {
14327 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14328 {
14329 assert_eq!(
14330 completion_menu_entries(menu),
14331 &["first", "last", "second"],
14332 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14333 );
14334 } else {
14335 panic!("expected completion menu to be open");
14336 }
14337 });
14338}
14339
14340#[gpui::test]
14341async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14342 init_test(cx, |language_settings| {
14343 language_settings.defaults.completions = Some(CompletionSettingsContent {
14344 words: Some(WordsCompletionMode::Disabled),
14345 words_min_length: Some(0),
14346 lsp_insert_mode: Some(LspInsertMode::Insert),
14347 ..Default::default()
14348 });
14349 });
14350
14351 let mut cx = EditorLspTestContext::new_rust(
14352 lsp::ServerCapabilities {
14353 completion_provider: Some(lsp::CompletionOptions {
14354 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14355 ..lsp::CompletionOptions::default()
14356 }),
14357 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14358 ..lsp::ServerCapabilities::default()
14359 },
14360 cx,
14361 )
14362 .await;
14363
14364 let _completion_requests_handler =
14365 cx.lsp
14366 .server
14367 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14368 panic!("LSP completions should not be queried when dealing with word completions")
14369 });
14370
14371 cx.set_state(indoc! {"ˇ
14372 first
14373 last
14374 second
14375 "});
14376 cx.update_editor(|editor, window, cx| {
14377 editor.show_word_completions(&ShowWordCompletions, window, cx);
14378 });
14379 cx.executor().run_until_parked();
14380 cx.condition(|editor, _| editor.context_menu_visible())
14381 .await;
14382 cx.update_editor(|editor, _, _| {
14383 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14384 {
14385 assert_eq!(
14386 completion_menu_entries(menu),
14387 &["first", "last", "second"],
14388 "`ShowWordCompletions` action should show word completions"
14389 );
14390 } else {
14391 panic!("expected completion menu to be open");
14392 }
14393 });
14394
14395 cx.simulate_keystroke("l");
14396 cx.executor().run_until_parked();
14397 cx.condition(|editor, _| editor.context_menu_visible())
14398 .await;
14399 cx.update_editor(|editor, _, _| {
14400 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14401 {
14402 assert_eq!(
14403 completion_menu_entries(menu),
14404 &["last"],
14405 "After showing word completions, further editing should filter them and not query the LSP"
14406 );
14407 } else {
14408 panic!("expected completion menu to be open");
14409 }
14410 });
14411}
14412
14413#[gpui::test]
14414async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14415 init_test(cx, |language_settings| {
14416 language_settings.defaults.completions = Some(CompletionSettingsContent {
14417 words_min_length: Some(0),
14418 lsp: Some(false),
14419 lsp_insert_mode: Some(LspInsertMode::Insert),
14420 ..Default::default()
14421 });
14422 });
14423
14424 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14425
14426 cx.set_state(indoc! {"ˇ
14427 0_usize
14428 let
14429 33
14430 4.5f32
14431 "});
14432 cx.update_editor(|editor, window, cx| {
14433 editor.show_completions(&ShowCompletions::default(), window, cx);
14434 });
14435 cx.executor().run_until_parked();
14436 cx.condition(|editor, _| editor.context_menu_visible())
14437 .await;
14438 cx.update_editor(|editor, window, cx| {
14439 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14440 {
14441 assert_eq!(
14442 completion_menu_entries(menu),
14443 &["let"],
14444 "With no digits in the completion query, no digits should be in the word completions"
14445 );
14446 } else {
14447 panic!("expected completion menu to be open");
14448 }
14449 editor.cancel(&Cancel, window, cx);
14450 });
14451
14452 cx.set_state(indoc! {"3ˇ
14453 0_usize
14454 let
14455 3
14456 33.35f32
14457 "});
14458 cx.update_editor(|editor, window, cx| {
14459 editor.show_completions(&ShowCompletions::default(), window, cx);
14460 });
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!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14468 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14469 } else {
14470 panic!("expected completion menu to be open");
14471 }
14472 });
14473}
14474
14475#[gpui::test]
14476async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14477 init_test(cx, |language_settings| {
14478 language_settings.defaults.completions = Some(CompletionSettingsContent {
14479 words: Some(WordsCompletionMode::Enabled),
14480 words_min_length: Some(3),
14481 lsp_insert_mode: Some(LspInsertMode::Insert),
14482 ..Default::default()
14483 });
14484 });
14485
14486 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14487 cx.set_state(indoc! {"ˇ
14488 wow
14489 wowen
14490 wowser
14491 "});
14492 cx.simulate_keystroke("w");
14493 cx.executor().run_until_parked();
14494 cx.update_editor(|editor, _, _| {
14495 if editor.context_menu.borrow_mut().is_some() {
14496 panic!(
14497 "expected completion menu to be hidden, as words completion threshold is not met"
14498 );
14499 }
14500 });
14501
14502 cx.update_editor(|editor, window, cx| {
14503 editor.show_word_completions(&ShowWordCompletions, window, cx);
14504 });
14505 cx.executor().run_until_parked();
14506 cx.update_editor(|editor, window, cx| {
14507 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14508 {
14509 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");
14510 } else {
14511 panic!("expected completion menu to be open after the word completions are called with an action");
14512 }
14513
14514 editor.cancel(&Cancel, window, cx);
14515 });
14516 cx.update_editor(|editor, _, _| {
14517 if editor.context_menu.borrow_mut().is_some() {
14518 panic!("expected completion menu to be hidden after canceling");
14519 }
14520 });
14521
14522 cx.simulate_keystroke("o");
14523 cx.executor().run_until_parked();
14524 cx.update_editor(|editor, _, _| {
14525 if editor.context_menu.borrow_mut().is_some() {
14526 panic!(
14527 "expected completion menu to be hidden, as words completion threshold is not met still"
14528 );
14529 }
14530 });
14531
14532 cx.simulate_keystroke("w");
14533 cx.executor().run_until_parked();
14534 cx.update_editor(|editor, _, _| {
14535 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14536 {
14537 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14538 } else {
14539 panic!("expected completion menu to be open after the word completions threshold is met");
14540 }
14541 });
14542}
14543
14544#[gpui::test]
14545async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14546 init_test(cx, |language_settings| {
14547 language_settings.defaults.completions = Some(CompletionSettingsContent {
14548 words: Some(WordsCompletionMode::Enabled),
14549 words_min_length: Some(0),
14550 lsp_insert_mode: Some(LspInsertMode::Insert),
14551 ..Default::default()
14552 });
14553 });
14554
14555 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14556 cx.update_editor(|editor, _, _| {
14557 editor.disable_word_completions();
14558 });
14559 cx.set_state(indoc! {"ˇ
14560 wow
14561 wowen
14562 wowser
14563 "});
14564 cx.simulate_keystroke("w");
14565 cx.executor().run_until_parked();
14566 cx.update_editor(|editor, _, _| {
14567 if editor.context_menu.borrow_mut().is_some() {
14568 panic!(
14569 "expected completion menu to be hidden, as words completion are disabled for this editor"
14570 );
14571 }
14572 });
14573
14574 cx.update_editor(|editor, window, cx| {
14575 editor.show_word_completions(&ShowWordCompletions, window, cx);
14576 });
14577 cx.executor().run_until_parked();
14578 cx.update_editor(|editor, _, _| {
14579 if editor.context_menu.borrow_mut().is_some() {
14580 panic!(
14581 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14582 );
14583 }
14584 });
14585}
14586
14587fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14588 let position = || lsp::Position {
14589 line: params.text_document_position.position.line,
14590 character: params.text_document_position.position.character,
14591 };
14592 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14593 range: lsp::Range {
14594 start: position(),
14595 end: position(),
14596 },
14597 new_text: text.to_string(),
14598 }))
14599}
14600
14601#[gpui::test]
14602async fn test_multiline_completion(cx: &mut TestAppContext) {
14603 init_test(cx, |_| {});
14604
14605 let fs = FakeFs::new(cx.executor());
14606 fs.insert_tree(
14607 path!("/a"),
14608 json!({
14609 "main.ts": "a",
14610 }),
14611 )
14612 .await;
14613
14614 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14615 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14616 let typescript_language = Arc::new(Language::new(
14617 LanguageConfig {
14618 name: "TypeScript".into(),
14619 matcher: LanguageMatcher {
14620 path_suffixes: vec!["ts".to_string()],
14621 ..LanguageMatcher::default()
14622 },
14623 line_comments: vec!["// ".into()],
14624 ..LanguageConfig::default()
14625 },
14626 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14627 ));
14628 language_registry.add(typescript_language.clone());
14629 let mut fake_servers = language_registry.register_fake_lsp(
14630 "TypeScript",
14631 FakeLspAdapter {
14632 capabilities: lsp::ServerCapabilities {
14633 completion_provider: Some(lsp::CompletionOptions {
14634 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14635 ..lsp::CompletionOptions::default()
14636 }),
14637 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14638 ..lsp::ServerCapabilities::default()
14639 },
14640 // Emulate vtsls label generation
14641 label_for_completion: Some(Box::new(|item, _| {
14642 let text = if let Some(description) = item
14643 .label_details
14644 .as_ref()
14645 .and_then(|label_details| label_details.description.as_ref())
14646 {
14647 format!("{} {}", item.label, description)
14648 } else if let Some(detail) = &item.detail {
14649 format!("{} {}", item.label, detail)
14650 } else {
14651 item.label.clone()
14652 };
14653 let len = text.len();
14654 Some(language::CodeLabel {
14655 text,
14656 runs: Vec::new(),
14657 filter_range: 0..len,
14658 })
14659 })),
14660 ..FakeLspAdapter::default()
14661 },
14662 );
14663 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14664 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14665 let worktree_id = workspace
14666 .update(cx, |workspace, _window, cx| {
14667 workspace.project().update(cx, |project, cx| {
14668 project.worktrees(cx).next().unwrap().read(cx).id()
14669 })
14670 })
14671 .unwrap();
14672 let _buffer = project
14673 .update(cx, |project, cx| {
14674 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14675 })
14676 .await
14677 .unwrap();
14678 let editor = workspace
14679 .update(cx, |workspace, window, cx| {
14680 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14681 })
14682 .unwrap()
14683 .await
14684 .unwrap()
14685 .downcast::<Editor>()
14686 .unwrap();
14687 let fake_server = fake_servers.next().await.unwrap();
14688
14689 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14690 let multiline_label_2 = "a\nb\nc\n";
14691 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14692 let multiline_description = "d\ne\nf\n";
14693 let multiline_detail_2 = "g\nh\ni\n";
14694
14695 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14696 move |params, _| async move {
14697 Ok(Some(lsp::CompletionResponse::Array(vec![
14698 lsp::CompletionItem {
14699 label: multiline_label.to_string(),
14700 text_edit: gen_text_edit(¶ms, "new_text_1"),
14701 ..lsp::CompletionItem::default()
14702 },
14703 lsp::CompletionItem {
14704 label: "single line label 1".to_string(),
14705 detail: Some(multiline_detail.to_string()),
14706 text_edit: gen_text_edit(¶ms, "new_text_2"),
14707 ..lsp::CompletionItem::default()
14708 },
14709 lsp::CompletionItem {
14710 label: "single line label 2".to_string(),
14711 label_details: Some(lsp::CompletionItemLabelDetails {
14712 description: Some(multiline_description.to_string()),
14713 detail: None,
14714 }),
14715 text_edit: gen_text_edit(¶ms, "new_text_2"),
14716 ..lsp::CompletionItem::default()
14717 },
14718 lsp::CompletionItem {
14719 label: multiline_label_2.to_string(),
14720 detail: Some(multiline_detail_2.to_string()),
14721 text_edit: gen_text_edit(¶ms, "new_text_3"),
14722 ..lsp::CompletionItem::default()
14723 },
14724 lsp::CompletionItem {
14725 label: "Label with many spaces and \t but without newlines".to_string(),
14726 detail: Some(
14727 "Details with many spaces and \t but without newlines".to_string(),
14728 ),
14729 text_edit: gen_text_edit(¶ms, "new_text_4"),
14730 ..lsp::CompletionItem::default()
14731 },
14732 ])))
14733 },
14734 );
14735
14736 editor.update_in(cx, |editor, window, cx| {
14737 cx.focus_self(window);
14738 editor.move_to_end(&MoveToEnd, window, cx);
14739 editor.handle_input(".", window, cx);
14740 });
14741 cx.run_until_parked();
14742 completion_handle.next().await.unwrap();
14743
14744 editor.update(cx, |editor, _| {
14745 assert!(editor.context_menu_visible());
14746 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14747 {
14748 let completion_labels = menu
14749 .completions
14750 .borrow()
14751 .iter()
14752 .map(|c| c.label.text.clone())
14753 .collect::<Vec<_>>();
14754 assert_eq!(
14755 completion_labels,
14756 &[
14757 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14758 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14759 "single line label 2 d e f ",
14760 "a b c g h i ",
14761 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14762 ],
14763 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14764 );
14765
14766 for completion in menu
14767 .completions
14768 .borrow()
14769 .iter() {
14770 assert_eq!(
14771 completion.label.filter_range,
14772 0..completion.label.text.len(),
14773 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14774 );
14775 }
14776 } else {
14777 panic!("expected completion menu to be open");
14778 }
14779 });
14780}
14781
14782#[gpui::test]
14783async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14784 init_test(cx, |_| {});
14785 let mut cx = EditorLspTestContext::new_rust(
14786 lsp::ServerCapabilities {
14787 completion_provider: Some(lsp::CompletionOptions {
14788 trigger_characters: Some(vec![".".to_string()]),
14789 ..Default::default()
14790 }),
14791 ..Default::default()
14792 },
14793 cx,
14794 )
14795 .await;
14796 cx.lsp
14797 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14798 Ok(Some(lsp::CompletionResponse::Array(vec![
14799 lsp::CompletionItem {
14800 label: "first".into(),
14801 ..Default::default()
14802 },
14803 lsp::CompletionItem {
14804 label: "last".into(),
14805 ..Default::default()
14806 },
14807 ])))
14808 });
14809 cx.set_state("variableˇ");
14810 cx.simulate_keystroke(".");
14811 cx.executor().run_until_parked();
14812
14813 cx.update_editor(|editor, _, _| {
14814 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14815 {
14816 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14817 } else {
14818 panic!("expected completion menu to be open");
14819 }
14820 });
14821
14822 cx.update_editor(|editor, window, cx| {
14823 editor.move_page_down(&MovePageDown::default(), window, cx);
14824 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14825 {
14826 assert!(
14827 menu.selected_item == 1,
14828 "expected PageDown to select the last item from the context menu"
14829 );
14830 } else {
14831 panic!("expected completion menu to stay open after PageDown");
14832 }
14833 });
14834
14835 cx.update_editor(|editor, window, cx| {
14836 editor.move_page_up(&MovePageUp::default(), window, cx);
14837 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14838 {
14839 assert!(
14840 menu.selected_item == 0,
14841 "expected PageUp to select the first item from the context menu"
14842 );
14843 } else {
14844 panic!("expected completion menu to stay open after PageUp");
14845 }
14846 });
14847}
14848
14849#[gpui::test]
14850async fn test_as_is_completions(cx: &mut TestAppContext) {
14851 init_test(cx, |_| {});
14852 let mut cx = EditorLspTestContext::new_rust(
14853 lsp::ServerCapabilities {
14854 completion_provider: Some(lsp::CompletionOptions {
14855 ..Default::default()
14856 }),
14857 ..Default::default()
14858 },
14859 cx,
14860 )
14861 .await;
14862 cx.lsp
14863 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14864 Ok(Some(lsp::CompletionResponse::Array(vec![
14865 lsp::CompletionItem {
14866 label: "unsafe".into(),
14867 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14868 range: lsp::Range {
14869 start: lsp::Position {
14870 line: 1,
14871 character: 2,
14872 },
14873 end: lsp::Position {
14874 line: 1,
14875 character: 3,
14876 },
14877 },
14878 new_text: "unsafe".to_string(),
14879 })),
14880 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14881 ..Default::default()
14882 },
14883 ])))
14884 });
14885 cx.set_state("fn a() {}\n nˇ");
14886 cx.executor().run_until_parked();
14887 cx.update_editor(|editor, window, cx| {
14888 editor.show_completions(
14889 &ShowCompletions {
14890 trigger: Some("\n".into()),
14891 },
14892 window,
14893 cx,
14894 );
14895 });
14896 cx.executor().run_until_parked();
14897
14898 cx.update_editor(|editor, window, cx| {
14899 editor.confirm_completion(&Default::default(), window, cx)
14900 });
14901 cx.executor().run_until_parked();
14902 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14903}
14904
14905#[gpui::test]
14906async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14907 init_test(cx, |_| {});
14908 let language =
14909 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14910 let mut cx = EditorLspTestContext::new(
14911 language,
14912 lsp::ServerCapabilities {
14913 completion_provider: Some(lsp::CompletionOptions {
14914 ..lsp::CompletionOptions::default()
14915 }),
14916 ..lsp::ServerCapabilities::default()
14917 },
14918 cx,
14919 )
14920 .await;
14921
14922 cx.set_state(
14923 "#ifndef BAR_H
14924#define BAR_H
14925
14926#include <stdbool.h>
14927
14928int fn_branch(bool do_branch1, bool do_branch2);
14929
14930#endif // BAR_H
14931ˇ",
14932 );
14933 cx.executor().run_until_parked();
14934 cx.update_editor(|editor, window, cx| {
14935 editor.handle_input("#", window, cx);
14936 });
14937 cx.executor().run_until_parked();
14938 cx.update_editor(|editor, window, cx| {
14939 editor.handle_input("i", window, cx);
14940 });
14941 cx.executor().run_until_parked();
14942 cx.update_editor(|editor, window, cx| {
14943 editor.handle_input("n", window, cx);
14944 });
14945 cx.executor().run_until_parked();
14946 cx.assert_editor_state(
14947 "#ifndef BAR_H
14948#define BAR_H
14949
14950#include <stdbool.h>
14951
14952int fn_branch(bool do_branch1, bool do_branch2);
14953
14954#endif // BAR_H
14955#inˇ",
14956 );
14957
14958 cx.lsp
14959 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14960 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14961 is_incomplete: false,
14962 item_defaults: None,
14963 items: vec![lsp::CompletionItem {
14964 kind: Some(lsp::CompletionItemKind::SNIPPET),
14965 label_details: Some(lsp::CompletionItemLabelDetails {
14966 detail: Some("header".to_string()),
14967 description: None,
14968 }),
14969 label: " include".to_string(),
14970 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14971 range: lsp::Range {
14972 start: lsp::Position {
14973 line: 8,
14974 character: 1,
14975 },
14976 end: lsp::Position {
14977 line: 8,
14978 character: 1,
14979 },
14980 },
14981 new_text: "include \"$0\"".to_string(),
14982 })),
14983 sort_text: Some("40b67681include".to_string()),
14984 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14985 filter_text: Some("include".to_string()),
14986 insert_text: Some("include \"$0\"".to_string()),
14987 ..lsp::CompletionItem::default()
14988 }],
14989 })))
14990 });
14991 cx.update_editor(|editor, window, cx| {
14992 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14993 });
14994 cx.executor().run_until_parked();
14995 cx.update_editor(|editor, window, cx| {
14996 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14997 });
14998 cx.executor().run_until_parked();
14999 cx.assert_editor_state(
15000 "#ifndef BAR_H
15001#define BAR_H
15002
15003#include <stdbool.h>
15004
15005int fn_branch(bool do_branch1, bool do_branch2);
15006
15007#endif // BAR_H
15008#include \"ˇ\"",
15009 );
15010
15011 cx.lsp
15012 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15013 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15014 is_incomplete: true,
15015 item_defaults: None,
15016 items: vec![lsp::CompletionItem {
15017 kind: Some(lsp::CompletionItemKind::FILE),
15018 label: "AGL/".to_string(),
15019 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15020 range: lsp::Range {
15021 start: lsp::Position {
15022 line: 8,
15023 character: 10,
15024 },
15025 end: lsp::Position {
15026 line: 8,
15027 character: 11,
15028 },
15029 },
15030 new_text: "AGL/".to_string(),
15031 })),
15032 sort_text: Some("40b67681AGL/".to_string()),
15033 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15034 filter_text: Some("AGL/".to_string()),
15035 insert_text: Some("AGL/".to_string()),
15036 ..lsp::CompletionItem::default()
15037 }],
15038 })))
15039 });
15040 cx.update_editor(|editor, window, cx| {
15041 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15042 });
15043 cx.executor().run_until_parked();
15044 cx.update_editor(|editor, window, cx| {
15045 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15046 });
15047 cx.executor().run_until_parked();
15048 cx.assert_editor_state(
15049 r##"#ifndef BAR_H
15050#define BAR_H
15051
15052#include <stdbool.h>
15053
15054int fn_branch(bool do_branch1, bool do_branch2);
15055
15056#endif // BAR_H
15057#include "AGL/ˇ"##,
15058 );
15059
15060 cx.update_editor(|editor, window, cx| {
15061 editor.handle_input("\"", window, cx);
15062 });
15063 cx.executor().run_until_parked();
15064 cx.assert_editor_state(
15065 r##"#ifndef BAR_H
15066#define BAR_H
15067
15068#include <stdbool.h>
15069
15070int fn_branch(bool do_branch1, bool do_branch2);
15071
15072#endif // BAR_H
15073#include "AGL/"ˇ"##,
15074 );
15075}
15076
15077#[gpui::test]
15078async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15079 init_test(cx, |_| {});
15080
15081 let mut cx = EditorLspTestContext::new_rust(
15082 lsp::ServerCapabilities {
15083 completion_provider: Some(lsp::CompletionOptions {
15084 trigger_characters: Some(vec![".".to_string()]),
15085 resolve_provider: Some(true),
15086 ..Default::default()
15087 }),
15088 ..Default::default()
15089 },
15090 cx,
15091 )
15092 .await;
15093
15094 cx.set_state("fn main() { let a = 2ˇ; }");
15095 cx.simulate_keystroke(".");
15096 let completion_item = lsp::CompletionItem {
15097 label: "Some".into(),
15098 kind: Some(lsp::CompletionItemKind::SNIPPET),
15099 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15100 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15101 kind: lsp::MarkupKind::Markdown,
15102 value: "```rust\nSome(2)\n```".to_string(),
15103 })),
15104 deprecated: Some(false),
15105 sort_text: Some("Some".to_string()),
15106 filter_text: Some("Some".to_string()),
15107 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15108 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15109 range: lsp::Range {
15110 start: lsp::Position {
15111 line: 0,
15112 character: 22,
15113 },
15114 end: lsp::Position {
15115 line: 0,
15116 character: 22,
15117 },
15118 },
15119 new_text: "Some(2)".to_string(),
15120 })),
15121 additional_text_edits: Some(vec![lsp::TextEdit {
15122 range: lsp::Range {
15123 start: lsp::Position {
15124 line: 0,
15125 character: 20,
15126 },
15127 end: lsp::Position {
15128 line: 0,
15129 character: 22,
15130 },
15131 },
15132 new_text: "".to_string(),
15133 }]),
15134 ..Default::default()
15135 };
15136
15137 let closure_completion_item = completion_item.clone();
15138 let counter = Arc::new(AtomicUsize::new(0));
15139 let counter_clone = counter.clone();
15140 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15141 let task_completion_item = closure_completion_item.clone();
15142 counter_clone.fetch_add(1, atomic::Ordering::Release);
15143 async move {
15144 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15145 is_incomplete: true,
15146 item_defaults: None,
15147 items: vec![task_completion_item],
15148 })))
15149 }
15150 });
15151
15152 cx.condition(|editor, _| editor.context_menu_visible())
15153 .await;
15154 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15155 assert!(request.next().await.is_some());
15156 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15157
15158 cx.simulate_keystrokes("S o m");
15159 cx.condition(|editor, _| editor.context_menu_visible())
15160 .await;
15161 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15162 assert!(request.next().await.is_some());
15163 assert!(request.next().await.is_some());
15164 assert!(request.next().await.is_some());
15165 request.close();
15166 assert!(request.next().await.is_none());
15167 assert_eq!(
15168 counter.load(atomic::Ordering::Acquire),
15169 4,
15170 "With the completions menu open, only one LSP request should happen per input"
15171 );
15172}
15173
15174#[gpui::test]
15175async fn test_toggle_comment(cx: &mut TestAppContext) {
15176 init_test(cx, |_| {});
15177 let mut cx = EditorTestContext::new(cx).await;
15178 let language = Arc::new(Language::new(
15179 LanguageConfig {
15180 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15181 ..Default::default()
15182 },
15183 Some(tree_sitter_rust::LANGUAGE.into()),
15184 ));
15185 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15186
15187 // If multiple selections intersect a line, the line is only toggled once.
15188 cx.set_state(indoc! {"
15189 fn a() {
15190 «//b();
15191 ˇ»// «c();
15192 //ˇ» d();
15193 }
15194 "});
15195
15196 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15197
15198 cx.assert_editor_state(indoc! {"
15199 fn a() {
15200 «b();
15201 c();
15202 ˇ» d();
15203 }
15204 "});
15205
15206 // The comment prefix is inserted at the same column for every line in a
15207 // selection.
15208 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15209
15210 cx.assert_editor_state(indoc! {"
15211 fn a() {
15212 // «b();
15213 // c();
15214 ˇ»// d();
15215 }
15216 "});
15217
15218 // If a selection ends at the beginning of a line, that line is not toggled.
15219 cx.set_selections_state(indoc! {"
15220 fn a() {
15221 // b();
15222 «// c();
15223 ˇ» // d();
15224 }
15225 "});
15226
15227 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15228
15229 cx.assert_editor_state(indoc! {"
15230 fn a() {
15231 // b();
15232 «c();
15233 ˇ» // d();
15234 }
15235 "});
15236
15237 // If a selection span a single line and is empty, the line is toggled.
15238 cx.set_state(indoc! {"
15239 fn a() {
15240 a();
15241 b();
15242 ˇ
15243 }
15244 "});
15245
15246 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15247
15248 cx.assert_editor_state(indoc! {"
15249 fn a() {
15250 a();
15251 b();
15252 //•ˇ
15253 }
15254 "});
15255
15256 // If a selection span multiple lines, empty lines are not toggled.
15257 cx.set_state(indoc! {"
15258 fn a() {
15259 «a();
15260
15261 c();ˇ»
15262 }
15263 "});
15264
15265 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15266
15267 cx.assert_editor_state(indoc! {"
15268 fn a() {
15269 // «a();
15270
15271 // c();ˇ»
15272 }
15273 "});
15274
15275 // If a selection includes multiple comment prefixes, all lines are uncommented.
15276 cx.set_state(indoc! {"
15277 fn a() {
15278 «// a();
15279 /// b();
15280 //! c();ˇ»
15281 }
15282 "});
15283
15284 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15285
15286 cx.assert_editor_state(indoc! {"
15287 fn a() {
15288 «a();
15289 b();
15290 c();ˇ»
15291 }
15292 "});
15293}
15294
15295#[gpui::test]
15296async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15297 init_test(cx, |_| {});
15298 let mut cx = EditorTestContext::new(cx).await;
15299 let language = Arc::new(Language::new(
15300 LanguageConfig {
15301 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15302 ..Default::default()
15303 },
15304 Some(tree_sitter_rust::LANGUAGE.into()),
15305 ));
15306 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15307
15308 let toggle_comments = &ToggleComments {
15309 advance_downwards: false,
15310 ignore_indent: true,
15311 };
15312
15313 // If multiple selections intersect a line, the line is only toggled once.
15314 cx.set_state(indoc! {"
15315 fn a() {
15316 // «b();
15317 // c();
15318 // ˇ» d();
15319 }
15320 "});
15321
15322 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15323
15324 cx.assert_editor_state(indoc! {"
15325 fn a() {
15326 «b();
15327 c();
15328 ˇ» d();
15329 }
15330 "});
15331
15332 // The comment prefix is inserted at the beginning of each line
15333 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15334
15335 cx.assert_editor_state(indoc! {"
15336 fn a() {
15337 // «b();
15338 // c();
15339 // ˇ» d();
15340 }
15341 "});
15342
15343 // If a selection ends at the beginning of a line, that line is not toggled.
15344 cx.set_selections_state(indoc! {"
15345 fn a() {
15346 // b();
15347 // «c();
15348 ˇ»// d();
15349 }
15350 "});
15351
15352 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15353
15354 cx.assert_editor_state(indoc! {"
15355 fn a() {
15356 // b();
15357 «c();
15358 ˇ»// d();
15359 }
15360 "});
15361
15362 // If a selection span a single line and is empty, the line is toggled.
15363 cx.set_state(indoc! {"
15364 fn a() {
15365 a();
15366 b();
15367 ˇ
15368 }
15369 "});
15370
15371 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15372
15373 cx.assert_editor_state(indoc! {"
15374 fn a() {
15375 a();
15376 b();
15377 //ˇ
15378 }
15379 "});
15380
15381 // If a selection span multiple lines, empty lines are not toggled.
15382 cx.set_state(indoc! {"
15383 fn a() {
15384 «a();
15385
15386 c();ˇ»
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 // «a();
15395
15396 // c();ˇ»
15397 }
15398 "});
15399
15400 // If a selection includes multiple comment prefixes, all lines are uncommented.
15401 cx.set_state(indoc! {"
15402 fn a() {
15403 // «a();
15404 /// b();
15405 //! c();ˇ»
15406 }
15407 "});
15408
15409 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15410
15411 cx.assert_editor_state(indoc! {"
15412 fn a() {
15413 «a();
15414 b();
15415 c();ˇ»
15416 }
15417 "});
15418}
15419
15420#[gpui::test]
15421async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15422 init_test(cx, |_| {});
15423
15424 let language = Arc::new(Language::new(
15425 LanguageConfig {
15426 line_comments: vec!["// ".into()],
15427 ..Default::default()
15428 },
15429 Some(tree_sitter_rust::LANGUAGE.into()),
15430 ));
15431
15432 let mut cx = EditorTestContext::new(cx).await;
15433
15434 cx.language_registry().add(language.clone());
15435 cx.update_buffer(|buffer, cx| {
15436 buffer.set_language(Some(language), cx);
15437 });
15438
15439 let toggle_comments = &ToggleComments {
15440 advance_downwards: true,
15441 ignore_indent: false,
15442 };
15443
15444 // Single cursor on one line -> advance
15445 // Cursor moves horizontally 3 characters as well on non-blank line
15446 cx.set_state(indoc!(
15447 "fn a() {
15448 ˇdog();
15449 cat();
15450 }"
15451 ));
15452 cx.update_editor(|editor, window, cx| {
15453 editor.toggle_comments(toggle_comments, window, cx);
15454 });
15455 cx.assert_editor_state(indoc!(
15456 "fn a() {
15457 // dog();
15458 catˇ();
15459 }"
15460 ));
15461
15462 // Single selection on one line -> don't advance
15463 cx.set_state(indoc!(
15464 "fn a() {
15465 «dog()ˇ»;
15466 cat();
15467 }"
15468 ));
15469 cx.update_editor(|editor, window, cx| {
15470 editor.toggle_comments(toggle_comments, window, cx);
15471 });
15472 cx.assert_editor_state(indoc!(
15473 "fn a() {
15474 // «dog()ˇ»;
15475 cat();
15476 }"
15477 ));
15478
15479 // Multiple cursors on one line -> advance
15480 cx.set_state(indoc!(
15481 "fn a() {
15482 ˇdˇog();
15483 cat();
15484 }"
15485 ));
15486 cx.update_editor(|editor, window, cx| {
15487 editor.toggle_comments(toggle_comments, window, cx);
15488 });
15489 cx.assert_editor_state(indoc!(
15490 "fn a() {
15491 // dog();
15492 catˇ(ˇ);
15493 }"
15494 ));
15495
15496 // Multiple cursors on one line, with selection -> don't advance
15497 cx.set_state(indoc!(
15498 "fn a() {
15499 ˇdˇog«()ˇ»;
15500 cat();
15501 }"
15502 ));
15503 cx.update_editor(|editor, window, cx| {
15504 editor.toggle_comments(toggle_comments, window, cx);
15505 });
15506 cx.assert_editor_state(indoc!(
15507 "fn a() {
15508 // ˇdˇog«()ˇ»;
15509 cat();
15510 }"
15511 ));
15512
15513 // Single cursor on one line -> advance
15514 // Cursor moves to column 0 on blank line
15515 cx.set_state(indoc!(
15516 "fn a() {
15517 ˇdog();
15518
15519 cat();
15520 }"
15521 ));
15522 cx.update_editor(|editor, window, cx| {
15523 editor.toggle_comments(toggle_comments, window, cx);
15524 });
15525 cx.assert_editor_state(indoc!(
15526 "fn a() {
15527 // dog();
15528 ˇ
15529 cat();
15530 }"
15531 ));
15532
15533 // Single cursor on one line -> advance
15534 // Cursor starts and ends at column 0
15535 cx.set_state(indoc!(
15536 "fn a() {
15537 ˇ dog();
15538 cat();
15539 }"
15540 ));
15541 cx.update_editor(|editor, window, cx| {
15542 editor.toggle_comments(toggle_comments, window, cx);
15543 });
15544 cx.assert_editor_state(indoc!(
15545 "fn a() {
15546 // dog();
15547 ˇ cat();
15548 }"
15549 ));
15550}
15551
15552#[gpui::test]
15553async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15554 init_test(cx, |_| {});
15555
15556 let mut cx = EditorTestContext::new(cx).await;
15557
15558 let html_language = Arc::new(
15559 Language::new(
15560 LanguageConfig {
15561 name: "HTML".into(),
15562 block_comment: Some(BlockCommentConfig {
15563 start: "<!-- ".into(),
15564 prefix: "".into(),
15565 end: " -->".into(),
15566 tab_size: 0,
15567 }),
15568 ..Default::default()
15569 },
15570 Some(tree_sitter_html::LANGUAGE.into()),
15571 )
15572 .with_injection_query(
15573 r#"
15574 (script_element
15575 (raw_text) @injection.content
15576 (#set! injection.language "javascript"))
15577 "#,
15578 )
15579 .unwrap(),
15580 );
15581
15582 let javascript_language = Arc::new(Language::new(
15583 LanguageConfig {
15584 name: "JavaScript".into(),
15585 line_comments: vec!["// ".into()],
15586 ..Default::default()
15587 },
15588 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15589 ));
15590
15591 cx.language_registry().add(html_language.clone());
15592 cx.language_registry().add(javascript_language);
15593 cx.update_buffer(|buffer, cx| {
15594 buffer.set_language(Some(html_language), cx);
15595 });
15596
15597 // Toggle comments for empty selections
15598 cx.set_state(
15599 &r#"
15600 <p>A</p>ˇ
15601 <p>B</p>ˇ
15602 <p>C</p>ˇ
15603 "#
15604 .unindent(),
15605 );
15606 cx.update_editor(|editor, window, cx| {
15607 editor.toggle_comments(&ToggleComments::default(), window, cx)
15608 });
15609 cx.assert_editor_state(
15610 &r#"
15611 <!-- <p>A</p>ˇ -->
15612 <!-- <p>B</p>ˇ -->
15613 <!-- <p>C</p>ˇ -->
15614 "#
15615 .unindent(),
15616 );
15617 cx.update_editor(|editor, window, cx| {
15618 editor.toggle_comments(&ToggleComments::default(), window, cx)
15619 });
15620 cx.assert_editor_state(
15621 &r#"
15622 <p>A</p>ˇ
15623 <p>B</p>ˇ
15624 <p>C</p>ˇ
15625 "#
15626 .unindent(),
15627 );
15628
15629 // Toggle comments for mixture of empty and non-empty selections, where
15630 // multiple selections occupy a given line.
15631 cx.set_state(
15632 &r#"
15633 <p>A«</p>
15634 <p>ˇ»B</p>ˇ
15635 <p>C«</p>
15636 <p>ˇ»D</p>ˇ
15637 "#
15638 .unindent(),
15639 );
15640
15641 cx.update_editor(|editor, window, cx| {
15642 editor.toggle_comments(&ToggleComments::default(), window, cx)
15643 });
15644 cx.assert_editor_state(
15645 &r#"
15646 <!-- <p>A«</p>
15647 <p>ˇ»B</p>ˇ -->
15648 <!-- <p>C«</p>
15649 <p>ˇ»D</p>ˇ -->
15650 "#
15651 .unindent(),
15652 );
15653 cx.update_editor(|editor, window, cx| {
15654 editor.toggle_comments(&ToggleComments::default(), window, cx)
15655 });
15656 cx.assert_editor_state(
15657 &r#"
15658 <p>A«</p>
15659 <p>ˇ»B</p>ˇ
15660 <p>C«</p>
15661 <p>ˇ»D</p>ˇ
15662 "#
15663 .unindent(),
15664 );
15665
15666 // Toggle comments when different languages are active for different
15667 // selections.
15668 cx.set_state(
15669 &r#"
15670 ˇ<script>
15671 ˇvar x = new Y();
15672 ˇ</script>
15673 "#
15674 .unindent(),
15675 );
15676 cx.executor().run_until_parked();
15677 cx.update_editor(|editor, window, cx| {
15678 editor.toggle_comments(&ToggleComments::default(), window, cx)
15679 });
15680 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15681 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15682 cx.assert_editor_state(
15683 &r#"
15684 <!-- ˇ<script> -->
15685 // ˇvar x = new Y();
15686 <!-- ˇ</script> -->
15687 "#
15688 .unindent(),
15689 );
15690}
15691
15692#[gpui::test]
15693fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15694 init_test(cx, |_| {});
15695
15696 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15697 let multibuffer = cx.new(|cx| {
15698 let mut multibuffer = MultiBuffer::new(ReadWrite);
15699 multibuffer.push_excerpts(
15700 buffer.clone(),
15701 [
15702 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15703 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15704 ],
15705 cx,
15706 );
15707 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15708 multibuffer
15709 });
15710
15711 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15712 editor.update_in(cx, |editor, window, cx| {
15713 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15715 s.select_ranges([
15716 Point::new(0, 0)..Point::new(0, 0),
15717 Point::new(1, 0)..Point::new(1, 0),
15718 ])
15719 });
15720
15721 editor.handle_input("X", window, cx);
15722 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15723 assert_eq!(
15724 editor.selections.ranges(cx),
15725 [
15726 Point::new(0, 1)..Point::new(0, 1),
15727 Point::new(1, 1)..Point::new(1, 1),
15728 ]
15729 );
15730
15731 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15732 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15733 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15734 });
15735 editor.backspace(&Default::default(), window, cx);
15736 assert_eq!(editor.text(cx), "Xa\nbbb");
15737 assert_eq!(
15738 editor.selections.ranges(cx),
15739 [Point::new(1, 0)..Point::new(1, 0)]
15740 );
15741
15742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15743 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15744 });
15745 editor.backspace(&Default::default(), window, cx);
15746 assert_eq!(editor.text(cx), "X\nbb");
15747 assert_eq!(
15748 editor.selections.ranges(cx),
15749 [Point::new(0, 1)..Point::new(0, 1)]
15750 );
15751 });
15752}
15753
15754#[gpui::test]
15755fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15756 init_test(cx, |_| {});
15757
15758 let markers = vec![('[', ']').into(), ('(', ')').into()];
15759 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15760 indoc! {"
15761 [aaaa
15762 (bbbb]
15763 cccc)",
15764 },
15765 markers.clone(),
15766 );
15767 let excerpt_ranges = markers.into_iter().map(|marker| {
15768 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15769 ExcerptRange::new(context)
15770 });
15771 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15772 let multibuffer = cx.new(|cx| {
15773 let mut multibuffer = MultiBuffer::new(ReadWrite);
15774 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15775 multibuffer
15776 });
15777
15778 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15779 editor.update_in(cx, |editor, window, cx| {
15780 let (expected_text, selection_ranges) = marked_text_ranges(
15781 indoc! {"
15782 aaaa
15783 bˇbbb
15784 bˇbbˇb
15785 cccc"
15786 },
15787 true,
15788 );
15789 assert_eq!(editor.text(cx), expected_text);
15790 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15791 s.select_ranges(selection_ranges)
15792 });
15793
15794 editor.handle_input("X", window, cx);
15795
15796 let (expected_text, expected_selections) = marked_text_ranges(
15797 indoc! {"
15798 aaaa
15799 bXˇbbXb
15800 bXˇbbXˇb
15801 cccc"
15802 },
15803 false,
15804 );
15805 assert_eq!(editor.text(cx), expected_text);
15806 assert_eq!(editor.selections.ranges(cx), expected_selections);
15807
15808 editor.newline(&Newline, window, cx);
15809 let (expected_text, expected_selections) = marked_text_ranges(
15810 indoc! {"
15811 aaaa
15812 bX
15813 ˇbbX
15814 b
15815 bX
15816 ˇbbX
15817 ˇb
15818 cccc"
15819 },
15820 false,
15821 );
15822 assert_eq!(editor.text(cx), expected_text);
15823 assert_eq!(editor.selections.ranges(cx), expected_selections);
15824 });
15825}
15826
15827#[gpui::test]
15828fn test_refresh_selections(cx: &mut TestAppContext) {
15829 init_test(cx, |_| {});
15830
15831 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15832 let mut excerpt1_id = None;
15833 let multibuffer = cx.new(|cx| {
15834 let mut multibuffer = MultiBuffer::new(ReadWrite);
15835 excerpt1_id = multibuffer
15836 .push_excerpts(
15837 buffer.clone(),
15838 [
15839 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15840 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15841 ],
15842 cx,
15843 )
15844 .into_iter()
15845 .next();
15846 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15847 multibuffer
15848 });
15849
15850 let editor = cx.add_window(|window, cx| {
15851 let mut editor = build_editor(multibuffer.clone(), window, cx);
15852 let snapshot = editor.snapshot(window, cx);
15853 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15854 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15855 });
15856 editor.begin_selection(
15857 Point::new(2, 1).to_display_point(&snapshot),
15858 true,
15859 1,
15860 window,
15861 cx,
15862 );
15863 assert_eq!(
15864 editor.selections.ranges(cx),
15865 [
15866 Point::new(1, 3)..Point::new(1, 3),
15867 Point::new(2, 1)..Point::new(2, 1),
15868 ]
15869 );
15870 editor
15871 });
15872
15873 // Refreshing selections is a no-op when excerpts haven't changed.
15874 _ = editor.update(cx, |editor, window, cx| {
15875 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15876 assert_eq!(
15877 editor.selections.ranges(cx),
15878 [
15879 Point::new(1, 3)..Point::new(1, 3),
15880 Point::new(2, 1)..Point::new(2, 1),
15881 ]
15882 );
15883 });
15884
15885 multibuffer.update(cx, |multibuffer, cx| {
15886 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15887 });
15888 _ = editor.update(cx, |editor, window, cx| {
15889 // Removing an excerpt causes the first selection to become degenerate.
15890 assert_eq!(
15891 editor.selections.ranges(cx),
15892 [
15893 Point::new(0, 0)..Point::new(0, 0),
15894 Point::new(0, 1)..Point::new(0, 1)
15895 ]
15896 );
15897
15898 // Refreshing selections will relocate the first selection to the original buffer
15899 // location.
15900 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15901 assert_eq!(
15902 editor.selections.ranges(cx),
15903 [
15904 Point::new(0, 1)..Point::new(0, 1),
15905 Point::new(0, 3)..Point::new(0, 3)
15906 ]
15907 );
15908 assert!(editor.selections.pending_anchor().is_some());
15909 });
15910}
15911
15912#[gpui::test]
15913fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15914 init_test(cx, |_| {});
15915
15916 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15917 let mut excerpt1_id = None;
15918 let multibuffer = cx.new(|cx| {
15919 let mut multibuffer = MultiBuffer::new(ReadWrite);
15920 excerpt1_id = multibuffer
15921 .push_excerpts(
15922 buffer.clone(),
15923 [
15924 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15925 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15926 ],
15927 cx,
15928 )
15929 .into_iter()
15930 .next();
15931 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15932 multibuffer
15933 });
15934
15935 let editor = cx.add_window(|window, cx| {
15936 let mut editor = build_editor(multibuffer.clone(), window, cx);
15937 let snapshot = editor.snapshot(window, cx);
15938 editor.begin_selection(
15939 Point::new(1, 3).to_display_point(&snapshot),
15940 false,
15941 1,
15942 window,
15943 cx,
15944 );
15945 assert_eq!(
15946 editor.selections.ranges(cx),
15947 [Point::new(1, 3)..Point::new(1, 3)]
15948 );
15949 editor
15950 });
15951
15952 multibuffer.update(cx, |multibuffer, cx| {
15953 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15954 });
15955 _ = editor.update(cx, |editor, window, cx| {
15956 assert_eq!(
15957 editor.selections.ranges(cx),
15958 [Point::new(0, 0)..Point::new(0, 0)]
15959 );
15960
15961 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15962 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15963 assert_eq!(
15964 editor.selections.ranges(cx),
15965 [Point::new(0, 3)..Point::new(0, 3)]
15966 );
15967 assert!(editor.selections.pending_anchor().is_some());
15968 });
15969}
15970
15971#[gpui::test]
15972async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15973 init_test(cx, |_| {});
15974
15975 let language = Arc::new(
15976 Language::new(
15977 LanguageConfig {
15978 brackets: BracketPairConfig {
15979 pairs: vec![
15980 BracketPair {
15981 start: "{".to_string(),
15982 end: "}".to_string(),
15983 close: true,
15984 surround: true,
15985 newline: true,
15986 },
15987 BracketPair {
15988 start: "/* ".to_string(),
15989 end: " */".to_string(),
15990 close: true,
15991 surround: true,
15992 newline: true,
15993 },
15994 ],
15995 ..Default::default()
15996 },
15997 ..Default::default()
15998 },
15999 Some(tree_sitter_rust::LANGUAGE.into()),
16000 )
16001 .with_indents_query("")
16002 .unwrap(),
16003 );
16004
16005 let text = concat!(
16006 "{ }\n", //
16007 " x\n", //
16008 " /* */\n", //
16009 "x\n", //
16010 "{{} }\n", //
16011 );
16012
16013 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16014 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16015 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16016 editor
16017 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16018 .await;
16019
16020 editor.update_in(cx, |editor, window, cx| {
16021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16022 s.select_display_ranges([
16023 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16024 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16025 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16026 ])
16027 });
16028 editor.newline(&Newline, window, cx);
16029
16030 assert_eq!(
16031 editor.buffer().read(cx).read(cx).text(),
16032 concat!(
16033 "{ \n", // Suppress rustfmt
16034 "\n", //
16035 "}\n", //
16036 " x\n", //
16037 " /* \n", //
16038 " \n", //
16039 " */\n", //
16040 "x\n", //
16041 "{{} \n", //
16042 "}\n", //
16043 )
16044 );
16045 });
16046}
16047
16048#[gpui::test]
16049fn test_highlighted_ranges(cx: &mut TestAppContext) {
16050 init_test(cx, |_| {});
16051
16052 let editor = cx.add_window(|window, cx| {
16053 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16054 build_editor(buffer, window, cx)
16055 });
16056
16057 _ = editor.update(cx, |editor, window, cx| {
16058 struct Type1;
16059 struct Type2;
16060
16061 let buffer = editor.buffer.read(cx).snapshot(cx);
16062
16063 let anchor_range =
16064 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16065
16066 editor.highlight_background::<Type1>(
16067 &[
16068 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16069 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16070 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16071 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16072 ],
16073 |_| Hsla::red(),
16074 cx,
16075 );
16076 editor.highlight_background::<Type2>(
16077 &[
16078 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16079 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16080 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16081 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16082 ],
16083 |_| Hsla::green(),
16084 cx,
16085 );
16086
16087 let snapshot = editor.snapshot(window, cx);
16088 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16089 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16090 &snapshot,
16091 cx.theme(),
16092 );
16093 assert_eq!(
16094 highlighted_ranges,
16095 &[
16096 (
16097 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16098 Hsla::green(),
16099 ),
16100 (
16101 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16102 Hsla::red(),
16103 ),
16104 (
16105 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16106 Hsla::green(),
16107 ),
16108 (
16109 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16110 Hsla::red(),
16111 ),
16112 ]
16113 );
16114 assert_eq!(
16115 editor.sorted_background_highlights_in_range(
16116 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16117 &snapshot,
16118 cx.theme(),
16119 ),
16120 &[(
16121 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16122 Hsla::red(),
16123 )]
16124 );
16125 });
16126}
16127
16128#[gpui::test]
16129async fn test_following(cx: &mut TestAppContext) {
16130 init_test(cx, |_| {});
16131
16132 let fs = FakeFs::new(cx.executor());
16133 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16134
16135 let buffer = project.update(cx, |project, cx| {
16136 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16137 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16138 });
16139 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16140 let follower = cx.update(|cx| {
16141 cx.open_window(
16142 WindowOptions {
16143 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16144 gpui::Point::new(px(0.), px(0.)),
16145 gpui::Point::new(px(10.), px(80.)),
16146 ))),
16147 ..Default::default()
16148 },
16149 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16150 )
16151 .unwrap()
16152 });
16153
16154 let is_still_following = Rc::new(RefCell::new(true));
16155 let follower_edit_event_count = Rc::new(RefCell::new(0));
16156 let pending_update = Rc::new(RefCell::new(None));
16157 let leader_entity = leader.root(cx).unwrap();
16158 let follower_entity = follower.root(cx).unwrap();
16159 _ = follower.update(cx, {
16160 let update = pending_update.clone();
16161 let is_still_following = is_still_following.clone();
16162 let follower_edit_event_count = follower_edit_event_count.clone();
16163 |_, window, cx| {
16164 cx.subscribe_in(
16165 &leader_entity,
16166 window,
16167 move |_, leader, event, window, cx| {
16168 leader.read(cx).add_event_to_update_proto(
16169 event,
16170 &mut update.borrow_mut(),
16171 window,
16172 cx,
16173 );
16174 },
16175 )
16176 .detach();
16177
16178 cx.subscribe_in(
16179 &follower_entity,
16180 window,
16181 move |_, _, event: &EditorEvent, _window, _cx| {
16182 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16183 *is_still_following.borrow_mut() = false;
16184 }
16185
16186 if let EditorEvent::BufferEdited = event {
16187 *follower_edit_event_count.borrow_mut() += 1;
16188 }
16189 },
16190 )
16191 .detach();
16192 }
16193 });
16194
16195 // Update the selections only
16196 _ = leader.update(cx, |leader, window, cx| {
16197 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16198 s.select_ranges([1..1])
16199 });
16200 });
16201 follower
16202 .update(cx, |follower, window, cx| {
16203 follower.apply_update_proto(
16204 &project,
16205 pending_update.borrow_mut().take().unwrap(),
16206 window,
16207 cx,
16208 )
16209 })
16210 .unwrap()
16211 .await
16212 .unwrap();
16213 _ = follower.update(cx, |follower, _, cx| {
16214 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16215 });
16216 assert!(*is_still_following.borrow());
16217 assert_eq!(*follower_edit_event_count.borrow(), 0);
16218
16219 // Update the scroll position only
16220 _ = leader.update(cx, |leader, window, cx| {
16221 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16222 });
16223 follower
16224 .update(cx, |follower, window, cx| {
16225 follower.apply_update_proto(
16226 &project,
16227 pending_update.borrow_mut().take().unwrap(),
16228 window,
16229 cx,
16230 )
16231 })
16232 .unwrap()
16233 .await
16234 .unwrap();
16235 assert_eq!(
16236 follower
16237 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16238 .unwrap(),
16239 gpui::Point::new(1.5, 3.5)
16240 );
16241 assert!(*is_still_following.borrow());
16242 assert_eq!(*follower_edit_event_count.borrow(), 0);
16243
16244 // Update the selections and scroll position. The follower's scroll position is updated
16245 // via autoscroll, not via the leader's exact scroll position.
16246 _ = leader.update(cx, |leader, window, cx| {
16247 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16248 s.select_ranges([0..0])
16249 });
16250 leader.request_autoscroll(Autoscroll::newest(), cx);
16251 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16252 });
16253 follower
16254 .update(cx, |follower, window, cx| {
16255 follower.apply_update_proto(
16256 &project,
16257 pending_update.borrow_mut().take().unwrap(),
16258 window,
16259 cx,
16260 )
16261 })
16262 .unwrap()
16263 .await
16264 .unwrap();
16265 _ = follower.update(cx, |follower, _, cx| {
16266 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16267 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16268 });
16269 assert!(*is_still_following.borrow());
16270
16271 // Creating a pending selection that precedes another selection
16272 _ = leader.update(cx, |leader, window, cx| {
16273 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16274 s.select_ranges([1..1])
16275 });
16276 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16277 });
16278 follower
16279 .update(cx, |follower, window, cx| {
16280 follower.apply_update_proto(
16281 &project,
16282 pending_update.borrow_mut().take().unwrap(),
16283 window,
16284 cx,
16285 )
16286 })
16287 .unwrap()
16288 .await
16289 .unwrap();
16290 _ = follower.update(cx, |follower, _, cx| {
16291 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16292 });
16293 assert!(*is_still_following.borrow());
16294
16295 // Extend the pending selection so that it surrounds another selection
16296 _ = leader.update(cx, |leader, window, cx| {
16297 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16298 });
16299 follower
16300 .update(cx, |follower, window, cx| {
16301 follower.apply_update_proto(
16302 &project,
16303 pending_update.borrow_mut().take().unwrap(),
16304 window,
16305 cx,
16306 )
16307 })
16308 .unwrap()
16309 .await
16310 .unwrap();
16311 _ = follower.update(cx, |follower, _, cx| {
16312 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16313 });
16314
16315 // Scrolling locally breaks the follow
16316 _ = follower.update(cx, |follower, window, cx| {
16317 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16318 follower.set_scroll_anchor(
16319 ScrollAnchor {
16320 anchor: top_anchor,
16321 offset: gpui::Point::new(0.0, 0.5),
16322 },
16323 window,
16324 cx,
16325 );
16326 });
16327 assert!(!(*is_still_following.borrow()));
16328}
16329
16330#[gpui::test]
16331async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16332 init_test(cx, |_| {});
16333
16334 let fs = FakeFs::new(cx.executor());
16335 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16336 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16337 let pane = workspace
16338 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16339 .unwrap();
16340
16341 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16342
16343 let leader = pane.update_in(cx, |_, window, cx| {
16344 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16345 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16346 });
16347
16348 // Start following the editor when it has no excerpts.
16349 let mut state_message =
16350 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16351 let workspace_entity = workspace.root(cx).unwrap();
16352 let follower_1 = cx
16353 .update_window(*workspace.deref(), |_, window, cx| {
16354 Editor::from_state_proto(
16355 workspace_entity,
16356 ViewId {
16357 creator: CollaboratorId::PeerId(PeerId::default()),
16358 id: 0,
16359 },
16360 &mut state_message,
16361 window,
16362 cx,
16363 )
16364 })
16365 .unwrap()
16366 .unwrap()
16367 .await
16368 .unwrap();
16369
16370 let update_message = Rc::new(RefCell::new(None));
16371 follower_1.update_in(cx, {
16372 let update = update_message.clone();
16373 |_, window, cx| {
16374 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16375 leader.read(cx).add_event_to_update_proto(
16376 event,
16377 &mut update.borrow_mut(),
16378 window,
16379 cx,
16380 );
16381 })
16382 .detach();
16383 }
16384 });
16385
16386 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16387 (
16388 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16389 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16390 )
16391 });
16392
16393 // Insert some excerpts.
16394 leader.update(cx, |leader, cx| {
16395 leader.buffer.update(cx, |multibuffer, cx| {
16396 multibuffer.set_excerpts_for_path(
16397 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16398 buffer_1.clone(),
16399 vec![
16400 Point::row_range(0..3),
16401 Point::row_range(1..6),
16402 Point::row_range(12..15),
16403 ],
16404 0,
16405 cx,
16406 );
16407 multibuffer.set_excerpts_for_path(
16408 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16409 buffer_2.clone(),
16410 vec![Point::row_range(0..6), Point::row_range(8..12)],
16411 0,
16412 cx,
16413 );
16414 });
16415 });
16416
16417 // Apply the update of adding the excerpts.
16418 follower_1
16419 .update_in(cx, |follower, window, cx| {
16420 follower.apply_update_proto(
16421 &project,
16422 update_message.borrow().clone().unwrap(),
16423 window,
16424 cx,
16425 )
16426 })
16427 .await
16428 .unwrap();
16429 assert_eq!(
16430 follower_1.update(cx, |editor, cx| editor.text(cx)),
16431 leader.update(cx, |editor, cx| editor.text(cx))
16432 );
16433 update_message.borrow_mut().take();
16434
16435 // Start following separately after it already has excerpts.
16436 let mut state_message =
16437 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16438 let workspace_entity = workspace.root(cx).unwrap();
16439 let follower_2 = cx
16440 .update_window(*workspace.deref(), |_, window, cx| {
16441 Editor::from_state_proto(
16442 workspace_entity,
16443 ViewId {
16444 creator: CollaboratorId::PeerId(PeerId::default()),
16445 id: 0,
16446 },
16447 &mut state_message,
16448 window,
16449 cx,
16450 )
16451 })
16452 .unwrap()
16453 .unwrap()
16454 .await
16455 .unwrap();
16456 assert_eq!(
16457 follower_2.update(cx, |editor, cx| editor.text(cx)),
16458 leader.update(cx, |editor, cx| editor.text(cx))
16459 );
16460
16461 // Remove some excerpts.
16462 leader.update(cx, |leader, cx| {
16463 leader.buffer.update(cx, |multibuffer, cx| {
16464 let excerpt_ids = multibuffer.excerpt_ids();
16465 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16466 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16467 });
16468 });
16469
16470 // Apply the update of removing the excerpts.
16471 follower_1
16472 .update_in(cx, |follower, window, cx| {
16473 follower.apply_update_proto(
16474 &project,
16475 update_message.borrow().clone().unwrap(),
16476 window,
16477 cx,
16478 )
16479 })
16480 .await
16481 .unwrap();
16482 follower_2
16483 .update_in(cx, |follower, window, cx| {
16484 follower.apply_update_proto(
16485 &project,
16486 update_message.borrow().clone().unwrap(),
16487 window,
16488 cx,
16489 )
16490 })
16491 .await
16492 .unwrap();
16493 update_message.borrow_mut().take();
16494 assert_eq!(
16495 follower_1.update(cx, |editor, cx| editor.text(cx)),
16496 leader.update(cx, |editor, cx| editor.text(cx))
16497 );
16498}
16499
16500#[gpui::test]
16501async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16502 init_test(cx, |_| {});
16503
16504 let mut cx = EditorTestContext::new(cx).await;
16505 let lsp_store =
16506 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16507
16508 cx.set_state(indoc! {"
16509 ˇfn func(abc def: i32) -> u32 {
16510 }
16511 "});
16512
16513 cx.update(|_, cx| {
16514 lsp_store.update(cx, |lsp_store, cx| {
16515 lsp_store
16516 .update_diagnostics(
16517 LanguageServerId(0),
16518 lsp::PublishDiagnosticsParams {
16519 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16520 version: None,
16521 diagnostics: vec![
16522 lsp::Diagnostic {
16523 range: lsp::Range::new(
16524 lsp::Position::new(0, 11),
16525 lsp::Position::new(0, 12),
16526 ),
16527 severity: Some(lsp::DiagnosticSeverity::ERROR),
16528 ..Default::default()
16529 },
16530 lsp::Diagnostic {
16531 range: lsp::Range::new(
16532 lsp::Position::new(0, 12),
16533 lsp::Position::new(0, 15),
16534 ),
16535 severity: Some(lsp::DiagnosticSeverity::ERROR),
16536 ..Default::default()
16537 },
16538 lsp::Diagnostic {
16539 range: lsp::Range::new(
16540 lsp::Position::new(0, 25),
16541 lsp::Position::new(0, 28),
16542 ),
16543 severity: Some(lsp::DiagnosticSeverity::ERROR),
16544 ..Default::default()
16545 },
16546 ],
16547 },
16548 None,
16549 DiagnosticSourceKind::Pushed,
16550 &[],
16551 cx,
16552 )
16553 .unwrap()
16554 });
16555 });
16556
16557 executor.run_until_parked();
16558
16559 cx.update_editor(|editor, window, cx| {
16560 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16561 });
16562
16563 cx.assert_editor_state(indoc! {"
16564 fn func(abc def: i32) -> ˇu32 {
16565 }
16566 "});
16567
16568 cx.update_editor(|editor, window, cx| {
16569 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16570 });
16571
16572 cx.assert_editor_state(indoc! {"
16573 fn func(abc ˇdef: i32) -> u32 {
16574 }
16575 "});
16576
16577 cx.update_editor(|editor, window, cx| {
16578 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16579 });
16580
16581 cx.assert_editor_state(indoc! {"
16582 fn func(abcˇ def: i32) -> u32 {
16583 }
16584 "});
16585
16586 cx.update_editor(|editor, window, cx| {
16587 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16588 });
16589
16590 cx.assert_editor_state(indoc! {"
16591 fn func(abc def: i32) -> ˇu32 {
16592 }
16593 "});
16594}
16595
16596#[gpui::test]
16597async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16598 init_test(cx, |_| {});
16599
16600 let mut cx = EditorTestContext::new(cx).await;
16601
16602 let diff_base = r#"
16603 use some::mod;
16604
16605 const A: u32 = 42;
16606
16607 fn main() {
16608 println!("hello");
16609
16610 println!("world");
16611 }
16612 "#
16613 .unindent();
16614
16615 // Edits are modified, removed, modified, added
16616 cx.set_state(
16617 &r#"
16618 use some::modified;
16619
16620 ˇ
16621 fn main() {
16622 println!("hello there");
16623
16624 println!("around the");
16625 println!("world");
16626 }
16627 "#
16628 .unindent(),
16629 );
16630
16631 cx.set_head_text(&diff_base);
16632 executor.run_until_parked();
16633
16634 cx.update_editor(|editor, window, cx| {
16635 //Wrap around the bottom of the buffer
16636 for _ in 0..3 {
16637 editor.go_to_next_hunk(&GoToHunk, window, cx);
16638 }
16639 });
16640
16641 cx.assert_editor_state(
16642 &r#"
16643 ˇuse some::modified;
16644
16645
16646 fn main() {
16647 println!("hello there");
16648
16649 println!("around the");
16650 println!("world");
16651 }
16652 "#
16653 .unindent(),
16654 );
16655
16656 cx.update_editor(|editor, window, cx| {
16657 //Wrap around the top of the buffer
16658 for _ in 0..2 {
16659 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16660 }
16661 });
16662
16663 cx.assert_editor_state(
16664 &r#"
16665 use some::modified;
16666
16667
16668 fn main() {
16669 ˇ println!("hello there");
16670
16671 println!("around the");
16672 println!("world");
16673 }
16674 "#
16675 .unindent(),
16676 );
16677
16678 cx.update_editor(|editor, window, cx| {
16679 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16680 });
16681
16682 cx.assert_editor_state(
16683 &r#"
16684 use some::modified;
16685
16686 ˇ
16687 fn main() {
16688 println!("hello there");
16689
16690 println!("around the");
16691 println!("world");
16692 }
16693 "#
16694 .unindent(),
16695 );
16696
16697 cx.update_editor(|editor, window, cx| {
16698 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16699 });
16700
16701 cx.assert_editor_state(
16702 &r#"
16703 ˇuse some::modified;
16704
16705
16706 fn main() {
16707 println!("hello there");
16708
16709 println!("around the");
16710 println!("world");
16711 }
16712 "#
16713 .unindent(),
16714 );
16715
16716 cx.update_editor(|editor, window, cx| {
16717 for _ in 0..2 {
16718 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16719 }
16720 });
16721
16722 cx.assert_editor_state(
16723 &r#"
16724 use some::modified;
16725
16726
16727 fn main() {
16728 ˇ println!("hello there");
16729
16730 println!("around the");
16731 println!("world");
16732 }
16733 "#
16734 .unindent(),
16735 );
16736
16737 cx.update_editor(|editor, window, cx| {
16738 editor.fold(&Fold, window, cx);
16739 });
16740
16741 cx.update_editor(|editor, window, cx| {
16742 editor.go_to_next_hunk(&GoToHunk, window, cx);
16743 });
16744
16745 cx.assert_editor_state(
16746 &r#"
16747 ˇuse some::modified;
16748
16749
16750 fn main() {
16751 println!("hello there");
16752
16753 println!("around the");
16754 println!("world");
16755 }
16756 "#
16757 .unindent(),
16758 );
16759}
16760
16761#[test]
16762fn test_split_words() {
16763 fn split(text: &str) -> Vec<&str> {
16764 split_words(text).collect()
16765 }
16766
16767 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16768 assert_eq!(split("hello_world"), &["hello_", "world"]);
16769 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16770 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16771 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16772 assert_eq!(split("helloworld"), &["helloworld"]);
16773
16774 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16775}
16776
16777#[gpui::test]
16778async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16779 init_test(cx, |_| {});
16780
16781 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16782 let mut assert = |before, after| {
16783 let _state_context = cx.set_state(before);
16784 cx.run_until_parked();
16785 cx.update_editor(|editor, window, cx| {
16786 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16787 });
16788 cx.run_until_parked();
16789 cx.assert_editor_state(after);
16790 };
16791
16792 // Outside bracket jumps to outside of matching bracket
16793 assert("console.logˇ(var);", "console.log(var)ˇ;");
16794 assert("console.log(var)ˇ;", "console.logˇ(var);");
16795
16796 // Inside bracket jumps to inside of matching bracket
16797 assert("console.log(ˇvar);", "console.log(varˇ);");
16798 assert("console.log(varˇ);", "console.log(ˇvar);");
16799
16800 // When outside a bracket and inside, favor jumping to the inside bracket
16801 assert(
16802 "console.log('foo', [1, 2, 3]ˇ);",
16803 "console.log(ˇ'foo', [1, 2, 3]);",
16804 );
16805 assert(
16806 "console.log(ˇ'foo', [1, 2, 3]);",
16807 "console.log('foo', [1, 2, 3]ˇ);",
16808 );
16809
16810 // Bias forward if two options are equally likely
16811 assert(
16812 "let result = curried_fun()ˇ();",
16813 "let result = curried_fun()()ˇ;",
16814 );
16815
16816 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16817 assert(
16818 indoc! {"
16819 function test() {
16820 console.log('test')ˇ
16821 }"},
16822 indoc! {"
16823 function test() {
16824 console.logˇ('test')
16825 }"},
16826 );
16827}
16828
16829#[gpui::test]
16830async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16831 init_test(cx, |_| {});
16832
16833 let fs = FakeFs::new(cx.executor());
16834 fs.insert_tree(
16835 path!("/a"),
16836 json!({
16837 "main.rs": "fn main() { let a = 5; }",
16838 "other.rs": "// Test file",
16839 }),
16840 )
16841 .await;
16842 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16843
16844 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16845 language_registry.add(Arc::new(Language::new(
16846 LanguageConfig {
16847 name: "Rust".into(),
16848 matcher: LanguageMatcher {
16849 path_suffixes: vec!["rs".to_string()],
16850 ..Default::default()
16851 },
16852 brackets: BracketPairConfig {
16853 pairs: vec![BracketPair {
16854 start: "{".to_string(),
16855 end: "}".to_string(),
16856 close: true,
16857 surround: true,
16858 newline: true,
16859 }],
16860 disabled_scopes_by_bracket_ix: Vec::new(),
16861 },
16862 ..Default::default()
16863 },
16864 Some(tree_sitter_rust::LANGUAGE.into()),
16865 )));
16866 let mut fake_servers = language_registry.register_fake_lsp(
16867 "Rust",
16868 FakeLspAdapter {
16869 capabilities: lsp::ServerCapabilities {
16870 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16871 first_trigger_character: "{".to_string(),
16872 more_trigger_character: None,
16873 }),
16874 ..Default::default()
16875 },
16876 ..Default::default()
16877 },
16878 );
16879
16880 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16881
16882 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16883
16884 let worktree_id = workspace
16885 .update(cx, |workspace, _, cx| {
16886 workspace.project().update(cx, |project, cx| {
16887 project.worktrees(cx).next().unwrap().read(cx).id()
16888 })
16889 })
16890 .unwrap();
16891
16892 let buffer = project
16893 .update(cx, |project, cx| {
16894 project.open_local_buffer(path!("/a/main.rs"), cx)
16895 })
16896 .await
16897 .unwrap();
16898 let editor_handle = workspace
16899 .update(cx, |workspace, window, cx| {
16900 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16901 })
16902 .unwrap()
16903 .await
16904 .unwrap()
16905 .downcast::<Editor>()
16906 .unwrap();
16907
16908 cx.executor().start_waiting();
16909 let fake_server = fake_servers.next().await.unwrap();
16910
16911 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16912 |params, _| async move {
16913 assert_eq!(
16914 params.text_document_position.text_document.uri,
16915 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16916 );
16917 assert_eq!(
16918 params.text_document_position.position,
16919 lsp::Position::new(0, 21),
16920 );
16921
16922 Ok(Some(vec![lsp::TextEdit {
16923 new_text: "]".to_string(),
16924 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16925 }]))
16926 },
16927 );
16928
16929 editor_handle.update_in(cx, |editor, window, cx| {
16930 window.focus(&editor.focus_handle(cx));
16931 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16932 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16933 });
16934 editor.handle_input("{", window, cx);
16935 });
16936
16937 cx.executor().run_until_parked();
16938
16939 buffer.update(cx, |buffer, _| {
16940 assert_eq!(
16941 buffer.text(),
16942 "fn main() { let a = {5}; }",
16943 "No extra braces from on type formatting should appear in the buffer"
16944 )
16945 });
16946}
16947
16948#[gpui::test(iterations = 20, seeds(31))]
16949async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16950 init_test(cx, |_| {});
16951
16952 let mut cx = EditorLspTestContext::new_rust(
16953 lsp::ServerCapabilities {
16954 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16955 first_trigger_character: ".".to_string(),
16956 more_trigger_character: None,
16957 }),
16958 ..Default::default()
16959 },
16960 cx,
16961 )
16962 .await;
16963
16964 cx.update_buffer(|buffer, _| {
16965 // This causes autoindent to be async.
16966 buffer.set_sync_parse_timeout(Duration::ZERO)
16967 });
16968
16969 cx.set_state("fn c() {\n d()ˇ\n}\n");
16970 cx.simulate_keystroke("\n");
16971 cx.run_until_parked();
16972
16973 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16974 let mut request =
16975 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16976 let buffer_cloned = buffer_cloned.clone();
16977 async move {
16978 buffer_cloned.update(&mut cx, |buffer, _| {
16979 assert_eq!(
16980 buffer.text(),
16981 "fn c() {\n d()\n .\n}\n",
16982 "OnTypeFormatting should triggered after autoindent applied"
16983 )
16984 })?;
16985
16986 Ok(Some(vec![]))
16987 }
16988 });
16989
16990 cx.simulate_keystroke(".");
16991 cx.run_until_parked();
16992
16993 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16994 assert!(request.next().await.is_some());
16995 request.close();
16996 assert!(request.next().await.is_none());
16997}
16998
16999#[gpui::test]
17000async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17001 init_test(cx, |_| {});
17002
17003 let fs = FakeFs::new(cx.executor());
17004 fs.insert_tree(
17005 path!("/a"),
17006 json!({
17007 "main.rs": "fn main() { let a = 5; }",
17008 "other.rs": "// Test file",
17009 }),
17010 )
17011 .await;
17012
17013 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17014
17015 let server_restarts = Arc::new(AtomicUsize::new(0));
17016 let closure_restarts = Arc::clone(&server_restarts);
17017 let language_server_name = "test language server";
17018 let language_name: LanguageName = "Rust".into();
17019
17020 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17021 language_registry.add(Arc::new(Language::new(
17022 LanguageConfig {
17023 name: language_name.clone(),
17024 matcher: LanguageMatcher {
17025 path_suffixes: vec!["rs".to_string()],
17026 ..Default::default()
17027 },
17028 ..Default::default()
17029 },
17030 Some(tree_sitter_rust::LANGUAGE.into()),
17031 )));
17032 let mut fake_servers = language_registry.register_fake_lsp(
17033 "Rust",
17034 FakeLspAdapter {
17035 name: language_server_name,
17036 initialization_options: Some(json!({
17037 "testOptionValue": true
17038 })),
17039 initializer: Some(Box::new(move |fake_server| {
17040 let task_restarts = Arc::clone(&closure_restarts);
17041 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17042 task_restarts.fetch_add(1, atomic::Ordering::Release);
17043 futures::future::ready(Ok(()))
17044 });
17045 })),
17046 ..Default::default()
17047 },
17048 );
17049
17050 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17051 let _buffer = project
17052 .update(cx, |project, cx| {
17053 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17054 })
17055 .await
17056 .unwrap();
17057 let _fake_server = fake_servers.next().await.unwrap();
17058 update_test_language_settings(cx, |language_settings| {
17059 language_settings.languages.0.insert(
17060 language_name.clone().0,
17061 LanguageSettingsContent {
17062 tab_size: NonZeroU32::new(8),
17063 ..Default::default()
17064 },
17065 );
17066 });
17067 cx.executor().run_until_parked();
17068 assert_eq!(
17069 server_restarts.load(atomic::Ordering::Acquire),
17070 0,
17071 "Should not restart LSP server on an unrelated change"
17072 );
17073
17074 update_test_project_settings(cx, |project_settings| {
17075 project_settings.lsp.insert(
17076 "Some other server name".into(),
17077 LspSettings {
17078 binary: None,
17079 settings: None,
17080 initialization_options: Some(json!({
17081 "some other init value": false
17082 })),
17083 enable_lsp_tasks: false,
17084 fetch: None,
17085 },
17086 );
17087 });
17088 cx.executor().run_until_parked();
17089 assert_eq!(
17090 server_restarts.load(atomic::Ordering::Acquire),
17091 0,
17092 "Should not restart LSP server on an unrelated LSP settings change"
17093 );
17094
17095 update_test_project_settings(cx, |project_settings| {
17096 project_settings.lsp.insert(
17097 language_server_name.into(),
17098 LspSettings {
17099 binary: None,
17100 settings: None,
17101 initialization_options: Some(json!({
17102 "anotherInitValue": false
17103 })),
17104 enable_lsp_tasks: false,
17105 fetch: None,
17106 },
17107 );
17108 });
17109 cx.executor().run_until_parked();
17110 assert_eq!(
17111 server_restarts.load(atomic::Ordering::Acquire),
17112 1,
17113 "Should restart LSP server on a related LSP settings change"
17114 );
17115
17116 update_test_project_settings(cx, |project_settings| {
17117 project_settings.lsp.insert(
17118 language_server_name.into(),
17119 LspSettings {
17120 binary: None,
17121 settings: None,
17122 initialization_options: Some(json!({
17123 "anotherInitValue": false
17124 })),
17125 enable_lsp_tasks: false,
17126 fetch: None,
17127 },
17128 );
17129 });
17130 cx.executor().run_until_parked();
17131 assert_eq!(
17132 server_restarts.load(atomic::Ordering::Acquire),
17133 1,
17134 "Should not restart LSP server on a related LSP settings change that is the same"
17135 );
17136
17137 update_test_project_settings(cx, |project_settings| {
17138 project_settings.lsp.insert(
17139 language_server_name.into(),
17140 LspSettings {
17141 binary: None,
17142 settings: None,
17143 initialization_options: None,
17144 enable_lsp_tasks: false,
17145 fetch: None,
17146 },
17147 );
17148 });
17149 cx.executor().run_until_parked();
17150 assert_eq!(
17151 server_restarts.load(atomic::Ordering::Acquire),
17152 2,
17153 "Should restart LSP server on another related LSP settings change"
17154 );
17155}
17156
17157#[gpui::test]
17158async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17159 init_test(cx, |_| {});
17160
17161 let mut cx = EditorLspTestContext::new_rust(
17162 lsp::ServerCapabilities {
17163 completion_provider: Some(lsp::CompletionOptions {
17164 trigger_characters: Some(vec![".".to_string()]),
17165 resolve_provider: Some(true),
17166 ..Default::default()
17167 }),
17168 ..Default::default()
17169 },
17170 cx,
17171 )
17172 .await;
17173
17174 cx.set_state("fn main() { let a = 2ˇ; }");
17175 cx.simulate_keystroke(".");
17176 let completion_item = lsp::CompletionItem {
17177 label: "some".into(),
17178 kind: Some(lsp::CompletionItemKind::SNIPPET),
17179 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17180 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17181 kind: lsp::MarkupKind::Markdown,
17182 value: "```rust\nSome(2)\n```".to_string(),
17183 })),
17184 deprecated: Some(false),
17185 sort_text: Some("fffffff2".to_string()),
17186 filter_text: Some("some".to_string()),
17187 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17188 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17189 range: lsp::Range {
17190 start: lsp::Position {
17191 line: 0,
17192 character: 22,
17193 },
17194 end: lsp::Position {
17195 line: 0,
17196 character: 22,
17197 },
17198 },
17199 new_text: "Some(2)".to_string(),
17200 })),
17201 additional_text_edits: Some(vec![lsp::TextEdit {
17202 range: lsp::Range {
17203 start: lsp::Position {
17204 line: 0,
17205 character: 20,
17206 },
17207 end: lsp::Position {
17208 line: 0,
17209 character: 22,
17210 },
17211 },
17212 new_text: "".to_string(),
17213 }]),
17214 ..Default::default()
17215 };
17216
17217 let closure_completion_item = completion_item.clone();
17218 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17219 let task_completion_item = closure_completion_item.clone();
17220 async move {
17221 Ok(Some(lsp::CompletionResponse::Array(vec![
17222 task_completion_item,
17223 ])))
17224 }
17225 });
17226
17227 request.next().await;
17228
17229 cx.condition(|editor, _| editor.context_menu_visible())
17230 .await;
17231 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17232 editor
17233 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17234 .unwrap()
17235 });
17236 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17237
17238 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17239 let task_completion_item = completion_item.clone();
17240 async move { Ok(task_completion_item) }
17241 })
17242 .next()
17243 .await
17244 .unwrap();
17245 apply_additional_edits.await.unwrap();
17246 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17247}
17248
17249#[gpui::test]
17250async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17251 init_test(cx, |_| {});
17252
17253 let mut cx = EditorLspTestContext::new_rust(
17254 lsp::ServerCapabilities {
17255 completion_provider: Some(lsp::CompletionOptions {
17256 trigger_characters: Some(vec![".".to_string()]),
17257 resolve_provider: Some(true),
17258 ..Default::default()
17259 }),
17260 ..Default::default()
17261 },
17262 cx,
17263 )
17264 .await;
17265
17266 cx.set_state("fn main() { let a = 2ˇ; }");
17267 cx.simulate_keystroke(".");
17268
17269 let item1 = lsp::CompletionItem {
17270 label: "method id()".to_string(),
17271 filter_text: Some("id".to_string()),
17272 detail: None,
17273 documentation: None,
17274 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17275 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17276 new_text: ".id".to_string(),
17277 })),
17278 ..lsp::CompletionItem::default()
17279 };
17280
17281 let item2 = lsp::CompletionItem {
17282 label: "other".to_string(),
17283 filter_text: Some("other".to_string()),
17284 detail: None,
17285 documentation: None,
17286 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17287 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17288 new_text: ".other".to_string(),
17289 })),
17290 ..lsp::CompletionItem::default()
17291 };
17292
17293 let item1 = item1.clone();
17294 cx.set_request_handler::<lsp::request::Completion, _, _>({
17295 let item1 = item1.clone();
17296 move |_, _, _| {
17297 let item1 = item1.clone();
17298 let item2 = item2.clone();
17299 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17300 }
17301 })
17302 .next()
17303 .await;
17304
17305 cx.condition(|editor, _| editor.context_menu_visible())
17306 .await;
17307 cx.update_editor(|editor, _, _| {
17308 let context_menu = editor.context_menu.borrow_mut();
17309 let context_menu = context_menu
17310 .as_ref()
17311 .expect("Should have the context menu deployed");
17312 match context_menu {
17313 CodeContextMenu::Completions(completions_menu) => {
17314 let completions = completions_menu.completions.borrow_mut();
17315 assert_eq!(
17316 completions
17317 .iter()
17318 .map(|completion| &completion.label.text)
17319 .collect::<Vec<_>>(),
17320 vec!["method id()", "other"]
17321 )
17322 }
17323 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17324 }
17325 });
17326
17327 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17328 let item1 = item1.clone();
17329 move |_, item_to_resolve, _| {
17330 let item1 = item1.clone();
17331 async move {
17332 if item1 == item_to_resolve {
17333 Ok(lsp::CompletionItem {
17334 label: "method id()".to_string(),
17335 filter_text: Some("id".to_string()),
17336 detail: Some("Now resolved!".to_string()),
17337 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17338 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17339 range: lsp::Range::new(
17340 lsp::Position::new(0, 22),
17341 lsp::Position::new(0, 22),
17342 ),
17343 new_text: ".id".to_string(),
17344 })),
17345 ..lsp::CompletionItem::default()
17346 })
17347 } else {
17348 Ok(item_to_resolve)
17349 }
17350 }
17351 }
17352 })
17353 .next()
17354 .await
17355 .unwrap();
17356 cx.run_until_parked();
17357
17358 cx.update_editor(|editor, window, cx| {
17359 editor.context_menu_next(&Default::default(), window, cx);
17360 });
17361
17362 cx.update_editor(|editor, _, _| {
17363 let context_menu = editor.context_menu.borrow_mut();
17364 let context_menu = context_menu
17365 .as_ref()
17366 .expect("Should have the context menu deployed");
17367 match context_menu {
17368 CodeContextMenu::Completions(completions_menu) => {
17369 let completions = completions_menu.completions.borrow_mut();
17370 assert_eq!(
17371 completions
17372 .iter()
17373 .map(|completion| &completion.label.text)
17374 .collect::<Vec<_>>(),
17375 vec!["method id() Now resolved!", "other"],
17376 "Should update first completion label, but not second as the filter text did not match."
17377 );
17378 }
17379 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17380 }
17381 });
17382}
17383
17384#[gpui::test]
17385async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17386 init_test(cx, |_| {});
17387 let mut cx = EditorLspTestContext::new_rust(
17388 lsp::ServerCapabilities {
17389 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17390 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17391 completion_provider: Some(lsp::CompletionOptions {
17392 resolve_provider: Some(true),
17393 ..Default::default()
17394 }),
17395 ..Default::default()
17396 },
17397 cx,
17398 )
17399 .await;
17400 cx.set_state(indoc! {"
17401 struct TestStruct {
17402 field: i32
17403 }
17404
17405 fn mainˇ() {
17406 let unused_var = 42;
17407 let test_struct = TestStruct { field: 42 };
17408 }
17409 "});
17410 let symbol_range = cx.lsp_range(indoc! {"
17411 struct TestStruct {
17412 field: i32
17413 }
17414
17415 «fn main»() {
17416 let unused_var = 42;
17417 let test_struct = TestStruct { field: 42 };
17418 }
17419 "});
17420 let mut hover_requests =
17421 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17422 Ok(Some(lsp::Hover {
17423 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17424 kind: lsp::MarkupKind::Markdown,
17425 value: "Function documentation".to_string(),
17426 }),
17427 range: Some(symbol_range),
17428 }))
17429 });
17430
17431 // Case 1: Test that code action menu hide hover popover
17432 cx.dispatch_action(Hover);
17433 hover_requests.next().await;
17434 cx.condition(|editor, _| editor.hover_state.visible()).await;
17435 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17436 move |_, _, _| async move {
17437 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17438 lsp::CodeAction {
17439 title: "Remove unused variable".to_string(),
17440 kind: Some(CodeActionKind::QUICKFIX),
17441 edit: Some(lsp::WorkspaceEdit {
17442 changes: Some(
17443 [(
17444 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17445 vec![lsp::TextEdit {
17446 range: lsp::Range::new(
17447 lsp::Position::new(5, 4),
17448 lsp::Position::new(5, 27),
17449 ),
17450 new_text: "".to_string(),
17451 }],
17452 )]
17453 .into_iter()
17454 .collect(),
17455 ),
17456 ..Default::default()
17457 }),
17458 ..Default::default()
17459 },
17460 )]))
17461 },
17462 );
17463 cx.update_editor(|editor, window, cx| {
17464 editor.toggle_code_actions(
17465 &ToggleCodeActions {
17466 deployed_from: None,
17467 quick_launch: false,
17468 },
17469 window,
17470 cx,
17471 );
17472 });
17473 code_action_requests.next().await;
17474 cx.run_until_parked();
17475 cx.condition(|editor, _| editor.context_menu_visible())
17476 .await;
17477 cx.update_editor(|editor, _, _| {
17478 assert!(
17479 !editor.hover_state.visible(),
17480 "Hover popover should be hidden when code action menu is shown"
17481 );
17482 // Hide code actions
17483 editor.context_menu.take();
17484 });
17485
17486 // Case 2: Test that code completions hide hover popover
17487 cx.dispatch_action(Hover);
17488 hover_requests.next().await;
17489 cx.condition(|editor, _| editor.hover_state.visible()).await;
17490 let counter = Arc::new(AtomicUsize::new(0));
17491 let mut completion_requests =
17492 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17493 let counter = counter.clone();
17494 async move {
17495 counter.fetch_add(1, atomic::Ordering::Release);
17496 Ok(Some(lsp::CompletionResponse::Array(vec![
17497 lsp::CompletionItem {
17498 label: "main".into(),
17499 kind: Some(lsp::CompletionItemKind::FUNCTION),
17500 detail: Some("() -> ()".to_string()),
17501 ..Default::default()
17502 },
17503 lsp::CompletionItem {
17504 label: "TestStruct".into(),
17505 kind: Some(lsp::CompletionItemKind::STRUCT),
17506 detail: Some("struct TestStruct".to_string()),
17507 ..Default::default()
17508 },
17509 ])))
17510 }
17511 });
17512 cx.update_editor(|editor, window, cx| {
17513 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17514 });
17515 completion_requests.next().await;
17516 cx.condition(|editor, _| editor.context_menu_visible())
17517 .await;
17518 cx.update_editor(|editor, _, _| {
17519 assert!(
17520 !editor.hover_state.visible(),
17521 "Hover popover should be hidden when completion menu is shown"
17522 );
17523 });
17524}
17525
17526#[gpui::test]
17527async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17528 init_test(cx, |_| {});
17529
17530 let mut cx = EditorLspTestContext::new_rust(
17531 lsp::ServerCapabilities {
17532 completion_provider: Some(lsp::CompletionOptions {
17533 trigger_characters: Some(vec![".".to_string()]),
17534 resolve_provider: Some(true),
17535 ..Default::default()
17536 }),
17537 ..Default::default()
17538 },
17539 cx,
17540 )
17541 .await;
17542
17543 cx.set_state("fn main() { let a = 2ˇ; }");
17544 cx.simulate_keystroke(".");
17545
17546 let unresolved_item_1 = lsp::CompletionItem {
17547 label: "id".to_string(),
17548 filter_text: Some("id".to_string()),
17549 detail: None,
17550 documentation: None,
17551 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17552 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17553 new_text: ".id".to_string(),
17554 })),
17555 ..lsp::CompletionItem::default()
17556 };
17557 let resolved_item_1 = lsp::CompletionItem {
17558 additional_text_edits: Some(vec![lsp::TextEdit {
17559 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17560 new_text: "!!".to_string(),
17561 }]),
17562 ..unresolved_item_1.clone()
17563 };
17564 let unresolved_item_2 = lsp::CompletionItem {
17565 label: "other".to_string(),
17566 filter_text: Some("other".to_string()),
17567 detail: None,
17568 documentation: None,
17569 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17570 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17571 new_text: ".other".to_string(),
17572 })),
17573 ..lsp::CompletionItem::default()
17574 };
17575 let resolved_item_2 = lsp::CompletionItem {
17576 additional_text_edits: Some(vec![lsp::TextEdit {
17577 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17578 new_text: "??".to_string(),
17579 }]),
17580 ..unresolved_item_2.clone()
17581 };
17582
17583 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17584 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17585 cx.lsp
17586 .server
17587 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17588 let unresolved_item_1 = unresolved_item_1.clone();
17589 let resolved_item_1 = resolved_item_1.clone();
17590 let unresolved_item_2 = unresolved_item_2.clone();
17591 let resolved_item_2 = resolved_item_2.clone();
17592 let resolve_requests_1 = resolve_requests_1.clone();
17593 let resolve_requests_2 = resolve_requests_2.clone();
17594 move |unresolved_request, _| {
17595 let unresolved_item_1 = unresolved_item_1.clone();
17596 let resolved_item_1 = resolved_item_1.clone();
17597 let unresolved_item_2 = unresolved_item_2.clone();
17598 let resolved_item_2 = resolved_item_2.clone();
17599 let resolve_requests_1 = resolve_requests_1.clone();
17600 let resolve_requests_2 = resolve_requests_2.clone();
17601 async move {
17602 if unresolved_request == unresolved_item_1 {
17603 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17604 Ok(resolved_item_1.clone())
17605 } else if unresolved_request == unresolved_item_2 {
17606 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17607 Ok(resolved_item_2.clone())
17608 } else {
17609 panic!("Unexpected completion item {unresolved_request:?}")
17610 }
17611 }
17612 }
17613 })
17614 .detach();
17615
17616 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17617 let unresolved_item_1 = unresolved_item_1.clone();
17618 let unresolved_item_2 = unresolved_item_2.clone();
17619 async move {
17620 Ok(Some(lsp::CompletionResponse::Array(vec![
17621 unresolved_item_1,
17622 unresolved_item_2,
17623 ])))
17624 }
17625 })
17626 .next()
17627 .await;
17628
17629 cx.condition(|editor, _| editor.context_menu_visible())
17630 .await;
17631 cx.update_editor(|editor, _, _| {
17632 let context_menu = editor.context_menu.borrow_mut();
17633 let context_menu = context_menu
17634 .as_ref()
17635 .expect("Should have the context menu deployed");
17636 match context_menu {
17637 CodeContextMenu::Completions(completions_menu) => {
17638 let completions = completions_menu.completions.borrow_mut();
17639 assert_eq!(
17640 completions
17641 .iter()
17642 .map(|completion| &completion.label.text)
17643 .collect::<Vec<_>>(),
17644 vec!["id", "other"]
17645 )
17646 }
17647 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17648 }
17649 });
17650 cx.run_until_parked();
17651
17652 cx.update_editor(|editor, window, cx| {
17653 editor.context_menu_next(&ContextMenuNext, window, cx);
17654 });
17655 cx.run_until_parked();
17656 cx.update_editor(|editor, window, cx| {
17657 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17658 });
17659 cx.run_until_parked();
17660 cx.update_editor(|editor, window, cx| {
17661 editor.context_menu_next(&ContextMenuNext, window, cx);
17662 });
17663 cx.run_until_parked();
17664 cx.update_editor(|editor, window, cx| {
17665 editor
17666 .compose_completion(&ComposeCompletion::default(), window, cx)
17667 .expect("No task returned")
17668 })
17669 .await
17670 .expect("Completion failed");
17671 cx.run_until_parked();
17672
17673 cx.update_editor(|editor, _, cx| {
17674 assert_eq!(
17675 resolve_requests_1.load(atomic::Ordering::Acquire),
17676 1,
17677 "Should always resolve once despite multiple selections"
17678 );
17679 assert_eq!(
17680 resolve_requests_2.load(atomic::Ordering::Acquire),
17681 1,
17682 "Should always resolve once after multiple selections and applying the completion"
17683 );
17684 assert_eq!(
17685 editor.text(cx),
17686 "fn main() { let a = ??.other; }",
17687 "Should use resolved data when applying the completion"
17688 );
17689 });
17690}
17691
17692#[gpui::test]
17693async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17694 init_test(cx, |_| {});
17695
17696 let item_0 = lsp::CompletionItem {
17697 label: "abs".into(),
17698 insert_text: Some("abs".into()),
17699 data: Some(json!({ "very": "special"})),
17700 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17701 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17702 lsp::InsertReplaceEdit {
17703 new_text: "abs".to_string(),
17704 insert: lsp::Range::default(),
17705 replace: lsp::Range::default(),
17706 },
17707 )),
17708 ..lsp::CompletionItem::default()
17709 };
17710 let items = iter::once(item_0.clone())
17711 .chain((11..51).map(|i| lsp::CompletionItem {
17712 label: format!("item_{}", i),
17713 insert_text: Some(format!("item_{}", i)),
17714 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17715 ..lsp::CompletionItem::default()
17716 }))
17717 .collect::<Vec<_>>();
17718
17719 let default_commit_characters = vec!["?".to_string()];
17720 let default_data = json!({ "default": "data"});
17721 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17722 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17723 let default_edit_range = lsp::Range {
17724 start: lsp::Position {
17725 line: 0,
17726 character: 5,
17727 },
17728 end: lsp::Position {
17729 line: 0,
17730 character: 5,
17731 },
17732 };
17733
17734 let mut cx = EditorLspTestContext::new_rust(
17735 lsp::ServerCapabilities {
17736 completion_provider: Some(lsp::CompletionOptions {
17737 trigger_characters: Some(vec![".".to_string()]),
17738 resolve_provider: Some(true),
17739 ..Default::default()
17740 }),
17741 ..Default::default()
17742 },
17743 cx,
17744 )
17745 .await;
17746
17747 cx.set_state("fn main() { let a = 2ˇ; }");
17748 cx.simulate_keystroke(".");
17749
17750 let completion_data = default_data.clone();
17751 let completion_characters = default_commit_characters.clone();
17752 let completion_items = items.clone();
17753 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17754 let default_data = completion_data.clone();
17755 let default_commit_characters = completion_characters.clone();
17756 let items = completion_items.clone();
17757 async move {
17758 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17759 items,
17760 item_defaults: Some(lsp::CompletionListItemDefaults {
17761 data: Some(default_data.clone()),
17762 commit_characters: Some(default_commit_characters.clone()),
17763 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17764 default_edit_range,
17765 )),
17766 insert_text_format: Some(default_insert_text_format),
17767 insert_text_mode: Some(default_insert_text_mode),
17768 }),
17769 ..lsp::CompletionList::default()
17770 })))
17771 }
17772 })
17773 .next()
17774 .await;
17775
17776 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17777 cx.lsp
17778 .server
17779 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17780 let closure_resolved_items = resolved_items.clone();
17781 move |item_to_resolve, _| {
17782 let closure_resolved_items = closure_resolved_items.clone();
17783 async move {
17784 closure_resolved_items.lock().push(item_to_resolve.clone());
17785 Ok(item_to_resolve)
17786 }
17787 }
17788 })
17789 .detach();
17790
17791 cx.condition(|editor, _| editor.context_menu_visible())
17792 .await;
17793 cx.run_until_parked();
17794 cx.update_editor(|editor, _, _| {
17795 let menu = editor.context_menu.borrow_mut();
17796 match menu.as_ref().expect("should have the completions menu") {
17797 CodeContextMenu::Completions(completions_menu) => {
17798 assert_eq!(
17799 completions_menu
17800 .entries
17801 .borrow()
17802 .iter()
17803 .map(|mat| mat.string.clone())
17804 .collect::<Vec<String>>(),
17805 items
17806 .iter()
17807 .map(|completion| completion.label.clone())
17808 .collect::<Vec<String>>()
17809 );
17810 }
17811 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17812 }
17813 });
17814 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17815 // with 4 from the end.
17816 assert_eq!(
17817 *resolved_items.lock(),
17818 [&items[0..16], &items[items.len() - 4..items.len()]]
17819 .concat()
17820 .iter()
17821 .cloned()
17822 .map(|mut item| {
17823 if item.data.is_none() {
17824 item.data = Some(default_data.clone());
17825 }
17826 item
17827 })
17828 .collect::<Vec<lsp::CompletionItem>>(),
17829 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17830 );
17831 resolved_items.lock().clear();
17832
17833 cx.update_editor(|editor, window, cx| {
17834 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17835 });
17836 cx.run_until_parked();
17837 // Completions that have already been resolved are skipped.
17838 assert_eq!(
17839 *resolved_items.lock(),
17840 items[items.len() - 17..items.len() - 4]
17841 .iter()
17842 .cloned()
17843 .map(|mut item| {
17844 if item.data.is_none() {
17845 item.data = Some(default_data.clone());
17846 }
17847 item
17848 })
17849 .collect::<Vec<lsp::CompletionItem>>()
17850 );
17851 resolved_items.lock().clear();
17852}
17853
17854#[gpui::test]
17855async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17856 init_test(cx, |_| {});
17857
17858 let mut cx = EditorLspTestContext::new(
17859 Language::new(
17860 LanguageConfig {
17861 matcher: LanguageMatcher {
17862 path_suffixes: vec!["jsx".into()],
17863 ..Default::default()
17864 },
17865 overrides: [(
17866 "element".into(),
17867 LanguageConfigOverride {
17868 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17869 ..Default::default()
17870 },
17871 )]
17872 .into_iter()
17873 .collect(),
17874 ..Default::default()
17875 },
17876 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17877 )
17878 .with_override_query("(jsx_self_closing_element) @element")
17879 .unwrap(),
17880 lsp::ServerCapabilities {
17881 completion_provider: Some(lsp::CompletionOptions {
17882 trigger_characters: Some(vec![":".to_string()]),
17883 ..Default::default()
17884 }),
17885 ..Default::default()
17886 },
17887 cx,
17888 )
17889 .await;
17890
17891 cx.lsp
17892 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17893 Ok(Some(lsp::CompletionResponse::Array(vec![
17894 lsp::CompletionItem {
17895 label: "bg-blue".into(),
17896 ..Default::default()
17897 },
17898 lsp::CompletionItem {
17899 label: "bg-red".into(),
17900 ..Default::default()
17901 },
17902 lsp::CompletionItem {
17903 label: "bg-yellow".into(),
17904 ..Default::default()
17905 },
17906 ])))
17907 });
17908
17909 cx.set_state(r#"<p class="bgˇ" />"#);
17910
17911 // Trigger completion when typing a dash, because the dash is an extra
17912 // word character in the 'element' scope, which contains the cursor.
17913 cx.simulate_keystroke("-");
17914 cx.executor().run_until_parked();
17915 cx.update_editor(|editor, _, _| {
17916 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17917 {
17918 assert_eq!(
17919 completion_menu_entries(menu),
17920 &["bg-blue", "bg-red", "bg-yellow"]
17921 );
17922 } else {
17923 panic!("expected completion menu to be open");
17924 }
17925 });
17926
17927 cx.simulate_keystroke("l");
17928 cx.executor().run_until_parked();
17929 cx.update_editor(|editor, _, _| {
17930 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17931 {
17932 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17933 } else {
17934 panic!("expected completion menu to be open");
17935 }
17936 });
17937
17938 // When filtering completions, consider the character after the '-' to
17939 // be the start of a subword.
17940 cx.set_state(r#"<p class="yelˇ" />"#);
17941 cx.simulate_keystroke("l");
17942 cx.executor().run_until_parked();
17943 cx.update_editor(|editor, _, _| {
17944 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17945 {
17946 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17947 } else {
17948 panic!("expected completion menu to be open");
17949 }
17950 });
17951}
17952
17953fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17954 let entries = menu.entries.borrow();
17955 entries.iter().map(|mat| mat.string.clone()).collect()
17956}
17957
17958#[gpui::test]
17959async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17960 init_test(cx, |settings| {
17961 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17962 Formatter::Prettier,
17963 )))
17964 });
17965
17966 let fs = FakeFs::new(cx.executor());
17967 fs.insert_file(path!("/file.ts"), Default::default()).await;
17968
17969 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17970 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17971
17972 language_registry.add(Arc::new(Language::new(
17973 LanguageConfig {
17974 name: "TypeScript".into(),
17975 matcher: LanguageMatcher {
17976 path_suffixes: vec!["ts".to_string()],
17977 ..Default::default()
17978 },
17979 ..Default::default()
17980 },
17981 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17982 )));
17983 update_test_language_settings(cx, |settings| {
17984 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
17985 });
17986
17987 let test_plugin = "test_plugin";
17988 let _ = language_registry.register_fake_lsp(
17989 "TypeScript",
17990 FakeLspAdapter {
17991 prettier_plugins: vec![test_plugin],
17992 ..Default::default()
17993 },
17994 );
17995
17996 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17997 let buffer = project
17998 .update(cx, |project, cx| {
17999 project.open_local_buffer(path!("/file.ts"), cx)
18000 })
18001 .await
18002 .unwrap();
18003
18004 let buffer_text = "one\ntwo\nthree\n";
18005 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18006 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18007 editor.update_in(cx, |editor, window, cx| {
18008 editor.set_text(buffer_text, window, cx)
18009 });
18010
18011 editor
18012 .update_in(cx, |editor, window, cx| {
18013 editor.perform_format(
18014 project.clone(),
18015 FormatTrigger::Manual,
18016 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18017 window,
18018 cx,
18019 )
18020 })
18021 .unwrap()
18022 .await;
18023 assert_eq!(
18024 editor.update(cx, |editor, cx| editor.text(cx)),
18025 buffer_text.to_string() + prettier_format_suffix,
18026 "Test prettier formatting was not applied to the original buffer text",
18027 );
18028
18029 update_test_language_settings(cx, |settings| {
18030 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18031 });
18032 let format = editor.update_in(cx, |editor, window, cx| {
18033 editor.perform_format(
18034 project.clone(),
18035 FormatTrigger::Manual,
18036 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18037 window,
18038 cx,
18039 )
18040 });
18041 format.await.unwrap();
18042 assert_eq!(
18043 editor.update(cx, |editor, cx| editor.text(cx)),
18044 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18045 "Autoformatting (via test prettier) was not applied to the original buffer text",
18046 );
18047}
18048
18049#[gpui::test]
18050async fn test_addition_reverts(cx: &mut TestAppContext) {
18051 init_test(cx, |_| {});
18052 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18053 let base_text = indoc! {r#"
18054 struct Row;
18055 struct Row1;
18056 struct Row2;
18057
18058 struct Row4;
18059 struct Row5;
18060 struct Row6;
18061
18062 struct Row8;
18063 struct Row9;
18064 struct Row10;"#};
18065
18066 // When addition hunks are not adjacent to carets, no hunk revert is performed
18067 assert_hunk_revert(
18068 indoc! {r#"struct Row;
18069 struct Row1;
18070 struct Row1.1;
18071 struct Row1.2;
18072 struct Row2;ˇ
18073
18074 struct Row4;
18075 struct Row5;
18076 struct Row6;
18077
18078 struct Row8;
18079 ˇstruct Row9;
18080 struct Row9.1;
18081 struct Row9.2;
18082 struct Row9.3;
18083 struct Row10;"#},
18084 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18085 indoc! {r#"struct Row;
18086 struct Row1;
18087 struct Row1.1;
18088 struct Row1.2;
18089 struct Row2;ˇ
18090
18091 struct Row4;
18092 struct Row5;
18093 struct Row6;
18094
18095 struct Row8;
18096 ˇstruct Row9;
18097 struct Row9.1;
18098 struct Row9.2;
18099 struct Row9.3;
18100 struct Row10;"#},
18101 base_text,
18102 &mut cx,
18103 );
18104 // Same for selections
18105 assert_hunk_revert(
18106 indoc! {r#"struct Row;
18107 struct Row1;
18108 struct Row2;
18109 struct Row2.1;
18110 struct Row2.2;
18111 «ˇ
18112 struct Row4;
18113 struct» Row5;
18114 «struct Row6;
18115 ˇ»
18116 struct Row9.1;
18117 struct Row9.2;
18118 struct Row9.3;
18119 struct Row8;
18120 struct Row9;
18121 struct Row10;"#},
18122 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18123 indoc! {r#"struct Row;
18124 struct Row1;
18125 struct Row2;
18126 struct Row2.1;
18127 struct Row2.2;
18128 «ˇ
18129 struct Row4;
18130 struct» Row5;
18131 «struct Row6;
18132 ˇ»
18133 struct Row9.1;
18134 struct Row9.2;
18135 struct Row9.3;
18136 struct Row8;
18137 struct Row9;
18138 struct Row10;"#},
18139 base_text,
18140 &mut cx,
18141 );
18142
18143 // When carets and selections intersect the addition hunks, those are reverted.
18144 // Adjacent carets got merged.
18145 assert_hunk_revert(
18146 indoc! {r#"struct Row;
18147 ˇ// something on the top
18148 struct Row1;
18149 struct Row2;
18150 struct Roˇw3.1;
18151 struct Row2.2;
18152 struct Row2.3;ˇ
18153
18154 struct Row4;
18155 struct ˇRow5.1;
18156 struct Row5.2;
18157 struct «Rowˇ»5.3;
18158 struct Row5;
18159 struct Row6;
18160 ˇ
18161 struct Row9.1;
18162 struct «Rowˇ»9.2;
18163 struct «ˇRow»9.3;
18164 struct Row8;
18165 struct Row9;
18166 «ˇ// something on bottom»
18167 struct Row10;"#},
18168 vec![
18169 DiffHunkStatusKind::Added,
18170 DiffHunkStatusKind::Added,
18171 DiffHunkStatusKind::Added,
18172 DiffHunkStatusKind::Added,
18173 DiffHunkStatusKind::Added,
18174 ],
18175 indoc! {r#"struct Row;
18176 ˇstruct Row1;
18177 struct Row2;
18178 ˇ
18179 struct Row4;
18180 ˇstruct Row5;
18181 struct Row6;
18182 ˇ
18183 ˇstruct Row8;
18184 struct Row9;
18185 ˇstruct Row10;"#},
18186 base_text,
18187 &mut cx,
18188 );
18189}
18190
18191#[gpui::test]
18192async fn test_modification_reverts(cx: &mut TestAppContext) {
18193 init_test(cx, |_| {});
18194 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18195 let base_text = indoc! {r#"
18196 struct Row;
18197 struct Row1;
18198 struct Row2;
18199
18200 struct Row4;
18201 struct Row5;
18202 struct Row6;
18203
18204 struct Row8;
18205 struct Row9;
18206 struct Row10;"#};
18207
18208 // Modification hunks behave the same as the addition ones.
18209 assert_hunk_revert(
18210 indoc! {r#"struct Row;
18211 struct Row1;
18212 struct Row33;
18213 ˇ
18214 struct Row4;
18215 struct Row5;
18216 struct Row6;
18217 ˇ
18218 struct Row99;
18219 struct Row9;
18220 struct Row10;"#},
18221 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18222 indoc! {r#"struct Row;
18223 struct Row1;
18224 struct Row33;
18225 ˇ
18226 struct Row4;
18227 struct Row5;
18228 struct Row6;
18229 ˇ
18230 struct Row99;
18231 struct Row9;
18232 struct Row10;"#},
18233 base_text,
18234 &mut cx,
18235 );
18236 assert_hunk_revert(
18237 indoc! {r#"struct Row;
18238 struct Row1;
18239 struct Row33;
18240 «ˇ
18241 struct Row4;
18242 struct» Row5;
18243 «struct Row6;
18244 ˇ»
18245 struct Row99;
18246 struct Row9;
18247 struct Row10;"#},
18248 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18249 indoc! {r#"struct Row;
18250 struct Row1;
18251 struct Row33;
18252 «ˇ
18253 struct Row4;
18254 struct» Row5;
18255 «struct Row6;
18256 ˇ»
18257 struct Row99;
18258 struct Row9;
18259 struct Row10;"#},
18260 base_text,
18261 &mut cx,
18262 );
18263
18264 assert_hunk_revert(
18265 indoc! {r#"ˇstruct Row1.1;
18266 struct Row1;
18267 «ˇstr»uct Row22;
18268
18269 struct ˇRow44;
18270 struct Row5;
18271 struct «Rˇ»ow66;ˇ
18272
18273 «struˇ»ct Row88;
18274 struct Row9;
18275 struct Row1011;ˇ"#},
18276 vec![
18277 DiffHunkStatusKind::Modified,
18278 DiffHunkStatusKind::Modified,
18279 DiffHunkStatusKind::Modified,
18280 DiffHunkStatusKind::Modified,
18281 DiffHunkStatusKind::Modified,
18282 DiffHunkStatusKind::Modified,
18283 ],
18284 indoc! {r#"struct Row;
18285 ˇstruct Row1;
18286 struct Row2;
18287 ˇ
18288 struct Row4;
18289 ˇstruct Row5;
18290 struct Row6;
18291 ˇ
18292 struct Row8;
18293 ˇstruct Row9;
18294 struct Row10;ˇ"#},
18295 base_text,
18296 &mut cx,
18297 );
18298}
18299
18300#[gpui::test]
18301async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18302 init_test(cx, |_| {});
18303 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18304 let base_text = indoc! {r#"
18305 one
18306
18307 two
18308 three
18309 "#};
18310
18311 cx.set_head_text(base_text);
18312 cx.set_state("\nˇ\n");
18313 cx.executor().run_until_parked();
18314 cx.update_editor(|editor, _window, cx| {
18315 editor.expand_selected_diff_hunks(cx);
18316 });
18317 cx.executor().run_until_parked();
18318 cx.update_editor(|editor, window, cx| {
18319 editor.backspace(&Default::default(), window, cx);
18320 });
18321 cx.run_until_parked();
18322 cx.assert_state_with_diff(
18323 indoc! {r#"
18324
18325 - two
18326 - threeˇ
18327 +
18328 "#}
18329 .to_string(),
18330 );
18331}
18332
18333#[gpui::test]
18334async fn test_deletion_reverts(cx: &mut TestAppContext) {
18335 init_test(cx, |_| {});
18336 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18337 let base_text = indoc! {r#"struct Row;
18338struct Row1;
18339struct Row2;
18340
18341struct Row4;
18342struct Row5;
18343struct Row6;
18344
18345struct Row8;
18346struct Row9;
18347struct Row10;"#};
18348
18349 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18350 assert_hunk_revert(
18351 indoc! {r#"struct Row;
18352 struct Row2;
18353
18354 ˇstruct Row4;
18355 struct Row5;
18356 struct Row6;
18357 ˇ
18358 struct Row8;
18359 struct Row10;"#},
18360 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18361 indoc! {r#"struct Row;
18362 struct Row2;
18363
18364 ˇstruct Row4;
18365 struct Row5;
18366 struct Row6;
18367 ˇ
18368 struct Row8;
18369 struct Row10;"#},
18370 base_text,
18371 &mut cx,
18372 );
18373 assert_hunk_revert(
18374 indoc! {r#"struct Row;
18375 struct Row2;
18376
18377 «ˇstruct Row4;
18378 struct» Row5;
18379 «struct Row6;
18380 ˇ»
18381 struct Row8;
18382 struct Row10;"#},
18383 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18384 indoc! {r#"struct Row;
18385 struct Row2;
18386
18387 «ˇstruct Row4;
18388 struct» Row5;
18389 «struct Row6;
18390 ˇ»
18391 struct Row8;
18392 struct Row10;"#},
18393 base_text,
18394 &mut cx,
18395 );
18396
18397 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18398 assert_hunk_revert(
18399 indoc! {r#"struct Row;
18400 ˇstruct Row2;
18401
18402 struct Row4;
18403 struct Row5;
18404 struct Row6;
18405
18406 struct Row8;ˇ
18407 struct Row10;"#},
18408 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18409 indoc! {r#"struct Row;
18410 struct Row1;
18411 ˇstruct Row2;
18412
18413 struct Row4;
18414 struct Row5;
18415 struct Row6;
18416
18417 struct Row8;ˇ
18418 struct Row9;
18419 struct Row10;"#},
18420 base_text,
18421 &mut cx,
18422 );
18423 assert_hunk_revert(
18424 indoc! {r#"struct Row;
18425 struct Row2«ˇ;
18426 struct Row4;
18427 struct» Row5;
18428 «struct Row6;
18429
18430 struct Row8;ˇ»
18431 struct Row10;"#},
18432 vec![
18433 DiffHunkStatusKind::Deleted,
18434 DiffHunkStatusKind::Deleted,
18435 DiffHunkStatusKind::Deleted,
18436 ],
18437 indoc! {r#"struct Row;
18438 struct Row1;
18439 struct Row2«ˇ;
18440
18441 struct Row4;
18442 struct» Row5;
18443 «struct Row6;
18444
18445 struct Row8;ˇ»
18446 struct Row9;
18447 struct Row10;"#},
18448 base_text,
18449 &mut cx,
18450 );
18451}
18452
18453#[gpui::test]
18454async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18455 init_test(cx, |_| {});
18456
18457 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18458 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18459 let base_text_3 =
18460 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18461
18462 let text_1 = edit_first_char_of_every_line(base_text_1);
18463 let text_2 = edit_first_char_of_every_line(base_text_2);
18464 let text_3 = edit_first_char_of_every_line(base_text_3);
18465
18466 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18467 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18468 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18469
18470 let multibuffer = cx.new(|cx| {
18471 let mut multibuffer = MultiBuffer::new(ReadWrite);
18472 multibuffer.push_excerpts(
18473 buffer_1.clone(),
18474 [
18475 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18476 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18477 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18478 ],
18479 cx,
18480 );
18481 multibuffer.push_excerpts(
18482 buffer_2.clone(),
18483 [
18484 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18485 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18486 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18487 ],
18488 cx,
18489 );
18490 multibuffer.push_excerpts(
18491 buffer_3.clone(),
18492 [
18493 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18494 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18495 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18496 ],
18497 cx,
18498 );
18499 multibuffer
18500 });
18501
18502 let fs = FakeFs::new(cx.executor());
18503 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18504 let (editor, cx) = cx
18505 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18506 editor.update_in(cx, |editor, _window, cx| {
18507 for (buffer, diff_base) in [
18508 (buffer_1.clone(), base_text_1),
18509 (buffer_2.clone(), base_text_2),
18510 (buffer_3.clone(), base_text_3),
18511 ] {
18512 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18513 editor
18514 .buffer
18515 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18516 }
18517 });
18518 cx.executor().run_until_parked();
18519
18520 editor.update_in(cx, |editor, window, cx| {
18521 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}");
18522 editor.select_all(&SelectAll, window, cx);
18523 editor.git_restore(&Default::default(), window, cx);
18524 });
18525 cx.executor().run_until_parked();
18526
18527 // When all ranges are selected, all buffer hunks are reverted.
18528 editor.update(cx, |editor, cx| {
18529 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");
18530 });
18531 buffer_1.update(cx, |buffer, _| {
18532 assert_eq!(buffer.text(), base_text_1);
18533 });
18534 buffer_2.update(cx, |buffer, _| {
18535 assert_eq!(buffer.text(), base_text_2);
18536 });
18537 buffer_3.update(cx, |buffer, _| {
18538 assert_eq!(buffer.text(), base_text_3);
18539 });
18540
18541 editor.update_in(cx, |editor, window, cx| {
18542 editor.undo(&Default::default(), window, cx);
18543 });
18544
18545 editor.update_in(cx, |editor, window, cx| {
18546 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18547 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18548 });
18549 editor.git_restore(&Default::default(), window, cx);
18550 });
18551
18552 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18553 // but not affect buffer_2 and its related excerpts.
18554 editor.update(cx, |editor, cx| {
18555 assert_eq!(
18556 editor.text(cx),
18557 "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}"
18558 );
18559 });
18560 buffer_1.update(cx, |buffer, _| {
18561 assert_eq!(buffer.text(), base_text_1);
18562 });
18563 buffer_2.update(cx, |buffer, _| {
18564 assert_eq!(
18565 buffer.text(),
18566 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18567 );
18568 });
18569 buffer_3.update(cx, |buffer, _| {
18570 assert_eq!(
18571 buffer.text(),
18572 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18573 );
18574 });
18575
18576 fn edit_first_char_of_every_line(text: &str) -> String {
18577 text.split('\n')
18578 .map(|line| format!("X{}", &line[1..]))
18579 .collect::<Vec<_>>()
18580 .join("\n")
18581 }
18582}
18583
18584#[gpui::test]
18585async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18586 init_test(cx, |_| {});
18587
18588 let cols = 4;
18589 let rows = 10;
18590 let sample_text_1 = sample_text(rows, cols, 'a');
18591 assert_eq!(
18592 sample_text_1,
18593 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18594 );
18595 let sample_text_2 = sample_text(rows, cols, 'l');
18596 assert_eq!(
18597 sample_text_2,
18598 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18599 );
18600 let sample_text_3 = sample_text(rows, cols, 'v');
18601 assert_eq!(
18602 sample_text_3,
18603 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18604 );
18605
18606 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18607 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18608 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18609
18610 let multi_buffer = cx.new(|cx| {
18611 let mut multibuffer = MultiBuffer::new(ReadWrite);
18612 multibuffer.push_excerpts(
18613 buffer_1.clone(),
18614 [
18615 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18616 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18617 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18618 ],
18619 cx,
18620 );
18621 multibuffer.push_excerpts(
18622 buffer_2.clone(),
18623 [
18624 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18625 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18626 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18627 ],
18628 cx,
18629 );
18630 multibuffer.push_excerpts(
18631 buffer_3.clone(),
18632 [
18633 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18634 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18635 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18636 ],
18637 cx,
18638 );
18639 multibuffer
18640 });
18641
18642 let fs = FakeFs::new(cx.executor());
18643 fs.insert_tree(
18644 "/a",
18645 json!({
18646 "main.rs": sample_text_1,
18647 "other.rs": sample_text_2,
18648 "lib.rs": sample_text_3,
18649 }),
18650 )
18651 .await;
18652 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18653 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18654 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18655 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18656 Editor::new(
18657 EditorMode::full(),
18658 multi_buffer,
18659 Some(project.clone()),
18660 window,
18661 cx,
18662 )
18663 });
18664 let multibuffer_item_id = workspace
18665 .update(cx, |workspace, window, cx| {
18666 assert!(
18667 workspace.active_item(cx).is_none(),
18668 "active item should be None before the first item is added"
18669 );
18670 workspace.add_item_to_active_pane(
18671 Box::new(multi_buffer_editor.clone()),
18672 None,
18673 true,
18674 window,
18675 cx,
18676 );
18677 let active_item = workspace
18678 .active_item(cx)
18679 .expect("should have an active item after adding the multi buffer");
18680 assert!(
18681 !active_item.is_singleton(cx),
18682 "A multi buffer was expected to active after adding"
18683 );
18684 active_item.item_id()
18685 })
18686 .unwrap();
18687 cx.executor().run_until_parked();
18688
18689 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18690 editor.change_selections(
18691 SelectionEffects::scroll(Autoscroll::Next),
18692 window,
18693 cx,
18694 |s| s.select_ranges(Some(1..2)),
18695 );
18696 editor.open_excerpts(&OpenExcerpts, window, cx);
18697 });
18698 cx.executor().run_until_parked();
18699 let first_item_id = workspace
18700 .update(cx, |workspace, window, cx| {
18701 let active_item = workspace
18702 .active_item(cx)
18703 .expect("should have an active item after navigating into the 1st buffer");
18704 let first_item_id = active_item.item_id();
18705 assert_ne!(
18706 first_item_id, multibuffer_item_id,
18707 "Should navigate into the 1st buffer and activate it"
18708 );
18709 assert!(
18710 active_item.is_singleton(cx),
18711 "New active item should be a singleton buffer"
18712 );
18713 assert_eq!(
18714 active_item
18715 .act_as::<Editor>(cx)
18716 .expect("should have navigated into an editor for the 1st buffer")
18717 .read(cx)
18718 .text(cx),
18719 sample_text_1
18720 );
18721
18722 workspace
18723 .go_back(workspace.active_pane().downgrade(), window, cx)
18724 .detach_and_log_err(cx);
18725
18726 first_item_id
18727 })
18728 .unwrap();
18729 cx.executor().run_until_parked();
18730 workspace
18731 .update(cx, |workspace, _, cx| {
18732 let active_item = workspace
18733 .active_item(cx)
18734 .expect("should have an active item after navigating back");
18735 assert_eq!(
18736 active_item.item_id(),
18737 multibuffer_item_id,
18738 "Should navigate back to the multi buffer"
18739 );
18740 assert!(!active_item.is_singleton(cx));
18741 })
18742 .unwrap();
18743
18744 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18745 editor.change_selections(
18746 SelectionEffects::scroll(Autoscroll::Next),
18747 window,
18748 cx,
18749 |s| s.select_ranges(Some(39..40)),
18750 );
18751 editor.open_excerpts(&OpenExcerpts, window, cx);
18752 });
18753 cx.executor().run_until_parked();
18754 let second_item_id = workspace
18755 .update(cx, |workspace, window, cx| {
18756 let active_item = workspace
18757 .active_item(cx)
18758 .expect("should have an active item after navigating into the 2nd buffer");
18759 let second_item_id = active_item.item_id();
18760 assert_ne!(
18761 second_item_id, multibuffer_item_id,
18762 "Should navigate away from the multibuffer"
18763 );
18764 assert_ne!(
18765 second_item_id, first_item_id,
18766 "Should navigate into the 2nd buffer and activate it"
18767 );
18768 assert!(
18769 active_item.is_singleton(cx),
18770 "New active item should be a singleton buffer"
18771 );
18772 assert_eq!(
18773 active_item
18774 .act_as::<Editor>(cx)
18775 .expect("should have navigated into an editor")
18776 .read(cx)
18777 .text(cx),
18778 sample_text_2
18779 );
18780
18781 workspace
18782 .go_back(workspace.active_pane().downgrade(), window, cx)
18783 .detach_and_log_err(cx);
18784
18785 second_item_id
18786 })
18787 .unwrap();
18788 cx.executor().run_until_parked();
18789 workspace
18790 .update(cx, |workspace, _, cx| {
18791 let active_item = workspace
18792 .active_item(cx)
18793 .expect("should have an active item after navigating back from the 2nd buffer");
18794 assert_eq!(
18795 active_item.item_id(),
18796 multibuffer_item_id,
18797 "Should navigate back from the 2nd buffer to the multi buffer"
18798 );
18799 assert!(!active_item.is_singleton(cx));
18800 })
18801 .unwrap();
18802
18803 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18804 editor.change_selections(
18805 SelectionEffects::scroll(Autoscroll::Next),
18806 window,
18807 cx,
18808 |s| s.select_ranges(Some(70..70)),
18809 );
18810 editor.open_excerpts(&OpenExcerpts, window, cx);
18811 });
18812 cx.executor().run_until_parked();
18813 workspace
18814 .update(cx, |workspace, window, cx| {
18815 let active_item = workspace
18816 .active_item(cx)
18817 .expect("should have an active item after navigating into the 3rd buffer");
18818 let third_item_id = active_item.item_id();
18819 assert_ne!(
18820 third_item_id, multibuffer_item_id,
18821 "Should navigate into the 3rd buffer and activate it"
18822 );
18823 assert_ne!(third_item_id, first_item_id);
18824 assert_ne!(third_item_id, second_item_id);
18825 assert!(
18826 active_item.is_singleton(cx),
18827 "New active item should be a singleton buffer"
18828 );
18829 assert_eq!(
18830 active_item
18831 .act_as::<Editor>(cx)
18832 .expect("should have navigated into an editor")
18833 .read(cx)
18834 .text(cx),
18835 sample_text_3
18836 );
18837
18838 workspace
18839 .go_back(workspace.active_pane().downgrade(), window, cx)
18840 .detach_and_log_err(cx);
18841 })
18842 .unwrap();
18843 cx.executor().run_until_parked();
18844 workspace
18845 .update(cx, |workspace, _, cx| {
18846 let active_item = workspace
18847 .active_item(cx)
18848 .expect("should have an active item after navigating back from the 3rd buffer");
18849 assert_eq!(
18850 active_item.item_id(),
18851 multibuffer_item_id,
18852 "Should navigate back from the 3rd buffer to the multi buffer"
18853 );
18854 assert!(!active_item.is_singleton(cx));
18855 })
18856 .unwrap();
18857}
18858
18859#[gpui::test]
18860async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18861 init_test(cx, |_| {});
18862
18863 let mut cx = EditorTestContext::new(cx).await;
18864
18865 let diff_base = r#"
18866 use some::mod;
18867
18868 const A: u32 = 42;
18869
18870 fn main() {
18871 println!("hello");
18872
18873 println!("world");
18874 }
18875 "#
18876 .unindent();
18877
18878 cx.set_state(
18879 &r#"
18880 use some::modified;
18881
18882 ˇ
18883 fn main() {
18884 println!("hello there");
18885
18886 println!("around the");
18887 println!("world");
18888 }
18889 "#
18890 .unindent(),
18891 );
18892
18893 cx.set_head_text(&diff_base);
18894 executor.run_until_parked();
18895
18896 cx.update_editor(|editor, window, cx| {
18897 editor.go_to_next_hunk(&GoToHunk, window, cx);
18898 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18899 });
18900 executor.run_until_parked();
18901 cx.assert_state_with_diff(
18902 r#"
18903 use some::modified;
18904
18905
18906 fn main() {
18907 - println!("hello");
18908 + ˇ println!("hello there");
18909
18910 println!("around the");
18911 println!("world");
18912 }
18913 "#
18914 .unindent(),
18915 );
18916
18917 cx.update_editor(|editor, window, cx| {
18918 for _ in 0..2 {
18919 editor.go_to_next_hunk(&GoToHunk, window, cx);
18920 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18921 }
18922 });
18923 executor.run_until_parked();
18924 cx.assert_state_with_diff(
18925 r#"
18926 - use some::mod;
18927 + ˇuse some::modified;
18928
18929
18930 fn main() {
18931 - println!("hello");
18932 + println!("hello there");
18933
18934 + println!("around the");
18935 println!("world");
18936 }
18937 "#
18938 .unindent(),
18939 );
18940
18941 cx.update_editor(|editor, window, cx| {
18942 editor.go_to_next_hunk(&GoToHunk, window, cx);
18943 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18944 });
18945 executor.run_until_parked();
18946 cx.assert_state_with_diff(
18947 r#"
18948 - use some::mod;
18949 + use some::modified;
18950
18951 - const A: u32 = 42;
18952 ˇ
18953 fn main() {
18954 - println!("hello");
18955 + println!("hello there");
18956
18957 + println!("around the");
18958 println!("world");
18959 }
18960 "#
18961 .unindent(),
18962 );
18963
18964 cx.update_editor(|editor, window, cx| {
18965 editor.cancel(&Cancel, window, cx);
18966 });
18967
18968 cx.assert_state_with_diff(
18969 r#"
18970 use some::modified;
18971
18972 ˇ
18973 fn main() {
18974 println!("hello there");
18975
18976 println!("around the");
18977 println!("world");
18978 }
18979 "#
18980 .unindent(),
18981 );
18982}
18983
18984#[gpui::test]
18985async fn test_diff_base_change_with_expanded_diff_hunks(
18986 executor: BackgroundExecutor,
18987 cx: &mut TestAppContext,
18988) {
18989 init_test(cx, |_| {});
18990
18991 let mut cx = EditorTestContext::new(cx).await;
18992
18993 let diff_base = r#"
18994 use some::mod1;
18995 use some::mod2;
18996
18997 const A: u32 = 42;
18998 const B: u32 = 42;
18999 const C: u32 = 42;
19000
19001 fn main() {
19002 println!("hello");
19003
19004 println!("world");
19005 }
19006 "#
19007 .unindent();
19008
19009 cx.set_state(
19010 &r#"
19011 use some::mod2;
19012
19013 const A: u32 = 42;
19014 const C: u32 = 42;
19015
19016 fn main(ˇ) {
19017 //println!("hello");
19018
19019 println!("world");
19020 //
19021 //
19022 }
19023 "#
19024 .unindent(),
19025 );
19026
19027 cx.set_head_text(&diff_base);
19028 executor.run_until_parked();
19029
19030 cx.update_editor(|editor, window, cx| {
19031 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19032 });
19033 executor.run_until_parked();
19034 cx.assert_state_with_diff(
19035 r#"
19036 - use some::mod1;
19037 use some::mod2;
19038
19039 const A: u32 = 42;
19040 - const B: u32 = 42;
19041 const C: u32 = 42;
19042
19043 fn main(ˇ) {
19044 - println!("hello");
19045 + //println!("hello");
19046
19047 println!("world");
19048 + //
19049 + //
19050 }
19051 "#
19052 .unindent(),
19053 );
19054
19055 cx.set_head_text("new diff base!");
19056 executor.run_until_parked();
19057 cx.assert_state_with_diff(
19058 r#"
19059 - new diff base!
19060 + use some::mod2;
19061 +
19062 + const A: u32 = 42;
19063 + const C: u32 = 42;
19064 +
19065 + fn main(ˇ) {
19066 + //println!("hello");
19067 +
19068 + println!("world");
19069 + //
19070 + //
19071 + }
19072 "#
19073 .unindent(),
19074 );
19075}
19076
19077#[gpui::test]
19078async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19079 init_test(cx, |_| {});
19080
19081 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19082 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19083 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19084 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19085 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19086 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19087
19088 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19089 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19090 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19091
19092 let multi_buffer = cx.new(|cx| {
19093 let mut multibuffer = MultiBuffer::new(ReadWrite);
19094 multibuffer.push_excerpts(
19095 buffer_1.clone(),
19096 [
19097 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19098 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19099 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19100 ],
19101 cx,
19102 );
19103 multibuffer.push_excerpts(
19104 buffer_2.clone(),
19105 [
19106 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19107 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19108 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19109 ],
19110 cx,
19111 );
19112 multibuffer.push_excerpts(
19113 buffer_3.clone(),
19114 [
19115 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19116 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19117 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19118 ],
19119 cx,
19120 );
19121 multibuffer
19122 });
19123
19124 let editor =
19125 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19126 editor
19127 .update(cx, |editor, _window, cx| {
19128 for (buffer, diff_base) in [
19129 (buffer_1.clone(), file_1_old),
19130 (buffer_2.clone(), file_2_old),
19131 (buffer_3.clone(), file_3_old),
19132 ] {
19133 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19134 editor
19135 .buffer
19136 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19137 }
19138 })
19139 .unwrap();
19140
19141 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19142 cx.run_until_parked();
19143
19144 cx.assert_editor_state(
19145 &"
19146 ˇaaa
19147 ccc
19148 ddd
19149
19150 ggg
19151 hhh
19152
19153
19154 lll
19155 mmm
19156 NNN
19157
19158 qqq
19159 rrr
19160
19161 uuu
19162 111
19163 222
19164 333
19165
19166 666
19167 777
19168
19169 000
19170 !!!"
19171 .unindent(),
19172 );
19173
19174 cx.update_editor(|editor, window, cx| {
19175 editor.select_all(&SelectAll, window, cx);
19176 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19177 });
19178 cx.executor().run_until_parked();
19179
19180 cx.assert_state_with_diff(
19181 "
19182 «aaa
19183 - bbb
19184 ccc
19185 ddd
19186
19187 ggg
19188 hhh
19189
19190
19191 lll
19192 mmm
19193 - nnn
19194 + NNN
19195
19196 qqq
19197 rrr
19198
19199 uuu
19200 111
19201 222
19202 333
19203
19204 + 666
19205 777
19206
19207 000
19208 !!!ˇ»"
19209 .unindent(),
19210 );
19211}
19212
19213#[gpui::test]
19214async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19215 init_test(cx, |_| {});
19216
19217 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19218 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19219
19220 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19221 let multi_buffer = cx.new(|cx| {
19222 let mut multibuffer = MultiBuffer::new(ReadWrite);
19223 multibuffer.push_excerpts(
19224 buffer.clone(),
19225 [
19226 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19227 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19228 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19229 ],
19230 cx,
19231 );
19232 multibuffer
19233 });
19234
19235 let editor =
19236 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19237 editor
19238 .update(cx, |editor, _window, cx| {
19239 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19240 editor
19241 .buffer
19242 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19243 })
19244 .unwrap();
19245
19246 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19247 cx.run_until_parked();
19248
19249 cx.update_editor(|editor, window, cx| {
19250 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19251 });
19252 cx.executor().run_until_parked();
19253
19254 // When the start of a hunk coincides with the start of its excerpt,
19255 // the hunk is expanded. When the start of a hunk is earlier than
19256 // the start of its excerpt, the hunk is not expanded.
19257 cx.assert_state_with_diff(
19258 "
19259 ˇaaa
19260 - bbb
19261 + BBB
19262
19263 - ddd
19264 - eee
19265 + DDD
19266 + EEE
19267 fff
19268
19269 iii
19270 "
19271 .unindent(),
19272 );
19273}
19274
19275#[gpui::test]
19276async fn test_edits_around_expanded_insertion_hunks(
19277 executor: BackgroundExecutor,
19278 cx: &mut TestAppContext,
19279) {
19280 init_test(cx, |_| {});
19281
19282 let mut cx = EditorTestContext::new(cx).await;
19283
19284 let diff_base = r#"
19285 use some::mod1;
19286 use some::mod2;
19287
19288 const A: u32 = 42;
19289
19290 fn main() {
19291 println!("hello");
19292
19293 println!("world");
19294 }
19295 "#
19296 .unindent();
19297 executor.run_until_parked();
19298 cx.set_state(
19299 &r#"
19300 use some::mod1;
19301 use some::mod2;
19302
19303 const A: u32 = 42;
19304 const B: u32 = 42;
19305 const C: u32 = 42;
19306 ˇ
19307
19308 fn main() {
19309 println!("hello");
19310
19311 println!("world");
19312 }
19313 "#
19314 .unindent(),
19315 );
19316
19317 cx.set_head_text(&diff_base);
19318 executor.run_until_parked();
19319
19320 cx.update_editor(|editor, window, cx| {
19321 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19322 });
19323 executor.run_until_parked();
19324
19325 cx.assert_state_with_diff(
19326 r#"
19327 use some::mod1;
19328 use some::mod2;
19329
19330 const A: u32 = 42;
19331 + const B: u32 = 42;
19332 + const C: u32 = 42;
19333 + ˇ
19334
19335 fn main() {
19336 println!("hello");
19337
19338 println!("world");
19339 }
19340 "#
19341 .unindent(),
19342 );
19343
19344 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19345 executor.run_until_parked();
19346
19347 cx.assert_state_with_diff(
19348 r#"
19349 use some::mod1;
19350 use some::mod2;
19351
19352 const A: u32 = 42;
19353 + const B: u32 = 42;
19354 + const C: u32 = 42;
19355 + const D: u32 = 42;
19356 + ˇ
19357
19358 fn main() {
19359 println!("hello");
19360
19361 println!("world");
19362 }
19363 "#
19364 .unindent(),
19365 );
19366
19367 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19368 executor.run_until_parked();
19369
19370 cx.assert_state_with_diff(
19371 r#"
19372 use some::mod1;
19373 use some::mod2;
19374
19375 const A: u32 = 42;
19376 + const B: u32 = 42;
19377 + const C: u32 = 42;
19378 + const D: u32 = 42;
19379 + const E: u32 = 42;
19380 + ˇ
19381
19382 fn main() {
19383 println!("hello");
19384
19385 println!("world");
19386 }
19387 "#
19388 .unindent(),
19389 );
19390
19391 cx.update_editor(|editor, window, cx| {
19392 editor.delete_line(&DeleteLine, 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 + const D: u32 = 42;
19405 + const E: u32 = 42;
19406 ˇ
19407 fn main() {
19408 println!("hello");
19409
19410 println!("world");
19411 }
19412 "#
19413 .unindent(),
19414 );
19415
19416 cx.update_editor(|editor, window, cx| {
19417 editor.move_up(&MoveUp, window, cx);
19418 editor.delete_line(&DeleteLine, window, cx);
19419 editor.move_up(&MoveUp, window, cx);
19420 editor.delete_line(&DeleteLine, window, cx);
19421 editor.move_up(&MoveUp, window, cx);
19422 editor.delete_line(&DeleteLine, window, cx);
19423 });
19424 executor.run_until_parked();
19425 cx.assert_state_with_diff(
19426 r#"
19427 use some::mod1;
19428 use some::mod2;
19429
19430 const A: u32 = 42;
19431 + const B: u32 = 42;
19432 ˇ
19433 fn main() {
19434 println!("hello");
19435
19436 println!("world");
19437 }
19438 "#
19439 .unindent(),
19440 );
19441
19442 cx.update_editor(|editor, window, cx| {
19443 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19444 editor.delete_line(&DeleteLine, window, cx);
19445 });
19446 executor.run_until_parked();
19447 cx.assert_state_with_diff(
19448 r#"
19449 ˇ
19450 fn main() {
19451 println!("hello");
19452
19453 println!("world");
19454 }
19455 "#
19456 .unindent(),
19457 );
19458}
19459
19460#[gpui::test]
19461async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19462 init_test(cx, |_| {});
19463
19464 let mut cx = EditorTestContext::new(cx).await;
19465 cx.set_head_text(indoc! { "
19466 one
19467 two
19468 three
19469 four
19470 five
19471 "
19472 });
19473 cx.set_state(indoc! { "
19474 one
19475 ˇthree
19476 five
19477 "});
19478 cx.run_until_parked();
19479 cx.update_editor(|editor, window, cx| {
19480 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19481 });
19482 cx.assert_state_with_diff(
19483 indoc! { "
19484 one
19485 - two
19486 ˇthree
19487 - four
19488 five
19489 "}
19490 .to_string(),
19491 );
19492 cx.update_editor(|editor, window, cx| {
19493 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19494 });
19495
19496 cx.assert_state_with_diff(
19497 indoc! { "
19498 one
19499 ˇthree
19500 five
19501 "}
19502 .to_string(),
19503 );
19504
19505 cx.set_state(indoc! { "
19506 one
19507 ˇTWO
19508 three
19509 four
19510 five
19511 "});
19512 cx.run_until_parked();
19513 cx.update_editor(|editor, window, cx| {
19514 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19515 });
19516
19517 cx.assert_state_with_diff(
19518 indoc! { "
19519 one
19520 - two
19521 + ˇTWO
19522 three
19523 four
19524 five
19525 "}
19526 .to_string(),
19527 );
19528 cx.update_editor(|editor, window, cx| {
19529 editor.move_up(&Default::default(), window, cx);
19530 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19531 });
19532 cx.assert_state_with_diff(
19533 indoc! { "
19534 one
19535 ˇTWO
19536 three
19537 four
19538 five
19539 "}
19540 .to_string(),
19541 );
19542}
19543
19544#[gpui::test]
19545async fn test_edits_around_expanded_deletion_hunks(
19546 executor: BackgroundExecutor,
19547 cx: &mut TestAppContext,
19548) {
19549 init_test(cx, |_| {});
19550
19551 let mut cx = EditorTestContext::new(cx).await;
19552
19553 let diff_base = r#"
19554 use some::mod1;
19555 use some::mod2;
19556
19557 const A: u32 = 42;
19558 const B: u32 = 42;
19559 const C: u32 = 42;
19560
19561
19562 fn main() {
19563 println!("hello");
19564
19565 println!("world");
19566 }
19567 "#
19568 .unindent();
19569 executor.run_until_parked();
19570 cx.set_state(
19571 &r#"
19572 use some::mod1;
19573 use some::mod2;
19574
19575 ˇconst B: u32 = 42;
19576 const C: u32 = 42;
19577
19578
19579 fn main() {
19580 println!("hello");
19581
19582 println!("world");
19583 }
19584 "#
19585 .unindent(),
19586 );
19587
19588 cx.set_head_text(&diff_base);
19589 executor.run_until_parked();
19590
19591 cx.update_editor(|editor, window, cx| {
19592 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19593 });
19594 executor.run_until_parked();
19595
19596 cx.assert_state_with_diff(
19597 r#"
19598 use some::mod1;
19599 use some::mod2;
19600
19601 - const A: u32 = 42;
19602 ˇconst B: u32 = 42;
19603 const C: u32 = 42;
19604
19605
19606 fn main() {
19607 println!("hello");
19608
19609 println!("world");
19610 }
19611 "#
19612 .unindent(),
19613 );
19614
19615 cx.update_editor(|editor, window, cx| {
19616 editor.delete_line(&DeleteLine, window, cx);
19617 });
19618 executor.run_until_parked();
19619 cx.assert_state_with_diff(
19620 r#"
19621 use some::mod1;
19622 use some::mod2;
19623
19624 - const A: u32 = 42;
19625 - const B: u32 = 42;
19626 ˇconst C: u32 = 42;
19627
19628
19629 fn main() {
19630 println!("hello");
19631
19632 println!("world");
19633 }
19634 "#
19635 .unindent(),
19636 );
19637
19638 cx.update_editor(|editor, window, cx| {
19639 editor.delete_line(&DeleteLine, window, cx);
19640 });
19641 executor.run_until_parked();
19642 cx.assert_state_with_diff(
19643 r#"
19644 use some::mod1;
19645 use some::mod2;
19646
19647 - const A: u32 = 42;
19648 - const B: u32 = 42;
19649 - const C: u32 = 42;
19650 ˇ
19651
19652 fn main() {
19653 println!("hello");
19654
19655 println!("world");
19656 }
19657 "#
19658 .unindent(),
19659 );
19660
19661 cx.update_editor(|editor, window, cx| {
19662 editor.handle_input("replacement", window, cx);
19663 });
19664 executor.run_until_parked();
19665 cx.assert_state_with_diff(
19666 r#"
19667 use some::mod1;
19668 use some::mod2;
19669
19670 - const A: u32 = 42;
19671 - const B: u32 = 42;
19672 - const C: u32 = 42;
19673 -
19674 + replacementˇ
19675
19676 fn main() {
19677 println!("hello");
19678
19679 println!("world");
19680 }
19681 "#
19682 .unindent(),
19683 );
19684}
19685
19686#[gpui::test]
19687async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19688 init_test(cx, |_| {});
19689
19690 let mut cx = EditorTestContext::new(cx).await;
19691
19692 let base_text = r#"
19693 one
19694 two
19695 three
19696 four
19697 five
19698 "#
19699 .unindent();
19700 executor.run_until_parked();
19701 cx.set_state(
19702 &r#"
19703 one
19704 two
19705 fˇour
19706 five
19707 "#
19708 .unindent(),
19709 );
19710
19711 cx.set_head_text(&base_text);
19712 executor.run_until_parked();
19713
19714 cx.update_editor(|editor, window, cx| {
19715 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19716 });
19717 executor.run_until_parked();
19718
19719 cx.assert_state_with_diff(
19720 r#"
19721 one
19722 two
19723 - three
19724 fˇour
19725 five
19726 "#
19727 .unindent(),
19728 );
19729
19730 cx.update_editor(|editor, window, cx| {
19731 editor.backspace(&Backspace, window, cx);
19732 editor.backspace(&Backspace, window, cx);
19733 });
19734 executor.run_until_parked();
19735 cx.assert_state_with_diff(
19736 r#"
19737 one
19738 two
19739 - threeˇ
19740 - four
19741 + our
19742 five
19743 "#
19744 .unindent(),
19745 );
19746}
19747
19748#[gpui::test]
19749async fn test_edit_after_expanded_modification_hunk(
19750 executor: BackgroundExecutor,
19751 cx: &mut TestAppContext,
19752) {
19753 init_test(cx, |_| {});
19754
19755 let mut cx = EditorTestContext::new(cx).await;
19756
19757 let diff_base = r#"
19758 use some::mod1;
19759 use some::mod2;
19760
19761 const A: u32 = 42;
19762 const B: u32 = 42;
19763 const C: u32 = 42;
19764 const D: u32 = 42;
19765
19766
19767 fn main() {
19768 println!("hello");
19769
19770 println!("world");
19771 }"#
19772 .unindent();
19773
19774 cx.set_state(
19775 &r#"
19776 use some::mod1;
19777 use some::mod2;
19778
19779 const A: u32 = 42;
19780 const B: u32 = 42;
19781 const C: u32 = 43ˇ
19782 const D: u32 = 42;
19783
19784
19785 fn main() {
19786 println!("hello");
19787
19788 println!("world");
19789 }"#
19790 .unindent(),
19791 );
19792
19793 cx.set_head_text(&diff_base);
19794 executor.run_until_parked();
19795 cx.update_editor(|editor, window, cx| {
19796 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19797 });
19798 executor.run_until_parked();
19799
19800 cx.assert_state_with_diff(
19801 r#"
19802 use some::mod1;
19803 use some::mod2;
19804
19805 const A: u32 = 42;
19806 const B: u32 = 42;
19807 - const C: u32 = 42;
19808 + const C: u32 = 43ˇ
19809 const D: u32 = 42;
19810
19811
19812 fn main() {
19813 println!("hello");
19814
19815 println!("world");
19816 }"#
19817 .unindent(),
19818 );
19819
19820 cx.update_editor(|editor, window, cx| {
19821 editor.handle_input("\nnew_line\n", window, cx);
19822 });
19823 executor.run_until_parked();
19824
19825 cx.assert_state_with_diff(
19826 r#"
19827 use some::mod1;
19828 use some::mod2;
19829
19830 const A: u32 = 42;
19831 const B: u32 = 42;
19832 - const C: u32 = 42;
19833 + const C: u32 = 43
19834 + new_line
19835 + ˇ
19836 const D: u32 = 42;
19837
19838
19839 fn main() {
19840 println!("hello");
19841
19842 println!("world");
19843 }"#
19844 .unindent(),
19845 );
19846}
19847
19848#[gpui::test]
19849async fn test_stage_and_unstage_added_file_hunk(
19850 executor: BackgroundExecutor,
19851 cx: &mut TestAppContext,
19852) {
19853 init_test(cx, |_| {});
19854
19855 let mut cx = EditorTestContext::new(cx).await;
19856 cx.update_editor(|editor, _, cx| {
19857 editor.set_expand_all_diff_hunks(cx);
19858 });
19859
19860 let working_copy = r#"
19861 ˇfn main() {
19862 println!("hello, world!");
19863 }
19864 "#
19865 .unindent();
19866
19867 cx.set_state(&working_copy);
19868 executor.run_until_parked();
19869
19870 cx.assert_state_with_diff(
19871 r#"
19872 + ˇfn main() {
19873 + println!("hello, world!");
19874 + }
19875 "#
19876 .unindent(),
19877 );
19878 cx.assert_index_text(None);
19879
19880 cx.update_editor(|editor, window, cx| {
19881 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19882 });
19883 executor.run_until_parked();
19884 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19885 cx.assert_state_with_diff(
19886 r#"
19887 + ˇfn main() {
19888 + println!("hello, world!");
19889 + }
19890 "#
19891 .unindent(),
19892 );
19893
19894 cx.update_editor(|editor, window, cx| {
19895 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19896 });
19897 executor.run_until_parked();
19898 cx.assert_index_text(None);
19899}
19900
19901async fn setup_indent_guides_editor(
19902 text: &str,
19903 cx: &mut TestAppContext,
19904) -> (BufferId, EditorTestContext) {
19905 init_test(cx, |_| {});
19906
19907 let mut cx = EditorTestContext::new(cx).await;
19908
19909 let buffer_id = cx.update_editor(|editor, window, cx| {
19910 editor.set_text(text, window, cx);
19911 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19912
19913 buffer_ids[0]
19914 });
19915
19916 (buffer_id, cx)
19917}
19918
19919fn assert_indent_guides(
19920 range: Range<u32>,
19921 expected: Vec<IndentGuide>,
19922 active_indices: Option<Vec<usize>>,
19923 cx: &mut EditorTestContext,
19924) {
19925 let indent_guides = cx.update_editor(|editor, window, cx| {
19926 let snapshot = editor.snapshot(window, cx).display_snapshot;
19927 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19928 editor,
19929 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19930 true,
19931 &snapshot,
19932 cx,
19933 );
19934
19935 indent_guides.sort_by(|a, b| {
19936 a.depth.cmp(&b.depth).then(
19937 a.start_row
19938 .cmp(&b.start_row)
19939 .then(a.end_row.cmp(&b.end_row)),
19940 )
19941 });
19942 indent_guides
19943 });
19944
19945 if let Some(expected) = active_indices {
19946 let active_indices = cx.update_editor(|editor, window, cx| {
19947 let snapshot = editor.snapshot(window, cx).display_snapshot;
19948 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19949 });
19950
19951 assert_eq!(
19952 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19953 expected,
19954 "Active indent guide indices do not match"
19955 );
19956 }
19957
19958 assert_eq!(indent_guides, expected, "Indent guides do not match");
19959}
19960
19961fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19962 IndentGuide {
19963 buffer_id,
19964 start_row: MultiBufferRow(start_row),
19965 end_row: MultiBufferRow(end_row),
19966 depth,
19967 tab_size: 4,
19968 settings: IndentGuideSettings {
19969 enabled: true,
19970 line_width: 1,
19971 active_line_width: 1,
19972 coloring: IndentGuideColoring::default(),
19973 background_coloring: IndentGuideBackgroundColoring::default(),
19974 },
19975 }
19976}
19977
19978#[gpui::test]
19979async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19980 let (buffer_id, mut cx) = setup_indent_guides_editor(
19981 &"
19982 fn main() {
19983 let a = 1;
19984 }"
19985 .unindent(),
19986 cx,
19987 )
19988 .await;
19989
19990 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19991}
19992
19993#[gpui::test]
19994async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19995 let (buffer_id, mut cx) = setup_indent_guides_editor(
19996 &"
19997 fn main() {
19998 let a = 1;
19999 let b = 2;
20000 }"
20001 .unindent(),
20002 cx,
20003 )
20004 .await;
20005
20006 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20007}
20008
20009#[gpui::test]
20010async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20011 let (buffer_id, mut cx) = setup_indent_guides_editor(
20012 &"
20013 fn main() {
20014 let a = 1;
20015 if a == 3 {
20016 let b = 2;
20017 } else {
20018 let c = 3;
20019 }
20020 }"
20021 .unindent(),
20022 cx,
20023 )
20024 .await;
20025
20026 assert_indent_guides(
20027 0..8,
20028 vec![
20029 indent_guide(buffer_id, 1, 6, 0),
20030 indent_guide(buffer_id, 3, 3, 1),
20031 indent_guide(buffer_id, 5, 5, 1),
20032 ],
20033 None,
20034 &mut cx,
20035 );
20036}
20037
20038#[gpui::test]
20039async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20040 let (buffer_id, mut cx) = setup_indent_guides_editor(
20041 &"
20042 fn main() {
20043 let a = 1;
20044 let b = 2;
20045 let c = 3;
20046 }"
20047 .unindent(),
20048 cx,
20049 )
20050 .await;
20051
20052 assert_indent_guides(
20053 0..5,
20054 vec![
20055 indent_guide(buffer_id, 1, 3, 0),
20056 indent_guide(buffer_id, 2, 2, 1),
20057 ],
20058 None,
20059 &mut cx,
20060 );
20061}
20062
20063#[gpui::test]
20064async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20065 let (buffer_id, mut cx) = setup_indent_guides_editor(
20066 &"
20067 fn main() {
20068 let a = 1;
20069
20070 let c = 3;
20071 }"
20072 .unindent(),
20073 cx,
20074 )
20075 .await;
20076
20077 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20078}
20079
20080#[gpui::test]
20081async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20082 let (buffer_id, mut cx) = setup_indent_guides_editor(
20083 &"
20084 fn main() {
20085 let a = 1;
20086
20087 let c = 3;
20088
20089 if a == 3 {
20090 let b = 2;
20091 } else {
20092 let c = 3;
20093 }
20094 }"
20095 .unindent(),
20096 cx,
20097 )
20098 .await;
20099
20100 assert_indent_guides(
20101 0..11,
20102 vec![
20103 indent_guide(buffer_id, 1, 9, 0),
20104 indent_guide(buffer_id, 6, 6, 1),
20105 indent_guide(buffer_id, 8, 8, 1),
20106 ],
20107 None,
20108 &mut cx,
20109 );
20110}
20111
20112#[gpui::test]
20113async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20114 let (buffer_id, mut cx) = setup_indent_guides_editor(
20115 &"
20116 fn main() {
20117 let a = 1;
20118
20119 let c = 3;
20120
20121 if a == 3 {
20122 let b = 2;
20123 } else {
20124 let c = 3;
20125 }
20126 }"
20127 .unindent(),
20128 cx,
20129 )
20130 .await;
20131
20132 assert_indent_guides(
20133 1..11,
20134 vec![
20135 indent_guide(buffer_id, 1, 9, 0),
20136 indent_guide(buffer_id, 6, 6, 1),
20137 indent_guide(buffer_id, 8, 8, 1),
20138 ],
20139 None,
20140 &mut cx,
20141 );
20142}
20143
20144#[gpui::test]
20145async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20146 let (buffer_id, mut cx) = setup_indent_guides_editor(
20147 &"
20148 fn main() {
20149 let a = 1;
20150
20151 let c = 3;
20152
20153 if a == 3 {
20154 let b = 2;
20155 } else {
20156 let c = 3;
20157 }
20158 }"
20159 .unindent(),
20160 cx,
20161 )
20162 .await;
20163
20164 assert_indent_guides(
20165 1..10,
20166 vec![
20167 indent_guide(buffer_id, 1, 9, 0),
20168 indent_guide(buffer_id, 6, 6, 1),
20169 indent_guide(buffer_id, 8, 8, 1),
20170 ],
20171 None,
20172 &mut cx,
20173 );
20174}
20175
20176#[gpui::test]
20177async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20178 let (buffer_id, mut cx) = setup_indent_guides_editor(
20179 &"
20180 fn main() {
20181 if a {
20182 b(
20183 c,
20184 d,
20185 )
20186 } else {
20187 e(
20188 f
20189 )
20190 }
20191 }"
20192 .unindent(),
20193 cx,
20194 )
20195 .await;
20196
20197 assert_indent_guides(
20198 0..11,
20199 vec![
20200 indent_guide(buffer_id, 1, 10, 0),
20201 indent_guide(buffer_id, 2, 5, 1),
20202 indent_guide(buffer_id, 7, 9, 1),
20203 indent_guide(buffer_id, 3, 4, 2),
20204 indent_guide(buffer_id, 8, 8, 2),
20205 ],
20206 None,
20207 &mut cx,
20208 );
20209
20210 cx.update_editor(|editor, window, cx| {
20211 editor.fold_at(MultiBufferRow(2), window, cx);
20212 assert_eq!(
20213 editor.display_text(cx),
20214 "
20215 fn main() {
20216 if a {
20217 b(⋯
20218 )
20219 } else {
20220 e(
20221 f
20222 )
20223 }
20224 }"
20225 .unindent()
20226 );
20227 });
20228
20229 assert_indent_guides(
20230 0..11,
20231 vec![
20232 indent_guide(buffer_id, 1, 10, 0),
20233 indent_guide(buffer_id, 2, 5, 1),
20234 indent_guide(buffer_id, 7, 9, 1),
20235 indent_guide(buffer_id, 8, 8, 2),
20236 ],
20237 None,
20238 &mut cx,
20239 );
20240}
20241
20242#[gpui::test]
20243async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20244 let (buffer_id, mut cx) = setup_indent_guides_editor(
20245 &"
20246 block1
20247 block2
20248 block3
20249 block4
20250 block2
20251 block1
20252 block1"
20253 .unindent(),
20254 cx,
20255 )
20256 .await;
20257
20258 assert_indent_guides(
20259 1..10,
20260 vec![
20261 indent_guide(buffer_id, 1, 4, 0),
20262 indent_guide(buffer_id, 2, 3, 1),
20263 indent_guide(buffer_id, 3, 3, 2),
20264 ],
20265 None,
20266 &mut cx,
20267 );
20268}
20269
20270#[gpui::test]
20271async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20272 let (buffer_id, mut cx) = setup_indent_guides_editor(
20273 &"
20274 block1
20275 block2
20276 block3
20277
20278 block1
20279 block1"
20280 .unindent(),
20281 cx,
20282 )
20283 .await;
20284
20285 assert_indent_guides(
20286 0..6,
20287 vec![
20288 indent_guide(buffer_id, 1, 2, 0),
20289 indent_guide(buffer_id, 2, 2, 1),
20290 ],
20291 None,
20292 &mut cx,
20293 );
20294}
20295
20296#[gpui::test]
20297async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20298 let (buffer_id, mut cx) = setup_indent_guides_editor(
20299 &"
20300 function component() {
20301 \treturn (
20302 \t\t\t
20303 \t\t<div>
20304 \t\t\t<abc></abc>
20305 \t\t</div>
20306 \t)
20307 }"
20308 .unindent(),
20309 cx,
20310 )
20311 .await;
20312
20313 assert_indent_guides(
20314 0..8,
20315 vec![
20316 indent_guide(buffer_id, 1, 6, 0),
20317 indent_guide(buffer_id, 2, 5, 1),
20318 indent_guide(buffer_id, 4, 4, 2),
20319 ],
20320 None,
20321 &mut cx,
20322 );
20323}
20324
20325#[gpui::test]
20326async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20327 let (buffer_id, mut cx) = setup_indent_guides_editor(
20328 &"
20329 function component() {
20330 \treturn (
20331 \t
20332 \t\t<div>
20333 \t\t\t<abc></abc>
20334 \t\t</div>
20335 \t)
20336 }"
20337 .unindent(),
20338 cx,
20339 )
20340 .await;
20341
20342 assert_indent_guides(
20343 0..8,
20344 vec![
20345 indent_guide(buffer_id, 1, 6, 0),
20346 indent_guide(buffer_id, 2, 5, 1),
20347 indent_guide(buffer_id, 4, 4, 2),
20348 ],
20349 None,
20350 &mut cx,
20351 );
20352}
20353
20354#[gpui::test]
20355async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20356 let (buffer_id, mut cx) = setup_indent_guides_editor(
20357 &"
20358 block1
20359
20360
20361
20362 block2
20363 "
20364 .unindent(),
20365 cx,
20366 )
20367 .await;
20368
20369 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20370}
20371
20372#[gpui::test]
20373async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20374 let (buffer_id, mut cx) = setup_indent_guides_editor(
20375 &"
20376 def a:
20377 \tb = 3
20378 \tif True:
20379 \t\tc = 4
20380 \t\td = 5
20381 \tprint(b)
20382 "
20383 .unindent(),
20384 cx,
20385 )
20386 .await;
20387
20388 assert_indent_guides(
20389 0..6,
20390 vec![
20391 indent_guide(buffer_id, 1, 5, 0),
20392 indent_guide(buffer_id, 3, 4, 1),
20393 ],
20394 None,
20395 &mut cx,
20396 );
20397}
20398
20399#[gpui::test]
20400async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20401 let (buffer_id, mut cx) = setup_indent_guides_editor(
20402 &"
20403 fn main() {
20404 let a = 1;
20405 }"
20406 .unindent(),
20407 cx,
20408 )
20409 .await;
20410
20411 cx.update_editor(|editor, window, cx| {
20412 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20413 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20414 });
20415 });
20416
20417 assert_indent_guides(
20418 0..3,
20419 vec![indent_guide(buffer_id, 1, 1, 0)],
20420 Some(vec![0]),
20421 &mut cx,
20422 );
20423}
20424
20425#[gpui::test]
20426async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20427 let (buffer_id, mut cx) = setup_indent_guides_editor(
20428 &"
20429 fn main() {
20430 if 1 == 2 {
20431 let a = 1;
20432 }
20433 }"
20434 .unindent(),
20435 cx,
20436 )
20437 .await;
20438
20439 cx.update_editor(|editor, window, cx| {
20440 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20441 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20442 });
20443 });
20444
20445 assert_indent_guides(
20446 0..4,
20447 vec![
20448 indent_guide(buffer_id, 1, 3, 0),
20449 indent_guide(buffer_id, 2, 2, 1),
20450 ],
20451 Some(vec![1]),
20452 &mut cx,
20453 );
20454
20455 cx.update_editor(|editor, window, cx| {
20456 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20457 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20458 });
20459 });
20460
20461 assert_indent_guides(
20462 0..4,
20463 vec![
20464 indent_guide(buffer_id, 1, 3, 0),
20465 indent_guide(buffer_id, 2, 2, 1),
20466 ],
20467 Some(vec![1]),
20468 &mut cx,
20469 );
20470
20471 cx.update_editor(|editor, window, cx| {
20472 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20473 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20474 });
20475 });
20476
20477 assert_indent_guides(
20478 0..4,
20479 vec![
20480 indent_guide(buffer_id, 1, 3, 0),
20481 indent_guide(buffer_id, 2, 2, 1),
20482 ],
20483 Some(vec![0]),
20484 &mut cx,
20485 );
20486}
20487
20488#[gpui::test]
20489async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20490 let (buffer_id, mut cx) = setup_indent_guides_editor(
20491 &"
20492 fn main() {
20493 let a = 1;
20494
20495 let b = 2;
20496 }"
20497 .unindent(),
20498 cx,
20499 )
20500 .await;
20501
20502 cx.update_editor(|editor, window, cx| {
20503 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20504 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20505 });
20506 });
20507
20508 assert_indent_guides(
20509 0..5,
20510 vec![indent_guide(buffer_id, 1, 3, 0)],
20511 Some(vec![0]),
20512 &mut cx,
20513 );
20514}
20515
20516#[gpui::test]
20517async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20518 let (buffer_id, mut cx) = setup_indent_guides_editor(
20519 &"
20520 def m:
20521 a = 1
20522 pass"
20523 .unindent(),
20524 cx,
20525 )
20526 .await;
20527
20528 cx.update_editor(|editor, window, cx| {
20529 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20530 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20531 });
20532 });
20533
20534 assert_indent_guides(
20535 0..3,
20536 vec![indent_guide(buffer_id, 1, 2, 0)],
20537 Some(vec![0]),
20538 &mut cx,
20539 );
20540}
20541
20542#[gpui::test]
20543async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20544 init_test(cx, |_| {});
20545 let mut cx = EditorTestContext::new(cx).await;
20546 let text = indoc! {
20547 "
20548 impl A {
20549 fn b() {
20550 0;
20551 3;
20552 5;
20553 6;
20554 7;
20555 }
20556 }
20557 "
20558 };
20559 let base_text = indoc! {
20560 "
20561 impl A {
20562 fn b() {
20563 0;
20564 1;
20565 2;
20566 3;
20567 4;
20568 }
20569 fn c() {
20570 5;
20571 6;
20572 7;
20573 }
20574 }
20575 "
20576 };
20577
20578 cx.update_editor(|editor, window, cx| {
20579 editor.set_text(text, window, cx);
20580
20581 editor.buffer().update(cx, |multibuffer, cx| {
20582 let buffer = multibuffer.as_singleton().unwrap();
20583 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20584
20585 multibuffer.set_all_diff_hunks_expanded(cx);
20586 multibuffer.add_diff(diff, cx);
20587
20588 buffer.read(cx).remote_id()
20589 })
20590 });
20591 cx.run_until_parked();
20592
20593 cx.assert_state_with_diff(
20594 indoc! { "
20595 impl A {
20596 fn b() {
20597 0;
20598 - 1;
20599 - 2;
20600 3;
20601 - 4;
20602 - }
20603 - fn c() {
20604 5;
20605 6;
20606 7;
20607 }
20608 }
20609 ˇ"
20610 }
20611 .to_string(),
20612 );
20613
20614 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20615 editor
20616 .snapshot(window, cx)
20617 .buffer_snapshot
20618 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20619 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20620 .collect::<Vec<_>>()
20621 });
20622 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20623 assert_eq!(
20624 actual_guides,
20625 vec![
20626 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20627 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20628 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20629 ]
20630 );
20631}
20632
20633#[gpui::test]
20634async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20635 init_test(cx, |_| {});
20636 let mut cx = EditorTestContext::new(cx).await;
20637
20638 let diff_base = r#"
20639 a
20640 b
20641 c
20642 "#
20643 .unindent();
20644
20645 cx.set_state(
20646 &r#"
20647 ˇA
20648 b
20649 C
20650 "#
20651 .unindent(),
20652 );
20653 cx.set_head_text(&diff_base);
20654 cx.update_editor(|editor, window, cx| {
20655 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20656 });
20657 executor.run_until_parked();
20658
20659 let both_hunks_expanded = r#"
20660 - a
20661 + ˇA
20662 b
20663 - c
20664 + C
20665 "#
20666 .unindent();
20667
20668 cx.assert_state_with_diff(both_hunks_expanded.clone());
20669
20670 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20671 let snapshot = editor.snapshot(window, cx);
20672 let hunks = editor
20673 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20674 .collect::<Vec<_>>();
20675 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20676 let buffer_id = hunks[0].buffer_id;
20677 hunks
20678 .into_iter()
20679 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20680 .collect::<Vec<_>>()
20681 });
20682 assert_eq!(hunk_ranges.len(), 2);
20683
20684 cx.update_editor(|editor, _, cx| {
20685 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20686 });
20687 executor.run_until_parked();
20688
20689 let second_hunk_expanded = r#"
20690 ˇA
20691 b
20692 - c
20693 + C
20694 "#
20695 .unindent();
20696
20697 cx.assert_state_with_diff(second_hunk_expanded);
20698
20699 cx.update_editor(|editor, _, cx| {
20700 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20701 });
20702 executor.run_until_parked();
20703
20704 cx.assert_state_with_diff(both_hunks_expanded.clone());
20705
20706 cx.update_editor(|editor, _, cx| {
20707 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20708 });
20709 executor.run_until_parked();
20710
20711 let first_hunk_expanded = r#"
20712 - a
20713 + ˇA
20714 b
20715 C
20716 "#
20717 .unindent();
20718
20719 cx.assert_state_with_diff(first_hunk_expanded);
20720
20721 cx.update_editor(|editor, _, cx| {
20722 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20723 });
20724 executor.run_until_parked();
20725
20726 cx.assert_state_with_diff(both_hunks_expanded);
20727
20728 cx.set_state(
20729 &r#"
20730 ˇA
20731 b
20732 "#
20733 .unindent(),
20734 );
20735 cx.run_until_parked();
20736
20737 // TODO this cursor position seems bad
20738 cx.assert_state_with_diff(
20739 r#"
20740 - ˇa
20741 + A
20742 b
20743 "#
20744 .unindent(),
20745 );
20746
20747 cx.update_editor(|editor, window, cx| {
20748 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20749 });
20750
20751 cx.assert_state_with_diff(
20752 r#"
20753 - ˇa
20754 + A
20755 b
20756 - c
20757 "#
20758 .unindent(),
20759 );
20760
20761 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20762 let snapshot = editor.snapshot(window, cx);
20763 let hunks = editor
20764 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20765 .collect::<Vec<_>>();
20766 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20767 let buffer_id = hunks[0].buffer_id;
20768 hunks
20769 .into_iter()
20770 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20771 .collect::<Vec<_>>()
20772 });
20773 assert_eq!(hunk_ranges.len(), 2);
20774
20775 cx.update_editor(|editor, _, cx| {
20776 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20777 });
20778 executor.run_until_parked();
20779
20780 cx.assert_state_with_diff(
20781 r#"
20782 - ˇa
20783 + A
20784 b
20785 "#
20786 .unindent(),
20787 );
20788}
20789
20790#[gpui::test]
20791async fn test_toggle_deletion_hunk_at_start_of_file(
20792 executor: BackgroundExecutor,
20793 cx: &mut TestAppContext,
20794) {
20795 init_test(cx, |_| {});
20796 let mut cx = EditorTestContext::new(cx).await;
20797
20798 let diff_base = r#"
20799 a
20800 b
20801 c
20802 "#
20803 .unindent();
20804
20805 cx.set_state(
20806 &r#"
20807 ˇb
20808 c
20809 "#
20810 .unindent(),
20811 );
20812 cx.set_head_text(&diff_base);
20813 cx.update_editor(|editor, window, cx| {
20814 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20815 });
20816 executor.run_until_parked();
20817
20818 let hunk_expanded = r#"
20819 - a
20820 ˇb
20821 c
20822 "#
20823 .unindent();
20824
20825 cx.assert_state_with_diff(hunk_expanded.clone());
20826
20827 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20828 let snapshot = editor.snapshot(window, cx);
20829 let hunks = editor
20830 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20831 .collect::<Vec<_>>();
20832 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20833 let buffer_id = hunks[0].buffer_id;
20834 hunks
20835 .into_iter()
20836 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20837 .collect::<Vec<_>>()
20838 });
20839 assert_eq!(hunk_ranges.len(), 1);
20840
20841 cx.update_editor(|editor, _, cx| {
20842 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20843 });
20844 executor.run_until_parked();
20845
20846 let hunk_collapsed = r#"
20847 ˇb
20848 c
20849 "#
20850 .unindent();
20851
20852 cx.assert_state_with_diff(hunk_collapsed);
20853
20854 cx.update_editor(|editor, _, cx| {
20855 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20856 });
20857 executor.run_until_parked();
20858
20859 cx.assert_state_with_diff(hunk_expanded);
20860}
20861
20862#[gpui::test]
20863async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20864 init_test(cx, |_| {});
20865
20866 let fs = FakeFs::new(cx.executor());
20867 fs.insert_tree(
20868 path!("/test"),
20869 json!({
20870 ".git": {},
20871 "file-1": "ONE\n",
20872 "file-2": "TWO\n",
20873 "file-3": "THREE\n",
20874 }),
20875 )
20876 .await;
20877
20878 fs.set_head_for_repo(
20879 path!("/test/.git").as_ref(),
20880 &[
20881 ("file-1".into(), "one\n".into()),
20882 ("file-2".into(), "two\n".into()),
20883 ("file-3".into(), "three\n".into()),
20884 ],
20885 "deadbeef",
20886 );
20887
20888 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20889 let mut buffers = vec![];
20890 for i in 1..=3 {
20891 let buffer = project
20892 .update(cx, |project, cx| {
20893 let path = format!(path!("/test/file-{}"), i);
20894 project.open_local_buffer(path, cx)
20895 })
20896 .await
20897 .unwrap();
20898 buffers.push(buffer);
20899 }
20900
20901 let multibuffer = cx.new(|cx| {
20902 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20903 multibuffer.set_all_diff_hunks_expanded(cx);
20904 for buffer in &buffers {
20905 let snapshot = buffer.read(cx).snapshot();
20906 multibuffer.set_excerpts_for_path(
20907 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20908 buffer.clone(),
20909 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20910 2,
20911 cx,
20912 );
20913 }
20914 multibuffer
20915 });
20916
20917 let editor = cx.add_window(|window, cx| {
20918 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20919 });
20920 cx.run_until_parked();
20921
20922 let snapshot = editor
20923 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20924 .unwrap();
20925 let hunks = snapshot
20926 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20927 .map(|hunk| match hunk {
20928 DisplayDiffHunk::Unfolded {
20929 display_row_range, ..
20930 } => display_row_range,
20931 DisplayDiffHunk::Folded { .. } => unreachable!(),
20932 })
20933 .collect::<Vec<_>>();
20934 assert_eq!(
20935 hunks,
20936 [
20937 DisplayRow(2)..DisplayRow(4),
20938 DisplayRow(7)..DisplayRow(9),
20939 DisplayRow(12)..DisplayRow(14),
20940 ]
20941 );
20942}
20943
20944#[gpui::test]
20945async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20946 init_test(cx, |_| {});
20947
20948 let mut cx = EditorTestContext::new(cx).await;
20949 cx.set_head_text(indoc! { "
20950 one
20951 two
20952 three
20953 four
20954 five
20955 "
20956 });
20957 cx.set_index_text(indoc! { "
20958 one
20959 two
20960 three
20961 four
20962 five
20963 "
20964 });
20965 cx.set_state(indoc! {"
20966 one
20967 TWO
20968 ˇTHREE
20969 FOUR
20970 five
20971 "});
20972 cx.run_until_parked();
20973 cx.update_editor(|editor, window, cx| {
20974 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20975 });
20976 cx.run_until_parked();
20977 cx.assert_index_text(Some(indoc! {"
20978 one
20979 TWO
20980 THREE
20981 FOUR
20982 five
20983 "}));
20984 cx.set_state(indoc! { "
20985 one
20986 TWO
20987 ˇTHREE-HUNDRED
20988 FOUR
20989 five
20990 "});
20991 cx.run_until_parked();
20992 cx.update_editor(|editor, window, cx| {
20993 let snapshot = editor.snapshot(window, cx);
20994 let hunks = editor
20995 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20996 .collect::<Vec<_>>();
20997 assert_eq!(hunks.len(), 1);
20998 assert_eq!(
20999 hunks[0].status(),
21000 DiffHunkStatus {
21001 kind: DiffHunkStatusKind::Modified,
21002 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21003 }
21004 );
21005
21006 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21007 });
21008 cx.run_until_parked();
21009 cx.assert_index_text(Some(indoc! {"
21010 one
21011 TWO
21012 THREE-HUNDRED
21013 FOUR
21014 five
21015 "}));
21016}
21017
21018#[gpui::test]
21019fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21020 init_test(cx, |_| {});
21021
21022 let editor = cx.add_window(|window, cx| {
21023 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21024 build_editor(buffer, window, cx)
21025 });
21026
21027 let render_args = Arc::new(Mutex::new(None));
21028 let snapshot = editor
21029 .update(cx, |editor, window, cx| {
21030 let snapshot = editor.buffer().read(cx).snapshot(cx);
21031 let range =
21032 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21033
21034 struct RenderArgs {
21035 row: MultiBufferRow,
21036 folded: bool,
21037 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21038 }
21039
21040 let crease = Crease::inline(
21041 range,
21042 FoldPlaceholder::test(),
21043 {
21044 let toggle_callback = render_args.clone();
21045 move |row, folded, callback, _window, _cx| {
21046 *toggle_callback.lock() = Some(RenderArgs {
21047 row,
21048 folded,
21049 callback,
21050 });
21051 div()
21052 }
21053 },
21054 |_row, _folded, _window, _cx| div(),
21055 );
21056
21057 editor.insert_creases(Some(crease), cx);
21058 let snapshot = editor.snapshot(window, cx);
21059 let _div =
21060 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21061 snapshot
21062 })
21063 .unwrap();
21064
21065 let render_args = render_args.lock().take().unwrap();
21066 assert_eq!(render_args.row, MultiBufferRow(1));
21067 assert!(!render_args.folded);
21068 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21069
21070 cx.update_window(*editor, |_, window, cx| {
21071 (render_args.callback)(true, window, cx)
21072 })
21073 .unwrap();
21074 let snapshot = editor
21075 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21076 .unwrap();
21077 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21078
21079 cx.update_window(*editor, |_, window, cx| {
21080 (render_args.callback)(false, window, cx)
21081 })
21082 .unwrap();
21083 let snapshot = editor
21084 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21085 .unwrap();
21086 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21087}
21088
21089#[gpui::test]
21090async fn test_input_text(cx: &mut TestAppContext) {
21091 init_test(cx, |_| {});
21092 let mut cx = EditorTestContext::new(cx).await;
21093
21094 cx.set_state(
21095 &r#"ˇone
21096 two
21097
21098 three
21099 fourˇ
21100 five
21101
21102 siˇx"#
21103 .unindent(),
21104 );
21105
21106 cx.dispatch_action(HandleInput(String::new()));
21107 cx.assert_editor_state(
21108 &r#"ˇone
21109 two
21110
21111 three
21112 fourˇ
21113 five
21114
21115 siˇx"#
21116 .unindent(),
21117 );
21118
21119 cx.dispatch_action(HandleInput("AAAA".to_string()));
21120 cx.assert_editor_state(
21121 &r#"AAAAˇone
21122 two
21123
21124 three
21125 fourAAAAˇ
21126 five
21127
21128 siAAAAˇx"#
21129 .unindent(),
21130 );
21131}
21132
21133#[gpui::test]
21134async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21135 init_test(cx, |_| {});
21136
21137 let mut cx = EditorTestContext::new(cx).await;
21138 cx.set_state(
21139 r#"let foo = 1;
21140let foo = 2;
21141let foo = 3;
21142let fooˇ = 4;
21143let foo = 5;
21144let foo = 6;
21145let foo = 7;
21146let foo = 8;
21147let foo = 9;
21148let foo = 10;
21149let foo = 11;
21150let foo = 12;
21151let foo = 13;
21152let foo = 14;
21153let foo = 15;"#,
21154 );
21155
21156 cx.update_editor(|e, window, cx| {
21157 assert_eq!(
21158 e.next_scroll_position,
21159 NextScrollCursorCenterTopBottom::Center,
21160 "Default next scroll direction is center",
21161 );
21162
21163 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21164 assert_eq!(
21165 e.next_scroll_position,
21166 NextScrollCursorCenterTopBottom::Top,
21167 "After center, next scroll direction should be top",
21168 );
21169
21170 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21171 assert_eq!(
21172 e.next_scroll_position,
21173 NextScrollCursorCenterTopBottom::Bottom,
21174 "After top, next scroll direction should be bottom",
21175 );
21176
21177 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21178 assert_eq!(
21179 e.next_scroll_position,
21180 NextScrollCursorCenterTopBottom::Center,
21181 "After bottom, scrolling should start over",
21182 );
21183
21184 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21185 assert_eq!(
21186 e.next_scroll_position,
21187 NextScrollCursorCenterTopBottom::Top,
21188 "Scrolling continues if retriggered fast enough"
21189 );
21190 });
21191
21192 cx.executor()
21193 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21194 cx.executor().run_until_parked();
21195 cx.update_editor(|e, _, _| {
21196 assert_eq!(
21197 e.next_scroll_position,
21198 NextScrollCursorCenterTopBottom::Center,
21199 "If scrolling is not triggered fast enough, it should reset"
21200 );
21201 });
21202}
21203
21204#[gpui::test]
21205async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21206 init_test(cx, |_| {});
21207 let mut cx = EditorLspTestContext::new_rust(
21208 lsp::ServerCapabilities {
21209 definition_provider: Some(lsp::OneOf::Left(true)),
21210 references_provider: Some(lsp::OneOf::Left(true)),
21211 ..lsp::ServerCapabilities::default()
21212 },
21213 cx,
21214 )
21215 .await;
21216
21217 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21218 let go_to_definition = cx
21219 .lsp
21220 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21221 move |params, _| async move {
21222 if empty_go_to_definition {
21223 Ok(None)
21224 } else {
21225 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21226 uri: params.text_document_position_params.text_document.uri,
21227 range: lsp::Range::new(
21228 lsp::Position::new(4, 3),
21229 lsp::Position::new(4, 6),
21230 ),
21231 })))
21232 }
21233 },
21234 );
21235 let references = cx
21236 .lsp
21237 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21238 Ok(Some(vec![lsp::Location {
21239 uri: params.text_document_position.text_document.uri,
21240 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21241 }]))
21242 });
21243 (go_to_definition, references)
21244 };
21245
21246 cx.set_state(
21247 &r#"fn one() {
21248 let mut a = ˇtwo();
21249 }
21250
21251 fn two() {}"#
21252 .unindent(),
21253 );
21254 set_up_lsp_handlers(false, &mut cx);
21255 let navigated = cx
21256 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21257 .await
21258 .expect("Failed to navigate to definition");
21259 assert_eq!(
21260 navigated,
21261 Navigated::Yes,
21262 "Should have navigated to definition from the GetDefinition response"
21263 );
21264 cx.assert_editor_state(
21265 &r#"fn one() {
21266 let mut a = two();
21267 }
21268
21269 fn «twoˇ»() {}"#
21270 .unindent(),
21271 );
21272
21273 let editors = cx.update_workspace(|workspace, _, cx| {
21274 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21275 });
21276 cx.update_editor(|_, _, test_editor_cx| {
21277 assert_eq!(
21278 editors.len(),
21279 1,
21280 "Initially, only one, test, editor should be open in the workspace"
21281 );
21282 assert_eq!(
21283 test_editor_cx.entity(),
21284 editors.last().expect("Asserted len is 1").clone()
21285 );
21286 });
21287
21288 set_up_lsp_handlers(true, &mut cx);
21289 let navigated = cx
21290 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21291 .await
21292 .expect("Failed to navigate to lookup references");
21293 assert_eq!(
21294 navigated,
21295 Navigated::Yes,
21296 "Should have navigated to references as a fallback after empty GoToDefinition response"
21297 );
21298 // We should not change the selections in the existing file,
21299 // if opening another milti buffer with the references
21300 cx.assert_editor_state(
21301 &r#"fn one() {
21302 let mut a = two();
21303 }
21304
21305 fn «twoˇ»() {}"#
21306 .unindent(),
21307 );
21308 let editors = cx.update_workspace(|workspace, _, cx| {
21309 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21310 });
21311 cx.update_editor(|_, _, test_editor_cx| {
21312 assert_eq!(
21313 editors.len(),
21314 2,
21315 "After falling back to references search, we open a new editor with the results"
21316 );
21317 let references_fallback_text = editors
21318 .into_iter()
21319 .find(|new_editor| *new_editor != test_editor_cx.entity())
21320 .expect("Should have one non-test editor now")
21321 .read(test_editor_cx)
21322 .text(test_editor_cx);
21323 assert_eq!(
21324 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21325 "Should use the range from the references response and not the GoToDefinition one"
21326 );
21327 });
21328}
21329
21330#[gpui::test]
21331async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21332 init_test(cx, |_| {});
21333 cx.update(|cx| {
21334 let mut editor_settings = EditorSettings::get_global(cx).clone();
21335 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21336 EditorSettings::override_global(editor_settings, cx);
21337 });
21338 let mut cx = EditorLspTestContext::new_rust(
21339 lsp::ServerCapabilities {
21340 definition_provider: Some(lsp::OneOf::Left(true)),
21341 references_provider: Some(lsp::OneOf::Left(true)),
21342 ..lsp::ServerCapabilities::default()
21343 },
21344 cx,
21345 )
21346 .await;
21347 let original_state = r#"fn one() {
21348 let mut a = ˇtwo();
21349 }
21350
21351 fn two() {}"#
21352 .unindent();
21353 cx.set_state(&original_state);
21354
21355 let mut go_to_definition = cx
21356 .lsp
21357 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21358 move |_, _| async move { Ok(None) },
21359 );
21360 let _references = cx
21361 .lsp
21362 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21363 panic!("Should not call for references with no go to definition fallback")
21364 });
21365
21366 let navigated = cx
21367 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21368 .await
21369 .expect("Failed to navigate to lookup references");
21370 go_to_definition
21371 .next()
21372 .await
21373 .expect("Should have called the go_to_definition handler");
21374
21375 assert_eq!(
21376 navigated,
21377 Navigated::No,
21378 "Should have navigated to references as a fallback after empty GoToDefinition response"
21379 );
21380 cx.assert_editor_state(&original_state);
21381 let editors = cx.update_workspace(|workspace, _, cx| {
21382 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21383 });
21384 cx.update_editor(|_, _, _| {
21385 assert_eq!(
21386 editors.len(),
21387 1,
21388 "After unsuccessful fallback, no other editor should have been opened"
21389 );
21390 });
21391}
21392
21393#[gpui::test]
21394async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21395 init_test(cx, |_| {});
21396 let mut cx = EditorLspTestContext::new_rust(
21397 lsp::ServerCapabilities {
21398 references_provider: Some(lsp::OneOf::Left(true)),
21399 ..lsp::ServerCapabilities::default()
21400 },
21401 cx,
21402 )
21403 .await;
21404
21405 cx.set_state(
21406 &r#"
21407 fn one() {
21408 let mut a = two();
21409 }
21410
21411 fn ˇtwo() {}"#
21412 .unindent(),
21413 );
21414 cx.lsp
21415 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21416 Ok(Some(vec![
21417 lsp::Location {
21418 uri: params.text_document_position.text_document.uri.clone(),
21419 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21420 },
21421 lsp::Location {
21422 uri: params.text_document_position.text_document.uri,
21423 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21424 },
21425 ]))
21426 });
21427 let navigated = cx
21428 .update_editor(|editor, window, cx| {
21429 editor.find_all_references(&FindAllReferences, window, cx)
21430 })
21431 .unwrap()
21432 .await
21433 .expect("Failed to navigate to references");
21434 assert_eq!(
21435 navigated,
21436 Navigated::Yes,
21437 "Should have navigated to references from the FindAllReferences response"
21438 );
21439 cx.assert_editor_state(
21440 &r#"fn one() {
21441 let mut a = two();
21442 }
21443
21444 fn ˇtwo() {}"#
21445 .unindent(),
21446 );
21447
21448 let editors = cx.update_workspace(|workspace, _, cx| {
21449 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21450 });
21451 cx.update_editor(|_, _, _| {
21452 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21453 });
21454
21455 cx.set_state(
21456 &r#"fn one() {
21457 let mut a = ˇtwo();
21458 }
21459
21460 fn two() {}"#
21461 .unindent(),
21462 );
21463 let navigated = cx
21464 .update_editor(|editor, window, cx| {
21465 editor.find_all_references(&FindAllReferences, window, cx)
21466 })
21467 .unwrap()
21468 .await
21469 .expect("Failed to navigate to references");
21470 assert_eq!(
21471 navigated,
21472 Navigated::Yes,
21473 "Should have navigated to references from the FindAllReferences response"
21474 );
21475 cx.assert_editor_state(
21476 &r#"fn one() {
21477 let mut a = ˇtwo();
21478 }
21479
21480 fn two() {}"#
21481 .unindent(),
21482 );
21483 let editors = cx.update_workspace(|workspace, _, cx| {
21484 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21485 });
21486 cx.update_editor(|_, _, _| {
21487 assert_eq!(
21488 editors.len(),
21489 2,
21490 "should have re-used the previous multibuffer"
21491 );
21492 });
21493
21494 cx.set_state(
21495 &r#"fn one() {
21496 let mut a = ˇtwo();
21497 }
21498 fn three() {}
21499 fn two() {}"#
21500 .unindent(),
21501 );
21502 cx.lsp
21503 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21504 Ok(Some(vec![
21505 lsp::Location {
21506 uri: params.text_document_position.text_document.uri.clone(),
21507 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21508 },
21509 lsp::Location {
21510 uri: params.text_document_position.text_document.uri,
21511 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21512 },
21513 ]))
21514 });
21515 let navigated = cx
21516 .update_editor(|editor, window, cx| {
21517 editor.find_all_references(&FindAllReferences, window, cx)
21518 })
21519 .unwrap()
21520 .await
21521 .expect("Failed to navigate to references");
21522 assert_eq!(
21523 navigated,
21524 Navigated::Yes,
21525 "Should have navigated to references from the FindAllReferences response"
21526 );
21527 cx.assert_editor_state(
21528 &r#"fn one() {
21529 let mut a = ˇtwo();
21530 }
21531 fn three() {}
21532 fn two() {}"#
21533 .unindent(),
21534 );
21535 let editors = cx.update_workspace(|workspace, _, cx| {
21536 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21537 });
21538 cx.update_editor(|_, _, _| {
21539 assert_eq!(
21540 editors.len(),
21541 3,
21542 "should have used a new multibuffer as offsets changed"
21543 );
21544 });
21545}
21546#[gpui::test]
21547async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21548 init_test(cx, |_| {});
21549
21550 let language = Arc::new(Language::new(
21551 LanguageConfig::default(),
21552 Some(tree_sitter_rust::LANGUAGE.into()),
21553 ));
21554
21555 let text = r#"
21556 #[cfg(test)]
21557 mod tests() {
21558 #[test]
21559 fn runnable_1() {
21560 let a = 1;
21561 }
21562
21563 #[test]
21564 fn runnable_2() {
21565 let a = 1;
21566 let b = 2;
21567 }
21568 }
21569 "#
21570 .unindent();
21571
21572 let fs = FakeFs::new(cx.executor());
21573 fs.insert_file("/file.rs", Default::default()).await;
21574
21575 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21576 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21577 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21578 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21579 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21580
21581 let editor = cx.new_window_entity(|window, cx| {
21582 Editor::new(
21583 EditorMode::full(),
21584 multi_buffer,
21585 Some(project.clone()),
21586 window,
21587 cx,
21588 )
21589 });
21590
21591 editor.update_in(cx, |editor, window, cx| {
21592 let snapshot = editor.buffer().read(cx).snapshot(cx);
21593 editor.tasks.insert(
21594 (buffer.read(cx).remote_id(), 3),
21595 RunnableTasks {
21596 templates: vec![],
21597 offset: snapshot.anchor_before(43),
21598 column: 0,
21599 extra_variables: HashMap::default(),
21600 context_range: BufferOffset(43)..BufferOffset(85),
21601 },
21602 );
21603 editor.tasks.insert(
21604 (buffer.read(cx).remote_id(), 8),
21605 RunnableTasks {
21606 templates: vec![],
21607 offset: snapshot.anchor_before(86),
21608 column: 0,
21609 extra_variables: HashMap::default(),
21610 context_range: BufferOffset(86)..BufferOffset(191),
21611 },
21612 );
21613
21614 // Test finding task when cursor is inside function body
21615 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21616 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21617 });
21618 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21619 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21620
21621 // Test finding task when cursor is on function name
21622 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21623 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21624 });
21625 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21626 assert_eq!(row, 8, "Should find task when cursor is on function name");
21627 });
21628}
21629
21630#[gpui::test]
21631async fn test_folding_buffers(cx: &mut TestAppContext) {
21632 init_test(cx, |_| {});
21633
21634 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21635 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21636 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21637
21638 let fs = FakeFs::new(cx.executor());
21639 fs.insert_tree(
21640 path!("/a"),
21641 json!({
21642 "first.rs": sample_text_1,
21643 "second.rs": sample_text_2,
21644 "third.rs": sample_text_3,
21645 }),
21646 )
21647 .await;
21648 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21649 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21650 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21651 let worktree = project.update(cx, |project, cx| {
21652 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21653 assert_eq!(worktrees.len(), 1);
21654 worktrees.pop().unwrap()
21655 });
21656 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21657
21658 let buffer_1 = project
21659 .update(cx, |project, cx| {
21660 project.open_buffer((worktree_id, "first.rs"), cx)
21661 })
21662 .await
21663 .unwrap();
21664 let buffer_2 = project
21665 .update(cx, |project, cx| {
21666 project.open_buffer((worktree_id, "second.rs"), cx)
21667 })
21668 .await
21669 .unwrap();
21670 let buffer_3 = project
21671 .update(cx, |project, cx| {
21672 project.open_buffer((worktree_id, "third.rs"), cx)
21673 })
21674 .await
21675 .unwrap();
21676
21677 let multi_buffer = cx.new(|cx| {
21678 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21679 multi_buffer.push_excerpts(
21680 buffer_1.clone(),
21681 [
21682 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21683 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21684 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21685 ],
21686 cx,
21687 );
21688 multi_buffer.push_excerpts(
21689 buffer_2.clone(),
21690 [
21691 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21692 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21693 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21694 ],
21695 cx,
21696 );
21697 multi_buffer.push_excerpts(
21698 buffer_3.clone(),
21699 [
21700 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21701 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21702 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21703 ],
21704 cx,
21705 );
21706 multi_buffer
21707 });
21708 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21709 Editor::new(
21710 EditorMode::full(),
21711 multi_buffer.clone(),
21712 Some(project.clone()),
21713 window,
21714 cx,
21715 )
21716 });
21717
21718 assert_eq!(
21719 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21720 "\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",
21721 );
21722
21723 multi_buffer_editor.update(cx, |editor, cx| {
21724 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21725 });
21726 assert_eq!(
21727 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21728 "\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",
21729 "After folding the first buffer, its text should not be displayed"
21730 );
21731
21732 multi_buffer_editor.update(cx, |editor, cx| {
21733 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21734 });
21735 assert_eq!(
21736 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21737 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21738 "After folding the second buffer, its text should not be displayed"
21739 );
21740
21741 multi_buffer_editor.update(cx, |editor, cx| {
21742 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21743 });
21744 assert_eq!(
21745 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21746 "\n\n\n\n\n",
21747 "After folding the third buffer, its text should not be displayed"
21748 );
21749
21750 // Emulate selection inside the fold logic, that should work
21751 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21752 editor
21753 .snapshot(window, cx)
21754 .next_line_boundary(Point::new(0, 4));
21755 });
21756
21757 multi_buffer_editor.update(cx, |editor, cx| {
21758 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21759 });
21760 assert_eq!(
21761 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21762 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21763 "After unfolding the second buffer, its text should be displayed"
21764 );
21765
21766 // Typing inside of buffer 1 causes that buffer to be unfolded.
21767 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21768 assert_eq!(
21769 multi_buffer
21770 .read(cx)
21771 .snapshot(cx)
21772 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21773 .collect::<String>(),
21774 "bbbb"
21775 );
21776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21777 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21778 });
21779 editor.handle_input("B", window, cx);
21780 });
21781
21782 assert_eq!(
21783 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21784 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21785 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21786 );
21787
21788 multi_buffer_editor.update(cx, |editor, cx| {
21789 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21790 });
21791 assert_eq!(
21792 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21793 "\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",
21794 "After unfolding the all buffers, all original text should be displayed"
21795 );
21796}
21797
21798#[gpui::test]
21799async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21800 init_test(cx, |_| {});
21801
21802 let sample_text_1 = "1111\n2222\n3333".to_string();
21803 let sample_text_2 = "4444\n5555\n6666".to_string();
21804 let sample_text_3 = "7777\n8888\n9999".to_string();
21805
21806 let fs = FakeFs::new(cx.executor());
21807 fs.insert_tree(
21808 path!("/a"),
21809 json!({
21810 "first.rs": sample_text_1,
21811 "second.rs": sample_text_2,
21812 "third.rs": sample_text_3,
21813 }),
21814 )
21815 .await;
21816 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21817 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21818 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21819 let worktree = project.update(cx, |project, cx| {
21820 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21821 assert_eq!(worktrees.len(), 1);
21822 worktrees.pop().unwrap()
21823 });
21824 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21825
21826 let buffer_1 = project
21827 .update(cx, |project, cx| {
21828 project.open_buffer((worktree_id, "first.rs"), cx)
21829 })
21830 .await
21831 .unwrap();
21832 let buffer_2 = project
21833 .update(cx, |project, cx| {
21834 project.open_buffer((worktree_id, "second.rs"), cx)
21835 })
21836 .await
21837 .unwrap();
21838 let buffer_3 = project
21839 .update(cx, |project, cx| {
21840 project.open_buffer((worktree_id, "third.rs"), cx)
21841 })
21842 .await
21843 .unwrap();
21844
21845 let multi_buffer = cx.new(|cx| {
21846 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21847 multi_buffer.push_excerpts(
21848 buffer_1.clone(),
21849 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21850 cx,
21851 );
21852 multi_buffer.push_excerpts(
21853 buffer_2.clone(),
21854 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21855 cx,
21856 );
21857 multi_buffer.push_excerpts(
21858 buffer_3.clone(),
21859 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21860 cx,
21861 );
21862 multi_buffer
21863 });
21864
21865 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21866 Editor::new(
21867 EditorMode::full(),
21868 multi_buffer,
21869 Some(project.clone()),
21870 window,
21871 cx,
21872 )
21873 });
21874
21875 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21876 assert_eq!(
21877 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21878 full_text,
21879 );
21880
21881 multi_buffer_editor.update(cx, |editor, cx| {
21882 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21883 });
21884 assert_eq!(
21885 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21886 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21887 "After folding the first buffer, its text should not be displayed"
21888 );
21889
21890 multi_buffer_editor.update(cx, |editor, cx| {
21891 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21892 });
21893
21894 assert_eq!(
21895 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21896 "\n\n\n\n\n\n7777\n8888\n9999",
21897 "After folding the second buffer, its text should not be displayed"
21898 );
21899
21900 multi_buffer_editor.update(cx, |editor, cx| {
21901 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21902 });
21903 assert_eq!(
21904 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21905 "\n\n\n\n\n",
21906 "After folding the third buffer, its text should not be displayed"
21907 );
21908
21909 multi_buffer_editor.update(cx, |editor, cx| {
21910 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21911 });
21912 assert_eq!(
21913 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21914 "\n\n\n\n4444\n5555\n6666\n\n",
21915 "After unfolding the second buffer, its text should be displayed"
21916 );
21917
21918 multi_buffer_editor.update(cx, |editor, cx| {
21919 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21920 });
21921 assert_eq!(
21922 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21923 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21924 "After unfolding the first buffer, its text should be displayed"
21925 );
21926
21927 multi_buffer_editor.update(cx, |editor, cx| {
21928 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21929 });
21930 assert_eq!(
21931 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21932 full_text,
21933 "After unfolding all buffers, all original text should be displayed"
21934 );
21935}
21936
21937#[gpui::test]
21938async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21939 init_test(cx, |_| {});
21940
21941 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21942
21943 let fs = FakeFs::new(cx.executor());
21944 fs.insert_tree(
21945 path!("/a"),
21946 json!({
21947 "main.rs": sample_text,
21948 }),
21949 )
21950 .await;
21951 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21952 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21953 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21954 let worktree = project.update(cx, |project, cx| {
21955 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21956 assert_eq!(worktrees.len(), 1);
21957 worktrees.pop().unwrap()
21958 });
21959 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21960
21961 let buffer_1 = project
21962 .update(cx, |project, cx| {
21963 project.open_buffer((worktree_id, "main.rs"), cx)
21964 })
21965 .await
21966 .unwrap();
21967
21968 let multi_buffer = cx.new(|cx| {
21969 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21970 multi_buffer.push_excerpts(
21971 buffer_1.clone(),
21972 [ExcerptRange::new(
21973 Point::new(0, 0)
21974 ..Point::new(
21975 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21976 0,
21977 ),
21978 )],
21979 cx,
21980 );
21981 multi_buffer
21982 });
21983 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21984 Editor::new(
21985 EditorMode::full(),
21986 multi_buffer,
21987 Some(project.clone()),
21988 window,
21989 cx,
21990 )
21991 });
21992
21993 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21994 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21995 enum TestHighlight {}
21996 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21997 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21998 editor.highlight_text::<TestHighlight>(
21999 vec![highlight_range.clone()],
22000 HighlightStyle::color(Hsla::green()),
22001 cx,
22002 );
22003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22004 s.select_ranges(Some(highlight_range))
22005 });
22006 });
22007
22008 let full_text = format!("\n\n{sample_text}");
22009 assert_eq!(
22010 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22011 full_text,
22012 );
22013}
22014
22015#[gpui::test]
22016async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22017 init_test(cx, |_| {});
22018 cx.update(|cx| {
22019 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22020 "keymaps/default-linux.json",
22021 cx,
22022 )
22023 .unwrap();
22024 cx.bind_keys(default_key_bindings);
22025 });
22026
22027 let (editor, cx) = cx.add_window_view(|window, cx| {
22028 let multi_buffer = MultiBuffer::build_multi(
22029 [
22030 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22031 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22032 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22033 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22034 ],
22035 cx,
22036 );
22037 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22038
22039 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22040 // fold all but the second buffer, so that we test navigating between two
22041 // adjacent folded buffers, as well as folded buffers at the start and
22042 // end the multibuffer
22043 editor.fold_buffer(buffer_ids[0], cx);
22044 editor.fold_buffer(buffer_ids[2], cx);
22045 editor.fold_buffer(buffer_ids[3], cx);
22046
22047 editor
22048 });
22049 cx.simulate_resize(size(px(1000.), px(1000.)));
22050
22051 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22052 cx.assert_excerpts_with_selections(indoc! {"
22053 [EXCERPT]
22054 ˇ[FOLDED]
22055 [EXCERPT]
22056 a1
22057 b1
22058 [EXCERPT]
22059 [FOLDED]
22060 [EXCERPT]
22061 [FOLDED]
22062 "
22063 });
22064 cx.simulate_keystroke("down");
22065 cx.assert_excerpts_with_selections(indoc! {"
22066 [EXCERPT]
22067 [FOLDED]
22068 [EXCERPT]
22069 ˇa1
22070 b1
22071 [EXCERPT]
22072 [FOLDED]
22073 [EXCERPT]
22074 [FOLDED]
22075 "
22076 });
22077 cx.simulate_keystroke("down");
22078 cx.assert_excerpts_with_selections(indoc! {"
22079 [EXCERPT]
22080 [FOLDED]
22081 [EXCERPT]
22082 a1
22083 ˇb1
22084 [EXCERPT]
22085 [FOLDED]
22086 [EXCERPT]
22087 [FOLDED]
22088 "
22089 });
22090 cx.simulate_keystroke("down");
22091 cx.assert_excerpts_with_selections(indoc! {"
22092 [EXCERPT]
22093 [FOLDED]
22094 [EXCERPT]
22095 a1
22096 b1
22097 ˇ[EXCERPT]
22098 [FOLDED]
22099 [EXCERPT]
22100 [FOLDED]
22101 "
22102 });
22103 cx.simulate_keystroke("down");
22104 cx.assert_excerpts_with_selections(indoc! {"
22105 [EXCERPT]
22106 [FOLDED]
22107 [EXCERPT]
22108 a1
22109 b1
22110 [EXCERPT]
22111 ˇ[FOLDED]
22112 [EXCERPT]
22113 [FOLDED]
22114 "
22115 });
22116 for _ in 0..5 {
22117 cx.simulate_keystroke("down");
22118 cx.assert_excerpts_with_selections(indoc! {"
22119 [EXCERPT]
22120 [FOLDED]
22121 [EXCERPT]
22122 a1
22123 b1
22124 [EXCERPT]
22125 [FOLDED]
22126 [EXCERPT]
22127 ˇ[FOLDED]
22128 "
22129 });
22130 }
22131
22132 cx.simulate_keystroke("up");
22133 cx.assert_excerpts_with_selections(indoc! {"
22134 [EXCERPT]
22135 [FOLDED]
22136 [EXCERPT]
22137 a1
22138 b1
22139 [EXCERPT]
22140 ˇ[FOLDED]
22141 [EXCERPT]
22142 [FOLDED]
22143 "
22144 });
22145 cx.simulate_keystroke("up");
22146 cx.assert_excerpts_with_selections(indoc! {"
22147 [EXCERPT]
22148 [FOLDED]
22149 [EXCERPT]
22150 a1
22151 b1
22152 ˇ[EXCERPT]
22153 [FOLDED]
22154 [EXCERPT]
22155 [FOLDED]
22156 "
22157 });
22158 cx.simulate_keystroke("up");
22159 cx.assert_excerpts_with_selections(indoc! {"
22160 [EXCERPT]
22161 [FOLDED]
22162 [EXCERPT]
22163 a1
22164 ˇb1
22165 [EXCERPT]
22166 [FOLDED]
22167 [EXCERPT]
22168 [FOLDED]
22169 "
22170 });
22171 cx.simulate_keystroke("up");
22172 cx.assert_excerpts_with_selections(indoc! {"
22173 [EXCERPT]
22174 [FOLDED]
22175 [EXCERPT]
22176 ˇa1
22177 b1
22178 [EXCERPT]
22179 [FOLDED]
22180 [EXCERPT]
22181 [FOLDED]
22182 "
22183 });
22184 for _ in 0..5 {
22185 cx.simulate_keystroke("up");
22186 cx.assert_excerpts_with_selections(indoc! {"
22187 [EXCERPT]
22188 ˇ[FOLDED]
22189 [EXCERPT]
22190 a1
22191 b1
22192 [EXCERPT]
22193 [FOLDED]
22194 [EXCERPT]
22195 [FOLDED]
22196 "
22197 });
22198 }
22199}
22200
22201#[gpui::test]
22202async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22203 init_test(cx, |_| {});
22204
22205 // Simple insertion
22206 assert_highlighted_edits(
22207 "Hello, world!",
22208 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22209 true,
22210 cx,
22211 |highlighted_edits, cx| {
22212 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22213 assert_eq!(highlighted_edits.highlights.len(), 1);
22214 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22215 assert_eq!(
22216 highlighted_edits.highlights[0].1.background_color,
22217 Some(cx.theme().status().created_background)
22218 );
22219 },
22220 )
22221 .await;
22222
22223 // Replacement
22224 assert_highlighted_edits(
22225 "This is a test.",
22226 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22227 false,
22228 cx,
22229 |highlighted_edits, cx| {
22230 assert_eq!(highlighted_edits.text, "That is a test.");
22231 assert_eq!(highlighted_edits.highlights.len(), 1);
22232 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22233 assert_eq!(
22234 highlighted_edits.highlights[0].1.background_color,
22235 Some(cx.theme().status().created_background)
22236 );
22237 },
22238 )
22239 .await;
22240
22241 // Multiple edits
22242 assert_highlighted_edits(
22243 "Hello, world!",
22244 vec![
22245 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22246 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22247 ],
22248 false,
22249 cx,
22250 |highlighted_edits, cx| {
22251 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22252 assert_eq!(highlighted_edits.highlights.len(), 2);
22253 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22254 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22255 assert_eq!(
22256 highlighted_edits.highlights[0].1.background_color,
22257 Some(cx.theme().status().created_background)
22258 );
22259 assert_eq!(
22260 highlighted_edits.highlights[1].1.background_color,
22261 Some(cx.theme().status().created_background)
22262 );
22263 },
22264 )
22265 .await;
22266
22267 // Multiple lines with edits
22268 assert_highlighted_edits(
22269 "First line\nSecond line\nThird line\nFourth line",
22270 vec![
22271 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22272 (
22273 Point::new(2, 0)..Point::new(2, 10),
22274 "New third line".to_string(),
22275 ),
22276 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22277 ],
22278 false,
22279 cx,
22280 |highlighted_edits, cx| {
22281 assert_eq!(
22282 highlighted_edits.text,
22283 "Second modified\nNew third line\nFourth updated line"
22284 );
22285 assert_eq!(highlighted_edits.highlights.len(), 3);
22286 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22287 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22288 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22289 for highlight in &highlighted_edits.highlights {
22290 assert_eq!(
22291 highlight.1.background_color,
22292 Some(cx.theme().status().created_background)
22293 );
22294 }
22295 },
22296 )
22297 .await;
22298}
22299
22300#[gpui::test]
22301async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22302 init_test(cx, |_| {});
22303
22304 // Deletion
22305 assert_highlighted_edits(
22306 "Hello, world!",
22307 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22308 true,
22309 cx,
22310 |highlighted_edits, cx| {
22311 assert_eq!(highlighted_edits.text, "Hello, world!");
22312 assert_eq!(highlighted_edits.highlights.len(), 1);
22313 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22314 assert_eq!(
22315 highlighted_edits.highlights[0].1.background_color,
22316 Some(cx.theme().status().deleted_background)
22317 );
22318 },
22319 )
22320 .await;
22321
22322 // Insertion
22323 assert_highlighted_edits(
22324 "Hello, world!",
22325 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22326 true,
22327 cx,
22328 |highlighted_edits, cx| {
22329 assert_eq!(highlighted_edits.highlights.len(), 1);
22330 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22331 assert_eq!(
22332 highlighted_edits.highlights[0].1.background_color,
22333 Some(cx.theme().status().created_background)
22334 );
22335 },
22336 )
22337 .await;
22338}
22339
22340async fn assert_highlighted_edits(
22341 text: &str,
22342 edits: Vec<(Range<Point>, String)>,
22343 include_deletions: bool,
22344 cx: &mut TestAppContext,
22345 assertion_fn: impl Fn(HighlightedText, &App),
22346) {
22347 let window = cx.add_window(|window, cx| {
22348 let buffer = MultiBuffer::build_simple(text, cx);
22349 Editor::new(EditorMode::full(), buffer, None, window, cx)
22350 });
22351 let cx = &mut VisualTestContext::from_window(*window, cx);
22352
22353 let (buffer, snapshot) = window
22354 .update(cx, |editor, _window, cx| {
22355 (
22356 editor.buffer().clone(),
22357 editor.buffer().read(cx).snapshot(cx),
22358 )
22359 })
22360 .unwrap();
22361
22362 let edits = edits
22363 .into_iter()
22364 .map(|(range, edit)| {
22365 (
22366 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22367 edit,
22368 )
22369 })
22370 .collect::<Vec<_>>();
22371
22372 let text_anchor_edits = edits
22373 .clone()
22374 .into_iter()
22375 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22376 .collect::<Vec<_>>();
22377
22378 let edit_preview = window
22379 .update(cx, |_, _window, cx| {
22380 buffer
22381 .read(cx)
22382 .as_singleton()
22383 .unwrap()
22384 .read(cx)
22385 .preview_edits(text_anchor_edits.into(), cx)
22386 })
22387 .unwrap()
22388 .await;
22389
22390 cx.update(|_window, cx| {
22391 let highlighted_edits = edit_prediction_edit_text(
22392 snapshot.as_singleton().unwrap().2,
22393 &edits,
22394 &edit_preview,
22395 include_deletions,
22396 cx,
22397 );
22398 assertion_fn(highlighted_edits, cx)
22399 });
22400}
22401
22402#[track_caller]
22403fn assert_breakpoint(
22404 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22405 path: &Arc<Path>,
22406 expected: Vec<(u32, Breakpoint)>,
22407) {
22408 if expected.is_empty() {
22409 assert!(!breakpoints.contains_key(path), "{}", path.display());
22410 } else {
22411 let mut breakpoint = breakpoints
22412 .get(path)
22413 .unwrap()
22414 .iter()
22415 .map(|breakpoint| {
22416 (
22417 breakpoint.row,
22418 Breakpoint {
22419 message: breakpoint.message.clone(),
22420 state: breakpoint.state,
22421 condition: breakpoint.condition.clone(),
22422 hit_condition: breakpoint.hit_condition.clone(),
22423 },
22424 )
22425 })
22426 .collect::<Vec<_>>();
22427
22428 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22429
22430 assert_eq!(expected, breakpoint);
22431 }
22432}
22433
22434fn add_log_breakpoint_at_cursor(
22435 editor: &mut Editor,
22436 log_message: &str,
22437 window: &mut Window,
22438 cx: &mut Context<Editor>,
22439) {
22440 let (anchor, bp) = editor
22441 .breakpoints_at_cursors(window, cx)
22442 .first()
22443 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22444 .unwrap_or_else(|| {
22445 let cursor_position: Point = editor.selections.newest(cx).head();
22446
22447 let breakpoint_position = editor
22448 .snapshot(window, cx)
22449 .display_snapshot
22450 .buffer_snapshot
22451 .anchor_before(Point::new(cursor_position.row, 0));
22452
22453 (breakpoint_position, Breakpoint::new_log(log_message))
22454 });
22455
22456 editor.edit_breakpoint_at_anchor(
22457 anchor,
22458 bp,
22459 BreakpointEditAction::EditLogMessage(log_message.into()),
22460 cx,
22461 );
22462}
22463
22464#[gpui::test]
22465async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22466 init_test(cx, |_| {});
22467
22468 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22469 let fs = FakeFs::new(cx.executor());
22470 fs.insert_tree(
22471 path!("/a"),
22472 json!({
22473 "main.rs": sample_text,
22474 }),
22475 )
22476 .await;
22477 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22478 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22479 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22480
22481 let fs = FakeFs::new(cx.executor());
22482 fs.insert_tree(
22483 path!("/a"),
22484 json!({
22485 "main.rs": sample_text,
22486 }),
22487 )
22488 .await;
22489 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22490 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22491 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22492 let worktree_id = workspace
22493 .update(cx, |workspace, _window, cx| {
22494 workspace.project().update(cx, |project, cx| {
22495 project.worktrees(cx).next().unwrap().read(cx).id()
22496 })
22497 })
22498 .unwrap();
22499
22500 let buffer = project
22501 .update(cx, |project, cx| {
22502 project.open_buffer((worktree_id, "main.rs"), cx)
22503 })
22504 .await
22505 .unwrap();
22506
22507 let (editor, cx) = cx.add_window_view(|window, cx| {
22508 Editor::new(
22509 EditorMode::full(),
22510 MultiBuffer::build_from_buffer(buffer, cx),
22511 Some(project.clone()),
22512 window,
22513 cx,
22514 )
22515 });
22516
22517 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22518 let abs_path = project.read_with(cx, |project, cx| {
22519 project
22520 .absolute_path(&project_path, cx)
22521 .map(Arc::from)
22522 .unwrap()
22523 });
22524
22525 // assert we can add breakpoint on the first line
22526 editor.update_in(cx, |editor, window, cx| {
22527 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22528 editor.move_to_end(&MoveToEnd, window, cx);
22529 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22530 });
22531
22532 let breakpoints = editor.update(cx, |editor, cx| {
22533 editor
22534 .breakpoint_store()
22535 .as_ref()
22536 .unwrap()
22537 .read(cx)
22538 .all_source_breakpoints(cx)
22539 });
22540
22541 assert_eq!(1, breakpoints.len());
22542 assert_breakpoint(
22543 &breakpoints,
22544 &abs_path,
22545 vec![
22546 (0, Breakpoint::new_standard()),
22547 (3, Breakpoint::new_standard()),
22548 ],
22549 );
22550
22551 editor.update_in(cx, |editor, window, cx| {
22552 editor.move_to_beginning(&MoveToBeginning, window, cx);
22553 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22554 });
22555
22556 let breakpoints = editor.update(cx, |editor, cx| {
22557 editor
22558 .breakpoint_store()
22559 .as_ref()
22560 .unwrap()
22561 .read(cx)
22562 .all_source_breakpoints(cx)
22563 });
22564
22565 assert_eq!(1, breakpoints.len());
22566 assert_breakpoint(
22567 &breakpoints,
22568 &abs_path,
22569 vec![(3, Breakpoint::new_standard())],
22570 );
22571
22572 editor.update_in(cx, |editor, window, cx| {
22573 editor.move_to_end(&MoveToEnd, window, cx);
22574 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22575 });
22576
22577 let breakpoints = editor.update(cx, |editor, cx| {
22578 editor
22579 .breakpoint_store()
22580 .as_ref()
22581 .unwrap()
22582 .read(cx)
22583 .all_source_breakpoints(cx)
22584 });
22585
22586 assert_eq!(0, breakpoints.len());
22587 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22588}
22589
22590#[gpui::test]
22591async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22592 init_test(cx, |_| {});
22593
22594 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22595
22596 let fs = FakeFs::new(cx.executor());
22597 fs.insert_tree(
22598 path!("/a"),
22599 json!({
22600 "main.rs": sample_text,
22601 }),
22602 )
22603 .await;
22604 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22605 let (workspace, cx) =
22606 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22607
22608 let worktree_id = workspace.update(cx, |workspace, cx| {
22609 workspace.project().update(cx, |project, cx| {
22610 project.worktrees(cx).next().unwrap().read(cx).id()
22611 })
22612 });
22613
22614 let buffer = project
22615 .update(cx, |project, cx| {
22616 project.open_buffer((worktree_id, "main.rs"), cx)
22617 })
22618 .await
22619 .unwrap();
22620
22621 let (editor, cx) = cx.add_window_view(|window, cx| {
22622 Editor::new(
22623 EditorMode::full(),
22624 MultiBuffer::build_from_buffer(buffer, cx),
22625 Some(project.clone()),
22626 window,
22627 cx,
22628 )
22629 });
22630
22631 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22632 let abs_path = project.read_with(cx, |project, cx| {
22633 project
22634 .absolute_path(&project_path, cx)
22635 .map(Arc::from)
22636 .unwrap()
22637 });
22638
22639 editor.update_in(cx, |editor, window, cx| {
22640 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22641 });
22642
22643 let breakpoints = editor.update(cx, |editor, cx| {
22644 editor
22645 .breakpoint_store()
22646 .as_ref()
22647 .unwrap()
22648 .read(cx)
22649 .all_source_breakpoints(cx)
22650 });
22651
22652 assert_breakpoint(
22653 &breakpoints,
22654 &abs_path,
22655 vec![(0, Breakpoint::new_log("hello world"))],
22656 );
22657
22658 // Removing a log message from a log breakpoint should remove it
22659 editor.update_in(cx, |editor, window, cx| {
22660 add_log_breakpoint_at_cursor(editor, "", window, cx);
22661 });
22662
22663 let breakpoints = editor.update(cx, |editor, cx| {
22664 editor
22665 .breakpoint_store()
22666 .as_ref()
22667 .unwrap()
22668 .read(cx)
22669 .all_source_breakpoints(cx)
22670 });
22671
22672 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22673
22674 editor.update_in(cx, |editor, window, cx| {
22675 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22676 editor.move_to_end(&MoveToEnd, window, cx);
22677 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22678 // Not adding a log message to a standard breakpoint shouldn't remove it
22679 add_log_breakpoint_at_cursor(editor, "", window, cx);
22680 });
22681
22682 let breakpoints = editor.update(cx, |editor, cx| {
22683 editor
22684 .breakpoint_store()
22685 .as_ref()
22686 .unwrap()
22687 .read(cx)
22688 .all_source_breakpoints(cx)
22689 });
22690
22691 assert_breakpoint(
22692 &breakpoints,
22693 &abs_path,
22694 vec![
22695 (0, Breakpoint::new_standard()),
22696 (3, Breakpoint::new_standard()),
22697 ],
22698 );
22699
22700 editor.update_in(cx, |editor, window, cx| {
22701 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22702 });
22703
22704 let breakpoints = editor.update(cx, |editor, cx| {
22705 editor
22706 .breakpoint_store()
22707 .as_ref()
22708 .unwrap()
22709 .read(cx)
22710 .all_source_breakpoints(cx)
22711 });
22712
22713 assert_breakpoint(
22714 &breakpoints,
22715 &abs_path,
22716 vec![
22717 (0, Breakpoint::new_standard()),
22718 (3, Breakpoint::new_log("hello world")),
22719 ],
22720 );
22721
22722 editor.update_in(cx, |editor, window, cx| {
22723 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22724 });
22725
22726 let breakpoints = editor.update(cx, |editor, cx| {
22727 editor
22728 .breakpoint_store()
22729 .as_ref()
22730 .unwrap()
22731 .read(cx)
22732 .all_source_breakpoints(cx)
22733 });
22734
22735 assert_breakpoint(
22736 &breakpoints,
22737 &abs_path,
22738 vec![
22739 (0, Breakpoint::new_standard()),
22740 (3, Breakpoint::new_log("hello Earth!!")),
22741 ],
22742 );
22743}
22744
22745/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22746/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22747/// or when breakpoints were placed out of order. This tests for a regression too
22748#[gpui::test]
22749async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22750 init_test(cx, |_| {});
22751
22752 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22753 let fs = FakeFs::new(cx.executor());
22754 fs.insert_tree(
22755 path!("/a"),
22756 json!({
22757 "main.rs": sample_text,
22758 }),
22759 )
22760 .await;
22761 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22762 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22763 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22764
22765 let fs = FakeFs::new(cx.executor());
22766 fs.insert_tree(
22767 path!("/a"),
22768 json!({
22769 "main.rs": sample_text,
22770 }),
22771 )
22772 .await;
22773 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22774 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22775 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22776 let worktree_id = workspace
22777 .update(cx, |workspace, _window, cx| {
22778 workspace.project().update(cx, |project, cx| {
22779 project.worktrees(cx).next().unwrap().read(cx).id()
22780 })
22781 })
22782 .unwrap();
22783
22784 let buffer = project
22785 .update(cx, |project, cx| {
22786 project.open_buffer((worktree_id, "main.rs"), cx)
22787 })
22788 .await
22789 .unwrap();
22790
22791 let (editor, cx) = cx.add_window_view(|window, cx| {
22792 Editor::new(
22793 EditorMode::full(),
22794 MultiBuffer::build_from_buffer(buffer, cx),
22795 Some(project.clone()),
22796 window,
22797 cx,
22798 )
22799 });
22800
22801 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22802 let abs_path = project.read_with(cx, |project, cx| {
22803 project
22804 .absolute_path(&project_path, cx)
22805 .map(Arc::from)
22806 .unwrap()
22807 });
22808
22809 // assert we can add breakpoint on the first line
22810 editor.update_in(cx, |editor, window, cx| {
22811 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22812 editor.move_to_end(&MoveToEnd, window, cx);
22813 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22814 editor.move_up(&MoveUp, window, cx);
22815 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22816 });
22817
22818 let breakpoints = editor.update(cx, |editor, cx| {
22819 editor
22820 .breakpoint_store()
22821 .as_ref()
22822 .unwrap()
22823 .read(cx)
22824 .all_source_breakpoints(cx)
22825 });
22826
22827 assert_eq!(1, breakpoints.len());
22828 assert_breakpoint(
22829 &breakpoints,
22830 &abs_path,
22831 vec![
22832 (0, Breakpoint::new_standard()),
22833 (2, Breakpoint::new_standard()),
22834 (3, Breakpoint::new_standard()),
22835 ],
22836 );
22837
22838 editor.update_in(cx, |editor, window, cx| {
22839 editor.move_to_beginning(&MoveToBeginning, window, cx);
22840 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22841 editor.move_to_end(&MoveToEnd, window, cx);
22842 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22843 // Disabling a breakpoint that doesn't exist should do nothing
22844 editor.move_up(&MoveUp, window, cx);
22845 editor.move_up(&MoveUp, window, cx);
22846 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22847 });
22848
22849 let breakpoints = editor.update(cx, |editor, cx| {
22850 editor
22851 .breakpoint_store()
22852 .as_ref()
22853 .unwrap()
22854 .read(cx)
22855 .all_source_breakpoints(cx)
22856 });
22857
22858 let disable_breakpoint = {
22859 let mut bp = Breakpoint::new_standard();
22860 bp.state = BreakpointState::Disabled;
22861 bp
22862 };
22863
22864 assert_eq!(1, breakpoints.len());
22865 assert_breakpoint(
22866 &breakpoints,
22867 &abs_path,
22868 vec![
22869 (0, disable_breakpoint.clone()),
22870 (2, Breakpoint::new_standard()),
22871 (3, disable_breakpoint.clone()),
22872 ],
22873 );
22874
22875 editor.update_in(cx, |editor, window, cx| {
22876 editor.move_to_beginning(&MoveToBeginning, window, cx);
22877 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22878 editor.move_to_end(&MoveToEnd, window, cx);
22879 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22880 editor.move_up(&MoveUp, window, cx);
22881 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22882 });
22883
22884 let breakpoints = editor.update(cx, |editor, cx| {
22885 editor
22886 .breakpoint_store()
22887 .as_ref()
22888 .unwrap()
22889 .read(cx)
22890 .all_source_breakpoints(cx)
22891 });
22892
22893 assert_eq!(1, breakpoints.len());
22894 assert_breakpoint(
22895 &breakpoints,
22896 &abs_path,
22897 vec![
22898 (0, Breakpoint::new_standard()),
22899 (2, disable_breakpoint),
22900 (3, Breakpoint::new_standard()),
22901 ],
22902 );
22903}
22904
22905#[gpui::test]
22906async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22907 init_test(cx, |_| {});
22908 let capabilities = lsp::ServerCapabilities {
22909 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22910 prepare_provider: Some(true),
22911 work_done_progress_options: Default::default(),
22912 })),
22913 ..Default::default()
22914 };
22915 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22916
22917 cx.set_state(indoc! {"
22918 struct Fˇoo {}
22919 "});
22920
22921 cx.update_editor(|editor, _, cx| {
22922 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22923 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22924 editor.highlight_background::<DocumentHighlightRead>(
22925 &[highlight_range],
22926 |theme| theme.colors().editor_document_highlight_read_background,
22927 cx,
22928 );
22929 });
22930
22931 let mut prepare_rename_handler = cx
22932 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22933 move |_, _, _| async move {
22934 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22935 start: lsp::Position {
22936 line: 0,
22937 character: 7,
22938 },
22939 end: lsp::Position {
22940 line: 0,
22941 character: 10,
22942 },
22943 })))
22944 },
22945 );
22946 let prepare_rename_task = cx
22947 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22948 .expect("Prepare rename was not started");
22949 prepare_rename_handler.next().await.unwrap();
22950 prepare_rename_task.await.expect("Prepare rename failed");
22951
22952 let mut rename_handler =
22953 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22954 let edit = lsp::TextEdit {
22955 range: lsp::Range {
22956 start: lsp::Position {
22957 line: 0,
22958 character: 7,
22959 },
22960 end: lsp::Position {
22961 line: 0,
22962 character: 10,
22963 },
22964 },
22965 new_text: "FooRenamed".to_string(),
22966 };
22967 Ok(Some(lsp::WorkspaceEdit::new(
22968 // Specify the same edit twice
22969 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22970 )))
22971 });
22972 let rename_task = cx
22973 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22974 .expect("Confirm rename was not started");
22975 rename_handler.next().await.unwrap();
22976 rename_task.await.expect("Confirm rename failed");
22977 cx.run_until_parked();
22978
22979 // Despite two edits, only one is actually applied as those are identical
22980 cx.assert_editor_state(indoc! {"
22981 struct FooRenamedˇ {}
22982 "});
22983}
22984
22985#[gpui::test]
22986async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22987 init_test(cx, |_| {});
22988 // These capabilities indicate that the server does not support prepare rename.
22989 let capabilities = lsp::ServerCapabilities {
22990 rename_provider: Some(lsp::OneOf::Left(true)),
22991 ..Default::default()
22992 };
22993 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22994
22995 cx.set_state(indoc! {"
22996 struct Fˇoo {}
22997 "});
22998
22999 cx.update_editor(|editor, _window, cx| {
23000 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23001 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23002 editor.highlight_background::<DocumentHighlightRead>(
23003 &[highlight_range],
23004 |theme| theme.colors().editor_document_highlight_read_background,
23005 cx,
23006 );
23007 });
23008
23009 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23010 .expect("Prepare rename was not started")
23011 .await
23012 .expect("Prepare rename failed");
23013
23014 let mut rename_handler =
23015 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23016 let edit = lsp::TextEdit {
23017 range: lsp::Range {
23018 start: lsp::Position {
23019 line: 0,
23020 character: 7,
23021 },
23022 end: lsp::Position {
23023 line: 0,
23024 character: 10,
23025 },
23026 },
23027 new_text: "FooRenamed".to_string(),
23028 };
23029 Ok(Some(lsp::WorkspaceEdit::new(
23030 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23031 )))
23032 });
23033 let rename_task = cx
23034 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23035 .expect("Confirm rename was not started");
23036 rename_handler.next().await.unwrap();
23037 rename_task.await.expect("Confirm rename failed");
23038 cx.run_until_parked();
23039
23040 // Correct range is renamed, as `surrounding_word` is used to find it.
23041 cx.assert_editor_state(indoc! {"
23042 struct FooRenamedˇ {}
23043 "});
23044}
23045
23046#[gpui::test]
23047async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23048 init_test(cx, |_| {});
23049 let mut cx = EditorTestContext::new(cx).await;
23050
23051 let language = Arc::new(
23052 Language::new(
23053 LanguageConfig::default(),
23054 Some(tree_sitter_html::LANGUAGE.into()),
23055 )
23056 .with_brackets_query(
23057 r#"
23058 ("<" @open "/>" @close)
23059 ("</" @open ">" @close)
23060 ("<" @open ">" @close)
23061 ("\"" @open "\"" @close)
23062 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23063 "#,
23064 )
23065 .unwrap(),
23066 );
23067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23068
23069 cx.set_state(indoc! {"
23070 <span>ˇ</span>
23071 "});
23072 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23073 cx.assert_editor_state(indoc! {"
23074 <span>
23075 ˇ
23076 </span>
23077 "});
23078
23079 cx.set_state(indoc! {"
23080 <span><span></span>ˇ</span>
23081 "});
23082 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23083 cx.assert_editor_state(indoc! {"
23084 <span><span></span>
23085 ˇ</span>
23086 "});
23087
23088 cx.set_state(indoc! {"
23089 <span>ˇ
23090 </span>
23091 "});
23092 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23093 cx.assert_editor_state(indoc! {"
23094 <span>
23095 ˇ
23096 </span>
23097 "});
23098}
23099
23100#[gpui::test(iterations = 10)]
23101async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23102 init_test(cx, |_| {});
23103
23104 let fs = FakeFs::new(cx.executor());
23105 fs.insert_tree(
23106 path!("/dir"),
23107 json!({
23108 "a.ts": "a",
23109 }),
23110 )
23111 .await;
23112
23113 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23114 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23115 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23116
23117 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23118 language_registry.add(Arc::new(Language::new(
23119 LanguageConfig {
23120 name: "TypeScript".into(),
23121 matcher: LanguageMatcher {
23122 path_suffixes: vec!["ts".to_string()],
23123 ..Default::default()
23124 },
23125 ..Default::default()
23126 },
23127 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23128 )));
23129 let mut fake_language_servers = language_registry.register_fake_lsp(
23130 "TypeScript",
23131 FakeLspAdapter {
23132 capabilities: lsp::ServerCapabilities {
23133 code_lens_provider: Some(lsp::CodeLensOptions {
23134 resolve_provider: Some(true),
23135 }),
23136 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23137 commands: vec!["_the/command".to_string()],
23138 ..lsp::ExecuteCommandOptions::default()
23139 }),
23140 ..lsp::ServerCapabilities::default()
23141 },
23142 ..FakeLspAdapter::default()
23143 },
23144 );
23145
23146 let editor = workspace
23147 .update(cx, |workspace, window, cx| {
23148 workspace.open_abs_path(
23149 PathBuf::from(path!("/dir/a.ts")),
23150 OpenOptions::default(),
23151 window,
23152 cx,
23153 )
23154 })
23155 .unwrap()
23156 .await
23157 .unwrap()
23158 .downcast::<Editor>()
23159 .unwrap();
23160 cx.executor().run_until_parked();
23161
23162 let fake_server = fake_language_servers.next().await.unwrap();
23163
23164 let buffer = editor.update(cx, |editor, cx| {
23165 editor
23166 .buffer()
23167 .read(cx)
23168 .as_singleton()
23169 .expect("have opened a single file by path")
23170 });
23171
23172 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23173 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23174 drop(buffer_snapshot);
23175 let actions = cx
23176 .update_window(*workspace, |_, window, cx| {
23177 project.code_actions(&buffer, anchor..anchor, window, cx)
23178 })
23179 .unwrap();
23180
23181 fake_server
23182 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23183 Ok(Some(vec![
23184 lsp::CodeLens {
23185 range: lsp::Range::default(),
23186 command: Some(lsp::Command {
23187 title: "Code lens command".to_owned(),
23188 command: "_the/command".to_owned(),
23189 arguments: None,
23190 }),
23191 data: None,
23192 },
23193 lsp::CodeLens {
23194 range: lsp::Range::default(),
23195 command: Some(lsp::Command {
23196 title: "Command not in capabilities".to_owned(),
23197 command: "not in capabilities".to_owned(),
23198 arguments: None,
23199 }),
23200 data: None,
23201 },
23202 lsp::CodeLens {
23203 range: lsp::Range {
23204 start: lsp::Position {
23205 line: 1,
23206 character: 1,
23207 },
23208 end: lsp::Position {
23209 line: 1,
23210 character: 1,
23211 },
23212 },
23213 command: Some(lsp::Command {
23214 title: "Command not in range".to_owned(),
23215 command: "_the/command".to_owned(),
23216 arguments: None,
23217 }),
23218 data: None,
23219 },
23220 ]))
23221 })
23222 .next()
23223 .await;
23224
23225 let actions = actions.await.unwrap();
23226 assert_eq!(
23227 actions.len(),
23228 1,
23229 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23230 );
23231 let action = actions[0].clone();
23232 let apply = project.update(cx, |project, cx| {
23233 project.apply_code_action(buffer.clone(), action, true, cx)
23234 });
23235
23236 // Resolving the code action does not populate its edits. In absence of
23237 // edits, we must execute the given command.
23238 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23239 |mut lens, _| async move {
23240 let lens_command = lens.command.as_mut().expect("should have a command");
23241 assert_eq!(lens_command.title, "Code lens command");
23242 lens_command.arguments = Some(vec![json!("the-argument")]);
23243 Ok(lens)
23244 },
23245 );
23246
23247 // While executing the command, the language server sends the editor
23248 // a `workspaceEdit` request.
23249 fake_server
23250 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23251 let fake = fake_server.clone();
23252 move |params, _| {
23253 assert_eq!(params.command, "_the/command");
23254 let fake = fake.clone();
23255 async move {
23256 fake.server
23257 .request::<lsp::request::ApplyWorkspaceEdit>(
23258 lsp::ApplyWorkspaceEditParams {
23259 label: None,
23260 edit: lsp::WorkspaceEdit {
23261 changes: Some(
23262 [(
23263 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23264 vec![lsp::TextEdit {
23265 range: lsp::Range::new(
23266 lsp::Position::new(0, 0),
23267 lsp::Position::new(0, 0),
23268 ),
23269 new_text: "X".into(),
23270 }],
23271 )]
23272 .into_iter()
23273 .collect(),
23274 ),
23275 ..lsp::WorkspaceEdit::default()
23276 },
23277 },
23278 )
23279 .await
23280 .into_response()
23281 .unwrap();
23282 Ok(Some(json!(null)))
23283 }
23284 }
23285 })
23286 .next()
23287 .await;
23288
23289 // Applying the code lens command returns a project transaction containing the edits
23290 // sent by the language server in its `workspaceEdit` request.
23291 let transaction = apply.await.unwrap();
23292 assert!(transaction.0.contains_key(&buffer));
23293 buffer.update(cx, |buffer, cx| {
23294 assert_eq!(buffer.text(), "Xa");
23295 buffer.undo(cx);
23296 assert_eq!(buffer.text(), "a");
23297 });
23298
23299 let actions_after_edits = cx
23300 .update_window(*workspace, |_, window, cx| {
23301 project.code_actions(&buffer, anchor..anchor, window, cx)
23302 })
23303 .unwrap()
23304 .await
23305 .unwrap();
23306 assert_eq!(
23307 actions, actions_after_edits,
23308 "For the same selection, same code lens actions should be returned"
23309 );
23310
23311 let _responses =
23312 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23313 panic!("No more code lens requests are expected");
23314 });
23315 editor.update_in(cx, |editor, window, cx| {
23316 editor.select_all(&SelectAll, window, cx);
23317 });
23318 cx.executor().run_until_parked();
23319 let new_actions = cx
23320 .update_window(*workspace, |_, window, cx| {
23321 project.code_actions(&buffer, anchor..anchor, window, cx)
23322 })
23323 .unwrap()
23324 .await
23325 .unwrap();
23326 assert_eq!(
23327 actions, new_actions,
23328 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23329 );
23330}
23331
23332#[gpui::test]
23333async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23334 init_test(cx, |_| {});
23335
23336 let fs = FakeFs::new(cx.executor());
23337 let main_text = r#"fn main() {
23338println!("1");
23339println!("2");
23340println!("3");
23341println!("4");
23342println!("5");
23343}"#;
23344 let lib_text = "mod foo {}";
23345 fs.insert_tree(
23346 path!("/a"),
23347 json!({
23348 "lib.rs": lib_text,
23349 "main.rs": main_text,
23350 }),
23351 )
23352 .await;
23353
23354 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23355 let (workspace, cx) =
23356 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23357 let worktree_id = workspace.update(cx, |workspace, cx| {
23358 workspace.project().update(cx, |project, cx| {
23359 project.worktrees(cx).next().unwrap().read(cx).id()
23360 })
23361 });
23362
23363 let expected_ranges = vec![
23364 Point::new(0, 0)..Point::new(0, 0),
23365 Point::new(1, 0)..Point::new(1, 1),
23366 Point::new(2, 0)..Point::new(2, 2),
23367 Point::new(3, 0)..Point::new(3, 3),
23368 ];
23369
23370 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23371 let editor_1 = workspace
23372 .update_in(cx, |workspace, window, cx| {
23373 workspace.open_path(
23374 (worktree_id, "main.rs"),
23375 Some(pane_1.downgrade()),
23376 true,
23377 window,
23378 cx,
23379 )
23380 })
23381 .unwrap()
23382 .await
23383 .downcast::<Editor>()
23384 .unwrap();
23385 pane_1.update(cx, |pane, cx| {
23386 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23387 open_editor.update(cx, |editor, cx| {
23388 assert_eq!(
23389 editor.display_text(cx),
23390 main_text,
23391 "Original main.rs text on initial open",
23392 );
23393 assert_eq!(
23394 editor
23395 .selections
23396 .all::<Point>(cx)
23397 .into_iter()
23398 .map(|s| s.range())
23399 .collect::<Vec<_>>(),
23400 vec![Point::zero()..Point::zero()],
23401 "Default selections on initial open",
23402 );
23403 })
23404 });
23405 editor_1.update_in(cx, |editor, window, cx| {
23406 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23407 s.select_ranges(expected_ranges.clone());
23408 });
23409 });
23410
23411 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23412 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23413 });
23414 let editor_2 = workspace
23415 .update_in(cx, |workspace, window, cx| {
23416 workspace.open_path(
23417 (worktree_id, "main.rs"),
23418 Some(pane_2.downgrade()),
23419 true,
23420 window,
23421 cx,
23422 )
23423 })
23424 .unwrap()
23425 .await
23426 .downcast::<Editor>()
23427 .unwrap();
23428 pane_2.update(cx, |pane, cx| {
23429 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23430 open_editor.update(cx, |editor, cx| {
23431 assert_eq!(
23432 editor.display_text(cx),
23433 main_text,
23434 "Original main.rs text on initial open in another panel",
23435 );
23436 assert_eq!(
23437 editor
23438 .selections
23439 .all::<Point>(cx)
23440 .into_iter()
23441 .map(|s| s.range())
23442 .collect::<Vec<_>>(),
23443 vec![Point::zero()..Point::zero()],
23444 "Default selections on initial open in another panel",
23445 );
23446 })
23447 });
23448
23449 editor_2.update_in(cx, |editor, window, cx| {
23450 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23451 });
23452
23453 let _other_editor_1 = workspace
23454 .update_in(cx, |workspace, window, cx| {
23455 workspace.open_path(
23456 (worktree_id, "lib.rs"),
23457 Some(pane_1.downgrade()),
23458 true,
23459 window,
23460 cx,
23461 )
23462 })
23463 .unwrap()
23464 .await
23465 .downcast::<Editor>()
23466 .unwrap();
23467 pane_1
23468 .update_in(cx, |pane, window, cx| {
23469 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23470 })
23471 .await
23472 .unwrap();
23473 drop(editor_1);
23474 pane_1.update(cx, |pane, cx| {
23475 pane.active_item()
23476 .unwrap()
23477 .downcast::<Editor>()
23478 .unwrap()
23479 .update(cx, |editor, cx| {
23480 assert_eq!(
23481 editor.display_text(cx),
23482 lib_text,
23483 "Other file should be open and active",
23484 );
23485 });
23486 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23487 });
23488
23489 let _other_editor_2 = workspace
23490 .update_in(cx, |workspace, window, cx| {
23491 workspace.open_path(
23492 (worktree_id, "lib.rs"),
23493 Some(pane_2.downgrade()),
23494 true,
23495 window,
23496 cx,
23497 )
23498 })
23499 .unwrap()
23500 .await
23501 .downcast::<Editor>()
23502 .unwrap();
23503 pane_2
23504 .update_in(cx, |pane, window, cx| {
23505 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23506 })
23507 .await
23508 .unwrap();
23509 drop(editor_2);
23510 pane_2.update(cx, |pane, cx| {
23511 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23512 open_editor.update(cx, |editor, cx| {
23513 assert_eq!(
23514 editor.display_text(cx),
23515 lib_text,
23516 "Other file should be open and active in another panel too",
23517 );
23518 });
23519 assert_eq!(
23520 pane.items().count(),
23521 1,
23522 "No other editors should be open in another pane",
23523 );
23524 });
23525
23526 let _editor_1_reopened = workspace
23527 .update_in(cx, |workspace, window, cx| {
23528 workspace.open_path(
23529 (worktree_id, "main.rs"),
23530 Some(pane_1.downgrade()),
23531 true,
23532 window,
23533 cx,
23534 )
23535 })
23536 .unwrap()
23537 .await
23538 .downcast::<Editor>()
23539 .unwrap();
23540 let _editor_2_reopened = workspace
23541 .update_in(cx, |workspace, window, cx| {
23542 workspace.open_path(
23543 (worktree_id, "main.rs"),
23544 Some(pane_2.downgrade()),
23545 true,
23546 window,
23547 cx,
23548 )
23549 })
23550 .unwrap()
23551 .await
23552 .downcast::<Editor>()
23553 .unwrap();
23554 pane_1.update(cx, |pane, cx| {
23555 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23556 open_editor.update(cx, |editor, cx| {
23557 assert_eq!(
23558 editor.display_text(cx),
23559 main_text,
23560 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23561 );
23562 assert_eq!(
23563 editor
23564 .selections
23565 .all::<Point>(cx)
23566 .into_iter()
23567 .map(|s| s.range())
23568 .collect::<Vec<_>>(),
23569 expected_ranges,
23570 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23571 );
23572 })
23573 });
23574 pane_2.update(cx, |pane, cx| {
23575 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23576 open_editor.update(cx, |editor, cx| {
23577 assert_eq!(
23578 editor.display_text(cx),
23579 r#"fn main() {
23580⋯rintln!("1");
23581⋯intln!("2");
23582⋯ntln!("3");
23583println!("4");
23584println!("5");
23585}"#,
23586 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23587 );
23588 assert_eq!(
23589 editor
23590 .selections
23591 .all::<Point>(cx)
23592 .into_iter()
23593 .map(|s| s.range())
23594 .collect::<Vec<_>>(),
23595 vec![Point::zero()..Point::zero()],
23596 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23597 );
23598 })
23599 });
23600}
23601
23602#[gpui::test]
23603async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23604 init_test(cx, |_| {});
23605
23606 let fs = FakeFs::new(cx.executor());
23607 let main_text = r#"fn main() {
23608println!("1");
23609println!("2");
23610println!("3");
23611println!("4");
23612println!("5");
23613}"#;
23614 let lib_text = "mod foo {}";
23615 fs.insert_tree(
23616 path!("/a"),
23617 json!({
23618 "lib.rs": lib_text,
23619 "main.rs": main_text,
23620 }),
23621 )
23622 .await;
23623
23624 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23625 let (workspace, cx) =
23626 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23627 let worktree_id = workspace.update(cx, |workspace, cx| {
23628 workspace.project().update(cx, |project, cx| {
23629 project.worktrees(cx).next().unwrap().read(cx).id()
23630 })
23631 });
23632
23633 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23634 let editor = workspace
23635 .update_in(cx, |workspace, window, cx| {
23636 workspace.open_path(
23637 (worktree_id, "main.rs"),
23638 Some(pane.downgrade()),
23639 true,
23640 window,
23641 cx,
23642 )
23643 })
23644 .unwrap()
23645 .await
23646 .downcast::<Editor>()
23647 .unwrap();
23648 pane.update(cx, |pane, cx| {
23649 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23650 open_editor.update(cx, |editor, cx| {
23651 assert_eq!(
23652 editor.display_text(cx),
23653 main_text,
23654 "Original main.rs text on initial open",
23655 );
23656 })
23657 });
23658 editor.update_in(cx, |editor, window, cx| {
23659 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23660 });
23661
23662 cx.update_global(|store: &mut SettingsStore, cx| {
23663 store.update_user_settings(cx, |s| {
23664 s.workspace.restore_on_file_reopen = Some(false);
23665 });
23666 });
23667 editor.update_in(cx, |editor, window, cx| {
23668 editor.fold_ranges(
23669 vec![
23670 Point::new(1, 0)..Point::new(1, 1),
23671 Point::new(2, 0)..Point::new(2, 2),
23672 Point::new(3, 0)..Point::new(3, 3),
23673 ],
23674 false,
23675 window,
23676 cx,
23677 );
23678 });
23679 pane.update_in(cx, |pane, window, cx| {
23680 pane.close_all_items(&CloseAllItems::default(), window, cx)
23681 })
23682 .await
23683 .unwrap();
23684 pane.update(cx, |pane, _| {
23685 assert!(pane.active_item().is_none());
23686 });
23687 cx.update_global(|store: &mut SettingsStore, cx| {
23688 store.update_user_settings(cx, |s| {
23689 s.workspace.restore_on_file_reopen = Some(true);
23690 });
23691 });
23692
23693 let _editor_reopened = workspace
23694 .update_in(cx, |workspace, window, cx| {
23695 workspace.open_path(
23696 (worktree_id, "main.rs"),
23697 Some(pane.downgrade()),
23698 true,
23699 window,
23700 cx,
23701 )
23702 })
23703 .unwrap()
23704 .await
23705 .downcast::<Editor>()
23706 .unwrap();
23707 pane.update(cx, |pane, cx| {
23708 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23709 open_editor.update(cx, |editor, cx| {
23710 assert_eq!(
23711 editor.display_text(cx),
23712 main_text,
23713 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23714 );
23715 })
23716 });
23717}
23718
23719#[gpui::test]
23720async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23721 struct EmptyModalView {
23722 focus_handle: gpui::FocusHandle,
23723 }
23724 impl EventEmitter<DismissEvent> for EmptyModalView {}
23725 impl Render for EmptyModalView {
23726 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23727 div()
23728 }
23729 }
23730 impl Focusable for EmptyModalView {
23731 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23732 self.focus_handle.clone()
23733 }
23734 }
23735 impl workspace::ModalView for EmptyModalView {}
23736 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23737 EmptyModalView {
23738 focus_handle: cx.focus_handle(),
23739 }
23740 }
23741
23742 init_test(cx, |_| {});
23743
23744 let fs = FakeFs::new(cx.executor());
23745 let project = Project::test(fs, [], cx).await;
23746 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23747 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23748 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23749 let editor = cx.new_window_entity(|window, cx| {
23750 Editor::new(
23751 EditorMode::full(),
23752 buffer,
23753 Some(project.clone()),
23754 window,
23755 cx,
23756 )
23757 });
23758 workspace
23759 .update(cx, |workspace, window, cx| {
23760 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23761 })
23762 .unwrap();
23763 editor.update_in(cx, |editor, window, cx| {
23764 editor.open_context_menu(&OpenContextMenu, window, cx);
23765 assert!(editor.mouse_context_menu.is_some());
23766 });
23767 workspace
23768 .update(cx, |workspace, window, cx| {
23769 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23770 })
23771 .unwrap();
23772 cx.read(|cx| {
23773 assert!(editor.read(cx).mouse_context_menu.is_none());
23774 });
23775}
23776
23777fn set_linked_edit_ranges(
23778 opening: (Point, Point),
23779 closing: (Point, Point),
23780 editor: &mut Editor,
23781 cx: &mut Context<Editor>,
23782) {
23783 let Some((buffer, _)) = editor
23784 .buffer
23785 .read(cx)
23786 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23787 else {
23788 panic!("Failed to get buffer for selection position");
23789 };
23790 let buffer = buffer.read(cx);
23791 let buffer_id = buffer.remote_id();
23792 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23793 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23794 let mut linked_ranges = HashMap::default();
23795 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23796 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23797}
23798
23799#[gpui::test]
23800async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23801 init_test(cx, |_| {});
23802
23803 let fs = FakeFs::new(cx.executor());
23804 fs.insert_file(path!("/file.html"), Default::default())
23805 .await;
23806
23807 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23808
23809 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23810 let html_language = Arc::new(Language::new(
23811 LanguageConfig {
23812 name: "HTML".into(),
23813 matcher: LanguageMatcher {
23814 path_suffixes: vec!["html".to_string()],
23815 ..LanguageMatcher::default()
23816 },
23817 brackets: BracketPairConfig {
23818 pairs: vec![BracketPair {
23819 start: "<".into(),
23820 end: ">".into(),
23821 close: true,
23822 ..Default::default()
23823 }],
23824 ..Default::default()
23825 },
23826 ..Default::default()
23827 },
23828 Some(tree_sitter_html::LANGUAGE.into()),
23829 ));
23830 language_registry.add(html_language);
23831 let mut fake_servers = language_registry.register_fake_lsp(
23832 "HTML",
23833 FakeLspAdapter {
23834 capabilities: lsp::ServerCapabilities {
23835 completion_provider: Some(lsp::CompletionOptions {
23836 resolve_provider: Some(true),
23837 ..Default::default()
23838 }),
23839 ..Default::default()
23840 },
23841 ..Default::default()
23842 },
23843 );
23844
23845 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23846 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23847
23848 let worktree_id = workspace
23849 .update(cx, |workspace, _window, cx| {
23850 workspace.project().update(cx, |project, cx| {
23851 project.worktrees(cx).next().unwrap().read(cx).id()
23852 })
23853 })
23854 .unwrap();
23855 project
23856 .update(cx, |project, cx| {
23857 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23858 })
23859 .await
23860 .unwrap();
23861 let editor = workspace
23862 .update(cx, |workspace, window, cx| {
23863 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23864 })
23865 .unwrap()
23866 .await
23867 .unwrap()
23868 .downcast::<Editor>()
23869 .unwrap();
23870
23871 let fake_server = fake_servers.next().await.unwrap();
23872 editor.update_in(cx, |editor, window, cx| {
23873 editor.set_text("<ad></ad>", window, cx);
23874 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23875 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23876 });
23877 set_linked_edit_ranges(
23878 (Point::new(0, 1), Point::new(0, 3)),
23879 (Point::new(0, 6), Point::new(0, 8)),
23880 editor,
23881 cx,
23882 );
23883 });
23884 let mut completion_handle =
23885 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23886 Ok(Some(lsp::CompletionResponse::Array(vec![
23887 lsp::CompletionItem {
23888 label: "head".to_string(),
23889 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23890 lsp::InsertReplaceEdit {
23891 new_text: "head".to_string(),
23892 insert: lsp::Range::new(
23893 lsp::Position::new(0, 1),
23894 lsp::Position::new(0, 3),
23895 ),
23896 replace: lsp::Range::new(
23897 lsp::Position::new(0, 1),
23898 lsp::Position::new(0, 3),
23899 ),
23900 },
23901 )),
23902 ..Default::default()
23903 },
23904 ])))
23905 });
23906 editor.update_in(cx, |editor, window, cx| {
23907 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23908 });
23909 cx.run_until_parked();
23910 completion_handle.next().await.unwrap();
23911 editor.update(cx, |editor, _| {
23912 assert!(
23913 editor.context_menu_visible(),
23914 "Completion menu should be visible"
23915 );
23916 });
23917 editor.update_in(cx, |editor, window, cx| {
23918 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23919 });
23920 cx.executor().run_until_parked();
23921 editor.update(cx, |editor, cx| {
23922 assert_eq!(editor.text(cx), "<head></head>");
23923 });
23924}
23925
23926#[gpui::test]
23927async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
23928 init_test(cx, |_| {});
23929
23930 let mut cx = EditorTestContext::new(cx).await;
23931 let language = Arc::new(Language::new(
23932 LanguageConfig {
23933 name: "TSX".into(),
23934 matcher: LanguageMatcher {
23935 path_suffixes: vec!["tsx".to_string()],
23936 ..LanguageMatcher::default()
23937 },
23938 brackets: BracketPairConfig {
23939 pairs: vec![BracketPair {
23940 start: "<".into(),
23941 end: ">".into(),
23942 close: true,
23943 ..Default::default()
23944 }],
23945 ..Default::default()
23946 },
23947 linked_edit_characters: HashSet::from_iter(['.']),
23948 ..Default::default()
23949 },
23950 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
23951 ));
23952 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23953
23954 // Test typing > does not extend linked pair
23955 cx.set_state("<divˇ<div></div>");
23956 cx.update_editor(|editor, _, cx| {
23957 set_linked_edit_ranges(
23958 (Point::new(0, 1), Point::new(0, 4)),
23959 (Point::new(0, 11), Point::new(0, 14)),
23960 editor,
23961 cx,
23962 );
23963 });
23964 cx.update_editor(|editor, window, cx| {
23965 editor.handle_input(">", window, cx);
23966 });
23967 cx.assert_editor_state("<div>ˇ<div></div>");
23968
23969 // Test typing . do extend linked pair
23970 cx.set_state("<Animatedˇ></Animated>");
23971 cx.update_editor(|editor, _, cx| {
23972 set_linked_edit_ranges(
23973 (Point::new(0, 1), Point::new(0, 9)),
23974 (Point::new(0, 12), Point::new(0, 20)),
23975 editor,
23976 cx,
23977 );
23978 });
23979 cx.update_editor(|editor, window, cx| {
23980 editor.handle_input(".", window, cx);
23981 });
23982 cx.assert_editor_state("<Animated.ˇ></Animated.>");
23983 cx.update_editor(|editor, _, cx| {
23984 set_linked_edit_ranges(
23985 (Point::new(0, 1), Point::new(0, 10)),
23986 (Point::new(0, 13), Point::new(0, 21)),
23987 editor,
23988 cx,
23989 );
23990 });
23991 cx.update_editor(|editor, window, cx| {
23992 editor.handle_input("V", window, cx);
23993 });
23994 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
23995}
23996
23997#[gpui::test]
23998async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23999 init_test(cx, |_| {});
24000
24001 let fs = FakeFs::new(cx.executor());
24002 fs.insert_tree(
24003 path!("/root"),
24004 json!({
24005 "a": {
24006 "main.rs": "fn main() {}",
24007 },
24008 "foo": {
24009 "bar": {
24010 "external_file.rs": "pub mod external {}",
24011 }
24012 }
24013 }),
24014 )
24015 .await;
24016
24017 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24018 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24019 language_registry.add(rust_lang());
24020 let _fake_servers = language_registry.register_fake_lsp(
24021 "Rust",
24022 FakeLspAdapter {
24023 ..FakeLspAdapter::default()
24024 },
24025 );
24026 let (workspace, cx) =
24027 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24028 let worktree_id = workspace.update(cx, |workspace, cx| {
24029 workspace.project().update(cx, |project, cx| {
24030 project.worktrees(cx).next().unwrap().read(cx).id()
24031 })
24032 });
24033
24034 let assert_language_servers_count =
24035 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24036 project.update(cx, |project, cx| {
24037 let current = project
24038 .lsp_store()
24039 .read(cx)
24040 .as_local()
24041 .unwrap()
24042 .language_servers
24043 .len();
24044 assert_eq!(expected, current, "{context}");
24045 });
24046 };
24047
24048 assert_language_servers_count(
24049 0,
24050 "No servers should be running before any file is open",
24051 cx,
24052 );
24053 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24054 let main_editor = workspace
24055 .update_in(cx, |workspace, window, cx| {
24056 workspace.open_path(
24057 (worktree_id, "main.rs"),
24058 Some(pane.downgrade()),
24059 true,
24060 window,
24061 cx,
24062 )
24063 })
24064 .unwrap()
24065 .await
24066 .downcast::<Editor>()
24067 .unwrap();
24068 pane.update(cx, |pane, cx| {
24069 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24070 open_editor.update(cx, |editor, cx| {
24071 assert_eq!(
24072 editor.display_text(cx),
24073 "fn main() {}",
24074 "Original main.rs text on initial open",
24075 );
24076 });
24077 assert_eq!(open_editor, main_editor);
24078 });
24079 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24080
24081 let external_editor = workspace
24082 .update_in(cx, |workspace, window, cx| {
24083 workspace.open_abs_path(
24084 PathBuf::from("/root/foo/bar/external_file.rs"),
24085 OpenOptions::default(),
24086 window,
24087 cx,
24088 )
24089 })
24090 .await
24091 .expect("opening external file")
24092 .downcast::<Editor>()
24093 .expect("downcasted external file's open element to editor");
24094 pane.update(cx, |pane, cx| {
24095 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24096 open_editor.update(cx, |editor, cx| {
24097 assert_eq!(
24098 editor.display_text(cx),
24099 "pub mod external {}",
24100 "External file is open now",
24101 );
24102 });
24103 assert_eq!(open_editor, external_editor);
24104 });
24105 assert_language_servers_count(
24106 1,
24107 "Second, external, *.rs file should join the existing server",
24108 cx,
24109 );
24110
24111 pane.update_in(cx, |pane, window, cx| {
24112 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24113 })
24114 .await
24115 .unwrap();
24116 pane.update_in(cx, |pane, window, cx| {
24117 pane.navigate_backward(&Default::default(), window, cx);
24118 });
24119 cx.run_until_parked();
24120 pane.update(cx, |pane, cx| {
24121 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24122 open_editor.update(cx, |editor, cx| {
24123 assert_eq!(
24124 editor.display_text(cx),
24125 "pub mod external {}",
24126 "External file is open now",
24127 );
24128 });
24129 });
24130 assert_language_servers_count(
24131 1,
24132 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24133 cx,
24134 );
24135
24136 cx.update(|_, cx| {
24137 workspace::reload(cx);
24138 });
24139 assert_language_servers_count(
24140 1,
24141 "After reloading the worktree with local and external files opened, only one project should be started",
24142 cx,
24143 );
24144}
24145
24146#[gpui::test]
24147async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24148 init_test(cx, |_| {});
24149
24150 let mut cx = EditorTestContext::new(cx).await;
24151 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24152 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24153
24154 // test cursor move to start of each line on tab
24155 // for `if`, `elif`, `else`, `while`, `with` and `for`
24156 cx.set_state(indoc! {"
24157 def main():
24158 ˇ for item in items:
24159 ˇ while item.active:
24160 ˇ if item.value > 10:
24161 ˇ continue
24162 ˇ elif item.value < 0:
24163 ˇ break
24164 ˇ else:
24165 ˇ with item.context() as ctx:
24166 ˇ yield count
24167 ˇ else:
24168 ˇ log('while else')
24169 ˇ else:
24170 ˇ log('for else')
24171 "});
24172 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24173 cx.assert_editor_state(indoc! {"
24174 def main():
24175 ˇfor item in items:
24176 ˇwhile item.active:
24177 ˇif item.value > 10:
24178 ˇcontinue
24179 ˇelif item.value < 0:
24180 ˇbreak
24181 ˇelse:
24182 ˇwith item.context() as ctx:
24183 ˇyield count
24184 ˇelse:
24185 ˇlog('while else')
24186 ˇelse:
24187 ˇlog('for else')
24188 "});
24189 // test relative indent is preserved when tab
24190 // for `if`, `elif`, `else`, `while`, `with` and `for`
24191 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24192 cx.assert_editor_state(indoc! {"
24193 def main():
24194 ˇfor item in items:
24195 ˇwhile item.active:
24196 ˇif item.value > 10:
24197 ˇcontinue
24198 ˇelif item.value < 0:
24199 ˇbreak
24200 ˇelse:
24201 ˇwith item.context() as ctx:
24202 ˇyield count
24203 ˇelse:
24204 ˇlog('while else')
24205 ˇelse:
24206 ˇlog('for else')
24207 "});
24208
24209 // test cursor move to start of each line on tab
24210 // for `try`, `except`, `else`, `finally`, `match` and `def`
24211 cx.set_state(indoc! {"
24212 def main():
24213 ˇ try:
24214 ˇ fetch()
24215 ˇ except ValueError:
24216 ˇ handle_error()
24217 ˇ else:
24218 ˇ match value:
24219 ˇ case _:
24220 ˇ finally:
24221 ˇ def status():
24222 ˇ return 0
24223 "});
24224 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24225 cx.assert_editor_state(indoc! {"
24226 def main():
24227 ˇtry:
24228 ˇfetch()
24229 ˇexcept ValueError:
24230 ˇhandle_error()
24231 ˇelse:
24232 ˇmatch value:
24233 ˇcase _:
24234 ˇfinally:
24235 ˇdef status():
24236 ˇreturn 0
24237 "});
24238 // test relative indent is preserved when tab
24239 // for `try`, `except`, `else`, `finally`, `match` and `def`
24240 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24241 cx.assert_editor_state(indoc! {"
24242 def main():
24243 ˇtry:
24244 ˇfetch()
24245 ˇexcept ValueError:
24246 ˇhandle_error()
24247 ˇelse:
24248 ˇmatch value:
24249 ˇcase _:
24250 ˇfinally:
24251 ˇdef status():
24252 ˇreturn 0
24253 "});
24254}
24255
24256#[gpui::test]
24257async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24258 init_test(cx, |_| {});
24259
24260 let mut cx = EditorTestContext::new(cx).await;
24261 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24262 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24263
24264 // test `else` auto outdents when typed inside `if` block
24265 cx.set_state(indoc! {"
24266 def main():
24267 if i == 2:
24268 return
24269 ˇ
24270 "});
24271 cx.update_editor(|editor, window, cx| {
24272 editor.handle_input("else:", window, cx);
24273 });
24274 cx.assert_editor_state(indoc! {"
24275 def main():
24276 if i == 2:
24277 return
24278 else:ˇ
24279 "});
24280
24281 // test `except` auto outdents when typed inside `try` block
24282 cx.set_state(indoc! {"
24283 def main():
24284 try:
24285 i = 2
24286 ˇ
24287 "});
24288 cx.update_editor(|editor, window, cx| {
24289 editor.handle_input("except:", window, cx);
24290 });
24291 cx.assert_editor_state(indoc! {"
24292 def main():
24293 try:
24294 i = 2
24295 except:ˇ
24296 "});
24297
24298 // test `else` auto outdents when typed inside `except` block
24299 cx.set_state(indoc! {"
24300 def main():
24301 try:
24302 i = 2
24303 except:
24304 j = 2
24305 ˇ
24306 "});
24307 cx.update_editor(|editor, window, cx| {
24308 editor.handle_input("else:", window, cx);
24309 });
24310 cx.assert_editor_state(indoc! {"
24311 def main():
24312 try:
24313 i = 2
24314 except:
24315 j = 2
24316 else:ˇ
24317 "});
24318
24319 // test `finally` auto outdents when typed inside `else` block
24320 cx.set_state(indoc! {"
24321 def main():
24322 try:
24323 i = 2
24324 except:
24325 j = 2
24326 else:
24327 k = 2
24328 ˇ
24329 "});
24330 cx.update_editor(|editor, window, cx| {
24331 editor.handle_input("finally:", window, cx);
24332 });
24333 cx.assert_editor_state(indoc! {"
24334 def main():
24335 try:
24336 i = 2
24337 except:
24338 j = 2
24339 else:
24340 k = 2
24341 finally:ˇ
24342 "});
24343
24344 // test `else` does not outdents when typed inside `except` block right after for block
24345 cx.set_state(indoc! {"
24346 def main():
24347 try:
24348 i = 2
24349 except:
24350 for i in range(n):
24351 pass
24352 ˇ
24353 "});
24354 cx.update_editor(|editor, window, cx| {
24355 editor.handle_input("else:", window, cx);
24356 });
24357 cx.assert_editor_state(indoc! {"
24358 def main():
24359 try:
24360 i = 2
24361 except:
24362 for i in range(n):
24363 pass
24364 else:ˇ
24365 "});
24366
24367 // test `finally` auto outdents when typed inside `else` block right after for block
24368 cx.set_state(indoc! {"
24369 def main():
24370 try:
24371 i = 2
24372 except:
24373 j = 2
24374 else:
24375 for i in range(n):
24376 pass
24377 ˇ
24378 "});
24379 cx.update_editor(|editor, window, cx| {
24380 editor.handle_input("finally:", window, cx);
24381 });
24382 cx.assert_editor_state(indoc! {"
24383 def main():
24384 try:
24385 i = 2
24386 except:
24387 j = 2
24388 else:
24389 for i in range(n):
24390 pass
24391 finally:ˇ
24392 "});
24393
24394 // test `except` outdents to inner "try" block
24395 cx.set_state(indoc! {"
24396 def main():
24397 try:
24398 i = 2
24399 if i == 2:
24400 try:
24401 i = 3
24402 ˇ
24403 "});
24404 cx.update_editor(|editor, window, cx| {
24405 editor.handle_input("except:", window, cx);
24406 });
24407 cx.assert_editor_state(indoc! {"
24408 def main():
24409 try:
24410 i = 2
24411 if i == 2:
24412 try:
24413 i = 3
24414 except:ˇ
24415 "});
24416
24417 // test `except` outdents to outer "try" block
24418 cx.set_state(indoc! {"
24419 def main():
24420 try:
24421 i = 2
24422 if i == 2:
24423 try:
24424 i = 3
24425 ˇ
24426 "});
24427 cx.update_editor(|editor, window, cx| {
24428 editor.handle_input("except:", window, cx);
24429 });
24430 cx.assert_editor_state(indoc! {"
24431 def main():
24432 try:
24433 i = 2
24434 if i == 2:
24435 try:
24436 i = 3
24437 except:ˇ
24438 "});
24439
24440 // test `else` stays at correct indent when typed after `for` block
24441 cx.set_state(indoc! {"
24442 def main():
24443 for i in range(10):
24444 if i == 3:
24445 break
24446 ˇ
24447 "});
24448 cx.update_editor(|editor, window, cx| {
24449 editor.handle_input("else:", window, cx);
24450 });
24451 cx.assert_editor_state(indoc! {"
24452 def main():
24453 for i in range(10):
24454 if i == 3:
24455 break
24456 else:ˇ
24457 "});
24458
24459 // test does not outdent on typing after line with square brackets
24460 cx.set_state(indoc! {"
24461 def f() -> list[str]:
24462 ˇ
24463 "});
24464 cx.update_editor(|editor, window, cx| {
24465 editor.handle_input("a", window, cx);
24466 });
24467 cx.assert_editor_state(indoc! {"
24468 def f() -> list[str]:
24469 aˇ
24470 "});
24471
24472 // test does not outdent on typing : after case keyword
24473 cx.set_state(indoc! {"
24474 match 1:
24475 caseˇ
24476 "});
24477 cx.update_editor(|editor, window, cx| {
24478 editor.handle_input(":", window, cx);
24479 });
24480 cx.assert_editor_state(indoc! {"
24481 match 1:
24482 case:ˇ
24483 "});
24484}
24485
24486#[gpui::test]
24487async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24488 init_test(cx, |_| {});
24489 update_test_language_settings(cx, |settings| {
24490 settings.defaults.extend_comment_on_newline = Some(false);
24491 });
24492 let mut cx = EditorTestContext::new(cx).await;
24493 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24494 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24495
24496 // test correct indent after newline on comment
24497 cx.set_state(indoc! {"
24498 # COMMENT:ˇ
24499 "});
24500 cx.update_editor(|editor, window, cx| {
24501 editor.newline(&Newline, window, cx);
24502 });
24503 cx.assert_editor_state(indoc! {"
24504 # COMMENT:
24505 ˇ
24506 "});
24507
24508 // test correct indent after newline in brackets
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 cx.set_state(indoc! {"
24523 (ˇ)
24524 "});
24525 cx.update_editor(|editor, window, cx| {
24526 editor.newline(&Newline, window, cx);
24527 });
24528 cx.run_until_parked();
24529 cx.assert_editor_state(indoc! {"
24530 (
24531 ˇ
24532 )
24533 "});
24534
24535 // do not indent after empty lists or dictionaries
24536 cx.set_state(indoc! {"
24537 a = []ˇ
24538 "});
24539 cx.update_editor(|editor, window, cx| {
24540 editor.newline(&Newline, window, cx);
24541 });
24542 cx.run_until_parked();
24543 cx.assert_editor_state(indoc! {"
24544 a = []
24545 ˇ
24546 "});
24547}
24548
24549#[gpui::test]
24550async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24551 init_test(cx, |_| {});
24552
24553 let mut cx = EditorTestContext::new(cx).await;
24554 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24555 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24556
24557 // test cursor move to start of each line on tab
24558 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24559 cx.set_state(indoc! {"
24560 function main() {
24561 ˇ for item in $items; do
24562 ˇ while [ -n \"$item\" ]; do
24563 ˇ if [ \"$value\" -gt 10 ]; then
24564 ˇ continue
24565 ˇ elif [ \"$value\" -lt 0 ]; then
24566 ˇ break
24567 ˇ else
24568 ˇ echo \"$item\"
24569 ˇ fi
24570 ˇ done
24571 ˇ done
24572 ˇ}
24573 "});
24574 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24575 cx.assert_editor_state(indoc! {"
24576 function main() {
24577 ˇfor item in $items; do
24578 ˇwhile [ -n \"$item\" ]; do
24579 ˇif [ \"$value\" -gt 10 ]; then
24580 ˇcontinue
24581 ˇelif [ \"$value\" -lt 0 ]; then
24582 ˇbreak
24583 ˇelse
24584 ˇecho \"$item\"
24585 ˇfi
24586 ˇdone
24587 ˇdone
24588 ˇ}
24589 "});
24590 // test relative indent is preserved when tab
24591 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24592 cx.assert_editor_state(indoc! {"
24593 function main() {
24594 ˇfor item in $items; do
24595 ˇwhile [ -n \"$item\" ]; do
24596 ˇif [ \"$value\" -gt 10 ]; then
24597 ˇcontinue
24598 ˇelif [ \"$value\" -lt 0 ]; then
24599 ˇbreak
24600 ˇelse
24601 ˇecho \"$item\"
24602 ˇfi
24603 ˇdone
24604 ˇdone
24605 ˇ}
24606 "});
24607
24608 // test cursor move to start of each line on tab
24609 // for `case` statement with patterns
24610 cx.set_state(indoc! {"
24611 function handle() {
24612 ˇ case \"$1\" in
24613 ˇ start)
24614 ˇ echo \"a\"
24615 ˇ ;;
24616 ˇ stop)
24617 ˇ echo \"b\"
24618 ˇ ;;
24619 ˇ *)
24620 ˇ echo \"c\"
24621 ˇ ;;
24622 ˇ esac
24623 ˇ}
24624 "});
24625 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24626 cx.assert_editor_state(indoc! {"
24627 function handle() {
24628 ˇcase \"$1\" in
24629 ˇstart)
24630 ˇecho \"a\"
24631 ˇ;;
24632 ˇstop)
24633 ˇecho \"b\"
24634 ˇ;;
24635 ˇ*)
24636 ˇecho \"c\"
24637 ˇ;;
24638 ˇesac
24639 ˇ}
24640 "});
24641}
24642
24643#[gpui::test]
24644async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24645 init_test(cx, |_| {});
24646
24647 let mut cx = EditorTestContext::new(cx).await;
24648 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24649 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24650
24651 // test indents on comment insert
24652 cx.set_state(indoc! {"
24653 function main() {
24654 ˇ for item in $items; do
24655 ˇ while [ -n \"$item\" ]; do
24656 ˇ if [ \"$value\" -gt 10 ]; then
24657 ˇ continue
24658 ˇ elif [ \"$value\" -lt 0 ]; then
24659 ˇ break
24660 ˇ else
24661 ˇ echo \"$item\"
24662 ˇ fi
24663 ˇ done
24664 ˇ done
24665 ˇ}
24666 "});
24667 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24668 cx.assert_editor_state(indoc! {"
24669 function main() {
24670 #ˇ for item in $items; do
24671 #ˇ while [ -n \"$item\" ]; do
24672 #ˇ if [ \"$value\" -gt 10 ]; then
24673 #ˇ continue
24674 #ˇ elif [ \"$value\" -lt 0 ]; then
24675 #ˇ break
24676 #ˇ else
24677 #ˇ echo \"$item\"
24678 #ˇ fi
24679 #ˇ done
24680 #ˇ done
24681 #ˇ}
24682 "});
24683}
24684
24685#[gpui::test]
24686async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24687 init_test(cx, |_| {});
24688
24689 let mut cx = EditorTestContext::new(cx).await;
24690 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24691 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24692
24693 // test `else` auto outdents when typed inside `if` block
24694 cx.set_state(indoc! {"
24695 if [ \"$1\" = \"test\" ]; then
24696 echo \"foo bar\"
24697 ˇ
24698 "});
24699 cx.update_editor(|editor, window, cx| {
24700 editor.handle_input("else", window, cx);
24701 });
24702 cx.assert_editor_state(indoc! {"
24703 if [ \"$1\" = \"test\" ]; then
24704 echo \"foo bar\"
24705 elseˇ
24706 "});
24707
24708 // test `elif` auto outdents when typed inside `if` block
24709 cx.set_state(indoc! {"
24710 if [ \"$1\" = \"test\" ]; then
24711 echo \"foo bar\"
24712 ˇ
24713 "});
24714 cx.update_editor(|editor, window, cx| {
24715 editor.handle_input("elif", window, cx);
24716 });
24717 cx.assert_editor_state(indoc! {"
24718 if [ \"$1\" = \"test\" ]; then
24719 echo \"foo bar\"
24720 elifˇ
24721 "});
24722
24723 // test `fi` auto outdents when typed inside `else` block
24724 cx.set_state(indoc! {"
24725 if [ \"$1\" = \"test\" ]; then
24726 echo \"foo bar\"
24727 else
24728 echo \"bar baz\"
24729 ˇ
24730 "});
24731 cx.update_editor(|editor, window, cx| {
24732 editor.handle_input("fi", window, cx);
24733 });
24734 cx.assert_editor_state(indoc! {"
24735 if [ \"$1\" = \"test\" ]; then
24736 echo \"foo bar\"
24737 else
24738 echo \"bar baz\"
24739 fiˇ
24740 "});
24741
24742 // test `done` auto outdents when typed inside `while` block
24743 cx.set_state(indoc! {"
24744 while read line; do
24745 echo \"$line\"
24746 ˇ
24747 "});
24748 cx.update_editor(|editor, window, cx| {
24749 editor.handle_input("done", window, cx);
24750 });
24751 cx.assert_editor_state(indoc! {"
24752 while read line; do
24753 echo \"$line\"
24754 doneˇ
24755 "});
24756
24757 // test `done` auto outdents when typed inside `for` block
24758 cx.set_state(indoc! {"
24759 for file in *.txt; do
24760 cat \"$file\"
24761 ˇ
24762 "});
24763 cx.update_editor(|editor, window, cx| {
24764 editor.handle_input("done", window, cx);
24765 });
24766 cx.assert_editor_state(indoc! {"
24767 for file in *.txt; do
24768 cat \"$file\"
24769 doneˇ
24770 "});
24771
24772 // test `esac` auto outdents when typed inside `case` block
24773 cx.set_state(indoc! {"
24774 case \"$1\" in
24775 start)
24776 echo \"foo bar\"
24777 ;;
24778 stop)
24779 echo \"bar baz\"
24780 ;;
24781 ˇ
24782 "});
24783 cx.update_editor(|editor, window, cx| {
24784 editor.handle_input("esac", window, cx);
24785 });
24786 cx.assert_editor_state(indoc! {"
24787 case \"$1\" in
24788 start)
24789 echo \"foo bar\"
24790 ;;
24791 stop)
24792 echo \"bar baz\"
24793 ;;
24794 esacˇ
24795 "});
24796
24797 // test `*)` auto outdents when typed inside `case` block
24798 cx.set_state(indoc! {"
24799 case \"$1\" in
24800 start)
24801 echo \"foo bar\"
24802 ;;
24803 ˇ
24804 "});
24805 cx.update_editor(|editor, window, cx| {
24806 editor.handle_input("*)", window, cx);
24807 });
24808 cx.assert_editor_state(indoc! {"
24809 case \"$1\" in
24810 start)
24811 echo \"foo bar\"
24812 ;;
24813 *)ˇ
24814 "});
24815
24816 // test `fi` outdents to correct level with nested if blocks
24817 cx.set_state(indoc! {"
24818 if [ \"$1\" = \"test\" ]; then
24819 echo \"outer if\"
24820 if [ \"$2\" = \"debug\" ]; then
24821 echo \"inner if\"
24822 ˇ
24823 "});
24824 cx.update_editor(|editor, window, cx| {
24825 editor.handle_input("fi", window, cx);
24826 });
24827 cx.assert_editor_state(indoc! {"
24828 if [ \"$1\" = \"test\" ]; then
24829 echo \"outer if\"
24830 if [ \"$2\" = \"debug\" ]; then
24831 echo \"inner if\"
24832 fiˇ
24833 "});
24834}
24835
24836#[gpui::test]
24837async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24838 init_test(cx, |_| {});
24839 update_test_language_settings(cx, |settings| {
24840 settings.defaults.extend_comment_on_newline = Some(false);
24841 });
24842 let mut cx = EditorTestContext::new(cx).await;
24843 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24844 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24845
24846 // test correct indent after newline on comment
24847 cx.set_state(indoc! {"
24848 # COMMENT:ˇ
24849 "});
24850 cx.update_editor(|editor, window, cx| {
24851 editor.newline(&Newline, window, cx);
24852 });
24853 cx.assert_editor_state(indoc! {"
24854 # COMMENT:
24855 ˇ
24856 "});
24857
24858 // test correct indent after newline after `then`
24859 cx.set_state(indoc! {"
24860
24861 if [ \"$1\" = \"test\" ]; thenˇ
24862 "});
24863 cx.update_editor(|editor, window, cx| {
24864 editor.newline(&Newline, window, cx);
24865 });
24866 cx.run_until_parked();
24867 cx.assert_editor_state(indoc! {"
24868
24869 if [ \"$1\" = \"test\" ]; then
24870 ˇ
24871 "});
24872
24873 // test correct indent after newline after `else`
24874 cx.set_state(indoc! {"
24875 if [ \"$1\" = \"test\" ]; then
24876 elseˇ
24877 "});
24878 cx.update_editor(|editor, window, cx| {
24879 editor.newline(&Newline, window, cx);
24880 });
24881 cx.run_until_parked();
24882 cx.assert_editor_state(indoc! {"
24883 if [ \"$1\" = \"test\" ]; then
24884 else
24885 ˇ
24886 "});
24887
24888 // test correct indent after newline after `elif`
24889 cx.set_state(indoc! {"
24890 if [ \"$1\" = \"test\" ]; then
24891 elifˇ
24892 "});
24893 cx.update_editor(|editor, window, cx| {
24894 editor.newline(&Newline, window, cx);
24895 });
24896 cx.run_until_parked();
24897 cx.assert_editor_state(indoc! {"
24898 if [ \"$1\" = \"test\" ]; then
24899 elif
24900 ˇ
24901 "});
24902
24903 // test correct indent after newline after `do`
24904 cx.set_state(indoc! {"
24905 for file in *.txt; doˇ
24906 "});
24907 cx.update_editor(|editor, window, cx| {
24908 editor.newline(&Newline, window, cx);
24909 });
24910 cx.run_until_parked();
24911 cx.assert_editor_state(indoc! {"
24912 for file in *.txt; do
24913 ˇ
24914 "});
24915
24916 // test correct indent after newline after case pattern
24917 cx.set_state(indoc! {"
24918 case \"$1\" in
24919 start)ˇ
24920 "});
24921 cx.update_editor(|editor, window, cx| {
24922 editor.newline(&Newline, window, cx);
24923 });
24924 cx.run_until_parked();
24925 cx.assert_editor_state(indoc! {"
24926 case \"$1\" in
24927 start)
24928 ˇ
24929 "});
24930
24931 // test correct indent after newline after case pattern
24932 cx.set_state(indoc! {"
24933 case \"$1\" in
24934 start)
24935 ;;
24936 *)ˇ
24937 "});
24938 cx.update_editor(|editor, window, cx| {
24939 editor.newline(&Newline, window, cx);
24940 });
24941 cx.run_until_parked();
24942 cx.assert_editor_state(indoc! {"
24943 case \"$1\" in
24944 start)
24945 ;;
24946 *)
24947 ˇ
24948 "});
24949
24950 // test correct indent after newline after function opening brace
24951 cx.set_state(indoc! {"
24952 function test() {ˇ}
24953 "});
24954 cx.update_editor(|editor, window, cx| {
24955 editor.newline(&Newline, window, cx);
24956 });
24957 cx.run_until_parked();
24958 cx.assert_editor_state(indoc! {"
24959 function test() {
24960 ˇ
24961 }
24962 "});
24963
24964 // test no extra indent after semicolon on same line
24965 cx.set_state(indoc! {"
24966 echo \"test\";ˇ
24967 "});
24968 cx.update_editor(|editor, window, cx| {
24969 editor.newline(&Newline, window, cx);
24970 });
24971 cx.run_until_parked();
24972 cx.assert_editor_state(indoc! {"
24973 echo \"test\";
24974 ˇ
24975 "});
24976}
24977
24978fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24979 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24980 point..point
24981}
24982
24983#[track_caller]
24984fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24985 let (text, ranges) = marked_text_ranges(marked_text, true);
24986 assert_eq!(editor.text(cx), text);
24987 assert_eq!(
24988 editor.selections.ranges(cx),
24989 ranges,
24990 "Assert selections are {}",
24991 marked_text
24992 );
24993}
24994
24995pub fn handle_signature_help_request(
24996 cx: &mut EditorLspTestContext,
24997 mocked_response: lsp::SignatureHelp,
24998) -> impl Future<Output = ()> + use<> {
24999 let mut request =
25000 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25001 let mocked_response = mocked_response.clone();
25002 async move { Ok(Some(mocked_response)) }
25003 });
25004
25005 async move {
25006 request.next().await;
25007 }
25008}
25009
25010#[track_caller]
25011pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25012 cx.update_editor(|editor, _, _| {
25013 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25014 let entries = menu.entries.borrow();
25015 let entries = entries
25016 .iter()
25017 .map(|entry| entry.string.as_str())
25018 .collect::<Vec<_>>();
25019 assert_eq!(entries, expected);
25020 } else {
25021 panic!("Expected completions menu");
25022 }
25023 });
25024}
25025
25026/// Handle completion request passing a marked string specifying where the completion
25027/// should be triggered from using '|' character, what range should be replaced, and what completions
25028/// should be returned using '<' and '>' to delimit the range.
25029///
25030/// Also see `handle_completion_request_with_insert_and_replace`.
25031#[track_caller]
25032pub fn handle_completion_request(
25033 marked_string: &str,
25034 completions: Vec<&'static str>,
25035 is_incomplete: bool,
25036 counter: Arc<AtomicUsize>,
25037 cx: &mut EditorLspTestContext,
25038) -> impl Future<Output = ()> {
25039 let complete_from_marker: TextRangeMarker = '|'.into();
25040 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25041 let (_, mut marked_ranges) = marked_text_ranges_by(
25042 marked_string,
25043 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25044 );
25045
25046 let complete_from_position =
25047 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25048 let replace_range =
25049 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25050
25051 let mut request =
25052 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25053 let completions = completions.clone();
25054 counter.fetch_add(1, atomic::Ordering::Release);
25055 async move {
25056 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25057 assert_eq!(
25058 params.text_document_position.position,
25059 complete_from_position
25060 );
25061 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25062 is_incomplete,
25063 item_defaults: None,
25064 items: completions
25065 .iter()
25066 .map(|completion_text| lsp::CompletionItem {
25067 label: completion_text.to_string(),
25068 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25069 range: replace_range,
25070 new_text: completion_text.to_string(),
25071 })),
25072 ..Default::default()
25073 })
25074 .collect(),
25075 })))
25076 }
25077 });
25078
25079 async move {
25080 request.next().await;
25081 }
25082}
25083
25084/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25085/// given instead, which also contains an `insert` range.
25086///
25087/// This function uses markers to define ranges:
25088/// - `|` marks the cursor position
25089/// - `<>` marks the replace range
25090/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25091pub fn handle_completion_request_with_insert_and_replace(
25092 cx: &mut EditorLspTestContext,
25093 marked_string: &str,
25094 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25095 counter: Arc<AtomicUsize>,
25096) -> impl Future<Output = ()> {
25097 let complete_from_marker: TextRangeMarker = '|'.into();
25098 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25099 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25100
25101 let (_, mut marked_ranges) = marked_text_ranges_by(
25102 marked_string,
25103 vec![
25104 complete_from_marker.clone(),
25105 replace_range_marker.clone(),
25106 insert_range_marker.clone(),
25107 ],
25108 );
25109
25110 let complete_from_position =
25111 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25112 let replace_range =
25113 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25114
25115 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25116 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25117 _ => lsp::Range {
25118 start: replace_range.start,
25119 end: complete_from_position,
25120 },
25121 };
25122
25123 let mut request =
25124 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25125 let completions = completions.clone();
25126 counter.fetch_add(1, atomic::Ordering::Release);
25127 async move {
25128 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25129 assert_eq!(
25130 params.text_document_position.position, complete_from_position,
25131 "marker `|` position doesn't match",
25132 );
25133 Ok(Some(lsp::CompletionResponse::Array(
25134 completions
25135 .iter()
25136 .map(|(label, new_text)| lsp::CompletionItem {
25137 label: label.to_string(),
25138 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25139 lsp::InsertReplaceEdit {
25140 insert: insert_range,
25141 replace: replace_range,
25142 new_text: new_text.to_string(),
25143 },
25144 )),
25145 ..Default::default()
25146 })
25147 .collect(),
25148 )))
25149 }
25150 });
25151
25152 async move {
25153 request.next().await;
25154 }
25155}
25156
25157fn handle_resolve_completion_request(
25158 cx: &mut EditorLspTestContext,
25159 edits: Option<Vec<(&'static str, &'static str)>>,
25160) -> impl Future<Output = ()> {
25161 let edits = edits.map(|edits| {
25162 edits
25163 .iter()
25164 .map(|(marked_string, new_text)| {
25165 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25166 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25167 lsp::TextEdit::new(replace_range, new_text.to_string())
25168 })
25169 .collect::<Vec<_>>()
25170 });
25171
25172 let mut request =
25173 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25174 let edits = edits.clone();
25175 async move {
25176 Ok(lsp::CompletionItem {
25177 additional_text_edits: edits,
25178 ..Default::default()
25179 })
25180 }
25181 });
25182
25183 async move {
25184 request.next().await;
25185 }
25186}
25187
25188pub(crate) fn update_test_language_settings(
25189 cx: &mut TestAppContext,
25190 f: impl Fn(&mut AllLanguageSettingsContent),
25191) {
25192 cx.update(|cx| {
25193 SettingsStore::update_global(cx, |store, cx| {
25194 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25195 });
25196 });
25197}
25198
25199pub(crate) fn update_test_project_settings(
25200 cx: &mut TestAppContext,
25201 f: impl Fn(&mut ProjectSettingsContent),
25202) {
25203 cx.update(|cx| {
25204 SettingsStore::update_global(cx, |store, cx| {
25205 store.update_user_settings(cx, |settings| f(&mut settings.project));
25206 });
25207 });
25208}
25209
25210pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25211 cx.update(|cx| {
25212 assets::Assets.load_test_fonts(cx);
25213 let store = SettingsStore::test(cx);
25214 cx.set_global(store);
25215 theme::init(theme::LoadThemes::JustBase, cx);
25216 release_channel::init(SemanticVersion::default(), cx);
25217 client::init_settings(cx);
25218 language::init(cx);
25219 Project::init_settings(cx);
25220 workspace::init_settings(cx);
25221 crate::init(cx);
25222 });
25223 zlog::init_test();
25224 update_test_language_settings(cx, f);
25225}
25226
25227#[track_caller]
25228fn assert_hunk_revert(
25229 not_reverted_text_with_selections: &str,
25230 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25231 expected_reverted_text_with_selections: &str,
25232 base_text: &str,
25233 cx: &mut EditorLspTestContext,
25234) {
25235 cx.set_state(not_reverted_text_with_selections);
25236 cx.set_head_text(base_text);
25237 cx.executor().run_until_parked();
25238
25239 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25240 let snapshot = editor.snapshot(window, cx);
25241 let reverted_hunk_statuses = snapshot
25242 .buffer_snapshot
25243 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25244 .map(|hunk| hunk.status().kind)
25245 .collect::<Vec<_>>();
25246
25247 editor.git_restore(&Default::default(), window, cx);
25248 reverted_hunk_statuses
25249 });
25250 cx.executor().run_until_parked();
25251 cx.assert_editor_state(expected_reverted_text_with_selections);
25252 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25253}
25254
25255#[gpui::test(iterations = 10)]
25256async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25257 init_test(cx, |_| {});
25258
25259 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25260 let counter = diagnostic_requests.clone();
25261
25262 let fs = FakeFs::new(cx.executor());
25263 fs.insert_tree(
25264 path!("/a"),
25265 json!({
25266 "first.rs": "fn main() { let a = 5; }",
25267 "second.rs": "// Test file",
25268 }),
25269 )
25270 .await;
25271
25272 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25273 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25274 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25275
25276 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25277 language_registry.add(rust_lang());
25278 let mut fake_servers = language_registry.register_fake_lsp(
25279 "Rust",
25280 FakeLspAdapter {
25281 capabilities: lsp::ServerCapabilities {
25282 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25283 lsp::DiagnosticOptions {
25284 identifier: None,
25285 inter_file_dependencies: true,
25286 workspace_diagnostics: true,
25287 work_done_progress_options: Default::default(),
25288 },
25289 )),
25290 ..Default::default()
25291 },
25292 ..Default::default()
25293 },
25294 );
25295
25296 let editor = workspace
25297 .update(cx, |workspace, window, cx| {
25298 workspace.open_abs_path(
25299 PathBuf::from(path!("/a/first.rs")),
25300 OpenOptions::default(),
25301 window,
25302 cx,
25303 )
25304 })
25305 .unwrap()
25306 .await
25307 .unwrap()
25308 .downcast::<Editor>()
25309 .unwrap();
25310 let fake_server = fake_servers.next().await.unwrap();
25311 let server_id = fake_server.server.server_id();
25312 let mut first_request = fake_server
25313 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25314 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25315 let result_id = Some(new_result_id.to_string());
25316 assert_eq!(
25317 params.text_document.uri,
25318 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25319 );
25320 async move {
25321 Ok(lsp::DocumentDiagnosticReportResult::Report(
25322 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25323 related_documents: None,
25324 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25325 items: Vec::new(),
25326 result_id,
25327 },
25328 }),
25329 ))
25330 }
25331 });
25332
25333 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25334 project.update(cx, |project, cx| {
25335 let buffer_id = editor
25336 .read(cx)
25337 .buffer()
25338 .read(cx)
25339 .as_singleton()
25340 .expect("created a singleton buffer")
25341 .read(cx)
25342 .remote_id();
25343 let buffer_result_id = project
25344 .lsp_store()
25345 .read(cx)
25346 .result_id(server_id, buffer_id, cx);
25347 assert_eq!(expected, buffer_result_id);
25348 });
25349 };
25350
25351 ensure_result_id(None, cx);
25352 cx.executor().advance_clock(Duration::from_millis(60));
25353 cx.executor().run_until_parked();
25354 assert_eq!(
25355 diagnostic_requests.load(atomic::Ordering::Acquire),
25356 1,
25357 "Opening file should trigger diagnostic request"
25358 );
25359 first_request
25360 .next()
25361 .await
25362 .expect("should have sent the first diagnostics pull request");
25363 ensure_result_id(Some("1".to_string()), cx);
25364
25365 // Editing should trigger diagnostics
25366 editor.update_in(cx, |editor, window, cx| {
25367 editor.handle_input("2", window, cx)
25368 });
25369 cx.executor().advance_clock(Duration::from_millis(60));
25370 cx.executor().run_until_parked();
25371 assert_eq!(
25372 diagnostic_requests.load(atomic::Ordering::Acquire),
25373 2,
25374 "Editing should trigger diagnostic request"
25375 );
25376 ensure_result_id(Some("2".to_string()), cx);
25377
25378 // Moving cursor should not trigger diagnostic request
25379 editor.update_in(cx, |editor, window, cx| {
25380 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25381 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25382 });
25383 });
25384 cx.executor().advance_clock(Duration::from_millis(60));
25385 cx.executor().run_until_parked();
25386 assert_eq!(
25387 diagnostic_requests.load(atomic::Ordering::Acquire),
25388 2,
25389 "Cursor movement should not trigger diagnostic request"
25390 );
25391 ensure_result_id(Some("2".to_string()), cx);
25392 // Multiple rapid edits should be debounced
25393 for _ in 0..5 {
25394 editor.update_in(cx, |editor, window, cx| {
25395 editor.handle_input("x", window, cx)
25396 });
25397 }
25398 cx.executor().advance_clock(Duration::from_millis(60));
25399 cx.executor().run_until_parked();
25400
25401 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25402 assert!(
25403 final_requests <= 4,
25404 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25405 );
25406 ensure_result_id(Some(final_requests.to_string()), cx);
25407}
25408
25409#[gpui::test]
25410async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25411 // Regression test for issue #11671
25412 // Previously, adding a cursor after moving multiple cursors would reset
25413 // the cursor count instead of adding to the existing cursors.
25414 init_test(cx, |_| {});
25415 let mut cx = EditorTestContext::new(cx).await;
25416
25417 // Create a simple buffer with cursor at start
25418 cx.set_state(indoc! {"
25419 ˇaaaa
25420 bbbb
25421 cccc
25422 dddd
25423 eeee
25424 ffff
25425 gggg
25426 hhhh"});
25427
25428 // Add 2 cursors below (so we have 3 total)
25429 cx.update_editor(|editor, window, cx| {
25430 editor.add_selection_below(&Default::default(), window, cx);
25431 editor.add_selection_below(&Default::default(), window, cx);
25432 });
25433
25434 // Verify we have 3 cursors
25435 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25436 assert_eq!(
25437 initial_count, 3,
25438 "Should have 3 cursors after adding 2 below"
25439 );
25440
25441 // Move down one line
25442 cx.update_editor(|editor, window, cx| {
25443 editor.move_down(&MoveDown, window, cx);
25444 });
25445
25446 // Add another cursor below
25447 cx.update_editor(|editor, window, cx| {
25448 editor.add_selection_below(&Default::default(), window, cx);
25449 });
25450
25451 // Should now have 4 cursors (3 original + 1 new)
25452 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25453 assert_eq!(
25454 final_count, 4,
25455 "Should have 4 cursors after moving and adding another"
25456 );
25457}
25458
25459#[gpui::test(iterations = 10)]
25460async fn test_document_colors(cx: &mut TestAppContext) {
25461 let expected_color = Rgba {
25462 r: 0.33,
25463 g: 0.33,
25464 b: 0.33,
25465 a: 0.33,
25466 };
25467
25468 init_test(cx, |_| {});
25469
25470 let fs = FakeFs::new(cx.executor());
25471 fs.insert_tree(
25472 path!("/a"),
25473 json!({
25474 "first.rs": "fn main() { let a = 5; }",
25475 }),
25476 )
25477 .await;
25478
25479 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25480 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25481 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25482
25483 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25484 language_registry.add(rust_lang());
25485 let mut fake_servers = language_registry.register_fake_lsp(
25486 "Rust",
25487 FakeLspAdapter {
25488 capabilities: lsp::ServerCapabilities {
25489 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25490 ..lsp::ServerCapabilities::default()
25491 },
25492 name: "rust-analyzer",
25493 ..FakeLspAdapter::default()
25494 },
25495 );
25496 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25497 "Rust",
25498 FakeLspAdapter {
25499 capabilities: lsp::ServerCapabilities {
25500 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25501 ..lsp::ServerCapabilities::default()
25502 },
25503 name: "not-rust-analyzer",
25504 ..FakeLspAdapter::default()
25505 },
25506 );
25507
25508 let editor = workspace
25509 .update(cx, |workspace, window, cx| {
25510 workspace.open_abs_path(
25511 PathBuf::from(path!("/a/first.rs")),
25512 OpenOptions::default(),
25513 window,
25514 cx,
25515 )
25516 })
25517 .unwrap()
25518 .await
25519 .unwrap()
25520 .downcast::<Editor>()
25521 .unwrap();
25522 let fake_language_server = fake_servers.next().await.unwrap();
25523 let fake_language_server_without_capabilities =
25524 fake_servers_without_capabilities.next().await.unwrap();
25525 let requests_made = Arc::new(AtomicUsize::new(0));
25526 let closure_requests_made = Arc::clone(&requests_made);
25527 let mut color_request_handle = fake_language_server
25528 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25529 let requests_made = Arc::clone(&closure_requests_made);
25530 async move {
25531 assert_eq!(
25532 params.text_document.uri,
25533 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25534 );
25535 requests_made.fetch_add(1, atomic::Ordering::Release);
25536 Ok(vec![
25537 lsp::ColorInformation {
25538 range: lsp::Range {
25539 start: lsp::Position {
25540 line: 0,
25541 character: 0,
25542 },
25543 end: lsp::Position {
25544 line: 0,
25545 character: 1,
25546 },
25547 },
25548 color: lsp::Color {
25549 red: 0.33,
25550 green: 0.33,
25551 blue: 0.33,
25552 alpha: 0.33,
25553 },
25554 },
25555 lsp::ColorInformation {
25556 range: lsp::Range {
25557 start: lsp::Position {
25558 line: 0,
25559 character: 0,
25560 },
25561 end: lsp::Position {
25562 line: 0,
25563 character: 1,
25564 },
25565 },
25566 color: lsp::Color {
25567 red: 0.33,
25568 green: 0.33,
25569 blue: 0.33,
25570 alpha: 0.33,
25571 },
25572 },
25573 ])
25574 }
25575 });
25576
25577 let _handle = fake_language_server_without_capabilities
25578 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25579 panic!("Should not be called");
25580 });
25581 cx.executor().advance_clock(Duration::from_millis(100));
25582 color_request_handle.next().await.unwrap();
25583 cx.run_until_parked();
25584 assert_eq!(
25585 1,
25586 requests_made.load(atomic::Ordering::Acquire),
25587 "Should query for colors once per editor open"
25588 );
25589 editor.update_in(cx, |editor, _, cx| {
25590 assert_eq!(
25591 vec![expected_color],
25592 extract_color_inlays(editor, cx),
25593 "Should have an initial inlay"
25594 );
25595 });
25596
25597 // opening another file in a split should not influence the LSP query counter
25598 workspace
25599 .update(cx, |workspace, window, cx| {
25600 assert_eq!(
25601 workspace.panes().len(),
25602 1,
25603 "Should have one pane with one editor"
25604 );
25605 workspace.move_item_to_pane_in_direction(
25606 &MoveItemToPaneInDirection {
25607 direction: SplitDirection::Right,
25608 focus: false,
25609 clone: true,
25610 },
25611 window,
25612 cx,
25613 );
25614 })
25615 .unwrap();
25616 cx.run_until_parked();
25617 workspace
25618 .update(cx, |workspace, _, cx| {
25619 let panes = workspace.panes();
25620 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25621 for pane in panes {
25622 let editor = pane
25623 .read(cx)
25624 .active_item()
25625 .and_then(|item| item.downcast::<Editor>())
25626 .expect("Should have opened an editor in each split");
25627 let editor_file = editor
25628 .read(cx)
25629 .buffer()
25630 .read(cx)
25631 .as_singleton()
25632 .expect("test deals with singleton buffers")
25633 .read(cx)
25634 .file()
25635 .expect("test buffese should have a file")
25636 .path();
25637 assert_eq!(
25638 editor_file.as_ref(),
25639 Path::new("first.rs"),
25640 "Both editors should be opened for the same file"
25641 )
25642 }
25643 })
25644 .unwrap();
25645
25646 cx.executor().advance_clock(Duration::from_millis(500));
25647 let save = editor.update_in(cx, |editor, window, cx| {
25648 editor.move_to_end(&MoveToEnd, window, cx);
25649 editor.handle_input("dirty", window, cx);
25650 editor.save(
25651 SaveOptions {
25652 format: true,
25653 autosave: true,
25654 },
25655 project.clone(),
25656 window,
25657 cx,
25658 )
25659 });
25660 save.await.unwrap();
25661
25662 color_request_handle.next().await.unwrap();
25663 cx.run_until_parked();
25664 assert_eq!(
25665 3,
25666 requests_made.load(atomic::Ordering::Acquire),
25667 "Should query for colors once per save and once per formatting after save"
25668 );
25669
25670 drop(editor);
25671 let close = workspace
25672 .update(cx, |workspace, window, cx| {
25673 workspace.active_pane().update(cx, |pane, cx| {
25674 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25675 })
25676 })
25677 .unwrap();
25678 close.await.unwrap();
25679 let close = workspace
25680 .update(cx, |workspace, window, cx| {
25681 workspace.active_pane().update(cx, |pane, cx| {
25682 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25683 })
25684 })
25685 .unwrap();
25686 close.await.unwrap();
25687 assert_eq!(
25688 3,
25689 requests_made.load(atomic::Ordering::Acquire),
25690 "After saving and closing all editors, no extra requests should be made"
25691 );
25692 workspace
25693 .update(cx, |workspace, _, cx| {
25694 assert!(
25695 workspace.active_item(cx).is_none(),
25696 "Should close all editors"
25697 )
25698 })
25699 .unwrap();
25700
25701 workspace
25702 .update(cx, |workspace, window, cx| {
25703 workspace.active_pane().update(cx, |pane, cx| {
25704 pane.navigate_backward(&Default::default(), window, cx);
25705 })
25706 })
25707 .unwrap();
25708 cx.executor().advance_clock(Duration::from_millis(100));
25709 cx.run_until_parked();
25710 let editor = workspace
25711 .update(cx, |workspace, _, cx| {
25712 workspace
25713 .active_item(cx)
25714 .expect("Should have reopened the editor again after navigating back")
25715 .downcast::<Editor>()
25716 .expect("Should be an editor")
25717 })
25718 .unwrap();
25719 color_request_handle.next().await.unwrap();
25720 assert_eq!(
25721 3,
25722 requests_made.load(atomic::Ordering::Acquire),
25723 "Cache should be reused on buffer close and reopen"
25724 );
25725 editor.update(cx, |editor, cx| {
25726 assert_eq!(
25727 vec![expected_color],
25728 extract_color_inlays(editor, cx),
25729 "Should have an initial inlay"
25730 );
25731 });
25732}
25733
25734#[gpui::test]
25735async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25736 init_test(cx, |_| {});
25737 let (editor, cx) = cx.add_window_view(Editor::single_line);
25738 editor.update_in(cx, |editor, window, cx| {
25739 editor.set_text("oops\n\nwow\n", window, cx)
25740 });
25741 cx.run_until_parked();
25742 editor.update(cx, |editor, cx| {
25743 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25744 });
25745 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25746 cx.run_until_parked();
25747 editor.update(cx, |editor, cx| {
25748 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25749 });
25750}
25751
25752#[gpui::test]
25753async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25754 init_test(cx, |_| {});
25755
25756 cx.update(|cx| {
25757 register_project_item::<Editor>(cx);
25758 });
25759
25760 let fs = FakeFs::new(cx.executor());
25761 fs.insert_tree("/root1", json!({})).await;
25762 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25763 .await;
25764
25765 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25766 let (workspace, cx) =
25767 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25768
25769 let worktree_id = project.update(cx, |project, cx| {
25770 project.worktrees(cx).next().unwrap().read(cx).id()
25771 });
25772
25773 let handle = workspace
25774 .update_in(cx, |workspace, window, cx| {
25775 let project_path = (worktree_id, "one.pdf");
25776 workspace.open_path(project_path, None, true, window, cx)
25777 })
25778 .await
25779 .unwrap();
25780
25781 assert_eq!(
25782 handle.to_any().entity_type(),
25783 TypeId::of::<InvalidBufferView>()
25784 );
25785}
25786
25787#[gpui::test]
25788async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25789 init_test(cx, |_| {});
25790
25791 let language = Arc::new(Language::new(
25792 LanguageConfig::default(),
25793 Some(tree_sitter_rust::LANGUAGE.into()),
25794 ));
25795
25796 // Test hierarchical sibling navigation
25797 let text = r#"
25798 fn outer() {
25799 if condition {
25800 let a = 1;
25801 }
25802 let b = 2;
25803 }
25804
25805 fn another() {
25806 let c = 3;
25807 }
25808 "#;
25809
25810 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25811 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25812 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25813
25814 // Wait for parsing to complete
25815 editor
25816 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25817 .await;
25818
25819 editor.update_in(cx, |editor, window, cx| {
25820 // Start by selecting "let a = 1;" inside the if block
25821 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25822 s.select_display_ranges([
25823 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25824 ]);
25825 });
25826
25827 let initial_selection = editor.selections.display_ranges(cx);
25828 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25829
25830 // Test select next sibling - should move up levels to find the next sibling
25831 // Since "let a = 1;" has no siblings in the if block, it should move up
25832 // to find "let b = 2;" which is a sibling of the if block
25833 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25834 let next_selection = editor.selections.display_ranges(cx);
25835
25836 // Should have a selection and it should be different from the initial
25837 assert_eq!(
25838 next_selection.len(),
25839 1,
25840 "Should have one selection after next"
25841 );
25842 assert_ne!(
25843 next_selection[0], initial_selection[0],
25844 "Next sibling selection should be different"
25845 );
25846
25847 // Test hierarchical navigation by going to the end of the current function
25848 // and trying to navigate to the next function
25849 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25850 s.select_display_ranges([
25851 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25852 ]);
25853 });
25854
25855 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25856 let function_next_selection = editor.selections.display_ranges(cx);
25857
25858 // Should move to the next function
25859 assert_eq!(
25860 function_next_selection.len(),
25861 1,
25862 "Should have one selection after function next"
25863 );
25864
25865 // Test select previous sibling navigation
25866 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25867 let prev_selection = editor.selections.display_ranges(cx);
25868
25869 // Should have a selection and it should be different
25870 assert_eq!(
25871 prev_selection.len(),
25872 1,
25873 "Should have one selection after prev"
25874 );
25875 assert_ne!(
25876 prev_selection[0], function_next_selection[0],
25877 "Previous sibling selection should be different from next"
25878 );
25879 });
25880}
25881
25882#[gpui::test]
25883async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25884 init_test(cx, |_| {});
25885
25886 let mut cx = EditorTestContext::new(cx).await;
25887 cx.set_state(
25888 "let ˇvariable = 42;
25889let another = variable + 1;
25890let result = variable * 2;",
25891 );
25892
25893 // Set up document highlights manually (simulating LSP response)
25894 cx.update_editor(|editor, _window, cx| {
25895 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25896
25897 // Create highlights for "variable" occurrences
25898 let highlight_ranges = [
25899 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25900 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25901 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25902 ];
25903
25904 let anchor_ranges: Vec<_> = highlight_ranges
25905 .iter()
25906 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25907 .collect();
25908
25909 editor.highlight_background::<DocumentHighlightRead>(
25910 &anchor_ranges,
25911 |theme| theme.colors().editor_document_highlight_read_background,
25912 cx,
25913 );
25914 });
25915
25916 // Go to next highlight - should move to second "variable"
25917 cx.update_editor(|editor, window, cx| {
25918 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25919 });
25920 cx.assert_editor_state(
25921 "let variable = 42;
25922let another = ˇvariable + 1;
25923let result = variable * 2;",
25924 );
25925
25926 // Go to next highlight - should move to third "variable"
25927 cx.update_editor(|editor, window, cx| {
25928 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25929 });
25930 cx.assert_editor_state(
25931 "let variable = 42;
25932let another = variable + 1;
25933let result = ˇvariable * 2;",
25934 );
25935
25936 // Go to next highlight - should stay at third "variable" (no wrap-around)
25937 cx.update_editor(|editor, window, cx| {
25938 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25939 });
25940 cx.assert_editor_state(
25941 "let variable = 42;
25942let another = variable + 1;
25943let result = ˇvariable * 2;",
25944 );
25945
25946 // Now test going backwards from third position
25947 cx.update_editor(|editor, window, cx| {
25948 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25949 });
25950 cx.assert_editor_state(
25951 "let variable = 42;
25952let another = ˇvariable + 1;
25953let result = variable * 2;",
25954 );
25955
25956 // Go to previous highlight - should move to first "variable"
25957 cx.update_editor(|editor, window, cx| {
25958 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25959 });
25960 cx.assert_editor_state(
25961 "let ˇvariable = 42;
25962let another = variable + 1;
25963let result = variable * 2;",
25964 );
25965
25966 // Go to previous highlight - should stay on first "variable"
25967 cx.update_editor(|editor, window, cx| {
25968 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25969 });
25970 cx.assert_editor_state(
25971 "let ˇvariable = 42;
25972let another = variable + 1;
25973let result = variable * 2;",
25974 );
25975}
25976
25977#[track_caller]
25978fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25979 editor
25980 .all_inlays(cx)
25981 .into_iter()
25982 .filter_map(|inlay| inlay.get_color())
25983 .map(Rgba::from)
25984 .collect()
25985}