1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 invalid_buffer_view::InvalidBufferView,
61 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
62 register_project_item,
63};
64
65#[gpui::test]
66fn test_edit_events(cx: &mut TestAppContext) {
67 init_test(cx, |_| {});
68
69 let buffer = cx.new(|cx| {
70 let mut buffer = language::Buffer::local("123456", cx);
71 buffer.set_group_interval(Duration::from_secs(1));
72 buffer
73 });
74
75 let events = Rc::new(RefCell::new(Vec::new()));
76 let editor1 = cx.add_window({
77 let events = events.clone();
78 |window, cx| {
79 let entity = cx.entity();
80 cx.subscribe_in(
81 &entity,
82 window,
83 move |_, _, event: &EditorEvent, _, _| match event {
84 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
85 EditorEvent::BufferEdited => {
86 events.borrow_mut().push(("editor1", "buffer edited"))
87 }
88 _ => {}
89 },
90 )
91 .detach();
92 Editor::for_buffer(buffer.clone(), None, window, cx)
93 }
94 });
95
96 let editor2 = cx.add_window({
97 let events = events.clone();
98 |window, cx| {
99 cx.subscribe_in(
100 &cx.entity(),
101 window,
102 move |_, _, event: &EditorEvent, _, _| match event {
103 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
104 EditorEvent::BufferEdited => {
105 events.borrow_mut().push(("editor2", "buffer edited"))
106 }
107 _ => {}
108 },
109 )
110 .detach();
111 Editor::for_buffer(buffer.clone(), None, window, cx)
112 }
113 });
114
115 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
116
117 // Mutating editor 1 will emit an `Edited` event only for that editor.
118 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
119 assert_eq!(
120 mem::take(&mut *events.borrow_mut()),
121 [
122 ("editor1", "edited"),
123 ("editor1", "buffer edited"),
124 ("editor2", "buffer edited"),
125 ]
126 );
127
128 // Mutating editor 2 will emit an `Edited` event only for that editor.
129 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
130 assert_eq!(
131 mem::take(&mut *events.borrow_mut()),
132 [
133 ("editor2", "edited"),
134 ("editor1", "buffer edited"),
135 ("editor2", "buffer edited"),
136 ]
137 );
138
139 // Undoing on editor 1 will emit an `Edited` event only for that editor.
140 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
141 assert_eq!(
142 mem::take(&mut *events.borrow_mut()),
143 [
144 ("editor1", "edited"),
145 ("editor1", "buffer edited"),
146 ("editor2", "buffer edited"),
147 ]
148 );
149
150 // Redoing on editor 1 will emit an `Edited` event only for that editor.
151 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
152 assert_eq!(
153 mem::take(&mut *events.borrow_mut()),
154 [
155 ("editor1", "edited"),
156 ("editor1", "buffer edited"),
157 ("editor2", "buffer edited"),
158 ]
159 );
160
161 // Undoing on editor 2 will emit an `Edited` event only for that editor.
162 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
163 assert_eq!(
164 mem::take(&mut *events.borrow_mut()),
165 [
166 ("editor2", "edited"),
167 ("editor1", "buffer edited"),
168 ("editor2", "buffer edited"),
169 ]
170 );
171
172 // Redoing on editor 2 will emit an `Edited` event only for that editor.
173 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
174 assert_eq!(
175 mem::take(&mut *events.borrow_mut()),
176 [
177 ("editor2", "edited"),
178 ("editor1", "buffer edited"),
179 ("editor2", "buffer edited"),
180 ]
181 );
182
183 // No event is emitted when the mutation is a no-op.
184 _ = editor2.update(cx, |editor, window, cx| {
185 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
186 s.select_ranges([0..0])
187 });
188
189 editor.backspace(&Backspace, window, cx);
190 });
191 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
192}
193
194#[gpui::test]
195fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
196 init_test(cx, |_| {});
197
198 let mut now = Instant::now();
199 let group_interval = Duration::from_millis(1);
200 let buffer = cx.new(|cx| {
201 let mut buf = language::Buffer::local("123456", cx);
202 buf.set_group_interval(group_interval);
203 buf
204 });
205 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
206 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
207
208 _ = editor.update(cx, |editor, window, cx| {
209 editor.start_transaction_at(now, window, cx);
210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
211 s.select_ranges([2..4])
212 });
213
214 editor.insert("cd", window, cx);
215 editor.end_transaction_at(now, cx);
216 assert_eq!(editor.text(cx), "12cd56");
217 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
218
219 editor.start_transaction_at(now, window, cx);
220 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
221 s.select_ranges([4..5])
222 });
223 editor.insert("e", window, cx);
224 editor.end_transaction_at(now, cx);
225 assert_eq!(editor.text(cx), "12cde6");
226 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
227
228 now += group_interval + Duration::from_millis(1);
229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
230 s.select_ranges([2..2])
231 });
232
233 // Simulate an edit in another editor
234 buffer.update(cx, |buffer, cx| {
235 buffer.start_transaction_at(now, cx);
236 buffer.edit([(0..1, "a")], None, cx);
237 buffer.edit([(1..1, "b")], None, cx);
238 buffer.end_transaction_at(now, cx);
239 });
240
241 assert_eq!(editor.text(cx), "ab2cde6");
242 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
243
244 // Last transaction happened past the group interval in a different editor.
245 // Undo it individually and don't restore selections.
246 editor.undo(&Undo, window, cx);
247 assert_eq!(editor.text(cx), "12cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
249
250 // First two transactions happened within the group interval in this editor.
251 // Undo them together and restore selections.
252 editor.undo(&Undo, window, cx);
253 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
254 assert_eq!(editor.text(cx), "123456");
255 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
256
257 // Redo the first two transactions together.
258 editor.redo(&Redo, window, cx);
259 assert_eq!(editor.text(cx), "12cde6");
260 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
261
262 // Redo the last transaction on its own.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "ab2cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
266
267 // Test empty transactions.
268 editor.start_transaction_at(now, window, cx);
269 editor.end_transaction_at(now, cx);
270 editor.undo(&Undo, window, cx);
271 assert_eq!(editor.text(cx), "12cde6");
272 });
273}
274
275#[gpui::test]
276fn test_ime_composition(cx: &mut TestAppContext) {
277 init_test(cx, |_| {});
278
279 let buffer = cx.new(|cx| {
280 let mut buffer = language::Buffer::local("abcde", cx);
281 // Ensure automatic grouping doesn't occur.
282 buffer.set_group_interval(Duration::ZERO);
283 buffer
284 });
285
286 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
287 cx.add_window(|window, cx| {
288 let mut editor = build_editor(buffer.clone(), window, cx);
289
290 // Start a new IME composition.
291 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
292 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
293 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
294 assert_eq!(editor.text(cx), "äbcde");
295 assert_eq!(
296 editor.marked_text_ranges(cx),
297 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
298 );
299
300 // Finalize IME composition.
301 editor.replace_text_in_range(None, "ā", window, cx);
302 assert_eq!(editor.text(cx), "ābcde");
303 assert_eq!(editor.marked_text_ranges(cx), None);
304
305 // IME composition edits are grouped and are undone/redone at once.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "abcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309 editor.redo(&Default::default(), window, cx);
310 assert_eq!(editor.text(cx), "ābcde");
311 assert_eq!(editor.marked_text_ranges(cx), None);
312
313 // Start a new IME composition.
314 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
315 assert_eq!(
316 editor.marked_text_ranges(cx),
317 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
318 );
319
320 // Undoing during an IME composition cancels it.
321 editor.undo(&Default::default(), window, cx);
322 assert_eq!(editor.text(cx), "ābcde");
323 assert_eq!(editor.marked_text_ranges(cx), None);
324
325 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
326 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
327 assert_eq!(editor.text(cx), "ābcdè");
328 assert_eq!(
329 editor.marked_text_ranges(cx),
330 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
331 );
332
333 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
334 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
335 assert_eq!(editor.text(cx), "ābcdę");
336 assert_eq!(editor.marked_text_ranges(cx), None);
337
338 // Start a new IME composition with multiple cursors.
339 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
340 s.select_ranges([
341 OffsetUtf16(1)..OffsetUtf16(1),
342 OffsetUtf16(3)..OffsetUtf16(3),
343 OffsetUtf16(5)..OffsetUtf16(5),
344 ])
345 });
346 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
347 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
348 assert_eq!(
349 editor.marked_text_ranges(cx),
350 Some(vec![
351 OffsetUtf16(0)..OffsetUtf16(3),
352 OffsetUtf16(4)..OffsetUtf16(7),
353 OffsetUtf16(8)..OffsetUtf16(11)
354 ])
355 );
356
357 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
358 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
359 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
360 assert_eq!(
361 editor.marked_text_ranges(cx),
362 Some(vec![
363 OffsetUtf16(1)..OffsetUtf16(2),
364 OffsetUtf16(5)..OffsetUtf16(6),
365 OffsetUtf16(9)..OffsetUtf16(10)
366 ])
367 );
368
369 // Finalize IME composition with multiple cursors.
370 editor.replace_text_in_range(Some(9..10), "2", window, cx);
371 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
372 assert_eq!(editor.marked_text_ranges(cx), None);
373
374 editor
375 });
376}
377
378#[gpui::test]
379fn test_selection_with_mouse(cx: &mut TestAppContext) {
380 init_test(cx, |_| {});
381
382 let editor = cx.add_window(|window, cx| {
383 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
384 build_editor(buffer, window, cx)
385 });
386
387 _ = editor.update(cx, |editor, window, cx| {
388 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
389 });
390 assert_eq!(
391 editor
392 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
393 .unwrap(),
394 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
395 );
396
397 _ = editor.update(cx, |editor, window, cx| {
398 editor.update_selection(
399 DisplayPoint::new(DisplayRow(3), 3),
400 0,
401 gpui::Point::<f32>::default(),
402 window,
403 cx,
404 );
405 });
406
407 assert_eq!(
408 editor
409 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
410 .unwrap(),
411 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
412 );
413
414 _ = editor.update(cx, |editor, window, cx| {
415 editor.update_selection(
416 DisplayPoint::new(DisplayRow(1), 1),
417 0,
418 gpui::Point::<f32>::default(),
419 window,
420 cx,
421 );
422 });
423
424 assert_eq!(
425 editor
426 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
427 .unwrap(),
428 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
429 );
430
431 _ = editor.update(cx, |editor, window, cx| {
432 editor.end_selection(window, cx);
433 editor.update_selection(
434 DisplayPoint::new(DisplayRow(3), 3),
435 0,
436 gpui::Point::<f32>::default(),
437 window,
438 cx,
439 );
440 });
441
442 assert_eq!(
443 editor
444 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
445 .unwrap(),
446 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
447 );
448
449 _ = editor.update(cx, |editor, window, cx| {
450 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
451 editor.update_selection(
452 DisplayPoint::new(DisplayRow(0), 0),
453 0,
454 gpui::Point::<f32>::default(),
455 window,
456 cx,
457 );
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
463 .unwrap(),
464 [
465 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
466 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
467 ]
468 );
469
470 _ = editor.update(cx, |editor, window, cx| {
471 editor.end_selection(window, cx);
472 });
473
474 assert_eq!(
475 editor
476 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
477 .unwrap(),
478 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
479 );
480}
481
482#[gpui::test]
483fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
484 init_test(cx, |_| {});
485
486 let editor = cx.add_window(|window, cx| {
487 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
488 build_editor(buffer, window, cx)
489 });
490
491 _ = editor.update(cx, |editor, window, cx| {
492 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
493 });
494
495 _ = editor.update(cx, |editor, window, cx| {
496 editor.end_selection(window, cx);
497 });
498
499 _ = editor.update(cx, |editor, window, cx| {
500 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
501 });
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.end_selection(window, cx);
505 });
506
507 assert_eq!(
508 editor
509 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
510 .unwrap(),
511 [
512 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
513 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
514 ]
515 );
516
517 _ = editor.update(cx, |editor, window, cx| {
518 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
519 });
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.end_selection(window, cx);
523 });
524
525 assert_eq!(
526 editor
527 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
528 .unwrap(),
529 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
530 );
531}
532
533#[gpui::test]
534fn test_canceling_pending_selection(cx: &mut TestAppContext) {
535 init_test(cx, |_| {});
536
537 let editor = cx.add_window(|window, cx| {
538 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
539 build_editor(buffer, window, cx)
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
544 assert_eq!(
545 editor.selections.display_ranges(cx),
546 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
547 );
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(3), 3),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563
564 _ = editor.update(cx, |editor, window, cx| {
565 editor.cancel(&Cancel, window, cx);
566 editor.update_selection(
567 DisplayPoint::new(DisplayRow(1), 1),
568 0,
569 gpui::Point::<f32>::default(),
570 window,
571 cx,
572 );
573 assert_eq!(
574 editor.selections.display_ranges(cx),
575 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
576 );
577 });
578}
579
580#[gpui::test]
581fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
582 init_test(cx, |_| {});
583
584 let editor = cx.add_window(|window, cx| {
585 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
586 build_editor(buffer, window, cx)
587 });
588
589 _ = editor.update(cx, |editor, window, cx| {
590 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
591 assert_eq!(
592 editor.selections.display_ranges(cx),
593 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
594 );
595
596 editor.move_down(&Default::default(), window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
600 );
601
602 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
606 );
607
608 editor.move_up(&Default::default(), window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
612 );
613 });
614}
615
616#[gpui::test]
617fn test_clone(cx: &mut TestAppContext) {
618 init_test(cx, |_| {});
619
620 let (text, selection_ranges) = marked_text_ranges(
621 indoc! {"
622 one
623 two
624 threeˇ
625 four
626 fiveˇ
627 "},
628 true,
629 );
630
631 let editor = cx.add_window(|window, cx| {
632 let buffer = MultiBuffer::build_simple(&text, cx);
633 build_editor(buffer, window, cx)
634 });
635
636 _ = editor.update(cx, |editor, window, cx| {
637 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
638 s.select_ranges(selection_ranges.clone())
639 });
640 editor.fold_creases(
641 vec![
642 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
643 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
644 ],
645 true,
646 window,
647 cx,
648 );
649 });
650
651 let cloned_editor = editor
652 .update(cx, |editor, _, cx| {
653 cx.open_window(Default::default(), |window, cx| {
654 cx.new(|cx| editor.clone(window, cx))
655 })
656 })
657 .unwrap()
658 .unwrap();
659
660 let snapshot = editor
661 .update(cx, |e, window, cx| e.snapshot(window, cx))
662 .unwrap();
663 let cloned_snapshot = cloned_editor
664 .update(cx, |e, window, cx| e.snapshot(window, cx))
665 .unwrap();
666
667 assert_eq!(
668 cloned_editor
669 .update(cx, |e, _, cx| e.display_text(cx))
670 .unwrap(),
671 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
672 );
673 assert_eq!(
674 cloned_snapshot
675 .folds_in_range(0..text.len())
676 .collect::<Vec<_>>(),
677 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
678 );
679 assert_set_eq!(
680 cloned_editor
681 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
682 .unwrap(),
683 editor
684 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
685 .unwrap()
686 );
687 assert_set_eq!(
688 cloned_editor
689 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
690 .unwrap(),
691 editor
692 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
693 .unwrap()
694 );
695}
696
697#[gpui::test]
698async fn test_navigation_history(cx: &mut TestAppContext) {
699 init_test(cx, |_| {});
700
701 use workspace::item::Item;
702
703 let fs = FakeFs::new(cx.executor());
704 let project = Project::test(fs, [], cx).await;
705 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
706 let pane = workspace
707 .update(cx, |workspace, _, _| workspace.active_pane().clone())
708 .unwrap();
709
710 _ = workspace.update(cx, |_v, window, cx| {
711 cx.new(|cx| {
712 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
713 let mut editor = build_editor(buffer, window, cx);
714 let handle = cx.entity();
715 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
716
717 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
718 editor.nav_history.as_mut().unwrap().pop_backward(cx)
719 }
720
721 // Move the cursor a small distance.
722 // Nothing is added to the navigation history.
723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
724 s.select_display_ranges([
725 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
726 ])
727 });
728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
729 s.select_display_ranges([
730 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
731 ])
732 });
733 assert!(pop_history(&mut editor, cx).is_none());
734
735 // Move the cursor a large distance.
736 // The history can jump back to the previous position.
737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
738 s.select_display_ranges([
739 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
740 ])
741 });
742 let nav_entry = pop_history(&mut editor, cx).unwrap();
743 editor.navigate(nav_entry.data.unwrap(), window, cx);
744 assert_eq!(nav_entry.item.id(), cx.entity_id());
745 assert_eq!(
746 editor.selections.display_ranges(cx),
747 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
748 );
749 assert!(pop_history(&mut editor, cx).is_none());
750
751 // Move the cursor a small distance via the mouse.
752 // Nothing is added to the navigation history.
753 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
754 editor.end_selection(window, cx);
755 assert_eq!(
756 editor.selections.display_ranges(cx),
757 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
758 );
759 assert!(pop_history(&mut editor, cx).is_none());
760
761 // Move the cursor a large distance via the mouse.
762 // The history can jump back to the previous position.
763 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
764 editor.end_selection(window, cx);
765 assert_eq!(
766 editor.selections.display_ranges(cx),
767 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
768 );
769 let nav_entry = pop_history(&mut editor, cx).unwrap();
770 editor.navigate(nav_entry.data.unwrap(), window, cx);
771 assert_eq!(nav_entry.item.id(), cx.entity_id());
772 assert_eq!(
773 editor.selections.display_ranges(cx),
774 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
775 );
776 assert!(pop_history(&mut editor, cx).is_none());
777
778 // Set scroll position to check later
779 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
780 let original_scroll_position = editor.scroll_manager.anchor();
781
782 // Jump to the end of the document and adjust scroll
783 editor.move_to_end(&MoveToEnd, window, cx);
784 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
785 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
786
787 let nav_entry = pop_history(&mut editor, cx).unwrap();
788 editor.navigate(nav_entry.data.unwrap(), window, cx);
789 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
790
791 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
792 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
793 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
794 let invalid_point = Point::new(9999, 0);
795 editor.navigate(
796 Box::new(NavigationData {
797 cursor_anchor: invalid_anchor,
798 cursor_position: invalid_point,
799 scroll_anchor: ScrollAnchor {
800 anchor: invalid_anchor,
801 offset: Default::default(),
802 },
803 scroll_top_row: invalid_point.row,
804 }),
805 window,
806 cx,
807 );
808 assert_eq!(
809 editor.selections.display_ranges(cx),
810 &[editor.max_point(cx)..editor.max_point(cx)]
811 );
812 assert_eq!(
813 editor.scroll_position(cx),
814 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
815 );
816
817 editor
818 })
819 });
820}
821
822#[gpui::test]
823fn test_cancel(cx: &mut TestAppContext) {
824 init_test(cx, |_| {});
825
826 let editor = cx.add_window(|window, cx| {
827 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
828 build_editor(buffer, window, cx)
829 });
830
831 _ = editor.update(cx, |editor, window, cx| {
832 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
833 editor.update_selection(
834 DisplayPoint::new(DisplayRow(1), 1),
835 0,
836 gpui::Point::<f32>::default(),
837 window,
838 cx,
839 );
840 editor.end_selection(window, cx);
841
842 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
843 editor.update_selection(
844 DisplayPoint::new(DisplayRow(0), 3),
845 0,
846 gpui::Point::<f32>::default(),
847 window,
848 cx,
849 );
850 editor.end_selection(window, cx);
851 assert_eq!(
852 editor.selections.display_ranges(cx),
853 [
854 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
855 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
856 ]
857 );
858 });
859
860 _ = editor.update(cx, |editor, window, cx| {
861 editor.cancel(&Cancel, window, cx);
862 assert_eq!(
863 editor.selections.display_ranges(cx),
864 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
865 );
866 });
867
868 _ = editor.update(cx, |editor, window, cx| {
869 editor.cancel(&Cancel, window, cx);
870 assert_eq!(
871 editor.selections.display_ranges(cx),
872 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
873 );
874 });
875}
876
877#[gpui::test]
878fn test_fold_action(cx: &mut TestAppContext) {
879 init_test(cx, |_| {});
880
881 let editor = cx.add_window(|window, cx| {
882 let buffer = MultiBuffer::build_simple(
883 &"
884 impl Foo {
885 // Hello!
886
887 fn a() {
888 1
889 }
890
891 fn b() {
892 2
893 }
894
895 fn c() {
896 3
897 }
898 }
899 "
900 .unindent(),
901 cx,
902 );
903 build_editor(buffer, window, cx)
904 });
905
906 _ = editor.update(cx, |editor, window, cx| {
907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
908 s.select_display_ranges([
909 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
910 ]);
911 });
912 editor.fold(&Fold, window, cx);
913 assert_eq!(
914 editor.display_text(cx),
915 "
916 impl Foo {
917 // Hello!
918
919 fn a() {
920 1
921 }
922
923 fn b() {⋯
924 }
925
926 fn c() {⋯
927 }
928 }
929 "
930 .unindent(),
931 );
932
933 editor.fold(&Fold, window, cx);
934 assert_eq!(
935 editor.display_text(cx),
936 "
937 impl Foo {⋯
938 }
939 "
940 .unindent(),
941 );
942
943 editor.unfold_lines(&UnfoldLines, window, cx);
944 assert_eq!(
945 editor.display_text(cx),
946 "
947 impl Foo {
948 // Hello!
949
950 fn a() {
951 1
952 }
953
954 fn b() {⋯
955 }
956
957 fn c() {⋯
958 }
959 }
960 "
961 .unindent(),
962 );
963
964 editor.unfold_lines(&UnfoldLines, window, cx);
965 assert_eq!(
966 editor.display_text(cx),
967 editor.buffer.read(cx).read(cx).text()
968 );
969 });
970}
971
972#[gpui::test]
973fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
974 init_test(cx, |_| {});
975
976 let editor = cx.add_window(|window, cx| {
977 let buffer = MultiBuffer::build_simple(
978 &"
979 class Foo:
980 # Hello!
981
982 def a():
983 print(1)
984
985 def b():
986 print(2)
987
988 def c():
989 print(3)
990 "
991 .unindent(),
992 cx,
993 );
994 build_editor(buffer, window, cx)
995 });
996
997 _ = editor.update(cx, |editor, window, cx| {
998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
999 s.select_display_ranges([
1000 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1001 ]);
1002 });
1003 editor.fold(&Fold, window, cx);
1004 assert_eq!(
1005 editor.display_text(cx),
1006 "
1007 class Foo:
1008 # Hello!
1009
1010 def a():
1011 print(1)
1012
1013 def b():⋯
1014
1015 def c():⋯
1016 "
1017 .unindent(),
1018 );
1019
1020 editor.fold(&Fold, window, cx);
1021 assert_eq!(
1022 editor.display_text(cx),
1023 "
1024 class Foo:⋯
1025 "
1026 .unindent(),
1027 );
1028
1029 editor.unfold_lines(&UnfoldLines, window, cx);
1030 assert_eq!(
1031 editor.display_text(cx),
1032 "
1033 class Foo:
1034 # Hello!
1035
1036 def a():
1037 print(1)
1038
1039 def b():⋯
1040
1041 def c():⋯
1042 "
1043 .unindent(),
1044 );
1045
1046 editor.unfold_lines(&UnfoldLines, window, cx);
1047 assert_eq!(
1048 editor.display_text(cx),
1049 editor.buffer.read(cx).read(cx).text()
1050 );
1051 });
1052}
1053
1054#[gpui::test]
1055fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1056 init_test(cx, |_| {});
1057
1058 let editor = cx.add_window(|window, cx| {
1059 let buffer = MultiBuffer::build_simple(
1060 &"
1061 class Foo:
1062 # Hello!
1063
1064 def a():
1065 print(1)
1066
1067 def b():
1068 print(2)
1069
1070
1071 def c():
1072 print(3)
1073
1074
1075 "
1076 .unindent(),
1077 cx,
1078 );
1079 build_editor(buffer, window, cx)
1080 });
1081
1082 _ = editor.update(cx, |editor, window, cx| {
1083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1084 s.select_display_ranges([
1085 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1086 ]);
1087 });
1088 editor.fold(&Fold, window, cx);
1089 assert_eq!(
1090 editor.display_text(cx),
1091 "
1092 class Foo:
1093 # Hello!
1094
1095 def a():
1096 print(1)
1097
1098 def b():⋯
1099
1100
1101 def c():⋯
1102
1103
1104 "
1105 .unindent(),
1106 );
1107
1108 editor.fold(&Fold, window, cx);
1109 assert_eq!(
1110 editor.display_text(cx),
1111 "
1112 class Foo:⋯
1113
1114
1115 "
1116 .unindent(),
1117 );
1118
1119 editor.unfold_lines(&UnfoldLines, window, cx);
1120 assert_eq!(
1121 editor.display_text(cx),
1122 "
1123 class Foo:
1124 # Hello!
1125
1126 def a():
1127 print(1)
1128
1129 def b():⋯
1130
1131
1132 def c():⋯
1133
1134
1135 "
1136 .unindent(),
1137 );
1138
1139 editor.unfold_lines(&UnfoldLines, window, cx);
1140 assert_eq!(
1141 editor.display_text(cx),
1142 editor.buffer.read(cx).read(cx).text()
1143 );
1144 });
1145}
1146
1147#[gpui::test]
1148fn test_fold_at_level(cx: &mut TestAppContext) {
1149 init_test(cx, |_| {});
1150
1151 let editor = cx.add_window(|window, cx| {
1152 let buffer = MultiBuffer::build_simple(
1153 &"
1154 class Foo:
1155 # Hello!
1156
1157 def a():
1158 print(1)
1159
1160 def b():
1161 print(2)
1162
1163
1164 class Bar:
1165 # World!
1166
1167 def a():
1168 print(1)
1169
1170 def b():
1171 print(2)
1172
1173
1174 "
1175 .unindent(),
1176 cx,
1177 );
1178 build_editor(buffer, window, cx)
1179 });
1180
1181 _ = editor.update(cx, |editor, window, cx| {
1182 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1183 assert_eq!(
1184 editor.display_text(cx),
1185 "
1186 class Foo:
1187 # Hello!
1188
1189 def a():⋯
1190
1191 def b():⋯
1192
1193
1194 class Bar:
1195 # World!
1196
1197 def a():⋯
1198
1199 def b():⋯
1200
1201
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:⋯
1211
1212
1213 class Bar:⋯
1214
1215
1216 "
1217 .unindent(),
1218 );
1219
1220 editor.unfold_all(&UnfoldAll, window, cx);
1221 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1222 assert_eq!(
1223 editor.display_text(cx),
1224 "
1225 class Foo:
1226 # Hello!
1227
1228 def a():
1229 print(1)
1230
1231 def b():
1232 print(2)
1233
1234
1235 class Bar:
1236 # World!
1237
1238 def a():
1239 print(1)
1240
1241 def b():
1242 print(2)
1243
1244
1245 "
1246 .unindent(),
1247 );
1248
1249 assert_eq!(
1250 editor.display_text(cx),
1251 editor.buffer.read(cx).read(cx).text()
1252 );
1253 });
1254}
1255
1256#[gpui::test]
1257fn test_move_cursor(cx: &mut TestAppContext) {
1258 init_test(cx, |_| {});
1259
1260 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1261 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1262
1263 buffer.update(cx, |buffer, cx| {
1264 buffer.edit(
1265 vec![
1266 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1267 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1268 ],
1269 None,
1270 cx,
1271 );
1272 });
1273 _ = editor.update(cx, |editor, window, cx| {
1274 assert_eq!(
1275 editor.selections.display_ranges(cx),
1276 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1277 );
1278
1279 editor.move_down(&MoveDown, window, cx);
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1283 );
1284
1285 editor.move_right(&MoveRight, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1289 );
1290
1291 editor.move_left(&MoveLeft, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1295 );
1296
1297 editor.move_up(&MoveUp, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1301 );
1302
1303 editor.move_to_end(&MoveToEnd, window, cx);
1304 assert_eq!(
1305 editor.selections.display_ranges(cx),
1306 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1307 );
1308
1309 editor.move_to_beginning(&MoveToBeginning, window, cx);
1310 assert_eq!(
1311 editor.selections.display_ranges(cx),
1312 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1313 );
1314
1315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1316 s.select_display_ranges([
1317 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1318 ]);
1319 });
1320 editor.select_to_beginning(&SelectToBeginning, window, cx);
1321 assert_eq!(
1322 editor.selections.display_ranges(cx),
1323 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1324 );
1325
1326 editor.select_to_end(&SelectToEnd, window, cx);
1327 assert_eq!(
1328 editor.selections.display_ranges(cx),
1329 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1330 );
1331 });
1332}
1333
1334#[gpui::test]
1335fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1336 init_test(cx, |_| {});
1337
1338 let editor = cx.add_window(|window, cx| {
1339 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1340 build_editor(buffer, window, cx)
1341 });
1342
1343 assert_eq!('🟥'.len_utf8(), 4);
1344 assert_eq!('α'.len_utf8(), 2);
1345
1346 _ = editor.update(cx, |editor, window, cx| {
1347 editor.fold_creases(
1348 vec![
1349 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1350 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1351 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1352 ],
1353 true,
1354 window,
1355 cx,
1356 );
1357 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1358
1359 editor.move_right(&MoveRight, window, cx);
1360 assert_eq!(
1361 editor.selections.display_ranges(cx),
1362 &[empty_range(0, "🟥".len())]
1363 );
1364 editor.move_right(&MoveRight, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(0, "🟥🟧".len())]
1368 );
1369 editor.move_right(&MoveRight, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(0, "🟥🟧⋯".len())]
1373 );
1374
1375 editor.move_down(&MoveDown, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(1, "ab⋯e".len())]
1379 );
1380 editor.move_left(&MoveLeft, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(1, "ab⋯".len())]
1384 );
1385 editor.move_left(&MoveLeft, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(1, "ab".len())]
1389 );
1390 editor.move_left(&MoveLeft, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(1, "a".len())]
1394 );
1395
1396 editor.move_down(&MoveDown, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(2, "α".len())]
1400 );
1401 editor.move_right(&MoveRight, window, cx);
1402 assert_eq!(
1403 editor.selections.display_ranges(cx),
1404 &[empty_range(2, "αβ".len())]
1405 );
1406 editor.move_right(&MoveRight, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(2, "αβ⋯".len())]
1410 );
1411 editor.move_right(&MoveRight, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯ε".len())]
1415 );
1416
1417 editor.move_up(&MoveUp, window, cx);
1418 assert_eq!(
1419 editor.selections.display_ranges(cx),
1420 &[empty_range(1, "ab⋯e".len())]
1421 );
1422 editor.move_down(&MoveDown, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(2, "αβ⋯ε".len())]
1426 );
1427 editor.move_up(&MoveUp, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(1, "ab⋯e".len())]
1431 );
1432
1433 editor.move_up(&MoveUp, window, cx);
1434 assert_eq!(
1435 editor.selections.display_ranges(cx),
1436 &[empty_range(0, "🟥🟧".len())]
1437 );
1438 editor.move_left(&MoveLeft, window, cx);
1439 assert_eq!(
1440 editor.selections.display_ranges(cx),
1441 &[empty_range(0, "🟥".len())]
1442 );
1443 editor.move_left(&MoveLeft, window, cx);
1444 assert_eq!(
1445 editor.selections.display_ranges(cx),
1446 &[empty_range(0, "".len())]
1447 );
1448 });
1449}
1450
1451#[gpui::test]
1452fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1453 init_test(cx, |_| {});
1454
1455 let editor = cx.add_window(|window, cx| {
1456 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1457 build_editor(buffer, window, cx)
1458 });
1459 _ = editor.update(cx, |editor, window, cx| {
1460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1461 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1462 });
1463
1464 // moving above start of document should move selection to start of document,
1465 // but the next move down should still be at the original goal_x
1466 editor.move_up(&MoveUp, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(0, "".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(1, "abcd".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(2, "αβγ".len())]
1482 );
1483
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(3, "abcd".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1494 );
1495
1496 // moving past end of document should not change goal_x
1497 editor.move_down(&MoveDown, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(5, "".len())]
1501 );
1502
1503 editor.move_down(&MoveDown, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(5, "".len())]
1507 );
1508
1509 editor.move_up(&MoveUp, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1513 );
1514
1515 editor.move_up(&MoveUp, window, cx);
1516 assert_eq!(
1517 editor.selections.display_ranges(cx),
1518 &[empty_range(3, "abcd".len())]
1519 );
1520
1521 editor.move_up(&MoveUp, window, cx);
1522 assert_eq!(
1523 editor.selections.display_ranges(cx),
1524 &[empty_range(2, "αβγ".len())]
1525 );
1526 });
1527}
1528
1529#[gpui::test]
1530fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1531 init_test(cx, |_| {});
1532 let move_to_beg = MoveToBeginningOfLine {
1533 stop_at_soft_wraps: true,
1534 stop_at_indent: true,
1535 };
1536
1537 let delete_to_beg = DeleteToBeginningOfLine {
1538 stop_at_indent: false,
1539 };
1540
1541 let move_to_end = MoveToEndOfLine {
1542 stop_at_soft_wraps: true,
1543 };
1544
1545 let editor = cx.add_window(|window, cx| {
1546 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1547 build_editor(buffer, window, cx)
1548 });
1549 _ = editor.update(cx, |editor, window, cx| {
1550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1551 s.select_display_ranges([
1552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1553 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1554 ]);
1555 });
1556 });
1557
1558 _ = editor.update(cx, |editor, window, cx| {
1559 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1560 assert_eq!(
1561 editor.selections.display_ranges(cx),
1562 &[
1563 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1565 ]
1566 );
1567 });
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1571 assert_eq!(
1572 editor.selections.display_ranges(cx),
1573 &[
1574 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1575 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1576 ]
1577 );
1578 });
1579
1580 _ = editor.update(cx, |editor, window, cx| {
1581 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1582 assert_eq!(
1583 editor.selections.display_ranges(cx),
1584 &[
1585 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1586 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1587 ]
1588 );
1589 });
1590
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_to_end_of_line(&move_to_end, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[
1596 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1597 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1598 ]
1599 );
1600 });
1601
1602 // Moving to the end of line again is a no-op.
1603 _ = editor.update(cx, |editor, window, cx| {
1604 editor.move_to_end_of_line(&move_to_end, window, cx);
1605 assert_eq!(
1606 editor.selections.display_ranges(cx),
1607 &[
1608 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1609 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1610 ]
1611 );
1612 });
1613
1614 _ = editor.update(cx, |editor, window, cx| {
1615 editor.move_left(&MoveLeft, window, cx);
1616 editor.select_to_beginning_of_line(
1617 &SelectToBeginningOfLine {
1618 stop_at_soft_wraps: true,
1619 stop_at_indent: true,
1620 },
1621 window,
1622 cx,
1623 );
1624 assert_eq!(
1625 editor.selections.display_ranges(cx),
1626 &[
1627 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1628 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1629 ]
1630 );
1631 });
1632
1633 _ = editor.update(cx, |editor, window, cx| {
1634 editor.select_to_beginning_of_line(
1635 &SelectToBeginningOfLine {
1636 stop_at_soft_wraps: true,
1637 stop_at_indent: true,
1638 },
1639 window,
1640 cx,
1641 );
1642 assert_eq!(
1643 editor.selections.display_ranges(cx),
1644 &[
1645 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1646 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1647 ]
1648 );
1649 });
1650
1651 _ = editor.update(cx, |editor, window, cx| {
1652 editor.select_to_beginning_of_line(
1653 &SelectToBeginningOfLine {
1654 stop_at_soft_wraps: true,
1655 stop_at_indent: true,
1656 },
1657 window,
1658 cx,
1659 );
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[
1663 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1664 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1665 ]
1666 );
1667 });
1668
1669 _ = editor.update(cx, |editor, window, cx| {
1670 editor.select_to_end_of_line(
1671 &SelectToEndOfLine {
1672 stop_at_soft_wraps: true,
1673 },
1674 window,
1675 cx,
1676 );
1677 assert_eq!(
1678 editor.selections.display_ranges(cx),
1679 &[
1680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1681 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1682 ]
1683 );
1684 });
1685
1686 _ = editor.update(cx, |editor, window, cx| {
1687 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1688 assert_eq!(editor.display_text(cx), "ab\n de");
1689 assert_eq!(
1690 editor.selections.display_ranges(cx),
1691 &[
1692 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1693 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1694 ]
1695 );
1696 });
1697
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1700 assert_eq!(editor.display_text(cx), "\n");
1701 assert_eq!(
1702 editor.selections.display_ranges(cx),
1703 &[
1704 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1706 ]
1707 );
1708 });
1709}
1710
1711#[gpui::test]
1712fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1713 init_test(cx, |_| {});
1714 let move_to_beg = MoveToBeginningOfLine {
1715 stop_at_soft_wraps: false,
1716 stop_at_indent: false,
1717 };
1718
1719 let move_to_end = MoveToEndOfLine {
1720 stop_at_soft_wraps: false,
1721 };
1722
1723 let editor = cx.add_window(|window, cx| {
1724 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1725 build_editor(buffer, window, cx)
1726 });
1727
1728 _ = editor.update(cx, |editor, window, cx| {
1729 editor.set_wrap_width(Some(140.0.into()), cx);
1730
1731 // We expect the following lines after wrapping
1732 // ```
1733 // thequickbrownfox
1734 // jumpedoverthelazydo
1735 // gs
1736 // ```
1737 // The final `gs` was soft-wrapped onto a new line.
1738 assert_eq!(
1739 "thequickbrownfox\njumpedoverthelaz\nydogs",
1740 editor.display_text(cx),
1741 );
1742
1743 // First, let's assert behavior on the first line, that was not soft-wrapped.
1744 // Start the cursor at the `k` on the first line
1745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1746 s.select_display_ranges([
1747 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1748 ]);
1749 });
1750
1751 // Moving to the beginning of the line should put us at the beginning of the line.
1752 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1753 assert_eq!(
1754 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1755 editor.selections.display_ranges(cx)
1756 );
1757
1758 // Moving to the end of the line should put us at the end of the line.
1759 editor.move_to_end_of_line(&move_to_end, window, cx);
1760 assert_eq!(
1761 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1762 editor.selections.display_ranges(cx)
1763 );
1764
1765 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1766 // Start the cursor at the last line (`y` that was wrapped to a new line)
1767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1768 s.select_display_ranges([
1769 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1770 ]);
1771 });
1772
1773 // Moving to the beginning of the line should put us at the start of the second line of
1774 // display text, i.e., the `j`.
1775 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1776 assert_eq!(
1777 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1778 editor.selections.display_ranges(cx)
1779 );
1780
1781 // Moving to the beginning of the line again should be a no-op.
1782 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1783 assert_eq!(
1784 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1785 editor.selections.display_ranges(cx)
1786 );
1787
1788 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1789 // next display line.
1790 editor.move_to_end_of_line(&move_to_end, window, cx);
1791 assert_eq!(
1792 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1793 editor.selections.display_ranges(cx)
1794 );
1795
1796 // Moving to the end of the line again should be a no-op.
1797 editor.move_to_end_of_line(&move_to_end, window, cx);
1798 assert_eq!(
1799 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1800 editor.selections.display_ranges(cx)
1801 );
1802 });
1803}
1804
1805#[gpui::test]
1806fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1807 init_test(cx, |_| {});
1808
1809 let move_to_beg = MoveToBeginningOfLine {
1810 stop_at_soft_wraps: true,
1811 stop_at_indent: true,
1812 };
1813
1814 let select_to_beg = SelectToBeginningOfLine {
1815 stop_at_soft_wraps: true,
1816 stop_at_indent: true,
1817 };
1818
1819 let delete_to_beg = DeleteToBeginningOfLine {
1820 stop_at_indent: true,
1821 };
1822
1823 let move_to_end = MoveToEndOfLine {
1824 stop_at_soft_wraps: false,
1825 };
1826
1827 let editor = cx.add_window(|window, cx| {
1828 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1829 build_editor(buffer, window, cx)
1830 });
1831
1832 _ = editor.update(cx, |editor, window, cx| {
1833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1834 s.select_display_ranges([
1835 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1836 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1837 ]);
1838 });
1839
1840 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1841 // and the second cursor at the first non-whitespace character in the line.
1842 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1843 assert_eq!(
1844 editor.selections.display_ranges(cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1848 ]
1849 );
1850
1851 // Moving to the beginning of the line again should be a no-op for the first cursor,
1852 // and should move the second cursor to the beginning of the line.
1853 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1854 assert_eq!(
1855 editor.selections.display_ranges(cx),
1856 &[
1857 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1858 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1859 ]
1860 );
1861
1862 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1863 // and should move the second cursor back to the first non-whitespace character in the line.
1864 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1874 // and to the first non-whitespace character in the line for the second cursor.
1875 editor.move_to_end_of_line(&move_to_end, window, cx);
1876 editor.move_left(&MoveLeft, window, cx);
1877 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1878 assert_eq!(
1879 editor.selections.display_ranges(cx),
1880 &[
1881 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1882 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1883 ]
1884 );
1885
1886 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1887 // and should select to the beginning of the line for the second cursor.
1888 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1889 assert_eq!(
1890 editor.selections.display_ranges(cx),
1891 &[
1892 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1893 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1894 ]
1895 );
1896
1897 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1898 // and should delete to the first non-whitespace character in the line for the second cursor.
1899 editor.move_to_end_of_line(&move_to_end, window, cx);
1900 editor.move_left(&MoveLeft, window, cx);
1901 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1902 assert_eq!(editor.text(cx), "c\n f");
1903 });
1904}
1905
1906#[gpui::test]
1907fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1908 init_test(cx, |_| {});
1909
1910 let move_to_beg = MoveToBeginningOfLine {
1911 stop_at_soft_wraps: true,
1912 stop_at_indent: true,
1913 };
1914
1915 let editor = cx.add_window(|window, cx| {
1916 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1917 build_editor(buffer, window, cx)
1918 });
1919
1920 _ = editor.update(cx, |editor, window, cx| {
1921 // test cursor between line_start and indent_start
1922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1923 s.select_display_ranges([
1924 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1925 ]);
1926 });
1927
1928 // cursor should move to line_start
1929 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1930 assert_eq!(
1931 editor.selections.display_ranges(cx),
1932 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1933 );
1934
1935 // cursor should move to indent_start
1936 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1937 assert_eq!(
1938 editor.selections.display_ranges(cx),
1939 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1940 );
1941
1942 // cursor should move to back to line_start
1943 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1944 assert_eq!(
1945 editor.selections.display_ranges(cx),
1946 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1947 );
1948 });
1949}
1950
1951#[gpui::test]
1952fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1953 init_test(cx, |_| {});
1954
1955 let editor = cx.add_window(|window, cx| {
1956 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1957 build_editor(buffer, window, cx)
1958 });
1959 _ = editor.update(cx, |editor, window, cx| {
1960 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1961 s.select_display_ranges([
1962 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1963 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1964 ])
1965 });
1966 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1967 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1968
1969 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1970 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1971
1972 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1973 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1974
1975 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1976 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1977
1978 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1979 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1980
1981 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1982 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1986
1987 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1988 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1989
1990 editor.move_right(&MoveRight, window, cx);
1991 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1992 assert_selection_ranges(
1993 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1994 editor,
1995 cx,
1996 );
1997
1998 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1999 assert_selection_ranges(
2000 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2001 editor,
2002 cx,
2003 );
2004
2005 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2006 assert_selection_ranges(
2007 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2008 editor,
2009 cx,
2010 );
2011 });
2012}
2013
2014#[gpui::test]
2015fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2016 init_test(cx, |_| {});
2017
2018 let editor = cx.add_window(|window, cx| {
2019 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2020 build_editor(buffer, window, cx)
2021 });
2022
2023 _ = editor.update(cx, |editor, window, cx| {
2024 editor.set_wrap_width(Some(140.0.into()), cx);
2025 assert_eq!(
2026 editor.display_text(cx),
2027 "use one::{\n two::three::\n four::five\n};"
2028 );
2029
2030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2031 s.select_display_ranges([
2032 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2033 ]);
2034 });
2035
2036 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2037 assert_eq!(
2038 editor.selections.display_ranges(cx),
2039 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2040 );
2041
2042 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2043 assert_eq!(
2044 editor.selections.display_ranges(cx),
2045 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2046 );
2047
2048 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2049 assert_eq!(
2050 editor.selections.display_ranges(cx),
2051 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2052 );
2053
2054 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2055 assert_eq!(
2056 editor.selections.display_ranges(cx),
2057 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2058 );
2059
2060 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2061 assert_eq!(
2062 editor.selections.display_ranges(cx),
2063 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2064 );
2065
2066 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2067 assert_eq!(
2068 editor.selections.display_ranges(cx),
2069 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2070 );
2071 });
2072}
2073
2074#[gpui::test]
2075async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2076 init_test(cx, |_| {});
2077 let mut cx = EditorTestContext::new(cx).await;
2078
2079 let line_height = cx.editor(|editor, window, _| {
2080 editor
2081 .style()
2082 .unwrap()
2083 .text
2084 .line_height_in_pixels(window.rem_size())
2085 });
2086 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2087
2088 cx.set_state(
2089 &r#"ˇone
2090 two
2091
2092 three
2093 fourˇ
2094 five
2095
2096 six"#
2097 .unindent(),
2098 );
2099
2100 cx.update_editor(|editor, window, cx| {
2101 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2102 });
2103 cx.assert_editor_state(
2104 &r#"one
2105 two
2106 ˇ
2107 three
2108 four
2109 five
2110 ˇ
2111 six"#
2112 .unindent(),
2113 );
2114
2115 cx.update_editor(|editor, window, cx| {
2116 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2117 });
2118 cx.assert_editor_state(
2119 &r#"one
2120 two
2121
2122 three
2123 four
2124 five
2125 ˇ
2126 sixˇ"#
2127 .unindent(),
2128 );
2129
2130 cx.update_editor(|editor, window, cx| {
2131 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2132 });
2133 cx.assert_editor_state(
2134 &r#"one
2135 two
2136
2137 three
2138 four
2139 five
2140
2141 sixˇ"#
2142 .unindent(),
2143 );
2144
2145 cx.update_editor(|editor, window, cx| {
2146 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2147 });
2148 cx.assert_editor_state(
2149 &r#"one
2150 two
2151
2152 three
2153 four
2154 five
2155 ˇ
2156 six"#
2157 .unindent(),
2158 );
2159
2160 cx.update_editor(|editor, window, cx| {
2161 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2162 });
2163 cx.assert_editor_state(
2164 &r#"one
2165 two
2166 ˇ
2167 three
2168 four
2169 five
2170
2171 six"#
2172 .unindent(),
2173 );
2174
2175 cx.update_editor(|editor, window, cx| {
2176 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2177 });
2178 cx.assert_editor_state(
2179 &r#"ˇone
2180 two
2181
2182 three
2183 four
2184 five
2185
2186 six"#
2187 .unindent(),
2188 );
2189}
2190
2191#[gpui::test]
2192async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2193 init_test(cx, |_| {});
2194 let mut cx = EditorTestContext::new(cx).await;
2195 let line_height = cx.editor(|editor, window, _| {
2196 editor
2197 .style()
2198 .unwrap()
2199 .text
2200 .line_height_in_pixels(window.rem_size())
2201 });
2202 let window = cx.window;
2203 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2204
2205 cx.set_state(
2206 r#"ˇone
2207 two
2208 three
2209 four
2210 five
2211 six
2212 seven
2213 eight
2214 nine
2215 ten
2216 "#,
2217 );
2218
2219 cx.update_editor(|editor, window, cx| {
2220 assert_eq!(
2221 editor.snapshot(window, cx).scroll_position(),
2222 gpui::Point::new(0., 0.)
2223 );
2224 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 3.)
2228 );
2229 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2230 assert_eq!(
2231 editor.snapshot(window, cx).scroll_position(),
2232 gpui::Point::new(0., 6.)
2233 );
2234 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2235 assert_eq!(
2236 editor.snapshot(window, cx).scroll_position(),
2237 gpui::Point::new(0., 3.)
2238 );
2239
2240 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2241 assert_eq!(
2242 editor.snapshot(window, cx).scroll_position(),
2243 gpui::Point::new(0., 1.)
2244 );
2245 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 3.)
2249 );
2250 });
2251}
2252
2253#[gpui::test]
2254async fn test_autoscroll(cx: &mut TestAppContext) {
2255 init_test(cx, |_| {});
2256 let mut cx = EditorTestContext::new(cx).await;
2257
2258 let line_height = cx.update_editor(|editor, window, cx| {
2259 editor.set_vertical_scroll_margin(2, cx);
2260 editor
2261 .style()
2262 .unwrap()
2263 .text
2264 .line_height_in_pixels(window.rem_size())
2265 });
2266 let window = cx.window;
2267 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2268
2269 cx.set_state(
2270 r#"ˇone
2271 two
2272 three
2273 four
2274 five
2275 six
2276 seven
2277 eight
2278 nine
2279 ten
2280 "#,
2281 );
2282 cx.update_editor(|editor, window, cx| {
2283 assert_eq!(
2284 editor.snapshot(window, cx).scroll_position(),
2285 gpui::Point::new(0., 0.0)
2286 );
2287 });
2288
2289 // Add a cursor below the visible area. Since both cursors cannot fit
2290 // on screen, the editor autoscrolls to reveal the newest cursor, and
2291 // allows the vertical scroll margin below that cursor.
2292 cx.update_editor(|editor, window, cx| {
2293 editor.change_selections(Default::default(), window, cx, |selections| {
2294 selections.select_ranges([
2295 Point::new(0, 0)..Point::new(0, 0),
2296 Point::new(6, 0)..Point::new(6, 0),
2297 ]);
2298 })
2299 });
2300 cx.update_editor(|editor, window, cx| {
2301 assert_eq!(
2302 editor.snapshot(window, cx).scroll_position(),
2303 gpui::Point::new(0., 3.0)
2304 );
2305 });
2306
2307 // Move down. The editor cursor scrolls down to track the newest cursor.
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_down(&Default::default(), window, cx);
2310 });
2311 cx.update_editor(|editor, window, cx| {
2312 assert_eq!(
2313 editor.snapshot(window, cx).scroll_position(),
2314 gpui::Point::new(0., 4.0)
2315 );
2316 });
2317
2318 // Add a cursor above the visible area. Since both cursors fit on screen,
2319 // the editor scrolls to show both.
2320 cx.update_editor(|editor, window, cx| {
2321 editor.change_selections(Default::default(), window, cx, |selections| {
2322 selections.select_ranges([
2323 Point::new(1, 0)..Point::new(1, 0),
2324 Point::new(6, 0)..Point::new(6, 0),
2325 ]);
2326 })
2327 });
2328 cx.update_editor(|editor, window, cx| {
2329 assert_eq!(
2330 editor.snapshot(window, cx).scroll_position(),
2331 gpui::Point::new(0., 1.0)
2332 );
2333 });
2334}
2335
2336#[gpui::test]
2337async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2338 init_test(cx, |_| {});
2339 let mut cx = EditorTestContext::new(cx).await;
2340
2341 let line_height = cx.editor(|editor, window, _cx| {
2342 editor
2343 .style()
2344 .unwrap()
2345 .text
2346 .line_height_in_pixels(window.rem_size())
2347 });
2348 let window = cx.window;
2349 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2350 cx.set_state(
2351 &r#"
2352 ˇone
2353 two
2354 threeˇ
2355 four
2356 five
2357 six
2358 seven
2359 eight
2360 nine
2361 ten
2362 "#
2363 .unindent(),
2364 );
2365
2366 cx.update_editor(|editor, window, cx| {
2367 editor.move_page_down(&MovePageDown::default(), window, cx)
2368 });
2369 cx.assert_editor_state(
2370 &r#"
2371 one
2372 two
2373 three
2374 ˇfour
2375 five
2376 sixˇ
2377 seven
2378 eight
2379 nine
2380 ten
2381 "#
2382 .unindent(),
2383 );
2384
2385 cx.update_editor(|editor, window, cx| {
2386 editor.move_page_down(&MovePageDown::default(), window, cx)
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 ˇseven
2397 eight
2398 nineˇ
2399 ten
2400 "#
2401 .unindent(),
2402 );
2403
2404 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2405 cx.assert_editor_state(
2406 &r#"
2407 one
2408 two
2409 three
2410 ˇfour
2411 five
2412 sixˇ
2413 seven
2414 eight
2415 nine
2416 ten
2417 "#
2418 .unindent(),
2419 );
2420
2421 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2422 cx.assert_editor_state(
2423 &r#"
2424 ˇone
2425 two
2426 threeˇ
2427 four
2428 five
2429 six
2430 seven
2431 eight
2432 nine
2433 ten
2434 "#
2435 .unindent(),
2436 );
2437
2438 // Test select collapsing
2439 cx.update_editor(|editor, window, cx| {
2440 editor.move_page_down(&MovePageDown::default(), window, cx);
2441 editor.move_page_down(&MovePageDown::default(), window, cx);
2442 editor.move_page_down(&MovePageDown::default(), window, cx);
2443 });
2444 cx.assert_editor_state(
2445 &r#"
2446 one
2447 two
2448 three
2449 four
2450 five
2451 six
2452 seven
2453 eight
2454 nine
2455 ˇten
2456 ˇ"#
2457 .unindent(),
2458 );
2459}
2460
2461#[gpui::test]
2462async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2463 init_test(cx, |_| {});
2464 let mut cx = EditorTestContext::new(cx).await;
2465 cx.set_state("one «two threeˇ» four");
2466 cx.update_editor(|editor, window, cx| {
2467 editor.delete_to_beginning_of_line(
2468 &DeleteToBeginningOfLine {
2469 stop_at_indent: false,
2470 },
2471 window,
2472 cx,
2473 );
2474 assert_eq!(editor.text(cx), " four");
2475 });
2476}
2477
2478#[gpui::test]
2479async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481
2482 let mut cx = EditorTestContext::new(cx).await;
2483
2484 // For an empty selection, the preceding word fragment is deleted.
2485 // For non-empty selections, only selected characters are deleted.
2486 cx.set_state("onˇe two t«hreˇ»e four");
2487 cx.update_editor(|editor, window, cx| {
2488 editor.delete_to_previous_word_start(
2489 &DeleteToPreviousWordStart {
2490 ignore_newlines: false,
2491 ignore_brackets: false,
2492 },
2493 window,
2494 cx,
2495 );
2496 });
2497 cx.assert_editor_state("ˇe two tˇe four");
2498
2499 cx.set_state("e tˇwo te «fˇ»our");
2500 cx.update_editor(|editor, window, cx| {
2501 editor.delete_to_next_word_end(
2502 &DeleteToNextWordEnd {
2503 ignore_newlines: false,
2504 ignore_brackets: false,
2505 },
2506 window,
2507 cx,
2508 );
2509 });
2510 cx.assert_editor_state("e tˇ te ˇour");
2511}
2512
2513#[gpui::test]
2514async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2515 init_test(cx, |_| {});
2516
2517 let mut cx = EditorTestContext::new(cx).await;
2518
2519 cx.set_state("here is some text ˇwith a space");
2520 cx.update_editor(|editor, window, cx| {
2521 editor.delete_to_previous_word_start(
2522 &DeleteToPreviousWordStart {
2523 ignore_newlines: false,
2524 ignore_brackets: true,
2525 },
2526 window,
2527 cx,
2528 );
2529 });
2530 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2531 cx.assert_editor_state("here is some textˇwith a space");
2532
2533 cx.set_state("here is some text ˇwith a space");
2534 cx.update_editor(|editor, window, cx| {
2535 editor.delete_to_previous_word_start(
2536 &DeleteToPreviousWordStart {
2537 ignore_newlines: false,
2538 ignore_brackets: false,
2539 },
2540 window,
2541 cx,
2542 );
2543 });
2544 cx.assert_editor_state("here is some textˇwith a space");
2545
2546 cx.set_state("here is some textˇ with a space");
2547 cx.update_editor(|editor, window, cx| {
2548 editor.delete_to_next_word_end(
2549 &DeleteToNextWordEnd {
2550 ignore_newlines: false,
2551 ignore_brackets: true,
2552 },
2553 window,
2554 cx,
2555 );
2556 });
2557 // Same happens in the other direction.
2558 cx.assert_editor_state("here is some textˇwith a space");
2559
2560 cx.set_state("here is some textˇ with a space");
2561 cx.update_editor(|editor, window, cx| {
2562 editor.delete_to_next_word_end(
2563 &DeleteToNextWordEnd {
2564 ignore_newlines: false,
2565 ignore_brackets: false,
2566 },
2567 window,
2568 cx,
2569 );
2570 });
2571 cx.assert_editor_state("here is some textˇwith a space");
2572
2573 cx.set_state("here is some textˇ with a space");
2574 cx.update_editor(|editor, window, cx| {
2575 editor.delete_to_next_word_end(
2576 &DeleteToNextWordEnd {
2577 ignore_newlines: true,
2578 ignore_brackets: false,
2579 },
2580 window,
2581 cx,
2582 );
2583 });
2584 cx.assert_editor_state("here is some textˇwith a space");
2585 cx.update_editor(|editor, window, cx| {
2586 editor.delete_to_previous_word_start(
2587 &DeleteToPreviousWordStart {
2588 ignore_newlines: true,
2589 ignore_brackets: false,
2590 },
2591 window,
2592 cx,
2593 );
2594 });
2595 cx.assert_editor_state("here is some ˇwith a space");
2596 cx.update_editor(|editor, window, cx| {
2597 editor.delete_to_previous_word_start(
2598 &DeleteToPreviousWordStart {
2599 ignore_newlines: true,
2600 ignore_brackets: false,
2601 },
2602 window,
2603 cx,
2604 );
2605 });
2606 // Single whitespaces are removed with the word behind them.
2607 cx.assert_editor_state("here is ˇwith a space");
2608 cx.update_editor(|editor, window, cx| {
2609 editor.delete_to_previous_word_start(
2610 &DeleteToPreviousWordStart {
2611 ignore_newlines: true,
2612 ignore_brackets: false,
2613 },
2614 window,
2615 cx,
2616 );
2617 });
2618 cx.assert_editor_state("here ˇwith a space");
2619 cx.update_editor(|editor, window, cx| {
2620 editor.delete_to_previous_word_start(
2621 &DeleteToPreviousWordStart {
2622 ignore_newlines: true,
2623 ignore_brackets: false,
2624 },
2625 window,
2626 cx,
2627 );
2628 });
2629 cx.assert_editor_state("ˇwith a space");
2630 cx.update_editor(|editor, window, cx| {
2631 editor.delete_to_previous_word_start(
2632 &DeleteToPreviousWordStart {
2633 ignore_newlines: true,
2634 ignore_brackets: false,
2635 },
2636 window,
2637 cx,
2638 );
2639 });
2640 cx.assert_editor_state("ˇwith a space");
2641 cx.update_editor(|editor, window, cx| {
2642 editor.delete_to_next_word_end(
2643 &DeleteToNextWordEnd {
2644 ignore_newlines: true,
2645 ignore_brackets: false,
2646 },
2647 window,
2648 cx,
2649 );
2650 });
2651 // Same happens in the other direction.
2652 cx.assert_editor_state("ˇ a space");
2653 cx.update_editor(|editor, window, cx| {
2654 editor.delete_to_next_word_end(
2655 &DeleteToNextWordEnd {
2656 ignore_newlines: true,
2657 ignore_brackets: false,
2658 },
2659 window,
2660 cx,
2661 );
2662 });
2663 cx.assert_editor_state("ˇ space");
2664 cx.update_editor(|editor, window, cx| {
2665 editor.delete_to_next_word_end(
2666 &DeleteToNextWordEnd {
2667 ignore_newlines: true,
2668 ignore_brackets: false,
2669 },
2670 window,
2671 cx,
2672 );
2673 });
2674 cx.assert_editor_state("ˇ");
2675 cx.update_editor(|editor, window, cx| {
2676 editor.delete_to_next_word_end(
2677 &DeleteToNextWordEnd {
2678 ignore_newlines: true,
2679 ignore_brackets: false,
2680 },
2681 window,
2682 cx,
2683 );
2684 });
2685 cx.assert_editor_state("ˇ");
2686 cx.update_editor(|editor, window, cx| {
2687 editor.delete_to_previous_word_start(
2688 &DeleteToPreviousWordStart {
2689 ignore_newlines: true,
2690 ignore_brackets: false,
2691 },
2692 window,
2693 cx,
2694 );
2695 });
2696 cx.assert_editor_state("ˇ");
2697}
2698
2699#[gpui::test]
2700async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2701 init_test(cx, |_| {});
2702
2703 let language = Arc::new(
2704 Language::new(
2705 LanguageConfig {
2706 brackets: BracketPairConfig {
2707 pairs: vec![
2708 BracketPair {
2709 start: "\"".to_string(),
2710 end: "\"".to_string(),
2711 close: true,
2712 surround: true,
2713 newline: false,
2714 },
2715 BracketPair {
2716 start: "(".to_string(),
2717 end: ")".to_string(),
2718 close: true,
2719 surround: true,
2720 newline: true,
2721 },
2722 ],
2723 ..BracketPairConfig::default()
2724 },
2725 ..LanguageConfig::default()
2726 },
2727 Some(tree_sitter_rust::LANGUAGE.into()),
2728 )
2729 .with_brackets_query(
2730 r#"
2731 ("(" @open ")" @close)
2732 ("\"" @open "\"" @close)
2733 "#,
2734 )
2735 .unwrap(),
2736 );
2737
2738 let mut cx = EditorTestContext::new(cx).await;
2739 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2740
2741 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2742 cx.update_editor(|editor, window, cx| {
2743 editor.delete_to_previous_word_start(
2744 &DeleteToPreviousWordStart {
2745 ignore_newlines: true,
2746 ignore_brackets: false,
2747 },
2748 window,
2749 cx,
2750 );
2751 });
2752 // Deletion stops before brackets if asked to not ignore them.
2753 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2754 cx.update_editor(|editor, window, cx| {
2755 editor.delete_to_previous_word_start(
2756 &DeleteToPreviousWordStart {
2757 ignore_newlines: true,
2758 ignore_brackets: false,
2759 },
2760 window,
2761 cx,
2762 );
2763 });
2764 // Deletion has to remove a single bracket and then stop again.
2765 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2766
2767 cx.update_editor(|editor, window, cx| {
2768 editor.delete_to_previous_word_start(
2769 &DeleteToPreviousWordStart {
2770 ignore_newlines: true,
2771 ignore_brackets: false,
2772 },
2773 window,
2774 cx,
2775 );
2776 });
2777 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2778
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_previous_word_start(
2781 &DeleteToPreviousWordStart {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2790
2791 cx.update_editor(|editor, window, cx| {
2792 editor.delete_to_previous_word_start(
2793 &DeleteToPreviousWordStart {
2794 ignore_newlines: true,
2795 ignore_brackets: false,
2796 },
2797 window,
2798 cx,
2799 );
2800 });
2801 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2802
2803 cx.update_editor(|editor, window, cx| {
2804 editor.delete_to_next_word_end(
2805 &DeleteToNextWordEnd {
2806 ignore_newlines: true,
2807 ignore_brackets: false,
2808 },
2809 window,
2810 cx,
2811 );
2812 });
2813 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2814 cx.assert_editor_state(r#"ˇ");"#);
2815
2816 cx.update_editor(|editor, window, cx| {
2817 editor.delete_to_next_word_end(
2818 &DeleteToNextWordEnd {
2819 ignore_newlines: true,
2820 ignore_brackets: false,
2821 },
2822 window,
2823 cx,
2824 );
2825 });
2826 cx.assert_editor_state(r#"ˇ"#);
2827
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state(r#"ˇ"#);
2839
2840 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2841 cx.update_editor(|editor, window, cx| {
2842 editor.delete_to_previous_word_start(
2843 &DeleteToPreviousWordStart {
2844 ignore_newlines: true,
2845 ignore_brackets: true,
2846 },
2847 window,
2848 cx,
2849 );
2850 });
2851 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2852}
2853
2854#[gpui::test]
2855fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2856 init_test(cx, |_| {});
2857
2858 let editor = cx.add_window(|window, cx| {
2859 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2860 build_editor(buffer, window, cx)
2861 });
2862 let del_to_prev_word_start = DeleteToPreviousWordStart {
2863 ignore_newlines: false,
2864 ignore_brackets: false,
2865 };
2866 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2867 ignore_newlines: true,
2868 ignore_brackets: false,
2869 };
2870
2871 _ = editor.update(cx, |editor, window, cx| {
2872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2873 s.select_display_ranges([
2874 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2875 ])
2876 });
2877 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2878 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2879 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2880 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2881 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2882 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2883 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2884 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2885 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2886 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2887 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2888 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2889 });
2890}
2891
2892#[gpui::test]
2893fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2894 init_test(cx, |_| {});
2895
2896 let editor = cx.add_window(|window, cx| {
2897 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2898 build_editor(buffer, window, cx)
2899 });
2900 let del_to_next_word_end = DeleteToNextWordEnd {
2901 ignore_newlines: false,
2902 ignore_brackets: false,
2903 };
2904 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2905 ignore_newlines: true,
2906 ignore_brackets: false,
2907 };
2908
2909 _ = editor.update(cx, |editor, window, cx| {
2910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2911 s.select_display_ranges([
2912 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2913 ])
2914 });
2915 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2916 assert_eq!(
2917 editor.buffer.read(cx).read(cx).text(),
2918 "one\n two\nthree\n four"
2919 );
2920 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2921 assert_eq!(
2922 editor.buffer.read(cx).read(cx).text(),
2923 "\n two\nthree\n four"
2924 );
2925 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2926 assert_eq!(
2927 editor.buffer.read(cx).read(cx).text(),
2928 "two\nthree\n four"
2929 );
2930 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2931 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2932 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2933 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2934 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2935 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2936 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2937 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2938 });
2939}
2940
2941#[gpui::test]
2942fn test_newline(cx: &mut TestAppContext) {
2943 init_test(cx, |_| {});
2944
2945 let editor = cx.add_window(|window, cx| {
2946 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2947 build_editor(buffer, window, cx)
2948 });
2949
2950 _ = editor.update(cx, |editor, window, cx| {
2951 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2952 s.select_display_ranges([
2953 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2954 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2955 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2956 ])
2957 });
2958
2959 editor.newline(&Newline, window, cx);
2960 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2961 });
2962}
2963
2964#[gpui::test]
2965fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2966 init_test(cx, |_| {});
2967
2968 let editor = cx.add_window(|window, cx| {
2969 let buffer = MultiBuffer::build_simple(
2970 "
2971 a
2972 b(
2973 X
2974 )
2975 c(
2976 X
2977 )
2978 "
2979 .unindent()
2980 .as_str(),
2981 cx,
2982 );
2983 let mut editor = build_editor(buffer, window, cx);
2984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2985 s.select_ranges([
2986 Point::new(2, 4)..Point::new(2, 5),
2987 Point::new(5, 4)..Point::new(5, 5),
2988 ])
2989 });
2990 editor
2991 });
2992
2993 _ = editor.update(cx, |editor, window, cx| {
2994 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2995 editor.buffer.update(cx, |buffer, cx| {
2996 buffer.edit(
2997 [
2998 (Point::new(1, 2)..Point::new(3, 0), ""),
2999 (Point::new(4, 2)..Point::new(6, 0), ""),
3000 ],
3001 None,
3002 cx,
3003 );
3004 assert_eq!(
3005 buffer.read(cx).text(),
3006 "
3007 a
3008 b()
3009 c()
3010 "
3011 .unindent()
3012 );
3013 });
3014 assert_eq!(
3015 editor.selections.ranges(cx),
3016 &[
3017 Point::new(1, 2)..Point::new(1, 2),
3018 Point::new(2, 2)..Point::new(2, 2),
3019 ],
3020 );
3021
3022 editor.newline(&Newline, window, cx);
3023 assert_eq!(
3024 editor.text(cx),
3025 "
3026 a
3027 b(
3028 )
3029 c(
3030 )
3031 "
3032 .unindent()
3033 );
3034
3035 // The selections are moved after the inserted newlines
3036 assert_eq!(
3037 editor.selections.ranges(cx),
3038 &[
3039 Point::new(2, 0)..Point::new(2, 0),
3040 Point::new(4, 0)..Point::new(4, 0),
3041 ],
3042 );
3043 });
3044}
3045
3046#[gpui::test]
3047async fn test_newline_above(cx: &mut TestAppContext) {
3048 init_test(cx, |settings| {
3049 settings.defaults.tab_size = NonZeroU32::new(4)
3050 });
3051
3052 let language = Arc::new(
3053 Language::new(
3054 LanguageConfig::default(),
3055 Some(tree_sitter_rust::LANGUAGE.into()),
3056 )
3057 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3058 .unwrap(),
3059 );
3060
3061 let mut cx = EditorTestContext::new(cx).await;
3062 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3063 cx.set_state(indoc! {"
3064 const a: ˇA = (
3065 (ˇ
3066 «const_functionˇ»(ˇ),
3067 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3068 )ˇ
3069 ˇ);ˇ
3070 "});
3071
3072 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3073 cx.assert_editor_state(indoc! {"
3074 ˇ
3075 const a: A = (
3076 ˇ
3077 (
3078 ˇ
3079 ˇ
3080 const_function(),
3081 ˇ
3082 ˇ
3083 ˇ
3084 ˇ
3085 something_else,
3086 ˇ
3087 )
3088 ˇ
3089 ˇ
3090 );
3091 "});
3092}
3093
3094#[gpui::test]
3095async fn test_newline_below(cx: &mut TestAppContext) {
3096 init_test(cx, |settings| {
3097 settings.defaults.tab_size = NonZeroU32::new(4)
3098 });
3099
3100 let language = Arc::new(
3101 Language::new(
3102 LanguageConfig::default(),
3103 Some(tree_sitter_rust::LANGUAGE.into()),
3104 )
3105 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3106 .unwrap(),
3107 );
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3111 cx.set_state(indoc! {"
3112 const a: ˇA = (
3113 (ˇ
3114 «const_functionˇ»(ˇ),
3115 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3116 )ˇ
3117 ˇ);ˇ
3118 "});
3119
3120 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 const a: A = (
3123 ˇ
3124 (
3125 ˇ
3126 const_function(),
3127 ˇ
3128 ˇ
3129 something_else,
3130 ˇ
3131 ˇ
3132 ˇ
3133 ˇ
3134 )
3135 ˇ
3136 );
3137 ˇ
3138 ˇ
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_newline_comments(cx: &mut TestAppContext) {
3144 init_test(cx, |settings| {
3145 settings.defaults.tab_size = NonZeroU32::new(4)
3146 });
3147
3148 let language = Arc::new(Language::new(
3149 LanguageConfig {
3150 line_comments: vec!["// ".into()],
3151 ..LanguageConfig::default()
3152 },
3153 None,
3154 ));
3155 {
3156 let mut cx = EditorTestContext::new(cx).await;
3157 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3158 cx.set_state(indoc! {"
3159 // Fooˇ
3160 "});
3161
3162 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3163 cx.assert_editor_state(indoc! {"
3164 // Foo
3165 // ˇ
3166 "});
3167 // Ensure that we add comment prefix when existing line contains space
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(
3170 indoc! {"
3171 // Foo
3172 //s
3173 // ˇ
3174 "}
3175 .replace("s", " ") // s is used as space placeholder to prevent format on save
3176 .as_str(),
3177 );
3178 // Ensure that we add comment prefix when existing line does not contain space
3179 cx.set_state(indoc! {"
3180 // Foo
3181 //ˇ
3182 "});
3183 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3184 cx.assert_editor_state(indoc! {"
3185 // Foo
3186 //
3187 // ˇ
3188 "});
3189 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3190 cx.set_state(indoc! {"
3191 ˇ// Foo
3192 "});
3193 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3194 cx.assert_editor_state(indoc! {"
3195
3196 ˇ// Foo
3197 "});
3198 }
3199 // Ensure that comment continuations can be disabled.
3200 update_test_language_settings(cx, |settings| {
3201 settings.defaults.extend_comment_on_newline = Some(false);
3202 });
3203 let mut cx = EditorTestContext::new(cx).await;
3204 cx.set_state(indoc! {"
3205 // Fooˇ
3206 "});
3207 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3208 cx.assert_editor_state(indoc! {"
3209 // Foo
3210 ˇ
3211 "});
3212}
3213
3214#[gpui::test]
3215async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3216 init_test(cx, |settings| {
3217 settings.defaults.tab_size = NonZeroU32::new(4)
3218 });
3219
3220 let language = Arc::new(Language::new(
3221 LanguageConfig {
3222 line_comments: vec!["// ".into(), "/// ".into()],
3223 ..LanguageConfig::default()
3224 },
3225 None,
3226 ));
3227 {
3228 let mut cx = EditorTestContext::new(cx).await;
3229 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3230 cx.set_state(indoc! {"
3231 //ˇ
3232 "});
3233 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3234 cx.assert_editor_state(indoc! {"
3235 //
3236 // ˇ
3237 "});
3238
3239 cx.set_state(indoc! {"
3240 ///ˇ
3241 "});
3242 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 ///
3245 /// ˇ
3246 "});
3247 }
3248}
3249
3250#[gpui::test]
3251async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3252 init_test(cx, |settings| {
3253 settings.defaults.tab_size = NonZeroU32::new(4)
3254 });
3255
3256 let language = Arc::new(
3257 Language::new(
3258 LanguageConfig {
3259 documentation_comment: Some(language::BlockCommentConfig {
3260 start: "/**".into(),
3261 end: "*/".into(),
3262 prefix: "* ".into(),
3263 tab_size: 1,
3264 }),
3265
3266 ..LanguageConfig::default()
3267 },
3268 Some(tree_sitter_rust::LANGUAGE.into()),
3269 )
3270 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3271 .unwrap(),
3272 );
3273
3274 {
3275 let mut cx = EditorTestContext::new(cx).await;
3276 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3277 cx.set_state(indoc! {"
3278 /**ˇ
3279 "});
3280
3281 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3282 cx.assert_editor_state(indoc! {"
3283 /**
3284 * ˇ
3285 "});
3286 // Ensure that if cursor is before the comment start,
3287 // we do not actually insert a comment prefix.
3288 cx.set_state(indoc! {"
3289 ˇ/**
3290 "});
3291 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3292 cx.assert_editor_state(indoc! {"
3293
3294 ˇ/**
3295 "});
3296 // Ensure that if cursor is between it doesn't add comment prefix.
3297 cx.set_state(indoc! {"
3298 /*ˇ*
3299 "});
3300 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 /*
3303 ˇ*
3304 "});
3305 // Ensure that if suffix exists on same line after cursor it adds new line.
3306 cx.set_state(indoc! {"
3307 /**ˇ*/
3308 "});
3309 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 /**
3312 * ˇ
3313 */
3314 "});
3315 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3316 cx.set_state(indoc! {"
3317 /**ˇ */
3318 "});
3319 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 /**
3322 * ˇ
3323 */
3324 "});
3325 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3326 cx.set_state(indoc! {"
3327 /** ˇ*/
3328 "});
3329 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3330 cx.assert_editor_state(
3331 indoc! {"
3332 /**s
3333 * ˇ
3334 */
3335 "}
3336 .replace("s", " ") // s is used as space placeholder to prevent format on save
3337 .as_str(),
3338 );
3339 // Ensure that delimiter space is preserved when newline on already
3340 // spaced delimiter.
3341 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3342 cx.assert_editor_state(
3343 indoc! {"
3344 /**s
3345 *s
3346 * ˇ
3347 */
3348 "}
3349 .replace("s", " ") // s is used as space placeholder to prevent format on save
3350 .as_str(),
3351 );
3352 // Ensure that delimiter space is preserved when space is not
3353 // on existing delimiter.
3354 cx.set_state(indoc! {"
3355 /**
3356 *ˇ
3357 */
3358 "});
3359 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3360 cx.assert_editor_state(indoc! {"
3361 /**
3362 *
3363 * ˇ
3364 */
3365 "});
3366 // Ensure that if suffix exists on same line after cursor it
3367 // doesn't add extra new line if prefix is not on same line.
3368 cx.set_state(indoc! {"
3369 /**
3370 ˇ*/
3371 "});
3372 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 /**
3375
3376 ˇ*/
3377 "});
3378 // Ensure that it detects suffix after existing prefix.
3379 cx.set_state(indoc! {"
3380 /**ˇ/
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 /**
3385 ˇ/
3386 "});
3387 // Ensure that if suffix exists on same line before
3388 // cursor it does not add comment prefix.
3389 cx.set_state(indoc! {"
3390 /** */ˇ
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 /** */
3395 ˇ
3396 "});
3397 // Ensure that if suffix exists on same line before
3398 // cursor it does not add comment prefix.
3399 cx.set_state(indoc! {"
3400 /**
3401 *
3402 */ˇ
3403 "});
3404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406 /**
3407 *
3408 */
3409 ˇ
3410 "});
3411
3412 // Ensure that inline comment followed by code
3413 // doesn't add comment prefix on newline
3414 cx.set_state(indoc! {"
3415 /** */ textˇ
3416 "});
3417 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 /** */ text
3420 ˇ
3421 "});
3422
3423 // Ensure that text after comment end tag
3424 // doesn't add comment prefix on newline
3425 cx.set_state(indoc! {"
3426 /**
3427 *
3428 */ˇtext
3429 "});
3430 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 /**
3433 *
3434 */
3435 ˇtext
3436 "});
3437
3438 // Ensure if not comment block it doesn't
3439 // add comment prefix on newline
3440 cx.set_state(indoc! {"
3441 * textˇ
3442 "});
3443 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3444 cx.assert_editor_state(indoc! {"
3445 * text
3446 ˇ
3447 "});
3448 }
3449 // Ensure that comment continuations can be disabled.
3450 update_test_language_settings(cx, |settings| {
3451 settings.defaults.extend_comment_on_newline = Some(false);
3452 });
3453 let mut cx = EditorTestContext::new(cx).await;
3454 cx.set_state(indoc! {"
3455 /**ˇ
3456 "});
3457 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 /**
3460 ˇ
3461 "});
3462}
3463
3464#[gpui::test]
3465async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3466 init_test(cx, |settings| {
3467 settings.defaults.tab_size = NonZeroU32::new(4)
3468 });
3469
3470 let lua_language = Arc::new(Language::new(
3471 LanguageConfig {
3472 line_comments: vec!["--".into()],
3473 block_comment: Some(language::BlockCommentConfig {
3474 start: "--[[".into(),
3475 prefix: "".into(),
3476 end: "]]".into(),
3477 tab_size: 0,
3478 }),
3479 ..LanguageConfig::default()
3480 },
3481 None,
3482 ));
3483
3484 let mut cx = EditorTestContext::new(cx).await;
3485 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3486
3487 // Line with line comment should extend
3488 cx.set_state(indoc! {"
3489 --ˇ
3490 "});
3491 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3492 cx.assert_editor_state(indoc! {"
3493 --
3494 --ˇ
3495 "});
3496
3497 // Line with block comment that matches line comment should not extend
3498 cx.set_state(indoc! {"
3499 --[[ˇ
3500 "});
3501 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3502 cx.assert_editor_state(indoc! {"
3503 --[[
3504 ˇ
3505 "});
3506}
3507
3508#[gpui::test]
3509fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3510 init_test(cx, |_| {});
3511
3512 let editor = cx.add_window(|window, cx| {
3513 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3514 let mut editor = build_editor(buffer, window, cx);
3515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3516 s.select_ranges([3..4, 11..12, 19..20])
3517 });
3518 editor
3519 });
3520
3521 _ = editor.update(cx, |editor, window, cx| {
3522 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3523 editor.buffer.update(cx, |buffer, cx| {
3524 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3525 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3526 });
3527 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3528
3529 editor.insert("Z", window, cx);
3530 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3531
3532 // The selections are moved after the inserted characters
3533 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3534 });
3535}
3536
3537#[gpui::test]
3538async fn test_tab(cx: &mut TestAppContext) {
3539 init_test(cx, |settings| {
3540 settings.defaults.tab_size = NonZeroU32::new(3)
3541 });
3542
3543 let mut cx = EditorTestContext::new(cx).await;
3544 cx.set_state(indoc! {"
3545 ˇabˇc
3546 ˇ🏀ˇ🏀ˇefg
3547 dˇ
3548 "});
3549 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3550 cx.assert_editor_state(indoc! {"
3551 ˇab ˇc
3552 ˇ🏀 ˇ🏀 ˇefg
3553 d ˇ
3554 "});
3555
3556 cx.set_state(indoc! {"
3557 a
3558 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3559 "});
3560 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3561 cx.assert_editor_state(indoc! {"
3562 a
3563 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3564 "});
3565}
3566
3567#[gpui::test]
3568async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3569 init_test(cx, |_| {});
3570
3571 let mut cx = EditorTestContext::new(cx).await;
3572 let language = Arc::new(
3573 Language::new(
3574 LanguageConfig::default(),
3575 Some(tree_sitter_rust::LANGUAGE.into()),
3576 )
3577 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3578 .unwrap(),
3579 );
3580 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3581
3582 // test when all cursors are not at suggested indent
3583 // then simply move to their suggested indent location
3584 cx.set_state(indoc! {"
3585 const a: B = (
3586 c(
3587 ˇ
3588 ˇ )
3589 );
3590 "});
3591 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3592 cx.assert_editor_state(indoc! {"
3593 const a: B = (
3594 c(
3595 ˇ
3596 ˇ)
3597 );
3598 "});
3599
3600 // test cursor already at suggested indent not moving when
3601 // other cursors are yet to reach their suggested indents
3602 cx.set_state(indoc! {"
3603 ˇ
3604 const a: B = (
3605 c(
3606 d(
3607 ˇ
3608 )
3609 ˇ
3610 ˇ )
3611 );
3612 "});
3613 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3614 cx.assert_editor_state(indoc! {"
3615 ˇ
3616 const a: B = (
3617 c(
3618 d(
3619 ˇ
3620 )
3621 ˇ
3622 ˇ)
3623 );
3624 "});
3625 // test when all cursors are at suggested indent then tab is inserted
3626 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3627 cx.assert_editor_state(indoc! {"
3628 ˇ
3629 const a: B = (
3630 c(
3631 d(
3632 ˇ
3633 )
3634 ˇ
3635 ˇ)
3636 );
3637 "});
3638
3639 // test when current indent is less than suggested indent,
3640 // we adjust line to match suggested indent and move cursor to it
3641 //
3642 // when no other cursor is at word boundary, all of them should move
3643 cx.set_state(indoc! {"
3644 const a: B = (
3645 c(
3646 d(
3647 ˇ
3648 ˇ )
3649 ˇ )
3650 );
3651 "});
3652 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3653 cx.assert_editor_state(indoc! {"
3654 const a: B = (
3655 c(
3656 d(
3657 ˇ
3658 ˇ)
3659 ˇ)
3660 );
3661 "});
3662
3663 // test when current indent is less than suggested indent,
3664 // we adjust line to match suggested indent and move cursor to it
3665 //
3666 // when some other cursor is at word boundary, it should not move
3667 cx.set_state(indoc! {"
3668 const a: B = (
3669 c(
3670 d(
3671 ˇ
3672 ˇ )
3673 ˇ)
3674 );
3675 "});
3676 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 const a: B = (
3679 c(
3680 d(
3681 ˇ
3682 ˇ)
3683 ˇ)
3684 );
3685 "});
3686
3687 // test when current indent is more than suggested indent,
3688 // we just move cursor to current indent instead of suggested indent
3689 //
3690 // when no other cursor is at word boundary, all of them should move
3691 cx.set_state(indoc! {"
3692 const a: B = (
3693 c(
3694 d(
3695 ˇ
3696 ˇ )
3697 ˇ )
3698 );
3699 "});
3700 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 const a: B = (
3703 c(
3704 d(
3705 ˇ
3706 ˇ)
3707 ˇ)
3708 );
3709 "});
3710 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3711 cx.assert_editor_state(indoc! {"
3712 const a: B = (
3713 c(
3714 d(
3715 ˇ
3716 ˇ)
3717 ˇ)
3718 );
3719 "});
3720
3721 // test when current indent is more than suggested indent,
3722 // we just move cursor to current indent instead of suggested indent
3723 //
3724 // when some other cursor is at word boundary, it doesn't move
3725 cx.set_state(indoc! {"
3726 const a: B = (
3727 c(
3728 d(
3729 ˇ
3730 ˇ )
3731 ˇ)
3732 );
3733 "});
3734 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3735 cx.assert_editor_state(indoc! {"
3736 const a: B = (
3737 c(
3738 d(
3739 ˇ
3740 ˇ)
3741 ˇ)
3742 );
3743 "});
3744
3745 // handle auto-indent when there are multiple cursors on the same line
3746 cx.set_state(indoc! {"
3747 const a: B = (
3748 c(
3749 ˇ ˇ
3750 ˇ )
3751 );
3752 "});
3753 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3754 cx.assert_editor_state(indoc! {"
3755 const a: B = (
3756 c(
3757 ˇ
3758 ˇ)
3759 );
3760 "});
3761}
3762
3763#[gpui::test]
3764async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3765 init_test(cx, |settings| {
3766 settings.defaults.tab_size = NonZeroU32::new(3)
3767 });
3768
3769 let mut cx = EditorTestContext::new(cx).await;
3770 cx.set_state(indoc! {"
3771 ˇ
3772 \t ˇ
3773 \t ˇ
3774 \t ˇ
3775 \t \t\t \t \t\t \t\t \t \t ˇ
3776 "});
3777
3778 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3779 cx.assert_editor_state(indoc! {"
3780 ˇ
3781 \t ˇ
3782 \t ˇ
3783 \t ˇ
3784 \t \t\t \t \t\t \t\t \t \t ˇ
3785 "});
3786}
3787
3788#[gpui::test]
3789async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3790 init_test(cx, |settings| {
3791 settings.defaults.tab_size = NonZeroU32::new(4)
3792 });
3793
3794 let language = Arc::new(
3795 Language::new(
3796 LanguageConfig::default(),
3797 Some(tree_sitter_rust::LANGUAGE.into()),
3798 )
3799 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3800 .unwrap(),
3801 );
3802
3803 let mut cx = EditorTestContext::new(cx).await;
3804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3805 cx.set_state(indoc! {"
3806 fn a() {
3807 if b {
3808 \t ˇc
3809 }
3810 }
3811 "});
3812
3813 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3814 cx.assert_editor_state(indoc! {"
3815 fn a() {
3816 if b {
3817 ˇc
3818 }
3819 }
3820 "});
3821}
3822
3823#[gpui::test]
3824async fn test_indent_outdent(cx: &mut TestAppContext) {
3825 init_test(cx, |settings| {
3826 settings.defaults.tab_size = NonZeroU32::new(4);
3827 });
3828
3829 let mut cx = EditorTestContext::new(cx).await;
3830
3831 cx.set_state(indoc! {"
3832 «oneˇ» «twoˇ»
3833 three
3834 four
3835 "});
3836 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3837 cx.assert_editor_state(indoc! {"
3838 «oneˇ» «twoˇ»
3839 three
3840 four
3841 "});
3842
3843 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3844 cx.assert_editor_state(indoc! {"
3845 «oneˇ» «twoˇ»
3846 three
3847 four
3848 "});
3849
3850 // select across line ending
3851 cx.set_state(indoc! {"
3852 one two
3853 t«hree
3854 ˇ» four
3855 "});
3856 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3857 cx.assert_editor_state(indoc! {"
3858 one two
3859 t«hree
3860 ˇ» four
3861 "});
3862
3863 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3864 cx.assert_editor_state(indoc! {"
3865 one two
3866 t«hree
3867 ˇ» four
3868 "});
3869
3870 // Ensure that indenting/outdenting works when the cursor is at column 0.
3871 cx.set_state(indoc! {"
3872 one two
3873 ˇthree
3874 four
3875 "});
3876 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3877 cx.assert_editor_state(indoc! {"
3878 one two
3879 ˇthree
3880 four
3881 "});
3882
3883 cx.set_state(indoc! {"
3884 one two
3885 ˇ three
3886 four
3887 "});
3888 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 one two
3891 ˇthree
3892 four
3893 "});
3894}
3895
3896#[gpui::test]
3897async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3898 // This is a regression test for issue #33761
3899 init_test(cx, |_| {});
3900
3901 let mut cx = EditorTestContext::new(cx).await;
3902 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3903 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3904
3905 cx.set_state(
3906 r#"ˇ# ingress:
3907ˇ# api:
3908ˇ# enabled: false
3909ˇ# pathType: Prefix
3910ˇ# console:
3911ˇ# enabled: false
3912ˇ# pathType: Prefix
3913"#,
3914 );
3915
3916 // Press tab to indent all lines
3917 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3918
3919 cx.assert_editor_state(
3920 r#" ˇ# ingress:
3921 ˇ# api:
3922 ˇ# enabled: false
3923 ˇ# pathType: Prefix
3924 ˇ# console:
3925 ˇ# enabled: false
3926 ˇ# pathType: Prefix
3927"#,
3928 );
3929}
3930
3931#[gpui::test]
3932async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3933 // This is a test to make sure our fix for issue #33761 didn't break anything
3934 init_test(cx, |_| {});
3935
3936 let mut cx = EditorTestContext::new(cx).await;
3937 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3938 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3939
3940 cx.set_state(
3941 r#"ˇingress:
3942ˇ api:
3943ˇ enabled: false
3944ˇ pathType: Prefix
3945"#,
3946 );
3947
3948 // Press tab to indent all lines
3949 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3950
3951 cx.assert_editor_state(
3952 r#"ˇingress:
3953 ˇapi:
3954 ˇenabled: false
3955 ˇpathType: Prefix
3956"#,
3957 );
3958}
3959
3960#[gpui::test]
3961async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3962 init_test(cx, |settings| {
3963 settings.defaults.hard_tabs = Some(true);
3964 });
3965
3966 let mut cx = EditorTestContext::new(cx).await;
3967
3968 // select two ranges on one line
3969 cx.set_state(indoc! {"
3970 «oneˇ» «twoˇ»
3971 three
3972 four
3973 "});
3974 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3975 cx.assert_editor_state(indoc! {"
3976 \t«oneˇ» «twoˇ»
3977 three
3978 four
3979 "});
3980 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3981 cx.assert_editor_state(indoc! {"
3982 \t\t«oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 \t«oneˇ» «twoˇ»
3989 three
3990 four
3991 "});
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 «oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998
3999 // select across a line ending
4000 cx.set_state(indoc! {"
4001 one two
4002 t«hree
4003 ˇ»four
4004 "});
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 one two
4008 \tt«hree
4009 ˇ»four
4010 "});
4011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4012 cx.assert_editor_state(indoc! {"
4013 one two
4014 \t\tt«hree
4015 ˇ»four
4016 "});
4017 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 one two
4020 \tt«hree
4021 ˇ»four
4022 "});
4023 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4024 cx.assert_editor_state(indoc! {"
4025 one two
4026 t«hree
4027 ˇ»four
4028 "});
4029
4030 // Ensure that indenting/outdenting works when the cursor is at column 0.
4031 cx.set_state(indoc! {"
4032 one two
4033 ˇthree
4034 four
4035 "});
4036 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4037 cx.assert_editor_state(indoc! {"
4038 one two
4039 ˇthree
4040 four
4041 "});
4042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4043 cx.assert_editor_state(indoc! {"
4044 one two
4045 \tˇthree
4046 four
4047 "});
4048 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 ˇthree
4052 four
4053 "});
4054}
4055
4056#[gpui::test]
4057fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4058 init_test(cx, |settings| {
4059 settings.languages.0.extend([
4060 (
4061 "TOML".into(),
4062 LanguageSettingsContent {
4063 tab_size: NonZeroU32::new(2),
4064 ..Default::default()
4065 },
4066 ),
4067 (
4068 "Rust".into(),
4069 LanguageSettingsContent {
4070 tab_size: NonZeroU32::new(4),
4071 ..Default::default()
4072 },
4073 ),
4074 ]);
4075 });
4076
4077 let toml_language = Arc::new(Language::new(
4078 LanguageConfig {
4079 name: "TOML".into(),
4080 ..Default::default()
4081 },
4082 None,
4083 ));
4084 let rust_language = Arc::new(Language::new(
4085 LanguageConfig {
4086 name: "Rust".into(),
4087 ..Default::default()
4088 },
4089 None,
4090 ));
4091
4092 let toml_buffer =
4093 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4094 let rust_buffer =
4095 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4096 let multibuffer = cx.new(|cx| {
4097 let mut multibuffer = MultiBuffer::new(ReadWrite);
4098 multibuffer.push_excerpts(
4099 toml_buffer.clone(),
4100 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4101 cx,
4102 );
4103 multibuffer.push_excerpts(
4104 rust_buffer.clone(),
4105 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4106 cx,
4107 );
4108 multibuffer
4109 });
4110
4111 cx.add_window(|window, cx| {
4112 let mut editor = build_editor(multibuffer, window, cx);
4113
4114 assert_eq!(
4115 editor.text(cx),
4116 indoc! {"
4117 a = 1
4118 b = 2
4119
4120 const c: usize = 3;
4121 "}
4122 );
4123
4124 select_ranges(
4125 &mut editor,
4126 indoc! {"
4127 «aˇ» = 1
4128 b = 2
4129
4130 «const c:ˇ» usize = 3;
4131 "},
4132 window,
4133 cx,
4134 );
4135
4136 editor.tab(&Tab, window, cx);
4137 assert_text_with_selections(
4138 &mut editor,
4139 indoc! {"
4140 «aˇ» = 1
4141 b = 2
4142
4143 «const c:ˇ» usize = 3;
4144 "},
4145 cx,
4146 );
4147 editor.backtab(&Backtab, window, cx);
4148 assert_text_with_selections(
4149 &mut editor,
4150 indoc! {"
4151 «aˇ» = 1
4152 b = 2
4153
4154 «const c:ˇ» usize = 3;
4155 "},
4156 cx,
4157 );
4158
4159 editor
4160 });
4161}
4162
4163#[gpui::test]
4164async fn test_backspace(cx: &mut TestAppContext) {
4165 init_test(cx, |_| {});
4166
4167 let mut cx = EditorTestContext::new(cx).await;
4168
4169 // Basic backspace
4170 cx.set_state(indoc! {"
4171 onˇe two three
4172 fou«rˇ» five six
4173 seven «ˇeight nine
4174 »ten
4175 "});
4176 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4177 cx.assert_editor_state(indoc! {"
4178 oˇe two three
4179 fouˇ five six
4180 seven ˇten
4181 "});
4182
4183 // Test backspace inside and around indents
4184 cx.set_state(indoc! {"
4185 zero
4186 ˇone
4187 ˇtwo
4188 ˇ ˇ ˇ three
4189 ˇ ˇ four
4190 "});
4191 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4192 cx.assert_editor_state(indoc! {"
4193 zero
4194 ˇone
4195 ˇtwo
4196 ˇ threeˇ four
4197 "});
4198}
4199
4200#[gpui::test]
4201async fn test_delete(cx: &mut TestAppContext) {
4202 init_test(cx, |_| {});
4203
4204 let mut cx = EditorTestContext::new(cx).await;
4205 cx.set_state(indoc! {"
4206 onˇe two three
4207 fou«rˇ» five six
4208 seven «ˇeight nine
4209 »ten
4210 "});
4211 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4212 cx.assert_editor_state(indoc! {"
4213 onˇ two three
4214 fouˇ five six
4215 seven ˇten
4216 "});
4217}
4218
4219#[gpui::test]
4220fn test_delete_line(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let editor = cx.add_window(|window, cx| {
4224 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4225 build_editor(buffer, window, cx)
4226 });
4227 _ = editor.update(cx, |editor, window, cx| {
4228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4229 s.select_display_ranges([
4230 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4231 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4232 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4233 ])
4234 });
4235 editor.delete_line(&DeleteLine, window, cx);
4236 assert_eq!(editor.display_text(cx), "ghi");
4237 assert_eq!(
4238 editor.selections.display_ranges(cx),
4239 vec![
4240 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4241 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4242 ]
4243 );
4244 });
4245
4246 let editor = cx.add_window(|window, cx| {
4247 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4248 build_editor(buffer, window, cx)
4249 });
4250 _ = editor.update(cx, |editor, window, cx| {
4251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4252 s.select_display_ranges([
4253 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4254 ])
4255 });
4256 editor.delete_line(&DeleteLine, window, cx);
4257 assert_eq!(editor.display_text(cx), "ghi\n");
4258 assert_eq!(
4259 editor.selections.display_ranges(cx),
4260 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4261 );
4262 });
4263}
4264
4265#[gpui::test]
4266fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4267 init_test(cx, |_| {});
4268
4269 cx.add_window(|window, cx| {
4270 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4271 let mut editor = build_editor(buffer.clone(), window, cx);
4272 let buffer = buffer.read(cx).as_singleton().unwrap();
4273
4274 assert_eq!(
4275 editor.selections.ranges::<Point>(cx),
4276 &[Point::new(0, 0)..Point::new(0, 0)]
4277 );
4278
4279 // When on single line, replace newline at end by space
4280 editor.join_lines(&JoinLines, window, cx);
4281 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4282 assert_eq!(
4283 editor.selections.ranges::<Point>(cx),
4284 &[Point::new(0, 3)..Point::new(0, 3)]
4285 );
4286
4287 // When multiple lines are selected, remove newlines that are spanned by the selection
4288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4289 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4290 });
4291 editor.join_lines(&JoinLines, window, cx);
4292 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4293 assert_eq!(
4294 editor.selections.ranges::<Point>(cx),
4295 &[Point::new(0, 11)..Point::new(0, 11)]
4296 );
4297
4298 // Undo should be transactional
4299 editor.undo(&Undo, window, cx);
4300 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4301 assert_eq!(
4302 editor.selections.ranges::<Point>(cx),
4303 &[Point::new(0, 5)..Point::new(2, 2)]
4304 );
4305
4306 // When joining an empty line don't insert a space
4307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4308 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4309 });
4310 editor.join_lines(&JoinLines, window, cx);
4311 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4312 assert_eq!(
4313 editor.selections.ranges::<Point>(cx),
4314 [Point::new(2, 3)..Point::new(2, 3)]
4315 );
4316
4317 // We can remove trailing newlines
4318 editor.join_lines(&JoinLines, window, cx);
4319 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4320 assert_eq!(
4321 editor.selections.ranges::<Point>(cx),
4322 [Point::new(2, 3)..Point::new(2, 3)]
4323 );
4324
4325 // We don't blow up on the last line
4326 editor.join_lines(&JoinLines, window, cx);
4327 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4328 assert_eq!(
4329 editor.selections.ranges::<Point>(cx),
4330 [Point::new(2, 3)..Point::new(2, 3)]
4331 );
4332
4333 // reset to test indentation
4334 editor.buffer.update(cx, |buffer, cx| {
4335 buffer.edit(
4336 [
4337 (Point::new(1, 0)..Point::new(1, 2), " "),
4338 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4339 ],
4340 None,
4341 cx,
4342 )
4343 });
4344
4345 // We remove any leading spaces
4346 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4348 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4349 });
4350 editor.join_lines(&JoinLines, window, cx);
4351 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4352
4353 // We don't insert a space for a line containing only spaces
4354 editor.join_lines(&JoinLines, window, cx);
4355 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4356
4357 // We ignore any leading tabs
4358 editor.join_lines(&JoinLines, window, cx);
4359 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4360
4361 editor
4362 });
4363}
4364
4365#[gpui::test]
4366fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4367 init_test(cx, |_| {});
4368
4369 cx.add_window(|window, cx| {
4370 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4371 let mut editor = build_editor(buffer.clone(), window, cx);
4372 let buffer = buffer.read(cx).as_singleton().unwrap();
4373
4374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4375 s.select_ranges([
4376 Point::new(0, 2)..Point::new(1, 1),
4377 Point::new(1, 2)..Point::new(1, 2),
4378 Point::new(3, 1)..Point::new(3, 2),
4379 ])
4380 });
4381
4382 editor.join_lines(&JoinLines, window, cx);
4383 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4384
4385 assert_eq!(
4386 editor.selections.ranges::<Point>(cx),
4387 [
4388 Point::new(0, 7)..Point::new(0, 7),
4389 Point::new(1, 3)..Point::new(1, 3)
4390 ]
4391 );
4392 editor
4393 });
4394}
4395
4396#[gpui::test]
4397async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4398 init_test(cx, |_| {});
4399
4400 let mut cx = EditorTestContext::new(cx).await;
4401
4402 let diff_base = r#"
4403 Line 0
4404 Line 1
4405 Line 2
4406 Line 3
4407 "#
4408 .unindent();
4409
4410 cx.set_state(
4411 &r#"
4412 ˇLine 0
4413 Line 1
4414 Line 2
4415 Line 3
4416 "#
4417 .unindent(),
4418 );
4419
4420 cx.set_head_text(&diff_base);
4421 executor.run_until_parked();
4422
4423 // Join lines
4424 cx.update_editor(|editor, window, cx| {
4425 editor.join_lines(&JoinLines, window, cx);
4426 });
4427 executor.run_until_parked();
4428
4429 cx.assert_editor_state(
4430 &r#"
4431 Line 0ˇ Line 1
4432 Line 2
4433 Line 3
4434 "#
4435 .unindent(),
4436 );
4437 // Join again
4438 cx.update_editor(|editor, window, cx| {
4439 editor.join_lines(&JoinLines, window, cx);
4440 });
4441 executor.run_until_parked();
4442
4443 cx.assert_editor_state(
4444 &r#"
4445 Line 0 Line 1ˇ Line 2
4446 Line 3
4447 "#
4448 .unindent(),
4449 );
4450}
4451
4452#[gpui::test]
4453async fn test_custom_newlines_cause_no_false_positive_diffs(
4454 executor: BackgroundExecutor,
4455 cx: &mut TestAppContext,
4456) {
4457 init_test(cx, |_| {});
4458 let mut cx = EditorTestContext::new(cx).await;
4459 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4460 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4461 executor.run_until_parked();
4462
4463 cx.update_editor(|editor, window, cx| {
4464 let snapshot = editor.snapshot(window, cx);
4465 assert_eq!(
4466 snapshot
4467 .buffer_snapshot
4468 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4469 .collect::<Vec<_>>(),
4470 Vec::new(),
4471 "Should not have any diffs for files with custom newlines"
4472 );
4473 });
4474}
4475
4476#[gpui::test]
4477async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4478 init_test(cx, |_| {});
4479
4480 let mut cx = EditorTestContext::new(cx).await;
4481
4482 // Test sort_lines_case_insensitive()
4483 cx.set_state(indoc! {"
4484 «z
4485 y
4486 x
4487 Z
4488 Y
4489 Xˇ»
4490 "});
4491 cx.update_editor(|e, window, cx| {
4492 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4493 });
4494 cx.assert_editor_state(indoc! {"
4495 «x
4496 X
4497 y
4498 Y
4499 z
4500 Zˇ»
4501 "});
4502
4503 // Test sort_lines_by_length()
4504 //
4505 // Demonstrates:
4506 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4507 // - sort is stable
4508 cx.set_state(indoc! {"
4509 «123
4510 æ
4511 12
4512 ∞
4513 1
4514 æˇ»
4515 "});
4516 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4517 cx.assert_editor_state(indoc! {"
4518 «æ
4519 ∞
4520 1
4521 æ
4522 12
4523 123ˇ»
4524 "});
4525
4526 // Test reverse_lines()
4527 cx.set_state(indoc! {"
4528 «5
4529 4
4530 3
4531 2
4532 1ˇ»
4533 "});
4534 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4535 cx.assert_editor_state(indoc! {"
4536 «1
4537 2
4538 3
4539 4
4540 5ˇ»
4541 "});
4542
4543 // Skip testing shuffle_line()
4544
4545 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4546 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4547
4548 // Don't manipulate when cursor is on single line, but expand the selection
4549 cx.set_state(indoc! {"
4550 ddˇdd
4551 ccc
4552 bb
4553 a
4554 "});
4555 cx.update_editor(|e, window, cx| {
4556 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4557 });
4558 cx.assert_editor_state(indoc! {"
4559 «ddddˇ»
4560 ccc
4561 bb
4562 a
4563 "});
4564
4565 // Basic manipulate case
4566 // Start selection moves to column 0
4567 // End of selection shrinks to fit shorter line
4568 cx.set_state(indoc! {"
4569 dd«d
4570 ccc
4571 bb
4572 aaaaaˇ»
4573 "});
4574 cx.update_editor(|e, window, cx| {
4575 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4576 });
4577 cx.assert_editor_state(indoc! {"
4578 «aaaaa
4579 bb
4580 ccc
4581 dddˇ»
4582 "});
4583
4584 // Manipulate case with newlines
4585 cx.set_state(indoc! {"
4586 dd«d
4587 ccc
4588
4589 bb
4590 aaaaa
4591
4592 ˇ»
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599
4600 aaaaa
4601 bb
4602 ccc
4603 dddˇ»
4604
4605 "});
4606
4607 // Adding new line
4608 cx.set_state(indoc! {"
4609 aa«a
4610 bbˇ»b
4611 "});
4612 cx.update_editor(|e, window, cx| {
4613 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4614 });
4615 cx.assert_editor_state(indoc! {"
4616 «aaa
4617 bbb
4618 added_lineˇ»
4619 "});
4620
4621 // Removing line
4622 cx.set_state(indoc! {"
4623 aa«a
4624 bbbˇ»
4625 "});
4626 cx.update_editor(|e, window, cx| {
4627 e.manipulate_immutable_lines(window, cx, |lines| {
4628 lines.pop();
4629 })
4630 });
4631 cx.assert_editor_state(indoc! {"
4632 «aaaˇ»
4633 "});
4634
4635 // Removing all lines
4636 cx.set_state(indoc! {"
4637 aa«a
4638 bbbˇ»
4639 "});
4640 cx.update_editor(|e, window, cx| {
4641 e.manipulate_immutable_lines(window, cx, |lines| {
4642 lines.drain(..);
4643 })
4644 });
4645 cx.assert_editor_state(indoc! {"
4646 ˇ
4647 "});
4648}
4649
4650#[gpui::test]
4651async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4652 init_test(cx, |_| {});
4653
4654 let mut cx = EditorTestContext::new(cx).await;
4655
4656 // Consider continuous selection as single selection
4657 cx.set_state(indoc! {"
4658 Aaa«aa
4659 cˇ»c«c
4660 bb
4661 aaaˇ»aa
4662 "});
4663 cx.update_editor(|e, window, cx| {
4664 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4665 });
4666 cx.assert_editor_state(indoc! {"
4667 «Aaaaa
4668 ccc
4669 bb
4670 aaaaaˇ»
4671 "});
4672
4673 cx.set_state(indoc! {"
4674 Aaa«aa
4675 cˇ»c«c
4676 bb
4677 aaaˇ»aa
4678 "});
4679 cx.update_editor(|e, window, cx| {
4680 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4681 });
4682 cx.assert_editor_state(indoc! {"
4683 «Aaaaa
4684 ccc
4685 bbˇ»
4686 "});
4687
4688 // Consider non continuous selection as distinct dedup operations
4689 cx.set_state(indoc! {"
4690 «aaaaa
4691 bb
4692 aaaaa
4693 aaaaaˇ»
4694
4695 aaa«aaˇ»
4696 "});
4697 cx.update_editor(|e, window, cx| {
4698 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4699 });
4700 cx.assert_editor_state(indoc! {"
4701 «aaaaa
4702 bbˇ»
4703
4704 «aaaaaˇ»
4705 "});
4706}
4707
4708#[gpui::test]
4709async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4710 init_test(cx, |_| {});
4711
4712 let mut cx = EditorTestContext::new(cx).await;
4713
4714 cx.set_state(indoc! {"
4715 «Aaa
4716 aAa
4717 Aaaˇ»
4718 "});
4719 cx.update_editor(|e, window, cx| {
4720 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4721 });
4722 cx.assert_editor_state(indoc! {"
4723 «Aaa
4724 aAaˇ»
4725 "});
4726
4727 cx.set_state(indoc! {"
4728 «Aaa
4729 aAa
4730 aaAˇ»
4731 "});
4732 cx.update_editor(|e, window, cx| {
4733 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4734 });
4735 cx.assert_editor_state(indoc! {"
4736 «Aaaˇ»
4737 "});
4738}
4739
4740#[gpui::test]
4741async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4742 init_test(cx, |_| {});
4743
4744 let mut cx = EditorTestContext::new(cx).await;
4745
4746 let js_language = Arc::new(Language::new(
4747 LanguageConfig {
4748 name: "JavaScript".into(),
4749 wrap_characters: Some(language::WrapCharactersConfig {
4750 start_prefix: "<".into(),
4751 start_suffix: ">".into(),
4752 end_prefix: "</".into(),
4753 end_suffix: ">".into(),
4754 }),
4755 ..LanguageConfig::default()
4756 },
4757 None,
4758 ));
4759
4760 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4761
4762 cx.set_state(indoc! {"
4763 «testˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4766 cx.assert_editor_state(indoc! {"
4767 <«ˇ»>test</«ˇ»>
4768 "});
4769
4770 cx.set_state(indoc! {"
4771 «test
4772 testˇ»
4773 "});
4774 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4775 cx.assert_editor_state(indoc! {"
4776 <«ˇ»>test
4777 test</«ˇ»>
4778 "});
4779
4780 cx.set_state(indoc! {"
4781 teˇst
4782 "});
4783 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4784 cx.assert_editor_state(indoc! {"
4785 te<«ˇ»></«ˇ»>st
4786 "});
4787}
4788
4789#[gpui::test]
4790async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4791 init_test(cx, |_| {});
4792
4793 let mut cx = EditorTestContext::new(cx).await;
4794
4795 let js_language = Arc::new(Language::new(
4796 LanguageConfig {
4797 name: "JavaScript".into(),
4798 wrap_characters: Some(language::WrapCharactersConfig {
4799 start_prefix: "<".into(),
4800 start_suffix: ">".into(),
4801 end_prefix: "</".into(),
4802 end_suffix: ">".into(),
4803 }),
4804 ..LanguageConfig::default()
4805 },
4806 None,
4807 ));
4808
4809 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4810
4811 cx.set_state(indoc! {"
4812 «testˇ»
4813 «testˇ» «testˇ»
4814 «testˇ»
4815 "});
4816 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4817 cx.assert_editor_state(indoc! {"
4818 <«ˇ»>test</«ˇ»>
4819 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4820 <«ˇ»>test</«ˇ»>
4821 "});
4822
4823 cx.set_state(indoc! {"
4824 «test
4825 testˇ»
4826 «test
4827 testˇ»
4828 "});
4829 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4830 cx.assert_editor_state(indoc! {"
4831 <«ˇ»>test
4832 test</«ˇ»>
4833 <«ˇ»>test
4834 test</«ˇ»>
4835 "});
4836}
4837
4838#[gpui::test]
4839async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4840 init_test(cx, |_| {});
4841
4842 let mut cx = EditorTestContext::new(cx).await;
4843
4844 let plaintext_language = Arc::new(Language::new(
4845 LanguageConfig {
4846 name: "Plain Text".into(),
4847 ..LanguageConfig::default()
4848 },
4849 None,
4850 ));
4851
4852 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4853
4854 cx.set_state(indoc! {"
4855 «testˇ»
4856 "});
4857 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4858 cx.assert_editor_state(indoc! {"
4859 «testˇ»
4860 "});
4861}
4862
4863#[gpui::test]
4864async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4865 init_test(cx, |_| {});
4866
4867 let mut cx = EditorTestContext::new(cx).await;
4868
4869 // Manipulate with multiple selections on a single line
4870 cx.set_state(indoc! {"
4871 dd«dd
4872 cˇ»c«c
4873 bb
4874 aaaˇ»aa
4875 "});
4876 cx.update_editor(|e, window, cx| {
4877 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4878 });
4879 cx.assert_editor_state(indoc! {"
4880 «aaaaa
4881 bb
4882 ccc
4883 ddddˇ»
4884 "});
4885
4886 // Manipulate with multiple disjoin selections
4887 cx.set_state(indoc! {"
4888 5«
4889 4
4890 3
4891 2
4892 1ˇ»
4893
4894 dd«dd
4895 ccc
4896 bb
4897 aaaˇ»aa
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «1
4904 2
4905 3
4906 4
4907 5ˇ»
4908
4909 «aaaaa
4910 bb
4911 ccc
4912 ddddˇ»
4913 "});
4914
4915 // Adding lines on each selection
4916 cx.set_state(indoc! {"
4917 2«
4918 1ˇ»
4919
4920 bb«bb
4921 aaaˇ»aa
4922 "});
4923 cx.update_editor(|e, window, cx| {
4924 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4925 });
4926 cx.assert_editor_state(indoc! {"
4927 «2
4928 1
4929 added lineˇ»
4930
4931 «bbbb
4932 aaaaa
4933 added lineˇ»
4934 "});
4935
4936 // Removing lines on each selection
4937 cx.set_state(indoc! {"
4938 2«
4939 1ˇ»
4940
4941 bb«bb
4942 aaaˇ»aa
4943 "});
4944 cx.update_editor(|e, window, cx| {
4945 e.manipulate_immutable_lines(window, cx, |lines| {
4946 lines.pop();
4947 })
4948 });
4949 cx.assert_editor_state(indoc! {"
4950 «2ˇ»
4951
4952 «bbbbˇ»
4953 "});
4954}
4955
4956#[gpui::test]
4957async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4958 init_test(cx, |settings| {
4959 settings.defaults.tab_size = NonZeroU32::new(3)
4960 });
4961
4962 let mut cx = EditorTestContext::new(cx).await;
4963
4964 // MULTI SELECTION
4965 // Ln.1 "«" tests empty lines
4966 // Ln.9 tests just leading whitespace
4967 cx.set_state(indoc! {"
4968 «
4969 abc // No indentationˇ»
4970 «\tabc // 1 tabˇ»
4971 \t\tabc « ˇ» // 2 tabs
4972 \t ab«c // Tab followed by space
4973 \tabc // Space followed by tab (3 spaces should be the result)
4974 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4975 abˇ»ˇc ˇ ˇ // Already space indented«
4976 \t
4977 \tabc\tdef // Only the leading tab is manipulatedˇ»
4978 "});
4979 cx.update_editor(|e, window, cx| {
4980 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4981 });
4982 cx.assert_editor_state(
4983 indoc! {"
4984 «
4985 abc // No indentation
4986 abc // 1 tab
4987 abc // 2 tabs
4988 abc // Tab followed by space
4989 abc // Space followed by tab (3 spaces should be the result)
4990 abc // Mixed indentation (tab conversion depends on the column)
4991 abc // Already space indented
4992 ·
4993 abc\tdef // Only the leading tab is manipulatedˇ»
4994 "}
4995 .replace("·", "")
4996 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4997 );
4998
4999 // Test on just a few lines, the others should remain unchanged
5000 // Only lines (3, 5, 10, 11) should change
5001 cx.set_state(
5002 indoc! {"
5003 ·
5004 abc // No indentation
5005 \tabcˇ // 1 tab
5006 \t\tabc // 2 tabs
5007 \t abcˇ // Tab followed by space
5008 \tabc // Space followed by tab (3 spaces should be the result)
5009 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5010 abc // Already space indented
5011 «\t
5012 \tabc\tdef // Only the leading tab is manipulatedˇ»
5013 "}
5014 .replace("·", "")
5015 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5016 );
5017 cx.update_editor(|e, window, cx| {
5018 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5019 });
5020 cx.assert_editor_state(
5021 indoc! {"
5022 ·
5023 abc // No indentation
5024 « abc // 1 tabˇ»
5025 \t\tabc // 2 tabs
5026 « abc // Tab followed by spaceˇ»
5027 \tabc // Space followed by tab (3 spaces should be the result)
5028 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5029 abc // Already space indented
5030 « ·
5031 abc\tdef // Only the leading tab is manipulatedˇ»
5032 "}
5033 .replace("·", "")
5034 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5035 );
5036
5037 // SINGLE SELECTION
5038 // Ln.1 "«" tests empty lines
5039 // Ln.9 tests just leading whitespace
5040 cx.set_state(indoc! {"
5041 «
5042 abc // No indentation
5043 \tabc // 1 tab
5044 \t\tabc // 2 tabs
5045 \t abc // Tab followed by space
5046 \tabc // Space followed by tab (3 spaces should be the result)
5047 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5048 abc // Already space indented
5049 \t
5050 \tabc\tdef // Only the leading tab is manipulatedˇ»
5051 "});
5052 cx.update_editor(|e, window, cx| {
5053 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5054 });
5055 cx.assert_editor_state(
5056 indoc! {"
5057 «
5058 abc // No indentation
5059 abc // 1 tab
5060 abc // 2 tabs
5061 abc // Tab followed by space
5062 abc // Space followed by tab (3 spaces should be the result)
5063 abc // Mixed indentation (tab conversion depends on the column)
5064 abc // Already space indented
5065 ·
5066 abc\tdef // Only the leading tab is manipulatedˇ»
5067 "}
5068 .replace("·", "")
5069 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5070 );
5071}
5072
5073#[gpui::test]
5074async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5075 init_test(cx, |settings| {
5076 settings.defaults.tab_size = NonZeroU32::new(3)
5077 });
5078
5079 let mut cx = EditorTestContext::new(cx).await;
5080
5081 // MULTI SELECTION
5082 // Ln.1 "«" tests empty lines
5083 // Ln.11 tests just leading whitespace
5084 cx.set_state(indoc! {"
5085 «
5086 abˇ»ˇc // No indentation
5087 abc ˇ ˇ // 1 space (< 3 so dont convert)
5088 abc « // 2 spaces (< 3 so dont convert)
5089 abc // 3 spaces (convert)
5090 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5091 «\tˇ»\t«\tˇ»abc // Already tab indented
5092 «\t abc // Tab followed by space
5093 \tabc // Space followed by tab (should be consumed due to tab)
5094 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5095 \tˇ» «\t
5096 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5097 "});
5098 cx.update_editor(|e, window, cx| {
5099 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5100 });
5101 cx.assert_editor_state(indoc! {"
5102 «
5103 abc // No indentation
5104 abc // 1 space (< 3 so dont convert)
5105 abc // 2 spaces (< 3 so dont convert)
5106 \tabc // 3 spaces (convert)
5107 \t abc // 5 spaces (1 tab + 2 spaces)
5108 \t\t\tabc // Already tab indented
5109 \t abc // Tab followed by space
5110 \tabc // Space followed by tab (should be consumed due to tab)
5111 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5112 \t\t\t
5113 \tabc \t // Only the leading spaces should be convertedˇ»
5114 "});
5115
5116 // Test on just a few lines, the other should remain unchanged
5117 // Only lines (4, 8, 11, 12) should change
5118 cx.set_state(
5119 indoc! {"
5120 ·
5121 abc // No indentation
5122 abc // 1 space (< 3 so dont convert)
5123 abc // 2 spaces (< 3 so dont convert)
5124 « abc // 3 spaces (convert)ˇ»
5125 abc // 5 spaces (1 tab + 2 spaces)
5126 \t\t\tabc // Already tab indented
5127 \t abc // Tab followed by space
5128 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5129 \t\t \tabc // Mixed indentation
5130 \t \t \t \tabc // Mixed indentation
5131 \t \tˇ
5132 « abc \t // Only the leading spaces should be convertedˇ»
5133 "}
5134 .replace("·", "")
5135 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5136 );
5137 cx.update_editor(|e, window, cx| {
5138 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5139 });
5140 cx.assert_editor_state(
5141 indoc! {"
5142 ·
5143 abc // No indentation
5144 abc // 1 space (< 3 so dont convert)
5145 abc // 2 spaces (< 3 so dont convert)
5146 «\tabc // 3 spaces (convert)ˇ»
5147 abc // 5 spaces (1 tab + 2 spaces)
5148 \t\t\tabc // Already tab indented
5149 \t abc // Tab followed by space
5150 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5151 \t\t \tabc // Mixed indentation
5152 \t \t \t \tabc // Mixed indentation
5153 «\t\t\t
5154 \tabc \t // Only the leading spaces should be convertedˇ»
5155 "}
5156 .replace("·", "")
5157 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5158 );
5159
5160 // SINGLE SELECTION
5161 // Ln.1 "«" tests empty lines
5162 // Ln.11 tests just leading whitespace
5163 cx.set_state(indoc! {"
5164 «
5165 abc // No indentation
5166 abc // 1 space (< 3 so dont convert)
5167 abc // 2 spaces (< 3 so dont convert)
5168 abc // 3 spaces (convert)
5169 abc // 5 spaces (1 tab + 2 spaces)
5170 \t\t\tabc // Already tab indented
5171 \t abc // Tab followed by space
5172 \tabc // Space followed by tab (should be consumed due to tab)
5173 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5174 \t \t
5175 abc \t // Only the leading spaces should be convertedˇ»
5176 "});
5177 cx.update_editor(|e, window, cx| {
5178 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5179 });
5180 cx.assert_editor_state(indoc! {"
5181 «
5182 abc // No indentation
5183 abc // 1 space (< 3 so dont convert)
5184 abc // 2 spaces (< 3 so dont convert)
5185 \tabc // 3 spaces (convert)
5186 \t abc // 5 spaces (1 tab + 2 spaces)
5187 \t\t\tabc // Already tab indented
5188 \t abc // Tab followed by space
5189 \tabc // Space followed by tab (should be consumed due to tab)
5190 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5191 \t\t\t
5192 \tabc \t // Only the leading spaces should be convertedˇ»
5193 "});
5194}
5195
5196#[gpui::test]
5197async fn test_toggle_case(cx: &mut TestAppContext) {
5198 init_test(cx, |_| {});
5199
5200 let mut cx = EditorTestContext::new(cx).await;
5201
5202 // If all lower case -> upper case
5203 cx.set_state(indoc! {"
5204 «hello worldˇ»
5205 "});
5206 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5207 cx.assert_editor_state(indoc! {"
5208 «HELLO WORLDˇ»
5209 "});
5210
5211 // If all upper case -> lower case
5212 cx.set_state(indoc! {"
5213 «HELLO WORLDˇ»
5214 "});
5215 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5216 cx.assert_editor_state(indoc! {"
5217 «hello worldˇ»
5218 "});
5219
5220 // If any upper case characters are identified -> lower case
5221 // This matches JetBrains IDEs
5222 cx.set_state(indoc! {"
5223 «hEllo worldˇ»
5224 "});
5225 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5226 cx.assert_editor_state(indoc! {"
5227 «hello worldˇ»
5228 "});
5229}
5230
5231#[gpui::test]
5232async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5233 init_test(cx, |_| {});
5234
5235 let mut cx = EditorTestContext::new(cx).await;
5236
5237 cx.set_state(indoc! {"
5238 «implement-windows-supportˇ»
5239 "});
5240 cx.update_editor(|e, window, cx| {
5241 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5242 });
5243 cx.assert_editor_state(indoc! {"
5244 «Implement windows supportˇ»
5245 "});
5246}
5247
5248#[gpui::test]
5249async fn test_manipulate_text(cx: &mut TestAppContext) {
5250 init_test(cx, |_| {});
5251
5252 let mut cx = EditorTestContext::new(cx).await;
5253
5254 // Test convert_to_upper_case()
5255 cx.set_state(indoc! {"
5256 «hello worldˇ»
5257 "});
5258 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5259 cx.assert_editor_state(indoc! {"
5260 «HELLO WORLDˇ»
5261 "});
5262
5263 // Test convert_to_lower_case()
5264 cx.set_state(indoc! {"
5265 «HELLO WORLDˇ»
5266 "});
5267 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5268 cx.assert_editor_state(indoc! {"
5269 «hello worldˇ»
5270 "});
5271
5272 // Test multiple line, single selection case
5273 cx.set_state(indoc! {"
5274 «The quick brown
5275 fox jumps over
5276 the lazy dogˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5279 cx.assert_editor_state(indoc! {"
5280 «The Quick Brown
5281 Fox Jumps Over
5282 The Lazy Dogˇ»
5283 "});
5284
5285 // Test multiple line, single selection case
5286 cx.set_state(indoc! {"
5287 «The quick brown
5288 fox jumps over
5289 the lazy dogˇ»
5290 "});
5291 cx.update_editor(|e, window, cx| {
5292 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5293 });
5294 cx.assert_editor_state(indoc! {"
5295 «TheQuickBrown
5296 FoxJumpsOver
5297 TheLazyDogˇ»
5298 "});
5299
5300 // From here on out, test more complex cases of manipulate_text()
5301
5302 // Test no selection case - should affect words cursors are in
5303 // Cursor at beginning, middle, and end of word
5304 cx.set_state(indoc! {"
5305 ˇhello big beauˇtiful worldˇ
5306 "});
5307 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5308 cx.assert_editor_state(indoc! {"
5309 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5310 "});
5311
5312 // Test multiple selections on a single line and across multiple lines
5313 cx.set_state(indoc! {"
5314 «Theˇ» quick «brown
5315 foxˇ» jumps «overˇ»
5316 the «lazyˇ» dog
5317 "});
5318 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5319 cx.assert_editor_state(indoc! {"
5320 «THEˇ» quick «BROWN
5321 FOXˇ» jumps «OVERˇ»
5322 the «LAZYˇ» dog
5323 "});
5324
5325 // Test case where text length grows
5326 cx.set_state(indoc! {"
5327 «tschüߡ»
5328 "});
5329 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5330 cx.assert_editor_state(indoc! {"
5331 «TSCHÜSSˇ»
5332 "});
5333
5334 // Test to make sure we don't crash when text shrinks
5335 cx.set_state(indoc! {"
5336 aaa_bbbˇ
5337 "});
5338 cx.update_editor(|e, window, cx| {
5339 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5340 });
5341 cx.assert_editor_state(indoc! {"
5342 «aaaBbbˇ»
5343 "});
5344
5345 // Test to make sure we all aware of the fact that each word can grow and shrink
5346 // Final selections should be aware of this fact
5347 cx.set_state(indoc! {"
5348 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5349 "});
5350 cx.update_editor(|e, window, cx| {
5351 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5352 });
5353 cx.assert_editor_state(indoc! {"
5354 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5355 "});
5356
5357 cx.set_state(indoc! {"
5358 «hElLo, WoRld!ˇ»
5359 "});
5360 cx.update_editor(|e, window, cx| {
5361 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5362 });
5363 cx.assert_editor_state(indoc! {"
5364 «HeLlO, wOrLD!ˇ»
5365 "});
5366}
5367
5368#[gpui::test]
5369fn test_duplicate_line(cx: &mut TestAppContext) {
5370 init_test(cx, |_| {});
5371
5372 let editor = cx.add_window(|window, cx| {
5373 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5374 build_editor(buffer, window, cx)
5375 });
5376 _ = editor.update(cx, |editor, window, cx| {
5377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5378 s.select_display_ranges([
5379 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5380 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5381 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5382 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5383 ])
5384 });
5385 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5386 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5387 assert_eq!(
5388 editor.selections.display_ranges(cx),
5389 vec![
5390 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5391 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5392 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5393 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5394 ]
5395 );
5396 });
5397
5398 let editor = cx.add_window(|window, cx| {
5399 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5400 build_editor(buffer, window, cx)
5401 });
5402 _ = editor.update(cx, |editor, window, cx| {
5403 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5404 s.select_display_ranges([
5405 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5406 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5407 ])
5408 });
5409 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5410 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5411 assert_eq!(
5412 editor.selections.display_ranges(cx),
5413 vec![
5414 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5415 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5416 ]
5417 );
5418 });
5419
5420 // With `move_upwards` the selections stay in place, except for
5421 // the lines inserted above them
5422 let editor = cx.add_window(|window, cx| {
5423 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5424 build_editor(buffer, window, cx)
5425 });
5426 _ = editor.update(cx, |editor, window, cx| {
5427 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5428 s.select_display_ranges([
5429 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5430 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5431 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5432 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5433 ])
5434 });
5435 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5436 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5437 assert_eq!(
5438 editor.selections.display_ranges(cx),
5439 vec![
5440 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5441 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5442 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5443 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5444 ]
5445 );
5446 });
5447
5448 let editor = cx.add_window(|window, cx| {
5449 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5450 build_editor(buffer, window, cx)
5451 });
5452 _ = editor.update(cx, |editor, window, cx| {
5453 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5454 s.select_display_ranges([
5455 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5456 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5457 ])
5458 });
5459 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5460 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5461 assert_eq!(
5462 editor.selections.display_ranges(cx),
5463 vec![
5464 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5465 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5466 ]
5467 );
5468 });
5469
5470 let editor = cx.add_window(|window, cx| {
5471 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5472 build_editor(buffer, window, cx)
5473 });
5474 _ = editor.update(cx, |editor, window, cx| {
5475 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5476 s.select_display_ranges([
5477 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5478 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5479 ])
5480 });
5481 editor.duplicate_selection(&DuplicateSelection, window, cx);
5482 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5483 assert_eq!(
5484 editor.selections.display_ranges(cx),
5485 vec![
5486 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5487 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5488 ]
5489 );
5490 });
5491}
5492
5493#[gpui::test]
5494fn test_move_line_up_down(cx: &mut TestAppContext) {
5495 init_test(cx, |_| {});
5496
5497 let editor = cx.add_window(|window, cx| {
5498 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5499 build_editor(buffer, window, cx)
5500 });
5501 _ = editor.update(cx, |editor, window, cx| {
5502 editor.fold_creases(
5503 vec![
5504 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5505 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5506 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5507 ],
5508 true,
5509 window,
5510 cx,
5511 );
5512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5513 s.select_display_ranges([
5514 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5515 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5516 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5517 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5518 ])
5519 });
5520 assert_eq!(
5521 editor.display_text(cx),
5522 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5523 );
5524
5525 editor.move_line_up(&MoveLineUp, window, cx);
5526 assert_eq!(
5527 editor.display_text(cx),
5528 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5529 );
5530 assert_eq!(
5531 editor.selections.display_ranges(cx),
5532 vec![
5533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5534 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5535 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5536 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5537 ]
5538 );
5539 });
5540
5541 _ = editor.update(cx, |editor, window, cx| {
5542 editor.move_line_down(&MoveLineDown, window, cx);
5543 assert_eq!(
5544 editor.display_text(cx),
5545 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5546 );
5547 assert_eq!(
5548 editor.selections.display_ranges(cx),
5549 vec![
5550 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5551 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5552 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5553 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5554 ]
5555 );
5556 });
5557
5558 _ = editor.update(cx, |editor, window, cx| {
5559 editor.move_line_down(&MoveLineDown, window, cx);
5560 assert_eq!(
5561 editor.display_text(cx),
5562 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5563 );
5564 assert_eq!(
5565 editor.selections.display_ranges(cx),
5566 vec![
5567 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5568 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5569 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5570 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5571 ]
5572 );
5573 });
5574
5575 _ = editor.update(cx, |editor, window, cx| {
5576 editor.move_line_up(&MoveLineUp, window, cx);
5577 assert_eq!(
5578 editor.display_text(cx),
5579 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5580 );
5581 assert_eq!(
5582 editor.selections.display_ranges(cx),
5583 vec![
5584 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5585 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5586 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5587 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5588 ]
5589 );
5590 });
5591}
5592
5593#[gpui::test]
5594fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5595 init_test(cx, |_| {});
5596 let editor = cx.add_window(|window, cx| {
5597 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5598 build_editor(buffer, window, cx)
5599 });
5600 _ = editor.update(cx, |editor, window, cx| {
5601 editor.fold_creases(
5602 vec![Crease::simple(
5603 Point::new(6, 4)..Point::new(7, 4),
5604 FoldPlaceholder::test(),
5605 )],
5606 true,
5607 window,
5608 cx,
5609 );
5610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5611 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5612 });
5613 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5614 editor.move_line_up(&MoveLineUp, window, cx);
5615 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5616 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5617 });
5618}
5619
5620#[gpui::test]
5621fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5622 init_test(cx, |_| {});
5623
5624 let editor = cx.add_window(|window, cx| {
5625 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5626 build_editor(buffer, window, cx)
5627 });
5628 _ = editor.update(cx, |editor, window, cx| {
5629 let snapshot = editor.buffer.read(cx).snapshot(cx);
5630 editor.insert_blocks(
5631 [BlockProperties {
5632 style: BlockStyle::Fixed,
5633 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5634 height: Some(1),
5635 render: Arc::new(|_| div().into_any()),
5636 priority: 0,
5637 }],
5638 Some(Autoscroll::fit()),
5639 cx,
5640 );
5641 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5642 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5643 });
5644 editor.move_line_down(&MoveLineDown, window, cx);
5645 });
5646}
5647
5648#[gpui::test]
5649async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5650 init_test(cx, |_| {});
5651
5652 let mut cx = EditorTestContext::new(cx).await;
5653 cx.set_state(
5654 &"
5655 ˇzero
5656 one
5657 two
5658 three
5659 four
5660 five
5661 "
5662 .unindent(),
5663 );
5664
5665 // Create a four-line block that replaces three lines of text.
5666 cx.update_editor(|editor, window, cx| {
5667 let snapshot = editor.snapshot(window, cx);
5668 let snapshot = &snapshot.buffer_snapshot;
5669 let placement = BlockPlacement::Replace(
5670 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5671 );
5672 editor.insert_blocks(
5673 [BlockProperties {
5674 placement,
5675 height: Some(4),
5676 style: BlockStyle::Sticky,
5677 render: Arc::new(|_| gpui::div().into_any_element()),
5678 priority: 0,
5679 }],
5680 None,
5681 cx,
5682 );
5683 });
5684
5685 // Move down so that the cursor touches the block.
5686 cx.update_editor(|editor, window, cx| {
5687 editor.move_down(&Default::default(), window, cx);
5688 });
5689 cx.assert_editor_state(
5690 &"
5691 zero
5692 «one
5693 two
5694 threeˇ»
5695 four
5696 five
5697 "
5698 .unindent(),
5699 );
5700
5701 // Move down past the block.
5702 cx.update_editor(|editor, window, cx| {
5703 editor.move_down(&Default::default(), window, cx);
5704 });
5705 cx.assert_editor_state(
5706 &"
5707 zero
5708 one
5709 two
5710 three
5711 ˇfour
5712 five
5713 "
5714 .unindent(),
5715 );
5716}
5717
5718#[gpui::test]
5719fn test_transpose(cx: &mut TestAppContext) {
5720 init_test(cx, |_| {});
5721
5722 _ = cx.add_window(|window, cx| {
5723 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5724 editor.set_style(EditorStyle::default(), window, cx);
5725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5726 s.select_ranges([1..1])
5727 });
5728 editor.transpose(&Default::default(), window, cx);
5729 assert_eq!(editor.text(cx), "bac");
5730 assert_eq!(editor.selections.ranges(cx), [2..2]);
5731
5732 editor.transpose(&Default::default(), window, cx);
5733 assert_eq!(editor.text(cx), "bca");
5734 assert_eq!(editor.selections.ranges(cx), [3..3]);
5735
5736 editor.transpose(&Default::default(), window, cx);
5737 assert_eq!(editor.text(cx), "bac");
5738 assert_eq!(editor.selections.ranges(cx), [3..3]);
5739
5740 editor
5741 });
5742
5743 _ = cx.add_window(|window, cx| {
5744 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5745 editor.set_style(EditorStyle::default(), window, cx);
5746 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5747 s.select_ranges([3..3])
5748 });
5749 editor.transpose(&Default::default(), window, cx);
5750 assert_eq!(editor.text(cx), "acb\nde");
5751 assert_eq!(editor.selections.ranges(cx), [3..3]);
5752
5753 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5754 s.select_ranges([4..4])
5755 });
5756 editor.transpose(&Default::default(), window, cx);
5757 assert_eq!(editor.text(cx), "acbd\ne");
5758 assert_eq!(editor.selections.ranges(cx), [5..5]);
5759
5760 editor.transpose(&Default::default(), window, cx);
5761 assert_eq!(editor.text(cx), "acbde\n");
5762 assert_eq!(editor.selections.ranges(cx), [6..6]);
5763
5764 editor.transpose(&Default::default(), window, cx);
5765 assert_eq!(editor.text(cx), "acbd\ne");
5766 assert_eq!(editor.selections.ranges(cx), [6..6]);
5767
5768 editor
5769 });
5770
5771 _ = cx.add_window(|window, cx| {
5772 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5773 editor.set_style(EditorStyle::default(), window, cx);
5774 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5775 s.select_ranges([1..1, 2..2, 4..4])
5776 });
5777 editor.transpose(&Default::default(), window, cx);
5778 assert_eq!(editor.text(cx), "bacd\ne");
5779 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5780
5781 editor.transpose(&Default::default(), window, cx);
5782 assert_eq!(editor.text(cx), "bcade\n");
5783 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5784
5785 editor.transpose(&Default::default(), window, cx);
5786 assert_eq!(editor.text(cx), "bcda\ne");
5787 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5788
5789 editor.transpose(&Default::default(), window, cx);
5790 assert_eq!(editor.text(cx), "bcade\n");
5791 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5792
5793 editor.transpose(&Default::default(), window, cx);
5794 assert_eq!(editor.text(cx), "bcaed\n");
5795 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5796
5797 editor
5798 });
5799
5800 _ = cx.add_window(|window, cx| {
5801 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5802 editor.set_style(EditorStyle::default(), window, cx);
5803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5804 s.select_ranges([4..4])
5805 });
5806 editor.transpose(&Default::default(), window, cx);
5807 assert_eq!(editor.text(cx), "🏀🍐✋");
5808 assert_eq!(editor.selections.ranges(cx), [8..8]);
5809
5810 editor.transpose(&Default::default(), window, cx);
5811 assert_eq!(editor.text(cx), "🏀✋🍐");
5812 assert_eq!(editor.selections.ranges(cx), [11..11]);
5813
5814 editor.transpose(&Default::default(), window, cx);
5815 assert_eq!(editor.text(cx), "🏀🍐✋");
5816 assert_eq!(editor.selections.ranges(cx), [11..11]);
5817
5818 editor
5819 });
5820}
5821
5822#[gpui::test]
5823async fn test_rewrap(cx: &mut TestAppContext) {
5824 init_test(cx, |settings| {
5825 settings.languages.0.extend([
5826 (
5827 "Markdown".into(),
5828 LanguageSettingsContent {
5829 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5830 preferred_line_length: Some(40),
5831 ..Default::default()
5832 },
5833 ),
5834 (
5835 "Plain Text".into(),
5836 LanguageSettingsContent {
5837 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5838 preferred_line_length: Some(40),
5839 ..Default::default()
5840 },
5841 ),
5842 (
5843 "C++".into(),
5844 LanguageSettingsContent {
5845 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5846 preferred_line_length: Some(40),
5847 ..Default::default()
5848 },
5849 ),
5850 (
5851 "Python".into(),
5852 LanguageSettingsContent {
5853 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5854 preferred_line_length: Some(40),
5855 ..Default::default()
5856 },
5857 ),
5858 (
5859 "Rust".into(),
5860 LanguageSettingsContent {
5861 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5862 preferred_line_length: Some(40),
5863 ..Default::default()
5864 },
5865 ),
5866 ])
5867 });
5868
5869 let mut cx = EditorTestContext::new(cx).await;
5870
5871 let cpp_language = Arc::new(Language::new(
5872 LanguageConfig {
5873 name: "C++".into(),
5874 line_comments: vec!["// ".into()],
5875 ..LanguageConfig::default()
5876 },
5877 None,
5878 ));
5879 let python_language = Arc::new(Language::new(
5880 LanguageConfig {
5881 name: "Python".into(),
5882 line_comments: vec!["# ".into()],
5883 ..LanguageConfig::default()
5884 },
5885 None,
5886 ));
5887 let markdown_language = Arc::new(Language::new(
5888 LanguageConfig {
5889 name: "Markdown".into(),
5890 rewrap_prefixes: vec![
5891 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5892 regex::Regex::new("[-*+]\\s+").unwrap(),
5893 ],
5894 ..LanguageConfig::default()
5895 },
5896 None,
5897 ));
5898 let rust_language = Arc::new(
5899 Language::new(
5900 LanguageConfig {
5901 name: "Rust".into(),
5902 line_comments: vec!["// ".into(), "/// ".into()],
5903 ..LanguageConfig::default()
5904 },
5905 Some(tree_sitter_rust::LANGUAGE.into()),
5906 )
5907 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5908 .unwrap(),
5909 );
5910
5911 let plaintext_language = Arc::new(Language::new(
5912 LanguageConfig {
5913 name: "Plain Text".into(),
5914 ..LanguageConfig::default()
5915 },
5916 None,
5917 ));
5918
5919 // Test basic rewrapping of a long line with a cursor
5920 assert_rewrap(
5921 indoc! {"
5922 // ˇThis is a long comment that needs to be wrapped.
5923 "},
5924 indoc! {"
5925 // ˇThis is a long comment that needs to
5926 // be wrapped.
5927 "},
5928 cpp_language.clone(),
5929 &mut cx,
5930 );
5931
5932 // Test rewrapping a full selection
5933 assert_rewrap(
5934 indoc! {"
5935 «// This selected long comment needs to be wrapped.ˇ»"
5936 },
5937 indoc! {"
5938 «// This selected long comment needs to
5939 // be wrapped.ˇ»"
5940 },
5941 cpp_language.clone(),
5942 &mut cx,
5943 );
5944
5945 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5946 assert_rewrap(
5947 indoc! {"
5948 // ˇThis is the first line.
5949 // Thisˇ is the second line.
5950 // This is the thirdˇ line, all part of one paragraph.
5951 "},
5952 indoc! {"
5953 // ˇThis is the first line. Thisˇ is the
5954 // second line. This is the thirdˇ line,
5955 // all part of one paragraph.
5956 "},
5957 cpp_language.clone(),
5958 &mut cx,
5959 );
5960
5961 // Test multiple cursors in different paragraphs trigger separate rewraps
5962 assert_rewrap(
5963 indoc! {"
5964 // ˇThis is the first paragraph, first line.
5965 // ˇThis is the first paragraph, second line.
5966
5967 // ˇThis is the second paragraph, first line.
5968 // ˇThis is the second paragraph, second line.
5969 "},
5970 indoc! {"
5971 // ˇThis is the first paragraph, first
5972 // line. ˇThis is the first paragraph,
5973 // second line.
5974
5975 // ˇThis is the second paragraph, first
5976 // line. ˇThis is the second paragraph,
5977 // second line.
5978 "},
5979 cpp_language.clone(),
5980 &mut cx,
5981 );
5982
5983 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5984 assert_rewrap(
5985 indoc! {"
5986 «// A regular long long comment to be wrapped.
5987 /// A documentation long comment to be wrapped.ˇ»
5988 "},
5989 indoc! {"
5990 «// A regular long long comment to be
5991 // wrapped.
5992 /// A documentation long comment to be
5993 /// wrapped.ˇ»
5994 "},
5995 rust_language.clone(),
5996 &mut cx,
5997 );
5998
5999 // Test that change in indentation level trigger seperate rewraps
6000 assert_rewrap(
6001 indoc! {"
6002 fn foo() {
6003 «// This is a long comment at the base indent.
6004 // This is a long comment at the next indent.ˇ»
6005 }
6006 "},
6007 indoc! {"
6008 fn foo() {
6009 «// This is a long comment at the
6010 // base indent.
6011 // This is a long comment at the
6012 // next indent.ˇ»
6013 }
6014 "},
6015 rust_language.clone(),
6016 &mut cx,
6017 );
6018
6019 // Test that different comment prefix characters (e.g., '#') are handled correctly
6020 assert_rewrap(
6021 indoc! {"
6022 # ˇThis is a long comment using a pound sign.
6023 "},
6024 indoc! {"
6025 # ˇThis is a long comment using a pound
6026 # sign.
6027 "},
6028 python_language,
6029 &mut cx,
6030 );
6031
6032 // Test rewrapping only affects comments, not code even when selected
6033 assert_rewrap(
6034 indoc! {"
6035 «/// This doc comment is long and should be wrapped.
6036 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6037 "},
6038 indoc! {"
6039 «/// This doc comment is long and should
6040 /// be wrapped.
6041 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6042 "},
6043 rust_language.clone(),
6044 &mut cx,
6045 );
6046
6047 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6048 assert_rewrap(
6049 indoc! {"
6050 # Header
6051
6052 A long long long line of markdown text to wrap.ˇ
6053 "},
6054 indoc! {"
6055 # Header
6056
6057 A long long long line of markdown text
6058 to wrap.ˇ
6059 "},
6060 markdown_language.clone(),
6061 &mut cx,
6062 );
6063
6064 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6065 assert_rewrap(
6066 indoc! {"
6067 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6068 2. This is a numbered list item that is very long and needs to be wrapped properly.
6069 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6070 "},
6071 indoc! {"
6072 «1. This is a numbered list item that is
6073 very long and needs to be wrapped
6074 properly.
6075 2. This is a numbered list item that is
6076 very long and needs to be wrapped
6077 properly.
6078 - This is an unordered list item that is
6079 also very long and should not merge
6080 with the numbered item.ˇ»
6081 "},
6082 markdown_language.clone(),
6083 &mut cx,
6084 );
6085
6086 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6087 assert_rewrap(
6088 indoc! {"
6089 «1. This is a numbered list item that is
6090 very long and needs to be wrapped
6091 properly.
6092 2. This is a numbered list item that is
6093 very long and needs to be wrapped
6094 properly.
6095 - This is an unordered list item that is
6096 also very long and should not merge with
6097 the numbered item.ˇ»
6098 "},
6099 indoc! {"
6100 «1. This is a numbered list item that is
6101 very long and needs to be wrapped
6102 properly.
6103 2. This is a numbered list item that is
6104 very long and needs to be wrapped
6105 properly.
6106 - This is an unordered list item that is
6107 also very long and should not merge
6108 with the numbered item.ˇ»
6109 "},
6110 markdown_language.clone(),
6111 &mut cx,
6112 );
6113
6114 // Test that rewrapping maintain indents even when they already exists.
6115 assert_rewrap(
6116 indoc! {"
6117 «1. This is a numbered list
6118 item that is very long and needs to be wrapped properly.
6119 2. This is a numbered list
6120 item that is very long and needs to be wrapped properly.
6121 - This is an unordered list item that is also very long and
6122 should not merge with the numbered item.ˇ»
6123 "},
6124 indoc! {"
6125 «1. This is a numbered list item that is
6126 very long and needs to be wrapped
6127 properly.
6128 2. This is a numbered list item that is
6129 very long and needs to be wrapped
6130 properly.
6131 - This is an unordered list item that is
6132 also very long and should not merge
6133 with the numbered item.ˇ»
6134 "},
6135 markdown_language,
6136 &mut cx,
6137 );
6138
6139 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6140 assert_rewrap(
6141 indoc! {"
6142 ˇThis is a very long line of plain text that will be wrapped.
6143 "},
6144 indoc! {"
6145 ˇThis is a very long line of plain text
6146 that will be wrapped.
6147 "},
6148 plaintext_language.clone(),
6149 &mut cx,
6150 );
6151
6152 // Test that non-commented code acts as a paragraph boundary within a selection
6153 assert_rewrap(
6154 indoc! {"
6155 «// This is the first long comment block to be wrapped.
6156 fn my_func(a: u32);
6157 // This is the second long comment block to be wrapped.ˇ»
6158 "},
6159 indoc! {"
6160 «// This is the first long comment block
6161 // to be wrapped.
6162 fn my_func(a: u32);
6163 // This is the second long comment block
6164 // to be wrapped.ˇ»
6165 "},
6166 rust_language,
6167 &mut cx,
6168 );
6169
6170 // Test rewrapping multiple selections, including ones with blank lines or tabs
6171 assert_rewrap(
6172 indoc! {"
6173 «ˇThis is a very long line that will be wrapped.
6174
6175 This is another paragraph in the same selection.»
6176
6177 «\tThis is a very long indented line that will be wrapped.ˇ»
6178 "},
6179 indoc! {"
6180 «ˇThis is a very long line that will be
6181 wrapped.
6182
6183 This is another paragraph in the same
6184 selection.»
6185
6186 «\tThis is a very long indented line
6187 \tthat will be wrapped.ˇ»
6188 "},
6189 plaintext_language,
6190 &mut cx,
6191 );
6192
6193 // Test that an empty comment line acts as a paragraph boundary
6194 assert_rewrap(
6195 indoc! {"
6196 // ˇThis is a long comment that will be wrapped.
6197 //
6198 // And this is another long comment that will also be wrapped.ˇ
6199 "},
6200 indoc! {"
6201 // ˇThis is a long comment that will be
6202 // wrapped.
6203 //
6204 // And this is another long comment that
6205 // will also be wrapped.ˇ
6206 "},
6207 cpp_language,
6208 &mut cx,
6209 );
6210
6211 #[track_caller]
6212 fn assert_rewrap(
6213 unwrapped_text: &str,
6214 wrapped_text: &str,
6215 language: Arc<Language>,
6216 cx: &mut EditorTestContext,
6217 ) {
6218 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6219 cx.set_state(unwrapped_text);
6220 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6221 cx.assert_editor_state(wrapped_text);
6222 }
6223}
6224
6225#[gpui::test]
6226async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6227 init_test(cx, |settings| {
6228 settings.languages.0.extend([(
6229 "Rust".into(),
6230 LanguageSettingsContent {
6231 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6232 preferred_line_length: Some(40),
6233 ..Default::default()
6234 },
6235 )])
6236 });
6237
6238 let mut cx = EditorTestContext::new(cx).await;
6239
6240 let rust_lang = Arc::new(
6241 Language::new(
6242 LanguageConfig {
6243 name: "Rust".into(),
6244 line_comments: vec!["// ".into()],
6245 block_comment: Some(BlockCommentConfig {
6246 start: "/*".into(),
6247 end: "*/".into(),
6248 prefix: "* ".into(),
6249 tab_size: 1,
6250 }),
6251 documentation_comment: Some(BlockCommentConfig {
6252 start: "/**".into(),
6253 end: "*/".into(),
6254 prefix: "* ".into(),
6255 tab_size: 1,
6256 }),
6257
6258 ..LanguageConfig::default()
6259 },
6260 Some(tree_sitter_rust::LANGUAGE.into()),
6261 )
6262 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6263 .unwrap(),
6264 );
6265
6266 // regular block comment
6267 assert_rewrap(
6268 indoc! {"
6269 /*
6270 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6271 */
6272 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6273 "},
6274 indoc! {"
6275 /*
6276 *ˇ Lorem ipsum dolor sit amet,
6277 * consectetur adipiscing elit.
6278 */
6279 /*
6280 *ˇ Lorem ipsum dolor sit amet,
6281 * consectetur adipiscing elit.
6282 */
6283 "},
6284 rust_lang.clone(),
6285 &mut cx,
6286 );
6287
6288 // indent is respected
6289 assert_rewrap(
6290 indoc! {"
6291 {}
6292 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6293 "},
6294 indoc! {"
6295 {}
6296 /*
6297 *ˇ Lorem ipsum dolor sit amet,
6298 * consectetur adipiscing elit.
6299 */
6300 "},
6301 rust_lang.clone(),
6302 &mut cx,
6303 );
6304
6305 // short block comments with inline delimiters
6306 assert_rewrap(
6307 indoc! {"
6308 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6309 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6310 */
6311 /*
6312 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6313 "},
6314 indoc! {"
6315 /*
6316 *ˇ Lorem ipsum dolor sit amet,
6317 * consectetur adipiscing elit.
6318 */
6319 /*
6320 *ˇ Lorem ipsum dolor sit amet,
6321 * consectetur adipiscing elit.
6322 */
6323 /*
6324 *ˇ Lorem ipsum dolor sit amet,
6325 * consectetur adipiscing elit.
6326 */
6327 "},
6328 rust_lang.clone(),
6329 &mut cx,
6330 );
6331
6332 // multiline block comment with inline start/end delimiters
6333 assert_rewrap(
6334 indoc! {"
6335 /*ˇ Lorem ipsum dolor sit amet,
6336 * consectetur adipiscing elit. */
6337 "},
6338 indoc! {"
6339 /*
6340 *ˇ Lorem ipsum dolor sit amet,
6341 * consectetur adipiscing elit.
6342 */
6343 "},
6344 rust_lang.clone(),
6345 &mut cx,
6346 );
6347
6348 // block comment rewrap still respects paragraph bounds
6349 assert_rewrap(
6350 indoc! {"
6351 /*
6352 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6353 *
6354 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6355 */
6356 "},
6357 indoc! {"
6358 /*
6359 *ˇ Lorem ipsum dolor sit amet,
6360 * consectetur adipiscing elit.
6361 *
6362 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6363 */
6364 "},
6365 rust_lang.clone(),
6366 &mut cx,
6367 );
6368
6369 // documentation comments
6370 assert_rewrap(
6371 indoc! {"
6372 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6373 /**
6374 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6375 */
6376 "},
6377 indoc! {"
6378 /**
6379 *ˇ Lorem ipsum dolor sit amet,
6380 * consectetur adipiscing elit.
6381 */
6382 /**
6383 *ˇ Lorem ipsum dolor sit amet,
6384 * consectetur adipiscing elit.
6385 */
6386 "},
6387 rust_lang.clone(),
6388 &mut cx,
6389 );
6390
6391 // different, adjacent comments
6392 assert_rewrap(
6393 indoc! {"
6394 /**
6395 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6396 */
6397 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6398 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6399 "},
6400 indoc! {"
6401 /**
6402 *ˇ Lorem ipsum dolor sit amet,
6403 * consectetur adipiscing elit.
6404 */
6405 /*
6406 *ˇ Lorem ipsum dolor sit amet,
6407 * consectetur adipiscing elit.
6408 */
6409 //ˇ Lorem ipsum dolor sit amet,
6410 // consectetur adipiscing elit.
6411 "},
6412 rust_lang.clone(),
6413 &mut cx,
6414 );
6415
6416 // selection w/ single short block comment
6417 assert_rewrap(
6418 indoc! {"
6419 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6420 "},
6421 indoc! {"
6422 «/*
6423 * Lorem ipsum dolor sit amet,
6424 * consectetur adipiscing elit.
6425 */ˇ»
6426 "},
6427 rust_lang.clone(),
6428 &mut cx,
6429 );
6430
6431 // rewrapping a single comment w/ abutting comments
6432 assert_rewrap(
6433 indoc! {"
6434 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6435 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6436 "},
6437 indoc! {"
6438 /*
6439 * ˇLorem ipsum dolor sit amet,
6440 * consectetur adipiscing elit.
6441 */
6442 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6443 "},
6444 rust_lang.clone(),
6445 &mut cx,
6446 );
6447
6448 // selection w/ non-abutting short block comments
6449 assert_rewrap(
6450 indoc! {"
6451 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6452
6453 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6454 "},
6455 indoc! {"
6456 «/*
6457 * Lorem ipsum dolor sit amet,
6458 * consectetur adipiscing elit.
6459 */
6460
6461 /*
6462 * Lorem ipsum dolor sit amet,
6463 * consectetur adipiscing elit.
6464 */ˇ»
6465 "},
6466 rust_lang.clone(),
6467 &mut cx,
6468 );
6469
6470 // selection of multiline block comments
6471 assert_rewrap(
6472 indoc! {"
6473 «/* Lorem ipsum dolor sit amet,
6474 * consectetur adipiscing elit. */ˇ»
6475 "},
6476 indoc! {"
6477 «/*
6478 * Lorem ipsum dolor sit amet,
6479 * consectetur adipiscing elit.
6480 */ˇ»
6481 "},
6482 rust_lang.clone(),
6483 &mut cx,
6484 );
6485
6486 // partial selection of multiline block comments
6487 assert_rewrap(
6488 indoc! {"
6489 «/* Lorem ipsum dolor sit amet,ˇ»
6490 * consectetur adipiscing elit. */
6491 /* Lorem ipsum dolor sit amet,
6492 «* consectetur adipiscing elit. */ˇ»
6493 "},
6494 indoc! {"
6495 «/*
6496 * Lorem ipsum dolor sit amet,ˇ»
6497 * consectetur adipiscing elit. */
6498 /* Lorem ipsum dolor sit amet,
6499 «* consectetur adipiscing elit.
6500 */ˇ»
6501 "},
6502 rust_lang.clone(),
6503 &mut cx,
6504 );
6505
6506 // selection w/ abutting short block comments
6507 // TODO: should not be combined; should rewrap as 2 comments
6508 assert_rewrap(
6509 indoc! {"
6510 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6511 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6512 "},
6513 // desired behavior:
6514 // indoc! {"
6515 // «/*
6516 // * Lorem ipsum dolor sit amet,
6517 // * consectetur adipiscing elit.
6518 // */
6519 // /*
6520 // * Lorem ipsum dolor sit amet,
6521 // * consectetur adipiscing elit.
6522 // */ˇ»
6523 // "},
6524 // actual behaviour:
6525 indoc! {"
6526 «/*
6527 * Lorem ipsum dolor sit amet,
6528 * consectetur adipiscing elit. Lorem
6529 * ipsum dolor sit amet, consectetur
6530 * adipiscing elit.
6531 */ˇ»
6532 "},
6533 rust_lang.clone(),
6534 &mut cx,
6535 );
6536
6537 // TODO: same as above, but with delimiters on separate line
6538 // assert_rewrap(
6539 // indoc! {"
6540 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6541 // */
6542 // /*
6543 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6544 // "},
6545 // // desired:
6546 // // indoc! {"
6547 // // «/*
6548 // // * Lorem ipsum dolor sit amet,
6549 // // * consectetur adipiscing elit.
6550 // // */
6551 // // /*
6552 // // * Lorem ipsum dolor sit amet,
6553 // // * consectetur adipiscing elit.
6554 // // */ˇ»
6555 // // "},
6556 // // actual: (but with trailing w/s on the empty lines)
6557 // indoc! {"
6558 // «/*
6559 // * Lorem ipsum dolor sit amet,
6560 // * consectetur adipiscing elit.
6561 // *
6562 // */
6563 // /*
6564 // *
6565 // * Lorem ipsum dolor sit amet,
6566 // * consectetur adipiscing elit.
6567 // */ˇ»
6568 // "},
6569 // rust_lang.clone(),
6570 // &mut cx,
6571 // );
6572
6573 // TODO these are unhandled edge cases; not correct, just documenting known issues
6574 assert_rewrap(
6575 indoc! {"
6576 /*
6577 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6578 */
6579 /*
6580 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6581 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6582 "},
6583 // desired:
6584 // indoc! {"
6585 // /*
6586 // *ˇ Lorem ipsum dolor sit amet,
6587 // * consectetur adipiscing elit.
6588 // */
6589 // /*
6590 // *ˇ Lorem ipsum dolor sit amet,
6591 // * consectetur adipiscing elit.
6592 // */
6593 // /*
6594 // *ˇ Lorem ipsum dolor sit amet
6595 // */ /* consectetur adipiscing elit. */
6596 // "},
6597 // actual:
6598 indoc! {"
6599 /*
6600 //ˇ Lorem ipsum dolor sit amet,
6601 // consectetur adipiscing elit.
6602 */
6603 /*
6604 * //ˇ Lorem ipsum dolor sit amet,
6605 * consectetur adipiscing elit.
6606 */
6607 /*
6608 *ˇ Lorem ipsum dolor sit amet */ /*
6609 * consectetur adipiscing elit.
6610 */
6611 "},
6612 rust_lang,
6613 &mut cx,
6614 );
6615
6616 #[track_caller]
6617 fn assert_rewrap(
6618 unwrapped_text: &str,
6619 wrapped_text: &str,
6620 language: Arc<Language>,
6621 cx: &mut EditorTestContext,
6622 ) {
6623 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6624 cx.set_state(unwrapped_text);
6625 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6626 cx.assert_editor_state(wrapped_text);
6627 }
6628}
6629
6630#[gpui::test]
6631async fn test_hard_wrap(cx: &mut TestAppContext) {
6632 init_test(cx, |_| {});
6633 let mut cx = EditorTestContext::new(cx).await;
6634
6635 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6636 cx.update_editor(|editor, _, cx| {
6637 editor.set_hard_wrap(Some(14), cx);
6638 });
6639
6640 cx.set_state(indoc!(
6641 "
6642 one two three ˇ
6643 "
6644 ));
6645 cx.simulate_input("four");
6646 cx.run_until_parked();
6647
6648 cx.assert_editor_state(indoc!(
6649 "
6650 one two three
6651 fourˇ
6652 "
6653 ));
6654
6655 cx.update_editor(|editor, window, cx| {
6656 editor.newline(&Default::default(), window, cx);
6657 });
6658 cx.run_until_parked();
6659 cx.assert_editor_state(indoc!(
6660 "
6661 one two three
6662 four
6663 ˇ
6664 "
6665 ));
6666
6667 cx.simulate_input("five");
6668 cx.run_until_parked();
6669 cx.assert_editor_state(indoc!(
6670 "
6671 one two three
6672 four
6673 fiveˇ
6674 "
6675 ));
6676
6677 cx.update_editor(|editor, window, cx| {
6678 editor.newline(&Default::default(), window, cx);
6679 });
6680 cx.run_until_parked();
6681 cx.simulate_input("# ");
6682 cx.run_until_parked();
6683 cx.assert_editor_state(indoc!(
6684 "
6685 one two three
6686 four
6687 five
6688 # ˇ
6689 "
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.newline(&Default::default(), window, cx);
6694 });
6695 cx.run_until_parked();
6696 cx.assert_editor_state(indoc!(
6697 "
6698 one two three
6699 four
6700 five
6701 #\x20
6702 #ˇ
6703 "
6704 ));
6705
6706 cx.simulate_input(" 6");
6707 cx.run_until_parked();
6708 cx.assert_editor_state(indoc!(
6709 "
6710 one two three
6711 four
6712 five
6713 #
6714 # 6ˇ
6715 "
6716 ));
6717}
6718
6719#[gpui::test]
6720async fn test_clipboard(cx: &mut TestAppContext) {
6721 init_test(cx, |_| {});
6722
6723 let mut cx = EditorTestContext::new(cx).await;
6724
6725 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6726 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6727 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6728
6729 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6730 cx.set_state("two ˇfour ˇsix ˇ");
6731 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6732 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6733
6734 // Paste again but with only two cursors. Since the number of cursors doesn't
6735 // match the number of slices in the clipboard, the entire clipboard text
6736 // is pasted at each cursor.
6737 cx.set_state("ˇtwo one✅ four three six five ˇ");
6738 cx.update_editor(|e, window, cx| {
6739 e.handle_input("( ", window, cx);
6740 e.paste(&Paste, window, cx);
6741 e.handle_input(") ", window, cx);
6742 });
6743 cx.assert_editor_state(
6744 &([
6745 "( one✅ ",
6746 "three ",
6747 "five ) ˇtwo one✅ four three six five ( one✅ ",
6748 "three ",
6749 "five ) ˇ",
6750 ]
6751 .join("\n")),
6752 );
6753
6754 // Cut with three selections, one of which is full-line.
6755 cx.set_state(indoc! {"
6756 1«2ˇ»3
6757 4ˇ567
6758 «8ˇ»9"});
6759 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6760 cx.assert_editor_state(indoc! {"
6761 1ˇ3
6762 ˇ9"});
6763
6764 // Paste with three selections, noticing how the copied selection that was full-line
6765 // gets inserted before the second cursor.
6766 cx.set_state(indoc! {"
6767 1ˇ3
6768 9ˇ
6769 «oˇ»ne"});
6770 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6771 cx.assert_editor_state(indoc! {"
6772 12ˇ3
6773 4567
6774 9ˇ
6775 8ˇne"});
6776
6777 // Copy with a single cursor only, which writes the whole line into the clipboard.
6778 cx.set_state(indoc! {"
6779 The quick brown
6780 fox juˇmps over
6781 the lazy dog"});
6782 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6783 assert_eq!(
6784 cx.read_from_clipboard()
6785 .and_then(|item| item.text().as_deref().map(str::to_string)),
6786 Some("fox jumps over\n".to_string())
6787 );
6788
6789 // Paste with three selections, noticing how the copied full-line selection is inserted
6790 // before the empty selections but replaces the selection that is non-empty.
6791 cx.set_state(indoc! {"
6792 Tˇhe quick brown
6793 «foˇ»x jumps over
6794 tˇhe lazy dog"});
6795 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6796 cx.assert_editor_state(indoc! {"
6797 fox jumps over
6798 Tˇhe quick brown
6799 fox jumps over
6800 ˇx jumps over
6801 fox jumps over
6802 tˇhe lazy dog"});
6803}
6804
6805#[gpui::test]
6806async fn test_copy_trim(cx: &mut TestAppContext) {
6807 init_test(cx, |_| {});
6808
6809 let mut cx = EditorTestContext::new(cx).await;
6810 cx.set_state(
6811 r#" «for selection in selections.iter() {
6812 let mut start = selection.start;
6813 let mut end = selection.end;
6814 let is_entire_line = selection.is_empty();
6815 if is_entire_line {
6816 start = Point::new(start.row, 0);ˇ»
6817 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6818 }
6819 "#,
6820 );
6821 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6822 assert_eq!(
6823 cx.read_from_clipboard()
6824 .and_then(|item| item.text().as_deref().map(str::to_string)),
6825 Some(
6826 "for selection in selections.iter() {
6827 let mut start = selection.start;
6828 let mut end = selection.end;
6829 let is_entire_line = selection.is_empty();
6830 if is_entire_line {
6831 start = Point::new(start.row, 0);"
6832 .to_string()
6833 ),
6834 "Regular copying preserves all indentation selected",
6835 );
6836 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6837 assert_eq!(
6838 cx.read_from_clipboard()
6839 .and_then(|item| item.text().as_deref().map(str::to_string)),
6840 Some(
6841 "for selection in selections.iter() {
6842let mut start = selection.start;
6843let mut end = selection.end;
6844let is_entire_line = selection.is_empty();
6845if is_entire_line {
6846 start = Point::new(start.row, 0);"
6847 .to_string()
6848 ),
6849 "Copying with stripping should strip all leading whitespaces"
6850 );
6851
6852 cx.set_state(
6853 r#" « for selection in selections.iter() {
6854 let mut start = selection.start;
6855 let mut end = selection.end;
6856 let is_entire_line = selection.is_empty();
6857 if is_entire_line {
6858 start = Point::new(start.row, 0);ˇ»
6859 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6860 }
6861 "#,
6862 );
6863 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6864 assert_eq!(
6865 cx.read_from_clipboard()
6866 .and_then(|item| item.text().as_deref().map(str::to_string)),
6867 Some(
6868 " for selection in selections.iter() {
6869 let mut start = selection.start;
6870 let mut end = selection.end;
6871 let is_entire_line = selection.is_empty();
6872 if is_entire_line {
6873 start = Point::new(start.row, 0);"
6874 .to_string()
6875 ),
6876 "Regular copying preserves all indentation selected",
6877 );
6878 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6879 assert_eq!(
6880 cx.read_from_clipboard()
6881 .and_then(|item| item.text().as_deref().map(str::to_string)),
6882 Some(
6883 "for selection in selections.iter() {
6884let mut start = selection.start;
6885let mut end = selection.end;
6886let is_entire_line = selection.is_empty();
6887if is_entire_line {
6888 start = Point::new(start.row, 0);"
6889 .to_string()
6890 ),
6891 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6892 );
6893
6894 cx.set_state(
6895 r#" «ˇ for selection in selections.iter() {
6896 let mut start = selection.start;
6897 let mut end = selection.end;
6898 let is_entire_line = selection.is_empty();
6899 if is_entire_line {
6900 start = Point::new(start.row, 0);»
6901 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6902 }
6903 "#,
6904 );
6905 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6906 assert_eq!(
6907 cx.read_from_clipboard()
6908 .and_then(|item| item.text().as_deref().map(str::to_string)),
6909 Some(
6910 " for selection in selections.iter() {
6911 let mut start = selection.start;
6912 let mut end = selection.end;
6913 let is_entire_line = selection.is_empty();
6914 if is_entire_line {
6915 start = Point::new(start.row, 0);"
6916 .to_string()
6917 ),
6918 "Regular copying for reverse selection works the same",
6919 );
6920 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6921 assert_eq!(
6922 cx.read_from_clipboard()
6923 .and_then(|item| item.text().as_deref().map(str::to_string)),
6924 Some(
6925 "for selection in selections.iter() {
6926let mut start = selection.start;
6927let mut end = selection.end;
6928let is_entire_line = selection.is_empty();
6929if is_entire_line {
6930 start = Point::new(start.row, 0);"
6931 .to_string()
6932 ),
6933 "Copying with stripping for reverse selection works the same"
6934 );
6935
6936 cx.set_state(
6937 r#" for selection «in selections.iter() {
6938 let mut start = selection.start;
6939 let mut end = selection.end;
6940 let is_entire_line = selection.is_empty();
6941 if is_entire_line {
6942 start = Point::new(start.row, 0);ˇ»
6943 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6944 }
6945 "#,
6946 );
6947 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6948 assert_eq!(
6949 cx.read_from_clipboard()
6950 .and_then(|item| item.text().as_deref().map(str::to_string)),
6951 Some(
6952 "in selections.iter() {
6953 let mut start = selection.start;
6954 let mut end = selection.end;
6955 let is_entire_line = selection.is_empty();
6956 if is_entire_line {
6957 start = Point::new(start.row, 0);"
6958 .to_string()
6959 ),
6960 "When selecting past the indent, the copying works as usual",
6961 );
6962 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6963 assert_eq!(
6964 cx.read_from_clipboard()
6965 .and_then(|item| item.text().as_deref().map(str::to_string)),
6966 Some(
6967 "in selections.iter() {
6968 let mut start = selection.start;
6969 let mut end = selection.end;
6970 let is_entire_line = selection.is_empty();
6971 if is_entire_line {
6972 start = Point::new(start.row, 0);"
6973 .to_string()
6974 ),
6975 "When selecting past the indent, nothing is trimmed"
6976 );
6977
6978 cx.set_state(
6979 r#" «for selection in selections.iter() {
6980 let mut start = selection.start;
6981
6982 let mut end = selection.end;
6983 let is_entire_line = selection.is_empty();
6984 if is_entire_line {
6985 start = Point::new(start.row, 0);
6986ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6987 }
6988 "#,
6989 );
6990 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6991 assert_eq!(
6992 cx.read_from_clipboard()
6993 .and_then(|item| item.text().as_deref().map(str::to_string)),
6994 Some(
6995 "for selection in selections.iter() {
6996let mut start = selection.start;
6997
6998let mut end = selection.end;
6999let is_entire_line = selection.is_empty();
7000if is_entire_line {
7001 start = Point::new(start.row, 0);
7002"
7003 .to_string()
7004 ),
7005 "Copying with stripping should ignore empty lines"
7006 );
7007}
7008
7009#[gpui::test]
7010async fn test_paste_multiline(cx: &mut TestAppContext) {
7011 init_test(cx, |_| {});
7012
7013 let mut cx = EditorTestContext::new(cx).await;
7014 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7015
7016 // Cut an indented block, without the leading whitespace.
7017 cx.set_state(indoc! {"
7018 const a: B = (
7019 c(),
7020 «d(
7021 e,
7022 f
7023 )ˇ»
7024 );
7025 "});
7026 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7027 cx.assert_editor_state(indoc! {"
7028 const a: B = (
7029 c(),
7030 ˇ
7031 );
7032 "});
7033
7034 // Paste it at the same position.
7035 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7036 cx.assert_editor_state(indoc! {"
7037 const a: B = (
7038 c(),
7039 d(
7040 e,
7041 f
7042 )ˇ
7043 );
7044 "});
7045
7046 // Paste it at a line with a lower indent level.
7047 cx.set_state(indoc! {"
7048 ˇ
7049 const a: B = (
7050 c(),
7051 );
7052 "});
7053 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7054 cx.assert_editor_state(indoc! {"
7055 d(
7056 e,
7057 f
7058 )ˇ
7059 const a: B = (
7060 c(),
7061 );
7062 "});
7063
7064 // Cut an indented block, with the leading whitespace.
7065 cx.set_state(indoc! {"
7066 const a: B = (
7067 c(),
7068 « d(
7069 e,
7070 f
7071 )
7072 ˇ»);
7073 "});
7074 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7075 cx.assert_editor_state(indoc! {"
7076 const a: B = (
7077 c(),
7078 ˇ);
7079 "});
7080
7081 // Paste it at the same position.
7082 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7083 cx.assert_editor_state(indoc! {"
7084 const a: B = (
7085 c(),
7086 d(
7087 e,
7088 f
7089 )
7090 ˇ);
7091 "});
7092
7093 // Paste it at a line with a higher indent level.
7094 cx.set_state(indoc! {"
7095 const a: B = (
7096 c(),
7097 d(
7098 e,
7099 fˇ
7100 )
7101 );
7102 "});
7103 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7104 cx.assert_editor_state(indoc! {"
7105 const a: B = (
7106 c(),
7107 d(
7108 e,
7109 f d(
7110 e,
7111 f
7112 )
7113 ˇ
7114 )
7115 );
7116 "});
7117
7118 // Copy an indented block, starting mid-line
7119 cx.set_state(indoc! {"
7120 const a: B = (
7121 c(),
7122 somethin«g(
7123 e,
7124 f
7125 )ˇ»
7126 );
7127 "});
7128 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7129
7130 // Paste it on a line with a lower indent level
7131 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7132 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7133 cx.assert_editor_state(indoc! {"
7134 const a: B = (
7135 c(),
7136 something(
7137 e,
7138 f
7139 )
7140 );
7141 g(
7142 e,
7143 f
7144 )ˇ"});
7145}
7146
7147#[gpui::test]
7148async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7149 init_test(cx, |_| {});
7150
7151 cx.write_to_clipboard(ClipboardItem::new_string(
7152 " d(\n e\n );\n".into(),
7153 ));
7154
7155 let mut cx = EditorTestContext::new(cx).await;
7156 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7157
7158 cx.set_state(indoc! {"
7159 fn a() {
7160 b();
7161 if c() {
7162 ˇ
7163 }
7164 }
7165 "});
7166
7167 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7168 cx.assert_editor_state(indoc! {"
7169 fn a() {
7170 b();
7171 if c() {
7172 d(
7173 e
7174 );
7175 ˇ
7176 }
7177 }
7178 "});
7179
7180 cx.set_state(indoc! {"
7181 fn a() {
7182 b();
7183 ˇ
7184 }
7185 "});
7186
7187 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7188 cx.assert_editor_state(indoc! {"
7189 fn a() {
7190 b();
7191 d(
7192 e
7193 );
7194 ˇ
7195 }
7196 "});
7197}
7198
7199#[gpui::test]
7200fn test_select_all(cx: &mut TestAppContext) {
7201 init_test(cx, |_| {});
7202
7203 let editor = cx.add_window(|window, cx| {
7204 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7205 build_editor(buffer, window, cx)
7206 });
7207 _ = editor.update(cx, |editor, window, cx| {
7208 editor.select_all(&SelectAll, window, cx);
7209 assert_eq!(
7210 editor.selections.display_ranges(cx),
7211 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7212 );
7213 });
7214}
7215
7216#[gpui::test]
7217fn test_select_line(cx: &mut TestAppContext) {
7218 init_test(cx, |_| {});
7219
7220 let editor = cx.add_window(|window, cx| {
7221 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7222 build_editor(buffer, window, cx)
7223 });
7224 _ = editor.update(cx, |editor, window, cx| {
7225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7226 s.select_display_ranges([
7227 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7228 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7229 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7230 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7231 ])
7232 });
7233 editor.select_line(&SelectLine, window, cx);
7234 assert_eq!(
7235 editor.selections.display_ranges(cx),
7236 vec![
7237 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7238 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7239 ]
7240 );
7241 });
7242
7243 _ = editor.update(cx, |editor, window, cx| {
7244 editor.select_line(&SelectLine, window, cx);
7245 assert_eq!(
7246 editor.selections.display_ranges(cx),
7247 vec![
7248 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7249 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7250 ]
7251 );
7252 });
7253
7254 _ = editor.update(cx, |editor, window, cx| {
7255 editor.select_line(&SelectLine, window, cx);
7256 assert_eq!(
7257 editor.selections.display_ranges(cx),
7258 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7259 );
7260 });
7261}
7262
7263#[gpui::test]
7264async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7265 init_test(cx, |_| {});
7266 let mut cx = EditorTestContext::new(cx).await;
7267
7268 #[track_caller]
7269 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7270 cx.set_state(initial_state);
7271 cx.update_editor(|e, window, cx| {
7272 e.split_selection_into_lines(&Default::default(), window, cx)
7273 });
7274 cx.assert_editor_state(expected_state);
7275 }
7276
7277 // Selection starts and ends at the middle of lines, left-to-right
7278 test(
7279 &mut cx,
7280 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7281 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7282 );
7283 // Same thing, right-to-left
7284 test(
7285 &mut cx,
7286 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7287 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7288 );
7289
7290 // Whole buffer, left-to-right, last line *doesn't* end with newline
7291 test(
7292 &mut cx,
7293 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7294 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7295 );
7296 // Same thing, right-to-left
7297 test(
7298 &mut cx,
7299 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7300 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7301 );
7302
7303 // Whole buffer, left-to-right, last line ends with newline
7304 test(
7305 &mut cx,
7306 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7307 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7308 );
7309 // Same thing, right-to-left
7310 test(
7311 &mut cx,
7312 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7313 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7314 );
7315
7316 // Starts at the end of a line, ends at the start of another
7317 test(
7318 &mut cx,
7319 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7320 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7321 );
7322}
7323
7324#[gpui::test]
7325async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7326 init_test(cx, |_| {});
7327
7328 let editor = cx.add_window(|window, cx| {
7329 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7330 build_editor(buffer, window, cx)
7331 });
7332
7333 // setup
7334 _ = editor.update(cx, |editor, window, cx| {
7335 editor.fold_creases(
7336 vec![
7337 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7338 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7339 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7340 ],
7341 true,
7342 window,
7343 cx,
7344 );
7345 assert_eq!(
7346 editor.display_text(cx),
7347 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7348 );
7349 });
7350
7351 _ = editor.update(cx, |editor, window, cx| {
7352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7353 s.select_display_ranges([
7354 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7355 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7356 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7357 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7358 ])
7359 });
7360 editor.split_selection_into_lines(&Default::default(), window, cx);
7361 assert_eq!(
7362 editor.display_text(cx),
7363 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7364 );
7365 });
7366 EditorTestContext::for_editor(editor, cx)
7367 .await
7368 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7369
7370 _ = editor.update(cx, |editor, window, cx| {
7371 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7372 s.select_display_ranges([
7373 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7374 ])
7375 });
7376 editor.split_selection_into_lines(&Default::default(), window, cx);
7377 assert_eq!(
7378 editor.display_text(cx),
7379 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7380 );
7381 assert_eq!(
7382 editor.selections.display_ranges(cx),
7383 [
7384 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7385 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7386 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7387 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7388 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7389 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7390 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7391 ]
7392 );
7393 });
7394 EditorTestContext::for_editor(editor, cx)
7395 .await
7396 .assert_editor_state(
7397 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7398 );
7399}
7400
7401#[gpui::test]
7402async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7403 init_test(cx, |_| {});
7404
7405 let mut cx = EditorTestContext::new(cx).await;
7406
7407 cx.set_state(indoc!(
7408 r#"abc
7409 defˇghi
7410
7411 jk
7412 nlmo
7413 "#
7414 ));
7415
7416 cx.update_editor(|editor, window, cx| {
7417 editor.add_selection_above(&Default::default(), window, cx);
7418 });
7419
7420 cx.assert_editor_state(indoc!(
7421 r#"abcˇ
7422 defˇghi
7423
7424 jk
7425 nlmo
7426 "#
7427 ));
7428
7429 cx.update_editor(|editor, window, cx| {
7430 editor.add_selection_above(&Default::default(), window, cx);
7431 });
7432
7433 cx.assert_editor_state(indoc!(
7434 r#"abcˇ
7435 defˇghi
7436
7437 jk
7438 nlmo
7439 "#
7440 ));
7441
7442 cx.update_editor(|editor, window, cx| {
7443 editor.add_selection_below(&Default::default(), window, cx);
7444 });
7445
7446 cx.assert_editor_state(indoc!(
7447 r#"abc
7448 defˇghi
7449
7450 jk
7451 nlmo
7452 "#
7453 ));
7454
7455 cx.update_editor(|editor, window, cx| {
7456 editor.undo_selection(&Default::default(), window, cx);
7457 });
7458
7459 cx.assert_editor_state(indoc!(
7460 r#"abcˇ
7461 defˇghi
7462
7463 jk
7464 nlmo
7465 "#
7466 ));
7467
7468 cx.update_editor(|editor, window, cx| {
7469 editor.redo_selection(&Default::default(), window, cx);
7470 });
7471
7472 cx.assert_editor_state(indoc!(
7473 r#"abc
7474 defˇghi
7475
7476 jk
7477 nlmo
7478 "#
7479 ));
7480
7481 cx.update_editor(|editor, window, cx| {
7482 editor.add_selection_below(&Default::default(), window, cx);
7483 });
7484
7485 cx.assert_editor_state(indoc!(
7486 r#"abc
7487 defˇghi
7488 ˇ
7489 jk
7490 nlmo
7491 "#
7492 ));
7493
7494 cx.update_editor(|editor, window, cx| {
7495 editor.add_selection_below(&Default::default(), window, cx);
7496 });
7497
7498 cx.assert_editor_state(indoc!(
7499 r#"abc
7500 defˇghi
7501 ˇ
7502 jkˇ
7503 nlmo
7504 "#
7505 ));
7506
7507 cx.update_editor(|editor, window, cx| {
7508 editor.add_selection_below(&Default::default(), window, cx);
7509 });
7510
7511 cx.assert_editor_state(indoc!(
7512 r#"abc
7513 defˇghi
7514 ˇ
7515 jkˇ
7516 nlmˇo
7517 "#
7518 ));
7519
7520 cx.update_editor(|editor, window, cx| {
7521 editor.add_selection_below(&Default::default(), window, cx);
7522 });
7523
7524 cx.assert_editor_state(indoc!(
7525 r#"abc
7526 defˇghi
7527 ˇ
7528 jkˇ
7529 nlmˇo
7530 ˇ"#
7531 ));
7532
7533 // change selections
7534 cx.set_state(indoc!(
7535 r#"abc
7536 def«ˇg»hi
7537
7538 jk
7539 nlmo
7540 "#
7541 ));
7542
7543 cx.update_editor(|editor, window, cx| {
7544 editor.add_selection_below(&Default::default(), window, cx);
7545 });
7546
7547 cx.assert_editor_state(indoc!(
7548 r#"abc
7549 def«ˇg»hi
7550
7551 jk
7552 nlm«ˇo»
7553 "#
7554 ));
7555
7556 cx.update_editor(|editor, window, cx| {
7557 editor.add_selection_below(&Default::default(), window, cx);
7558 });
7559
7560 cx.assert_editor_state(indoc!(
7561 r#"abc
7562 def«ˇg»hi
7563
7564 jk
7565 nlm«ˇo»
7566 "#
7567 ));
7568
7569 cx.update_editor(|editor, window, cx| {
7570 editor.add_selection_above(&Default::default(), window, cx);
7571 });
7572
7573 cx.assert_editor_state(indoc!(
7574 r#"abc
7575 def«ˇg»hi
7576
7577 jk
7578 nlmo
7579 "#
7580 ));
7581
7582 cx.update_editor(|editor, window, cx| {
7583 editor.add_selection_above(&Default::default(), window, cx);
7584 });
7585
7586 cx.assert_editor_state(indoc!(
7587 r#"abc
7588 def«ˇg»hi
7589
7590 jk
7591 nlmo
7592 "#
7593 ));
7594
7595 // Change selections again
7596 cx.set_state(indoc!(
7597 r#"a«bc
7598 defgˇ»hi
7599
7600 jk
7601 nlmo
7602 "#
7603 ));
7604
7605 cx.update_editor(|editor, window, cx| {
7606 editor.add_selection_below(&Default::default(), window, cx);
7607 });
7608
7609 cx.assert_editor_state(indoc!(
7610 r#"a«bcˇ»
7611 d«efgˇ»hi
7612
7613 j«kˇ»
7614 nlmo
7615 "#
7616 ));
7617
7618 cx.update_editor(|editor, window, cx| {
7619 editor.add_selection_below(&Default::default(), window, cx);
7620 });
7621 cx.assert_editor_state(indoc!(
7622 r#"a«bcˇ»
7623 d«efgˇ»hi
7624
7625 j«kˇ»
7626 n«lmoˇ»
7627 "#
7628 ));
7629 cx.update_editor(|editor, window, cx| {
7630 editor.add_selection_above(&Default::default(), window, cx);
7631 });
7632
7633 cx.assert_editor_state(indoc!(
7634 r#"a«bcˇ»
7635 d«efgˇ»hi
7636
7637 j«kˇ»
7638 nlmo
7639 "#
7640 ));
7641
7642 // Change selections again
7643 cx.set_state(indoc!(
7644 r#"abc
7645 d«ˇefghi
7646
7647 jk
7648 nlm»o
7649 "#
7650 ));
7651
7652 cx.update_editor(|editor, window, cx| {
7653 editor.add_selection_above(&Default::default(), window, cx);
7654 });
7655
7656 cx.assert_editor_state(indoc!(
7657 r#"a«ˇbc»
7658 d«ˇef»ghi
7659
7660 j«ˇk»
7661 n«ˇlm»o
7662 "#
7663 ));
7664
7665 cx.update_editor(|editor, window, cx| {
7666 editor.add_selection_below(&Default::default(), window, cx);
7667 });
7668
7669 cx.assert_editor_state(indoc!(
7670 r#"abc
7671 d«ˇef»ghi
7672
7673 j«ˇk»
7674 n«ˇlm»o
7675 "#
7676 ));
7677}
7678
7679#[gpui::test]
7680async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7681 init_test(cx, |_| {});
7682 let mut cx = EditorTestContext::new(cx).await;
7683
7684 cx.set_state(indoc!(
7685 r#"line onˇe
7686 liˇne two
7687 line three
7688 line four"#
7689 ));
7690
7691 cx.update_editor(|editor, window, cx| {
7692 editor.add_selection_below(&Default::default(), window, cx);
7693 });
7694
7695 // test multiple cursors expand in the same direction
7696 cx.assert_editor_state(indoc!(
7697 r#"line onˇe
7698 liˇne twˇo
7699 liˇne three
7700 line four"#
7701 ));
7702
7703 cx.update_editor(|editor, window, cx| {
7704 editor.add_selection_below(&Default::default(), window, cx);
7705 });
7706
7707 cx.update_editor(|editor, window, cx| {
7708 editor.add_selection_below(&Default::default(), window, cx);
7709 });
7710
7711 // test multiple cursors expand below overflow
7712 cx.assert_editor_state(indoc!(
7713 r#"line onˇe
7714 liˇne twˇo
7715 liˇne thˇree
7716 liˇne foˇur"#
7717 ));
7718
7719 cx.update_editor(|editor, window, cx| {
7720 editor.add_selection_above(&Default::default(), window, cx);
7721 });
7722
7723 // test multiple cursors retrieves back correctly
7724 cx.assert_editor_state(indoc!(
7725 r#"line onˇe
7726 liˇne twˇo
7727 liˇne thˇree
7728 line four"#
7729 ));
7730
7731 cx.update_editor(|editor, window, cx| {
7732 editor.add_selection_above(&Default::default(), window, cx);
7733 });
7734
7735 cx.update_editor(|editor, window, cx| {
7736 editor.add_selection_above(&Default::default(), window, cx);
7737 });
7738
7739 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7740 cx.assert_editor_state(indoc!(
7741 r#"liˇne onˇe
7742 liˇne two
7743 line three
7744 line four"#
7745 ));
7746
7747 cx.update_editor(|editor, window, cx| {
7748 editor.undo_selection(&Default::default(), window, cx);
7749 });
7750
7751 // test undo
7752 cx.assert_editor_state(indoc!(
7753 r#"line onˇe
7754 liˇne twˇo
7755 line three
7756 line four"#
7757 ));
7758
7759 cx.update_editor(|editor, window, cx| {
7760 editor.redo_selection(&Default::default(), window, cx);
7761 });
7762
7763 // test redo
7764 cx.assert_editor_state(indoc!(
7765 r#"liˇne onˇe
7766 liˇne two
7767 line three
7768 line four"#
7769 ));
7770
7771 cx.set_state(indoc!(
7772 r#"abcd
7773 ef«ghˇ»
7774 ijkl
7775 «mˇ»nop"#
7776 ));
7777
7778 cx.update_editor(|editor, window, cx| {
7779 editor.add_selection_above(&Default::default(), window, cx);
7780 });
7781
7782 // test multiple selections expand in the same direction
7783 cx.assert_editor_state(indoc!(
7784 r#"ab«cdˇ»
7785 ef«ghˇ»
7786 «iˇ»jkl
7787 «mˇ»nop"#
7788 ));
7789
7790 cx.update_editor(|editor, window, cx| {
7791 editor.add_selection_above(&Default::default(), window, cx);
7792 });
7793
7794 // test multiple selection upward overflow
7795 cx.assert_editor_state(indoc!(
7796 r#"ab«cdˇ»
7797 «eˇ»f«ghˇ»
7798 «iˇ»jkl
7799 «mˇ»nop"#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.add_selection_below(&Default::default(), window, cx);
7804 });
7805
7806 // test multiple selection retrieves back correctly
7807 cx.assert_editor_state(indoc!(
7808 r#"abcd
7809 ef«ghˇ»
7810 «iˇ»jkl
7811 «mˇ»nop"#
7812 ));
7813
7814 cx.update_editor(|editor, window, cx| {
7815 editor.add_selection_below(&Default::default(), window, cx);
7816 });
7817
7818 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7819 cx.assert_editor_state(indoc!(
7820 r#"abcd
7821 ef«ghˇ»
7822 ij«klˇ»
7823 «mˇ»nop"#
7824 ));
7825
7826 cx.update_editor(|editor, window, cx| {
7827 editor.undo_selection(&Default::default(), window, cx);
7828 });
7829
7830 // test undo
7831 cx.assert_editor_state(indoc!(
7832 r#"abcd
7833 ef«ghˇ»
7834 «iˇ»jkl
7835 «mˇ»nop"#
7836 ));
7837
7838 cx.update_editor(|editor, window, cx| {
7839 editor.redo_selection(&Default::default(), window, cx);
7840 });
7841
7842 // test redo
7843 cx.assert_editor_state(indoc!(
7844 r#"abcd
7845 ef«ghˇ»
7846 ij«klˇ»
7847 «mˇ»nop"#
7848 ));
7849}
7850
7851#[gpui::test]
7852async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7853 init_test(cx, |_| {});
7854 let mut cx = EditorTestContext::new(cx).await;
7855
7856 cx.set_state(indoc!(
7857 r#"line onˇe
7858 liˇne two
7859 line three
7860 line four"#
7861 ));
7862
7863 cx.update_editor(|editor, window, cx| {
7864 editor.add_selection_below(&Default::default(), window, cx);
7865 editor.add_selection_below(&Default::default(), window, cx);
7866 editor.add_selection_below(&Default::default(), window, cx);
7867 });
7868
7869 // initial state with two multi cursor groups
7870 cx.assert_editor_state(indoc!(
7871 r#"line onˇe
7872 liˇne twˇo
7873 liˇne thˇree
7874 liˇne foˇur"#
7875 ));
7876
7877 // add single cursor in middle - simulate opt click
7878 cx.update_editor(|editor, window, cx| {
7879 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7880 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7881 editor.end_selection(window, cx);
7882 });
7883
7884 cx.assert_editor_state(indoc!(
7885 r#"line onˇe
7886 liˇne twˇo
7887 liˇneˇ thˇree
7888 liˇne foˇur"#
7889 ));
7890
7891 cx.update_editor(|editor, window, cx| {
7892 editor.add_selection_above(&Default::default(), window, cx);
7893 });
7894
7895 // test new added selection expands above and existing selection shrinks
7896 cx.assert_editor_state(indoc!(
7897 r#"line onˇe
7898 liˇneˇ twˇo
7899 liˇneˇ thˇree
7900 line four"#
7901 ));
7902
7903 cx.update_editor(|editor, window, cx| {
7904 editor.add_selection_above(&Default::default(), window, cx);
7905 });
7906
7907 // test new added selection expands above and existing selection shrinks
7908 cx.assert_editor_state(indoc!(
7909 r#"lineˇ onˇe
7910 liˇneˇ twˇo
7911 lineˇ three
7912 line four"#
7913 ));
7914
7915 // intial state with two selection groups
7916 cx.set_state(indoc!(
7917 r#"abcd
7918 ef«ghˇ»
7919 ijkl
7920 «mˇ»nop"#
7921 ));
7922
7923 cx.update_editor(|editor, window, cx| {
7924 editor.add_selection_above(&Default::default(), window, cx);
7925 editor.add_selection_above(&Default::default(), window, cx);
7926 });
7927
7928 cx.assert_editor_state(indoc!(
7929 r#"ab«cdˇ»
7930 «eˇ»f«ghˇ»
7931 «iˇ»jkl
7932 «mˇ»nop"#
7933 ));
7934
7935 // add single selection in middle - simulate opt drag
7936 cx.update_editor(|editor, window, cx| {
7937 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7938 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7939 editor.update_selection(
7940 DisplayPoint::new(DisplayRow(2), 4),
7941 0,
7942 gpui::Point::<f32>::default(),
7943 window,
7944 cx,
7945 );
7946 editor.end_selection(window, cx);
7947 });
7948
7949 cx.assert_editor_state(indoc!(
7950 r#"ab«cdˇ»
7951 «eˇ»f«ghˇ»
7952 «iˇ»jk«lˇ»
7953 «mˇ»nop"#
7954 ));
7955
7956 cx.update_editor(|editor, window, cx| {
7957 editor.add_selection_below(&Default::default(), window, cx);
7958 });
7959
7960 // test new added selection expands below, others shrinks from above
7961 cx.assert_editor_state(indoc!(
7962 r#"abcd
7963 ef«ghˇ»
7964 «iˇ»jk«lˇ»
7965 «mˇ»no«pˇ»"#
7966 ));
7967}
7968
7969#[gpui::test]
7970async fn test_select_next(cx: &mut TestAppContext) {
7971 init_test(cx, |_| {});
7972
7973 let mut cx = EditorTestContext::new(cx).await;
7974 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7975
7976 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7977 .unwrap();
7978 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7979
7980 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7981 .unwrap();
7982 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7983
7984 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7985 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7986
7987 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7988 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7989
7990 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7991 .unwrap();
7992 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7993
7994 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7995 .unwrap();
7996 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7997
7998 // Test selection direction should be preserved
7999 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8000
8001 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8002 .unwrap();
8003 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8004}
8005
8006#[gpui::test]
8007async fn test_select_all_matches(cx: &mut TestAppContext) {
8008 init_test(cx, |_| {});
8009
8010 let mut cx = EditorTestContext::new(cx).await;
8011
8012 // Test caret-only selections
8013 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8014 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8015 .unwrap();
8016 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8017
8018 // Test left-to-right selections
8019 cx.set_state("abc\n«abcˇ»\nabc");
8020 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8021 .unwrap();
8022 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8023
8024 // Test right-to-left selections
8025 cx.set_state("abc\n«ˇabc»\nabc");
8026 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8027 .unwrap();
8028 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8029
8030 // Test selecting whitespace with caret selection
8031 cx.set_state("abc\nˇ abc\nabc");
8032 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8033 .unwrap();
8034 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8035
8036 // Test selecting whitespace with left-to-right selection
8037 cx.set_state("abc\n«ˇ »abc\nabc");
8038 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8039 .unwrap();
8040 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8041
8042 // Test no matches with right-to-left selection
8043 cx.set_state("abc\n« ˇ»abc\nabc");
8044 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8045 .unwrap();
8046 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8047
8048 // Test with a single word and clip_at_line_ends=true (#29823)
8049 cx.set_state("aˇbc");
8050 cx.update_editor(|e, window, cx| {
8051 e.set_clip_at_line_ends(true, cx);
8052 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8053 e.set_clip_at_line_ends(false, cx);
8054 });
8055 cx.assert_editor_state("«abcˇ»");
8056}
8057
8058#[gpui::test]
8059async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8060 init_test(cx, |_| {});
8061
8062 let mut cx = EditorTestContext::new(cx).await;
8063
8064 let large_body_1 = "\nd".repeat(200);
8065 let large_body_2 = "\ne".repeat(200);
8066
8067 cx.set_state(&format!(
8068 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8069 ));
8070 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8071 let scroll_position = editor.scroll_position(cx);
8072 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8073 scroll_position
8074 });
8075
8076 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8077 .unwrap();
8078 cx.assert_editor_state(&format!(
8079 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8080 ));
8081 let scroll_position_after_selection =
8082 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8083 assert_eq!(
8084 initial_scroll_position, scroll_position_after_selection,
8085 "Scroll position should not change after selecting all matches"
8086 );
8087}
8088
8089#[gpui::test]
8090async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8091 init_test(cx, |_| {});
8092
8093 let mut cx = EditorLspTestContext::new_rust(
8094 lsp::ServerCapabilities {
8095 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8096 ..Default::default()
8097 },
8098 cx,
8099 )
8100 .await;
8101
8102 cx.set_state(indoc! {"
8103 line 1
8104 line 2
8105 linˇe 3
8106 line 4
8107 line 5
8108 "});
8109
8110 // Make an edit
8111 cx.update_editor(|editor, window, cx| {
8112 editor.handle_input("X", window, cx);
8113 });
8114
8115 // Move cursor to a different position
8116 cx.update_editor(|editor, window, cx| {
8117 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8118 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8119 });
8120 });
8121
8122 cx.assert_editor_state(indoc! {"
8123 line 1
8124 line 2
8125 linXe 3
8126 line 4
8127 liˇne 5
8128 "});
8129
8130 cx.lsp
8131 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8132 Ok(Some(vec![lsp::TextEdit::new(
8133 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8134 "PREFIX ".to_string(),
8135 )]))
8136 });
8137
8138 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8139 .unwrap()
8140 .await
8141 .unwrap();
8142
8143 cx.assert_editor_state(indoc! {"
8144 PREFIX line 1
8145 line 2
8146 linXe 3
8147 line 4
8148 liˇne 5
8149 "});
8150
8151 // Undo formatting
8152 cx.update_editor(|editor, window, cx| {
8153 editor.undo(&Default::default(), window, cx);
8154 });
8155
8156 // Verify cursor moved back to position after edit
8157 cx.assert_editor_state(indoc! {"
8158 line 1
8159 line 2
8160 linXˇe 3
8161 line 4
8162 line 5
8163 "});
8164}
8165
8166#[gpui::test]
8167async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8168 init_test(cx, |_| {});
8169
8170 let mut cx = EditorTestContext::new(cx).await;
8171
8172 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8173 cx.update_editor(|editor, window, cx| {
8174 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8175 });
8176
8177 cx.set_state(indoc! {"
8178 line 1
8179 line 2
8180 linˇe 3
8181 line 4
8182 line 5
8183 line 6
8184 line 7
8185 line 8
8186 line 9
8187 line 10
8188 "});
8189
8190 let snapshot = cx.buffer_snapshot();
8191 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8192
8193 cx.update(|_, cx| {
8194 provider.update(cx, |provider, _| {
8195 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8196 id: None,
8197 edits: vec![(edit_position..edit_position, "X".into())],
8198 edit_preview: None,
8199 }))
8200 })
8201 });
8202
8203 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8204 cx.update_editor(|editor, window, cx| {
8205 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8206 });
8207
8208 cx.assert_editor_state(indoc! {"
8209 line 1
8210 line 2
8211 lineXˇ 3
8212 line 4
8213 line 5
8214 line 6
8215 line 7
8216 line 8
8217 line 9
8218 line 10
8219 "});
8220
8221 cx.update_editor(|editor, window, cx| {
8222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8223 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8224 });
8225 });
8226
8227 cx.assert_editor_state(indoc! {"
8228 line 1
8229 line 2
8230 lineX 3
8231 line 4
8232 line 5
8233 line 6
8234 line 7
8235 line 8
8236 line 9
8237 liˇne 10
8238 "});
8239
8240 cx.update_editor(|editor, window, cx| {
8241 editor.undo(&Default::default(), window, cx);
8242 });
8243
8244 cx.assert_editor_state(indoc! {"
8245 line 1
8246 line 2
8247 lineˇ 3
8248 line 4
8249 line 5
8250 line 6
8251 line 7
8252 line 8
8253 line 9
8254 line 10
8255 "});
8256}
8257
8258#[gpui::test]
8259async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8260 init_test(cx, |_| {});
8261
8262 let mut cx = EditorTestContext::new(cx).await;
8263 cx.set_state(
8264 r#"let foo = 2;
8265lˇet foo = 2;
8266let fooˇ = 2;
8267let foo = 2;
8268let foo = ˇ2;"#,
8269 );
8270
8271 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8272 .unwrap();
8273 cx.assert_editor_state(
8274 r#"let foo = 2;
8275«letˇ» foo = 2;
8276let «fooˇ» = 2;
8277let foo = 2;
8278let foo = «2ˇ»;"#,
8279 );
8280
8281 // noop for multiple selections with different contents
8282 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8283 .unwrap();
8284 cx.assert_editor_state(
8285 r#"let foo = 2;
8286«letˇ» foo = 2;
8287let «fooˇ» = 2;
8288let foo = 2;
8289let foo = «2ˇ»;"#,
8290 );
8291
8292 // Test last selection direction should be preserved
8293 cx.set_state(
8294 r#"let foo = 2;
8295let foo = 2;
8296let «fooˇ» = 2;
8297let «ˇfoo» = 2;
8298let foo = 2;"#,
8299 );
8300
8301 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8302 .unwrap();
8303 cx.assert_editor_state(
8304 r#"let foo = 2;
8305let foo = 2;
8306let «fooˇ» = 2;
8307let «ˇfoo» = 2;
8308let «ˇfoo» = 2;"#,
8309 );
8310}
8311
8312#[gpui::test]
8313async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8314 init_test(cx, |_| {});
8315
8316 let mut cx =
8317 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8318
8319 cx.assert_editor_state(indoc! {"
8320 ˇbbb
8321 ccc
8322
8323 bbb
8324 ccc
8325 "});
8326 cx.dispatch_action(SelectPrevious::default());
8327 cx.assert_editor_state(indoc! {"
8328 «bbbˇ»
8329 ccc
8330
8331 bbb
8332 ccc
8333 "});
8334 cx.dispatch_action(SelectPrevious::default());
8335 cx.assert_editor_state(indoc! {"
8336 «bbbˇ»
8337 ccc
8338
8339 «bbbˇ»
8340 ccc
8341 "});
8342}
8343
8344#[gpui::test]
8345async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8346 init_test(cx, |_| {});
8347
8348 let mut cx = EditorTestContext::new(cx).await;
8349 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8350
8351 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8352 .unwrap();
8353 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8354
8355 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8356 .unwrap();
8357 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8358
8359 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8360 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8361
8362 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8363 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8364
8365 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8366 .unwrap();
8367 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8368
8369 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8370 .unwrap();
8371 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8372}
8373
8374#[gpui::test]
8375async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8376 init_test(cx, |_| {});
8377
8378 let mut cx = EditorTestContext::new(cx).await;
8379 cx.set_state("aˇ");
8380
8381 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8382 .unwrap();
8383 cx.assert_editor_state("«aˇ»");
8384 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8385 .unwrap();
8386 cx.assert_editor_state("«aˇ»");
8387}
8388
8389#[gpui::test]
8390async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8391 init_test(cx, |_| {});
8392
8393 let mut cx = EditorTestContext::new(cx).await;
8394 cx.set_state(
8395 r#"let foo = 2;
8396lˇet foo = 2;
8397let fooˇ = 2;
8398let foo = 2;
8399let foo = ˇ2;"#,
8400 );
8401
8402 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8403 .unwrap();
8404 cx.assert_editor_state(
8405 r#"let foo = 2;
8406«letˇ» foo = 2;
8407let «fooˇ» = 2;
8408let foo = 2;
8409let foo = «2ˇ»;"#,
8410 );
8411
8412 // noop for multiple selections with different contents
8413 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8414 .unwrap();
8415 cx.assert_editor_state(
8416 r#"let foo = 2;
8417«letˇ» foo = 2;
8418let «fooˇ» = 2;
8419let foo = 2;
8420let foo = «2ˇ»;"#,
8421 );
8422}
8423
8424#[gpui::test]
8425async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8426 init_test(cx, |_| {});
8427
8428 let mut cx = EditorTestContext::new(cx).await;
8429 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8430
8431 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8432 .unwrap();
8433 // selection direction is preserved
8434 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8435
8436 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8437 .unwrap();
8438 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8439
8440 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8441 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8442
8443 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8444 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8445
8446 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8447 .unwrap();
8448 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8449
8450 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8451 .unwrap();
8452 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8453}
8454
8455#[gpui::test]
8456async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8457 init_test(cx, |_| {});
8458
8459 let language = Arc::new(Language::new(
8460 LanguageConfig::default(),
8461 Some(tree_sitter_rust::LANGUAGE.into()),
8462 ));
8463
8464 let text = r#"
8465 use mod1::mod2::{mod3, mod4};
8466
8467 fn fn_1(param1: bool, param2: &str) {
8468 let var1 = "text";
8469 }
8470 "#
8471 .unindent();
8472
8473 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8474 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8475 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8476
8477 editor
8478 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8479 .await;
8480
8481 editor.update_in(cx, |editor, window, cx| {
8482 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8483 s.select_display_ranges([
8484 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8485 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8486 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8487 ]);
8488 });
8489 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8490 });
8491 editor.update(cx, |editor, cx| {
8492 assert_text_with_selections(
8493 editor,
8494 indoc! {r#"
8495 use mod1::mod2::{mod3, «mod4ˇ»};
8496
8497 fn fn_1«ˇ(param1: bool, param2: &str)» {
8498 let var1 = "«ˇtext»";
8499 }
8500 "#},
8501 cx,
8502 );
8503 });
8504
8505 editor.update_in(cx, |editor, window, cx| {
8506 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8507 });
8508 editor.update(cx, |editor, cx| {
8509 assert_text_with_selections(
8510 editor,
8511 indoc! {r#"
8512 use mod1::mod2::«{mod3, mod4}ˇ»;
8513
8514 «ˇfn fn_1(param1: bool, param2: &str) {
8515 let var1 = "text";
8516 }»
8517 "#},
8518 cx,
8519 );
8520 });
8521
8522 editor.update_in(cx, |editor, window, cx| {
8523 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8524 });
8525 assert_eq!(
8526 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8527 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8528 );
8529
8530 // Trying to expand the selected syntax node one more time has no effect.
8531 editor.update_in(cx, |editor, window, cx| {
8532 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8533 });
8534 assert_eq!(
8535 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8536 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8537 );
8538
8539 editor.update_in(cx, |editor, window, cx| {
8540 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8541 });
8542 editor.update(cx, |editor, cx| {
8543 assert_text_with_selections(
8544 editor,
8545 indoc! {r#"
8546 use mod1::mod2::«{mod3, mod4}ˇ»;
8547
8548 «ˇfn fn_1(param1: bool, param2: &str) {
8549 let var1 = "text";
8550 }»
8551 "#},
8552 cx,
8553 );
8554 });
8555
8556 editor.update_in(cx, |editor, window, cx| {
8557 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8558 });
8559 editor.update(cx, |editor, cx| {
8560 assert_text_with_selections(
8561 editor,
8562 indoc! {r#"
8563 use mod1::mod2::{mod3, «mod4ˇ»};
8564
8565 fn fn_1«ˇ(param1: bool, param2: &str)» {
8566 let var1 = "«ˇtext»";
8567 }
8568 "#},
8569 cx,
8570 );
8571 });
8572
8573 editor.update_in(cx, |editor, window, cx| {
8574 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8575 });
8576 editor.update(cx, |editor, cx| {
8577 assert_text_with_selections(
8578 editor,
8579 indoc! {r#"
8580 use mod1::mod2::{mod3, mo«ˇ»d4};
8581
8582 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8583 let var1 = "te«ˇ»xt";
8584 }
8585 "#},
8586 cx,
8587 );
8588 });
8589
8590 // Trying to shrink the selected syntax node one more time has no effect.
8591 editor.update_in(cx, |editor, window, cx| {
8592 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8593 });
8594 editor.update_in(cx, |editor, _, cx| {
8595 assert_text_with_selections(
8596 editor,
8597 indoc! {r#"
8598 use mod1::mod2::{mod3, mo«ˇ»d4};
8599
8600 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8601 let var1 = "te«ˇ»xt";
8602 }
8603 "#},
8604 cx,
8605 );
8606 });
8607
8608 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8609 // a fold.
8610 editor.update_in(cx, |editor, window, cx| {
8611 editor.fold_creases(
8612 vec![
8613 Crease::simple(
8614 Point::new(0, 21)..Point::new(0, 24),
8615 FoldPlaceholder::test(),
8616 ),
8617 Crease::simple(
8618 Point::new(3, 20)..Point::new(3, 22),
8619 FoldPlaceholder::test(),
8620 ),
8621 ],
8622 true,
8623 window,
8624 cx,
8625 );
8626 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8627 });
8628 editor.update(cx, |editor, cx| {
8629 assert_text_with_selections(
8630 editor,
8631 indoc! {r#"
8632 use mod1::mod2::«{mod3, mod4}ˇ»;
8633
8634 fn fn_1«ˇ(param1: bool, param2: &str)» {
8635 let var1 = "«ˇtext»";
8636 }
8637 "#},
8638 cx,
8639 );
8640 });
8641}
8642
8643#[gpui::test]
8644async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8645 init_test(cx, |_| {});
8646
8647 let language = Arc::new(Language::new(
8648 LanguageConfig::default(),
8649 Some(tree_sitter_rust::LANGUAGE.into()),
8650 ));
8651
8652 let text = "let a = 2;";
8653
8654 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8655 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8656 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8657
8658 editor
8659 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8660 .await;
8661
8662 // Test case 1: Cursor at end of word
8663 editor.update_in(cx, |editor, window, cx| {
8664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8665 s.select_display_ranges([
8666 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8667 ]);
8668 });
8669 });
8670 editor.update(cx, |editor, cx| {
8671 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8672 });
8673 editor.update_in(cx, |editor, window, cx| {
8674 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8675 });
8676 editor.update(cx, |editor, cx| {
8677 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8678 });
8679 editor.update_in(cx, |editor, window, cx| {
8680 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8681 });
8682 editor.update(cx, |editor, cx| {
8683 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8684 });
8685
8686 // Test case 2: Cursor at end of statement
8687 editor.update_in(cx, |editor, window, cx| {
8688 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8689 s.select_display_ranges([
8690 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8691 ]);
8692 });
8693 });
8694 editor.update(cx, |editor, cx| {
8695 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8696 });
8697 editor.update_in(cx, |editor, window, cx| {
8698 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8699 });
8700 editor.update(cx, |editor, cx| {
8701 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8702 });
8703}
8704
8705#[gpui::test]
8706async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8707 init_test(cx, |_| {});
8708
8709 let language = Arc::new(Language::new(
8710 LanguageConfig::default(),
8711 Some(tree_sitter_rust::LANGUAGE.into()),
8712 ));
8713
8714 let text = r#"
8715 use mod1::mod2::{mod3, mod4};
8716
8717 fn fn_1(param1: bool, param2: &str) {
8718 let var1 = "hello world";
8719 }
8720 "#
8721 .unindent();
8722
8723 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8724 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8725 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8726
8727 editor
8728 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8729 .await;
8730
8731 // Test 1: Cursor on a letter of a string word
8732 editor.update_in(cx, |editor, window, cx| {
8733 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8734 s.select_display_ranges([
8735 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8736 ]);
8737 });
8738 });
8739 editor.update_in(cx, |editor, window, cx| {
8740 assert_text_with_selections(
8741 editor,
8742 indoc! {r#"
8743 use mod1::mod2::{mod3, mod4};
8744
8745 fn fn_1(param1: bool, param2: &str) {
8746 let var1 = "hˇello world";
8747 }
8748 "#},
8749 cx,
8750 );
8751 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8752 assert_text_with_selections(
8753 editor,
8754 indoc! {r#"
8755 use mod1::mod2::{mod3, mod4};
8756
8757 fn fn_1(param1: bool, param2: &str) {
8758 let var1 = "«ˇhello» world";
8759 }
8760 "#},
8761 cx,
8762 );
8763 });
8764
8765 // Test 2: Partial selection within a word
8766 editor.update_in(cx, |editor, window, cx| {
8767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8768 s.select_display_ranges([
8769 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
8770 ]);
8771 });
8772 });
8773 editor.update_in(cx, |editor, window, cx| {
8774 assert_text_with_selections(
8775 editor,
8776 indoc! {r#"
8777 use mod1::mod2::{mod3, mod4};
8778
8779 fn fn_1(param1: bool, param2: &str) {
8780 let var1 = "h«elˇ»lo world";
8781 }
8782 "#},
8783 cx,
8784 );
8785 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8786 assert_text_with_selections(
8787 editor,
8788 indoc! {r#"
8789 use mod1::mod2::{mod3, mod4};
8790
8791 fn fn_1(param1: bool, param2: &str) {
8792 let var1 = "«ˇhello» world";
8793 }
8794 "#},
8795 cx,
8796 );
8797 });
8798
8799 // Test 3: Complete word already selected
8800 editor.update_in(cx, |editor, window, cx| {
8801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8802 s.select_display_ranges([
8803 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
8804 ]);
8805 });
8806 });
8807 editor.update_in(cx, |editor, window, cx| {
8808 assert_text_with_selections(
8809 editor,
8810 indoc! {r#"
8811 use mod1::mod2::{mod3, mod4};
8812
8813 fn fn_1(param1: bool, param2: &str) {
8814 let var1 = "«helloˇ» world";
8815 }
8816 "#},
8817 cx,
8818 );
8819 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8820 assert_text_with_selections(
8821 editor,
8822 indoc! {r#"
8823 use mod1::mod2::{mod3, mod4};
8824
8825 fn fn_1(param1: bool, param2: &str) {
8826 let var1 = "«hello worldˇ»";
8827 }
8828 "#},
8829 cx,
8830 );
8831 });
8832
8833 // Test 4: Selection spanning across words
8834 editor.update_in(cx, |editor, window, cx| {
8835 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8836 s.select_display_ranges([
8837 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
8838 ]);
8839 });
8840 });
8841 editor.update_in(cx, |editor, window, cx| {
8842 assert_text_with_selections(
8843 editor,
8844 indoc! {r#"
8845 use mod1::mod2::{mod3, mod4};
8846
8847 fn fn_1(param1: bool, param2: &str) {
8848 let var1 = "hel«lo woˇ»rld";
8849 }
8850 "#},
8851 cx,
8852 );
8853 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8854 assert_text_with_selections(
8855 editor,
8856 indoc! {r#"
8857 use mod1::mod2::{mod3, mod4};
8858
8859 fn fn_1(param1: bool, param2: &str) {
8860 let var1 = "«ˇhello world»";
8861 }
8862 "#},
8863 cx,
8864 );
8865 });
8866
8867 // Test 5: Expansion beyond string
8868 editor.update_in(cx, |editor, window, cx| {
8869 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8870 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8871 assert_text_with_selections(
8872 editor,
8873 indoc! {r#"
8874 use mod1::mod2::{mod3, mod4};
8875
8876 fn fn_1(param1: bool, param2: &str) {
8877 «ˇlet var1 = "hello world";»
8878 }
8879 "#},
8880 cx,
8881 );
8882 });
8883}
8884
8885#[gpui::test]
8886async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
8887 init_test(cx, |_| {});
8888
8889 let mut cx = EditorTestContext::new(cx).await;
8890
8891 let language = Arc::new(Language::new(
8892 LanguageConfig::default(),
8893 Some(tree_sitter_rust::LANGUAGE.into()),
8894 ));
8895
8896 cx.update_buffer(|buffer, cx| {
8897 buffer.set_language(Some(language), cx);
8898 });
8899
8900 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
8901 cx.update_editor(|editor, window, cx| {
8902 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8903 });
8904
8905 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
8906}
8907
8908#[gpui::test]
8909async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8910 init_test(cx, |_| {});
8911
8912 let base_text = r#"
8913 impl A {
8914 // this is an uncommitted comment
8915
8916 fn b() {
8917 c();
8918 }
8919
8920 // this is another uncommitted comment
8921
8922 fn d() {
8923 // e
8924 // f
8925 }
8926 }
8927
8928 fn g() {
8929 // h
8930 }
8931 "#
8932 .unindent();
8933
8934 let text = r#"
8935 ˇimpl A {
8936
8937 fn b() {
8938 c();
8939 }
8940
8941 fn d() {
8942 // e
8943 // f
8944 }
8945 }
8946
8947 fn g() {
8948 // h
8949 }
8950 "#
8951 .unindent();
8952
8953 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8954 cx.set_state(&text);
8955 cx.set_head_text(&base_text);
8956 cx.update_editor(|editor, window, cx| {
8957 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8958 });
8959
8960 cx.assert_state_with_diff(
8961 "
8962 ˇimpl A {
8963 - // this is an uncommitted comment
8964
8965 fn b() {
8966 c();
8967 }
8968
8969 - // this is another uncommitted comment
8970 -
8971 fn d() {
8972 // e
8973 // f
8974 }
8975 }
8976
8977 fn g() {
8978 // h
8979 }
8980 "
8981 .unindent(),
8982 );
8983
8984 let expected_display_text = "
8985 impl A {
8986 // this is an uncommitted comment
8987
8988 fn b() {
8989 ⋯
8990 }
8991
8992 // this is another uncommitted comment
8993
8994 fn d() {
8995 ⋯
8996 }
8997 }
8998
8999 fn g() {
9000 ⋯
9001 }
9002 "
9003 .unindent();
9004
9005 cx.update_editor(|editor, window, cx| {
9006 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9007 assert_eq!(editor.display_text(cx), expected_display_text);
9008 });
9009}
9010
9011#[gpui::test]
9012async fn test_autoindent(cx: &mut TestAppContext) {
9013 init_test(cx, |_| {});
9014
9015 let language = Arc::new(
9016 Language::new(
9017 LanguageConfig {
9018 brackets: BracketPairConfig {
9019 pairs: vec![
9020 BracketPair {
9021 start: "{".to_string(),
9022 end: "}".to_string(),
9023 close: false,
9024 surround: false,
9025 newline: true,
9026 },
9027 BracketPair {
9028 start: "(".to_string(),
9029 end: ")".to_string(),
9030 close: false,
9031 surround: false,
9032 newline: true,
9033 },
9034 ],
9035 ..Default::default()
9036 },
9037 ..Default::default()
9038 },
9039 Some(tree_sitter_rust::LANGUAGE.into()),
9040 )
9041 .with_indents_query(
9042 r#"
9043 (_ "(" ")" @end) @indent
9044 (_ "{" "}" @end) @indent
9045 "#,
9046 )
9047 .unwrap(),
9048 );
9049
9050 let text = "fn a() {}";
9051
9052 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9053 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9054 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9055 editor
9056 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9057 .await;
9058
9059 editor.update_in(cx, |editor, window, cx| {
9060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9061 s.select_ranges([5..5, 8..8, 9..9])
9062 });
9063 editor.newline(&Newline, window, cx);
9064 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9065 assert_eq!(
9066 editor.selections.ranges(cx),
9067 &[
9068 Point::new(1, 4)..Point::new(1, 4),
9069 Point::new(3, 4)..Point::new(3, 4),
9070 Point::new(5, 0)..Point::new(5, 0)
9071 ]
9072 );
9073 });
9074}
9075
9076#[gpui::test]
9077async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9078 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9079
9080 let language = Arc::new(
9081 Language::new(
9082 LanguageConfig {
9083 brackets: BracketPairConfig {
9084 pairs: vec![
9085 BracketPair {
9086 start: "{".to_string(),
9087 end: "}".to_string(),
9088 close: false,
9089 surround: false,
9090 newline: true,
9091 },
9092 BracketPair {
9093 start: "(".to_string(),
9094 end: ")".to_string(),
9095 close: false,
9096 surround: false,
9097 newline: true,
9098 },
9099 ],
9100 ..Default::default()
9101 },
9102 ..Default::default()
9103 },
9104 Some(tree_sitter_rust::LANGUAGE.into()),
9105 )
9106 .with_indents_query(
9107 r#"
9108 (_ "(" ")" @end) @indent
9109 (_ "{" "}" @end) @indent
9110 "#,
9111 )
9112 .unwrap(),
9113 );
9114
9115 let text = "fn a() {}";
9116
9117 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9118 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9119 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9120 editor
9121 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9122 .await;
9123
9124 editor.update_in(cx, |editor, window, cx| {
9125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9126 s.select_ranges([5..5, 8..8, 9..9])
9127 });
9128 editor.newline(&Newline, window, cx);
9129 assert_eq!(
9130 editor.text(cx),
9131 indoc!(
9132 "
9133 fn a(
9134
9135 ) {
9136
9137 }
9138 "
9139 )
9140 );
9141 assert_eq!(
9142 editor.selections.ranges(cx),
9143 &[
9144 Point::new(1, 0)..Point::new(1, 0),
9145 Point::new(3, 0)..Point::new(3, 0),
9146 Point::new(5, 0)..Point::new(5, 0)
9147 ]
9148 );
9149 });
9150}
9151
9152#[gpui::test]
9153async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9154 init_test(cx, |settings| {
9155 settings.defaults.auto_indent = Some(true);
9156 settings.languages.0.insert(
9157 "python".into(),
9158 LanguageSettingsContent {
9159 auto_indent: Some(false),
9160 ..Default::default()
9161 },
9162 );
9163 });
9164
9165 let mut cx = EditorTestContext::new(cx).await;
9166
9167 let injected_language = Arc::new(
9168 Language::new(
9169 LanguageConfig {
9170 brackets: BracketPairConfig {
9171 pairs: vec![
9172 BracketPair {
9173 start: "{".to_string(),
9174 end: "}".to_string(),
9175 close: false,
9176 surround: false,
9177 newline: true,
9178 },
9179 BracketPair {
9180 start: "(".to_string(),
9181 end: ")".to_string(),
9182 close: true,
9183 surround: false,
9184 newline: true,
9185 },
9186 ],
9187 ..Default::default()
9188 },
9189 name: "python".into(),
9190 ..Default::default()
9191 },
9192 Some(tree_sitter_python::LANGUAGE.into()),
9193 )
9194 .with_indents_query(
9195 r#"
9196 (_ "(" ")" @end) @indent
9197 (_ "{" "}" @end) @indent
9198 "#,
9199 )
9200 .unwrap(),
9201 );
9202
9203 let language = Arc::new(
9204 Language::new(
9205 LanguageConfig {
9206 brackets: BracketPairConfig {
9207 pairs: vec![
9208 BracketPair {
9209 start: "{".to_string(),
9210 end: "}".to_string(),
9211 close: false,
9212 surround: false,
9213 newline: true,
9214 },
9215 BracketPair {
9216 start: "(".to_string(),
9217 end: ")".to_string(),
9218 close: true,
9219 surround: false,
9220 newline: true,
9221 },
9222 ],
9223 ..Default::default()
9224 },
9225 name: LanguageName::new("rust"),
9226 ..Default::default()
9227 },
9228 Some(tree_sitter_rust::LANGUAGE.into()),
9229 )
9230 .with_indents_query(
9231 r#"
9232 (_ "(" ")" @end) @indent
9233 (_ "{" "}" @end) @indent
9234 "#,
9235 )
9236 .unwrap()
9237 .with_injection_query(
9238 r#"
9239 (macro_invocation
9240 macro: (identifier) @_macro_name
9241 (token_tree) @injection.content
9242 (#set! injection.language "python"))
9243 "#,
9244 )
9245 .unwrap(),
9246 );
9247
9248 cx.language_registry().add(injected_language);
9249 cx.language_registry().add(language.clone());
9250
9251 cx.update_buffer(|buffer, cx| {
9252 buffer.set_language(Some(language), cx);
9253 });
9254
9255 cx.set_state(r#"struct A {ˇ}"#);
9256
9257 cx.update_editor(|editor, window, cx| {
9258 editor.newline(&Default::default(), window, cx);
9259 });
9260
9261 cx.assert_editor_state(indoc!(
9262 "struct A {
9263 ˇ
9264 }"
9265 ));
9266
9267 cx.set_state(r#"select_biased!(ˇ)"#);
9268
9269 cx.update_editor(|editor, window, cx| {
9270 editor.newline(&Default::default(), window, cx);
9271 editor.handle_input("def ", window, cx);
9272 editor.handle_input("(", window, cx);
9273 editor.newline(&Default::default(), window, cx);
9274 editor.handle_input("a", window, cx);
9275 });
9276
9277 cx.assert_editor_state(indoc!(
9278 "select_biased!(
9279 def (
9280 aˇ
9281 )
9282 )"
9283 ));
9284}
9285
9286#[gpui::test]
9287async fn test_autoindent_selections(cx: &mut TestAppContext) {
9288 init_test(cx, |_| {});
9289
9290 {
9291 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9292 cx.set_state(indoc! {"
9293 impl A {
9294
9295 fn b() {}
9296
9297 «fn c() {
9298
9299 }ˇ»
9300 }
9301 "});
9302
9303 cx.update_editor(|editor, window, cx| {
9304 editor.autoindent(&Default::default(), window, cx);
9305 });
9306
9307 cx.assert_editor_state(indoc! {"
9308 impl A {
9309
9310 fn b() {}
9311
9312 «fn c() {
9313
9314 }ˇ»
9315 }
9316 "});
9317 }
9318
9319 {
9320 let mut cx = EditorTestContext::new_multibuffer(
9321 cx,
9322 [indoc! { "
9323 impl A {
9324 «
9325 // a
9326 fn b(){}
9327 »
9328 «
9329 }
9330 fn c(){}
9331 »
9332 "}],
9333 );
9334
9335 let buffer = cx.update_editor(|editor, _, cx| {
9336 let buffer = editor.buffer().update(cx, |buffer, _| {
9337 buffer.all_buffers().iter().next().unwrap().clone()
9338 });
9339 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9340 buffer
9341 });
9342
9343 cx.run_until_parked();
9344 cx.update_editor(|editor, window, cx| {
9345 editor.select_all(&Default::default(), window, cx);
9346 editor.autoindent(&Default::default(), window, cx)
9347 });
9348 cx.run_until_parked();
9349
9350 cx.update(|_, cx| {
9351 assert_eq!(
9352 buffer.read(cx).text(),
9353 indoc! { "
9354 impl A {
9355
9356 // a
9357 fn b(){}
9358
9359
9360 }
9361 fn c(){}
9362
9363 " }
9364 )
9365 });
9366 }
9367}
9368
9369#[gpui::test]
9370async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9371 init_test(cx, |_| {});
9372
9373 let mut cx = EditorTestContext::new(cx).await;
9374
9375 let language = Arc::new(Language::new(
9376 LanguageConfig {
9377 brackets: BracketPairConfig {
9378 pairs: vec![
9379 BracketPair {
9380 start: "{".to_string(),
9381 end: "}".to_string(),
9382 close: true,
9383 surround: true,
9384 newline: true,
9385 },
9386 BracketPair {
9387 start: "(".to_string(),
9388 end: ")".to_string(),
9389 close: true,
9390 surround: true,
9391 newline: true,
9392 },
9393 BracketPair {
9394 start: "/*".to_string(),
9395 end: " */".to_string(),
9396 close: true,
9397 surround: true,
9398 newline: true,
9399 },
9400 BracketPair {
9401 start: "[".to_string(),
9402 end: "]".to_string(),
9403 close: false,
9404 surround: false,
9405 newline: true,
9406 },
9407 BracketPair {
9408 start: "\"".to_string(),
9409 end: "\"".to_string(),
9410 close: true,
9411 surround: true,
9412 newline: false,
9413 },
9414 BracketPair {
9415 start: "<".to_string(),
9416 end: ">".to_string(),
9417 close: false,
9418 surround: true,
9419 newline: true,
9420 },
9421 ],
9422 ..Default::default()
9423 },
9424 autoclose_before: "})]".to_string(),
9425 ..Default::default()
9426 },
9427 Some(tree_sitter_rust::LANGUAGE.into()),
9428 ));
9429
9430 cx.language_registry().add(language.clone());
9431 cx.update_buffer(|buffer, cx| {
9432 buffer.set_language(Some(language), cx);
9433 });
9434
9435 cx.set_state(
9436 &r#"
9437 🏀ˇ
9438 εˇ
9439 ❤️ˇ
9440 "#
9441 .unindent(),
9442 );
9443
9444 // autoclose multiple nested brackets at multiple cursors
9445 cx.update_editor(|editor, window, cx| {
9446 editor.handle_input("{", window, cx);
9447 editor.handle_input("{", window, cx);
9448 editor.handle_input("{", window, cx);
9449 });
9450 cx.assert_editor_state(
9451 &"
9452 🏀{{{ˇ}}}
9453 ε{{{ˇ}}}
9454 ❤️{{{ˇ}}}
9455 "
9456 .unindent(),
9457 );
9458
9459 // insert a different closing bracket
9460 cx.update_editor(|editor, window, cx| {
9461 editor.handle_input(")", window, cx);
9462 });
9463 cx.assert_editor_state(
9464 &"
9465 🏀{{{)ˇ}}}
9466 ε{{{)ˇ}}}
9467 ❤️{{{)ˇ}}}
9468 "
9469 .unindent(),
9470 );
9471
9472 // skip over the auto-closed brackets when typing a closing bracket
9473 cx.update_editor(|editor, window, cx| {
9474 editor.move_right(&MoveRight, window, cx);
9475 editor.handle_input("}", window, cx);
9476 editor.handle_input("}", window, cx);
9477 editor.handle_input("}", window, cx);
9478 });
9479 cx.assert_editor_state(
9480 &"
9481 🏀{{{)}}}}ˇ
9482 ε{{{)}}}}ˇ
9483 ❤️{{{)}}}}ˇ
9484 "
9485 .unindent(),
9486 );
9487
9488 // autoclose multi-character pairs
9489 cx.set_state(
9490 &"
9491 ˇ
9492 ˇ
9493 "
9494 .unindent(),
9495 );
9496 cx.update_editor(|editor, window, cx| {
9497 editor.handle_input("/", window, cx);
9498 editor.handle_input("*", window, cx);
9499 });
9500 cx.assert_editor_state(
9501 &"
9502 /*ˇ */
9503 /*ˇ */
9504 "
9505 .unindent(),
9506 );
9507
9508 // one cursor autocloses a multi-character pair, one cursor
9509 // does not autoclose.
9510 cx.set_state(
9511 &"
9512 /ˇ
9513 ˇ
9514 "
9515 .unindent(),
9516 );
9517 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9518 cx.assert_editor_state(
9519 &"
9520 /*ˇ */
9521 *ˇ
9522 "
9523 .unindent(),
9524 );
9525
9526 // Don't autoclose if the next character isn't whitespace and isn't
9527 // listed in the language's "autoclose_before" section.
9528 cx.set_state("ˇa b");
9529 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9530 cx.assert_editor_state("{ˇa b");
9531
9532 // Don't autoclose if `close` is false for the bracket pair
9533 cx.set_state("ˇ");
9534 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9535 cx.assert_editor_state("[ˇ");
9536
9537 // Surround with brackets if text is selected
9538 cx.set_state("«aˇ» b");
9539 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9540 cx.assert_editor_state("{«aˇ»} b");
9541
9542 // Autoclose when not immediately after a word character
9543 cx.set_state("a ˇ");
9544 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9545 cx.assert_editor_state("a \"ˇ\"");
9546
9547 // Autoclose pair where the start and end characters are the same
9548 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9549 cx.assert_editor_state("a \"\"ˇ");
9550
9551 // Don't autoclose when immediately after a word character
9552 cx.set_state("aˇ");
9553 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9554 cx.assert_editor_state("a\"ˇ");
9555
9556 // Do autoclose when after a non-word character
9557 cx.set_state("{ˇ");
9558 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9559 cx.assert_editor_state("{\"ˇ\"");
9560
9561 // Non identical pairs autoclose regardless of preceding character
9562 cx.set_state("aˇ");
9563 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9564 cx.assert_editor_state("a{ˇ}");
9565
9566 // Don't autoclose pair if autoclose is disabled
9567 cx.set_state("ˇ");
9568 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9569 cx.assert_editor_state("<ˇ");
9570
9571 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9572 cx.set_state("«aˇ» b");
9573 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9574 cx.assert_editor_state("<«aˇ»> b");
9575}
9576
9577#[gpui::test]
9578async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9579 init_test(cx, |settings| {
9580 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9581 });
9582
9583 let mut cx = EditorTestContext::new(cx).await;
9584
9585 let language = Arc::new(Language::new(
9586 LanguageConfig {
9587 brackets: BracketPairConfig {
9588 pairs: vec![
9589 BracketPair {
9590 start: "{".to_string(),
9591 end: "}".to_string(),
9592 close: true,
9593 surround: true,
9594 newline: true,
9595 },
9596 BracketPair {
9597 start: "(".to_string(),
9598 end: ")".to_string(),
9599 close: true,
9600 surround: true,
9601 newline: true,
9602 },
9603 BracketPair {
9604 start: "[".to_string(),
9605 end: "]".to_string(),
9606 close: false,
9607 surround: false,
9608 newline: true,
9609 },
9610 ],
9611 ..Default::default()
9612 },
9613 autoclose_before: "})]".to_string(),
9614 ..Default::default()
9615 },
9616 Some(tree_sitter_rust::LANGUAGE.into()),
9617 ));
9618
9619 cx.language_registry().add(language.clone());
9620 cx.update_buffer(|buffer, cx| {
9621 buffer.set_language(Some(language), cx);
9622 });
9623
9624 cx.set_state(
9625 &"
9626 ˇ
9627 ˇ
9628 ˇ
9629 "
9630 .unindent(),
9631 );
9632
9633 // ensure only matching closing brackets are skipped over
9634 cx.update_editor(|editor, window, cx| {
9635 editor.handle_input("}", window, cx);
9636 editor.move_left(&MoveLeft, window, cx);
9637 editor.handle_input(")", window, cx);
9638 editor.move_left(&MoveLeft, window, cx);
9639 });
9640 cx.assert_editor_state(
9641 &"
9642 ˇ)}
9643 ˇ)}
9644 ˇ)}
9645 "
9646 .unindent(),
9647 );
9648
9649 // skip-over closing brackets at multiple cursors
9650 cx.update_editor(|editor, window, cx| {
9651 editor.handle_input(")", window, cx);
9652 editor.handle_input("}", window, cx);
9653 });
9654 cx.assert_editor_state(
9655 &"
9656 )}ˇ
9657 )}ˇ
9658 )}ˇ
9659 "
9660 .unindent(),
9661 );
9662
9663 // ignore non-close brackets
9664 cx.update_editor(|editor, window, cx| {
9665 editor.handle_input("]", window, cx);
9666 editor.move_left(&MoveLeft, window, cx);
9667 editor.handle_input("]", window, cx);
9668 });
9669 cx.assert_editor_state(
9670 &"
9671 )}]ˇ]
9672 )}]ˇ]
9673 )}]ˇ]
9674 "
9675 .unindent(),
9676 );
9677}
9678
9679#[gpui::test]
9680async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9681 init_test(cx, |_| {});
9682
9683 let mut cx = EditorTestContext::new(cx).await;
9684
9685 let html_language = Arc::new(
9686 Language::new(
9687 LanguageConfig {
9688 name: "HTML".into(),
9689 brackets: BracketPairConfig {
9690 pairs: vec![
9691 BracketPair {
9692 start: "<".into(),
9693 end: ">".into(),
9694 close: true,
9695 ..Default::default()
9696 },
9697 BracketPair {
9698 start: "{".into(),
9699 end: "}".into(),
9700 close: true,
9701 ..Default::default()
9702 },
9703 BracketPair {
9704 start: "(".into(),
9705 end: ")".into(),
9706 close: true,
9707 ..Default::default()
9708 },
9709 ],
9710 ..Default::default()
9711 },
9712 autoclose_before: "})]>".into(),
9713 ..Default::default()
9714 },
9715 Some(tree_sitter_html::LANGUAGE.into()),
9716 )
9717 .with_injection_query(
9718 r#"
9719 (script_element
9720 (raw_text) @injection.content
9721 (#set! injection.language "javascript"))
9722 "#,
9723 )
9724 .unwrap(),
9725 );
9726
9727 let javascript_language = Arc::new(Language::new(
9728 LanguageConfig {
9729 name: "JavaScript".into(),
9730 brackets: BracketPairConfig {
9731 pairs: vec![
9732 BracketPair {
9733 start: "/*".into(),
9734 end: " */".into(),
9735 close: true,
9736 ..Default::default()
9737 },
9738 BracketPair {
9739 start: "{".into(),
9740 end: "}".into(),
9741 close: true,
9742 ..Default::default()
9743 },
9744 BracketPair {
9745 start: "(".into(),
9746 end: ")".into(),
9747 close: true,
9748 ..Default::default()
9749 },
9750 ],
9751 ..Default::default()
9752 },
9753 autoclose_before: "})]>".into(),
9754 ..Default::default()
9755 },
9756 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9757 ));
9758
9759 cx.language_registry().add(html_language.clone());
9760 cx.language_registry().add(javascript_language);
9761 cx.executor().run_until_parked();
9762
9763 cx.update_buffer(|buffer, cx| {
9764 buffer.set_language(Some(html_language), cx);
9765 });
9766
9767 cx.set_state(
9768 &r#"
9769 <body>ˇ
9770 <script>
9771 var x = 1;ˇ
9772 </script>
9773 </body>ˇ
9774 "#
9775 .unindent(),
9776 );
9777
9778 // Precondition: different languages are active at different locations.
9779 cx.update_editor(|editor, window, cx| {
9780 let snapshot = editor.snapshot(window, cx);
9781 let cursors = editor.selections.ranges::<usize>(cx);
9782 let languages = cursors
9783 .iter()
9784 .map(|c| snapshot.language_at(c.start).unwrap().name())
9785 .collect::<Vec<_>>();
9786 assert_eq!(
9787 languages,
9788 &["HTML".into(), "JavaScript".into(), "HTML".into()]
9789 );
9790 });
9791
9792 // Angle brackets autoclose in HTML, but not JavaScript.
9793 cx.update_editor(|editor, window, cx| {
9794 editor.handle_input("<", window, cx);
9795 editor.handle_input("a", window, cx);
9796 });
9797 cx.assert_editor_state(
9798 &r#"
9799 <body><aˇ>
9800 <script>
9801 var x = 1;<aˇ
9802 </script>
9803 </body><aˇ>
9804 "#
9805 .unindent(),
9806 );
9807
9808 // Curly braces and parens autoclose in both HTML and JavaScript.
9809 cx.update_editor(|editor, window, cx| {
9810 editor.handle_input(" b=", window, cx);
9811 editor.handle_input("{", window, cx);
9812 editor.handle_input("c", window, cx);
9813 editor.handle_input("(", window, cx);
9814 });
9815 cx.assert_editor_state(
9816 &r#"
9817 <body><a b={c(ˇ)}>
9818 <script>
9819 var x = 1;<a b={c(ˇ)}
9820 </script>
9821 </body><a b={c(ˇ)}>
9822 "#
9823 .unindent(),
9824 );
9825
9826 // Brackets that were already autoclosed are skipped.
9827 cx.update_editor(|editor, window, cx| {
9828 editor.handle_input(")", window, cx);
9829 editor.handle_input("d", window, cx);
9830 editor.handle_input("}", window, cx);
9831 });
9832 cx.assert_editor_state(
9833 &r#"
9834 <body><a b={c()d}ˇ>
9835 <script>
9836 var x = 1;<a b={c()d}ˇ
9837 </script>
9838 </body><a b={c()d}ˇ>
9839 "#
9840 .unindent(),
9841 );
9842 cx.update_editor(|editor, window, cx| {
9843 editor.handle_input(">", window, cx);
9844 });
9845 cx.assert_editor_state(
9846 &r#"
9847 <body><a b={c()d}>ˇ
9848 <script>
9849 var x = 1;<a b={c()d}>ˇ
9850 </script>
9851 </body><a b={c()d}>ˇ
9852 "#
9853 .unindent(),
9854 );
9855
9856 // Reset
9857 cx.set_state(
9858 &r#"
9859 <body>ˇ
9860 <script>
9861 var x = 1;ˇ
9862 </script>
9863 </body>ˇ
9864 "#
9865 .unindent(),
9866 );
9867
9868 cx.update_editor(|editor, window, cx| {
9869 editor.handle_input("<", window, cx);
9870 });
9871 cx.assert_editor_state(
9872 &r#"
9873 <body><ˇ>
9874 <script>
9875 var x = 1;<ˇ
9876 </script>
9877 </body><ˇ>
9878 "#
9879 .unindent(),
9880 );
9881
9882 // When backspacing, the closing angle brackets are removed.
9883 cx.update_editor(|editor, window, cx| {
9884 editor.backspace(&Backspace, window, cx);
9885 });
9886 cx.assert_editor_state(
9887 &r#"
9888 <body>ˇ
9889 <script>
9890 var x = 1;ˇ
9891 </script>
9892 </body>ˇ
9893 "#
9894 .unindent(),
9895 );
9896
9897 // Block comments autoclose in JavaScript, but not HTML.
9898 cx.update_editor(|editor, window, cx| {
9899 editor.handle_input("/", window, cx);
9900 editor.handle_input("*", window, cx);
9901 });
9902 cx.assert_editor_state(
9903 &r#"
9904 <body>/*ˇ
9905 <script>
9906 var x = 1;/*ˇ */
9907 </script>
9908 </body>/*ˇ
9909 "#
9910 .unindent(),
9911 );
9912}
9913
9914#[gpui::test]
9915async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9916 init_test(cx, |_| {});
9917
9918 let mut cx = EditorTestContext::new(cx).await;
9919
9920 let rust_language = Arc::new(
9921 Language::new(
9922 LanguageConfig {
9923 name: "Rust".into(),
9924 brackets: serde_json::from_value(json!([
9925 { "start": "{", "end": "}", "close": true, "newline": true },
9926 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9927 ]))
9928 .unwrap(),
9929 autoclose_before: "})]>".into(),
9930 ..Default::default()
9931 },
9932 Some(tree_sitter_rust::LANGUAGE.into()),
9933 )
9934 .with_override_query("(string_literal) @string")
9935 .unwrap(),
9936 );
9937
9938 cx.language_registry().add(rust_language.clone());
9939 cx.update_buffer(|buffer, cx| {
9940 buffer.set_language(Some(rust_language), cx);
9941 });
9942
9943 cx.set_state(
9944 &r#"
9945 let x = ˇ
9946 "#
9947 .unindent(),
9948 );
9949
9950 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9951 cx.update_editor(|editor, window, cx| {
9952 editor.handle_input("\"", window, cx);
9953 });
9954 cx.assert_editor_state(
9955 &r#"
9956 let x = "ˇ"
9957 "#
9958 .unindent(),
9959 );
9960
9961 // Inserting another quotation mark. The cursor moves across the existing
9962 // automatically-inserted quotation mark.
9963 cx.update_editor(|editor, window, cx| {
9964 editor.handle_input("\"", window, cx);
9965 });
9966 cx.assert_editor_state(
9967 &r#"
9968 let x = ""ˇ
9969 "#
9970 .unindent(),
9971 );
9972
9973 // Reset
9974 cx.set_state(
9975 &r#"
9976 let x = ˇ
9977 "#
9978 .unindent(),
9979 );
9980
9981 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9982 cx.update_editor(|editor, window, cx| {
9983 editor.handle_input("\"", window, cx);
9984 editor.handle_input(" ", window, cx);
9985 editor.move_left(&Default::default(), window, cx);
9986 editor.handle_input("\\", window, cx);
9987 editor.handle_input("\"", window, cx);
9988 });
9989 cx.assert_editor_state(
9990 &r#"
9991 let x = "\"ˇ "
9992 "#
9993 .unindent(),
9994 );
9995
9996 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
9997 // mark. Nothing is inserted.
9998 cx.update_editor(|editor, window, cx| {
9999 editor.move_right(&Default::default(), window, cx);
10000 editor.handle_input("\"", window, cx);
10001 });
10002 cx.assert_editor_state(
10003 &r#"
10004 let x = "\" "ˇ
10005 "#
10006 .unindent(),
10007 );
10008}
10009
10010#[gpui::test]
10011async fn test_surround_with_pair(cx: &mut TestAppContext) {
10012 init_test(cx, |_| {});
10013
10014 let language = Arc::new(Language::new(
10015 LanguageConfig {
10016 brackets: BracketPairConfig {
10017 pairs: vec![
10018 BracketPair {
10019 start: "{".to_string(),
10020 end: "}".to_string(),
10021 close: true,
10022 surround: true,
10023 newline: true,
10024 },
10025 BracketPair {
10026 start: "/* ".to_string(),
10027 end: "*/".to_string(),
10028 close: true,
10029 surround: true,
10030 ..Default::default()
10031 },
10032 ],
10033 ..Default::default()
10034 },
10035 ..Default::default()
10036 },
10037 Some(tree_sitter_rust::LANGUAGE.into()),
10038 ));
10039
10040 let text = r#"
10041 a
10042 b
10043 c
10044 "#
10045 .unindent();
10046
10047 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10048 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10049 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10050 editor
10051 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10052 .await;
10053
10054 editor.update_in(cx, |editor, window, cx| {
10055 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10056 s.select_display_ranges([
10057 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10058 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10059 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10060 ])
10061 });
10062
10063 editor.handle_input("{", window, cx);
10064 editor.handle_input("{", window, cx);
10065 editor.handle_input("{", window, cx);
10066 assert_eq!(
10067 editor.text(cx),
10068 "
10069 {{{a}}}
10070 {{{b}}}
10071 {{{c}}}
10072 "
10073 .unindent()
10074 );
10075 assert_eq!(
10076 editor.selections.display_ranges(cx),
10077 [
10078 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10079 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10080 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10081 ]
10082 );
10083
10084 editor.undo(&Undo, window, cx);
10085 editor.undo(&Undo, window, cx);
10086 editor.undo(&Undo, window, cx);
10087 assert_eq!(
10088 editor.text(cx),
10089 "
10090 a
10091 b
10092 c
10093 "
10094 .unindent()
10095 );
10096 assert_eq!(
10097 editor.selections.display_ranges(cx),
10098 [
10099 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10100 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10101 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10102 ]
10103 );
10104
10105 // Ensure inserting the first character of a multi-byte bracket pair
10106 // doesn't surround the selections with the bracket.
10107 editor.handle_input("/", window, cx);
10108 assert_eq!(
10109 editor.text(cx),
10110 "
10111 /
10112 /
10113 /
10114 "
10115 .unindent()
10116 );
10117 assert_eq!(
10118 editor.selections.display_ranges(cx),
10119 [
10120 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10121 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10122 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10123 ]
10124 );
10125
10126 editor.undo(&Undo, window, cx);
10127 assert_eq!(
10128 editor.text(cx),
10129 "
10130 a
10131 b
10132 c
10133 "
10134 .unindent()
10135 );
10136 assert_eq!(
10137 editor.selections.display_ranges(cx),
10138 [
10139 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10140 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10141 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10142 ]
10143 );
10144
10145 // Ensure inserting the last character of a multi-byte bracket pair
10146 // doesn't surround the selections with the bracket.
10147 editor.handle_input("*", window, cx);
10148 assert_eq!(
10149 editor.text(cx),
10150 "
10151 *
10152 *
10153 *
10154 "
10155 .unindent()
10156 );
10157 assert_eq!(
10158 editor.selections.display_ranges(cx),
10159 [
10160 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10161 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10162 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10163 ]
10164 );
10165 });
10166}
10167
10168#[gpui::test]
10169async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10170 init_test(cx, |_| {});
10171
10172 let language = Arc::new(Language::new(
10173 LanguageConfig {
10174 brackets: BracketPairConfig {
10175 pairs: vec![BracketPair {
10176 start: "{".to_string(),
10177 end: "}".to_string(),
10178 close: true,
10179 surround: true,
10180 newline: true,
10181 }],
10182 ..Default::default()
10183 },
10184 autoclose_before: "}".to_string(),
10185 ..Default::default()
10186 },
10187 Some(tree_sitter_rust::LANGUAGE.into()),
10188 ));
10189
10190 let text = r#"
10191 a
10192 b
10193 c
10194 "#
10195 .unindent();
10196
10197 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10198 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10199 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10200 editor
10201 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10202 .await;
10203
10204 editor.update_in(cx, |editor, window, cx| {
10205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10206 s.select_ranges([
10207 Point::new(0, 1)..Point::new(0, 1),
10208 Point::new(1, 1)..Point::new(1, 1),
10209 Point::new(2, 1)..Point::new(2, 1),
10210 ])
10211 });
10212
10213 editor.handle_input("{", window, cx);
10214 editor.handle_input("{", window, cx);
10215 editor.handle_input("_", window, cx);
10216 assert_eq!(
10217 editor.text(cx),
10218 "
10219 a{{_}}
10220 b{{_}}
10221 c{{_}}
10222 "
10223 .unindent()
10224 );
10225 assert_eq!(
10226 editor.selections.ranges::<Point>(cx),
10227 [
10228 Point::new(0, 4)..Point::new(0, 4),
10229 Point::new(1, 4)..Point::new(1, 4),
10230 Point::new(2, 4)..Point::new(2, 4)
10231 ]
10232 );
10233
10234 editor.backspace(&Default::default(), window, cx);
10235 editor.backspace(&Default::default(), window, cx);
10236 assert_eq!(
10237 editor.text(cx),
10238 "
10239 a{}
10240 b{}
10241 c{}
10242 "
10243 .unindent()
10244 );
10245 assert_eq!(
10246 editor.selections.ranges::<Point>(cx),
10247 [
10248 Point::new(0, 2)..Point::new(0, 2),
10249 Point::new(1, 2)..Point::new(1, 2),
10250 Point::new(2, 2)..Point::new(2, 2)
10251 ]
10252 );
10253
10254 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10255 assert_eq!(
10256 editor.text(cx),
10257 "
10258 a
10259 b
10260 c
10261 "
10262 .unindent()
10263 );
10264 assert_eq!(
10265 editor.selections.ranges::<Point>(cx),
10266 [
10267 Point::new(0, 1)..Point::new(0, 1),
10268 Point::new(1, 1)..Point::new(1, 1),
10269 Point::new(2, 1)..Point::new(2, 1)
10270 ]
10271 );
10272 });
10273}
10274
10275#[gpui::test]
10276async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10277 init_test(cx, |settings| {
10278 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10279 });
10280
10281 let mut cx = EditorTestContext::new(cx).await;
10282
10283 let language = Arc::new(Language::new(
10284 LanguageConfig {
10285 brackets: BracketPairConfig {
10286 pairs: vec![
10287 BracketPair {
10288 start: "{".to_string(),
10289 end: "}".to_string(),
10290 close: true,
10291 surround: true,
10292 newline: true,
10293 },
10294 BracketPair {
10295 start: "(".to_string(),
10296 end: ")".to_string(),
10297 close: true,
10298 surround: true,
10299 newline: true,
10300 },
10301 BracketPair {
10302 start: "[".to_string(),
10303 end: "]".to_string(),
10304 close: false,
10305 surround: true,
10306 newline: true,
10307 },
10308 ],
10309 ..Default::default()
10310 },
10311 autoclose_before: "})]".to_string(),
10312 ..Default::default()
10313 },
10314 Some(tree_sitter_rust::LANGUAGE.into()),
10315 ));
10316
10317 cx.language_registry().add(language.clone());
10318 cx.update_buffer(|buffer, cx| {
10319 buffer.set_language(Some(language), cx);
10320 });
10321
10322 cx.set_state(
10323 &"
10324 {(ˇ)}
10325 [[ˇ]]
10326 {(ˇ)}
10327 "
10328 .unindent(),
10329 );
10330
10331 cx.update_editor(|editor, window, cx| {
10332 editor.backspace(&Default::default(), window, cx);
10333 editor.backspace(&Default::default(), window, cx);
10334 });
10335
10336 cx.assert_editor_state(
10337 &"
10338 ˇ
10339 ˇ]]
10340 ˇ
10341 "
10342 .unindent(),
10343 );
10344
10345 cx.update_editor(|editor, window, cx| {
10346 editor.handle_input("{", window, cx);
10347 editor.handle_input("{", window, cx);
10348 editor.move_right(&MoveRight, window, cx);
10349 editor.move_right(&MoveRight, window, cx);
10350 editor.move_left(&MoveLeft, window, cx);
10351 editor.move_left(&MoveLeft, window, cx);
10352 editor.backspace(&Default::default(), window, cx);
10353 });
10354
10355 cx.assert_editor_state(
10356 &"
10357 {ˇ}
10358 {ˇ}]]
10359 {ˇ}
10360 "
10361 .unindent(),
10362 );
10363
10364 cx.update_editor(|editor, window, cx| {
10365 editor.backspace(&Default::default(), window, cx);
10366 });
10367
10368 cx.assert_editor_state(
10369 &"
10370 ˇ
10371 ˇ]]
10372 ˇ
10373 "
10374 .unindent(),
10375 );
10376}
10377
10378#[gpui::test]
10379async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10380 init_test(cx, |_| {});
10381
10382 let language = Arc::new(Language::new(
10383 LanguageConfig::default(),
10384 Some(tree_sitter_rust::LANGUAGE.into()),
10385 ));
10386
10387 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10388 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10389 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10390 editor
10391 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10392 .await;
10393
10394 editor.update_in(cx, |editor, window, cx| {
10395 editor.set_auto_replace_emoji_shortcode(true);
10396
10397 editor.handle_input("Hello ", window, cx);
10398 editor.handle_input(":wave", window, cx);
10399 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10400
10401 editor.handle_input(":", window, cx);
10402 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10403
10404 editor.handle_input(" :smile", window, cx);
10405 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10406
10407 editor.handle_input(":", window, cx);
10408 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10409
10410 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10411 editor.handle_input(":wave", window, cx);
10412 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10413
10414 editor.handle_input(":", window, cx);
10415 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10416
10417 editor.handle_input(":1", window, cx);
10418 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10419
10420 editor.handle_input(":", window, cx);
10421 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10422
10423 // Ensure shortcode does not get replaced when it is part of a word
10424 editor.handle_input(" Test:wave", window, cx);
10425 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10426
10427 editor.handle_input(":", window, cx);
10428 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10429
10430 editor.set_auto_replace_emoji_shortcode(false);
10431
10432 // Ensure shortcode does not get replaced when auto replace is off
10433 editor.handle_input(" :wave", window, cx);
10434 assert_eq!(
10435 editor.text(cx),
10436 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10437 );
10438
10439 editor.handle_input(":", window, cx);
10440 assert_eq!(
10441 editor.text(cx),
10442 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10443 );
10444 });
10445}
10446
10447#[gpui::test]
10448async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10449 init_test(cx, |_| {});
10450
10451 let (text, insertion_ranges) = marked_text_ranges(
10452 indoc! {"
10453 ˇ
10454 "},
10455 false,
10456 );
10457
10458 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10459 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10460
10461 _ = editor.update_in(cx, |editor, window, cx| {
10462 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10463
10464 editor
10465 .insert_snippet(&insertion_ranges, snippet, window, cx)
10466 .unwrap();
10467
10468 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10469 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10470 assert_eq!(editor.text(cx), expected_text);
10471 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10472 }
10473
10474 assert(
10475 editor,
10476 cx,
10477 indoc! {"
10478 type «» =•
10479 "},
10480 );
10481
10482 assert!(editor.context_menu_visible(), "There should be a matches");
10483 });
10484}
10485
10486#[gpui::test]
10487async fn test_snippets(cx: &mut TestAppContext) {
10488 init_test(cx, |_| {});
10489
10490 let mut cx = EditorTestContext::new(cx).await;
10491
10492 cx.set_state(indoc! {"
10493 a.ˇ b
10494 a.ˇ b
10495 a.ˇ b
10496 "});
10497
10498 cx.update_editor(|editor, window, cx| {
10499 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10500 let insertion_ranges = editor
10501 .selections
10502 .all(cx)
10503 .iter()
10504 .map(|s| s.range())
10505 .collect::<Vec<_>>();
10506 editor
10507 .insert_snippet(&insertion_ranges, snippet, window, cx)
10508 .unwrap();
10509 });
10510
10511 cx.assert_editor_state(indoc! {"
10512 a.f(«oneˇ», two, «threeˇ») b
10513 a.f(«oneˇ», two, «threeˇ») b
10514 a.f(«oneˇ», two, «threeˇ») b
10515 "});
10516
10517 // Can't move earlier than the first tab stop
10518 cx.update_editor(|editor, window, cx| {
10519 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10520 });
10521 cx.assert_editor_state(indoc! {"
10522 a.f(«oneˇ», two, «threeˇ») b
10523 a.f(«oneˇ», two, «threeˇ») b
10524 a.f(«oneˇ», two, «threeˇ») b
10525 "});
10526
10527 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10528 cx.assert_editor_state(indoc! {"
10529 a.f(one, «twoˇ», three) b
10530 a.f(one, «twoˇ», three) b
10531 a.f(one, «twoˇ», three) b
10532 "});
10533
10534 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10535 cx.assert_editor_state(indoc! {"
10536 a.f(«oneˇ», two, «threeˇ») b
10537 a.f(«oneˇ», two, «threeˇ») b
10538 a.f(«oneˇ», two, «threeˇ») b
10539 "});
10540
10541 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10542 cx.assert_editor_state(indoc! {"
10543 a.f(one, «twoˇ», three) b
10544 a.f(one, «twoˇ», three) b
10545 a.f(one, «twoˇ», three) b
10546 "});
10547 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10548 cx.assert_editor_state(indoc! {"
10549 a.f(one, two, three)ˇ b
10550 a.f(one, two, three)ˇ b
10551 a.f(one, two, three)ˇ b
10552 "});
10553
10554 // As soon as the last tab stop is reached, snippet state is gone
10555 cx.update_editor(|editor, window, cx| {
10556 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10557 });
10558 cx.assert_editor_state(indoc! {"
10559 a.f(one, two, three)ˇ b
10560 a.f(one, two, three)ˇ b
10561 a.f(one, two, three)ˇ b
10562 "});
10563}
10564
10565#[gpui::test]
10566async fn test_snippet_indentation(cx: &mut TestAppContext) {
10567 init_test(cx, |_| {});
10568
10569 let mut cx = EditorTestContext::new(cx).await;
10570
10571 cx.update_editor(|editor, window, cx| {
10572 let snippet = Snippet::parse(indoc! {"
10573 /*
10574 * Multiline comment with leading indentation
10575 *
10576 * $1
10577 */
10578 $0"})
10579 .unwrap();
10580 let insertion_ranges = editor
10581 .selections
10582 .all(cx)
10583 .iter()
10584 .map(|s| s.range())
10585 .collect::<Vec<_>>();
10586 editor
10587 .insert_snippet(&insertion_ranges, snippet, window, cx)
10588 .unwrap();
10589 });
10590
10591 cx.assert_editor_state(indoc! {"
10592 /*
10593 * Multiline comment with leading indentation
10594 *
10595 * ˇ
10596 */
10597 "});
10598
10599 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10600 cx.assert_editor_state(indoc! {"
10601 /*
10602 * Multiline comment with leading indentation
10603 *
10604 *•
10605 */
10606 ˇ"});
10607}
10608
10609#[gpui::test]
10610async fn test_document_format_during_save(cx: &mut TestAppContext) {
10611 init_test(cx, |_| {});
10612
10613 let fs = FakeFs::new(cx.executor());
10614 fs.insert_file(path!("/file.rs"), Default::default()).await;
10615
10616 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10617
10618 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10619 language_registry.add(rust_lang());
10620 let mut fake_servers = language_registry.register_fake_lsp(
10621 "Rust",
10622 FakeLspAdapter {
10623 capabilities: lsp::ServerCapabilities {
10624 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10625 ..Default::default()
10626 },
10627 ..Default::default()
10628 },
10629 );
10630
10631 let buffer = project
10632 .update(cx, |project, cx| {
10633 project.open_local_buffer(path!("/file.rs"), cx)
10634 })
10635 .await
10636 .unwrap();
10637
10638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10639 let (editor, cx) = cx.add_window_view(|window, cx| {
10640 build_editor_with_project(project.clone(), buffer, window, cx)
10641 });
10642 editor.update_in(cx, |editor, window, cx| {
10643 editor.set_text("one\ntwo\nthree\n", window, cx)
10644 });
10645 assert!(cx.read(|cx| editor.is_dirty(cx)));
10646
10647 cx.executor().start_waiting();
10648 let fake_server = fake_servers.next().await.unwrap();
10649
10650 {
10651 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10652 move |params, _| async move {
10653 assert_eq!(
10654 params.text_document.uri,
10655 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10656 );
10657 assert_eq!(params.options.tab_size, 4);
10658 Ok(Some(vec![lsp::TextEdit::new(
10659 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10660 ", ".to_string(),
10661 )]))
10662 },
10663 );
10664 let save = editor
10665 .update_in(cx, |editor, window, cx| {
10666 editor.save(
10667 SaveOptions {
10668 format: true,
10669 autosave: false,
10670 },
10671 project.clone(),
10672 window,
10673 cx,
10674 )
10675 })
10676 .unwrap();
10677 cx.executor().start_waiting();
10678 save.await;
10679
10680 assert_eq!(
10681 editor.update(cx, |editor, cx| editor.text(cx)),
10682 "one, two\nthree\n"
10683 );
10684 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10685 }
10686
10687 {
10688 editor.update_in(cx, |editor, window, cx| {
10689 editor.set_text("one\ntwo\nthree\n", window, cx)
10690 });
10691 assert!(cx.read(|cx| editor.is_dirty(cx)));
10692
10693 // Ensure we can still save even if formatting hangs.
10694 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10695 move |params, _| async move {
10696 assert_eq!(
10697 params.text_document.uri,
10698 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10699 );
10700 futures::future::pending::<()>().await;
10701 unreachable!()
10702 },
10703 );
10704 let save = editor
10705 .update_in(cx, |editor, window, cx| {
10706 editor.save(
10707 SaveOptions {
10708 format: true,
10709 autosave: false,
10710 },
10711 project.clone(),
10712 window,
10713 cx,
10714 )
10715 })
10716 .unwrap();
10717 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10718 cx.executor().start_waiting();
10719 save.await;
10720 assert_eq!(
10721 editor.update(cx, |editor, cx| editor.text(cx)),
10722 "one\ntwo\nthree\n"
10723 );
10724 }
10725
10726 // Set rust language override and assert overridden tabsize is sent to language server
10727 update_test_language_settings(cx, |settings| {
10728 settings.languages.0.insert(
10729 "Rust".into(),
10730 LanguageSettingsContent {
10731 tab_size: NonZeroU32::new(8),
10732 ..Default::default()
10733 },
10734 );
10735 });
10736
10737 {
10738 editor.update_in(cx, |editor, window, cx| {
10739 editor.set_text("somehting_new\n", window, cx)
10740 });
10741 assert!(cx.read(|cx| editor.is_dirty(cx)));
10742 let _formatting_request_signal = fake_server
10743 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10744 assert_eq!(
10745 params.text_document.uri,
10746 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10747 );
10748 assert_eq!(params.options.tab_size, 8);
10749 Ok(Some(vec![]))
10750 });
10751 let save = editor
10752 .update_in(cx, |editor, window, cx| {
10753 editor.save(
10754 SaveOptions {
10755 format: true,
10756 autosave: false,
10757 },
10758 project.clone(),
10759 window,
10760 cx,
10761 )
10762 })
10763 .unwrap();
10764 cx.executor().start_waiting();
10765 save.await;
10766 }
10767}
10768
10769#[gpui::test]
10770async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
10771 init_test(cx, |settings| {
10772 settings.defaults.ensure_final_newline_on_save = Some(false);
10773 });
10774
10775 let fs = FakeFs::new(cx.executor());
10776 fs.insert_file(path!("/file.txt"), "foo".into()).await;
10777
10778 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
10779
10780 let buffer = project
10781 .update(cx, |project, cx| {
10782 project.open_local_buffer(path!("/file.txt"), cx)
10783 })
10784 .await
10785 .unwrap();
10786
10787 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10788 let (editor, cx) = cx.add_window_view(|window, cx| {
10789 build_editor_with_project(project.clone(), buffer, window, cx)
10790 });
10791 editor.update_in(cx, |editor, window, cx| {
10792 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
10793 s.select_ranges([0..0])
10794 });
10795 });
10796 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10797
10798 editor.update_in(cx, |editor, window, cx| {
10799 editor.handle_input("\n", window, cx)
10800 });
10801 cx.run_until_parked();
10802 save(&editor, &project, cx).await;
10803 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10804
10805 editor.update_in(cx, |editor, window, cx| {
10806 editor.undo(&Default::default(), window, cx);
10807 });
10808 save(&editor, &project, cx).await;
10809 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10810
10811 editor.update_in(cx, |editor, window, cx| {
10812 editor.redo(&Default::default(), window, cx);
10813 });
10814 cx.run_until_parked();
10815 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
10816
10817 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
10818 let save = editor
10819 .update_in(cx, |editor, window, cx| {
10820 editor.save(
10821 SaveOptions {
10822 format: true,
10823 autosave: false,
10824 },
10825 project.clone(),
10826 window,
10827 cx,
10828 )
10829 })
10830 .unwrap();
10831 cx.executor().start_waiting();
10832 save.await;
10833 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10834 }
10835}
10836
10837#[gpui::test]
10838async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
10839 init_test(cx, |_| {});
10840
10841 let cols = 4;
10842 let rows = 10;
10843 let sample_text_1 = sample_text(rows, cols, 'a');
10844 assert_eq!(
10845 sample_text_1,
10846 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
10847 );
10848 let sample_text_2 = sample_text(rows, cols, 'l');
10849 assert_eq!(
10850 sample_text_2,
10851 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
10852 );
10853 let sample_text_3 = sample_text(rows, cols, 'v');
10854 assert_eq!(
10855 sample_text_3,
10856 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
10857 );
10858
10859 let fs = FakeFs::new(cx.executor());
10860 fs.insert_tree(
10861 path!("/a"),
10862 json!({
10863 "main.rs": sample_text_1,
10864 "other.rs": sample_text_2,
10865 "lib.rs": sample_text_3,
10866 }),
10867 )
10868 .await;
10869
10870 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10871 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10872 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10873
10874 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10875 language_registry.add(rust_lang());
10876 let mut fake_servers = language_registry.register_fake_lsp(
10877 "Rust",
10878 FakeLspAdapter {
10879 capabilities: lsp::ServerCapabilities {
10880 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10881 ..Default::default()
10882 },
10883 ..Default::default()
10884 },
10885 );
10886
10887 let worktree = project.update(cx, |project, cx| {
10888 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10889 assert_eq!(worktrees.len(), 1);
10890 worktrees.pop().unwrap()
10891 });
10892 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10893
10894 let buffer_1 = project
10895 .update(cx, |project, cx| {
10896 project.open_buffer((worktree_id, "main.rs"), cx)
10897 })
10898 .await
10899 .unwrap();
10900 let buffer_2 = project
10901 .update(cx, |project, cx| {
10902 project.open_buffer((worktree_id, "other.rs"), cx)
10903 })
10904 .await
10905 .unwrap();
10906 let buffer_3 = project
10907 .update(cx, |project, cx| {
10908 project.open_buffer((worktree_id, "lib.rs"), cx)
10909 })
10910 .await
10911 .unwrap();
10912
10913 let multi_buffer = cx.new(|cx| {
10914 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10915 multi_buffer.push_excerpts(
10916 buffer_1.clone(),
10917 [
10918 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10919 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10920 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10921 ],
10922 cx,
10923 );
10924 multi_buffer.push_excerpts(
10925 buffer_2.clone(),
10926 [
10927 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10928 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10929 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10930 ],
10931 cx,
10932 );
10933 multi_buffer.push_excerpts(
10934 buffer_3.clone(),
10935 [
10936 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10937 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10938 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10939 ],
10940 cx,
10941 );
10942 multi_buffer
10943 });
10944 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10945 Editor::new(
10946 EditorMode::full(),
10947 multi_buffer,
10948 Some(project.clone()),
10949 window,
10950 cx,
10951 )
10952 });
10953
10954 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10955 editor.change_selections(
10956 SelectionEffects::scroll(Autoscroll::Next),
10957 window,
10958 cx,
10959 |s| s.select_ranges(Some(1..2)),
10960 );
10961 editor.insert("|one|two|three|", window, cx);
10962 });
10963 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10964 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10965 editor.change_selections(
10966 SelectionEffects::scroll(Autoscroll::Next),
10967 window,
10968 cx,
10969 |s| s.select_ranges(Some(60..70)),
10970 );
10971 editor.insert("|four|five|six|", window, cx);
10972 });
10973 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10974
10975 // First two buffers should be edited, but not the third one.
10976 assert_eq!(
10977 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10978 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
10979 );
10980 buffer_1.update(cx, |buffer, _| {
10981 assert!(buffer.is_dirty());
10982 assert_eq!(
10983 buffer.text(),
10984 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10985 )
10986 });
10987 buffer_2.update(cx, |buffer, _| {
10988 assert!(buffer.is_dirty());
10989 assert_eq!(
10990 buffer.text(),
10991 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
10992 )
10993 });
10994 buffer_3.update(cx, |buffer, _| {
10995 assert!(!buffer.is_dirty());
10996 assert_eq!(buffer.text(), sample_text_3,)
10997 });
10998 cx.executor().run_until_parked();
10999
11000 cx.executor().start_waiting();
11001 let save = multi_buffer_editor
11002 .update_in(cx, |editor, window, cx| {
11003 editor.save(
11004 SaveOptions {
11005 format: true,
11006 autosave: false,
11007 },
11008 project.clone(),
11009 window,
11010 cx,
11011 )
11012 })
11013 .unwrap();
11014
11015 let fake_server = fake_servers.next().await.unwrap();
11016 fake_server
11017 .server
11018 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11019 Ok(Some(vec![lsp::TextEdit::new(
11020 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11021 format!("[{} formatted]", params.text_document.uri),
11022 )]))
11023 })
11024 .detach();
11025 save.await;
11026
11027 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11028 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11029 assert_eq!(
11030 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11031 uri!(
11032 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
11033 ),
11034 );
11035 buffer_1.update(cx, |buffer, _| {
11036 assert!(!buffer.is_dirty());
11037 assert_eq!(
11038 buffer.text(),
11039 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11040 )
11041 });
11042 buffer_2.update(cx, |buffer, _| {
11043 assert!(!buffer.is_dirty());
11044 assert_eq!(
11045 buffer.text(),
11046 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11047 )
11048 });
11049 buffer_3.update(cx, |buffer, _| {
11050 assert!(!buffer.is_dirty());
11051 assert_eq!(buffer.text(), sample_text_3,)
11052 });
11053}
11054
11055#[gpui::test]
11056async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11057 init_test(cx, |_| {});
11058
11059 let fs = FakeFs::new(cx.executor());
11060 fs.insert_tree(
11061 path!("/dir"),
11062 json!({
11063 "file1.rs": "fn main() { println!(\"hello\"); }",
11064 "file2.rs": "fn test() { println!(\"test\"); }",
11065 "file3.rs": "fn other() { println!(\"other\"); }\n",
11066 }),
11067 )
11068 .await;
11069
11070 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11071 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11072 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11073
11074 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11075 language_registry.add(rust_lang());
11076
11077 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11078 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11079
11080 // Open three buffers
11081 let buffer_1 = project
11082 .update(cx, |project, cx| {
11083 project.open_buffer((worktree_id, "file1.rs"), cx)
11084 })
11085 .await
11086 .unwrap();
11087 let buffer_2 = project
11088 .update(cx, |project, cx| {
11089 project.open_buffer((worktree_id, "file2.rs"), cx)
11090 })
11091 .await
11092 .unwrap();
11093 let buffer_3 = project
11094 .update(cx, |project, cx| {
11095 project.open_buffer((worktree_id, "file3.rs"), cx)
11096 })
11097 .await
11098 .unwrap();
11099
11100 // Create a multi-buffer with all three buffers
11101 let multi_buffer = cx.new(|cx| {
11102 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11103 multi_buffer.push_excerpts(
11104 buffer_1.clone(),
11105 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11106 cx,
11107 );
11108 multi_buffer.push_excerpts(
11109 buffer_2.clone(),
11110 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11111 cx,
11112 );
11113 multi_buffer.push_excerpts(
11114 buffer_3.clone(),
11115 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11116 cx,
11117 );
11118 multi_buffer
11119 });
11120
11121 let editor = cx.new_window_entity(|window, cx| {
11122 Editor::new(
11123 EditorMode::full(),
11124 multi_buffer,
11125 Some(project.clone()),
11126 window,
11127 cx,
11128 )
11129 });
11130
11131 // Edit only the first buffer
11132 editor.update_in(cx, |editor, window, cx| {
11133 editor.change_selections(
11134 SelectionEffects::scroll(Autoscroll::Next),
11135 window,
11136 cx,
11137 |s| s.select_ranges(Some(10..10)),
11138 );
11139 editor.insert("// edited", window, cx);
11140 });
11141
11142 // Verify that only buffer 1 is dirty
11143 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11144 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11145 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11146
11147 // Get write counts after file creation (files were created with initial content)
11148 // We expect each file to have been written once during creation
11149 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11150 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11151 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11152
11153 // Perform autosave
11154 let save_task = editor.update_in(cx, |editor, window, cx| {
11155 editor.save(
11156 SaveOptions {
11157 format: true,
11158 autosave: true,
11159 },
11160 project.clone(),
11161 window,
11162 cx,
11163 )
11164 });
11165 save_task.await.unwrap();
11166
11167 // Only the dirty buffer should have been saved
11168 assert_eq!(
11169 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11170 1,
11171 "Buffer 1 was dirty, so it should have been written once during autosave"
11172 );
11173 assert_eq!(
11174 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11175 0,
11176 "Buffer 2 was clean, so it should not have been written during autosave"
11177 );
11178 assert_eq!(
11179 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11180 0,
11181 "Buffer 3 was clean, so it should not have been written during autosave"
11182 );
11183
11184 // Verify buffer states after autosave
11185 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11186 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11187 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11188
11189 // Now perform a manual save (format = true)
11190 let save_task = editor.update_in(cx, |editor, window, cx| {
11191 editor.save(
11192 SaveOptions {
11193 format: true,
11194 autosave: false,
11195 },
11196 project.clone(),
11197 window,
11198 cx,
11199 )
11200 });
11201 save_task.await.unwrap();
11202
11203 // During manual save, clean buffers don't get written to disk
11204 // They just get did_save called for language server notifications
11205 assert_eq!(
11206 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11207 1,
11208 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11209 );
11210 assert_eq!(
11211 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11212 0,
11213 "Buffer 2 should not have been written at all"
11214 );
11215 assert_eq!(
11216 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11217 0,
11218 "Buffer 3 should not have been written at all"
11219 );
11220}
11221
11222async fn setup_range_format_test(
11223 cx: &mut TestAppContext,
11224) -> (
11225 Entity<Project>,
11226 Entity<Editor>,
11227 &mut gpui::VisualTestContext,
11228 lsp::FakeLanguageServer,
11229) {
11230 init_test(cx, |_| {});
11231
11232 let fs = FakeFs::new(cx.executor());
11233 fs.insert_file(path!("/file.rs"), Default::default()).await;
11234
11235 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11236
11237 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11238 language_registry.add(rust_lang());
11239 let mut fake_servers = language_registry.register_fake_lsp(
11240 "Rust",
11241 FakeLspAdapter {
11242 capabilities: lsp::ServerCapabilities {
11243 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11244 ..lsp::ServerCapabilities::default()
11245 },
11246 ..FakeLspAdapter::default()
11247 },
11248 );
11249
11250 let buffer = project
11251 .update(cx, |project, cx| {
11252 project.open_local_buffer(path!("/file.rs"), cx)
11253 })
11254 .await
11255 .unwrap();
11256
11257 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11258 let (editor, cx) = cx.add_window_view(|window, cx| {
11259 build_editor_with_project(project.clone(), buffer, window, cx)
11260 });
11261
11262 cx.executor().start_waiting();
11263 let fake_server = fake_servers.next().await.unwrap();
11264
11265 (project, editor, cx, fake_server)
11266}
11267
11268#[gpui::test]
11269async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11270 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11271
11272 editor.update_in(cx, |editor, window, cx| {
11273 editor.set_text("one\ntwo\nthree\n", window, cx)
11274 });
11275 assert!(cx.read(|cx| editor.is_dirty(cx)));
11276
11277 let save = editor
11278 .update_in(cx, |editor, window, cx| {
11279 editor.save(
11280 SaveOptions {
11281 format: true,
11282 autosave: false,
11283 },
11284 project.clone(),
11285 window,
11286 cx,
11287 )
11288 })
11289 .unwrap();
11290 fake_server
11291 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11292 assert_eq!(
11293 params.text_document.uri,
11294 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11295 );
11296 assert_eq!(params.options.tab_size, 4);
11297 Ok(Some(vec![lsp::TextEdit::new(
11298 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11299 ", ".to_string(),
11300 )]))
11301 })
11302 .next()
11303 .await;
11304 cx.executor().start_waiting();
11305 save.await;
11306 assert_eq!(
11307 editor.update(cx, |editor, cx| editor.text(cx)),
11308 "one, two\nthree\n"
11309 );
11310 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11311}
11312
11313#[gpui::test]
11314async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11315 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11316
11317 editor.update_in(cx, |editor, window, cx| {
11318 editor.set_text("one\ntwo\nthree\n", window, cx)
11319 });
11320 assert!(cx.read(|cx| editor.is_dirty(cx)));
11321
11322 // Test that save still works when formatting hangs
11323 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11324 move |params, _| async move {
11325 assert_eq!(
11326 params.text_document.uri,
11327 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11328 );
11329 futures::future::pending::<()>().await;
11330 unreachable!()
11331 },
11332 );
11333 let save = editor
11334 .update_in(cx, |editor, window, cx| {
11335 editor.save(
11336 SaveOptions {
11337 format: true,
11338 autosave: false,
11339 },
11340 project.clone(),
11341 window,
11342 cx,
11343 )
11344 })
11345 .unwrap();
11346 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11347 cx.executor().start_waiting();
11348 save.await;
11349 assert_eq!(
11350 editor.update(cx, |editor, cx| editor.text(cx)),
11351 "one\ntwo\nthree\n"
11352 );
11353 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11354}
11355
11356#[gpui::test]
11357async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11358 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11359
11360 // Buffer starts clean, no formatting should be requested
11361 let save = editor
11362 .update_in(cx, |editor, window, cx| {
11363 editor.save(
11364 SaveOptions {
11365 format: false,
11366 autosave: false,
11367 },
11368 project.clone(),
11369 window,
11370 cx,
11371 )
11372 })
11373 .unwrap();
11374 let _pending_format_request = fake_server
11375 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11376 panic!("Should not be invoked");
11377 })
11378 .next();
11379 cx.executor().start_waiting();
11380 save.await;
11381 cx.run_until_parked();
11382}
11383
11384#[gpui::test]
11385async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11386 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11387
11388 // Set Rust language override and assert overridden tabsize is sent to language server
11389 update_test_language_settings(cx, |settings| {
11390 settings.languages.0.insert(
11391 "Rust".into(),
11392 LanguageSettingsContent {
11393 tab_size: NonZeroU32::new(8),
11394 ..Default::default()
11395 },
11396 );
11397 });
11398
11399 editor.update_in(cx, |editor, window, cx| {
11400 editor.set_text("something_new\n", window, cx)
11401 });
11402 assert!(cx.read(|cx| editor.is_dirty(cx)));
11403 let save = editor
11404 .update_in(cx, |editor, window, cx| {
11405 editor.save(
11406 SaveOptions {
11407 format: true,
11408 autosave: false,
11409 },
11410 project.clone(),
11411 window,
11412 cx,
11413 )
11414 })
11415 .unwrap();
11416 fake_server
11417 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11418 assert_eq!(
11419 params.text_document.uri,
11420 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11421 );
11422 assert_eq!(params.options.tab_size, 8);
11423 Ok(Some(Vec::new()))
11424 })
11425 .next()
11426 .await;
11427 save.await;
11428}
11429
11430#[gpui::test]
11431async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11432 init_test(cx, |settings| {
11433 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11434 Formatter::LanguageServer { name: None },
11435 )))
11436 });
11437
11438 let fs = FakeFs::new(cx.executor());
11439 fs.insert_file(path!("/file.rs"), Default::default()).await;
11440
11441 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11442
11443 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11444 language_registry.add(Arc::new(Language::new(
11445 LanguageConfig {
11446 name: "Rust".into(),
11447 matcher: LanguageMatcher {
11448 path_suffixes: vec!["rs".to_string()],
11449 ..Default::default()
11450 },
11451 ..LanguageConfig::default()
11452 },
11453 Some(tree_sitter_rust::LANGUAGE.into()),
11454 )));
11455 update_test_language_settings(cx, |settings| {
11456 // Enable Prettier formatting for the same buffer, and ensure
11457 // LSP is called instead of Prettier.
11458 settings.defaults.prettier = Some(PrettierSettings {
11459 allowed: true,
11460 ..PrettierSettings::default()
11461 });
11462 });
11463 let mut fake_servers = language_registry.register_fake_lsp(
11464 "Rust",
11465 FakeLspAdapter {
11466 capabilities: lsp::ServerCapabilities {
11467 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11468 ..Default::default()
11469 },
11470 ..Default::default()
11471 },
11472 );
11473
11474 let buffer = project
11475 .update(cx, |project, cx| {
11476 project.open_local_buffer(path!("/file.rs"), cx)
11477 })
11478 .await
11479 .unwrap();
11480
11481 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11482 let (editor, cx) = cx.add_window_view(|window, cx| {
11483 build_editor_with_project(project.clone(), buffer, window, cx)
11484 });
11485 editor.update_in(cx, |editor, window, cx| {
11486 editor.set_text("one\ntwo\nthree\n", window, cx)
11487 });
11488
11489 cx.executor().start_waiting();
11490 let fake_server = fake_servers.next().await.unwrap();
11491
11492 let format = editor
11493 .update_in(cx, |editor, window, cx| {
11494 editor.perform_format(
11495 project.clone(),
11496 FormatTrigger::Manual,
11497 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11498 window,
11499 cx,
11500 )
11501 })
11502 .unwrap();
11503 fake_server
11504 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11505 assert_eq!(
11506 params.text_document.uri,
11507 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11508 );
11509 assert_eq!(params.options.tab_size, 4);
11510 Ok(Some(vec![lsp::TextEdit::new(
11511 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11512 ", ".to_string(),
11513 )]))
11514 })
11515 .next()
11516 .await;
11517 cx.executor().start_waiting();
11518 format.await;
11519 assert_eq!(
11520 editor.update(cx, |editor, cx| editor.text(cx)),
11521 "one, two\nthree\n"
11522 );
11523
11524 editor.update_in(cx, |editor, window, cx| {
11525 editor.set_text("one\ntwo\nthree\n", window, cx)
11526 });
11527 // Ensure we don't lock if formatting hangs.
11528 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11529 move |params, _| async move {
11530 assert_eq!(
11531 params.text_document.uri,
11532 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11533 );
11534 futures::future::pending::<()>().await;
11535 unreachable!()
11536 },
11537 );
11538 let format = editor
11539 .update_in(cx, |editor, window, cx| {
11540 editor.perform_format(
11541 project,
11542 FormatTrigger::Manual,
11543 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11544 window,
11545 cx,
11546 )
11547 })
11548 .unwrap();
11549 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11550 cx.executor().start_waiting();
11551 format.await;
11552 assert_eq!(
11553 editor.update(cx, |editor, cx| editor.text(cx)),
11554 "one\ntwo\nthree\n"
11555 );
11556}
11557
11558#[gpui::test]
11559async fn test_multiple_formatters(cx: &mut TestAppContext) {
11560 init_test(cx, |settings| {
11561 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11562 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11563 Formatter::LanguageServer { name: None },
11564 Formatter::CodeActions(
11565 [
11566 ("code-action-1".into(), true),
11567 ("code-action-2".into(), true),
11568 ]
11569 .into_iter()
11570 .collect(),
11571 ),
11572 ])))
11573 });
11574
11575 let fs = FakeFs::new(cx.executor());
11576 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11577 .await;
11578
11579 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11580 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11581 language_registry.add(rust_lang());
11582
11583 let mut fake_servers = language_registry.register_fake_lsp(
11584 "Rust",
11585 FakeLspAdapter {
11586 capabilities: lsp::ServerCapabilities {
11587 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11588 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11589 commands: vec!["the-command-for-code-action-1".into()],
11590 ..Default::default()
11591 }),
11592 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11593 ..Default::default()
11594 },
11595 ..Default::default()
11596 },
11597 );
11598
11599 let buffer = project
11600 .update(cx, |project, cx| {
11601 project.open_local_buffer(path!("/file.rs"), cx)
11602 })
11603 .await
11604 .unwrap();
11605
11606 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11607 let (editor, cx) = cx.add_window_view(|window, cx| {
11608 build_editor_with_project(project.clone(), buffer, window, cx)
11609 });
11610
11611 cx.executor().start_waiting();
11612
11613 let fake_server = fake_servers.next().await.unwrap();
11614 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11615 move |_params, _| async move {
11616 Ok(Some(vec![lsp::TextEdit::new(
11617 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11618 "applied-formatting\n".to_string(),
11619 )]))
11620 },
11621 );
11622 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11623 move |params, _| async move {
11624 assert_eq!(
11625 params.context.only,
11626 Some(vec!["code-action-1".into(), "code-action-2".into()])
11627 );
11628 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11629 Ok(Some(vec![
11630 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11631 kind: Some("code-action-1".into()),
11632 edit: Some(lsp::WorkspaceEdit::new(
11633 [(
11634 uri.clone(),
11635 vec![lsp::TextEdit::new(
11636 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11637 "applied-code-action-1-edit\n".to_string(),
11638 )],
11639 )]
11640 .into_iter()
11641 .collect(),
11642 )),
11643 command: Some(lsp::Command {
11644 command: "the-command-for-code-action-1".into(),
11645 ..Default::default()
11646 }),
11647 ..Default::default()
11648 }),
11649 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11650 kind: Some("code-action-2".into()),
11651 edit: Some(lsp::WorkspaceEdit::new(
11652 [(
11653 uri,
11654 vec![lsp::TextEdit::new(
11655 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11656 "applied-code-action-2-edit\n".to_string(),
11657 )],
11658 )]
11659 .into_iter()
11660 .collect(),
11661 )),
11662 ..Default::default()
11663 }),
11664 ]))
11665 },
11666 );
11667
11668 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11669 move |params, _| async move { Ok(params) }
11670 });
11671
11672 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11673 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11674 let fake = fake_server.clone();
11675 let lock = command_lock.clone();
11676 move |params, _| {
11677 assert_eq!(params.command, "the-command-for-code-action-1");
11678 let fake = fake.clone();
11679 let lock = lock.clone();
11680 async move {
11681 lock.lock().await;
11682 fake.server
11683 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11684 label: None,
11685 edit: lsp::WorkspaceEdit {
11686 changes: Some(
11687 [(
11688 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11689 vec![lsp::TextEdit {
11690 range: lsp::Range::new(
11691 lsp::Position::new(0, 0),
11692 lsp::Position::new(0, 0),
11693 ),
11694 new_text: "applied-code-action-1-command\n".into(),
11695 }],
11696 )]
11697 .into_iter()
11698 .collect(),
11699 ),
11700 ..Default::default()
11701 },
11702 })
11703 .await
11704 .into_response()
11705 .unwrap();
11706 Ok(Some(json!(null)))
11707 }
11708 }
11709 });
11710
11711 cx.executor().start_waiting();
11712 editor
11713 .update_in(cx, |editor, window, cx| {
11714 editor.perform_format(
11715 project.clone(),
11716 FormatTrigger::Manual,
11717 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11718 window,
11719 cx,
11720 )
11721 })
11722 .unwrap()
11723 .await;
11724 editor.update(cx, |editor, cx| {
11725 assert_eq!(
11726 editor.text(cx),
11727 r#"
11728 applied-code-action-2-edit
11729 applied-code-action-1-command
11730 applied-code-action-1-edit
11731 applied-formatting
11732 one
11733 two
11734 three
11735 "#
11736 .unindent()
11737 );
11738 });
11739
11740 editor.update_in(cx, |editor, window, cx| {
11741 editor.undo(&Default::default(), window, cx);
11742 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11743 });
11744
11745 // Perform a manual edit while waiting for an LSP command
11746 // that's being run as part of a formatting code action.
11747 let lock_guard = command_lock.lock().await;
11748 let format = editor
11749 .update_in(cx, |editor, window, cx| {
11750 editor.perform_format(
11751 project.clone(),
11752 FormatTrigger::Manual,
11753 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11754 window,
11755 cx,
11756 )
11757 })
11758 .unwrap();
11759 cx.run_until_parked();
11760 editor.update(cx, |editor, cx| {
11761 assert_eq!(
11762 editor.text(cx),
11763 r#"
11764 applied-code-action-1-edit
11765 applied-formatting
11766 one
11767 two
11768 three
11769 "#
11770 .unindent()
11771 );
11772
11773 editor.buffer.update(cx, |buffer, cx| {
11774 let ix = buffer.len(cx);
11775 buffer.edit([(ix..ix, "edited\n")], None, cx);
11776 });
11777 });
11778
11779 // Allow the LSP command to proceed. Because the buffer was edited,
11780 // the second code action will not be run.
11781 drop(lock_guard);
11782 format.await;
11783 editor.update_in(cx, |editor, window, cx| {
11784 assert_eq!(
11785 editor.text(cx),
11786 r#"
11787 applied-code-action-1-command
11788 applied-code-action-1-edit
11789 applied-formatting
11790 one
11791 two
11792 three
11793 edited
11794 "#
11795 .unindent()
11796 );
11797
11798 // The manual edit is undone first, because it is the last thing the user did
11799 // (even though the command completed afterwards).
11800 editor.undo(&Default::default(), window, cx);
11801 assert_eq!(
11802 editor.text(cx),
11803 r#"
11804 applied-code-action-1-command
11805 applied-code-action-1-edit
11806 applied-formatting
11807 one
11808 two
11809 three
11810 "#
11811 .unindent()
11812 );
11813
11814 // All the formatting (including the command, which completed after the manual edit)
11815 // is undone together.
11816 editor.undo(&Default::default(), window, cx);
11817 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11818 });
11819}
11820
11821#[gpui::test]
11822async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
11823 init_test(cx, |settings| {
11824 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11825 Formatter::LanguageServer { name: None },
11826 ])))
11827 });
11828
11829 let fs = FakeFs::new(cx.executor());
11830 fs.insert_file(path!("/file.ts"), Default::default()).await;
11831
11832 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11833
11834 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11835 language_registry.add(Arc::new(Language::new(
11836 LanguageConfig {
11837 name: "TypeScript".into(),
11838 matcher: LanguageMatcher {
11839 path_suffixes: vec!["ts".to_string()],
11840 ..Default::default()
11841 },
11842 ..LanguageConfig::default()
11843 },
11844 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11845 )));
11846 update_test_language_settings(cx, |settings| {
11847 settings.defaults.prettier = Some(PrettierSettings {
11848 allowed: true,
11849 ..PrettierSettings::default()
11850 });
11851 });
11852 let mut fake_servers = language_registry.register_fake_lsp(
11853 "TypeScript",
11854 FakeLspAdapter {
11855 capabilities: lsp::ServerCapabilities {
11856 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11857 ..Default::default()
11858 },
11859 ..Default::default()
11860 },
11861 );
11862
11863 let buffer = project
11864 .update(cx, |project, cx| {
11865 project.open_local_buffer(path!("/file.ts"), cx)
11866 })
11867 .await
11868 .unwrap();
11869
11870 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11871 let (editor, cx) = cx.add_window_view(|window, cx| {
11872 build_editor_with_project(project.clone(), buffer, window, cx)
11873 });
11874 editor.update_in(cx, |editor, window, cx| {
11875 editor.set_text(
11876 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11877 window,
11878 cx,
11879 )
11880 });
11881
11882 cx.executor().start_waiting();
11883 let fake_server = fake_servers.next().await.unwrap();
11884
11885 let format = editor
11886 .update_in(cx, |editor, window, cx| {
11887 editor.perform_code_action_kind(
11888 project.clone(),
11889 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11890 window,
11891 cx,
11892 )
11893 })
11894 .unwrap();
11895 fake_server
11896 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11897 assert_eq!(
11898 params.text_document.uri,
11899 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11900 );
11901 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11902 lsp::CodeAction {
11903 title: "Organize Imports".to_string(),
11904 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11905 edit: Some(lsp::WorkspaceEdit {
11906 changes: Some(
11907 [(
11908 params.text_document.uri.clone(),
11909 vec![lsp::TextEdit::new(
11910 lsp::Range::new(
11911 lsp::Position::new(1, 0),
11912 lsp::Position::new(2, 0),
11913 ),
11914 "".to_string(),
11915 )],
11916 )]
11917 .into_iter()
11918 .collect(),
11919 ),
11920 ..Default::default()
11921 }),
11922 ..Default::default()
11923 },
11924 )]))
11925 })
11926 .next()
11927 .await;
11928 cx.executor().start_waiting();
11929 format.await;
11930 assert_eq!(
11931 editor.update(cx, |editor, cx| editor.text(cx)),
11932 "import { a } from 'module';\n\nconst x = a;\n"
11933 );
11934
11935 editor.update_in(cx, |editor, window, cx| {
11936 editor.set_text(
11937 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11938 window,
11939 cx,
11940 )
11941 });
11942 // Ensure we don't lock if code action hangs.
11943 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11944 move |params, _| async move {
11945 assert_eq!(
11946 params.text_document.uri,
11947 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
11948 );
11949 futures::future::pending::<()>().await;
11950 unreachable!()
11951 },
11952 );
11953 let format = editor
11954 .update_in(cx, |editor, window, cx| {
11955 editor.perform_code_action_kind(
11956 project,
11957 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11958 window,
11959 cx,
11960 )
11961 })
11962 .unwrap();
11963 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11964 cx.executor().start_waiting();
11965 format.await;
11966 assert_eq!(
11967 editor.update(cx, |editor, cx| editor.text(cx)),
11968 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11969 );
11970}
11971
11972#[gpui::test]
11973async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11974 init_test(cx, |_| {});
11975
11976 let mut cx = EditorLspTestContext::new_rust(
11977 lsp::ServerCapabilities {
11978 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11979 ..Default::default()
11980 },
11981 cx,
11982 )
11983 .await;
11984
11985 cx.set_state(indoc! {"
11986 one.twoˇ
11987 "});
11988
11989 // The format request takes a long time. When it completes, it inserts
11990 // a newline and an indent before the `.`
11991 cx.lsp
11992 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
11993 let executor = cx.background_executor().clone();
11994 async move {
11995 executor.timer(Duration::from_millis(100)).await;
11996 Ok(Some(vec![lsp::TextEdit {
11997 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
11998 new_text: "\n ".into(),
11999 }]))
12000 }
12001 });
12002
12003 // Submit a format request.
12004 let format_1 = cx
12005 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12006 .unwrap();
12007 cx.executor().run_until_parked();
12008
12009 // Submit a second format request.
12010 let format_2 = cx
12011 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12012 .unwrap();
12013 cx.executor().run_until_parked();
12014
12015 // Wait for both format requests to complete
12016 cx.executor().advance_clock(Duration::from_millis(200));
12017 cx.executor().start_waiting();
12018 format_1.await.unwrap();
12019 cx.executor().start_waiting();
12020 format_2.await.unwrap();
12021
12022 // The formatting edits only happens once.
12023 cx.assert_editor_state(indoc! {"
12024 one
12025 .twoˇ
12026 "});
12027}
12028
12029#[gpui::test]
12030async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12031 init_test(cx, |settings| {
12032 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12033 });
12034
12035 let mut cx = EditorLspTestContext::new_rust(
12036 lsp::ServerCapabilities {
12037 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12038 ..Default::default()
12039 },
12040 cx,
12041 )
12042 .await;
12043
12044 // Set up a buffer white some trailing whitespace and no trailing newline.
12045 cx.set_state(
12046 &[
12047 "one ", //
12048 "twoˇ", //
12049 "three ", //
12050 "four", //
12051 ]
12052 .join("\n"),
12053 );
12054
12055 // Submit a format request.
12056 let format = cx
12057 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12058 .unwrap();
12059
12060 // Record which buffer changes have been sent to the language server
12061 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12062 cx.lsp
12063 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12064 let buffer_changes = buffer_changes.clone();
12065 move |params, _| {
12066 buffer_changes.lock().extend(
12067 params
12068 .content_changes
12069 .into_iter()
12070 .map(|e| (e.range.unwrap(), e.text)),
12071 );
12072 }
12073 });
12074
12075 // Handle formatting requests to the language server.
12076 cx.lsp
12077 .set_request_handler::<lsp::request::Formatting, _, _>({
12078 let buffer_changes = buffer_changes.clone();
12079 move |_, _| {
12080 // When formatting is requested, trailing whitespace has already been stripped,
12081 // and the trailing newline has already been added.
12082 assert_eq!(
12083 &buffer_changes.lock()[1..],
12084 &[
12085 (
12086 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12087 "".into()
12088 ),
12089 (
12090 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12091 "".into()
12092 ),
12093 (
12094 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12095 "\n".into()
12096 ),
12097 ]
12098 );
12099
12100 // Insert blank lines between each line of the buffer.
12101 async move {
12102 Ok(Some(vec![
12103 lsp::TextEdit {
12104 range: lsp::Range::new(
12105 lsp::Position::new(1, 0),
12106 lsp::Position::new(1, 0),
12107 ),
12108 new_text: "\n".into(),
12109 },
12110 lsp::TextEdit {
12111 range: lsp::Range::new(
12112 lsp::Position::new(2, 0),
12113 lsp::Position::new(2, 0),
12114 ),
12115 new_text: "\n".into(),
12116 },
12117 ]))
12118 }
12119 }
12120 });
12121
12122 // After formatting the buffer, the trailing whitespace is stripped,
12123 // a newline is appended, and the edits provided by the language server
12124 // have been applied.
12125 format.await.unwrap();
12126 cx.assert_editor_state(
12127 &[
12128 "one", //
12129 "", //
12130 "twoˇ", //
12131 "", //
12132 "three", //
12133 "four", //
12134 "", //
12135 ]
12136 .join("\n"),
12137 );
12138
12139 // Undoing the formatting undoes the trailing whitespace removal, the
12140 // trailing newline, and the LSP edits.
12141 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12142 cx.assert_editor_state(
12143 &[
12144 "one ", //
12145 "twoˇ", //
12146 "three ", //
12147 "four", //
12148 ]
12149 .join("\n"),
12150 );
12151}
12152
12153#[gpui::test]
12154async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12155 cx: &mut TestAppContext,
12156) {
12157 init_test(cx, |_| {});
12158
12159 cx.update(|cx| {
12160 cx.update_global::<SettingsStore, _>(|settings, cx| {
12161 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12162 settings.auto_signature_help = Some(true);
12163 });
12164 });
12165 });
12166
12167 let mut cx = EditorLspTestContext::new_rust(
12168 lsp::ServerCapabilities {
12169 signature_help_provider: Some(lsp::SignatureHelpOptions {
12170 ..Default::default()
12171 }),
12172 ..Default::default()
12173 },
12174 cx,
12175 )
12176 .await;
12177
12178 let language = Language::new(
12179 LanguageConfig {
12180 name: "Rust".into(),
12181 brackets: BracketPairConfig {
12182 pairs: vec![
12183 BracketPair {
12184 start: "{".to_string(),
12185 end: "}".to_string(),
12186 close: true,
12187 surround: true,
12188 newline: true,
12189 },
12190 BracketPair {
12191 start: "(".to_string(),
12192 end: ")".to_string(),
12193 close: true,
12194 surround: true,
12195 newline: true,
12196 },
12197 BracketPair {
12198 start: "/*".to_string(),
12199 end: " */".to_string(),
12200 close: true,
12201 surround: true,
12202 newline: true,
12203 },
12204 BracketPair {
12205 start: "[".to_string(),
12206 end: "]".to_string(),
12207 close: false,
12208 surround: false,
12209 newline: true,
12210 },
12211 BracketPair {
12212 start: "\"".to_string(),
12213 end: "\"".to_string(),
12214 close: true,
12215 surround: true,
12216 newline: false,
12217 },
12218 BracketPair {
12219 start: "<".to_string(),
12220 end: ">".to_string(),
12221 close: false,
12222 surround: true,
12223 newline: true,
12224 },
12225 ],
12226 ..Default::default()
12227 },
12228 autoclose_before: "})]".to_string(),
12229 ..Default::default()
12230 },
12231 Some(tree_sitter_rust::LANGUAGE.into()),
12232 );
12233 let language = Arc::new(language);
12234
12235 cx.language_registry().add(language.clone());
12236 cx.update_buffer(|buffer, cx| {
12237 buffer.set_language(Some(language), cx);
12238 });
12239
12240 cx.set_state(
12241 &r#"
12242 fn main() {
12243 sampleˇ
12244 }
12245 "#
12246 .unindent(),
12247 );
12248
12249 cx.update_editor(|editor, window, cx| {
12250 editor.handle_input("(", window, cx);
12251 });
12252 cx.assert_editor_state(
12253 &"
12254 fn main() {
12255 sample(ˇ)
12256 }
12257 "
12258 .unindent(),
12259 );
12260
12261 let mocked_response = lsp::SignatureHelp {
12262 signatures: vec![lsp::SignatureInformation {
12263 label: "fn sample(param1: u8, param2: u8)".to_string(),
12264 documentation: None,
12265 parameters: Some(vec![
12266 lsp::ParameterInformation {
12267 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12268 documentation: None,
12269 },
12270 lsp::ParameterInformation {
12271 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12272 documentation: None,
12273 },
12274 ]),
12275 active_parameter: None,
12276 }],
12277 active_signature: Some(0),
12278 active_parameter: Some(0),
12279 };
12280 handle_signature_help_request(&mut cx, mocked_response).await;
12281
12282 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12283 .await;
12284
12285 cx.editor(|editor, _, _| {
12286 let signature_help_state = editor.signature_help_state.popover().cloned();
12287 let signature = signature_help_state.unwrap();
12288 assert_eq!(
12289 signature.signatures[signature.current_signature].label,
12290 "fn sample(param1: u8, param2: u8)"
12291 );
12292 });
12293}
12294
12295#[gpui::test]
12296async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12297 init_test(cx, |_| {});
12298
12299 cx.update(|cx| {
12300 cx.update_global::<SettingsStore, _>(|settings, cx| {
12301 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12302 settings.auto_signature_help = Some(false);
12303 settings.show_signature_help_after_edits = Some(false);
12304 });
12305 });
12306 });
12307
12308 let mut cx = EditorLspTestContext::new_rust(
12309 lsp::ServerCapabilities {
12310 signature_help_provider: Some(lsp::SignatureHelpOptions {
12311 ..Default::default()
12312 }),
12313 ..Default::default()
12314 },
12315 cx,
12316 )
12317 .await;
12318
12319 let language = Language::new(
12320 LanguageConfig {
12321 name: "Rust".into(),
12322 brackets: BracketPairConfig {
12323 pairs: vec![
12324 BracketPair {
12325 start: "{".to_string(),
12326 end: "}".to_string(),
12327 close: true,
12328 surround: true,
12329 newline: true,
12330 },
12331 BracketPair {
12332 start: "(".to_string(),
12333 end: ")".to_string(),
12334 close: true,
12335 surround: true,
12336 newline: true,
12337 },
12338 BracketPair {
12339 start: "/*".to_string(),
12340 end: " */".to_string(),
12341 close: true,
12342 surround: true,
12343 newline: true,
12344 },
12345 BracketPair {
12346 start: "[".to_string(),
12347 end: "]".to_string(),
12348 close: false,
12349 surround: false,
12350 newline: true,
12351 },
12352 BracketPair {
12353 start: "\"".to_string(),
12354 end: "\"".to_string(),
12355 close: true,
12356 surround: true,
12357 newline: false,
12358 },
12359 BracketPair {
12360 start: "<".to_string(),
12361 end: ">".to_string(),
12362 close: false,
12363 surround: true,
12364 newline: true,
12365 },
12366 ],
12367 ..Default::default()
12368 },
12369 autoclose_before: "})]".to_string(),
12370 ..Default::default()
12371 },
12372 Some(tree_sitter_rust::LANGUAGE.into()),
12373 );
12374 let language = Arc::new(language);
12375
12376 cx.language_registry().add(language.clone());
12377 cx.update_buffer(|buffer, cx| {
12378 buffer.set_language(Some(language), cx);
12379 });
12380
12381 // Ensure that signature_help is not called when no signature help is enabled.
12382 cx.set_state(
12383 &r#"
12384 fn main() {
12385 sampleˇ
12386 }
12387 "#
12388 .unindent(),
12389 );
12390 cx.update_editor(|editor, window, cx| {
12391 editor.handle_input("(", window, cx);
12392 });
12393 cx.assert_editor_state(
12394 &"
12395 fn main() {
12396 sample(ˇ)
12397 }
12398 "
12399 .unindent(),
12400 );
12401 cx.editor(|editor, _, _| {
12402 assert!(editor.signature_help_state.task().is_none());
12403 });
12404
12405 let mocked_response = lsp::SignatureHelp {
12406 signatures: vec![lsp::SignatureInformation {
12407 label: "fn sample(param1: u8, param2: u8)".to_string(),
12408 documentation: None,
12409 parameters: Some(vec![
12410 lsp::ParameterInformation {
12411 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12412 documentation: None,
12413 },
12414 lsp::ParameterInformation {
12415 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12416 documentation: None,
12417 },
12418 ]),
12419 active_parameter: None,
12420 }],
12421 active_signature: Some(0),
12422 active_parameter: Some(0),
12423 };
12424
12425 // Ensure that signature_help is called when enabled afte edits
12426 cx.update(|_, cx| {
12427 cx.update_global::<SettingsStore, _>(|settings, cx| {
12428 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12429 settings.auto_signature_help = Some(false);
12430 settings.show_signature_help_after_edits = Some(true);
12431 });
12432 });
12433 });
12434 cx.set_state(
12435 &r#"
12436 fn main() {
12437 sampleˇ
12438 }
12439 "#
12440 .unindent(),
12441 );
12442 cx.update_editor(|editor, window, cx| {
12443 editor.handle_input("(", window, cx);
12444 });
12445 cx.assert_editor_state(
12446 &"
12447 fn main() {
12448 sample(ˇ)
12449 }
12450 "
12451 .unindent(),
12452 );
12453 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12454 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12455 .await;
12456 cx.update_editor(|editor, _, _| {
12457 let signature_help_state = editor.signature_help_state.popover().cloned();
12458 assert!(signature_help_state.is_some());
12459 let signature = signature_help_state.unwrap();
12460 assert_eq!(
12461 signature.signatures[signature.current_signature].label,
12462 "fn sample(param1: u8, param2: u8)"
12463 );
12464 editor.signature_help_state = SignatureHelpState::default();
12465 });
12466
12467 // Ensure that signature_help is called when auto signature help override is enabled
12468 cx.update(|_, cx| {
12469 cx.update_global::<SettingsStore, _>(|settings, cx| {
12470 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12471 settings.auto_signature_help = Some(true);
12472 settings.show_signature_help_after_edits = Some(false);
12473 });
12474 });
12475 });
12476 cx.set_state(
12477 &r#"
12478 fn main() {
12479 sampleˇ
12480 }
12481 "#
12482 .unindent(),
12483 );
12484 cx.update_editor(|editor, window, cx| {
12485 editor.handle_input("(", window, cx);
12486 });
12487 cx.assert_editor_state(
12488 &"
12489 fn main() {
12490 sample(ˇ)
12491 }
12492 "
12493 .unindent(),
12494 );
12495 handle_signature_help_request(&mut cx, mocked_response).await;
12496 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12497 .await;
12498 cx.editor(|editor, _, _| {
12499 let signature_help_state = editor.signature_help_state.popover().cloned();
12500 assert!(signature_help_state.is_some());
12501 let signature = signature_help_state.unwrap();
12502 assert_eq!(
12503 signature.signatures[signature.current_signature].label,
12504 "fn sample(param1: u8, param2: u8)"
12505 );
12506 });
12507}
12508
12509#[gpui::test]
12510async fn test_signature_help(cx: &mut TestAppContext) {
12511 init_test(cx, |_| {});
12512 cx.update(|cx| {
12513 cx.update_global::<SettingsStore, _>(|settings, cx| {
12514 settings.update_user_settings::<EditorSettings>(cx, |settings| {
12515 settings.auto_signature_help = Some(true);
12516 });
12517 });
12518 });
12519
12520 let mut cx = EditorLspTestContext::new_rust(
12521 lsp::ServerCapabilities {
12522 signature_help_provider: Some(lsp::SignatureHelpOptions {
12523 ..Default::default()
12524 }),
12525 ..Default::default()
12526 },
12527 cx,
12528 )
12529 .await;
12530
12531 // A test that directly calls `show_signature_help`
12532 cx.update_editor(|editor, window, cx| {
12533 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12534 });
12535
12536 let mocked_response = lsp::SignatureHelp {
12537 signatures: vec![lsp::SignatureInformation {
12538 label: "fn sample(param1: u8, param2: u8)".to_string(),
12539 documentation: None,
12540 parameters: Some(vec![
12541 lsp::ParameterInformation {
12542 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12543 documentation: None,
12544 },
12545 lsp::ParameterInformation {
12546 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12547 documentation: None,
12548 },
12549 ]),
12550 active_parameter: None,
12551 }],
12552 active_signature: Some(0),
12553 active_parameter: Some(0),
12554 };
12555 handle_signature_help_request(&mut cx, mocked_response).await;
12556
12557 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12558 .await;
12559
12560 cx.editor(|editor, _, _| {
12561 let signature_help_state = editor.signature_help_state.popover().cloned();
12562 assert!(signature_help_state.is_some());
12563 let signature = signature_help_state.unwrap();
12564 assert_eq!(
12565 signature.signatures[signature.current_signature].label,
12566 "fn sample(param1: u8, param2: u8)"
12567 );
12568 });
12569
12570 // When exiting outside from inside the brackets, `signature_help` is closed.
12571 cx.set_state(indoc! {"
12572 fn main() {
12573 sample(ˇ);
12574 }
12575
12576 fn sample(param1: u8, param2: u8) {}
12577 "});
12578
12579 cx.update_editor(|editor, window, cx| {
12580 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12581 s.select_ranges([0..0])
12582 });
12583 });
12584
12585 let mocked_response = lsp::SignatureHelp {
12586 signatures: Vec::new(),
12587 active_signature: None,
12588 active_parameter: None,
12589 };
12590 handle_signature_help_request(&mut cx, mocked_response).await;
12591
12592 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12593 .await;
12594
12595 cx.editor(|editor, _, _| {
12596 assert!(!editor.signature_help_state.is_shown());
12597 });
12598
12599 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12600 cx.set_state(indoc! {"
12601 fn main() {
12602 sample(ˇ);
12603 }
12604
12605 fn sample(param1: u8, param2: u8) {}
12606 "});
12607
12608 let mocked_response = lsp::SignatureHelp {
12609 signatures: vec![lsp::SignatureInformation {
12610 label: "fn sample(param1: u8, param2: u8)".to_string(),
12611 documentation: None,
12612 parameters: Some(vec![
12613 lsp::ParameterInformation {
12614 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12615 documentation: None,
12616 },
12617 lsp::ParameterInformation {
12618 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12619 documentation: None,
12620 },
12621 ]),
12622 active_parameter: None,
12623 }],
12624 active_signature: Some(0),
12625 active_parameter: Some(0),
12626 };
12627 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12628 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12629 .await;
12630 cx.editor(|editor, _, _| {
12631 assert!(editor.signature_help_state.is_shown());
12632 });
12633
12634 // Restore the popover with more parameter input
12635 cx.set_state(indoc! {"
12636 fn main() {
12637 sample(param1, param2ˇ);
12638 }
12639
12640 fn sample(param1: u8, param2: u8) {}
12641 "});
12642
12643 let mocked_response = lsp::SignatureHelp {
12644 signatures: vec![lsp::SignatureInformation {
12645 label: "fn sample(param1: u8, param2: u8)".to_string(),
12646 documentation: None,
12647 parameters: Some(vec![
12648 lsp::ParameterInformation {
12649 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12650 documentation: None,
12651 },
12652 lsp::ParameterInformation {
12653 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12654 documentation: None,
12655 },
12656 ]),
12657 active_parameter: None,
12658 }],
12659 active_signature: Some(0),
12660 active_parameter: Some(1),
12661 };
12662 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12663 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12664 .await;
12665
12666 // When selecting a range, the popover is gone.
12667 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12668 cx.update_editor(|editor, window, cx| {
12669 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12670 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12671 })
12672 });
12673 cx.assert_editor_state(indoc! {"
12674 fn main() {
12675 sample(param1, «ˇparam2»);
12676 }
12677
12678 fn sample(param1: u8, param2: u8) {}
12679 "});
12680 cx.editor(|editor, _, _| {
12681 assert!(!editor.signature_help_state.is_shown());
12682 });
12683
12684 // When unselecting again, the popover is back if within the brackets.
12685 cx.update_editor(|editor, window, cx| {
12686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12687 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12688 })
12689 });
12690 cx.assert_editor_state(indoc! {"
12691 fn main() {
12692 sample(param1, ˇparam2);
12693 }
12694
12695 fn sample(param1: u8, param2: u8) {}
12696 "});
12697 handle_signature_help_request(&mut cx, mocked_response).await;
12698 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12699 .await;
12700 cx.editor(|editor, _, _| {
12701 assert!(editor.signature_help_state.is_shown());
12702 });
12703
12704 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12705 cx.update_editor(|editor, window, cx| {
12706 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12707 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12708 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12709 })
12710 });
12711 cx.assert_editor_state(indoc! {"
12712 fn main() {
12713 sample(param1, ˇparam2);
12714 }
12715
12716 fn sample(param1: u8, param2: u8) {}
12717 "});
12718
12719 let mocked_response = lsp::SignatureHelp {
12720 signatures: vec![lsp::SignatureInformation {
12721 label: "fn sample(param1: u8, param2: u8)".to_string(),
12722 documentation: None,
12723 parameters: Some(vec![
12724 lsp::ParameterInformation {
12725 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12726 documentation: None,
12727 },
12728 lsp::ParameterInformation {
12729 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12730 documentation: None,
12731 },
12732 ]),
12733 active_parameter: None,
12734 }],
12735 active_signature: Some(0),
12736 active_parameter: Some(1),
12737 };
12738 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12739 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12740 .await;
12741 cx.update_editor(|editor, _, cx| {
12742 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12743 });
12744 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12745 .await;
12746 cx.update_editor(|editor, window, cx| {
12747 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12748 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12749 })
12750 });
12751 cx.assert_editor_state(indoc! {"
12752 fn main() {
12753 sample(param1, «ˇparam2»);
12754 }
12755
12756 fn sample(param1: u8, param2: u8) {}
12757 "});
12758 cx.update_editor(|editor, window, cx| {
12759 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12760 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12761 })
12762 });
12763 cx.assert_editor_state(indoc! {"
12764 fn main() {
12765 sample(param1, ˇparam2);
12766 }
12767
12768 fn sample(param1: u8, param2: u8) {}
12769 "});
12770 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
12771 .await;
12772}
12773
12774#[gpui::test]
12775async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
12776 init_test(cx, |_| {});
12777
12778 let mut cx = EditorLspTestContext::new_rust(
12779 lsp::ServerCapabilities {
12780 signature_help_provider: Some(lsp::SignatureHelpOptions {
12781 ..Default::default()
12782 }),
12783 ..Default::default()
12784 },
12785 cx,
12786 )
12787 .await;
12788
12789 cx.set_state(indoc! {"
12790 fn main() {
12791 overloadedˇ
12792 }
12793 "});
12794
12795 cx.update_editor(|editor, window, cx| {
12796 editor.handle_input("(", window, cx);
12797 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12798 });
12799
12800 // Mock response with 3 signatures
12801 let mocked_response = lsp::SignatureHelp {
12802 signatures: vec![
12803 lsp::SignatureInformation {
12804 label: "fn overloaded(x: i32)".to_string(),
12805 documentation: None,
12806 parameters: Some(vec![lsp::ParameterInformation {
12807 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12808 documentation: None,
12809 }]),
12810 active_parameter: None,
12811 },
12812 lsp::SignatureInformation {
12813 label: "fn overloaded(x: i32, y: i32)".to_string(),
12814 documentation: None,
12815 parameters: Some(vec![
12816 lsp::ParameterInformation {
12817 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12818 documentation: None,
12819 },
12820 lsp::ParameterInformation {
12821 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12822 documentation: None,
12823 },
12824 ]),
12825 active_parameter: None,
12826 },
12827 lsp::SignatureInformation {
12828 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
12829 documentation: None,
12830 parameters: Some(vec![
12831 lsp::ParameterInformation {
12832 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
12833 documentation: None,
12834 },
12835 lsp::ParameterInformation {
12836 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
12837 documentation: None,
12838 },
12839 lsp::ParameterInformation {
12840 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
12841 documentation: None,
12842 },
12843 ]),
12844 active_parameter: None,
12845 },
12846 ],
12847 active_signature: Some(1),
12848 active_parameter: Some(0),
12849 };
12850 handle_signature_help_request(&mut cx, mocked_response).await;
12851
12852 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12853 .await;
12854
12855 // Verify we have multiple signatures and the right one is selected
12856 cx.editor(|editor, _, _| {
12857 let popover = editor.signature_help_state.popover().cloned().unwrap();
12858 assert_eq!(popover.signatures.len(), 3);
12859 // active_signature was 1, so that should be the current
12860 assert_eq!(popover.current_signature, 1);
12861 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
12862 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
12863 assert_eq!(
12864 popover.signatures[2].label,
12865 "fn overloaded(x: i32, y: i32, z: i32)"
12866 );
12867 });
12868
12869 // Test navigation functionality
12870 cx.update_editor(|editor, window, cx| {
12871 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12872 });
12873
12874 cx.editor(|editor, _, _| {
12875 let popover = editor.signature_help_state.popover().cloned().unwrap();
12876 assert_eq!(popover.current_signature, 2);
12877 });
12878
12879 // Test wrap around
12880 cx.update_editor(|editor, window, cx| {
12881 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12882 });
12883
12884 cx.editor(|editor, _, _| {
12885 let popover = editor.signature_help_state.popover().cloned().unwrap();
12886 assert_eq!(popover.current_signature, 0);
12887 });
12888
12889 // Test previous navigation
12890 cx.update_editor(|editor, window, cx| {
12891 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12892 });
12893
12894 cx.editor(|editor, _, _| {
12895 let popover = editor.signature_help_state.popover().cloned().unwrap();
12896 assert_eq!(popover.current_signature, 2);
12897 });
12898}
12899
12900#[gpui::test]
12901async fn test_completion_mode(cx: &mut TestAppContext) {
12902 init_test(cx, |_| {});
12903 let mut cx = EditorLspTestContext::new_rust(
12904 lsp::ServerCapabilities {
12905 completion_provider: Some(lsp::CompletionOptions {
12906 resolve_provider: Some(true),
12907 ..Default::default()
12908 }),
12909 ..Default::default()
12910 },
12911 cx,
12912 )
12913 .await;
12914
12915 struct Run {
12916 run_description: &'static str,
12917 initial_state: String,
12918 buffer_marked_text: String,
12919 completion_label: &'static str,
12920 completion_text: &'static str,
12921 expected_with_insert_mode: String,
12922 expected_with_replace_mode: String,
12923 expected_with_replace_subsequence_mode: String,
12924 expected_with_replace_suffix_mode: String,
12925 }
12926
12927 let runs = [
12928 Run {
12929 run_description: "Start of word matches completion text",
12930 initial_state: "before ediˇ after".into(),
12931 buffer_marked_text: "before <edi|> after".into(),
12932 completion_label: "editor",
12933 completion_text: "editor",
12934 expected_with_insert_mode: "before editorˇ after".into(),
12935 expected_with_replace_mode: "before editorˇ after".into(),
12936 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12937 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12938 },
12939 Run {
12940 run_description: "Accept same text at the middle of the word",
12941 initial_state: "before ediˇtor after".into(),
12942 buffer_marked_text: "before <edi|tor> after".into(),
12943 completion_label: "editor",
12944 completion_text: "editor",
12945 expected_with_insert_mode: "before editorˇtor after".into(),
12946 expected_with_replace_mode: "before editorˇ after".into(),
12947 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12948 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12949 },
12950 Run {
12951 run_description: "End of word matches completion text -- cursor at end",
12952 initial_state: "before torˇ after".into(),
12953 buffer_marked_text: "before <tor|> after".into(),
12954 completion_label: "editor",
12955 completion_text: "editor",
12956 expected_with_insert_mode: "before editorˇ after".into(),
12957 expected_with_replace_mode: "before editorˇ after".into(),
12958 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12959 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12960 },
12961 Run {
12962 run_description: "End of word matches completion text -- cursor at start",
12963 initial_state: "before ˇtor after".into(),
12964 buffer_marked_text: "before <|tor> after".into(),
12965 completion_label: "editor",
12966 completion_text: "editor",
12967 expected_with_insert_mode: "before editorˇtor after".into(),
12968 expected_with_replace_mode: "before editorˇ after".into(),
12969 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12970 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12971 },
12972 Run {
12973 run_description: "Prepend text containing whitespace",
12974 initial_state: "pˇfield: bool".into(),
12975 buffer_marked_text: "<p|field>: bool".into(),
12976 completion_label: "pub ",
12977 completion_text: "pub ",
12978 expected_with_insert_mode: "pub ˇfield: bool".into(),
12979 expected_with_replace_mode: "pub ˇ: bool".into(),
12980 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12981 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12982 },
12983 Run {
12984 run_description: "Add element to start of list",
12985 initial_state: "[element_ˇelement_2]".into(),
12986 buffer_marked_text: "[<element_|element_2>]".into(),
12987 completion_label: "element_1",
12988 completion_text: "element_1",
12989 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
12990 expected_with_replace_mode: "[element_1ˇ]".into(),
12991 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
12992 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
12993 },
12994 Run {
12995 run_description: "Add element to start of list -- first and second elements are equal",
12996 initial_state: "[elˇelement]".into(),
12997 buffer_marked_text: "[<el|element>]".into(),
12998 completion_label: "element",
12999 completion_text: "element",
13000 expected_with_insert_mode: "[elementˇelement]".into(),
13001 expected_with_replace_mode: "[elementˇ]".into(),
13002 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13003 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13004 },
13005 Run {
13006 run_description: "Ends with matching suffix",
13007 initial_state: "SubˇError".into(),
13008 buffer_marked_text: "<Sub|Error>".into(),
13009 completion_label: "SubscriptionError",
13010 completion_text: "SubscriptionError",
13011 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13012 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13013 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13014 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13015 },
13016 Run {
13017 run_description: "Suffix is a subsequence -- contiguous",
13018 initial_state: "SubˇErr".into(),
13019 buffer_marked_text: "<Sub|Err>".into(),
13020 completion_label: "SubscriptionError",
13021 completion_text: "SubscriptionError",
13022 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13023 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13024 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13025 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13026 },
13027 Run {
13028 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13029 initial_state: "Suˇscrirr".into(),
13030 buffer_marked_text: "<Su|scrirr>".into(),
13031 completion_label: "SubscriptionError",
13032 completion_text: "SubscriptionError",
13033 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13034 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13035 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13036 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13037 },
13038 Run {
13039 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13040 initial_state: "foo(indˇix)".into(),
13041 buffer_marked_text: "foo(<ind|ix>)".into(),
13042 completion_label: "node_index",
13043 completion_text: "node_index",
13044 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13045 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13046 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13047 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13048 },
13049 Run {
13050 run_description: "Replace range ends before cursor - should extend to cursor",
13051 initial_state: "before editˇo after".into(),
13052 buffer_marked_text: "before <{ed}>it|o after".into(),
13053 completion_label: "editor",
13054 completion_text: "editor",
13055 expected_with_insert_mode: "before editorˇo after".into(),
13056 expected_with_replace_mode: "before editorˇo after".into(),
13057 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13058 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13059 },
13060 Run {
13061 run_description: "Uses label for suffix matching",
13062 initial_state: "before ediˇtor after".into(),
13063 buffer_marked_text: "before <edi|tor> after".into(),
13064 completion_label: "editor",
13065 completion_text: "editor()",
13066 expected_with_insert_mode: "before editor()ˇtor after".into(),
13067 expected_with_replace_mode: "before editor()ˇ after".into(),
13068 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13069 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13070 },
13071 Run {
13072 run_description: "Case insensitive subsequence and suffix matching",
13073 initial_state: "before EDiˇtoR after".into(),
13074 buffer_marked_text: "before <EDi|toR> after".into(),
13075 completion_label: "editor",
13076 completion_text: "editor",
13077 expected_with_insert_mode: "before editorˇtoR after".into(),
13078 expected_with_replace_mode: "before editorˇ after".into(),
13079 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13080 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13081 },
13082 ];
13083
13084 for run in runs {
13085 let run_variations = [
13086 (LspInsertMode::Insert, run.expected_with_insert_mode),
13087 (LspInsertMode::Replace, run.expected_with_replace_mode),
13088 (
13089 LspInsertMode::ReplaceSubsequence,
13090 run.expected_with_replace_subsequence_mode,
13091 ),
13092 (
13093 LspInsertMode::ReplaceSuffix,
13094 run.expected_with_replace_suffix_mode,
13095 ),
13096 ];
13097
13098 for (lsp_insert_mode, expected_text) in run_variations {
13099 eprintln!(
13100 "run = {:?}, mode = {lsp_insert_mode:.?}",
13101 run.run_description,
13102 );
13103
13104 update_test_language_settings(&mut cx, |settings| {
13105 settings.defaults.completions = Some(CompletionSettings {
13106 lsp_insert_mode,
13107 words: WordsCompletionMode::Disabled,
13108 words_min_length: 0,
13109 lsp: true,
13110 lsp_fetch_timeout_ms: 0,
13111 });
13112 });
13113
13114 cx.set_state(&run.initial_state);
13115 cx.update_editor(|editor, window, cx| {
13116 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13117 });
13118
13119 let counter = Arc::new(AtomicUsize::new(0));
13120 handle_completion_request_with_insert_and_replace(
13121 &mut cx,
13122 &run.buffer_marked_text,
13123 vec![(run.completion_label, run.completion_text)],
13124 counter.clone(),
13125 )
13126 .await;
13127 cx.condition(|editor, _| editor.context_menu_visible())
13128 .await;
13129 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13130
13131 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13132 editor
13133 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13134 .unwrap()
13135 });
13136 cx.assert_editor_state(&expected_text);
13137 handle_resolve_completion_request(&mut cx, None).await;
13138 apply_additional_edits.await.unwrap();
13139 }
13140 }
13141}
13142
13143#[gpui::test]
13144async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13145 init_test(cx, |_| {});
13146 let mut cx = EditorLspTestContext::new_rust(
13147 lsp::ServerCapabilities {
13148 completion_provider: Some(lsp::CompletionOptions {
13149 resolve_provider: Some(true),
13150 ..Default::default()
13151 }),
13152 ..Default::default()
13153 },
13154 cx,
13155 )
13156 .await;
13157
13158 let initial_state = "SubˇError";
13159 let buffer_marked_text = "<Sub|Error>";
13160 let completion_text = "SubscriptionError";
13161 let expected_with_insert_mode = "SubscriptionErrorˇError";
13162 let expected_with_replace_mode = "SubscriptionErrorˇ";
13163
13164 update_test_language_settings(&mut cx, |settings| {
13165 settings.defaults.completions = Some(CompletionSettings {
13166 words: WordsCompletionMode::Disabled,
13167 words_min_length: 0,
13168 // set the opposite here to ensure that the action is overriding the default behavior
13169 lsp_insert_mode: LspInsertMode::Insert,
13170 lsp: true,
13171 lsp_fetch_timeout_ms: 0,
13172 });
13173 });
13174
13175 cx.set_state(initial_state);
13176 cx.update_editor(|editor, window, cx| {
13177 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13178 });
13179
13180 let counter = Arc::new(AtomicUsize::new(0));
13181 handle_completion_request_with_insert_and_replace(
13182 &mut cx,
13183 buffer_marked_text,
13184 vec![(completion_text, completion_text)],
13185 counter.clone(),
13186 )
13187 .await;
13188 cx.condition(|editor, _| editor.context_menu_visible())
13189 .await;
13190 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13191
13192 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13193 editor
13194 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13195 .unwrap()
13196 });
13197 cx.assert_editor_state(expected_with_replace_mode);
13198 handle_resolve_completion_request(&mut cx, None).await;
13199 apply_additional_edits.await.unwrap();
13200
13201 update_test_language_settings(&mut cx, |settings| {
13202 settings.defaults.completions = Some(CompletionSettings {
13203 words: WordsCompletionMode::Disabled,
13204 words_min_length: 0,
13205 // set the opposite here to ensure that the action is overriding the default behavior
13206 lsp_insert_mode: LspInsertMode::Replace,
13207 lsp: true,
13208 lsp_fetch_timeout_ms: 0,
13209 });
13210 });
13211
13212 cx.set_state(initial_state);
13213 cx.update_editor(|editor, window, cx| {
13214 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13215 });
13216 handle_completion_request_with_insert_and_replace(
13217 &mut cx,
13218 buffer_marked_text,
13219 vec![(completion_text, completion_text)],
13220 counter.clone(),
13221 )
13222 .await;
13223 cx.condition(|editor, _| editor.context_menu_visible())
13224 .await;
13225 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13226
13227 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13228 editor
13229 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13230 .unwrap()
13231 });
13232 cx.assert_editor_state(expected_with_insert_mode);
13233 handle_resolve_completion_request(&mut cx, None).await;
13234 apply_additional_edits.await.unwrap();
13235}
13236
13237#[gpui::test]
13238async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13239 init_test(cx, |_| {});
13240 let mut cx = EditorLspTestContext::new_rust(
13241 lsp::ServerCapabilities {
13242 completion_provider: Some(lsp::CompletionOptions {
13243 resolve_provider: Some(true),
13244 ..Default::default()
13245 }),
13246 ..Default::default()
13247 },
13248 cx,
13249 )
13250 .await;
13251
13252 // scenario: surrounding text matches completion text
13253 let completion_text = "to_offset";
13254 let initial_state = indoc! {"
13255 1. buf.to_offˇsuffix
13256 2. buf.to_offˇsuf
13257 3. buf.to_offˇfix
13258 4. buf.to_offˇ
13259 5. into_offˇensive
13260 6. ˇsuffix
13261 7. let ˇ //
13262 8. aaˇzz
13263 9. buf.to_off«zzzzzˇ»suffix
13264 10. buf.«ˇzzzzz»suffix
13265 11. to_off«ˇzzzzz»
13266
13267 buf.to_offˇsuffix // newest cursor
13268 "};
13269 let completion_marked_buffer = indoc! {"
13270 1. buf.to_offsuffix
13271 2. buf.to_offsuf
13272 3. buf.to_offfix
13273 4. buf.to_off
13274 5. into_offensive
13275 6. suffix
13276 7. let //
13277 8. aazz
13278 9. buf.to_offzzzzzsuffix
13279 10. buf.zzzzzsuffix
13280 11. to_offzzzzz
13281
13282 buf.<to_off|suffix> // newest cursor
13283 "};
13284 let expected = indoc! {"
13285 1. buf.to_offsetˇ
13286 2. buf.to_offsetˇsuf
13287 3. buf.to_offsetˇfix
13288 4. buf.to_offsetˇ
13289 5. into_offsetˇensive
13290 6. to_offsetˇsuffix
13291 7. let to_offsetˇ //
13292 8. aato_offsetˇzz
13293 9. buf.to_offsetˇ
13294 10. buf.to_offsetˇsuffix
13295 11. to_offsetˇ
13296
13297 buf.to_offsetˇ // newest cursor
13298 "};
13299 cx.set_state(initial_state);
13300 cx.update_editor(|editor, window, cx| {
13301 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13302 });
13303 handle_completion_request_with_insert_and_replace(
13304 &mut cx,
13305 completion_marked_buffer,
13306 vec![(completion_text, completion_text)],
13307 Arc::new(AtomicUsize::new(0)),
13308 )
13309 .await;
13310 cx.condition(|editor, _| editor.context_menu_visible())
13311 .await;
13312 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13313 editor
13314 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13315 .unwrap()
13316 });
13317 cx.assert_editor_state(expected);
13318 handle_resolve_completion_request(&mut cx, None).await;
13319 apply_additional_edits.await.unwrap();
13320
13321 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13322 let completion_text = "foo_and_bar";
13323 let initial_state = indoc! {"
13324 1. ooanbˇ
13325 2. zooanbˇ
13326 3. ooanbˇz
13327 4. zooanbˇz
13328 5. ooanˇ
13329 6. oanbˇ
13330
13331 ooanbˇ
13332 "};
13333 let completion_marked_buffer = indoc! {"
13334 1. ooanb
13335 2. zooanb
13336 3. ooanbz
13337 4. zooanbz
13338 5. ooan
13339 6. oanb
13340
13341 <ooanb|>
13342 "};
13343 let expected = indoc! {"
13344 1. foo_and_barˇ
13345 2. zfoo_and_barˇ
13346 3. foo_and_barˇz
13347 4. zfoo_and_barˇz
13348 5. ooanfoo_and_barˇ
13349 6. oanbfoo_and_barˇ
13350
13351 foo_and_barˇ
13352 "};
13353 cx.set_state(initial_state);
13354 cx.update_editor(|editor, window, cx| {
13355 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13356 });
13357 handle_completion_request_with_insert_and_replace(
13358 &mut cx,
13359 completion_marked_buffer,
13360 vec![(completion_text, completion_text)],
13361 Arc::new(AtomicUsize::new(0)),
13362 )
13363 .await;
13364 cx.condition(|editor, _| editor.context_menu_visible())
13365 .await;
13366 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13367 editor
13368 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13369 .unwrap()
13370 });
13371 cx.assert_editor_state(expected);
13372 handle_resolve_completion_request(&mut cx, None).await;
13373 apply_additional_edits.await.unwrap();
13374
13375 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13376 // (expects the same as if it was inserted at the end)
13377 let completion_text = "foo_and_bar";
13378 let initial_state = indoc! {"
13379 1. ooˇanb
13380 2. zooˇanb
13381 3. ooˇanbz
13382 4. zooˇanbz
13383
13384 ooˇanb
13385 "};
13386 let completion_marked_buffer = indoc! {"
13387 1. ooanb
13388 2. zooanb
13389 3. ooanbz
13390 4. zooanbz
13391
13392 <oo|anb>
13393 "};
13394 let expected = indoc! {"
13395 1. foo_and_barˇ
13396 2. zfoo_and_barˇ
13397 3. foo_and_barˇz
13398 4. zfoo_and_barˇz
13399
13400 foo_and_barˇ
13401 "};
13402 cx.set_state(initial_state);
13403 cx.update_editor(|editor, window, cx| {
13404 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13405 });
13406 handle_completion_request_with_insert_and_replace(
13407 &mut cx,
13408 completion_marked_buffer,
13409 vec![(completion_text, completion_text)],
13410 Arc::new(AtomicUsize::new(0)),
13411 )
13412 .await;
13413 cx.condition(|editor, _| editor.context_menu_visible())
13414 .await;
13415 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13416 editor
13417 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13418 .unwrap()
13419 });
13420 cx.assert_editor_state(expected);
13421 handle_resolve_completion_request(&mut cx, None).await;
13422 apply_additional_edits.await.unwrap();
13423}
13424
13425// This used to crash
13426#[gpui::test]
13427async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13428 init_test(cx, |_| {});
13429
13430 let buffer_text = indoc! {"
13431 fn main() {
13432 10.satu;
13433
13434 //
13435 // separate cursors so they open in different excerpts (manually reproducible)
13436 //
13437
13438 10.satu20;
13439 }
13440 "};
13441 let multibuffer_text_with_selections = indoc! {"
13442 fn main() {
13443 10.satuˇ;
13444
13445 //
13446
13447 //
13448
13449 10.satuˇ20;
13450 }
13451 "};
13452 let expected_multibuffer = indoc! {"
13453 fn main() {
13454 10.saturating_sub()ˇ;
13455
13456 //
13457
13458 //
13459
13460 10.saturating_sub()ˇ;
13461 }
13462 "};
13463
13464 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13465 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13466
13467 let fs = FakeFs::new(cx.executor());
13468 fs.insert_tree(
13469 path!("/a"),
13470 json!({
13471 "main.rs": buffer_text,
13472 }),
13473 )
13474 .await;
13475
13476 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13477 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13478 language_registry.add(rust_lang());
13479 let mut fake_servers = language_registry.register_fake_lsp(
13480 "Rust",
13481 FakeLspAdapter {
13482 capabilities: lsp::ServerCapabilities {
13483 completion_provider: Some(lsp::CompletionOptions {
13484 resolve_provider: None,
13485 ..lsp::CompletionOptions::default()
13486 }),
13487 ..lsp::ServerCapabilities::default()
13488 },
13489 ..FakeLspAdapter::default()
13490 },
13491 );
13492 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13493 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13494 let buffer = project
13495 .update(cx, |project, cx| {
13496 project.open_local_buffer(path!("/a/main.rs"), cx)
13497 })
13498 .await
13499 .unwrap();
13500
13501 let multi_buffer = cx.new(|cx| {
13502 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13503 multi_buffer.push_excerpts(
13504 buffer.clone(),
13505 [ExcerptRange::new(0..first_excerpt_end)],
13506 cx,
13507 );
13508 multi_buffer.push_excerpts(
13509 buffer.clone(),
13510 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13511 cx,
13512 );
13513 multi_buffer
13514 });
13515
13516 let editor = workspace
13517 .update(cx, |_, window, cx| {
13518 cx.new(|cx| {
13519 Editor::new(
13520 EditorMode::Full {
13521 scale_ui_elements_with_buffer_font_size: false,
13522 show_active_line_background: false,
13523 sized_by_content: false,
13524 },
13525 multi_buffer.clone(),
13526 Some(project.clone()),
13527 window,
13528 cx,
13529 )
13530 })
13531 })
13532 .unwrap();
13533
13534 let pane = workspace
13535 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13536 .unwrap();
13537 pane.update_in(cx, |pane, window, cx| {
13538 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13539 });
13540
13541 let fake_server = fake_servers.next().await.unwrap();
13542
13543 editor.update_in(cx, |editor, window, cx| {
13544 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13545 s.select_ranges([
13546 Point::new(1, 11)..Point::new(1, 11),
13547 Point::new(7, 11)..Point::new(7, 11),
13548 ])
13549 });
13550
13551 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13552 });
13553
13554 editor.update_in(cx, |editor, window, cx| {
13555 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13556 });
13557
13558 fake_server
13559 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13560 let completion_item = lsp::CompletionItem {
13561 label: "saturating_sub()".into(),
13562 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13563 lsp::InsertReplaceEdit {
13564 new_text: "saturating_sub()".to_owned(),
13565 insert: lsp::Range::new(
13566 lsp::Position::new(7, 7),
13567 lsp::Position::new(7, 11),
13568 ),
13569 replace: lsp::Range::new(
13570 lsp::Position::new(7, 7),
13571 lsp::Position::new(7, 13),
13572 ),
13573 },
13574 )),
13575 ..lsp::CompletionItem::default()
13576 };
13577
13578 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13579 })
13580 .next()
13581 .await
13582 .unwrap();
13583
13584 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13585 .await;
13586
13587 editor
13588 .update_in(cx, |editor, window, cx| {
13589 editor
13590 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13591 .unwrap()
13592 })
13593 .await
13594 .unwrap();
13595
13596 editor.update(cx, |editor, cx| {
13597 assert_text_with_selections(editor, expected_multibuffer, cx);
13598 })
13599}
13600
13601#[gpui::test]
13602async fn test_completion(cx: &mut TestAppContext) {
13603 init_test(cx, |_| {});
13604
13605 let mut cx = EditorLspTestContext::new_rust(
13606 lsp::ServerCapabilities {
13607 completion_provider: Some(lsp::CompletionOptions {
13608 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13609 resolve_provider: Some(true),
13610 ..Default::default()
13611 }),
13612 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13613 ..Default::default()
13614 },
13615 cx,
13616 )
13617 .await;
13618 let counter = Arc::new(AtomicUsize::new(0));
13619
13620 cx.set_state(indoc! {"
13621 oneˇ
13622 two
13623 three
13624 "});
13625 cx.simulate_keystroke(".");
13626 handle_completion_request(
13627 indoc! {"
13628 one.|<>
13629 two
13630 three
13631 "},
13632 vec!["first_completion", "second_completion"],
13633 true,
13634 counter.clone(),
13635 &mut cx,
13636 )
13637 .await;
13638 cx.condition(|editor, _| editor.context_menu_visible())
13639 .await;
13640 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13641
13642 let _handler = handle_signature_help_request(
13643 &mut cx,
13644 lsp::SignatureHelp {
13645 signatures: vec![lsp::SignatureInformation {
13646 label: "test signature".to_string(),
13647 documentation: None,
13648 parameters: Some(vec![lsp::ParameterInformation {
13649 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13650 documentation: None,
13651 }]),
13652 active_parameter: None,
13653 }],
13654 active_signature: None,
13655 active_parameter: None,
13656 },
13657 );
13658 cx.update_editor(|editor, window, cx| {
13659 assert!(
13660 !editor.signature_help_state.is_shown(),
13661 "No signature help was called for"
13662 );
13663 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13664 });
13665 cx.run_until_parked();
13666 cx.update_editor(|editor, _, _| {
13667 assert!(
13668 !editor.signature_help_state.is_shown(),
13669 "No signature help should be shown when completions menu is open"
13670 );
13671 });
13672
13673 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13674 editor.context_menu_next(&Default::default(), window, cx);
13675 editor
13676 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13677 .unwrap()
13678 });
13679 cx.assert_editor_state(indoc! {"
13680 one.second_completionˇ
13681 two
13682 three
13683 "});
13684
13685 handle_resolve_completion_request(
13686 &mut cx,
13687 Some(vec![
13688 (
13689 //This overlaps with the primary completion edit which is
13690 //misbehavior from the LSP spec, test that we filter it out
13691 indoc! {"
13692 one.second_ˇcompletion
13693 two
13694 threeˇ
13695 "},
13696 "overlapping additional edit",
13697 ),
13698 (
13699 indoc! {"
13700 one.second_completion
13701 two
13702 threeˇ
13703 "},
13704 "\nadditional edit",
13705 ),
13706 ]),
13707 )
13708 .await;
13709 apply_additional_edits.await.unwrap();
13710 cx.assert_editor_state(indoc! {"
13711 one.second_completionˇ
13712 two
13713 three
13714 additional edit
13715 "});
13716
13717 cx.set_state(indoc! {"
13718 one.second_completion
13719 twoˇ
13720 threeˇ
13721 additional edit
13722 "});
13723 cx.simulate_keystroke(" ");
13724 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13725 cx.simulate_keystroke("s");
13726 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13727
13728 cx.assert_editor_state(indoc! {"
13729 one.second_completion
13730 two sˇ
13731 three sˇ
13732 additional edit
13733 "});
13734 handle_completion_request(
13735 indoc! {"
13736 one.second_completion
13737 two s
13738 three <s|>
13739 additional edit
13740 "},
13741 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13742 true,
13743 counter.clone(),
13744 &mut cx,
13745 )
13746 .await;
13747 cx.condition(|editor, _| editor.context_menu_visible())
13748 .await;
13749 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13750
13751 cx.simulate_keystroke("i");
13752
13753 handle_completion_request(
13754 indoc! {"
13755 one.second_completion
13756 two si
13757 three <si|>
13758 additional edit
13759 "},
13760 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13761 true,
13762 counter.clone(),
13763 &mut cx,
13764 )
13765 .await;
13766 cx.condition(|editor, _| editor.context_menu_visible())
13767 .await;
13768 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13769
13770 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13771 editor
13772 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13773 .unwrap()
13774 });
13775 cx.assert_editor_state(indoc! {"
13776 one.second_completion
13777 two sixth_completionˇ
13778 three sixth_completionˇ
13779 additional edit
13780 "});
13781
13782 apply_additional_edits.await.unwrap();
13783
13784 update_test_language_settings(&mut cx, |settings| {
13785 settings.defaults.show_completions_on_input = Some(false);
13786 });
13787 cx.set_state("editorˇ");
13788 cx.simulate_keystroke(".");
13789 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13790 cx.simulate_keystrokes("c l o");
13791 cx.assert_editor_state("editor.cloˇ");
13792 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13793 cx.update_editor(|editor, window, cx| {
13794 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13795 });
13796 handle_completion_request(
13797 "editor.<clo|>",
13798 vec!["close", "clobber"],
13799 true,
13800 counter.clone(),
13801 &mut cx,
13802 )
13803 .await;
13804 cx.condition(|editor, _| editor.context_menu_visible())
13805 .await;
13806 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
13807
13808 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13809 editor
13810 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13811 .unwrap()
13812 });
13813 cx.assert_editor_state("editor.clobberˇ");
13814 handle_resolve_completion_request(&mut cx, None).await;
13815 apply_additional_edits.await.unwrap();
13816}
13817
13818#[gpui::test]
13819async fn test_completion_reuse(cx: &mut TestAppContext) {
13820 init_test(cx, |_| {});
13821
13822 let mut cx = EditorLspTestContext::new_rust(
13823 lsp::ServerCapabilities {
13824 completion_provider: Some(lsp::CompletionOptions {
13825 trigger_characters: Some(vec![".".to_string()]),
13826 ..Default::default()
13827 }),
13828 ..Default::default()
13829 },
13830 cx,
13831 )
13832 .await;
13833
13834 let counter = Arc::new(AtomicUsize::new(0));
13835 cx.set_state("objˇ");
13836 cx.simulate_keystroke(".");
13837
13838 // Initial completion request returns complete results
13839 let is_incomplete = false;
13840 handle_completion_request(
13841 "obj.|<>",
13842 vec!["a", "ab", "abc"],
13843 is_incomplete,
13844 counter.clone(),
13845 &mut cx,
13846 )
13847 .await;
13848 cx.run_until_parked();
13849 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13850 cx.assert_editor_state("obj.ˇ");
13851 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13852
13853 // Type "a" - filters existing completions
13854 cx.simulate_keystroke("a");
13855 cx.run_until_parked();
13856 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13857 cx.assert_editor_state("obj.aˇ");
13858 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13859
13860 // Type "b" - filters existing completions
13861 cx.simulate_keystroke("b");
13862 cx.run_until_parked();
13863 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13864 cx.assert_editor_state("obj.abˇ");
13865 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13866
13867 // Type "c" - filters existing completions
13868 cx.simulate_keystroke("c");
13869 cx.run_until_parked();
13870 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13871 cx.assert_editor_state("obj.abcˇ");
13872 check_displayed_completions(vec!["abc"], &mut cx);
13873
13874 // Backspace to delete "c" - filters existing completions
13875 cx.update_editor(|editor, window, cx| {
13876 editor.backspace(&Backspace, window, cx);
13877 });
13878 cx.run_until_parked();
13879 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13880 cx.assert_editor_state("obj.abˇ");
13881 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13882
13883 // Moving cursor to the left dismisses menu.
13884 cx.update_editor(|editor, window, cx| {
13885 editor.move_left(&MoveLeft, window, cx);
13886 });
13887 cx.run_until_parked();
13888 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13889 cx.assert_editor_state("obj.aˇb");
13890 cx.update_editor(|editor, _, _| {
13891 assert_eq!(editor.context_menu_visible(), false);
13892 });
13893
13894 // Type "b" - new request
13895 cx.simulate_keystroke("b");
13896 let is_incomplete = false;
13897 handle_completion_request(
13898 "obj.<ab|>a",
13899 vec!["ab", "abc"],
13900 is_incomplete,
13901 counter.clone(),
13902 &mut cx,
13903 )
13904 .await;
13905 cx.run_until_parked();
13906 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13907 cx.assert_editor_state("obj.abˇb");
13908 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13909
13910 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13911 cx.update_editor(|editor, window, cx| {
13912 editor.backspace(&Backspace, window, cx);
13913 });
13914 let is_incomplete = false;
13915 handle_completion_request(
13916 "obj.<a|>b",
13917 vec!["a", "ab", "abc"],
13918 is_incomplete,
13919 counter.clone(),
13920 &mut cx,
13921 )
13922 .await;
13923 cx.run_until_parked();
13924 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13925 cx.assert_editor_state("obj.aˇb");
13926 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13927
13928 // Backspace to delete "a" - dismisses menu.
13929 cx.update_editor(|editor, window, cx| {
13930 editor.backspace(&Backspace, window, cx);
13931 });
13932 cx.run_until_parked();
13933 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13934 cx.assert_editor_state("obj.ˇb");
13935 cx.update_editor(|editor, _, _| {
13936 assert_eq!(editor.context_menu_visible(), false);
13937 });
13938}
13939
13940#[gpui::test]
13941async fn test_word_completion(cx: &mut TestAppContext) {
13942 let lsp_fetch_timeout_ms = 10;
13943 init_test(cx, |language_settings| {
13944 language_settings.defaults.completions = Some(CompletionSettings {
13945 words: WordsCompletionMode::Fallback,
13946 words_min_length: 0,
13947 lsp: true,
13948 lsp_fetch_timeout_ms: 10,
13949 lsp_insert_mode: LspInsertMode::Insert,
13950 });
13951 });
13952
13953 let mut cx = EditorLspTestContext::new_rust(
13954 lsp::ServerCapabilities {
13955 completion_provider: Some(lsp::CompletionOptions {
13956 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13957 ..lsp::CompletionOptions::default()
13958 }),
13959 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13960 ..lsp::ServerCapabilities::default()
13961 },
13962 cx,
13963 )
13964 .await;
13965
13966 let throttle_completions = Arc::new(AtomicBool::new(false));
13967
13968 let lsp_throttle_completions = throttle_completions.clone();
13969 let _completion_requests_handler =
13970 cx.lsp
13971 .server
13972 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13973 let lsp_throttle_completions = lsp_throttle_completions.clone();
13974 let cx = cx.clone();
13975 async move {
13976 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13977 cx.background_executor()
13978 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13979 .await;
13980 }
13981 Ok(Some(lsp::CompletionResponse::Array(vec![
13982 lsp::CompletionItem {
13983 label: "first".into(),
13984 ..lsp::CompletionItem::default()
13985 },
13986 lsp::CompletionItem {
13987 label: "last".into(),
13988 ..lsp::CompletionItem::default()
13989 },
13990 ])))
13991 }
13992 });
13993
13994 cx.set_state(indoc! {"
13995 oneˇ
13996 two
13997 three
13998 "});
13999 cx.simulate_keystroke(".");
14000 cx.executor().run_until_parked();
14001 cx.condition(|editor, _| editor.context_menu_visible())
14002 .await;
14003 cx.update_editor(|editor, window, cx| {
14004 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14005 {
14006 assert_eq!(
14007 completion_menu_entries(menu),
14008 &["first", "last"],
14009 "When LSP server is fast to reply, no fallback word completions are used"
14010 );
14011 } else {
14012 panic!("expected completion menu to be open");
14013 }
14014 editor.cancel(&Cancel, window, cx);
14015 });
14016 cx.executor().run_until_parked();
14017 cx.condition(|editor, _| !editor.context_menu_visible())
14018 .await;
14019
14020 throttle_completions.store(true, atomic::Ordering::Release);
14021 cx.simulate_keystroke(".");
14022 cx.executor()
14023 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14024 cx.executor().run_until_parked();
14025 cx.condition(|editor, _| editor.context_menu_visible())
14026 .await;
14027 cx.update_editor(|editor, _, _| {
14028 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14029 {
14030 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14031 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14032 } else {
14033 panic!("expected completion menu to be open");
14034 }
14035 });
14036}
14037
14038#[gpui::test]
14039async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14040 init_test(cx, |language_settings| {
14041 language_settings.defaults.completions = Some(CompletionSettings {
14042 words: WordsCompletionMode::Enabled,
14043 words_min_length: 0,
14044 lsp: true,
14045 lsp_fetch_timeout_ms: 0,
14046 lsp_insert_mode: LspInsertMode::Insert,
14047 });
14048 });
14049
14050 let mut cx = EditorLspTestContext::new_rust(
14051 lsp::ServerCapabilities {
14052 completion_provider: Some(lsp::CompletionOptions {
14053 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14054 ..lsp::CompletionOptions::default()
14055 }),
14056 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14057 ..lsp::ServerCapabilities::default()
14058 },
14059 cx,
14060 )
14061 .await;
14062
14063 let _completion_requests_handler =
14064 cx.lsp
14065 .server
14066 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14067 Ok(Some(lsp::CompletionResponse::Array(vec![
14068 lsp::CompletionItem {
14069 label: "first".into(),
14070 ..lsp::CompletionItem::default()
14071 },
14072 lsp::CompletionItem {
14073 label: "last".into(),
14074 ..lsp::CompletionItem::default()
14075 },
14076 ])))
14077 });
14078
14079 cx.set_state(indoc! {"ˇ
14080 first
14081 last
14082 second
14083 "});
14084 cx.simulate_keystroke(".");
14085 cx.executor().run_until_parked();
14086 cx.condition(|editor, _| editor.context_menu_visible())
14087 .await;
14088 cx.update_editor(|editor, _, _| {
14089 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14090 {
14091 assert_eq!(
14092 completion_menu_entries(menu),
14093 &["first", "last", "second"],
14094 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14095 );
14096 } else {
14097 panic!("expected completion menu to be open");
14098 }
14099 });
14100}
14101
14102#[gpui::test]
14103async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14104 init_test(cx, |language_settings| {
14105 language_settings.defaults.completions = Some(CompletionSettings {
14106 words: WordsCompletionMode::Disabled,
14107 words_min_length: 0,
14108 lsp: true,
14109 lsp_fetch_timeout_ms: 0,
14110 lsp_insert_mode: LspInsertMode::Insert,
14111 });
14112 });
14113
14114 let mut cx = EditorLspTestContext::new_rust(
14115 lsp::ServerCapabilities {
14116 completion_provider: Some(lsp::CompletionOptions {
14117 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14118 ..lsp::CompletionOptions::default()
14119 }),
14120 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14121 ..lsp::ServerCapabilities::default()
14122 },
14123 cx,
14124 )
14125 .await;
14126
14127 let _completion_requests_handler =
14128 cx.lsp
14129 .server
14130 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14131 panic!("LSP completions should not be queried when dealing with word completions")
14132 });
14133
14134 cx.set_state(indoc! {"ˇ
14135 first
14136 last
14137 second
14138 "});
14139 cx.update_editor(|editor, window, cx| {
14140 editor.show_word_completions(&ShowWordCompletions, window, cx);
14141 });
14142 cx.executor().run_until_parked();
14143 cx.condition(|editor, _| editor.context_menu_visible())
14144 .await;
14145 cx.update_editor(|editor, _, _| {
14146 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14147 {
14148 assert_eq!(
14149 completion_menu_entries(menu),
14150 &["first", "last", "second"],
14151 "`ShowWordCompletions` action should show word completions"
14152 );
14153 } else {
14154 panic!("expected completion menu to be open");
14155 }
14156 });
14157
14158 cx.simulate_keystroke("l");
14159 cx.executor().run_until_parked();
14160 cx.condition(|editor, _| editor.context_menu_visible())
14161 .await;
14162 cx.update_editor(|editor, _, _| {
14163 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14164 {
14165 assert_eq!(
14166 completion_menu_entries(menu),
14167 &["last"],
14168 "After showing word completions, further editing should filter them and not query the LSP"
14169 );
14170 } else {
14171 panic!("expected completion menu to be open");
14172 }
14173 });
14174}
14175
14176#[gpui::test]
14177async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14178 init_test(cx, |language_settings| {
14179 language_settings.defaults.completions = Some(CompletionSettings {
14180 words: WordsCompletionMode::Fallback,
14181 words_min_length: 0,
14182 lsp: false,
14183 lsp_fetch_timeout_ms: 0,
14184 lsp_insert_mode: LspInsertMode::Insert,
14185 });
14186 });
14187
14188 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14189
14190 cx.set_state(indoc! {"ˇ
14191 0_usize
14192 let
14193 33
14194 4.5f32
14195 "});
14196 cx.update_editor(|editor, window, cx| {
14197 editor.show_completions(&ShowCompletions::default(), window, cx);
14198 });
14199 cx.executor().run_until_parked();
14200 cx.condition(|editor, _| editor.context_menu_visible())
14201 .await;
14202 cx.update_editor(|editor, window, cx| {
14203 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14204 {
14205 assert_eq!(
14206 completion_menu_entries(menu),
14207 &["let"],
14208 "With no digits in the completion query, no digits should be in the word completions"
14209 );
14210 } else {
14211 panic!("expected completion menu to be open");
14212 }
14213 editor.cancel(&Cancel, window, cx);
14214 });
14215
14216 cx.set_state(indoc! {"3ˇ
14217 0_usize
14218 let
14219 3
14220 33.35f32
14221 "});
14222 cx.update_editor(|editor, window, cx| {
14223 editor.show_completions(&ShowCompletions::default(), window, cx);
14224 });
14225 cx.executor().run_until_parked();
14226 cx.condition(|editor, _| editor.context_menu_visible())
14227 .await;
14228 cx.update_editor(|editor, _, _| {
14229 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14230 {
14231 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14232 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14233 } else {
14234 panic!("expected completion menu to be open");
14235 }
14236 });
14237}
14238
14239#[gpui::test]
14240async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14241 init_test(cx, |language_settings| {
14242 language_settings.defaults.completions = Some(CompletionSettings {
14243 words: WordsCompletionMode::Enabled,
14244 words_min_length: 3,
14245 lsp: true,
14246 lsp_fetch_timeout_ms: 0,
14247 lsp_insert_mode: LspInsertMode::Insert,
14248 });
14249 });
14250
14251 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14252 cx.set_state(indoc! {"ˇ
14253 wow
14254 wowen
14255 wowser
14256 "});
14257 cx.simulate_keystroke("w");
14258 cx.executor().run_until_parked();
14259 cx.update_editor(|editor, _, _| {
14260 if editor.context_menu.borrow_mut().is_some() {
14261 panic!(
14262 "expected completion menu to be hidden, as words completion threshold is not met"
14263 );
14264 }
14265 });
14266
14267 cx.simulate_keystroke("o");
14268 cx.executor().run_until_parked();
14269 cx.update_editor(|editor, _, _| {
14270 if editor.context_menu.borrow_mut().is_some() {
14271 panic!(
14272 "expected completion menu to be hidden, as words completion threshold is not met still"
14273 );
14274 }
14275 });
14276
14277 cx.simulate_keystroke("w");
14278 cx.executor().run_until_parked();
14279 cx.update_editor(|editor, _, _| {
14280 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14281 {
14282 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14283 } else {
14284 panic!("expected completion menu to be open after the word completions threshold is met");
14285 }
14286 });
14287}
14288
14289fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14290 let position = || lsp::Position {
14291 line: params.text_document_position.position.line,
14292 character: params.text_document_position.position.character,
14293 };
14294 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14295 range: lsp::Range {
14296 start: position(),
14297 end: position(),
14298 },
14299 new_text: text.to_string(),
14300 }))
14301}
14302
14303#[gpui::test]
14304async fn test_multiline_completion(cx: &mut TestAppContext) {
14305 init_test(cx, |_| {});
14306
14307 let fs = FakeFs::new(cx.executor());
14308 fs.insert_tree(
14309 path!("/a"),
14310 json!({
14311 "main.ts": "a",
14312 }),
14313 )
14314 .await;
14315
14316 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14317 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14318 let typescript_language = Arc::new(Language::new(
14319 LanguageConfig {
14320 name: "TypeScript".into(),
14321 matcher: LanguageMatcher {
14322 path_suffixes: vec!["ts".to_string()],
14323 ..LanguageMatcher::default()
14324 },
14325 line_comments: vec!["// ".into()],
14326 ..LanguageConfig::default()
14327 },
14328 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14329 ));
14330 language_registry.add(typescript_language.clone());
14331 let mut fake_servers = language_registry.register_fake_lsp(
14332 "TypeScript",
14333 FakeLspAdapter {
14334 capabilities: lsp::ServerCapabilities {
14335 completion_provider: Some(lsp::CompletionOptions {
14336 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14337 ..lsp::CompletionOptions::default()
14338 }),
14339 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14340 ..lsp::ServerCapabilities::default()
14341 },
14342 // Emulate vtsls label generation
14343 label_for_completion: Some(Box::new(|item, _| {
14344 let text = if let Some(description) = item
14345 .label_details
14346 .as_ref()
14347 .and_then(|label_details| label_details.description.as_ref())
14348 {
14349 format!("{} {}", item.label, description)
14350 } else if let Some(detail) = &item.detail {
14351 format!("{} {}", item.label, detail)
14352 } else {
14353 item.label.clone()
14354 };
14355 let len = text.len();
14356 Some(language::CodeLabel {
14357 text,
14358 runs: Vec::new(),
14359 filter_range: 0..len,
14360 })
14361 })),
14362 ..FakeLspAdapter::default()
14363 },
14364 );
14365 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14366 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14367 let worktree_id = workspace
14368 .update(cx, |workspace, _window, cx| {
14369 workspace.project().update(cx, |project, cx| {
14370 project.worktrees(cx).next().unwrap().read(cx).id()
14371 })
14372 })
14373 .unwrap();
14374 let _buffer = project
14375 .update(cx, |project, cx| {
14376 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14377 })
14378 .await
14379 .unwrap();
14380 let editor = workspace
14381 .update(cx, |workspace, window, cx| {
14382 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14383 })
14384 .unwrap()
14385 .await
14386 .unwrap()
14387 .downcast::<Editor>()
14388 .unwrap();
14389 let fake_server = fake_servers.next().await.unwrap();
14390
14391 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14392 let multiline_label_2 = "a\nb\nc\n";
14393 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14394 let multiline_description = "d\ne\nf\n";
14395 let multiline_detail_2 = "g\nh\ni\n";
14396
14397 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14398 move |params, _| async move {
14399 Ok(Some(lsp::CompletionResponse::Array(vec![
14400 lsp::CompletionItem {
14401 label: multiline_label.to_string(),
14402 text_edit: gen_text_edit(¶ms, "new_text_1"),
14403 ..lsp::CompletionItem::default()
14404 },
14405 lsp::CompletionItem {
14406 label: "single line label 1".to_string(),
14407 detail: Some(multiline_detail.to_string()),
14408 text_edit: gen_text_edit(¶ms, "new_text_2"),
14409 ..lsp::CompletionItem::default()
14410 },
14411 lsp::CompletionItem {
14412 label: "single line label 2".to_string(),
14413 label_details: Some(lsp::CompletionItemLabelDetails {
14414 description: Some(multiline_description.to_string()),
14415 detail: None,
14416 }),
14417 text_edit: gen_text_edit(¶ms, "new_text_2"),
14418 ..lsp::CompletionItem::default()
14419 },
14420 lsp::CompletionItem {
14421 label: multiline_label_2.to_string(),
14422 detail: Some(multiline_detail_2.to_string()),
14423 text_edit: gen_text_edit(¶ms, "new_text_3"),
14424 ..lsp::CompletionItem::default()
14425 },
14426 lsp::CompletionItem {
14427 label: "Label with many spaces and \t but without newlines".to_string(),
14428 detail: Some(
14429 "Details with many spaces and \t but without newlines".to_string(),
14430 ),
14431 text_edit: gen_text_edit(¶ms, "new_text_4"),
14432 ..lsp::CompletionItem::default()
14433 },
14434 ])))
14435 },
14436 );
14437
14438 editor.update_in(cx, |editor, window, cx| {
14439 cx.focus_self(window);
14440 editor.move_to_end(&MoveToEnd, window, cx);
14441 editor.handle_input(".", window, cx);
14442 });
14443 cx.run_until_parked();
14444 completion_handle.next().await.unwrap();
14445
14446 editor.update(cx, |editor, _| {
14447 assert!(editor.context_menu_visible());
14448 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14449 {
14450 let completion_labels = menu
14451 .completions
14452 .borrow()
14453 .iter()
14454 .map(|c| c.label.text.clone())
14455 .collect::<Vec<_>>();
14456 assert_eq!(
14457 completion_labels,
14458 &[
14459 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14460 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14461 "single line label 2 d e f ",
14462 "a b c g h i ",
14463 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14464 ],
14465 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14466 );
14467
14468 for completion in menu
14469 .completions
14470 .borrow()
14471 .iter() {
14472 assert_eq!(
14473 completion.label.filter_range,
14474 0..completion.label.text.len(),
14475 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14476 );
14477 }
14478 } else {
14479 panic!("expected completion menu to be open");
14480 }
14481 });
14482}
14483
14484#[gpui::test]
14485async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14486 init_test(cx, |_| {});
14487 let mut cx = EditorLspTestContext::new_rust(
14488 lsp::ServerCapabilities {
14489 completion_provider: Some(lsp::CompletionOptions {
14490 trigger_characters: Some(vec![".".to_string()]),
14491 ..Default::default()
14492 }),
14493 ..Default::default()
14494 },
14495 cx,
14496 )
14497 .await;
14498 cx.lsp
14499 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14500 Ok(Some(lsp::CompletionResponse::Array(vec![
14501 lsp::CompletionItem {
14502 label: "first".into(),
14503 ..Default::default()
14504 },
14505 lsp::CompletionItem {
14506 label: "last".into(),
14507 ..Default::default()
14508 },
14509 ])))
14510 });
14511 cx.set_state("variableˇ");
14512 cx.simulate_keystroke(".");
14513 cx.executor().run_until_parked();
14514
14515 cx.update_editor(|editor, _, _| {
14516 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14517 {
14518 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14519 } else {
14520 panic!("expected completion menu to be open");
14521 }
14522 });
14523
14524 cx.update_editor(|editor, window, cx| {
14525 editor.move_page_down(&MovePageDown::default(), window, cx);
14526 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14527 {
14528 assert!(
14529 menu.selected_item == 1,
14530 "expected PageDown to select the last item from the context menu"
14531 );
14532 } else {
14533 panic!("expected completion menu to stay open after PageDown");
14534 }
14535 });
14536
14537 cx.update_editor(|editor, window, cx| {
14538 editor.move_page_up(&MovePageUp::default(), window, cx);
14539 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14540 {
14541 assert!(
14542 menu.selected_item == 0,
14543 "expected PageUp to select the first item from the context menu"
14544 );
14545 } else {
14546 panic!("expected completion menu to stay open after PageUp");
14547 }
14548 });
14549}
14550
14551#[gpui::test]
14552async fn test_as_is_completions(cx: &mut TestAppContext) {
14553 init_test(cx, |_| {});
14554 let mut cx = EditorLspTestContext::new_rust(
14555 lsp::ServerCapabilities {
14556 completion_provider: Some(lsp::CompletionOptions {
14557 ..Default::default()
14558 }),
14559 ..Default::default()
14560 },
14561 cx,
14562 )
14563 .await;
14564 cx.lsp
14565 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14566 Ok(Some(lsp::CompletionResponse::Array(vec![
14567 lsp::CompletionItem {
14568 label: "unsafe".into(),
14569 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14570 range: lsp::Range {
14571 start: lsp::Position {
14572 line: 1,
14573 character: 2,
14574 },
14575 end: lsp::Position {
14576 line: 1,
14577 character: 3,
14578 },
14579 },
14580 new_text: "unsafe".to_string(),
14581 })),
14582 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14583 ..Default::default()
14584 },
14585 ])))
14586 });
14587 cx.set_state("fn a() {}\n nˇ");
14588 cx.executor().run_until_parked();
14589 cx.update_editor(|editor, window, cx| {
14590 editor.show_completions(
14591 &ShowCompletions {
14592 trigger: Some("\n".into()),
14593 },
14594 window,
14595 cx,
14596 );
14597 });
14598 cx.executor().run_until_parked();
14599
14600 cx.update_editor(|editor, window, cx| {
14601 editor.confirm_completion(&Default::default(), window, cx)
14602 });
14603 cx.executor().run_until_parked();
14604 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14605}
14606
14607#[gpui::test]
14608async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14609 init_test(cx, |_| {});
14610 let language =
14611 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14612 let mut cx = EditorLspTestContext::new(
14613 language,
14614 lsp::ServerCapabilities {
14615 completion_provider: Some(lsp::CompletionOptions {
14616 ..lsp::CompletionOptions::default()
14617 }),
14618 ..lsp::ServerCapabilities::default()
14619 },
14620 cx,
14621 )
14622 .await;
14623
14624 cx.set_state(
14625 "#ifndef BAR_H
14626#define BAR_H
14627
14628#include <stdbool.h>
14629
14630int fn_branch(bool do_branch1, bool do_branch2);
14631
14632#endif // BAR_H
14633ˇ",
14634 );
14635 cx.executor().run_until_parked();
14636 cx.update_editor(|editor, window, cx| {
14637 editor.handle_input("#", window, cx);
14638 });
14639 cx.executor().run_until_parked();
14640 cx.update_editor(|editor, window, cx| {
14641 editor.handle_input("i", window, cx);
14642 });
14643 cx.executor().run_until_parked();
14644 cx.update_editor(|editor, window, cx| {
14645 editor.handle_input("n", window, cx);
14646 });
14647 cx.executor().run_until_parked();
14648 cx.assert_editor_state(
14649 "#ifndef BAR_H
14650#define BAR_H
14651
14652#include <stdbool.h>
14653
14654int fn_branch(bool do_branch1, bool do_branch2);
14655
14656#endif // BAR_H
14657#inˇ",
14658 );
14659
14660 cx.lsp
14661 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14662 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14663 is_incomplete: false,
14664 item_defaults: None,
14665 items: vec![lsp::CompletionItem {
14666 kind: Some(lsp::CompletionItemKind::SNIPPET),
14667 label_details: Some(lsp::CompletionItemLabelDetails {
14668 detail: Some("header".to_string()),
14669 description: None,
14670 }),
14671 label: " include".to_string(),
14672 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14673 range: lsp::Range {
14674 start: lsp::Position {
14675 line: 8,
14676 character: 1,
14677 },
14678 end: lsp::Position {
14679 line: 8,
14680 character: 1,
14681 },
14682 },
14683 new_text: "include \"$0\"".to_string(),
14684 })),
14685 sort_text: Some("40b67681include".to_string()),
14686 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14687 filter_text: Some("include".to_string()),
14688 insert_text: Some("include \"$0\"".to_string()),
14689 ..lsp::CompletionItem::default()
14690 }],
14691 })))
14692 });
14693 cx.update_editor(|editor, window, cx| {
14694 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14695 });
14696 cx.executor().run_until_parked();
14697 cx.update_editor(|editor, window, cx| {
14698 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14699 });
14700 cx.executor().run_until_parked();
14701 cx.assert_editor_state(
14702 "#ifndef BAR_H
14703#define BAR_H
14704
14705#include <stdbool.h>
14706
14707int fn_branch(bool do_branch1, bool do_branch2);
14708
14709#endif // BAR_H
14710#include \"ˇ\"",
14711 );
14712
14713 cx.lsp
14714 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14715 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14716 is_incomplete: true,
14717 item_defaults: None,
14718 items: vec![lsp::CompletionItem {
14719 kind: Some(lsp::CompletionItemKind::FILE),
14720 label: "AGL/".to_string(),
14721 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14722 range: lsp::Range {
14723 start: lsp::Position {
14724 line: 8,
14725 character: 10,
14726 },
14727 end: lsp::Position {
14728 line: 8,
14729 character: 11,
14730 },
14731 },
14732 new_text: "AGL/".to_string(),
14733 })),
14734 sort_text: Some("40b67681AGL/".to_string()),
14735 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14736 filter_text: Some("AGL/".to_string()),
14737 insert_text: Some("AGL/".to_string()),
14738 ..lsp::CompletionItem::default()
14739 }],
14740 })))
14741 });
14742 cx.update_editor(|editor, window, cx| {
14743 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14744 });
14745 cx.executor().run_until_parked();
14746 cx.update_editor(|editor, window, cx| {
14747 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14748 });
14749 cx.executor().run_until_parked();
14750 cx.assert_editor_state(
14751 r##"#ifndef BAR_H
14752#define BAR_H
14753
14754#include <stdbool.h>
14755
14756int fn_branch(bool do_branch1, bool do_branch2);
14757
14758#endif // BAR_H
14759#include "AGL/ˇ"##,
14760 );
14761
14762 cx.update_editor(|editor, window, cx| {
14763 editor.handle_input("\"", window, cx);
14764 });
14765 cx.executor().run_until_parked();
14766 cx.assert_editor_state(
14767 r##"#ifndef BAR_H
14768#define BAR_H
14769
14770#include <stdbool.h>
14771
14772int fn_branch(bool do_branch1, bool do_branch2);
14773
14774#endif // BAR_H
14775#include "AGL/"ˇ"##,
14776 );
14777}
14778
14779#[gpui::test]
14780async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
14781 init_test(cx, |_| {});
14782
14783 let mut cx = EditorLspTestContext::new_rust(
14784 lsp::ServerCapabilities {
14785 completion_provider: Some(lsp::CompletionOptions {
14786 trigger_characters: Some(vec![".".to_string()]),
14787 resolve_provider: Some(true),
14788 ..Default::default()
14789 }),
14790 ..Default::default()
14791 },
14792 cx,
14793 )
14794 .await;
14795
14796 cx.set_state("fn main() { let a = 2ˇ; }");
14797 cx.simulate_keystroke(".");
14798 let completion_item = lsp::CompletionItem {
14799 label: "Some".into(),
14800 kind: Some(lsp::CompletionItemKind::SNIPPET),
14801 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14802 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14803 kind: lsp::MarkupKind::Markdown,
14804 value: "```rust\nSome(2)\n```".to_string(),
14805 })),
14806 deprecated: Some(false),
14807 sort_text: Some("Some".to_string()),
14808 filter_text: Some("Some".to_string()),
14809 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14810 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14811 range: lsp::Range {
14812 start: lsp::Position {
14813 line: 0,
14814 character: 22,
14815 },
14816 end: lsp::Position {
14817 line: 0,
14818 character: 22,
14819 },
14820 },
14821 new_text: "Some(2)".to_string(),
14822 })),
14823 additional_text_edits: Some(vec![lsp::TextEdit {
14824 range: lsp::Range {
14825 start: lsp::Position {
14826 line: 0,
14827 character: 20,
14828 },
14829 end: lsp::Position {
14830 line: 0,
14831 character: 22,
14832 },
14833 },
14834 new_text: "".to_string(),
14835 }]),
14836 ..Default::default()
14837 };
14838
14839 let closure_completion_item = completion_item.clone();
14840 let counter = Arc::new(AtomicUsize::new(0));
14841 let counter_clone = counter.clone();
14842 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14843 let task_completion_item = closure_completion_item.clone();
14844 counter_clone.fetch_add(1, atomic::Ordering::Release);
14845 async move {
14846 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14847 is_incomplete: true,
14848 item_defaults: None,
14849 items: vec![task_completion_item],
14850 })))
14851 }
14852 });
14853
14854 cx.condition(|editor, _| editor.context_menu_visible())
14855 .await;
14856 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
14857 assert!(request.next().await.is_some());
14858 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14859
14860 cx.simulate_keystrokes("S o m");
14861 cx.condition(|editor, _| editor.context_menu_visible())
14862 .await;
14863 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
14864 assert!(request.next().await.is_some());
14865 assert!(request.next().await.is_some());
14866 assert!(request.next().await.is_some());
14867 request.close();
14868 assert!(request.next().await.is_none());
14869 assert_eq!(
14870 counter.load(atomic::Ordering::Acquire),
14871 4,
14872 "With the completions menu open, only one LSP request should happen per input"
14873 );
14874}
14875
14876#[gpui::test]
14877async fn test_toggle_comment(cx: &mut TestAppContext) {
14878 init_test(cx, |_| {});
14879 let mut cx = EditorTestContext::new(cx).await;
14880 let language = Arc::new(Language::new(
14881 LanguageConfig {
14882 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14883 ..Default::default()
14884 },
14885 Some(tree_sitter_rust::LANGUAGE.into()),
14886 ));
14887 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14888
14889 // If multiple selections intersect a line, the line is only toggled once.
14890 cx.set_state(indoc! {"
14891 fn a() {
14892 «//b();
14893 ˇ»// «c();
14894 //ˇ» d();
14895 }
14896 "});
14897
14898 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14899
14900 cx.assert_editor_state(indoc! {"
14901 fn a() {
14902 «b();
14903 c();
14904 ˇ» d();
14905 }
14906 "});
14907
14908 // The comment prefix is inserted at the same column for every line in a
14909 // selection.
14910 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14911
14912 cx.assert_editor_state(indoc! {"
14913 fn a() {
14914 // «b();
14915 // c();
14916 ˇ»// d();
14917 }
14918 "});
14919
14920 // If a selection ends at the beginning of a line, that line is not toggled.
14921 cx.set_selections_state(indoc! {"
14922 fn a() {
14923 // b();
14924 «// c();
14925 ˇ» // d();
14926 }
14927 "});
14928
14929 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14930
14931 cx.assert_editor_state(indoc! {"
14932 fn a() {
14933 // b();
14934 «c();
14935 ˇ» // d();
14936 }
14937 "});
14938
14939 // If a selection span a single line and is empty, the line is toggled.
14940 cx.set_state(indoc! {"
14941 fn a() {
14942 a();
14943 b();
14944 ˇ
14945 }
14946 "});
14947
14948 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14949
14950 cx.assert_editor_state(indoc! {"
14951 fn a() {
14952 a();
14953 b();
14954 //•ˇ
14955 }
14956 "});
14957
14958 // If a selection span multiple lines, empty lines are not toggled.
14959 cx.set_state(indoc! {"
14960 fn a() {
14961 «a();
14962
14963 c();ˇ»
14964 }
14965 "});
14966
14967 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14968
14969 cx.assert_editor_state(indoc! {"
14970 fn a() {
14971 // «a();
14972
14973 // c();ˇ»
14974 }
14975 "});
14976
14977 // If a selection includes multiple comment prefixes, all lines are uncommented.
14978 cx.set_state(indoc! {"
14979 fn a() {
14980 «// a();
14981 /// b();
14982 //! c();ˇ»
14983 }
14984 "});
14985
14986 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14987
14988 cx.assert_editor_state(indoc! {"
14989 fn a() {
14990 «a();
14991 b();
14992 c();ˇ»
14993 }
14994 "});
14995}
14996
14997#[gpui::test]
14998async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
14999 init_test(cx, |_| {});
15000 let mut cx = EditorTestContext::new(cx).await;
15001 let language = Arc::new(Language::new(
15002 LanguageConfig {
15003 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15004 ..Default::default()
15005 },
15006 Some(tree_sitter_rust::LANGUAGE.into()),
15007 ));
15008 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15009
15010 let toggle_comments = &ToggleComments {
15011 advance_downwards: false,
15012 ignore_indent: true,
15013 };
15014
15015 // If multiple selections intersect a line, the line is only toggled once.
15016 cx.set_state(indoc! {"
15017 fn a() {
15018 // «b();
15019 // c();
15020 // ˇ» d();
15021 }
15022 "});
15023
15024 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15025
15026 cx.assert_editor_state(indoc! {"
15027 fn a() {
15028 «b();
15029 c();
15030 ˇ» d();
15031 }
15032 "});
15033
15034 // The comment prefix is inserted at the beginning of each line
15035 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15036
15037 cx.assert_editor_state(indoc! {"
15038 fn a() {
15039 // «b();
15040 // c();
15041 // ˇ» d();
15042 }
15043 "});
15044
15045 // If a selection ends at the beginning of a line, that line is not toggled.
15046 cx.set_selections_state(indoc! {"
15047 fn a() {
15048 // b();
15049 // «c();
15050 ˇ»// d();
15051 }
15052 "});
15053
15054 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15055
15056 cx.assert_editor_state(indoc! {"
15057 fn a() {
15058 // b();
15059 «c();
15060 ˇ»// d();
15061 }
15062 "});
15063
15064 // If a selection span a single line and is empty, the line is toggled.
15065 cx.set_state(indoc! {"
15066 fn a() {
15067 a();
15068 b();
15069 ˇ
15070 }
15071 "});
15072
15073 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15074
15075 cx.assert_editor_state(indoc! {"
15076 fn a() {
15077 a();
15078 b();
15079 //ˇ
15080 }
15081 "});
15082
15083 // If a selection span multiple lines, empty lines are not toggled.
15084 cx.set_state(indoc! {"
15085 fn a() {
15086 «a();
15087
15088 c();ˇ»
15089 }
15090 "});
15091
15092 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15093
15094 cx.assert_editor_state(indoc! {"
15095 fn a() {
15096 // «a();
15097
15098 // c();ˇ»
15099 }
15100 "});
15101
15102 // If a selection includes multiple comment prefixes, all lines are uncommented.
15103 cx.set_state(indoc! {"
15104 fn a() {
15105 // «a();
15106 /// b();
15107 //! c();ˇ»
15108 }
15109 "});
15110
15111 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15112
15113 cx.assert_editor_state(indoc! {"
15114 fn a() {
15115 «a();
15116 b();
15117 c();ˇ»
15118 }
15119 "});
15120}
15121
15122#[gpui::test]
15123async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15124 init_test(cx, |_| {});
15125
15126 let language = Arc::new(Language::new(
15127 LanguageConfig {
15128 line_comments: vec!["// ".into()],
15129 ..Default::default()
15130 },
15131 Some(tree_sitter_rust::LANGUAGE.into()),
15132 ));
15133
15134 let mut cx = EditorTestContext::new(cx).await;
15135
15136 cx.language_registry().add(language.clone());
15137 cx.update_buffer(|buffer, cx| {
15138 buffer.set_language(Some(language), cx);
15139 });
15140
15141 let toggle_comments = &ToggleComments {
15142 advance_downwards: true,
15143 ignore_indent: false,
15144 };
15145
15146 // Single cursor on one line -> advance
15147 // Cursor moves horizontally 3 characters as well on non-blank line
15148 cx.set_state(indoc!(
15149 "fn a() {
15150 ˇdog();
15151 cat();
15152 }"
15153 ));
15154 cx.update_editor(|editor, window, cx| {
15155 editor.toggle_comments(toggle_comments, window, cx);
15156 });
15157 cx.assert_editor_state(indoc!(
15158 "fn a() {
15159 // dog();
15160 catˇ();
15161 }"
15162 ));
15163
15164 // Single selection on one line -> don't advance
15165 cx.set_state(indoc!(
15166 "fn a() {
15167 «dog()ˇ»;
15168 cat();
15169 }"
15170 ));
15171 cx.update_editor(|editor, window, cx| {
15172 editor.toggle_comments(toggle_comments, window, cx);
15173 });
15174 cx.assert_editor_state(indoc!(
15175 "fn a() {
15176 // «dog()ˇ»;
15177 cat();
15178 }"
15179 ));
15180
15181 // Multiple cursors on one line -> advance
15182 cx.set_state(indoc!(
15183 "fn a() {
15184 ˇdˇog();
15185 cat();
15186 }"
15187 ));
15188 cx.update_editor(|editor, window, cx| {
15189 editor.toggle_comments(toggle_comments, window, cx);
15190 });
15191 cx.assert_editor_state(indoc!(
15192 "fn a() {
15193 // dog();
15194 catˇ(ˇ);
15195 }"
15196 ));
15197
15198 // Multiple cursors on one line, with selection -> don't advance
15199 cx.set_state(indoc!(
15200 "fn a() {
15201 ˇdˇog«()ˇ»;
15202 cat();
15203 }"
15204 ));
15205 cx.update_editor(|editor, window, cx| {
15206 editor.toggle_comments(toggle_comments, window, cx);
15207 });
15208 cx.assert_editor_state(indoc!(
15209 "fn a() {
15210 // ˇdˇog«()ˇ»;
15211 cat();
15212 }"
15213 ));
15214
15215 // Single cursor on one line -> advance
15216 // Cursor moves to column 0 on blank line
15217 cx.set_state(indoc!(
15218 "fn a() {
15219 ˇdog();
15220
15221 cat();
15222 }"
15223 ));
15224 cx.update_editor(|editor, window, cx| {
15225 editor.toggle_comments(toggle_comments, window, cx);
15226 });
15227 cx.assert_editor_state(indoc!(
15228 "fn a() {
15229 // dog();
15230 ˇ
15231 cat();
15232 }"
15233 ));
15234
15235 // Single cursor on one line -> advance
15236 // Cursor starts and ends at column 0
15237 cx.set_state(indoc!(
15238 "fn a() {
15239 ˇ dog();
15240 cat();
15241 }"
15242 ));
15243 cx.update_editor(|editor, window, cx| {
15244 editor.toggle_comments(toggle_comments, window, cx);
15245 });
15246 cx.assert_editor_state(indoc!(
15247 "fn a() {
15248 // dog();
15249 ˇ cat();
15250 }"
15251 ));
15252}
15253
15254#[gpui::test]
15255async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15256 init_test(cx, |_| {});
15257
15258 let mut cx = EditorTestContext::new(cx).await;
15259
15260 let html_language = Arc::new(
15261 Language::new(
15262 LanguageConfig {
15263 name: "HTML".into(),
15264 block_comment: Some(BlockCommentConfig {
15265 start: "<!-- ".into(),
15266 prefix: "".into(),
15267 end: " -->".into(),
15268 tab_size: 0,
15269 }),
15270 ..Default::default()
15271 },
15272 Some(tree_sitter_html::LANGUAGE.into()),
15273 )
15274 .with_injection_query(
15275 r#"
15276 (script_element
15277 (raw_text) @injection.content
15278 (#set! injection.language "javascript"))
15279 "#,
15280 )
15281 .unwrap(),
15282 );
15283
15284 let javascript_language = Arc::new(Language::new(
15285 LanguageConfig {
15286 name: "JavaScript".into(),
15287 line_comments: vec!["// ".into()],
15288 ..Default::default()
15289 },
15290 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15291 ));
15292
15293 cx.language_registry().add(html_language.clone());
15294 cx.language_registry().add(javascript_language);
15295 cx.update_buffer(|buffer, cx| {
15296 buffer.set_language(Some(html_language), cx);
15297 });
15298
15299 // Toggle comments for empty selections
15300 cx.set_state(
15301 &r#"
15302 <p>A</p>ˇ
15303 <p>B</p>ˇ
15304 <p>C</p>ˇ
15305 "#
15306 .unindent(),
15307 );
15308 cx.update_editor(|editor, window, cx| {
15309 editor.toggle_comments(&ToggleComments::default(), window, cx)
15310 });
15311 cx.assert_editor_state(
15312 &r#"
15313 <!-- <p>A</p>ˇ -->
15314 <!-- <p>B</p>ˇ -->
15315 <!-- <p>C</p>ˇ -->
15316 "#
15317 .unindent(),
15318 );
15319 cx.update_editor(|editor, window, cx| {
15320 editor.toggle_comments(&ToggleComments::default(), window, cx)
15321 });
15322 cx.assert_editor_state(
15323 &r#"
15324 <p>A</p>ˇ
15325 <p>B</p>ˇ
15326 <p>C</p>ˇ
15327 "#
15328 .unindent(),
15329 );
15330
15331 // Toggle comments for mixture of empty and non-empty selections, where
15332 // multiple selections occupy a given line.
15333 cx.set_state(
15334 &r#"
15335 <p>A«</p>
15336 <p>ˇ»B</p>ˇ
15337 <p>C«</p>
15338 <p>ˇ»D</p>ˇ
15339 "#
15340 .unindent(),
15341 );
15342
15343 cx.update_editor(|editor, window, cx| {
15344 editor.toggle_comments(&ToggleComments::default(), window, cx)
15345 });
15346 cx.assert_editor_state(
15347 &r#"
15348 <!-- <p>A«</p>
15349 <p>ˇ»B</p>ˇ -->
15350 <!-- <p>C«</p>
15351 <p>ˇ»D</p>ˇ -->
15352 "#
15353 .unindent(),
15354 );
15355 cx.update_editor(|editor, window, cx| {
15356 editor.toggle_comments(&ToggleComments::default(), window, cx)
15357 });
15358 cx.assert_editor_state(
15359 &r#"
15360 <p>A«</p>
15361 <p>ˇ»B</p>ˇ
15362 <p>C«</p>
15363 <p>ˇ»D</p>ˇ
15364 "#
15365 .unindent(),
15366 );
15367
15368 // Toggle comments when different languages are active for different
15369 // selections.
15370 cx.set_state(
15371 &r#"
15372 ˇ<script>
15373 ˇvar x = new Y();
15374 ˇ</script>
15375 "#
15376 .unindent(),
15377 );
15378 cx.executor().run_until_parked();
15379 cx.update_editor(|editor, window, cx| {
15380 editor.toggle_comments(&ToggleComments::default(), window, cx)
15381 });
15382 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15383 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15384 cx.assert_editor_state(
15385 &r#"
15386 <!-- ˇ<script> -->
15387 // ˇvar x = new Y();
15388 <!-- ˇ</script> -->
15389 "#
15390 .unindent(),
15391 );
15392}
15393
15394#[gpui::test]
15395fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15396 init_test(cx, |_| {});
15397
15398 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15399 let multibuffer = cx.new(|cx| {
15400 let mut multibuffer = MultiBuffer::new(ReadWrite);
15401 multibuffer.push_excerpts(
15402 buffer.clone(),
15403 [
15404 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15405 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15406 ],
15407 cx,
15408 );
15409 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15410 multibuffer
15411 });
15412
15413 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15414 editor.update_in(cx, |editor, window, cx| {
15415 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15417 s.select_ranges([
15418 Point::new(0, 0)..Point::new(0, 0),
15419 Point::new(1, 0)..Point::new(1, 0),
15420 ])
15421 });
15422
15423 editor.handle_input("X", window, cx);
15424 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15425 assert_eq!(
15426 editor.selections.ranges(cx),
15427 [
15428 Point::new(0, 1)..Point::new(0, 1),
15429 Point::new(1, 1)..Point::new(1, 1),
15430 ]
15431 );
15432
15433 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15434 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15435 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15436 });
15437 editor.backspace(&Default::default(), window, cx);
15438 assert_eq!(editor.text(cx), "Xa\nbbb");
15439 assert_eq!(
15440 editor.selections.ranges(cx),
15441 [Point::new(1, 0)..Point::new(1, 0)]
15442 );
15443
15444 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15445 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15446 });
15447 editor.backspace(&Default::default(), window, cx);
15448 assert_eq!(editor.text(cx), "X\nbb");
15449 assert_eq!(
15450 editor.selections.ranges(cx),
15451 [Point::new(0, 1)..Point::new(0, 1)]
15452 );
15453 });
15454}
15455
15456#[gpui::test]
15457fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15458 init_test(cx, |_| {});
15459
15460 let markers = vec![('[', ']').into(), ('(', ')').into()];
15461 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15462 indoc! {"
15463 [aaaa
15464 (bbbb]
15465 cccc)",
15466 },
15467 markers.clone(),
15468 );
15469 let excerpt_ranges = markers.into_iter().map(|marker| {
15470 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15471 ExcerptRange::new(context)
15472 });
15473 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15474 let multibuffer = cx.new(|cx| {
15475 let mut multibuffer = MultiBuffer::new(ReadWrite);
15476 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15477 multibuffer
15478 });
15479
15480 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15481 editor.update_in(cx, |editor, window, cx| {
15482 let (expected_text, selection_ranges) = marked_text_ranges(
15483 indoc! {"
15484 aaaa
15485 bˇbbb
15486 bˇbbˇb
15487 cccc"
15488 },
15489 true,
15490 );
15491 assert_eq!(editor.text(cx), expected_text);
15492 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15493 s.select_ranges(selection_ranges)
15494 });
15495
15496 editor.handle_input("X", window, cx);
15497
15498 let (expected_text, expected_selections) = marked_text_ranges(
15499 indoc! {"
15500 aaaa
15501 bXˇbbXb
15502 bXˇbbXˇb
15503 cccc"
15504 },
15505 false,
15506 );
15507 assert_eq!(editor.text(cx), expected_text);
15508 assert_eq!(editor.selections.ranges(cx), expected_selections);
15509
15510 editor.newline(&Newline, window, cx);
15511 let (expected_text, expected_selections) = marked_text_ranges(
15512 indoc! {"
15513 aaaa
15514 bX
15515 ˇbbX
15516 b
15517 bX
15518 ˇbbX
15519 ˇb
15520 cccc"
15521 },
15522 false,
15523 );
15524 assert_eq!(editor.text(cx), expected_text);
15525 assert_eq!(editor.selections.ranges(cx), expected_selections);
15526 });
15527}
15528
15529#[gpui::test]
15530fn test_refresh_selections(cx: &mut TestAppContext) {
15531 init_test(cx, |_| {});
15532
15533 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15534 let mut excerpt1_id = None;
15535 let multibuffer = cx.new(|cx| {
15536 let mut multibuffer = MultiBuffer::new(ReadWrite);
15537 excerpt1_id = multibuffer
15538 .push_excerpts(
15539 buffer.clone(),
15540 [
15541 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15542 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15543 ],
15544 cx,
15545 )
15546 .into_iter()
15547 .next();
15548 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15549 multibuffer
15550 });
15551
15552 let editor = cx.add_window(|window, cx| {
15553 let mut editor = build_editor(multibuffer.clone(), window, cx);
15554 let snapshot = editor.snapshot(window, cx);
15555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15556 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15557 });
15558 editor.begin_selection(
15559 Point::new(2, 1).to_display_point(&snapshot),
15560 true,
15561 1,
15562 window,
15563 cx,
15564 );
15565 assert_eq!(
15566 editor.selections.ranges(cx),
15567 [
15568 Point::new(1, 3)..Point::new(1, 3),
15569 Point::new(2, 1)..Point::new(2, 1),
15570 ]
15571 );
15572 editor
15573 });
15574
15575 // Refreshing selections is a no-op when excerpts haven't changed.
15576 _ = editor.update(cx, |editor, window, cx| {
15577 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15578 assert_eq!(
15579 editor.selections.ranges(cx),
15580 [
15581 Point::new(1, 3)..Point::new(1, 3),
15582 Point::new(2, 1)..Point::new(2, 1),
15583 ]
15584 );
15585 });
15586
15587 multibuffer.update(cx, |multibuffer, cx| {
15588 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15589 });
15590 _ = editor.update(cx, |editor, window, cx| {
15591 // Removing an excerpt causes the first selection to become degenerate.
15592 assert_eq!(
15593 editor.selections.ranges(cx),
15594 [
15595 Point::new(0, 0)..Point::new(0, 0),
15596 Point::new(0, 1)..Point::new(0, 1)
15597 ]
15598 );
15599
15600 // Refreshing selections will relocate the first selection to the original buffer
15601 // location.
15602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15603 assert_eq!(
15604 editor.selections.ranges(cx),
15605 [
15606 Point::new(0, 1)..Point::new(0, 1),
15607 Point::new(0, 3)..Point::new(0, 3)
15608 ]
15609 );
15610 assert!(editor.selections.pending_anchor().is_some());
15611 });
15612}
15613
15614#[gpui::test]
15615fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15616 init_test(cx, |_| {});
15617
15618 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15619 let mut excerpt1_id = None;
15620 let multibuffer = cx.new(|cx| {
15621 let mut multibuffer = MultiBuffer::new(ReadWrite);
15622 excerpt1_id = multibuffer
15623 .push_excerpts(
15624 buffer.clone(),
15625 [
15626 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15627 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15628 ],
15629 cx,
15630 )
15631 .into_iter()
15632 .next();
15633 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15634 multibuffer
15635 });
15636
15637 let editor = cx.add_window(|window, cx| {
15638 let mut editor = build_editor(multibuffer.clone(), window, cx);
15639 let snapshot = editor.snapshot(window, cx);
15640 editor.begin_selection(
15641 Point::new(1, 3).to_display_point(&snapshot),
15642 false,
15643 1,
15644 window,
15645 cx,
15646 );
15647 assert_eq!(
15648 editor.selections.ranges(cx),
15649 [Point::new(1, 3)..Point::new(1, 3)]
15650 );
15651 editor
15652 });
15653
15654 multibuffer.update(cx, |multibuffer, cx| {
15655 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15656 });
15657 _ = editor.update(cx, |editor, window, cx| {
15658 assert_eq!(
15659 editor.selections.ranges(cx),
15660 [Point::new(0, 0)..Point::new(0, 0)]
15661 );
15662
15663 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15665 assert_eq!(
15666 editor.selections.ranges(cx),
15667 [Point::new(0, 3)..Point::new(0, 3)]
15668 );
15669 assert!(editor.selections.pending_anchor().is_some());
15670 });
15671}
15672
15673#[gpui::test]
15674async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15675 init_test(cx, |_| {});
15676
15677 let language = Arc::new(
15678 Language::new(
15679 LanguageConfig {
15680 brackets: BracketPairConfig {
15681 pairs: vec![
15682 BracketPair {
15683 start: "{".to_string(),
15684 end: "}".to_string(),
15685 close: true,
15686 surround: true,
15687 newline: true,
15688 },
15689 BracketPair {
15690 start: "/* ".to_string(),
15691 end: " */".to_string(),
15692 close: true,
15693 surround: true,
15694 newline: true,
15695 },
15696 ],
15697 ..Default::default()
15698 },
15699 ..Default::default()
15700 },
15701 Some(tree_sitter_rust::LANGUAGE.into()),
15702 )
15703 .with_indents_query("")
15704 .unwrap(),
15705 );
15706
15707 let text = concat!(
15708 "{ }\n", //
15709 " x\n", //
15710 " /* */\n", //
15711 "x\n", //
15712 "{{} }\n", //
15713 );
15714
15715 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
15716 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15717 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15718 editor
15719 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
15720 .await;
15721
15722 editor.update_in(cx, |editor, window, cx| {
15723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15724 s.select_display_ranges([
15725 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
15726 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
15727 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
15728 ])
15729 });
15730 editor.newline(&Newline, window, cx);
15731
15732 assert_eq!(
15733 editor.buffer().read(cx).read(cx).text(),
15734 concat!(
15735 "{ \n", // Suppress rustfmt
15736 "\n", //
15737 "}\n", //
15738 " x\n", //
15739 " /* \n", //
15740 " \n", //
15741 " */\n", //
15742 "x\n", //
15743 "{{} \n", //
15744 "}\n", //
15745 )
15746 );
15747 });
15748}
15749
15750#[gpui::test]
15751fn test_highlighted_ranges(cx: &mut TestAppContext) {
15752 init_test(cx, |_| {});
15753
15754 let editor = cx.add_window(|window, cx| {
15755 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
15756 build_editor(buffer, window, cx)
15757 });
15758
15759 _ = editor.update(cx, |editor, window, cx| {
15760 struct Type1;
15761 struct Type2;
15762
15763 let buffer = editor.buffer.read(cx).snapshot(cx);
15764
15765 let anchor_range =
15766 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
15767
15768 editor.highlight_background::<Type1>(
15769 &[
15770 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
15771 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
15772 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
15773 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
15774 ],
15775 |_| Hsla::red(),
15776 cx,
15777 );
15778 editor.highlight_background::<Type2>(
15779 &[
15780 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
15781 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
15782 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
15783 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
15784 ],
15785 |_| Hsla::green(),
15786 cx,
15787 );
15788
15789 let snapshot = editor.snapshot(window, cx);
15790 let highlighted_ranges = editor.sorted_background_highlights_in_range(
15791 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
15792 &snapshot,
15793 cx.theme(),
15794 );
15795 assert_eq!(
15796 highlighted_ranges,
15797 &[
15798 (
15799 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
15800 Hsla::green(),
15801 ),
15802 (
15803 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
15804 Hsla::red(),
15805 ),
15806 (
15807 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
15808 Hsla::green(),
15809 ),
15810 (
15811 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15812 Hsla::red(),
15813 ),
15814 ]
15815 );
15816 assert_eq!(
15817 editor.sorted_background_highlights_in_range(
15818 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
15819 &snapshot,
15820 cx.theme(),
15821 ),
15822 &[(
15823 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
15824 Hsla::red(),
15825 )]
15826 );
15827 });
15828}
15829
15830#[gpui::test]
15831async fn test_following(cx: &mut TestAppContext) {
15832 init_test(cx, |_| {});
15833
15834 let fs = FakeFs::new(cx.executor());
15835 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15836
15837 let buffer = project.update(cx, |project, cx| {
15838 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
15839 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
15840 });
15841 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
15842 let follower = cx.update(|cx| {
15843 cx.open_window(
15844 WindowOptions {
15845 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
15846 gpui::Point::new(px(0.), px(0.)),
15847 gpui::Point::new(px(10.), px(80.)),
15848 ))),
15849 ..Default::default()
15850 },
15851 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
15852 )
15853 .unwrap()
15854 });
15855
15856 let is_still_following = Rc::new(RefCell::new(true));
15857 let follower_edit_event_count = Rc::new(RefCell::new(0));
15858 let pending_update = Rc::new(RefCell::new(None));
15859 let leader_entity = leader.root(cx).unwrap();
15860 let follower_entity = follower.root(cx).unwrap();
15861 _ = follower.update(cx, {
15862 let update = pending_update.clone();
15863 let is_still_following = is_still_following.clone();
15864 let follower_edit_event_count = follower_edit_event_count.clone();
15865 |_, window, cx| {
15866 cx.subscribe_in(
15867 &leader_entity,
15868 window,
15869 move |_, leader, event, window, cx| {
15870 leader.read(cx).add_event_to_update_proto(
15871 event,
15872 &mut update.borrow_mut(),
15873 window,
15874 cx,
15875 );
15876 },
15877 )
15878 .detach();
15879
15880 cx.subscribe_in(
15881 &follower_entity,
15882 window,
15883 move |_, _, event: &EditorEvent, _window, _cx| {
15884 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
15885 *is_still_following.borrow_mut() = false;
15886 }
15887
15888 if let EditorEvent::BufferEdited = event {
15889 *follower_edit_event_count.borrow_mut() += 1;
15890 }
15891 },
15892 )
15893 .detach();
15894 }
15895 });
15896
15897 // Update the selections only
15898 _ = leader.update(cx, |leader, window, cx| {
15899 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15900 s.select_ranges([1..1])
15901 });
15902 });
15903 follower
15904 .update(cx, |follower, window, cx| {
15905 follower.apply_update_proto(
15906 &project,
15907 pending_update.borrow_mut().take().unwrap(),
15908 window,
15909 cx,
15910 )
15911 })
15912 .unwrap()
15913 .await
15914 .unwrap();
15915 _ = follower.update(cx, |follower, _, cx| {
15916 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15917 });
15918 assert!(*is_still_following.borrow());
15919 assert_eq!(*follower_edit_event_count.borrow(), 0);
15920
15921 // Update the scroll position only
15922 _ = leader.update(cx, |leader, window, cx| {
15923 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15924 });
15925 follower
15926 .update(cx, |follower, window, cx| {
15927 follower.apply_update_proto(
15928 &project,
15929 pending_update.borrow_mut().take().unwrap(),
15930 window,
15931 cx,
15932 )
15933 })
15934 .unwrap()
15935 .await
15936 .unwrap();
15937 assert_eq!(
15938 follower
15939 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15940 .unwrap(),
15941 gpui::Point::new(1.5, 3.5)
15942 );
15943 assert!(*is_still_following.borrow());
15944 assert_eq!(*follower_edit_event_count.borrow(), 0);
15945
15946 // Update the selections and scroll position. The follower's scroll position is updated
15947 // via autoscroll, not via the leader's exact scroll position.
15948 _ = leader.update(cx, |leader, window, cx| {
15949 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15950 s.select_ranges([0..0])
15951 });
15952 leader.request_autoscroll(Autoscroll::newest(), cx);
15953 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15954 });
15955 follower
15956 .update(cx, |follower, window, cx| {
15957 follower.apply_update_proto(
15958 &project,
15959 pending_update.borrow_mut().take().unwrap(),
15960 window,
15961 cx,
15962 )
15963 })
15964 .unwrap()
15965 .await
15966 .unwrap();
15967 _ = follower.update(cx, |follower, _, cx| {
15968 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15969 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15970 });
15971 assert!(*is_still_following.borrow());
15972
15973 // Creating a pending selection that precedes another selection
15974 _ = leader.update(cx, |leader, window, cx| {
15975 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15976 s.select_ranges([1..1])
15977 });
15978 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15979 });
15980 follower
15981 .update(cx, |follower, window, cx| {
15982 follower.apply_update_proto(
15983 &project,
15984 pending_update.borrow_mut().take().unwrap(),
15985 window,
15986 cx,
15987 )
15988 })
15989 .unwrap()
15990 .await
15991 .unwrap();
15992 _ = follower.update(cx, |follower, _, cx| {
15993 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
15994 });
15995 assert!(*is_still_following.borrow());
15996
15997 // Extend the pending selection so that it surrounds another selection
15998 _ = leader.update(cx, |leader, window, cx| {
15999 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16000 });
16001 follower
16002 .update(cx, |follower, window, cx| {
16003 follower.apply_update_proto(
16004 &project,
16005 pending_update.borrow_mut().take().unwrap(),
16006 window,
16007 cx,
16008 )
16009 })
16010 .unwrap()
16011 .await
16012 .unwrap();
16013 _ = follower.update(cx, |follower, _, cx| {
16014 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16015 });
16016
16017 // Scrolling locally breaks the follow
16018 _ = follower.update(cx, |follower, window, cx| {
16019 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16020 follower.set_scroll_anchor(
16021 ScrollAnchor {
16022 anchor: top_anchor,
16023 offset: gpui::Point::new(0.0, 0.5),
16024 },
16025 window,
16026 cx,
16027 );
16028 });
16029 assert!(!(*is_still_following.borrow()));
16030}
16031
16032#[gpui::test]
16033async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16034 init_test(cx, |_| {});
16035
16036 let fs = FakeFs::new(cx.executor());
16037 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16038 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16039 let pane = workspace
16040 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16041 .unwrap();
16042
16043 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16044
16045 let leader = pane.update_in(cx, |_, window, cx| {
16046 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16047 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16048 });
16049
16050 // Start following the editor when it has no excerpts.
16051 let mut state_message =
16052 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16053 let workspace_entity = workspace.root(cx).unwrap();
16054 let follower_1 = cx
16055 .update_window(*workspace.deref(), |_, window, cx| {
16056 Editor::from_state_proto(
16057 workspace_entity,
16058 ViewId {
16059 creator: CollaboratorId::PeerId(PeerId::default()),
16060 id: 0,
16061 },
16062 &mut state_message,
16063 window,
16064 cx,
16065 )
16066 })
16067 .unwrap()
16068 .unwrap()
16069 .await
16070 .unwrap();
16071
16072 let update_message = Rc::new(RefCell::new(None));
16073 follower_1.update_in(cx, {
16074 let update = update_message.clone();
16075 |_, window, cx| {
16076 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16077 leader.read(cx).add_event_to_update_proto(
16078 event,
16079 &mut update.borrow_mut(),
16080 window,
16081 cx,
16082 );
16083 })
16084 .detach();
16085 }
16086 });
16087
16088 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16089 (
16090 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
16091 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
16092 )
16093 });
16094
16095 // Insert some excerpts.
16096 leader.update(cx, |leader, cx| {
16097 leader.buffer.update(cx, |multibuffer, cx| {
16098 multibuffer.set_excerpts_for_path(
16099 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16100 buffer_1.clone(),
16101 vec![
16102 Point::row_range(0..3),
16103 Point::row_range(1..6),
16104 Point::row_range(12..15),
16105 ],
16106 0,
16107 cx,
16108 );
16109 multibuffer.set_excerpts_for_path(
16110 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16111 buffer_2.clone(),
16112 vec![Point::row_range(0..6), Point::row_range(8..12)],
16113 0,
16114 cx,
16115 );
16116 });
16117 });
16118
16119 // Apply the update of adding the excerpts.
16120 follower_1
16121 .update_in(cx, |follower, window, cx| {
16122 follower.apply_update_proto(
16123 &project,
16124 update_message.borrow().clone().unwrap(),
16125 window,
16126 cx,
16127 )
16128 })
16129 .await
16130 .unwrap();
16131 assert_eq!(
16132 follower_1.update(cx, |editor, cx| editor.text(cx)),
16133 leader.update(cx, |editor, cx| editor.text(cx))
16134 );
16135 update_message.borrow_mut().take();
16136
16137 // Start following separately after it already has excerpts.
16138 let mut state_message =
16139 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16140 let workspace_entity = workspace.root(cx).unwrap();
16141 let follower_2 = cx
16142 .update_window(*workspace.deref(), |_, window, cx| {
16143 Editor::from_state_proto(
16144 workspace_entity,
16145 ViewId {
16146 creator: CollaboratorId::PeerId(PeerId::default()),
16147 id: 0,
16148 },
16149 &mut state_message,
16150 window,
16151 cx,
16152 )
16153 })
16154 .unwrap()
16155 .unwrap()
16156 .await
16157 .unwrap();
16158 assert_eq!(
16159 follower_2.update(cx, |editor, cx| editor.text(cx)),
16160 leader.update(cx, |editor, cx| editor.text(cx))
16161 );
16162
16163 // Remove some excerpts.
16164 leader.update(cx, |leader, cx| {
16165 leader.buffer.update(cx, |multibuffer, cx| {
16166 let excerpt_ids = multibuffer.excerpt_ids();
16167 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16168 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16169 });
16170 });
16171
16172 // Apply the update of removing the excerpts.
16173 follower_1
16174 .update_in(cx, |follower, window, cx| {
16175 follower.apply_update_proto(
16176 &project,
16177 update_message.borrow().clone().unwrap(),
16178 window,
16179 cx,
16180 )
16181 })
16182 .await
16183 .unwrap();
16184 follower_2
16185 .update_in(cx, |follower, window, cx| {
16186 follower.apply_update_proto(
16187 &project,
16188 update_message.borrow().clone().unwrap(),
16189 window,
16190 cx,
16191 )
16192 })
16193 .await
16194 .unwrap();
16195 update_message.borrow_mut().take();
16196 assert_eq!(
16197 follower_1.update(cx, |editor, cx| editor.text(cx)),
16198 leader.update(cx, |editor, cx| editor.text(cx))
16199 );
16200}
16201
16202#[gpui::test]
16203async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16204 init_test(cx, |_| {});
16205
16206 let mut cx = EditorTestContext::new(cx).await;
16207 let lsp_store =
16208 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16209
16210 cx.set_state(indoc! {"
16211 ˇfn func(abc def: i32) -> u32 {
16212 }
16213 "});
16214
16215 cx.update(|_, cx| {
16216 lsp_store.update(cx, |lsp_store, cx| {
16217 lsp_store
16218 .update_diagnostics(
16219 LanguageServerId(0),
16220 lsp::PublishDiagnosticsParams {
16221 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16222 version: None,
16223 diagnostics: vec![
16224 lsp::Diagnostic {
16225 range: lsp::Range::new(
16226 lsp::Position::new(0, 11),
16227 lsp::Position::new(0, 12),
16228 ),
16229 severity: Some(lsp::DiagnosticSeverity::ERROR),
16230 ..Default::default()
16231 },
16232 lsp::Diagnostic {
16233 range: lsp::Range::new(
16234 lsp::Position::new(0, 12),
16235 lsp::Position::new(0, 15),
16236 ),
16237 severity: Some(lsp::DiagnosticSeverity::ERROR),
16238 ..Default::default()
16239 },
16240 lsp::Diagnostic {
16241 range: lsp::Range::new(
16242 lsp::Position::new(0, 25),
16243 lsp::Position::new(0, 28),
16244 ),
16245 severity: Some(lsp::DiagnosticSeverity::ERROR),
16246 ..Default::default()
16247 },
16248 ],
16249 },
16250 None,
16251 DiagnosticSourceKind::Pushed,
16252 &[],
16253 cx,
16254 )
16255 .unwrap()
16256 });
16257 });
16258
16259 executor.run_until_parked();
16260
16261 cx.update_editor(|editor, window, cx| {
16262 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16263 });
16264
16265 cx.assert_editor_state(indoc! {"
16266 fn func(abc def: i32) -> ˇu32 {
16267 }
16268 "});
16269
16270 cx.update_editor(|editor, window, cx| {
16271 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16272 });
16273
16274 cx.assert_editor_state(indoc! {"
16275 fn func(abc ˇdef: i32) -> u32 {
16276 }
16277 "});
16278
16279 cx.update_editor(|editor, window, cx| {
16280 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16281 });
16282
16283 cx.assert_editor_state(indoc! {"
16284 fn func(abcˇ def: i32) -> u32 {
16285 }
16286 "});
16287
16288 cx.update_editor(|editor, window, cx| {
16289 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16290 });
16291
16292 cx.assert_editor_state(indoc! {"
16293 fn func(abc def: i32) -> ˇu32 {
16294 }
16295 "});
16296}
16297
16298#[gpui::test]
16299async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16300 init_test(cx, |_| {});
16301
16302 let mut cx = EditorTestContext::new(cx).await;
16303
16304 let diff_base = r#"
16305 use some::mod;
16306
16307 const A: u32 = 42;
16308
16309 fn main() {
16310 println!("hello");
16311
16312 println!("world");
16313 }
16314 "#
16315 .unindent();
16316
16317 // Edits are modified, removed, modified, added
16318 cx.set_state(
16319 &r#"
16320 use some::modified;
16321
16322 ˇ
16323 fn main() {
16324 println!("hello there");
16325
16326 println!("around the");
16327 println!("world");
16328 }
16329 "#
16330 .unindent(),
16331 );
16332
16333 cx.set_head_text(&diff_base);
16334 executor.run_until_parked();
16335
16336 cx.update_editor(|editor, window, cx| {
16337 //Wrap around the bottom of the buffer
16338 for _ in 0..3 {
16339 editor.go_to_next_hunk(&GoToHunk, window, cx);
16340 }
16341 });
16342
16343 cx.assert_editor_state(
16344 &r#"
16345 ˇuse some::modified;
16346
16347
16348 fn main() {
16349 println!("hello there");
16350
16351 println!("around the");
16352 println!("world");
16353 }
16354 "#
16355 .unindent(),
16356 );
16357
16358 cx.update_editor(|editor, window, cx| {
16359 //Wrap around the top of the buffer
16360 for _ in 0..2 {
16361 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16362 }
16363 });
16364
16365 cx.assert_editor_state(
16366 &r#"
16367 use some::modified;
16368
16369
16370 fn main() {
16371 ˇ println!("hello there");
16372
16373 println!("around the");
16374 println!("world");
16375 }
16376 "#
16377 .unindent(),
16378 );
16379
16380 cx.update_editor(|editor, window, cx| {
16381 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16382 });
16383
16384 cx.assert_editor_state(
16385 &r#"
16386 use some::modified;
16387
16388 ˇ
16389 fn main() {
16390 println!("hello there");
16391
16392 println!("around the");
16393 println!("world");
16394 }
16395 "#
16396 .unindent(),
16397 );
16398
16399 cx.update_editor(|editor, window, cx| {
16400 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16401 });
16402
16403 cx.assert_editor_state(
16404 &r#"
16405 ˇuse some::modified;
16406
16407
16408 fn main() {
16409 println!("hello there");
16410
16411 println!("around the");
16412 println!("world");
16413 }
16414 "#
16415 .unindent(),
16416 );
16417
16418 cx.update_editor(|editor, window, cx| {
16419 for _ in 0..2 {
16420 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16421 }
16422 });
16423
16424 cx.assert_editor_state(
16425 &r#"
16426 use some::modified;
16427
16428
16429 fn main() {
16430 ˇ println!("hello there");
16431
16432 println!("around the");
16433 println!("world");
16434 }
16435 "#
16436 .unindent(),
16437 );
16438
16439 cx.update_editor(|editor, window, cx| {
16440 editor.fold(&Fold, window, cx);
16441 });
16442
16443 cx.update_editor(|editor, window, cx| {
16444 editor.go_to_next_hunk(&GoToHunk, window, cx);
16445 });
16446
16447 cx.assert_editor_state(
16448 &r#"
16449 ˇuse some::modified;
16450
16451
16452 fn main() {
16453 println!("hello there");
16454
16455 println!("around the");
16456 println!("world");
16457 }
16458 "#
16459 .unindent(),
16460 );
16461}
16462
16463#[test]
16464fn test_split_words() {
16465 fn split(text: &str) -> Vec<&str> {
16466 split_words(text).collect()
16467 }
16468
16469 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16470 assert_eq!(split("hello_world"), &["hello_", "world"]);
16471 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16472 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16473 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16474 assert_eq!(split("helloworld"), &["helloworld"]);
16475
16476 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16477}
16478
16479#[gpui::test]
16480async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16481 init_test(cx, |_| {});
16482
16483 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16484 let mut assert = |before, after| {
16485 let _state_context = cx.set_state(before);
16486 cx.run_until_parked();
16487 cx.update_editor(|editor, window, cx| {
16488 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16489 });
16490 cx.run_until_parked();
16491 cx.assert_editor_state(after);
16492 };
16493
16494 // Outside bracket jumps to outside of matching bracket
16495 assert("console.logˇ(var);", "console.log(var)ˇ;");
16496 assert("console.log(var)ˇ;", "console.logˇ(var);");
16497
16498 // Inside bracket jumps to inside of matching bracket
16499 assert("console.log(ˇvar);", "console.log(varˇ);");
16500 assert("console.log(varˇ);", "console.log(ˇvar);");
16501
16502 // When outside a bracket and inside, favor jumping to the inside bracket
16503 assert(
16504 "console.log('foo', [1, 2, 3]ˇ);",
16505 "console.log(ˇ'foo', [1, 2, 3]);",
16506 );
16507 assert(
16508 "console.log(ˇ'foo', [1, 2, 3]);",
16509 "console.log('foo', [1, 2, 3]ˇ);",
16510 );
16511
16512 // Bias forward if two options are equally likely
16513 assert(
16514 "let result = curried_fun()ˇ();",
16515 "let result = curried_fun()()ˇ;",
16516 );
16517
16518 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16519 assert(
16520 indoc! {"
16521 function test() {
16522 console.log('test')ˇ
16523 }"},
16524 indoc! {"
16525 function test() {
16526 console.logˇ('test')
16527 }"},
16528 );
16529}
16530
16531#[gpui::test]
16532async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16533 init_test(cx, |_| {});
16534
16535 let fs = FakeFs::new(cx.executor());
16536 fs.insert_tree(
16537 path!("/a"),
16538 json!({
16539 "main.rs": "fn main() { let a = 5; }",
16540 "other.rs": "// Test file",
16541 }),
16542 )
16543 .await;
16544 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16545
16546 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16547 language_registry.add(Arc::new(Language::new(
16548 LanguageConfig {
16549 name: "Rust".into(),
16550 matcher: LanguageMatcher {
16551 path_suffixes: vec!["rs".to_string()],
16552 ..Default::default()
16553 },
16554 brackets: BracketPairConfig {
16555 pairs: vec![BracketPair {
16556 start: "{".to_string(),
16557 end: "}".to_string(),
16558 close: true,
16559 surround: true,
16560 newline: true,
16561 }],
16562 disabled_scopes_by_bracket_ix: Vec::new(),
16563 },
16564 ..Default::default()
16565 },
16566 Some(tree_sitter_rust::LANGUAGE.into()),
16567 )));
16568 let mut fake_servers = language_registry.register_fake_lsp(
16569 "Rust",
16570 FakeLspAdapter {
16571 capabilities: lsp::ServerCapabilities {
16572 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16573 first_trigger_character: "{".to_string(),
16574 more_trigger_character: None,
16575 }),
16576 ..Default::default()
16577 },
16578 ..Default::default()
16579 },
16580 );
16581
16582 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16583
16584 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16585
16586 let worktree_id = workspace
16587 .update(cx, |workspace, _, cx| {
16588 workspace.project().update(cx, |project, cx| {
16589 project.worktrees(cx).next().unwrap().read(cx).id()
16590 })
16591 })
16592 .unwrap();
16593
16594 let buffer = project
16595 .update(cx, |project, cx| {
16596 project.open_local_buffer(path!("/a/main.rs"), cx)
16597 })
16598 .await
16599 .unwrap();
16600 let editor_handle = workspace
16601 .update(cx, |workspace, window, cx| {
16602 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16603 })
16604 .unwrap()
16605 .await
16606 .unwrap()
16607 .downcast::<Editor>()
16608 .unwrap();
16609
16610 cx.executor().start_waiting();
16611 let fake_server = fake_servers.next().await.unwrap();
16612
16613 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16614 |params, _| async move {
16615 assert_eq!(
16616 params.text_document_position.text_document.uri,
16617 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16618 );
16619 assert_eq!(
16620 params.text_document_position.position,
16621 lsp::Position::new(0, 21),
16622 );
16623
16624 Ok(Some(vec![lsp::TextEdit {
16625 new_text: "]".to_string(),
16626 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16627 }]))
16628 },
16629 );
16630
16631 editor_handle.update_in(cx, |editor, window, cx| {
16632 window.focus(&editor.focus_handle(cx));
16633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16634 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16635 });
16636 editor.handle_input("{", window, cx);
16637 });
16638
16639 cx.executor().run_until_parked();
16640
16641 buffer.update(cx, |buffer, _| {
16642 assert_eq!(
16643 buffer.text(),
16644 "fn main() { let a = {5}; }",
16645 "No extra braces from on type formatting should appear in the buffer"
16646 )
16647 });
16648}
16649
16650#[gpui::test(iterations = 20, seeds(31))]
16651async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16652 init_test(cx, |_| {});
16653
16654 let mut cx = EditorLspTestContext::new_rust(
16655 lsp::ServerCapabilities {
16656 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16657 first_trigger_character: ".".to_string(),
16658 more_trigger_character: None,
16659 }),
16660 ..Default::default()
16661 },
16662 cx,
16663 )
16664 .await;
16665
16666 cx.update_buffer(|buffer, _| {
16667 // This causes autoindent to be async.
16668 buffer.set_sync_parse_timeout(Duration::ZERO)
16669 });
16670
16671 cx.set_state("fn c() {\n d()ˇ\n}\n");
16672 cx.simulate_keystroke("\n");
16673 cx.run_until_parked();
16674
16675 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16676 let mut request =
16677 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16678 let buffer_cloned = buffer_cloned.clone();
16679 async move {
16680 buffer_cloned.update(&mut cx, |buffer, _| {
16681 assert_eq!(
16682 buffer.text(),
16683 "fn c() {\n d()\n .\n}\n",
16684 "OnTypeFormatting should triggered after autoindent applied"
16685 )
16686 })?;
16687
16688 Ok(Some(vec![]))
16689 }
16690 });
16691
16692 cx.simulate_keystroke(".");
16693 cx.run_until_parked();
16694
16695 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16696 assert!(request.next().await.is_some());
16697 request.close();
16698 assert!(request.next().await.is_none());
16699}
16700
16701#[gpui::test]
16702async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
16703 init_test(cx, |_| {});
16704
16705 let fs = FakeFs::new(cx.executor());
16706 fs.insert_tree(
16707 path!("/a"),
16708 json!({
16709 "main.rs": "fn main() { let a = 5; }",
16710 "other.rs": "// Test file",
16711 }),
16712 )
16713 .await;
16714
16715 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16716
16717 let server_restarts = Arc::new(AtomicUsize::new(0));
16718 let closure_restarts = Arc::clone(&server_restarts);
16719 let language_server_name = "test language server";
16720 let language_name: LanguageName = "Rust".into();
16721
16722 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16723 language_registry.add(Arc::new(Language::new(
16724 LanguageConfig {
16725 name: language_name.clone(),
16726 matcher: LanguageMatcher {
16727 path_suffixes: vec!["rs".to_string()],
16728 ..Default::default()
16729 },
16730 ..Default::default()
16731 },
16732 Some(tree_sitter_rust::LANGUAGE.into()),
16733 )));
16734 let mut fake_servers = language_registry.register_fake_lsp(
16735 "Rust",
16736 FakeLspAdapter {
16737 name: language_server_name,
16738 initialization_options: Some(json!({
16739 "testOptionValue": true
16740 })),
16741 initializer: Some(Box::new(move |fake_server| {
16742 let task_restarts = Arc::clone(&closure_restarts);
16743 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
16744 task_restarts.fetch_add(1, atomic::Ordering::Release);
16745 futures::future::ready(Ok(()))
16746 });
16747 })),
16748 ..Default::default()
16749 },
16750 );
16751
16752 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16753 let _buffer = project
16754 .update(cx, |project, cx| {
16755 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
16756 })
16757 .await
16758 .unwrap();
16759 let _fake_server = fake_servers.next().await.unwrap();
16760 update_test_language_settings(cx, |language_settings| {
16761 language_settings.languages.0.insert(
16762 language_name.clone(),
16763 LanguageSettingsContent {
16764 tab_size: NonZeroU32::new(8),
16765 ..Default::default()
16766 },
16767 );
16768 });
16769 cx.executor().run_until_parked();
16770 assert_eq!(
16771 server_restarts.load(atomic::Ordering::Acquire),
16772 0,
16773 "Should not restart LSP server on an unrelated change"
16774 );
16775
16776 update_test_project_settings(cx, |project_settings| {
16777 project_settings.lsp.insert(
16778 "Some other server name".into(),
16779 LspSettings {
16780 binary: None,
16781 settings: None,
16782 initialization_options: Some(json!({
16783 "some other init value": false
16784 })),
16785 enable_lsp_tasks: false,
16786 fetch: None,
16787 },
16788 );
16789 });
16790 cx.executor().run_until_parked();
16791 assert_eq!(
16792 server_restarts.load(atomic::Ordering::Acquire),
16793 0,
16794 "Should not restart LSP server on an unrelated LSP settings change"
16795 );
16796
16797 update_test_project_settings(cx, |project_settings| {
16798 project_settings.lsp.insert(
16799 language_server_name.into(),
16800 LspSettings {
16801 binary: None,
16802 settings: None,
16803 initialization_options: Some(json!({
16804 "anotherInitValue": false
16805 })),
16806 enable_lsp_tasks: false,
16807 fetch: None,
16808 },
16809 );
16810 });
16811 cx.executor().run_until_parked();
16812 assert_eq!(
16813 server_restarts.load(atomic::Ordering::Acquire),
16814 1,
16815 "Should restart LSP server on a related LSP settings change"
16816 );
16817
16818 update_test_project_settings(cx, |project_settings| {
16819 project_settings.lsp.insert(
16820 language_server_name.into(),
16821 LspSettings {
16822 binary: None,
16823 settings: None,
16824 initialization_options: Some(json!({
16825 "anotherInitValue": false
16826 })),
16827 enable_lsp_tasks: false,
16828 fetch: None,
16829 },
16830 );
16831 });
16832 cx.executor().run_until_parked();
16833 assert_eq!(
16834 server_restarts.load(atomic::Ordering::Acquire),
16835 1,
16836 "Should not restart LSP server on a related LSP settings change that is the same"
16837 );
16838
16839 update_test_project_settings(cx, |project_settings| {
16840 project_settings.lsp.insert(
16841 language_server_name.into(),
16842 LspSettings {
16843 binary: None,
16844 settings: None,
16845 initialization_options: None,
16846 enable_lsp_tasks: false,
16847 fetch: None,
16848 },
16849 );
16850 });
16851 cx.executor().run_until_parked();
16852 assert_eq!(
16853 server_restarts.load(atomic::Ordering::Acquire),
16854 2,
16855 "Should restart LSP server on another related LSP settings change"
16856 );
16857}
16858
16859#[gpui::test]
16860async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
16861 init_test(cx, |_| {});
16862
16863 let mut cx = EditorLspTestContext::new_rust(
16864 lsp::ServerCapabilities {
16865 completion_provider: Some(lsp::CompletionOptions {
16866 trigger_characters: Some(vec![".".to_string()]),
16867 resolve_provider: Some(true),
16868 ..Default::default()
16869 }),
16870 ..Default::default()
16871 },
16872 cx,
16873 )
16874 .await;
16875
16876 cx.set_state("fn main() { let a = 2ˇ; }");
16877 cx.simulate_keystroke(".");
16878 let completion_item = lsp::CompletionItem {
16879 label: "some".into(),
16880 kind: Some(lsp::CompletionItemKind::SNIPPET),
16881 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16882 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16883 kind: lsp::MarkupKind::Markdown,
16884 value: "```rust\nSome(2)\n```".to_string(),
16885 })),
16886 deprecated: Some(false),
16887 sort_text: Some("fffffff2".to_string()),
16888 filter_text: Some("some".to_string()),
16889 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16890 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16891 range: lsp::Range {
16892 start: lsp::Position {
16893 line: 0,
16894 character: 22,
16895 },
16896 end: lsp::Position {
16897 line: 0,
16898 character: 22,
16899 },
16900 },
16901 new_text: "Some(2)".to_string(),
16902 })),
16903 additional_text_edits: Some(vec![lsp::TextEdit {
16904 range: lsp::Range {
16905 start: lsp::Position {
16906 line: 0,
16907 character: 20,
16908 },
16909 end: lsp::Position {
16910 line: 0,
16911 character: 22,
16912 },
16913 },
16914 new_text: "".to_string(),
16915 }]),
16916 ..Default::default()
16917 };
16918
16919 let closure_completion_item = completion_item.clone();
16920 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16921 let task_completion_item = closure_completion_item.clone();
16922 async move {
16923 Ok(Some(lsp::CompletionResponse::Array(vec![
16924 task_completion_item,
16925 ])))
16926 }
16927 });
16928
16929 request.next().await;
16930
16931 cx.condition(|editor, _| editor.context_menu_visible())
16932 .await;
16933 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16934 editor
16935 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16936 .unwrap()
16937 });
16938 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16939
16940 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16941 let task_completion_item = completion_item.clone();
16942 async move { Ok(task_completion_item) }
16943 })
16944 .next()
16945 .await
16946 .unwrap();
16947 apply_additional_edits.await.unwrap();
16948 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16949}
16950
16951#[gpui::test]
16952async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16953 init_test(cx, |_| {});
16954
16955 let mut cx = EditorLspTestContext::new_rust(
16956 lsp::ServerCapabilities {
16957 completion_provider: Some(lsp::CompletionOptions {
16958 trigger_characters: Some(vec![".".to_string()]),
16959 resolve_provider: Some(true),
16960 ..Default::default()
16961 }),
16962 ..Default::default()
16963 },
16964 cx,
16965 )
16966 .await;
16967
16968 cx.set_state("fn main() { let a = 2ˇ; }");
16969 cx.simulate_keystroke(".");
16970
16971 let item1 = lsp::CompletionItem {
16972 label: "method id()".to_string(),
16973 filter_text: Some("id".to_string()),
16974 detail: None,
16975 documentation: None,
16976 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16977 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16978 new_text: ".id".to_string(),
16979 })),
16980 ..lsp::CompletionItem::default()
16981 };
16982
16983 let item2 = lsp::CompletionItem {
16984 label: "other".to_string(),
16985 filter_text: Some("other".to_string()),
16986 detail: None,
16987 documentation: None,
16988 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16989 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16990 new_text: ".other".to_string(),
16991 })),
16992 ..lsp::CompletionItem::default()
16993 };
16994
16995 let item1 = item1.clone();
16996 cx.set_request_handler::<lsp::request::Completion, _, _>({
16997 let item1 = item1.clone();
16998 move |_, _, _| {
16999 let item1 = item1.clone();
17000 let item2 = item2.clone();
17001 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17002 }
17003 })
17004 .next()
17005 .await;
17006
17007 cx.condition(|editor, _| editor.context_menu_visible())
17008 .await;
17009 cx.update_editor(|editor, _, _| {
17010 let context_menu = editor.context_menu.borrow_mut();
17011 let context_menu = context_menu
17012 .as_ref()
17013 .expect("Should have the context menu deployed");
17014 match context_menu {
17015 CodeContextMenu::Completions(completions_menu) => {
17016 let completions = completions_menu.completions.borrow_mut();
17017 assert_eq!(
17018 completions
17019 .iter()
17020 .map(|completion| &completion.label.text)
17021 .collect::<Vec<_>>(),
17022 vec!["method id()", "other"]
17023 )
17024 }
17025 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17026 }
17027 });
17028
17029 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17030 let item1 = item1.clone();
17031 move |_, item_to_resolve, _| {
17032 let item1 = item1.clone();
17033 async move {
17034 if item1 == item_to_resolve {
17035 Ok(lsp::CompletionItem {
17036 label: "method id()".to_string(),
17037 filter_text: Some("id".to_string()),
17038 detail: Some("Now resolved!".to_string()),
17039 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17040 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17041 range: lsp::Range::new(
17042 lsp::Position::new(0, 22),
17043 lsp::Position::new(0, 22),
17044 ),
17045 new_text: ".id".to_string(),
17046 })),
17047 ..lsp::CompletionItem::default()
17048 })
17049 } else {
17050 Ok(item_to_resolve)
17051 }
17052 }
17053 }
17054 })
17055 .next()
17056 .await
17057 .unwrap();
17058 cx.run_until_parked();
17059
17060 cx.update_editor(|editor, window, cx| {
17061 editor.context_menu_next(&Default::default(), window, cx);
17062 });
17063
17064 cx.update_editor(|editor, _, _| {
17065 let context_menu = editor.context_menu.borrow_mut();
17066 let context_menu = context_menu
17067 .as_ref()
17068 .expect("Should have the context menu deployed");
17069 match context_menu {
17070 CodeContextMenu::Completions(completions_menu) => {
17071 let completions = completions_menu.completions.borrow_mut();
17072 assert_eq!(
17073 completions
17074 .iter()
17075 .map(|completion| &completion.label.text)
17076 .collect::<Vec<_>>(),
17077 vec!["method id() Now resolved!", "other"],
17078 "Should update first completion label, but not second as the filter text did not match."
17079 );
17080 }
17081 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17082 }
17083 });
17084}
17085
17086#[gpui::test]
17087async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17088 init_test(cx, |_| {});
17089 let mut cx = EditorLspTestContext::new_rust(
17090 lsp::ServerCapabilities {
17091 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17092 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17093 completion_provider: Some(lsp::CompletionOptions {
17094 resolve_provider: Some(true),
17095 ..Default::default()
17096 }),
17097 ..Default::default()
17098 },
17099 cx,
17100 )
17101 .await;
17102 cx.set_state(indoc! {"
17103 struct TestStruct {
17104 field: i32
17105 }
17106
17107 fn mainˇ() {
17108 let unused_var = 42;
17109 let test_struct = TestStruct { field: 42 };
17110 }
17111 "});
17112 let symbol_range = cx.lsp_range(indoc! {"
17113 struct TestStruct {
17114 field: i32
17115 }
17116
17117 «fn main»() {
17118 let unused_var = 42;
17119 let test_struct = TestStruct { field: 42 };
17120 }
17121 "});
17122 let mut hover_requests =
17123 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17124 Ok(Some(lsp::Hover {
17125 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17126 kind: lsp::MarkupKind::Markdown,
17127 value: "Function documentation".to_string(),
17128 }),
17129 range: Some(symbol_range),
17130 }))
17131 });
17132
17133 // Case 1: Test that code action menu hide hover popover
17134 cx.dispatch_action(Hover);
17135 hover_requests.next().await;
17136 cx.condition(|editor, _| editor.hover_state.visible()).await;
17137 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17138 move |_, _, _| async move {
17139 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17140 lsp::CodeAction {
17141 title: "Remove unused variable".to_string(),
17142 kind: Some(CodeActionKind::QUICKFIX),
17143 edit: Some(lsp::WorkspaceEdit {
17144 changes: Some(
17145 [(
17146 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17147 vec![lsp::TextEdit {
17148 range: lsp::Range::new(
17149 lsp::Position::new(5, 4),
17150 lsp::Position::new(5, 27),
17151 ),
17152 new_text: "".to_string(),
17153 }],
17154 )]
17155 .into_iter()
17156 .collect(),
17157 ),
17158 ..Default::default()
17159 }),
17160 ..Default::default()
17161 },
17162 )]))
17163 },
17164 );
17165 cx.update_editor(|editor, window, cx| {
17166 editor.toggle_code_actions(
17167 &ToggleCodeActions {
17168 deployed_from: None,
17169 quick_launch: false,
17170 },
17171 window,
17172 cx,
17173 );
17174 });
17175 code_action_requests.next().await;
17176 cx.run_until_parked();
17177 cx.condition(|editor, _| editor.context_menu_visible())
17178 .await;
17179 cx.update_editor(|editor, _, _| {
17180 assert!(
17181 !editor.hover_state.visible(),
17182 "Hover popover should be hidden when code action menu is shown"
17183 );
17184 // Hide code actions
17185 editor.context_menu.take();
17186 });
17187
17188 // Case 2: Test that code completions hide hover popover
17189 cx.dispatch_action(Hover);
17190 hover_requests.next().await;
17191 cx.condition(|editor, _| editor.hover_state.visible()).await;
17192 let counter = Arc::new(AtomicUsize::new(0));
17193 let mut completion_requests =
17194 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17195 let counter = counter.clone();
17196 async move {
17197 counter.fetch_add(1, atomic::Ordering::Release);
17198 Ok(Some(lsp::CompletionResponse::Array(vec![
17199 lsp::CompletionItem {
17200 label: "main".into(),
17201 kind: Some(lsp::CompletionItemKind::FUNCTION),
17202 detail: Some("() -> ()".to_string()),
17203 ..Default::default()
17204 },
17205 lsp::CompletionItem {
17206 label: "TestStruct".into(),
17207 kind: Some(lsp::CompletionItemKind::STRUCT),
17208 detail: Some("struct TestStruct".to_string()),
17209 ..Default::default()
17210 },
17211 ])))
17212 }
17213 });
17214 cx.update_editor(|editor, window, cx| {
17215 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17216 });
17217 completion_requests.next().await;
17218 cx.condition(|editor, _| editor.context_menu_visible())
17219 .await;
17220 cx.update_editor(|editor, _, _| {
17221 assert!(
17222 !editor.hover_state.visible(),
17223 "Hover popover should be hidden when completion menu is shown"
17224 );
17225 });
17226}
17227
17228#[gpui::test]
17229async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17230 init_test(cx, |_| {});
17231
17232 let mut cx = EditorLspTestContext::new_rust(
17233 lsp::ServerCapabilities {
17234 completion_provider: Some(lsp::CompletionOptions {
17235 trigger_characters: Some(vec![".".to_string()]),
17236 resolve_provider: Some(true),
17237 ..Default::default()
17238 }),
17239 ..Default::default()
17240 },
17241 cx,
17242 )
17243 .await;
17244
17245 cx.set_state("fn main() { let a = 2ˇ; }");
17246 cx.simulate_keystroke(".");
17247
17248 let unresolved_item_1 = lsp::CompletionItem {
17249 label: "id".to_string(),
17250 filter_text: Some("id".to_string()),
17251 detail: None,
17252 documentation: None,
17253 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17254 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17255 new_text: ".id".to_string(),
17256 })),
17257 ..lsp::CompletionItem::default()
17258 };
17259 let resolved_item_1 = lsp::CompletionItem {
17260 additional_text_edits: Some(vec![lsp::TextEdit {
17261 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17262 new_text: "!!".to_string(),
17263 }]),
17264 ..unresolved_item_1.clone()
17265 };
17266 let unresolved_item_2 = lsp::CompletionItem {
17267 label: "other".to_string(),
17268 filter_text: Some("other".to_string()),
17269 detail: None,
17270 documentation: None,
17271 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17272 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17273 new_text: ".other".to_string(),
17274 })),
17275 ..lsp::CompletionItem::default()
17276 };
17277 let resolved_item_2 = lsp::CompletionItem {
17278 additional_text_edits: Some(vec![lsp::TextEdit {
17279 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17280 new_text: "??".to_string(),
17281 }]),
17282 ..unresolved_item_2.clone()
17283 };
17284
17285 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17286 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17287 cx.lsp
17288 .server
17289 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17290 let unresolved_item_1 = unresolved_item_1.clone();
17291 let resolved_item_1 = resolved_item_1.clone();
17292 let unresolved_item_2 = unresolved_item_2.clone();
17293 let resolved_item_2 = resolved_item_2.clone();
17294 let resolve_requests_1 = resolve_requests_1.clone();
17295 let resolve_requests_2 = resolve_requests_2.clone();
17296 move |unresolved_request, _| {
17297 let unresolved_item_1 = unresolved_item_1.clone();
17298 let resolved_item_1 = resolved_item_1.clone();
17299 let unresolved_item_2 = unresolved_item_2.clone();
17300 let resolved_item_2 = resolved_item_2.clone();
17301 let resolve_requests_1 = resolve_requests_1.clone();
17302 let resolve_requests_2 = resolve_requests_2.clone();
17303 async move {
17304 if unresolved_request == unresolved_item_1 {
17305 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17306 Ok(resolved_item_1.clone())
17307 } else if unresolved_request == unresolved_item_2 {
17308 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17309 Ok(resolved_item_2.clone())
17310 } else {
17311 panic!("Unexpected completion item {unresolved_request:?}")
17312 }
17313 }
17314 }
17315 })
17316 .detach();
17317
17318 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17319 let unresolved_item_1 = unresolved_item_1.clone();
17320 let unresolved_item_2 = unresolved_item_2.clone();
17321 async move {
17322 Ok(Some(lsp::CompletionResponse::Array(vec![
17323 unresolved_item_1,
17324 unresolved_item_2,
17325 ])))
17326 }
17327 })
17328 .next()
17329 .await;
17330
17331 cx.condition(|editor, _| editor.context_menu_visible())
17332 .await;
17333 cx.update_editor(|editor, _, _| {
17334 let context_menu = editor.context_menu.borrow_mut();
17335 let context_menu = context_menu
17336 .as_ref()
17337 .expect("Should have the context menu deployed");
17338 match context_menu {
17339 CodeContextMenu::Completions(completions_menu) => {
17340 let completions = completions_menu.completions.borrow_mut();
17341 assert_eq!(
17342 completions
17343 .iter()
17344 .map(|completion| &completion.label.text)
17345 .collect::<Vec<_>>(),
17346 vec!["id", "other"]
17347 )
17348 }
17349 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17350 }
17351 });
17352 cx.run_until_parked();
17353
17354 cx.update_editor(|editor, window, cx| {
17355 editor.context_menu_next(&ContextMenuNext, window, cx);
17356 });
17357 cx.run_until_parked();
17358 cx.update_editor(|editor, window, cx| {
17359 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17360 });
17361 cx.run_until_parked();
17362 cx.update_editor(|editor, window, cx| {
17363 editor.context_menu_next(&ContextMenuNext, window, cx);
17364 });
17365 cx.run_until_parked();
17366 cx.update_editor(|editor, window, cx| {
17367 editor
17368 .compose_completion(&ComposeCompletion::default(), window, cx)
17369 .expect("No task returned")
17370 })
17371 .await
17372 .expect("Completion failed");
17373 cx.run_until_parked();
17374
17375 cx.update_editor(|editor, _, cx| {
17376 assert_eq!(
17377 resolve_requests_1.load(atomic::Ordering::Acquire),
17378 1,
17379 "Should always resolve once despite multiple selections"
17380 );
17381 assert_eq!(
17382 resolve_requests_2.load(atomic::Ordering::Acquire),
17383 1,
17384 "Should always resolve once after multiple selections and applying the completion"
17385 );
17386 assert_eq!(
17387 editor.text(cx),
17388 "fn main() { let a = ??.other; }",
17389 "Should use resolved data when applying the completion"
17390 );
17391 });
17392}
17393
17394#[gpui::test]
17395async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17396 init_test(cx, |_| {});
17397
17398 let item_0 = lsp::CompletionItem {
17399 label: "abs".into(),
17400 insert_text: Some("abs".into()),
17401 data: Some(json!({ "very": "special"})),
17402 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17403 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17404 lsp::InsertReplaceEdit {
17405 new_text: "abs".to_string(),
17406 insert: lsp::Range::default(),
17407 replace: lsp::Range::default(),
17408 },
17409 )),
17410 ..lsp::CompletionItem::default()
17411 };
17412 let items = iter::once(item_0.clone())
17413 .chain((11..51).map(|i| lsp::CompletionItem {
17414 label: format!("item_{}", i),
17415 insert_text: Some(format!("item_{}", i)),
17416 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17417 ..lsp::CompletionItem::default()
17418 }))
17419 .collect::<Vec<_>>();
17420
17421 let default_commit_characters = vec!["?".to_string()];
17422 let default_data = json!({ "default": "data"});
17423 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17424 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17425 let default_edit_range = lsp::Range {
17426 start: lsp::Position {
17427 line: 0,
17428 character: 5,
17429 },
17430 end: lsp::Position {
17431 line: 0,
17432 character: 5,
17433 },
17434 };
17435
17436 let mut cx = EditorLspTestContext::new_rust(
17437 lsp::ServerCapabilities {
17438 completion_provider: Some(lsp::CompletionOptions {
17439 trigger_characters: Some(vec![".".to_string()]),
17440 resolve_provider: Some(true),
17441 ..Default::default()
17442 }),
17443 ..Default::default()
17444 },
17445 cx,
17446 )
17447 .await;
17448
17449 cx.set_state("fn main() { let a = 2ˇ; }");
17450 cx.simulate_keystroke(".");
17451
17452 let completion_data = default_data.clone();
17453 let completion_characters = default_commit_characters.clone();
17454 let completion_items = items.clone();
17455 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17456 let default_data = completion_data.clone();
17457 let default_commit_characters = completion_characters.clone();
17458 let items = completion_items.clone();
17459 async move {
17460 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17461 items,
17462 item_defaults: Some(lsp::CompletionListItemDefaults {
17463 data: Some(default_data.clone()),
17464 commit_characters: Some(default_commit_characters.clone()),
17465 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17466 default_edit_range,
17467 )),
17468 insert_text_format: Some(default_insert_text_format),
17469 insert_text_mode: Some(default_insert_text_mode),
17470 }),
17471 ..lsp::CompletionList::default()
17472 })))
17473 }
17474 })
17475 .next()
17476 .await;
17477
17478 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17479 cx.lsp
17480 .server
17481 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17482 let closure_resolved_items = resolved_items.clone();
17483 move |item_to_resolve, _| {
17484 let closure_resolved_items = closure_resolved_items.clone();
17485 async move {
17486 closure_resolved_items.lock().push(item_to_resolve.clone());
17487 Ok(item_to_resolve)
17488 }
17489 }
17490 })
17491 .detach();
17492
17493 cx.condition(|editor, _| editor.context_menu_visible())
17494 .await;
17495 cx.run_until_parked();
17496 cx.update_editor(|editor, _, _| {
17497 let menu = editor.context_menu.borrow_mut();
17498 match menu.as_ref().expect("should have the completions menu") {
17499 CodeContextMenu::Completions(completions_menu) => {
17500 assert_eq!(
17501 completions_menu
17502 .entries
17503 .borrow()
17504 .iter()
17505 .map(|mat| mat.string.clone())
17506 .collect::<Vec<String>>(),
17507 items
17508 .iter()
17509 .map(|completion| completion.label.clone())
17510 .collect::<Vec<String>>()
17511 );
17512 }
17513 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17514 }
17515 });
17516 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17517 // with 4 from the end.
17518 assert_eq!(
17519 *resolved_items.lock(),
17520 [&items[0..16], &items[items.len() - 4..items.len()]]
17521 .concat()
17522 .iter()
17523 .cloned()
17524 .map(|mut item| {
17525 if item.data.is_none() {
17526 item.data = Some(default_data.clone());
17527 }
17528 item
17529 })
17530 .collect::<Vec<lsp::CompletionItem>>(),
17531 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17532 );
17533 resolved_items.lock().clear();
17534
17535 cx.update_editor(|editor, window, cx| {
17536 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17537 });
17538 cx.run_until_parked();
17539 // Completions that have already been resolved are skipped.
17540 assert_eq!(
17541 *resolved_items.lock(),
17542 items[items.len() - 17..items.len() - 4]
17543 .iter()
17544 .cloned()
17545 .map(|mut item| {
17546 if item.data.is_none() {
17547 item.data = Some(default_data.clone());
17548 }
17549 item
17550 })
17551 .collect::<Vec<lsp::CompletionItem>>()
17552 );
17553 resolved_items.lock().clear();
17554}
17555
17556#[gpui::test]
17557async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17558 init_test(cx, |_| {});
17559
17560 let mut cx = EditorLspTestContext::new(
17561 Language::new(
17562 LanguageConfig {
17563 matcher: LanguageMatcher {
17564 path_suffixes: vec!["jsx".into()],
17565 ..Default::default()
17566 },
17567 overrides: [(
17568 "element".into(),
17569 LanguageConfigOverride {
17570 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17571 ..Default::default()
17572 },
17573 )]
17574 .into_iter()
17575 .collect(),
17576 ..Default::default()
17577 },
17578 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17579 )
17580 .with_override_query("(jsx_self_closing_element) @element")
17581 .unwrap(),
17582 lsp::ServerCapabilities {
17583 completion_provider: Some(lsp::CompletionOptions {
17584 trigger_characters: Some(vec![":".to_string()]),
17585 ..Default::default()
17586 }),
17587 ..Default::default()
17588 },
17589 cx,
17590 )
17591 .await;
17592
17593 cx.lsp
17594 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17595 Ok(Some(lsp::CompletionResponse::Array(vec![
17596 lsp::CompletionItem {
17597 label: "bg-blue".into(),
17598 ..Default::default()
17599 },
17600 lsp::CompletionItem {
17601 label: "bg-red".into(),
17602 ..Default::default()
17603 },
17604 lsp::CompletionItem {
17605 label: "bg-yellow".into(),
17606 ..Default::default()
17607 },
17608 ])))
17609 });
17610
17611 cx.set_state(r#"<p class="bgˇ" />"#);
17612
17613 // Trigger completion when typing a dash, because the dash is an extra
17614 // word character in the 'element' scope, which contains the cursor.
17615 cx.simulate_keystroke("-");
17616 cx.executor().run_until_parked();
17617 cx.update_editor(|editor, _, _| {
17618 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17619 {
17620 assert_eq!(
17621 completion_menu_entries(menu),
17622 &["bg-blue", "bg-red", "bg-yellow"]
17623 );
17624 } else {
17625 panic!("expected completion menu to be open");
17626 }
17627 });
17628
17629 cx.simulate_keystroke("l");
17630 cx.executor().run_until_parked();
17631 cx.update_editor(|editor, _, _| {
17632 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17633 {
17634 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17635 } else {
17636 panic!("expected completion menu to be open");
17637 }
17638 });
17639
17640 // When filtering completions, consider the character after the '-' to
17641 // be the start of a subword.
17642 cx.set_state(r#"<p class="yelˇ" />"#);
17643 cx.simulate_keystroke("l");
17644 cx.executor().run_until_parked();
17645 cx.update_editor(|editor, _, _| {
17646 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17647 {
17648 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17649 } else {
17650 panic!("expected completion menu to be open");
17651 }
17652 });
17653}
17654
17655fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17656 let entries = menu.entries.borrow();
17657 entries.iter().map(|mat| mat.string.clone()).collect()
17658}
17659
17660#[gpui::test]
17661async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17662 init_test(cx, |settings| {
17663 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17664 Formatter::Prettier,
17665 )))
17666 });
17667
17668 let fs = FakeFs::new(cx.executor());
17669 fs.insert_file(path!("/file.ts"), Default::default()).await;
17670
17671 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17672 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17673
17674 language_registry.add(Arc::new(Language::new(
17675 LanguageConfig {
17676 name: "TypeScript".into(),
17677 matcher: LanguageMatcher {
17678 path_suffixes: vec!["ts".to_string()],
17679 ..Default::default()
17680 },
17681 ..Default::default()
17682 },
17683 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17684 )));
17685 update_test_language_settings(cx, |settings| {
17686 settings.defaults.prettier = Some(PrettierSettings {
17687 allowed: true,
17688 ..PrettierSettings::default()
17689 });
17690 });
17691
17692 let test_plugin = "test_plugin";
17693 let _ = language_registry.register_fake_lsp(
17694 "TypeScript",
17695 FakeLspAdapter {
17696 prettier_plugins: vec![test_plugin],
17697 ..Default::default()
17698 },
17699 );
17700
17701 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17702 let buffer = project
17703 .update(cx, |project, cx| {
17704 project.open_local_buffer(path!("/file.ts"), cx)
17705 })
17706 .await
17707 .unwrap();
17708
17709 let buffer_text = "one\ntwo\nthree\n";
17710 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17711 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17712 editor.update_in(cx, |editor, window, cx| {
17713 editor.set_text(buffer_text, window, cx)
17714 });
17715
17716 editor
17717 .update_in(cx, |editor, window, cx| {
17718 editor.perform_format(
17719 project.clone(),
17720 FormatTrigger::Manual,
17721 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17722 window,
17723 cx,
17724 )
17725 })
17726 .unwrap()
17727 .await;
17728 assert_eq!(
17729 editor.update(cx, |editor, cx| editor.text(cx)),
17730 buffer_text.to_string() + prettier_format_suffix,
17731 "Test prettier formatting was not applied to the original buffer text",
17732 );
17733
17734 update_test_language_settings(cx, |settings| {
17735 settings.defaults.formatter = Some(SelectedFormatter::Auto)
17736 });
17737 let format = editor.update_in(cx, |editor, window, cx| {
17738 editor.perform_format(
17739 project.clone(),
17740 FormatTrigger::Manual,
17741 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
17742 window,
17743 cx,
17744 )
17745 });
17746 format.await.unwrap();
17747 assert_eq!(
17748 editor.update(cx, |editor, cx| editor.text(cx)),
17749 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
17750 "Autoformatting (via test prettier) was not applied to the original buffer text",
17751 );
17752}
17753
17754#[gpui::test]
17755async fn test_addition_reverts(cx: &mut TestAppContext) {
17756 init_test(cx, |_| {});
17757 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17758 let base_text = indoc! {r#"
17759 struct Row;
17760 struct Row1;
17761 struct Row2;
17762
17763 struct Row4;
17764 struct Row5;
17765 struct Row6;
17766
17767 struct Row8;
17768 struct Row9;
17769 struct Row10;"#};
17770
17771 // When addition hunks are not adjacent to carets, no hunk revert is performed
17772 assert_hunk_revert(
17773 indoc! {r#"struct Row;
17774 struct Row1;
17775 struct Row1.1;
17776 struct Row1.2;
17777 struct Row2;ˇ
17778
17779 struct Row4;
17780 struct Row5;
17781 struct Row6;
17782
17783 struct Row8;
17784 ˇstruct Row9;
17785 struct Row9.1;
17786 struct Row9.2;
17787 struct Row9.3;
17788 struct Row10;"#},
17789 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17790 indoc! {r#"struct Row;
17791 struct Row1;
17792 struct Row1.1;
17793 struct Row1.2;
17794 struct Row2;ˇ
17795
17796 struct Row4;
17797 struct Row5;
17798 struct Row6;
17799
17800 struct Row8;
17801 ˇstruct Row9;
17802 struct Row9.1;
17803 struct Row9.2;
17804 struct Row9.3;
17805 struct Row10;"#},
17806 base_text,
17807 &mut cx,
17808 );
17809 // Same for selections
17810 assert_hunk_revert(
17811 indoc! {r#"struct Row;
17812 struct Row1;
17813 struct Row2;
17814 struct Row2.1;
17815 struct Row2.2;
17816 «ˇ
17817 struct Row4;
17818 struct» Row5;
17819 «struct Row6;
17820 ˇ»
17821 struct Row9.1;
17822 struct Row9.2;
17823 struct Row9.3;
17824 struct Row8;
17825 struct Row9;
17826 struct Row10;"#},
17827 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
17828 indoc! {r#"struct Row;
17829 struct Row1;
17830 struct Row2;
17831 struct Row2.1;
17832 struct Row2.2;
17833 «ˇ
17834 struct Row4;
17835 struct» Row5;
17836 «struct Row6;
17837 ˇ»
17838 struct Row9.1;
17839 struct Row9.2;
17840 struct Row9.3;
17841 struct Row8;
17842 struct Row9;
17843 struct Row10;"#},
17844 base_text,
17845 &mut cx,
17846 );
17847
17848 // When carets and selections intersect the addition hunks, those are reverted.
17849 // Adjacent carets got merged.
17850 assert_hunk_revert(
17851 indoc! {r#"struct Row;
17852 ˇ// something on the top
17853 struct Row1;
17854 struct Row2;
17855 struct Roˇw3.1;
17856 struct Row2.2;
17857 struct Row2.3;ˇ
17858
17859 struct Row4;
17860 struct ˇRow5.1;
17861 struct Row5.2;
17862 struct «Rowˇ»5.3;
17863 struct Row5;
17864 struct Row6;
17865 ˇ
17866 struct Row9.1;
17867 struct «Rowˇ»9.2;
17868 struct «ˇRow»9.3;
17869 struct Row8;
17870 struct Row9;
17871 «ˇ// something on bottom»
17872 struct Row10;"#},
17873 vec![
17874 DiffHunkStatusKind::Added,
17875 DiffHunkStatusKind::Added,
17876 DiffHunkStatusKind::Added,
17877 DiffHunkStatusKind::Added,
17878 DiffHunkStatusKind::Added,
17879 ],
17880 indoc! {r#"struct Row;
17881 ˇstruct Row1;
17882 struct Row2;
17883 ˇ
17884 struct Row4;
17885 ˇstruct Row5;
17886 struct Row6;
17887 ˇ
17888 ˇstruct Row8;
17889 struct Row9;
17890 ˇstruct Row10;"#},
17891 base_text,
17892 &mut cx,
17893 );
17894}
17895
17896#[gpui::test]
17897async fn test_modification_reverts(cx: &mut TestAppContext) {
17898 init_test(cx, |_| {});
17899 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17900 let base_text = indoc! {r#"
17901 struct Row;
17902 struct Row1;
17903 struct Row2;
17904
17905 struct Row4;
17906 struct Row5;
17907 struct Row6;
17908
17909 struct Row8;
17910 struct Row9;
17911 struct Row10;"#};
17912
17913 // Modification hunks behave the same as the addition ones.
17914 assert_hunk_revert(
17915 indoc! {r#"struct Row;
17916 struct Row1;
17917 struct Row33;
17918 ˇ
17919 struct Row4;
17920 struct Row5;
17921 struct Row6;
17922 ˇ
17923 struct Row99;
17924 struct Row9;
17925 struct Row10;"#},
17926 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17927 indoc! {r#"struct Row;
17928 struct Row1;
17929 struct Row33;
17930 ˇ
17931 struct Row4;
17932 struct Row5;
17933 struct Row6;
17934 ˇ
17935 struct Row99;
17936 struct Row9;
17937 struct Row10;"#},
17938 base_text,
17939 &mut cx,
17940 );
17941 assert_hunk_revert(
17942 indoc! {r#"struct Row;
17943 struct Row1;
17944 struct Row33;
17945 «ˇ
17946 struct Row4;
17947 struct» Row5;
17948 «struct Row6;
17949 ˇ»
17950 struct Row99;
17951 struct Row9;
17952 struct Row10;"#},
17953 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17954 indoc! {r#"struct Row;
17955 struct Row1;
17956 struct Row33;
17957 «ˇ
17958 struct Row4;
17959 struct» Row5;
17960 «struct Row6;
17961 ˇ»
17962 struct Row99;
17963 struct Row9;
17964 struct Row10;"#},
17965 base_text,
17966 &mut cx,
17967 );
17968
17969 assert_hunk_revert(
17970 indoc! {r#"ˇstruct Row1.1;
17971 struct Row1;
17972 «ˇstr»uct Row22;
17973
17974 struct ˇRow44;
17975 struct Row5;
17976 struct «Rˇ»ow66;ˇ
17977
17978 «struˇ»ct Row88;
17979 struct Row9;
17980 struct Row1011;ˇ"#},
17981 vec![
17982 DiffHunkStatusKind::Modified,
17983 DiffHunkStatusKind::Modified,
17984 DiffHunkStatusKind::Modified,
17985 DiffHunkStatusKind::Modified,
17986 DiffHunkStatusKind::Modified,
17987 DiffHunkStatusKind::Modified,
17988 ],
17989 indoc! {r#"struct Row;
17990 ˇstruct Row1;
17991 struct Row2;
17992 ˇ
17993 struct Row4;
17994 ˇstruct Row5;
17995 struct Row6;
17996 ˇ
17997 struct Row8;
17998 ˇstruct Row9;
17999 struct Row10;ˇ"#},
18000 base_text,
18001 &mut cx,
18002 );
18003}
18004
18005#[gpui::test]
18006async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18007 init_test(cx, |_| {});
18008 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18009 let base_text = indoc! {r#"
18010 one
18011
18012 two
18013 three
18014 "#};
18015
18016 cx.set_head_text(base_text);
18017 cx.set_state("\nˇ\n");
18018 cx.executor().run_until_parked();
18019 cx.update_editor(|editor, _window, cx| {
18020 editor.expand_selected_diff_hunks(cx);
18021 });
18022 cx.executor().run_until_parked();
18023 cx.update_editor(|editor, window, cx| {
18024 editor.backspace(&Default::default(), window, cx);
18025 });
18026 cx.run_until_parked();
18027 cx.assert_state_with_diff(
18028 indoc! {r#"
18029
18030 - two
18031 - threeˇ
18032 +
18033 "#}
18034 .to_string(),
18035 );
18036}
18037
18038#[gpui::test]
18039async fn test_deletion_reverts(cx: &mut TestAppContext) {
18040 init_test(cx, |_| {});
18041 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18042 let base_text = indoc! {r#"struct Row;
18043struct Row1;
18044struct Row2;
18045
18046struct Row4;
18047struct Row5;
18048struct Row6;
18049
18050struct Row8;
18051struct Row9;
18052struct Row10;"#};
18053
18054 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18055 assert_hunk_revert(
18056 indoc! {r#"struct Row;
18057 struct Row2;
18058
18059 ˇstruct Row4;
18060 struct Row5;
18061 struct Row6;
18062 ˇ
18063 struct Row8;
18064 struct Row10;"#},
18065 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18066 indoc! {r#"struct Row;
18067 struct Row2;
18068
18069 ˇstruct Row4;
18070 struct Row5;
18071 struct Row6;
18072 ˇ
18073 struct Row8;
18074 struct Row10;"#},
18075 base_text,
18076 &mut cx,
18077 );
18078 assert_hunk_revert(
18079 indoc! {r#"struct Row;
18080 struct Row2;
18081
18082 «ˇstruct Row4;
18083 struct» Row5;
18084 «struct Row6;
18085 ˇ»
18086 struct Row8;
18087 struct Row10;"#},
18088 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18089 indoc! {r#"struct Row;
18090 struct Row2;
18091
18092 «ˇstruct Row4;
18093 struct» Row5;
18094 «struct Row6;
18095 ˇ»
18096 struct Row8;
18097 struct Row10;"#},
18098 base_text,
18099 &mut cx,
18100 );
18101
18102 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18103 assert_hunk_revert(
18104 indoc! {r#"struct Row;
18105 ˇstruct Row2;
18106
18107 struct Row4;
18108 struct Row5;
18109 struct Row6;
18110
18111 struct Row8;ˇ
18112 struct Row10;"#},
18113 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18114 indoc! {r#"struct Row;
18115 struct Row1;
18116 ˇstruct Row2;
18117
18118 struct Row4;
18119 struct Row5;
18120 struct Row6;
18121
18122 struct Row8;ˇ
18123 struct Row9;
18124 struct Row10;"#},
18125 base_text,
18126 &mut cx,
18127 );
18128 assert_hunk_revert(
18129 indoc! {r#"struct Row;
18130 struct Row2«ˇ;
18131 struct Row4;
18132 struct» Row5;
18133 «struct Row6;
18134
18135 struct Row8;ˇ»
18136 struct Row10;"#},
18137 vec![
18138 DiffHunkStatusKind::Deleted,
18139 DiffHunkStatusKind::Deleted,
18140 DiffHunkStatusKind::Deleted,
18141 ],
18142 indoc! {r#"struct Row;
18143 struct Row1;
18144 struct Row2«ˇ;
18145
18146 struct Row4;
18147 struct» Row5;
18148 «struct Row6;
18149
18150 struct Row8;ˇ»
18151 struct Row9;
18152 struct Row10;"#},
18153 base_text,
18154 &mut cx,
18155 );
18156}
18157
18158#[gpui::test]
18159async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18160 init_test(cx, |_| {});
18161
18162 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18163 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18164 let base_text_3 =
18165 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18166
18167 let text_1 = edit_first_char_of_every_line(base_text_1);
18168 let text_2 = edit_first_char_of_every_line(base_text_2);
18169 let text_3 = edit_first_char_of_every_line(base_text_3);
18170
18171 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18172 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18173 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18174
18175 let multibuffer = cx.new(|cx| {
18176 let mut multibuffer = MultiBuffer::new(ReadWrite);
18177 multibuffer.push_excerpts(
18178 buffer_1.clone(),
18179 [
18180 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18181 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18182 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18183 ],
18184 cx,
18185 );
18186 multibuffer.push_excerpts(
18187 buffer_2.clone(),
18188 [
18189 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18190 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18191 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18192 ],
18193 cx,
18194 );
18195 multibuffer.push_excerpts(
18196 buffer_3.clone(),
18197 [
18198 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18199 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18200 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18201 ],
18202 cx,
18203 );
18204 multibuffer
18205 });
18206
18207 let fs = FakeFs::new(cx.executor());
18208 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18209 let (editor, cx) = cx
18210 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18211 editor.update_in(cx, |editor, _window, cx| {
18212 for (buffer, diff_base) in [
18213 (buffer_1.clone(), base_text_1),
18214 (buffer_2.clone(), base_text_2),
18215 (buffer_3.clone(), base_text_3),
18216 ] {
18217 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18218 editor
18219 .buffer
18220 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18221 }
18222 });
18223 cx.executor().run_until_parked();
18224
18225 editor.update_in(cx, |editor, window, cx| {
18226 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}");
18227 editor.select_all(&SelectAll, window, cx);
18228 editor.git_restore(&Default::default(), window, cx);
18229 });
18230 cx.executor().run_until_parked();
18231
18232 // When all ranges are selected, all buffer hunks are reverted.
18233 editor.update(cx, |editor, cx| {
18234 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");
18235 });
18236 buffer_1.update(cx, |buffer, _| {
18237 assert_eq!(buffer.text(), base_text_1);
18238 });
18239 buffer_2.update(cx, |buffer, _| {
18240 assert_eq!(buffer.text(), base_text_2);
18241 });
18242 buffer_3.update(cx, |buffer, _| {
18243 assert_eq!(buffer.text(), base_text_3);
18244 });
18245
18246 editor.update_in(cx, |editor, window, cx| {
18247 editor.undo(&Default::default(), window, cx);
18248 });
18249
18250 editor.update_in(cx, |editor, window, cx| {
18251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18252 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18253 });
18254 editor.git_restore(&Default::default(), window, cx);
18255 });
18256
18257 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18258 // but not affect buffer_2 and its related excerpts.
18259 editor.update(cx, |editor, cx| {
18260 assert_eq!(
18261 editor.text(cx),
18262 "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}"
18263 );
18264 });
18265 buffer_1.update(cx, |buffer, _| {
18266 assert_eq!(buffer.text(), base_text_1);
18267 });
18268 buffer_2.update(cx, |buffer, _| {
18269 assert_eq!(
18270 buffer.text(),
18271 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18272 );
18273 });
18274 buffer_3.update(cx, |buffer, _| {
18275 assert_eq!(
18276 buffer.text(),
18277 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18278 );
18279 });
18280
18281 fn edit_first_char_of_every_line(text: &str) -> String {
18282 text.split('\n')
18283 .map(|line| format!("X{}", &line[1..]))
18284 .collect::<Vec<_>>()
18285 .join("\n")
18286 }
18287}
18288
18289#[gpui::test]
18290async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18291 init_test(cx, |_| {});
18292
18293 let cols = 4;
18294 let rows = 10;
18295 let sample_text_1 = sample_text(rows, cols, 'a');
18296 assert_eq!(
18297 sample_text_1,
18298 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18299 );
18300 let sample_text_2 = sample_text(rows, cols, 'l');
18301 assert_eq!(
18302 sample_text_2,
18303 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18304 );
18305 let sample_text_3 = sample_text(rows, cols, 'v');
18306 assert_eq!(
18307 sample_text_3,
18308 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18309 );
18310
18311 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18312 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18313 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18314
18315 let multi_buffer = cx.new(|cx| {
18316 let mut multibuffer = MultiBuffer::new(ReadWrite);
18317 multibuffer.push_excerpts(
18318 buffer_1.clone(),
18319 [
18320 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18321 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18322 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18323 ],
18324 cx,
18325 );
18326 multibuffer.push_excerpts(
18327 buffer_2.clone(),
18328 [
18329 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18330 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18331 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18332 ],
18333 cx,
18334 );
18335 multibuffer.push_excerpts(
18336 buffer_3.clone(),
18337 [
18338 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18339 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18340 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18341 ],
18342 cx,
18343 );
18344 multibuffer
18345 });
18346
18347 let fs = FakeFs::new(cx.executor());
18348 fs.insert_tree(
18349 "/a",
18350 json!({
18351 "main.rs": sample_text_1,
18352 "other.rs": sample_text_2,
18353 "lib.rs": sample_text_3,
18354 }),
18355 )
18356 .await;
18357 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18358 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18359 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18360 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18361 Editor::new(
18362 EditorMode::full(),
18363 multi_buffer,
18364 Some(project.clone()),
18365 window,
18366 cx,
18367 )
18368 });
18369 let multibuffer_item_id = workspace
18370 .update(cx, |workspace, window, cx| {
18371 assert!(
18372 workspace.active_item(cx).is_none(),
18373 "active item should be None before the first item is added"
18374 );
18375 workspace.add_item_to_active_pane(
18376 Box::new(multi_buffer_editor.clone()),
18377 None,
18378 true,
18379 window,
18380 cx,
18381 );
18382 let active_item = workspace
18383 .active_item(cx)
18384 .expect("should have an active item after adding the multi buffer");
18385 assert!(
18386 !active_item.is_singleton(cx),
18387 "A multi buffer was expected to active after adding"
18388 );
18389 active_item.item_id()
18390 })
18391 .unwrap();
18392 cx.executor().run_until_parked();
18393
18394 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18395 editor.change_selections(
18396 SelectionEffects::scroll(Autoscroll::Next),
18397 window,
18398 cx,
18399 |s| s.select_ranges(Some(1..2)),
18400 );
18401 editor.open_excerpts(&OpenExcerpts, window, cx);
18402 });
18403 cx.executor().run_until_parked();
18404 let first_item_id = workspace
18405 .update(cx, |workspace, window, cx| {
18406 let active_item = workspace
18407 .active_item(cx)
18408 .expect("should have an active item after navigating into the 1st buffer");
18409 let first_item_id = active_item.item_id();
18410 assert_ne!(
18411 first_item_id, multibuffer_item_id,
18412 "Should navigate into the 1st buffer and activate it"
18413 );
18414 assert!(
18415 active_item.is_singleton(cx),
18416 "New active item should be a singleton buffer"
18417 );
18418 assert_eq!(
18419 active_item
18420 .act_as::<Editor>(cx)
18421 .expect("should have navigated into an editor for the 1st buffer")
18422 .read(cx)
18423 .text(cx),
18424 sample_text_1
18425 );
18426
18427 workspace
18428 .go_back(workspace.active_pane().downgrade(), window, cx)
18429 .detach_and_log_err(cx);
18430
18431 first_item_id
18432 })
18433 .unwrap();
18434 cx.executor().run_until_parked();
18435 workspace
18436 .update(cx, |workspace, _, cx| {
18437 let active_item = workspace
18438 .active_item(cx)
18439 .expect("should have an active item after navigating back");
18440 assert_eq!(
18441 active_item.item_id(),
18442 multibuffer_item_id,
18443 "Should navigate back to the multi buffer"
18444 );
18445 assert!(!active_item.is_singleton(cx));
18446 })
18447 .unwrap();
18448
18449 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18450 editor.change_selections(
18451 SelectionEffects::scroll(Autoscroll::Next),
18452 window,
18453 cx,
18454 |s| s.select_ranges(Some(39..40)),
18455 );
18456 editor.open_excerpts(&OpenExcerpts, window, cx);
18457 });
18458 cx.executor().run_until_parked();
18459 let second_item_id = workspace
18460 .update(cx, |workspace, window, cx| {
18461 let active_item = workspace
18462 .active_item(cx)
18463 .expect("should have an active item after navigating into the 2nd buffer");
18464 let second_item_id = active_item.item_id();
18465 assert_ne!(
18466 second_item_id, multibuffer_item_id,
18467 "Should navigate away from the multibuffer"
18468 );
18469 assert_ne!(
18470 second_item_id, first_item_id,
18471 "Should navigate into the 2nd buffer and activate it"
18472 );
18473 assert!(
18474 active_item.is_singleton(cx),
18475 "New active item should be a singleton buffer"
18476 );
18477 assert_eq!(
18478 active_item
18479 .act_as::<Editor>(cx)
18480 .expect("should have navigated into an editor")
18481 .read(cx)
18482 .text(cx),
18483 sample_text_2
18484 );
18485
18486 workspace
18487 .go_back(workspace.active_pane().downgrade(), window, cx)
18488 .detach_and_log_err(cx);
18489
18490 second_item_id
18491 })
18492 .unwrap();
18493 cx.executor().run_until_parked();
18494 workspace
18495 .update(cx, |workspace, _, cx| {
18496 let active_item = workspace
18497 .active_item(cx)
18498 .expect("should have an active item after navigating back from the 2nd buffer");
18499 assert_eq!(
18500 active_item.item_id(),
18501 multibuffer_item_id,
18502 "Should navigate back from the 2nd buffer to the multi buffer"
18503 );
18504 assert!(!active_item.is_singleton(cx));
18505 })
18506 .unwrap();
18507
18508 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18509 editor.change_selections(
18510 SelectionEffects::scroll(Autoscroll::Next),
18511 window,
18512 cx,
18513 |s| s.select_ranges(Some(70..70)),
18514 );
18515 editor.open_excerpts(&OpenExcerpts, window, cx);
18516 });
18517 cx.executor().run_until_parked();
18518 workspace
18519 .update(cx, |workspace, window, cx| {
18520 let active_item = workspace
18521 .active_item(cx)
18522 .expect("should have an active item after navigating into the 3rd buffer");
18523 let third_item_id = active_item.item_id();
18524 assert_ne!(
18525 third_item_id, multibuffer_item_id,
18526 "Should navigate into the 3rd buffer and activate it"
18527 );
18528 assert_ne!(third_item_id, first_item_id);
18529 assert_ne!(third_item_id, second_item_id);
18530 assert!(
18531 active_item.is_singleton(cx),
18532 "New active item should be a singleton buffer"
18533 );
18534 assert_eq!(
18535 active_item
18536 .act_as::<Editor>(cx)
18537 .expect("should have navigated into an editor")
18538 .read(cx)
18539 .text(cx),
18540 sample_text_3
18541 );
18542
18543 workspace
18544 .go_back(workspace.active_pane().downgrade(), window, cx)
18545 .detach_and_log_err(cx);
18546 })
18547 .unwrap();
18548 cx.executor().run_until_parked();
18549 workspace
18550 .update(cx, |workspace, _, cx| {
18551 let active_item = workspace
18552 .active_item(cx)
18553 .expect("should have an active item after navigating back from the 3rd buffer");
18554 assert_eq!(
18555 active_item.item_id(),
18556 multibuffer_item_id,
18557 "Should navigate back from the 3rd buffer to the multi buffer"
18558 );
18559 assert!(!active_item.is_singleton(cx));
18560 })
18561 .unwrap();
18562}
18563
18564#[gpui::test]
18565async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18566 init_test(cx, |_| {});
18567
18568 let mut cx = EditorTestContext::new(cx).await;
18569
18570 let diff_base = r#"
18571 use some::mod;
18572
18573 const A: u32 = 42;
18574
18575 fn main() {
18576 println!("hello");
18577
18578 println!("world");
18579 }
18580 "#
18581 .unindent();
18582
18583 cx.set_state(
18584 &r#"
18585 use some::modified;
18586
18587 ˇ
18588 fn main() {
18589 println!("hello there");
18590
18591 println!("around the");
18592 println!("world");
18593 }
18594 "#
18595 .unindent(),
18596 );
18597
18598 cx.set_head_text(&diff_base);
18599 executor.run_until_parked();
18600
18601 cx.update_editor(|editor, window, cx| {
18602 editor.go_to_next_hunk(&GoToHunk, window, cx);
18603 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18604 });
18605 executor.run_until_parked();
18606 cx.assert_state_with_diff(
18607 r#"
18608 use some::modified;
18609
18610
18611 fn main() {
18612 - println!("hello");
18613 + ˇ println!("hello there");
18614
18615 println!("around the");
18616 println!("world");
18617 }
18618 "#
18619 .unindent(),
18620 );
18621
18622 cx.update_editor(|editor, window, cx| {
18623 for _ in 0..2 {
18624 editor.go_to_next_hunk(&GoToHunk, window, cx);
18625 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18626 }
18627 });
18628 executor.run_until_parked();
18629 cx.assert_state_with_diff(
18630 r#"
18631 - use some::mod;
18632 + ˇuse some::modified;
18633
18634
18635 fn main() {
18636 - println!("hello");
18637 + println!("hello there");
18638
18639 + println!("around the");
18640 println!("world");
18641 }
18642 "#
18643 .unindent(),
18644 );
18645
18646 cx.update_editor(|editor, window, cx| {
18647 editor.go_to_next_hunk(&GoToHunk, window, cx);
18648 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18649 });
18650 executor.run_until_parked();
18651 cx.assert_state_with_diff(
18652 r#"
18653 - use some::mod;
18654 + use some::modified;
18655
18656 - const A: u32 = 42;
18657 ˇ
18658 fn main() {
18659 - println!("hello");
18660 + println!("hello there");
18661
18662 + println!("around the");
18663 println!("world");
18664 }
18665 "#
18666 .unindent(),
18667 );
18668
18669 cx.update_editor(|editor, window, cx| {
18670 editor.cancel(&Cancel, window, cx);
18671 });
18672
18673 cx.assert_state_with_diff(
18674 r#"
18675 use some::modified;
18676
18677 ˇ
18678 fn main() {
18679 println!("hello there");
18680
18681 println!("around the");
18682 println!("world");
18683 }
18684 "#
18685 .unindent(),
18686 );
18687}
18688
18689#[gpui::test]
18690async fn test_diff_base_change_with_expanded_diff_hunks(
18691 executor: BackgroundExecutor,
18692 cx: &mut TestAppContext,
18693) {
18694 init_test(cx, |_| {});
18695
18696 let mut cx = EditorTestContext::new(cx).await;
18697
18698 let diff_base = r#"
18699 use some::mod1;
18700 use some::mod2;
18701
18702 const A: u32 = 42;
18703 const B: u32 = 42;
18704 const C: u32 = 42;
18705
18706 fn main() {
18707 println!("hello");
18708
18709 println!("world");
18710 }
18711 "#
18712 .unindent();
18713
18714 cx.set_state(
18715 &r#"
18716 use some::mod2;
18717
18718 const A: u32 = 42;
18719 const C: u32 = 42;
18720
18721 fn main(ˇ) {
18722 //println!("hello");
18723
18724 println!("world");
18725 //
18726 //
18727 }
18728 "#
18729 .unindent(),
18730 );
18731
18732 cx.set_head_text(&diff_base);
18733 executor.run_until_parked();
18734
18735 cx.update_editor(|editor, window, cx| {
18736 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18737 });
18738 executor.run_until_parked();
18739 cx.assert_state_with_diff(
18740 r#"
18741 - use some::mod1;
18742 use some::mod2;
18743
18744 const A: u32 = 42;
18745 - const B: u32 = 42;
18746 const C: u32 = 42;
18747
18748 fn main(ˇ) {
18749 - println!("hello");
18750 + //println!("hello");
18751
18752 println!("world");
18753 + //
18754 + //
18755 }
18756 "#
18757 .unindent(),
18758 );
18759
18760 cx.set_head_text("new diff base!");
18761 executor.run_until_parked();
18762 cx.assert_state_with_diff(
18763 r#"
18764 - new diff base!
18765 + use some::mod2;
18766 +
18767 + const A: u32 = 42;
18768 + const C: u32 = 42;
18769 +
18770 + fn main(ˇ) {
18771 + //println!("hello");
18772 +
18773 + println!("world");
18774 + //
18775 + //
18776 + }
18777 "#
18778 .unindent(),
18779 );
18780}
18781
18782#[gpui::test]
18783async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
18784 init_test(cx, |_| {});
18785
18786 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18787 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
18788 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18789 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
18790 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
18791 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
18792
18793 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
18794 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
18795 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
18796
18797 let multi_buffer = cx.new(|cx| {
18798 let mut multibuffer = MultiBuffer::new(ReadWrite);
18799 multibuffer.push_excerpts(
18800 buffer_1.clone(),
18801 [
18802 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18803 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18804 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18805 ],
18806 cx,
18807 );
18808 multibuffer.push_excerpts(
18809 buffer_2.clone(),
18810 [
18811 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18812 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18813 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18814 ],
18815 cx,
18816 );
18817 multibuffer.push_excerpts(
18818 buffer_3.clone(),
18819 [
18820 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18821 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18822 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
18823 ],
18824 cx,
18825 );
18826 multibuffer
18827 });
18828
18829 let editor =
18830 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18831 editor
18832 .update(cx, |editor, _window, cx| {
18833 for (buffer, diff_base) in [
18834 (buffer_1.clone(), file_1_old),
18835 (buffer_2.clone(), file_2_old),
18836 (buffer_3.clone(), file_3_old),
18837 ] {
18838 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18839 editor
18840 .buffer
18841 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18842 }
18843 })
18844 .unwrap();
18845
18846 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18847 cx.run_until_parked();
18848
18849 cx.assert_editor_state(
18850 &"
18851 ˇaaa
18852 ccc
18853 ddd
18854
18855 ggg
18856 hhh
18857
18858
18859 lll
18860 mmm
18861 NNN
18862
18863 qqq
18864 rrr
18865
18866 uuu
18867 111
18868 222
18869 333
18870
18871 666
18872 777
18873
18874 000
18875 !!!"
18876 .unindent(),
18877 );
18878
18879 cx.update_editor(|editor, window, cx| {
18880 editor.select_all(&SelectAll, window, cx);
18881 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18882 });
18883 cx.executor().run_until_parked();
18884
18885 cx.assert_state_with_diff(
18886 "
18887 «aaa
18888 - bbb
18889 ccc
18890 ddd
18891
18892 ggg
18893 hhh
18894
18895
18896 lll
18897 mmm
18898 - nnn
18899 + NNN
18900
18901 qqq
18902 rrr
18903
18904 uuu
18905 111
18906 222
18907 333
18908
18909 + 666
18910 777
18911
18912 000
18913 !!!ˇ»"
18914 .unindent(),
18915 );
18916}
18917
18918#[gpui::test]
18919async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18920 init_test(cx, |_| {});
18921
18922 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
18923 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
18924
18925 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18926 let multi_buffer = cx.new(|cx| {
18927 let mut multibuffer = MultiBuffer::new(ReadWrite);
18928 multibuffer.push_excerpts(
18929 buffer.clone(),
18930 [
18931 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18932 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18933 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18934 ],
18935 cx,
18936 );
18937 multibuffer
18938 });
18939
18940 let editor =
18941 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18942 editor
18943 .update(cx, |editor, _window, cx| {
18944 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18945 editor
18946 .buffer
18947 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18948 })
18949 .unwrap();
18950
18951 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18952 cx.run_until_parked();
18953
18954 cx.update_editor(|editor, window, cx| {
18955 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18956 });
18957 cx.executor().run_until_parked();
18958
18959 // When the start of a hunk coincides with the start of its excerpt,
18960 // the hunk is expanded. When the start of a a hunk is earlier than
18961 // the start of its excerpt, the hunk is not expanded.
18962 cx.assert_state_with_diff(
18963 "
18964 ˇaaa
18965 - bbb
18966 + BBB
18967
18968 - ddd
18969 - eee
18970 + DDD
18971 + EEE
18972 fff
18973
18974 iii
18975 "
18976 .unindent(),
18977 );
18978}
18979
18980#[gpui::test]
18981async fn test_edits_around_expanded_insertion_hunks(
18982 executor: BackgroundExecutor,
18983 cx: &mut TestAppContext,
18984) {
18985 init_test(cx, |_| {});
18986
18987 let mut cx = EditorTestContext::new(cx).await;
18988
18989 let diff_base = r#"
18990 use some::mod1;
18991 use some::mod2;
18992
18993 const A: u32 = 42;
18994
18995 fn main() {
18996 println!("hello");
18997
18998 println!("world");
18999 }
19000 "#
19001 .unindent();
19002 executor.run_until_parked();
19003 cx.set_state(
19004 &r#"
19005 use some::mod1;
19006 use some::mod2;
19007
19008 const A: u32 = 42;
19009 const B: u32 = 42;
19010 const C: u32 = 42;
19011 ˇ
19012
19013 fn main() {
19014 println!("hello");
19015
19016 println!("world");
19017 }
19018 "#
19019 .unindent(),
19020 );
19021
19022 cx.set_head_text(&diff_base);
19023 executor.run_until_parked();
19024
19025 cx.update_editor(|editor, window, cx| {
19026 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19027 });
19028 executor.run_until_parked();
19029
19030 cx.assert_state_with_diff(
19031 r#"
19032 use some::mod1;
19033 use some::mod2;
19034
19035 const A: u32 = 42;
19036 + const B: u32 = 42;
19037 + const C: u32 = 42;
19038 + ˇ
19039
19040 fn main() {
19041 println!("hello");
19042
19043 println!("world");
19044 }
19045 "#
19046 .unindent(),
19047 );
19048
19049 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19050 executor.run_until_parked();
19051
19052 cx.assert_state_with_diff(
19053 r#"
19054 use some::mod1;
19055 use some::mod2;
19056
19057 const A: u32 = 42;
19058 + const B: u32 = 42;
19059 + const C: u32 = 42;
19060 + const D: u32 = 42;
19061 + ˇ
19062
19063 fn main() {
19064 println!("hello");
19065
19066 println!("world");
19067 }
19068 "#
19069 .unindent(),
19070 );
19071
19072 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19073 executor.run_until_parked();
19074
19075 cx.assert_state_with_diff(
19076 r#"
19077 use some::mod1;
19078 use some::mod2;
19079
19080 const A: u32 = 42;
19081 + const B: u32 = 42;
19082 + const C: u32 = 42;
19083 + const D: u32 = 42;
19084 + const E: u32 = 42;
19085 + ˇ
19086
19087 fn main() {
19088 println!("hello");
19089
19090 println!("world");
19091 }
19092 "#
19093 .unindent(),
19094 );
19095
19096 cx.update_editor(|editor, window, cx| {
19097 editor.delete_line(&DeleteLine, window, cx);
19098 });
19099 executor.run_until_parked();
19100
19101 cx.assert_state_with_diff(
19102 r#"
19103 use some::mod1;
19104 use some::mod2;
19105
19106 const A: u32 = 42;
19107 + const B: u32 = 42;
19108 + const C: u32 = 42;
19109 + const D: u32 = 42;
19110 + const E: u32 = 42;
19111 ˇ
19112 fn main() {
19113 println!("hello");
19114
19115 println!("world");
19116 }
19117 "#
19118 .unindent(),
19119 );
19120
19121 cx.update_editor(|editor, window, cx| {
19122 editor.move_up(&MoveUp, window, cx);
19123 editor.delete_line(&DeleteLine, window, cx);
19124 editor.move_up(&MoveUp, window, cx);
19125 editor.delete_line(&DeleteLine, window, cx);
19126 editor.move_up(&MoveUp, window, cx);
19127 editor.delete_line(&DeleteLine, window, cx);
19128 });
19129 executor.run_until_parked();
19130 cx.assert_state_with_diff(
19131 r#"
19132 use some::mod1;
19133 use some::mod2;
19134
19135 const A: u32 = 42;
19136 + const B: u32 = 42;
19137 ˇ
19138 fn main() {
19139 println!("hello");
19140
19141 println!("world");
19142 }
19143 "#
19144 .unindent(),
19145 );
19146
19147 cx.update_editor(|editor, window, cx| {
19148 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19149 editor.delete_line(&DeleteLine, window, cx);
19150 });
19151 executor.run_until_parked();
19152 cx.assert_state_with_diff(
19153 r#"
19154 ˇ
19155 fn main() {
19156 println!("hello");
19157
19158 println!("world");
19159 }
19160 "#
19161 .unindent(),
19162 );
19163}
19164
19165#[gpui::test]
19166async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19167 init_test(cx, |_| {});
19168
19169 let mut cx = EditorTestContext::new(cx).await;
19170 cx.set_head_text(indoc! { "
19171 one
19172 two
19173 three
19174 four
19175 five
19176 "
19177 });
19178 cx.set_state(indoc! { "
19179 one
19180 ˇthree
19181 five
19182 "});
19183 cx.run_until_parked();
19184 cx.update_editor(|editor, window, cx| {
19185 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19186 });
19187 cx.assert_state_with_diff(
19188 indoc! { "
19189 one
19190 - two
19191 ˇthree
19192 - four
19193 five
19194 "}
19195 .to_string(),
19196 );
19197 cx.update_editor(|editor, window, cx| {
19198 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19199 });
19200
19201 cx.assert_state_with_diff(
19202 indoc! { "
19203 one
19204 ˇthree
19205 five
19206 "}
19207 .to_string(),
19208 );
19209
19210 cx.set_state(indoc! { "
19211 one
19212 ˇTWO
19213 three
19214 four
19215 five
19216 "});
19217 cx.run_until_parked();
19218 cx.update_editor(|editor, window, cx| {
19219 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19220 });
19221
19222 cx.assert_state_with_diff(
19223 indoc! { "
19224 one
19225 - two
19226 + ˇTWO
19227 three
19228 four
19229 five
19230 "}
19231 .to_string(),
19232 );
19233 cx.update_editor(|editor, window, cx| {
19234 editor.move_up(&Default::default(), window, cx);
19235 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19236 });
19237 cx.assert_state_with_diff(
19238 indoc! { "
19239 one
19240 ˇTWO
19241 three
19242 four
19243 five
19244 "}
19245 .to_string(),
19246 );
19247}
19248
19249#[gpui::test]
19250async fn test_edits_around_expanded_deletion_hunks(
19251 executor: BackgroundExecutor,
19252 cx: &mut TestAppContext,
19253) {
19254 init_test(cx, |_| {});
19255
19256 let mut cx = EditorTestContext::new(cx).await;
19257
19258 let diff_base = r#"
19259 use some::mod1;
19260 use some::mod2;
19261
19262 const A: u32 = 42;
19263 const B: u32 = 42;
19264 const C: u32 = 42;
19265
19266
19267 fn main() {
19268 println!("hello");
19269
19270 println!("world");
19271 }
19272 "#
19273 .unindent();
19274 executor.run_until_parked();
19275 cx.set_state(
19276 &r#"
19277 use some::mod1;
19278 use some::mod2;
19279
19280 ˇconst B: u32 = 42;
19281 const C: u32 = 42;
19282
19283
19284 fn main() {
19285 println!("hello");
19286
19287 println!("world");
19288 }
19289 "#
19290 .unindent(),
19291 );
19292
19293 cx.set_head_text(&diff_base);
19294 executor.run_until_parked();
19295
19296 cx.update_editor(|editor, window, cx| {
19297 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19298 });
19299 executor.run_until_parked();
19300
19301 cx.assert_state_with_diff(
19302 r#"
19303 use some::mod1;
19304 use some::mod2;
19305
19306 - const A: u32 = 42;
19307 ˇconst B: u32 = 42;
19308 const C: u32 = 42;
19309
19310
19311 fn main() {
19312 println!("hello");
19313
19314 println!("world");
19315 }
19316 "#
19317 .unindent(),
19318 );
19319
19320 cx.update_editor(|editor, window, cx| {
19321 editor.delete_line(&DeleteLine, window, cx);
19322 });
19323 executor.run_until_parked();
19324 cx.assert_state_with_diff(
19325 r#"
19326 use some::mod1;
19327 use some::mod2;
19328
19329 - const A: u32 = 42;
19330 - const B: u32 = 42;
19331 ˇconst C: u32 = 42;
19332
19333
19334 fn main() {
19335 println!("hello");
19336
19337 println!("world");
19338 }
19339 "#
19340 .unindent(),
19341 );
19342
19343 cx.update_editor(|editor, window, cx| {
19344 editor.delete_line(&DeleteLine, window, cx);
19345 });
19346 executor.run_until_parked();
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 ˇ
19356
19357 fn main() {
19358 println!("hello");
19359
19360 println!("world");
19361 }
19362 "#
19363 .unindent(),
19364 );
19365
19366 cx.update_editor(|editor, window, cx| {
19367 editor.handle_input("replacement", window, cx);
19368 });
19369 executor.run_until_parked();
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 -
19379 + replacementˇ
19380
19381 fn main() {
19382 println!("hello");
19383
19384 println!("world");
19385 }
19386 "#
19387 .unindent(),
19388 );
19389}
19390
19391#[gpui::test]
19392async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19393 init_test(cx, |_| {});
19394
19395 let mut cx = EditorTestContext::new(cx).await;
19396
19397 let base_text = r#"
19398 one
19399 two
19400 three
19401 four
19402 five
19403 "#
19404 .unindent();
19405 executor.run_until_parked();
19406 cx.set_state(
19407 &r#"
19408 one
19409 two
19410 fˇour
19411 five
19412 "#
19413 .unindent(),
19414 );
19415
19416 cx.set_head_text(&base_text);
19417 executor.run_until_parked();
19418
19419 cx.update_editor(|editor, window, cx| {
19420 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19421 });
19422 executor.run_until_parked();
19423
19424 cx.assert_state_with_diff(
19425 r#"
19426 one
19427 two
19428 - three
19429 fˇour
19430 five
19431 "#
19432 .unindent(),
19433 );
19434
19435 cx.update_editor(|editor, window, cx| {
19436 editor.backspace(&Backspace, window, cx);
19437 editor.backspace(&Backspace, window, cx);
19438 });
19439 executor.run_until_parked();
19440 cx.assert_state_with_diff(
19441 r#"
19442 one
19443 two
19444 - threeˇ
19445 - four
19446 + our
19447 five
19448 "#
19449 .unindent(),
19450 );
19451}
19452
19453#[gpui::test]
19454async fn test_edit_after_expanded_modification_hunk(
19455 executor: BackgroundExecutor,
19456 cx: &mut TestAppContext,
19457) {
19458 init_test(cx, |_| {});
19459
19460 let mut cx = EditorTestContext::new(cx).await;
19461
19462 let diff_base = r#"
19463 use some::mod1;
19464 use some::mod2;
19465
19466 const A: u32 = 42;
19467 const B: u32 = 42;
19468 const C: u32 = 42;
19469 const D: u32 = 42;
19470
19471
19472 fn main() {
19473 println!("hello");
19474
19475 println!("world");
19476 }"#
19477 .unindent();
19478
19479 cx.set_state(
19480 &r#"
19481 use some::mod1;
19482 use some::mod2;
19483
19484 const A: u32 = 42;
19485 const B: u32 = 42;
19486 const C: u32 = 43ˇ
19487 const D: u32 = 42;
19488
19489
19490 fn main() {
19491 println!("hello");
19492
19493 println!("world");
19494 }"#
19495 .unindent(),
19496 );
19497
19498 cx.set_head_text(&diff_base);
19499 executor.run_until_parked();
19500 cx.update_editor(|editor, window, cx| {
19501 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19502 });
19503 executor.run_until_parked();
19504
19505 cx.assert_state_with_diff(
19506 r#"
19507 use some::mod1;
19508 use some::mod2;
19509
19510 const A: u32 = 42;
19511 const B: u32 = 42;
19512 - const C: u32 = 42;
19513 + const C: u32 = 43ˇ
19514 const D: u32 = 42;
19515
19516
19517 fn main() {
19518 println!("hello");
19519
19520 println!("world");
19521 }"#
19522 .unindent(),
19523 );
19524
19525 cx.update_editor(|editor, window, cx| {
19526 editor.handle_input("\nnew_line\n", window, cx);
19527 });
19528 executor.run_until_parked();
19529
19530 cx.assert_state_with_diff(
19531 r#"
19532 use some::mod1;
19533 use some::mod2;
19534
19535 const A: u32 = 42;
19536 const B: u32 = 42;
19537 - const C: u32 = 42;
19538 + const C: u32 = 43
19539 + new_line
19540 + ˇ
19541 const D: u32 = 42;
19542
19543
19544 fn main() {
19545 println!("hello");
19546
19547 println!("world");
19548 }"#
19549 .unindent(),
19550 );
19551}
19552
19553#[gpui::test]
19554async fn test_stage_and_unstage_added_file_hunk(
19555 executor: BackgroundExecutor,
19556 cx: &mut TestAppContext,
19557) {
19558 init_test(cx, |_| {});
19559
19560 let mut cx = EditorTestContext::new(cx).await;
19561 cx.update_editor(|editor, _, cx| {
19562 editor.set_expand_all_diff_hunks(cx);
19563 });
19564
19565 let working_copy = r#"
19566 ˇfn main() {
19567 println!("hello, world!");
19568 }
19569 "#
19570 .unindent();
19571
19572 cx.set_state(&working_copy);
19573 executor.run_until_parked();
19574
19575 cx.assert_state_with_diff(
19576 r#"
19577 + ˇfn main() {
19578 + println!("hello, world!");
19579 + }
19580 "#
19581 .unindent(),
19582 );
19583 cx.assert_index_text(None);
19584
19585 cx.update_editor(|editor, window, cx| {
19586 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19587 });
19588 executor.run_until_parked();
19589 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19590 cx.assert_state_with_diff(
19591 r#"
19592 + ˇfn main() {
19593 + println!("hello, world!");
19594 + }
19595 "#
19596 .unindent(),
19597 );
19598
19599 cx.update_editor(|editor, window, cx| {
19600 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19601 });
19602 executor.run_until_parked();
19603 cx.assert_index_text(None);
19604}
19605
19606async fn setup_indent_guides_editor(
19607 text: &str,
19608 cx: &mut TestAppContext,
19609) -> (BufferId, EditorTestContext) {
19610 init_test(cx, |_| {});
19611
19612 let mut cx = EditorTestContext::new(cx).await;
19613
19614 let buffer_id = cx.update_editor(|editor, window, cx| {
19615 editor.set_text(text, window, cx);
19616 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19617
19618 buffer_ids[0]
19619 });
19620
19621 (buffer_id, cx)
19622}
19623
19624fn assert_indent_guides(
19625 range: Range<u32>,
19626 expected: Vec<IndentGuide>,
19627 active_indices: Option<Vec<usize>>,
19628 cx: &mut EditorTestContext,
19629) {
19630 let indent_guides = cx.update_editor(|editor, window, cx| {
19631 let snapshot = editor.snapshot(window, cx).display_snapshot;
19632 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19633 editor,
19634 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19635 true,
19636 &snapshot,
19637 cx,
19638 );
19639
19640 indent_guides.sort_by(|a, b| {
19641 a.depth.cmp(&b.depth).then(
19642 a.start_row
19643 .cmp(&b.start_row)
19644 .then(a.end_row.cmp(&b.end_row)),
19645 )
19646 });
19647 indent_guides
19648 });
19649
19650 if let Some(expected) = active_indices {
19651 let active_indices = cx.update_editor(|editor, window, cx| {
19652 let snapshot = editor.snapshot(window, cx).display_snapshot;
19653 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19654 });
19655
19656 assert_eq!(
19657 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19658 expected,
19659 "Active indent guide indices do not match"
19660 );
19661 }
19662
19663 assert_eq!(indent_guides, expected, "Indent guides do not match");
19664}
19665
19666fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19667 IndentGuide {
19668 buffer_id,
19669 start_row: MultiBufferRow(start_row),
19670 end_row: MultiBufferRow(end_row),
19671 depth,
19672 tab_size: 4,
19673 settings: IndentGuideSettings {
19674 enabled: true,
19675 line_width: 1,
19676 active_line_width: 1,
19677 ..Default::default()
19678 },
19679 }
19680}
19681
19682#[gpui::test]
19683async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19684 let (buffer_id, mut cx) = setup_indent_guides_editor(
19685 &"
19686 fn main() {
19687 let a = 1;
19688 }"
19689 .unindent(),
19690 cx,
19691 )
19692 .await;
19693
19694 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19695}
19696
19697#[gpui::test]
19698async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19699 let (buffer_id, mut cx) = setup_indent_guides_editor(
19700 &"
19701 fn main() {
19702 let a = 1;
19703 let b = 2;
19704 }"
19705 .unindent(),
19706 cx,
19707 )
19708 .await;
19709
19710 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
19711}
19712
19713#[gpui::test]
19714async fn test_indent_guide_nested(cx: &mut TestAppContext) {
19715 let (buffer_id, mut cx) = setup_indent_guides_editor(
19716 &"
19717 fn main() {
19718 let a = 1;
19719 if a == 3 {
19720 let b = 2;
19721 } else {
19722 let c = 3;
19723 }
19724 }"
19725 .unindent(),
19726 cx,
19727 )
19728 .await;
19729
19730 assert_indent_guides(
19731 0..8,
19732 vec![
19733 indent_guide(buffer_id, 1, 6, 0),
19734 indent_guide(buffer_id, 3, 3, 1),
19735 indent_guide(buffer_id, 5, 5, 1),
19736 ],
19737 None,
19738 &mut cx,
19739 );
19740}
19741
19742#[gpui::test]
19743async fn test_indent_guide_tab(cx: &mut TestAppContext) {
19744 let (buffer_id, mut cx) = setup_indent_guides_editor(
19745 &"
19746 fn main() {
19747 let a = 1;
19748 let b = 2;
19749 let c = 3;
19750 }"
19751 .unindent(),
19752 cx,
19753 )
19754 .await;
19755
19756 assert_indent_guides(
19757 0..5,
19758 vec![
19759 indent_guide(buffer_id, 1, 3, 0),
19760 indent_guide(buffer_id, 2, 2, 1),
19761 ],
19762 None,
19763 &mut cx,
19764 );
19765}
19766
19767#[gpui::test]
19768async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
19769 let (buffer_id, mut cx) = setup_indent_guides_editor(
19770 &"
19771 fn main() {
19772 let a = 1;
19773
19774 let c = 3;
19775 }"
19776 .unindent(),
19777 cx,
19778 )
19779 .await;
19780
19781 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
19782}
19783
19784#[gpui::test]
19785async fn test_indent_guide_complex(cx: &mut TestAppContext) {
19786 let (buffer_id, mut cx) = setup_indent_guides_editor(
19787 &"
19788 fn main() {
19789 let a = 1;
19790
19791 let c = 3;
19792
19793 if a == 3 {
19794 let b = 2;
19795 } else {
19796 let c = 3;
19797 }
19798 }"
19799 .unindent(),
19800 cx,
19801 )
19802 .await;
19803
19804 assert_indent_guides(
19805 0..11,
19806 vec![
19807 indent_guide(buffer_id, 1, 9, 0),
19808 indent_guide(buffer_id, 6, 6, 1),
19809 indent_guide(buffer_id, 8, 8, 1),
19810 ],
19811 None,
19812 &mut cx,
19813 );
19814}
19815
19816#[gpui::test]
19817async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
19818 let (buffer_id, mut cx) = setup_indent_guides_editor(
19819 &"
19820 fn main() {
19821 let a = 1;
19822
19823 let c = 3;
19824
19825 if a == 3 {
19826 let b = 2;
19827 } else {
19828 let c = 3;
19829 }
19830 }"
19831 .unindent(),
19832 cx,
19833 )
19834 .await;
19835
19836 assert_indent_guides(
19837 1..11,
19838 vec![
19839 indent_guide(buffer_id, 1, 9, 0),
19840 indent_guide(buffer_id, 6, 6, 1),
19841 indent_guide(buffer_id, 8, 8, 1),
19842 ],
19843 None,
19844 &mut cx,
19845 );
19846}
19847
19848#[gpui::test]
19849async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
19850 let (buffer_id, mut cx) = setup_indent_guides_editor(
19851 &"
19852 fn main() {
19853 let a = 1;
19854
19855 let c = 3;
19856
19857 if a == 3 {
19858 let b = 2;
19859 } else {
19860 let c = 3;
19861 }
19862 }"
19863 .unindent(),
19864 cx,
19865 )
19866 .await;
19867
19868 assert_indent_guides(
19869 1..10,
19870 vec![
19871 indent_guide(buffer_id, 1, 9, 0),
19872 indent_guide(buffer_id, 6, 6, 1),
19873 indent_guide(buffer_id, 8, 8, 1),
19874 ],
19875 None,
19876 &mut cx,
19877 );
19878}
19879
19880#[gpui::test]
19881async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
19882 let (buffer_id, mut cx) = setup_indent_guides_editor(
19883 &"
19884 fn main() {
19885 if a {
19886 b(
19887 c,
19888 d,
19889 )
19890 } else {
19891 e(
19892 f
19893 )
19894 }
19895 }"
19896 .unindent(),
19897 cx,
19898 )
19899 .await;
19900
19901 assert_indent_guides(
19902 0..11,
19903 vec![
19904 indent_guide(buffer_id, 1, 10, 0),
19905 indent_guide(buffer_id, 2, 5, 1),
19906 indent_guide(buffer_id, 7, 9, 1),
19907 indent_guide(buffer_id, 3, 4, 2),
19908 indent_guide(buffer_id, 8, 8, 2),
19909 ],
19910 None,
19911 &mut cx,
19912 );
19913
19914 cx.update_editor(|editor, window, cx| {
19915 editor.fold_at(MultiBufferRow(2), window, cx);
19916 assert_eq!(
19917 editor.display_text(cx),
19918 "
19919 fn main() {
19920 if a {
19921 b(⋯
19922 )
19923 } else {
19924 e(
19925 f
19926 )
19927 }
19928 }"
19929 .unindent()
19930 );
19931 });
19932
19933 assert_indent_guides(
19934 0..11,
19935 vec![
19936 indent_guide(buffer_id, 1, 10, 0),
19937 indent_guide(buffer_id, 2, 5, 1),
19938 indent_guide(buffer_id, 7, 9, 1),
19939 indent_guide(buffer_id, 8, 8, 2),
19940 ],
19941 None,
19942 &mut cx,
19943 );
19944}
19945
19946#[gpui::test]
19947async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19948 let (buffer_id, mut cx) = setup_indent_guides_editor(
19949 &"
19950 block1
19951 block2
19952 block3
19953 block4
19954 block2
19955 block1
19956 block1"
19957 .unindent(),
19958 cx,
19959 )
19960 .await;
19961
19962 assert_indent_guides(
19963 1..10,
19964 vec![
19965 indent_guide(buffer_id, 1, 4, 0),
19966 indent_guide(buffer_id, 2, 3, 1),
19967 indent_guide(buffer_id, 3, 3, 2),
19968 ],
19969 None,
19970 &mut cx,
19971 );
19972}
19973
19974#[gpui::test]
19975async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19976 let (buffer_id, mut cx) = setup_indent_guides_editor(
19977 &"
19978 block1
19979 block2
19980 block3
19981
19982 block1
19983 block1"
19984 .unindent(),
19985 cx,
19986 )
19987 .await;
19988
19989 assert_indent_guides(
19990 0..6,
19991 vec![
19992 indent_guide(buffer_id, 1, 2, 0),
19993 indent_guide(buffer_id, 2, 2, 1),
19994 ],
19995 None,
19996 &mut cx,
19997 );
19998}
19999
20000#[gpui::test]
20001async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20002 let (buffer_id, mut cx) = setup_indent_guides_editor(
20003 &"
20004 function component() {
20005 \treturn (
20006 \t\t\t
20007 \t\t<div>
20008 \t\t\t<abc></abc>
20009 \t\t</div>
20010 \t)
20011 }"
20012 .unindent(),
20013 cx,
20014 )
20015 .await;
20016
20017 assert_indent_guides(
20018 0..8,
20019 vec![
20020 indent_guide(buffer_id, 1, 6, 0),
20021 indent_guide(buffer_id, 2, 5, 1),
20022 indent_guide(buffer_id, 4, 4, 2),
20023 ],
20024 None,
20025 &mut cx,
20026 );
20027}
20028
20029#[gpui::test]
20030async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20031 let (buffer_id, mut cx) = setup_indent_guides_editor(
20032 &"
20033 function component() {
20034 \treturn (
20035 \t
20036 \t\t<div>
20037 \t\t\t<abc></abc>
20038 \t\t</div>
20039 \t)
20040 }"
20041 .unindent(),
20042 cx,
20043 )
20044 .await;
20045
20046 assert_indent_guides(
20047 0..8,
20048 vec![
20049 indent_guide(buffer_id, 1, 6, 0),
20050 indent_guide(buffer_id, 2, 5, 1),
20051 indent_guide(buffer_id, 4, 4, 2),
20052 ],
20053 None,
20054 &mut cx,
20055 );
20056}
20057
20058#[gpui::test]
20059async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20060 let (buffer_id, mut cx) = setup_indent_guides_editor(
20061 &"
20062 block1
20063
20064
20065
20066 block2
20067 "
20068 .unindent(),
20069 cx,
20070 )
20071 .await;
20072
20073 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20074}
20075
20076#[gpui::test]
20077async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20078 let (buffer_id, mut cx) = setup_indent_guides_editor(
20079 &"
20080 def a:
20081 \tb = 3
20082 \tif True:
20083 \t\tc = 4
20084 \t\td = 5
20085 \tprint(b)
20086 "
20087 .unindent(),
20088 cx,
20089 )
20090 .await;
20091
20092 assert_indent_guides(
20093 0..6,
20094 vec![
20095 indent_guide(buffer_id, 1, 5, 0),
20096 indent_guide(buffer_id, 3, 4, 1),
20097 ],
20098 None,
20099 &mut cx,
20100 );
20101}
20102
20103#[gpui::test]
20104async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20105 let (buffer_id, mut cx) = setup_indent_guides_editor(
20106 &"
20107 fn main() {
20108 let a = 1;
20109 }"
20110 .unindent(),
20111 cx,
20112 )
20113 .await;
20114
20115 cx.update_editor(|editor, window, cx| {
20116 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20117 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20118 });
20119 });
20120
20121 assert_indent_guides(
20122 0..3,
20123 vec![indent_guide(buffer_id, 1, 1, 0)],
20124 Some(vec![0]),
20125 &mut cx,
20126 );
20127}
20128
20129#[gpui::test]
20130async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20131 let (buffer_id, mut cx) = setup_indent_guides_editor(
20132 &"
20133 fn main() {
20134 if 1 == 2 {
20135 let a = 1;
20136 }
20137 }"
20138 .unindent(),
20139 cx,
20140 )
20141 .await;
20142
20143 cx.update_editor(|editor, window, cx| {
20144 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20145 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20146 });
20147 });
20148
20149 assert_indent_guides(
20150 0..4,
20151 vec![
20152 indent_guide(buffer_id, 1, 3, 0),
20153 indent_guide(buffer_id, 2, 2, 1),
20154 ],
20155 Some(vec![1]),
20156 &mut cx,
20157 );
20158
20159 cx.update_editor(|editor, window, cx| {
20160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20161 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20162 });
20163 });
20164
20165 assert_indent_guides(
20166 0..4,
20167 vec![
20168 indent_guide(buffer_id, 1, 3, 0),
20169 indent_guide(buffer_id, 2, 2, 1),
20170 ],
20171 Some(vec![1]),
20172 &mut cx,
20173 );
20174
20175 cx.update_editor(|editor, window, cx| {
20176 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20177 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20178 });
20179 });
20180
20181 assert_indent_guides(
20182 0..4,
20183 vec![
20184 indent_guide(buffer_id, 1, 3, 0),
20185 indent_guide(buffer_id, 2, 2, 1),
20186 ],
20187 Some(vec![0]),
20188 &mut cx,
20189 );
20190}
20191
20192#[gpui::test]
20193async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20194 let (buffer_id, mut cx) = setup_indent_guides_editor(
20195 &"
20196 fn main() {
20197 let a = 1;
20198
20199 let b = 2;
20200 }"
20201 .unindent(),
20202 cx,
20203 )
20204 .await;
20205
20206 cx.update_editor(|editor, window, cx| {
20207 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20208 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20209 });
20210 });
20211
20212 assert_indent_guides(
20213 0..5,
20214 vec![indent_guide(buffer_id, 1, 3, 0)],
20215 Some(vec![0]),
20216 &mut cx,
20217 );
20218}
20219
20220#[gpui::test]
20221async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20222 let (buffer_id, mut cx) = setup_indent_guides_editor(
20223 &"
20224 def m:
20225 a = 1
20226 pass"
20227 .unindent(),
20228 cx,
20229 )
20230 .await;
20231
20232 cx.update_editor(|editor, window, cx| {
20233 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20234 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20235 });
20236 });
20237
20238 assert_indent_guides(
20239 0..3,
20240 vec![indent_guide(buffer_id, 1, 2, 0)],
20241 Some(vec![0]),
20242 &mut cx,
20243 );
20244}
20245
20246#[gpui::test]
20247async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20248 init_test(cx, |_| {});
20249 let mut cx = EditorTestContext::new(cx).await;
20250 let text = indoc! {
20251 "
20252 impl A {
20253 fn b() {
20254 0;
20255 3;
20256 5;
20257 6;
20258 7;
20259 }
20260 }
20261 "
20262 };
20263 let base_text = indoc! {
20264 "
20265 impl A {
20266 fn b() {
20267 0;
20268 1;
20269 2;
20270 3;
20271 4;
20272 }
20273 fn c() {
20274 5;
20275 6;
20276 7;
20277 }
20278 }
20279 "
20280 };
20281
20282 cx.update_editor(|editor, window, cx| {
20283 editor.set_text(text, window, cx);
20284
20285 editor.buffer().update(cx, |multibuffer, cx| {
20286 let buffer = multibuffer.as_singleton().unwrap();
20287 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20288
20289 multibuffer.set_all_diff_hunks_expanded(cx);
20290 multibuffer.add_diff(diff, cx);
20291
20292 buffer.read(cx).remote_id()
20293 })
20294 });
20295 cx.run_until_parked();
20296
20297 cx.assert_state_with_diff(
20298 indoc! { "
20299 impl A {
20300 fn b() {
20301 0;
20302 - 1;
20303 - 2;
20304 3;
20305 - 4;
20306 - }
20307 - fn c() {
20308 5;
20309 6;
20310 7;
20311 }
20312 }
20313 ˇ"
20314 }
20315 .to_string(),
20316 );
20317
20318 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20319 editor
20320 .snapshot(window, cx)
20321 .buffer_snapshot
20322 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20323 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20324 .collect::<Vec<_>>()
20325 });
20326 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20327 assert_eq!(
20328 actual_guides,
20329 vec![
20330 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20331 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20332 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20333 ]
20334 );
20335}
20336
20337#[gpui::test]
20338async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20339 init_test(cx, |_| {});
20340 let mut cx = EditorTestContext::new(cx).await;
20341
20342 let diff_base = r#"
20343 a
20344 b
20345 c
20346 "#
20347 .unindent();
20348
20349 cx.set_state(
20350 &r#"
20351 ˇA
20352 b
20353 C
20354 "#
20355 .unindent(),
20356 );
20357 cx.set_head_text(&diff_base);
20358 cx.update_editor(|editor, window, cx| {
20359 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20360 });
20361 executor.run_until_parked();
20362
20363 let both_hunks_expanded = r#"
20364 - a
20365 + ˇA
20366 b
20367 - c
20368 + C
20369 "#
20370 .unindent();
20371
20372 cx.assert_state_with_diff(both_hunks_expanded.clone());
20373
20374 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20375 let snapshot = editor.snapshot(window, cx);
20376 let hunks = editor
20377 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20378 .collect::<Vec<_>>();
20379 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20380 let buffer_id = hunks[0].buffer_id;
20381 hunks
20382 .into_iter()
20383 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20384 .collect::<Vec<_>>()
20385 });
20386 assert_eq!(hunk_ranges.len(), 2);
20387
20388 cx.update_editor(|editor, _, cx| {
20389 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20390 });
20391 executor.run_until_parked();
20392
20393 let second_hunk_expanded = r#"
20394 ˇA
20395 b
20396 - c
20397 + C
20398 "#
20399 .unindent();
20400
20401 cx.assert_state_with_diff(second_hunk_expanded);
20402
20403 cx.update_editor(|editor, _, cx| {
20404 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20405 });
20406 executor.run_until_parked();
20407
20408 cx.assert_state_with_diff(both_hunks_expanded.clone());
20409
20410 cx.update_editor(|editor, _, cx| {
20411 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20412 });
20413 executor.run_until_parked();
20414
20415 let first_hunk_expanded = r#"
20416 - a
20417 + ˇA
20418 b
20419 C
20420 "#
20421 .unindent();
20422
20423 cx.assert_state_with_diff(first_hunk_expanded);
20424
20425 cx.update_editor(|editor, _, cx| {
20426 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20427 });
20428 executor.run_until_parked();
20429
20430 cx.assert_state_with_diff(both_hunks_expanded);
20431
20432 cx.set_state(
20433 &r#"
20434 ˇA
20435 b
20436 "#
20437 .unindent(),
20438 );
20439 cx.run_until_parked();
20440
20441 // TODO this cursor position seems bad
20442 cx.assert_state_with_diff(
20443 r#"
20444 - ˇa
20445 + A
20446 b
20447 "#
20448 .unindent(),
20449 );
20450
20451 cx.update_editor(|editor, window, cx| {
20452 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20453 });
20454
20455 cx.assert_state_with_diff(
20456 r#"
20457 - ˇa
20458 + A
20459 b
20460 - c
20461 "#
20462 .unindent(),
20463 );
20464
20465 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20466 let snapshot = editor.snapshot(window, cx);
20467 let hunks = editor
20468 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20469 .collect::<Vec<_>>();
20470 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20471 let buffer_id = hunks[0].buffer_id;
20472 hunks
20473 .into_iter()
20474 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20475 .collect::<Vec<_>>()
20476 });
20477 assert_eq!(hunk_ranges.len(), 2);
20478
20479 cx.update_editor(|editor, _, cx| {
20480 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20481 });
20482 executor.run_until_parked();
20483
20484 cx.assert_state_with_diff(
20485 r#"
20486 - ˇa
20487 + A
20488 b
20489 "#
20490 .unindent(),
20491 );
20492}
20493
20494#[gpui::test]
20495async fn test_toggle_deletion_hunk_at_start_of_file(
20496 executor: BackgroundExecutor,
20497 cx: &mut TestAppContext,
20498) {
20499 init_test(cx, |_| {});
20500 let mut cx = EditorTestContext::new(cx).await;
20501
20502 let diff_base = r#"
20503 a
20504 b
20505 c
20506 "#
20507 .unindent();
20508
20509 cx.set_state(
20510 &r#"
20511 ˇb
20512 c
20513 "#
20514 .unindent(),
20515 );
20516 cx.set_head_text(&diff_base);
20517 cx.update_editor(|editor, window, cx| {
20518 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20519 });
20520 executor.run_until_parked();
20521
20522 let hunk_expanded = r#"
20523 - a
20524 ˇb
20525 c
20526 "#
20527 .unindent();
20528
20529 cx.assert_state_with_diff(hunk_expanded.clone());
20530
20531 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20532 let snapshot = editor.snapshot(window, cx);
20533 let hunks = editor
20534 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20535 .collect::<Vec<_>>();
20536 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20537 let buffer_id = hunks[0].buffer_id;
20538 hunks
20539 .into_iter()
20540 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20541 .collect::<Vec<_>>()
20542 });
20543 assert_eq!(hunk_ranges.len(), 1);
20544
20545 cx.update_editor(|editor, _, cx| {
20546 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20547 });
20548 executor.run_until_parked();
20549
20550 let hunk_collapsed = r#"
20551 ˇb
20552 c
20553 "#
20554 .unindent();
20555
20556 cx.assert_state_with_diff(hunk_collapsed);
20557
20558 cx.update_editor(|editor, _, cx| {
20559 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20560 });
20561 executor.run_until_parked();
20562
20563 cx.assert_state_with_diff(hunk_expanded);
20564}
20565
20566#[gpui::test]
20567async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20568 init_test(cx, |_| {});
20569
20570 let fs = FakeFs::new(cx.executor());
20571 fs.insert_tree(
20572 path!("/test"),
20573 json!({
20574 ".git": {},
20575 "file-1": "ONE\n",
20576 "file-2": "TWO\n",
20577 "file-3": "THREE\n",
20578 }),
20579 )
20580 .await;
20581
20582 fs.set_head_for_repo(
20583 path!("/test/.git").as_ref(),
20584 &[
20585 ("file-1".into(), "one\n".into()),
20586 ("file-2".into(), "two\n".into()),
20587 ("file-3".into(), "three\n".into()),
20588 ],
20589 "deadbeef",
20590 );
20591
20592 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20593 let mut buffers = vec![];
20594 for i in 1..=3 {
20595 let buffer = project
20596 .update(cx, |project, cx| {
20597 let path = format!(path!("/test/file-{}"), i);
20598 project.open_local_buffer(path, cx)
20599 })
20600 .await
20601 .unwrap();
20602 buffers.push(buffer);
20603 }
20604
20605 let multibuffer = cx.new(|cx| {
20606 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20607 multibuffer.set_all_diff_hunks_expanded(cx);
20608 for buffer in &buffers {
20609 let snapshot = buffer.read(cx).snapshot();
20610 multibuffer.set_excerpts_for_path(
20611 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20612 buffer.clone(),
20613 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20614 2,
20615 cx,
20616 );
20617 }
20618 multibuffer
20619 });
20620
20621 let editor = cx.add_window(|window, cx| {
20622 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20623 });
20624 cx.run_until_parked();
20625
20626 let snapshot = editor
20627 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20628 .unwrap();
20629 let hunks = snapshot
20630 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20631 .map(|hunk| match hunk {
20632 DisplayDiffHunk::Unfolded {
20633 display_row_range, ..
20634 } => display_row_range,
20635 DisplayDiffHunk::Folded { .. } => unreachable!(),
20636 })
20637 .collect::<Vec<_>>();
20638 assert_eq!(
20639 hunks,
20640 [
20641 DisplayRow(2)..DisplayRow(4),
20642 DisplayRow(7)..DisplayRow(9),
20643 DisplayRow(12)..DisplayRow(14),
20644 ]
20645 );
20646}
20647
20648#[gpui::test]
20649async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20650 init_test(cx, |_| {});
20651
20652 let mut cx = EditorTestContext::new(cx).await;
20653 cx.set_head_text(indoc! { "
20654 one
20655 two
20656 three
20657 four
20658 five
20659 "
20660 });
20661 cx.set_index_text(indoc! { "
20662 one
20663 two
20664 three
20665 four
20666 five
20667 "
20668 });
20669 cx.set_state(indoc! {"
20670 one
20671 TWO
20672 ˇTHREE
20673 FOUR
20674 five
20675 "});
20676 cx.run_until_parked();
20677 cx.update_editor(|editor, window, cx| {
20678 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20679 });
20680 cx.run_until_parked();
20681 cx.assert_index_text(Some(indoc! {"
20682 one
20683 TWO
20684 THREE
20685 FOUR
20686 five
20687 "}));
20688 cx.set_state(indoc! { "
20689 one
20690 TWO
20691 ˇTHREE-HUNDRED
20692 FOUR
20693 five
20694 "});
20695 cx.run_until_parked();
20696 cx.update_editor(|editor, window, cx| {
20697 let snapshot = editor.snapshot(window, cx);
20698 let hunks = editor
20699 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20700 .collect::<Vec<_>>();
20701 assert_eq!(hunks.len(), 1);
20702 assert_eq!(
20703 hunks[0].status(),
20704 DiffHunkStatus {
20705 kind: DiffHunkStatusKind::Modified,
20706 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
20707 }
20708 );
20709
20710 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20711 });
20712 cx.run_until_parked();
20713 cx.assert_index_text(Some(indoc! {"
20714 one
20715 TWO
20716 THREE-HUNDRED
20717 FOUR
20718 five
20719 "}));
20720}
20721
20722#[gpui::test]
20723fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
20724 init_test(cx, |_| {});
20725
20726 let editor = cx.add_window(|window, cx| {
20727 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
20728 build_editor(buffer, window, cx)
20729 });
20730
20731 let render_args = Arc::new(Mutex::new(None));
20732 let snapshot = editor
20733 .update(cx, |editor, window, cx| {
20734 let snapshot = editor.buffer().read(cx).snapshot(cx);
20735 let range =
20736 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
20737
20738 struct RenderArgs {
20739 row: MultiBufferRow,
20740 folded: bool,
20741 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
20742 }
20743
20744 let crease = Crease::inline(
20745 range,
20746 FoldPlaceholder::test(),
20747 {
20748 let toggle_callback = render_args.clone();
20749 move |row, folded, callback, _window, _cx| {
20750 *toggle_callback.lock() = Some(RenderArgs {
20751 row,
20752 folded,
20753 callback,
20754 });
20755 div()
20756 }
20757 },
20758 |_row, _folded, _window, _cx| div(),
20759 );
20760
20761 editor.insert_creases(Some(crease), cx);
20762 let snapshot = editor.snapshot(window, cx);
20763 let _div =
20764 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
20765 snapshot
20766 })
20767 .unwrap();
20768
20769 let render_args = render_args.lock().take().unwrap();
20770 assert_eq!(render_args.row, MultiBufferRow(1));
20771 assert!(!render_args.folded);
20772 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20773
20774 cx.update_window(*editor, |_, window, cx| {
20775 (render_args.callback)(true, window, cx)
20776 })
20777 .unwrap();
20778 let snapshot = editor
20779 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20780 .unwrap();
20781 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
20782
20783 cx.update_window(*editor, |_, window, cx| {
20784 (render_args.callback)(false, window, cx)
20785 })
20786 .unwrap();
20787 let snapshot = editor
20788 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20789 .unwrap();
20790 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
20791}
20792
20793#[gpui::test]
20794async fn test_input_text(cx: &mut TestAppContext) {
20795 init_test(cx, |_| {});
20796 let mut cx = EditorTestContext::new(cx).await;
20797
20798 cx.set_state(
20799 &r#"ˇone
20800 two
20801
20802 three
20803 fourˇ
20804 five
20805
20806 siˇx"#
20807 .unindent(),
20808 );
20809
20810 cx.dispatch_action(HandleInput(String::new()));
20811 cx.assert_editor_state(
20812 &r#"ˇone
20813 two
20814
20815 three
20816 fourˇ
20817 five
20818
20819 siˇx"#
20820 .unindent(),
20821 );
20822
20823 cx.dispatch_action(HandleInput("AAAA".to_string()));
20824 cx.assert_editor_state(
20825 &r#"AAAAˇone
20826 two
20827
20828 three
20829 fourAAAAˇ
20830 five
20831
20832 siAAAAˇx"#
20833 .unindent(),
20834 );
20835}
20836
20837#[gpui::test]
20838async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
20839 init_test(cx, |_| {});
20840
20841 let mut cx = EditorTestContext::new(cx).await;
20842 cx.set_state(
20843 r#"let foo = 1;
20844let foo = 2;
20845let foo = 3;
20846let fooˇ = 4;
20847let foo = 5;
20848let foo = 6;
20849let foo = 7;
20850let foo = 8;
20851let foo = 9;
20852let foo = 10;
20853let foo = 11;
20854let foo = 12;
20855let foo = 13;
20856let foo = 14;
20857let foo = 15;"#,
20858 );
20859
20860 cx.update_editor(|e, window, cx| {
20861 assert_eq!(
20862 e.next_scroll_position,
20863 NextScrollCursorCenterTopBottom::Center,
20864 "Default next scroll direction is center",
20865 );
20866
20867 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20868 assert_eq!(
20869 e.next_scroll_position,
20870 NextScrollCursorCenterTopBottom::Top,
20871 "After center, next scroll direction should be top",
20872 );
20873
20874 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20875 assert_eq!(
20876 e.next_scroll_position,
20877 NextScrollCursorCenterTopBottom::Bottom,
20878 "After top, next scroll direction should be bottom",
20879 );
20880
20881 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20882 assert_eq!(
20883 e.next_scroll_position,
20884 NextScrollCursorCenterTopBottom::Center,
20885 "After bottom, scrolling should start over",
20886 );
20887
20888 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20889 assert_eq!(
20890 e.next_scroll_position,
20891 NextScrollCursorCenterTopBottom::Top,
20892 "Scrolling continues if retriggered fast enough"
20893 );
20894 });
20895
20896 cx.executor()
20897 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
20898 cx.executor().run_until_parked();
20899 cx.update_editor(|e, _, _| {
20900 assert_eq!(
20901 e.next_scroll_position,
20902 NextScrollCursorCenterTopBottom::Center,
20903 "If scrolling is not triggered fast enough, it should reset"
20904 );
20905 });
20906}
20907
20908#[gpui::test]
20909async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
20910 init_test(cx, |_| {});
20911 let mut cx = EditorLspTestContext::new_rust(
20912 lsp::ServerCapabilities {
20913 definition_provider: Some(lsp::OneOf::Left(true)),
20914 references_provider: Some(lsp::OneOf::Left(true)),
20915 ..lsp::ServerCapabilities::default()
20916 },
20917 cx,
20918 )
20919 .await;
20920
20921 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
20922 let go_to_definition = cx
20923 .lsp
20924 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20925 move |params, _| async move {
20926 if empty_go_to_definition {
20927 Ok(None)
20928 } else {
20929 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20930 uri: params.text_document_position_params.text_document.uri,
20931 range: lsp::Range::new(
20932 lsp::Position::new(4, 3),
20933 lsp::Position::new(4, 6),
20934 ),
20935 })))
20936 }
20937 },
20938 );
20939 let references = cx
20940 .lsp
20941 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20942 Ok(Some(vec![lsp::Location {
20943 uri: params.text_document_position.text_document.uri,
20944 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20945 }]))
20946 });
20947 (go_to_definition, references)
20948 };
20949
20950 cx.set_state(
20951 &r#"fn one() {
20952 let mut a = ˇtwo();
20953 }
20954
20955 fn two() {}"#
20956 .unindent(),
20957 );
20958 set_up_lsp_handlers(false, &mut cx);
20959 let navigated = cx
20960 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20961 .await
20962 .expect("Failed to navigate to definition");
20963 assert_eq!(
20964 navigated,
20965 Navigated::Yes,
20966 "Should have navigated to definition from the GetDefinition response"
20967 );
20968 cx.assert_editor_state(
20969 &r#"fn one() {
20970 let mut a = two();
20971 }
20972
20973 fn «twoˇ»() {}"#
20974 .unindent(),
20975 );
20976
20977 let editors = cx.update_workspace(|workspace, _, cx| {
20978 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20979 });
20980 cx.update_editor(|_, _, test_editor_cx| {
20981 assert_eq!(
20982 editors.len(),
20983 1,
20984 "Initially, only one, test, editor should be open in the workspace"
20985 );
20986 assert_eq!(
20987 test_editor_cx.entity(),
20988 editors.last().expect("Asserted len is 1").clone()
20989 );
20990 });
20991
20992 set_up_lsp_handlers(true, &mut cx);
20993 let navigated = cx
20994 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20995 .await
20996 .expect("Failed to navigate to lookup references");
20997 assert_eq!(
20998 navigated,
20999 Navigated::Yes,
21000 "Should have navigated to references as a fallback after empty GoToDefinition response"
21001 );
21002 // We should not change the selections in the existing file,
21003 // if opening another milti buffer with the references
21004 cx.assert_editor_state(
21005 &r#"fn one() {
21006 let mut a = two();
21007 }
21008
21009 fn «twoˇ»() {}"#
21010 .unindent(),
21011 );
21012 let editors = cx.update_workspace(|workspace, _, cx| {
21013 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21014 });
21015 cx.update_editor(|_, _, test_editor_cx| {
21016 assert_eq!(
21017 editors.len(),
21018 2,
21019 "After falling back to references search, we open a new editor with the results"
21020 );
21021 let references_fallback_text = editors
21022 .into_iter()
21023 .find(|new_editor| *new_editor != test_editor_cx.entity())
21024 .expect("Should have one non-test editor now")
21025 .read(test_editor_cx)
21026 .text(test_editor_cx);
21027 assert_eq!(
21028 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21029 "Should use the range from the references response and not the GoToDefinition one"
21030 );
21031 });
21032}
21033
21034#[gpui::test]
21035async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21036 init_test(cx, |_| {});
21037 cx.update(|cx| {
21038 let mut editor_settings = EditorSettings::get_global(cx).clone();
21039 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21040 EditorSettings::override_global(editor_settings, cx);
21041 });
21042 let mut cx = EditorLspTestContext::new_rust(
21043 lsp::ServerCapabilities {
21044 definition_provider: Some(lsp::OneOf::Left(true)),
21045 references_provider: Some(lsp::OneOf::Left(true)),
21046 ..lsp::ServerCapabilities::default()
21047 },
21048 cx,
21049 )
21050 .await;
21051 let original_state = r#"fn one() {
21052 let mut a = ˇtwo();
21053 }
21054
21055 fn two() {}"#
21056 .unindent();
21057 cx.set_state(&original_state);
21058
21059 let mut go_to_definition = cx
21060 .lsp
21061 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21062 move |_, _| async move { Ok(None) },
21063 );
21064 let _references = cx
21065 .lsp
21066 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21067 panic!("Should not call for references with no go to definition fallback")
21068 });
21069
21070 let navigated = cx
21071 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21072 .await
21073 .expect("Failed to navigate to lookup references");
21074 go_to_definition
21075 .next()
21076 .await
21077 .expect("Should have called the go_to_definition handler");
21078
21079 assert_eq!(
21080 navigated,
21081 Navigated::No,
21082 "Should have navigated to references as a fallback after empty GoToDefinition response"
21083 );
21084 cx.assert_editor_state(&original_state);
21085 let editors = cx.update_workspace(|workspace, _, cx| {
21086 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21087 });
21088 cx.update_editor(|_, _, _| {
21089 assert_eq!(
21090 editors.len(),
21091 1,
21092 "After unsuccessful fallback, no other editor should have been opened"
21093 );
21094 });
21095}
21096
21097#[gpui::test]
21098async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21099 init_test(cx, |_| {});
21100
21101 let language = Arc::new(Language::new(
21102 LanguageConfig::default(),
21103 Some(tree_sitter_rust::LANGUAGE.into()),
21104 ));
21105
21106 let text = r#"
21107 #[cfg(test)]
21108 mod tests() {
21109 #[test]
21110 fn runnable_1() {
21111 let a = 1;
21112 }
21113
21114 #[test]
21115 fn runnable_2() {
21116 let a = 1;
21117 let b = 2;
21118 }
21119 }
21120 "#
21121 .unindent();
21122
21123 let fs = FakeFs::new(cx.executor());
21124 fs.insert_file("/file.rs", Default::default()).await;
21125
21126 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21127 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21128 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21129 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21130 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21131
21132 let editor = cx.new_window_entity(|window, cx| {
21133 Editor::new(
21134 EditorMode::full(),
21135 multi_buffer,
21136 Some(project.clone()),
21137 window,
21138 cx,
21139 )
21140 });
21141
21142 editor.update_in(cx, |editor, window, cx| {
21143 let snapshot = editor.buffer().read(cx).snapshot(cx);
21144 editor.tasks.insert(
21145 (buffer.read(cx).remote_id(), 3),
21146 RunnableTasks {
21147 templates: vec![],
21148 offset: snapshot.anchor_before(43),
21149 column: 0,
21150 extra_variables: HashMap::default(),
21151 context_range: BufferOffset(43)..BufferOffset(85),
21152 },
21153 );
21154 editor.tasks.insert(
21155 (buffer.read(cx).remote_id(), 8),
21156 RunnableTasks {
21157 templates: vec![],
21158 offset: snapshot.anchor_before(86),
21159 column: 0,
21160 extra_variables: HashMap::default(),
21161 context_range: BufferOffset(86)..BufferOffset(191),
21162 },
21163 );
21164
21165 // Test finding task when cursor is inside function body
21166 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21167 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21168 });
21169 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21170 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21171
21172 // Test finding task when cursor is on function name
21173 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21174 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21175 });
21176 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21177 assert_eq!(row, 8, "Should find task when cursor is on function name");
21178 });
21179}
21180
21181#[gpui::test]
21182async fn test_folding_buffers(cx: &mut TestAppContext) {
21183 init_test(cx, |_| {});
21184
21185 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21186 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21187 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21188
21189 let fs = FakeFs::new(cx.executor());
21190 fs.insert_tree(
21191 path!("/a"),
21192 json!({
21193 "first.rs": sample_text_1,
21194 "second.rs": sample_text_2,
21195 "third.rs": sample_text_3,
21196 }),
21197 )
21198 .await;
21199 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21200 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21201 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21202 let worktree = project.update(cx, |project, cx| {
21203 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21204 assert_eq!(worktrees.len(), 1);
21205 worktrees.pop().unwrap()
21206 });
21207 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21208
21209 let buffer_1 = project
21210 .update(cx, |project, cx| {
21211 project.open_buffer((worktree_id, "first.rs"), cx)
21212 })
21213 .await
21214 .unwrap();
21215 let buffer_2 = project
21216 .update(cx, |project, cx| {
21217 project.open_buffer((worktree_id, "second.rs"), cx)
21218 })
21219 .await
21220 .unwrap();
21221 let buffer_3 = project
21222 .update(cx, |project, cx| {
21223 project.open_buffer((worktree_id, "third.rs"), cx)
21224 })
21225 .await
21226 .unwrap();
21227
21228 let multi_buffer = cx.new(|cx| {
21229 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21230 multi_buffer.push_excerpts(
21231 buffer_1.clone(),
21232 [
21233 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21234 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21235 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21236 ],
21237 cx,
21238 );
21239 multi_buffer.push_excerpts(
21240 buffer_2.clone(),
21241 [
21242 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21243 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21244 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21245 ],
21246 cx,
21247 );
21248 multi_buffer.push_excerpts(
21249 buffer_3.clone(),
21250 [
21251 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21252 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21253 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21254 ],
21255 cx,
21256 );
21257 multi_buffer
21258 });
21259 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21260 Editor::new(
21261 EditorMode::full(),
21262 multi_buffer.clone(),
21263 Some(project.clone()),
21264 window,
21265 cx,
21266 )
21267 });
21268
21269 assert_eq!(
21270 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21271 "\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",
21272 );
21273
21274 multi_buffer_editor.update(cx, |editor, cx| {
21275 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21276 });
21277 assert_eq!(
21278 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21279 "\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",
21280 "After folding the first buffer, its text should not be displayed"
21281 );
21282
21283 multi_buffer_editor.update(cx, |editor, cx| {
21284 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21285 });
21286 assert_eq!(
21287 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21288 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21289 "After folding the second buffer, its text should not be displayed"
21290 );
21291
21292 multi_buffer_editor.update(cx, |editor, cx| {
21293 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21294 });
21295 assert_eq!(
21296 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21297 "\n\n\n\n\n",
21298 "After folding the third buffer, its text should not be displayed"
21299 );
21300
21301 // Emulate selection inside the fold logic, that should work
21302 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21303 editor
21304 .snapshot(window, cx)
21305 .next_line_boundary(Point::new(0, 4));
21306 });
21307
21308 multi_buffer_editor.update(cx, |editor, cx| {
21309 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21310 });
21311 assert_eq!(
21312 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21313 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21314 "After unfolding the second buffer, its text should be displayed"
21315 );
21316
21317 // Typing inside of buffer 1 causes that buffer to be unfolded.
21318 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21319 assert_eq!(
21320 multi_buffer
21321 .read(cx)
21322 .snapshot(cx)
21323 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21324 .collect::<String>(),
21325 "bbbb"
21326 );
21327 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21328 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21329 });
21330 editor.handle_input("B", window, cx);
21331 });
21332
21333 assert_eq!(
21334 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21335 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21336 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21337 );
21338
21339 multi_buffer_editor.update(cx, |editor, cx| {
21340 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21341 });
21342 assert_eq!(
21343 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21344 "\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",
21345 "After unfolding the all buffers, all original text should be displayed"
21346 );
21347}
21348
21349#[gpui::test]
21350async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21351 init_test(cx, |_| {});
21352
21353 let sample_text_1 = "1111\n2222\n3333".to_string();
21354 let sample_text_2 = "4444\n5555\n6666".to_string();
21355 let sample_text_3 = "7777\n8888\n9999".to_string();
21356
21357 let fs = FakeFs::new(cx.executor());
21358 fs.insert_tree(
21359 path!("/a"),
21360 json!({
21361 "first.rs": sample_text_1,
21362 "second.rs": sample_text_2,
21363 "third.rs": sample_text_3,
21364 }),
21365 )
21366 .await;
21367 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21368 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21369 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21370 let worktree = project.update(cx, |project, cx| {
21371 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21372 assert_eq!(worktrees.len(), 1);
21373 worktrees.pop().unwrap()
21374 });
21375 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21376
21377 let buffer_1 = project
21378 .update(cx, |project, cx| {
21379 project.open_buffer((worktree_id, "first.rs"), cx)
21380 })
21381 .await
21382 .unwrap();
21383 let buffer_2 = project
21384 .update(cx, |project, cx| {
21385 project.open_buffer((worktree_id, "second.rs"), cx)
21386 })
21387 .await
21388 .unwrap();
21389 let buffer_3 = project
21390 .update(cx, |project, cx| {
21391 project.open_buffer((worktree_id, "third.rs"), cx)
21392 })
21393 .await
21394 .unwrap();
21395
21396 let multi_buffer = cx.new(|cx| {
21397 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21398 multi_buffer.push_excerpts(
21399 buffer_1.clone(),
21400 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21401 cx,
21402 );
21403 multi_buffer.push_excerpts(
21404 buffer_2.clone(),
21405 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21406 cx,
21407 );
21408 multi_buffer.push_excerpts(
21409 buffer_3.clone(),
21410 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21411 cx,
21412 );
21413 multi_buffer
21414 });
21415
21416 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21417 Editor::new(
21418 EditorMode::full(),
21419 multi_buffer,
21420 Some(project.clone()),
21421 window,
21422 cx,
21423 )
21424 });
21425
21426 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21427 assert_eq!(
21428 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21429 full_text,
21430 );
21431
21432 multi_buffer_editor.update(cx, |editor, cx| {
21433 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21434 });
21435 assert_eq!(
21436 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21437 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21438 "After folding the first buffer, its text should not be displayed"
21439 );
21440
21441 multi_buffer_editor.update(cx, |editor, cx| {
21442 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21443 });
21444
21445 assert_eq!(
21446 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21447 "\n\n\n\n\n\n7777\n8888\n9999",
21448 "After folding the second buffer, its text should not be displayed"
21449 );
21450
21451 multi_buffer_editor.update(cx, |editor, cx| {
21452 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21453 });
21454 assert_eq!(
21455 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21456 "\n\n\n\n\n",
21457 "After folding the third buffer, its text should not be displayed"
21458 );
21459
21460 multi_buffer_editor.update(cx, |editor, cx| {
21461 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21462 });
21463 assert_eq!(
21464 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21465 "\n\n\n\n4444\n5555\n6666\n\n",
21466 "After unfolding the second buffer, its text should be displayed"
21467 );
21468
21469 multi_buffer_editor.update(cx, |editor, cx| {
21470 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21471 });
21472 assert_eq!(
21473 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21474 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21475 "After unfolding the first buffer, its text should be displayed"
21476 );
21477
21478 multi_buffer_editor.update(cx, |editor, cx| {
21479 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21480 });
21481 assert_eq!(
21482 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21483 full_text,
21484 "After unfolding all buffers, all original text should be displayed"
21485 );
21486}
21487
21488#[gpui::test]
21489async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21490 init_test(cx, |_| {});
21491
21492 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21493
21494 let fs = FakeFs::new(cx.executor());
21495 fs.insert_tree(
21496 path!("/a"),
21497 json!({
21498 "main.rs": sample_text,
21499 }),
21500 )
21501 .await;
21502 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21503 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21504 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21505 let worktree = project.update(cx, |project, cx| {
21506 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21507 assert_eq!(worktrees.len(), 1);
21508 worktrees.pop().unwrap()
21509 });
21510 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21511
21512 let buffer_1 = project
21513 .update(cx, |project, cx| {
21514 project.open_buffer((worktree_id, "main.rs"), cx)
21515 })
21516 .await
21517 .unwrap();
21518
21519 let multi_buffer = cx.new(|cx| {
21520 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21521 multi_buffer.push_excerpts(
21522 buffer_1.clone(),
21523 [ExcerptRange::new(
21524 Point::new(0, 0)
21525 ..Point::new(
21526 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21527 0,
21528 ),
21529 )],
21530 cx,
21531 );
21532 multi_buffer
21533 });
21534 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21535 Editor::new(
21536 EditorMode::full(),
21537 multi_buffer,
21538 Some(project.clone()),
21539 window,
21540 cx,
21541 )
21542 });
21543
21544 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21545 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21546 enum TestHighlight {}
21547 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21548 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21549 editor.highlight_text::<TestHighlight>(
21550 vec![highlight_range.clone()],
21551 HighlightStyle::color(Hsla::green()),
21552 cx,
21553 );
21554 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21555 s.select_ranges(Some(highlight_range))
21556 });
21557 });
21558
21559 let full_text = format!("\n\n{sample_text}");
21560 assert_eq!(
21561 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21562 full_text,
21563 );
21564}
21565
21566#[gpui::test]
21567async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
21568 init_test(cx, |_| {});
21569 cx.update(|cx| {
21570 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
21571 "keymaps/default-linux.json",
21572 cx,
21573 )
21574 .unwrap();
21575 cx.bind_keys(default_key_bindings);
21576 });
21577
21578 let (editor, cx) = cx.add_window_view(|window, cx| {
21579 let multi_buffer = MultiBuffer::build_multi(
21580 [
21581 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
21582 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
21583 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
21584 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
21585 ],
21586 cx,
21587 );
21588 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
21589
21590 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
21591 // fold all but the second buffer, so that we test navigating between two
21592 // adjacent folded buffers, as well as folded buffers at the start and
21593 // end the multibuffer
21594 editor.fold_buffer(buffer_ids[0], cx);
21595 editor.fold_buffer(buffer_ids[2], cx);
21596 editor.fold_buffer(buffer_ids[3], cx);
21597
21598 editor
21599 });
21600 cx.simulate_resize(size(px(1000.), px(1000.)));
21601
21602 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
21603 cx.assert_excerpts_with_selections(indoc! {"
21604 [EXCERPT]
21605 ˇ[FOLDED]
21606 [EXCERPT]
21607 a1
21608 b1
21609 [EXCERPT]
21610 [FOLDED]
21611 [EXCERPT]
21612 [FOLDED]
21613 "
21614 });
21615 cx.simulate_keystroke("down");
21616 cx.assert_excerpts_with_selections(indoc! {"
21617 [EXCERPT]
21618 [FOLDED]
21619 [EXCERPT]
21620 ˇa1
21621 b1
21622 [EXCERPT]
21623 [FOLDED]
21624 [EXCERPT]
21625 [FOLDED]
21626 "
21627 });
21628 cx.simulate_keystroke("down");
21629 cx.assert_excerpts_with_selections(indoc! {"
21630 [EXCERPT]
21631 [FOLDED]
21632 [EXCERPT]
21633 a1
21634 ˇb1
21635 [EXCERPT]
21636 [FOLDED]
21637 [EXCERPT]
21638 [FOLDED]
21639 "
21640 });
21641 cx.simulate_keystroke("down");
21642 cx.assert_excerpts_with_selections(indoc! {"
21643 [EXCERPT]
21644 [FOLDED]
21645 [EXCERPT]
21646 a1
21647 b1
21648 ˇ[EXCERPT]
21649 [FOLDED]
21650 [EXCERPT]
21651 [FOLDED]
21652 "
21653 });
21654 cx.simulate_keystroke("down");
21655 cx.assert_excerpts_with_selections(indoc! {"
21656 [EXCERPT]
21657 [FOLDED]
21658 [EXCERPT]
21659 a1
21660 b1
21661 [EXCERPT]
21662 ˇ[FOLDED]
21663 [EXCERPT]
21664 [FOLDED]
21665 "
21666 });
21667 for _ in 0..5 {
21668 cx.simulate_keystroke("down");
21669 cx.assert_excerpts_with_selections(indoc! {"
21670 [EXCERPT]
21671 [FOLDED]
21672 [EXCERPT]
21673 a1
21674 b1
21675 [EXCERPT]
21676 [FOLDED]
21677 [EXCERPT]
21678 ˇ[FOLDED]
21679 "
21680 });
21681 }
21682
21683 cx.simulate_keystroke("up");
21684 cx.assert_excerpts_with_selections(indoc! {"
21685 [EXCERPT]
21686 [FOLDED]
21687 [EXCERPT]
21688 a1
21689 b1
21690 [EXCERPT]
21691 ˇ[FOLDED]
21692 [EXCERPT]
21693 [FOLDED]
21694 "
21695 });
21696 cx.simulate_keystroke("up");
21697 cx.assert_excerpts_with_selections(indoc! {"
21698 [EXCERPT]
21699 [FOLDED]
21700 [EXCERPT]
21701 a1
21702 b1
21703 ˇ[EXCERPT]
21704 [FOLDED]
21705 [EXCERPT]
21706 [FOLDED]
21707 "
21708 });
21709 cx.simulate_keystroke("up");
21710 cx.assert_excerpts_with_selections(indoc! {"
21711 [EXCERPT]
21712 [FOLDED]
21713 [EXCERPT]
21714 a1
21715 ˇb1
21716 [EXCERPT]
21717 [FOLDED]
21718 [EXCERPT]
21719 [FOLDED]
21720 "
21721 });
21722 cx.simulate_keystroke("up");
21723 cx.assert_excerpts_with_selections(indoc! {"
21724 [EXCERPT]
21725 [FOLDED]
21726 [EXCERPT]
21727 ˇa1
21728 b1
21729 [EXCERPT]
21730 [FOLDED]
21731 [EXCERPT]
21732 [FOLDED]
21733 "
21734 });
21735 for _ in 0..5 {
21736 cx.simulate_keystroke("up");
21737 cx.assert_excerpts_with_selections(indoc! {"
21738 [EXCERPT]
21739 ˇ[FOLDED]
21740 [EXCERPT]
21741 a1
21742 b1
21743 [EXCERPT]
21744 [FOLDED]
21745 [EXCERPT]
21746 [FOLDED]
21747 "
21748 });
21749 }
21750}
21751
21752#[gpui::test]
21753async fn test_edit_prediction_text(cx: &mut TestAppContext) {
21754 init_test(cx, |_| {});
21755
21756 // Simple insertion
21757 assert_highlighted_edits(
21758 "Hello, world!",
21759 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
21760 true,
21761 cx,
21762 |highlighted_edits, cx| {
21763 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
21764 assert_eq!(highlighted_edits.highlights.len(), 1);
21765 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
21766 assert_eq!(
21767 highlighted_edits.highlights[0].1.background_color,
21768 Some(cx.theme().status().created_background)
21769 );
21770 },
21771 )
21772 .await;
21773
21774 // Replacement
21775 assert_highlighted_edits(
21776 "This is a test.",
21777 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
21778 false,
21779 cx,
21780 |highlighted_edits, cx| {
21781 assert_eq!(highlighted_edits.text, "That is a test.");
21782 assert_eq!(highlighted_edits.highlights.len(), 1);
21783 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
21784 assert_eq!(
21785 highlighted_edits.highlights[0].1.background_color,
21786 Some(cx.theme().status().created_background)
21787 );
21788 },
21789 )
21790 .await;
21791
21792 // Multiple edits
21793 assert_highlighted_edits(
21794 "Hello, world!",
21795 vec![
21796 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
21797 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
21798 ],
21799 false,
21800 cx,
21801 |highlighted_edits, cx| {
21802 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
21803 assert_eq!(highlighted_edits.highlights.len(), 2);
21804 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
21805 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
21806 assert_eq!(
21807 highlighted_edits.highlights[0].1.background_color,
21808 Some(cx.theme().status().created_background)
21809 );
21810 assert_eq!(
21811 highlighted_edits.highlights[1].1.background_color,
21812 Some(cx.theme().status().created_background)
21813 );
21814 },
21815 )
21816 .await;
21817
21818 // Multiple lines with edits
21819 assert_highlighted_edits(
21820 "First line\nSecond line\nThird line\nFourth line",
21821 vec![
21822 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
21823 (
21824 Point::new(2, 0)..Point::new(2, 10),
21825 "New third line".to_string(),
21826 ),
21827 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
21828 ],
21829 false,
21830 cx,
21831 |highlighted_edits, cx| {
21832 assert_eq!(
21833 highlighted_edits.text,
21834 "Second modified\nNew third line\nFourth updated line"
21835 );
21836 assert_eq!(highlighted_edits.highlights.len(), 3);
21837 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
21838 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
21839 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
21840 for highlight in &highlighted_edits.highlights {
21841 assert_eq!(
21842 highlight.1.background_color,
21843 Some(cx.theme().status().created_background)
21844 );
21845 }
21846 },
21847 )
21848 .await;
21849}
21850
21851#[gpui::test]
21852async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
21853 init_test(cx, |_| {});
21854
21855 // Deletion
21856 assert_highlighted_edits(
21857 "Hello, world!",
21858 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
21859 true,
21860 cx,
21861 |highlighted_edits, cx| {
21862 assert_eq!(highlighted_edits.text, "Hello, world!");
21863 assert_eq!(highlighted_edits.highlights.len(), 1);
21864 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
21865 assert_eq!(
21866 highlighted_edits.highlights[0].1.background_color,
21867 Some(cx.theme().status().deleted_background)
21868 );
21869 },
21870 )
21871 .await;
21872
21873 // Insertion
21874 assert_highlighted_edits(
21875 "Hello, world!",
21876 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
21877 true,
21878 cx,
21879 |highlighted_edits, cx| {
21880 assert_eq!(highlighted_edits.highlights.len(), 1);
21881 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
21882 assert_eq!(
21883 highlighted_edits.highlights[0].1.background_color,
21884 Some(cx.theme().status().created_background)
21885 );
21886 },
21887 )
21888 .await;
21889}
21890
21891async fn assert_highlighted_edits(
21892 text: &str,
21893 edits: Vec<(Range<Point>, String)>,
21894 include_deletions: bool,
21895 cx: &mut TestAppContext,
21896 assertion_fn: impl Fn(HighlightedText, &App),
21897) {
21898 let window = cx.add_window(|window, cx| {
21899 let buffer = MultiBuffer::build_simple(text, cx);
21900 Editor::new(EditorMode::full(), buffer, None, window, cx)
21901 });
21902 let cx = &mut VisualTestContext::from_window(*window, cx);
21903
21904 let (buffer, snapshot) = window
21905 .update(cx, |editor, _window, cx| {
21906 (
21907 editor.buffer().clone(),
21908 editor.buffer().read(cx).snapshot(cx),
21909 )
21910 })
21911 .unwrap();
21912
21913 let edits = edits
21914 .into_iter()
21915 .map(|(range, edit)| {
21916 (
21917 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21918 edit,
21919 )
21920 })
21921 .collect::<Vec<_>>();
21922
21923 let text_anchor_edits = edits
21924 .clone()
21925 .into_iter()
21926 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21927 .collect::<Vec<_>>();
21928
21929 let edit_preview = window
21930 .update(cx, |_, _window, cx| {
21931 buffer
21932 .read(cx)
21933 .as_singleton()
21934 .unwrap()
21935 .read(cx)
21936 .preview_edits(text_anchor_edits.into(), cx)
21937 })
21938 .unwrap()
21939 .await;
21940
21941 cx.update(|_window, cx| {
21942 let highlighted_edits = edit_prediction_edit_text(
21943 snapshot.as_singleton().unwrap().2,
21944 &edits,
21945 &edit_preview,
21946 include_deletions,
21947 cx,
21948 );
21949 assertion_fn(highlighted_edits, cx)
21950 });
21951}
21952
21953#[track_caller]
21954fn assert_breakpoint(
21955 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21956 path: &Arc<Path>,
21957 expected: Vec<(u32, Breakpoint)>,
21958) {
21959 if expected.is_empty() {
21960 assert!(!breakpoints.contains_key(path), "{}", path.display());
21961 } else {
21962 let mut breakpoint = breakpoints
21963 .get(path)
21964 .unwrap()
21965 .iter()
21966 .map(|breakpoint| {
21967 (
21968 breakpoint.row,
21969 Breakpoint {
21970 message: breakpoint.message.clone(),
21971 state: breakpoint.state,
21972 condition: breakpoint.condition.clone(),
21973 hit_condition: breakpoint.hit_condition.clone(),
21974 },
21975 )
21976 })
21977 .collect::<Vec<_>>();
21978
21979 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21980
21981 assert_eq!(expected, breakpoint);
21982 }
21983}
21984
21985fn add_log_breakpoint_at_cursor(
21986 editor: &mut Editor,
21987 log_message: &str,
21988 window: &mut Window,
21989 cx: &mut Context<Editor>,
21990) {
21991 let (anchor, bp) = editor
21992 .breakpoints_at_cursors(window, cx)
21993 .first()
21994 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
21995 .unwrap_or_else(|| {
21996 let cursor_position: Point = editor.selections.newest(cx).head();
21997
21998 let breakpoint_position = editor
21999 .snapshot(window, cx)
22000 .display_snapshot
22001 .buffer_snapshot
22002 .anchor_before(Point::new(cursor_position.row, 0));
22003
22004 (breakpoint_position, Breakpoint::new_log(log_message))
22005 });
22006
22007 editor.edit_breakpoint_at_anchor(
22008 anchor,
22009 bp,
22010 BreakpointEditAction::EditLogMessage(log_message.into()),
22011 cx,
22012 );
22013}
22014
22015#[gpui::test]
22016async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22017 init_test(cx, |_| {});
22018
22019 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22020 let fs = FakeFs::new(cx.executor());
22021 fs.insert_tree(
22022 path!("/a"),
22023 json!({
22024 "main.rs": sample_text,
22025 }),
22026 )
22027 .await;
22028 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22029 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22030 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22031
22032 let fs = FakeFs::new(cx.executor());
22033 fs.insert_tree(
22034 path!("/a"),
22035 json!({
22036 "main.rs": sample_text,
22037 }),
22038 )
22039 .await;
22040 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22041 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22042 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22043 let worktree_id = workspace
22044 .update(cx, |workspace, _window, cx| {
22045 workspace.project().update(cx, |project, cx| {
22046 project.worktrees(cx).next().unwrap().read(cx).id()
22047 })
22048 })
22049 .unwrap();
22050
22051 let buffer = project
22052 .update(cx, |project, cx| {
22053 project.open_buffer((worktree_id, "main.rs"), cx)
22054 })
22055 .await
22056 .unwrap();
22057
22058 let (editor, cx) = cx.add_window_view(|window, cx| {
22059 Editor::new(
22060 EditorMode::full(),
22061 MultiBuffer::build_from_buffer(buffer, cx),
22062 Some(project.clone()),
22063 window,
22064 cx,
22065 )
22066 });
22067
22068 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22069 let abs_path = project.read_with(cx, |project, cx| {
22070 project
22071 .absolute_path(&project_path, cx)
22072 .map(Arc::from)
22073 .unwrap()
22074 });
22075
22076 // assert we can add breakpoint on the first line
22077 editor.update_in(cx, |editor, window, cx| {
22078 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22079 editor.move_to_end(&MoveToEnd, window, cx);
22080 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22081 });
22082
22083 let breakpoints = editor.update(cx, |editor, cx| {
22084 editor
22085 .breakpoint_store()
22086 .as_ref()
22087 .unwrap()
22088 .read(cx)
22089 .all_source_breakpoints(cx)
22090 });
22091
22092 assert_eq!(1, breakpoints.len());
22093 assert_breakpoint(
22094 &breakpoints,
22095 &abs_path,
22096 vec![
22097 (0, Breakpoint::new_standard()),
22098 (3, Breakpoint::new_standard()),
22099 ],
22100 );
22101
22102 editor.update_in(cx, |editor, window, cx| {
22103 editor.move_to_beginning(&MoveToBeginning, window, cx);
22104 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22105 });
22106
22107 let breakpoints = editor.update(cx, |editor, cx| {
22108 editor
22109 .breakpoint_store()
22110 .as_ref()
22111 .unwrap()
22112 .read(cx)
22113 .all_source_breakpoints(cx)
22114 });
22115
22116 assert_eq!(1, breakpoints.len());
22117 assert_breakpoint(
22118 &breakpoints,
22119 &abs_path,
22120 vec![(3, Breakpoint::new_standard())],
22121 );
22122
22123 editor.update_in(cx, |editor, window, cx| {
22124 editor.move_to_end(&MoveToEnd, window, cx);
22125 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22126 });
22127
22128 let breakpoints = editor.update(cx, |editor, cx| {
22129 editor
22130 .breakpoint_store()
22131 .as_ref()
22132 .unwrap()
22133 .read(cx)
22134 .all_source_breakpoints(cx)
22135 });
22136
22137 assert_eq!(0, breakpoints.len());
22138 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22139}
22140
22141#[gpui::test]
22142async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22143 init_test(cx, |_| {});
22144
22145 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22146
22147 let fs = FakeFs::new(cx.executor());
22148 fs.insert_tree(
22149 path!("/a"),
22150 json!({
22151 "main.rs": sample_text,
22152 }),
22153 )
22154 .await;
22155 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22156 let (workspace, cx) =
22157 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22158
22159 let worktree_id = workspace.update(cx, |workspace, cx| {
22160 workspace.project().update(cx, |project, cx| {
22161 project.worktrees(cx).next().unwrap().read(cx).id()
22162 })
22163 });
22164
22165 let buffer = project
22166 .update(cx, |project, cx| {
22167 project.open_buffer((worktree_id, "main.rs"), cx)
22168 })
22169 .await
22170 .unwrap();
22171
22172 let (editor, cx) = cx.add_window_view(|window, cx| {
22173 Editor::new(
22174 EditorMode::full(),
22175 MultiBuffer::build_from_buffer(buffer, cx),
22176 Some(project.clone()),
22177 window,
22178 cx,
22179 )
22180 });
22181
22182 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22183 let abs_path = project.read_with(cx, |project, cx| {
22184 project
22185 .absolute_path(&project_path, cx)
22186 .map(Arc::from)
22187 .unwrap()
22188 });
22189
22190 editor.update_in(cx, |editor, window, cx| {
22191 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22192 });
22193
22194 let breakpoints = editor.update(cx, |editor, cx| {
22195 editor
22196 .breakpoint_store()
22197 .as_ref()
22198 .unwrap()
22199 .read(cx)
22200 .all_source_breakpoints(cx)
22201 });
22202
22203 assert_breakpoint(
22204 &breakpoints,
22205 &abs_path,
22206 vec![(0, Breakpoint::new_log("hello world"))],
22207 );
22208
22209 // Removing a log message from a log breakpoint should remove it
22210 editor.update_in(cx, |editor, window, cx| {
22211 add_log_breakpoint_at_cursor(editor, "", window, cx);
22212 });
22213
22214 let breakpoints = editor.update(cx, |editor, cx| {
22215 editor
22216 .breakpoint_store()
22217 .as_ref()
22218 .unwrap()
22219 .read(cx)
22220 .all_source_breakpoints(cx)
22221 });
22222
22223 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22224
22225 editor.update_in(cx, |editor, window, cx| {
22226 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22227 editor.move_to_end(&MoveToEnd, window, cx);
22228 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22229 // Not adding a log message to a standard breakpoint shouldn't remove it
22230 add_log_breakpoint_at_cursor(editor, "", window, cx);
22231 });
22232
22233 let breakpoints = editor.update(cx, |editor, cx| {
22234 editor
22235 .breakpoint_store()
22236 .as_ref()
22237 .unwrap()
22238 .read(cx)
22239 .all_source_breakpoints(cx)
22240 });
22241
22242 assert_breakpoint(
22243 &breakpoints,
22244 &abs_path,
22245 vec![
22246 (0, Breakpoint::new_standard()),
22247 (3, Breakpoint::new_standard()),
22248 ],
22249 );
22250
22251 editor.update_in(cx, |editor, window, cx| {
22252 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22253 });
22254
22255 let breakpoints = editor.update(cx, |editor, cx| {
22256 editor
22257 .breakpoint_store()
22258 .as_ref()
22259 .unwrap()
22260 .read(cx)
22261 .all_source_breakpoints(cx)
22262 });
22263
22264 assert_breakpoint(
22265 &breakpoints,
22266 &abs_path,
22267 vec![
22268 (0, Breakpoint::new_standard()),
22269 (3, Breakpoint::new_log("hello world")),
22270 ],
22271 );
22272
22273 editor.update_in(cx, |editor, window, cx| {
22274 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22275 });
22276
22277 let breakpoints = editor.update(cx, |editor, cx| {
22278 editor
22279 .breakpoint_store()
22280 .as_ref()
22281 .unwrap()
22282 .read(cx)
22283 .all_source_breakpoints(cx)
22284 });
22285
22286 assert_breakpoint(
22287 &breakpoints,
22288 &abs_path,
22289 vec![
22290 (0, Breakpoint::new_standard()),
22291 (3, Breakpoint::new_log("hello Earth!!")),
22292 ],
22293 );
22294}
22295
22296/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22297/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22298/// or when breakpoints were placed out of order. This tests for a regression too
22299#[gpui::test]
22300async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22301 init_test(cx, |_| {});
22302
22303 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22304 let fs = FakeFs::new(cx.executor());
22305 fs.insert_tree(
22306 path!("/a"),
22307 json!({
22308 "main.rs": sample_text,
22309 }),
22310 )
22311 .await;
22312 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22313 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22314 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22315
22316 let fs = FakeFs::new(cx.executor());
22317 fs.insert_tree(
22318 path!("/a"),
22319 json!({
22320 "main.rs": sample_text,
22321 }),
22322 )
22323 .await;
22324 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22325 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22326 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22327 let worktree_id = workspace
22328 .update(cx, |workspace, _window, cx| {
22329 workspace.project().update(cx, |project, cx| {
22330 project.worktrees(cx).next().unwrap().read(cx).id()
22331 })
22332 })
22333 .unwrap();
22334
22335 let buffer = project
22336 .update(cx, |project, cx| {
22337 project.open_buffer((worktree_id, "main.rs"), cx)
22338 })
22339 .await
22340 .unwrap();
22341
22342 let (editor, cx) = cx.add_window_view(|window, cx| {
22343 Editor::new(
22344 EditorMode::full(),
22345 MultiBuffer::build_from_buffer(buffer, cx),
22346 Some(project.clone()),
22347 window,
22348 cx,
22349 )
22350 });
22351
22352 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22353 let abs_path = project.read_with(cx, |project, cx| {
22354 project
22355 .absolute_path(&project_path, cx)
22356 .map(Arc::from)
22357 .unwrap()
22358 });
22359
22360 // assert we can add breakpoint on the first line
22361 editor.update_in(cx, |editor, window, cx| {
22362 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22363 editor.move_to_end(&MoveToEnd, window, cx);
22364 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22365 editor.move_up(&MoveUp, window, cx);
22366 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22367 });
22368
22369 let breakpoints = editor.update(cx, |editor, cx| {
22370 editor
22371 .breakpoint_store()
22372 .as_ref()
22373 .unwrap()
22374 .read(cx)
22375 .all_source_breakpoints(cx)
22376 });
22377
22378 assert_eq!(1, breakpoints.len());
22379 assert_breakpoint(
22380 &breakpoints,
22381 &abs_path,
22382 vec![
22383 (0, Breakpoint::new_standard()),
22384 (2, Breakpoint::new_standard()),
22385 (3, Breakpoint::new_standard()),
22386 ],
22387 );
22388
22389 editor.update_in(cx, |editor, window, cx| {
22390 editor.move_to_beginning(&MoveToBeginning, window, cx);
22391 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22392 editor.move_to_end(&MoveToEnd, window, cx);
22393 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22394 // Disabling a breakpoint that doesn't exist should do nothing
22395 editor.move_up(&MoveUp, window, cx);
22396 editor.move_up(&MoveUp, window, cx);
22397 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22398 });
22399
22400 let breakpoints = editor.update(cx, |editor, cx| {
22401 editor
22402 .breakpoint_store()
22403 .as_ref()
22404 .unwrap()
22405 .read(cx)
22406 .all_source_breakpoints(cx)
22407 });
22408
22409 let disable_breakpoint = {
22410 let mut bp = Breakpoint::new_standard();
22411 bp.state = BreakpointState::Disabled;
22412 bp
22413 };
22414
22415 assert_eq!(1, breakpoints.len());
22416 assert_breakpoint(
22417 &breakpoints,
22418 &abs_path,
22419 vec![
22420 (0, disable_breakpoint.clone()),
22421 (2, Breakpoint::new_standard()),
22422 (3, disable_breakpoint.clone()),
22423 ],
22424 );
22425
22426 editor.update_in(cx, |editor, window, cx| {
22427 editor.move_to_beginning(&MoveToBeginning, window, cx);
22428 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22429 editor.move_to_end(&MoveToEnd, window, cx);
22430 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22431 editor.move_up(&MoveUp, window, cx);
22432 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22433 });
22434
22435 let breakpoints = editor.update(cx, |editor, cx| {
22436 editor
22437 .breakpoint_store()
22438 .as_ref()
22439 .unwrap()
22440 .read(cx)
22441 .all_source_breakpoints(cx)
22442 });
22443
22444 assert_eq!(1, breakpoints.len());
22445 assert_breakpoint(
22446 &breakpoints,
22447 &abs_path,
22448 vec![
22449 (0, Breakpoint::new_standard()),
22450 (2, disable_breakpoint),
22451 (3, Breakpoint::new_standard()),
22452 ],
22453 );
22454}
22455
22456#[gpui::test]
22457async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22458 init_test(cx, |_| {});
22459 let capabilities = lsp::ServerCapabilities {
22460 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22461 prepare_provider: Some(true),
22462 work_done_progress_options: Default::default(),
22463 })),
22464 ..Default::default()
22465 };
22466 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22467
22468 cx.set_state(indoc! {"
22469 struct Fˇoo {}
22470 "});
22471
22472 cx.update_editor(|editor, _, cx| {
22473 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22474 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22475 editor.highlight_background::<DocumentHighlightRead>(
22476 &[highlight_range],
22477 |theme| theme.colors().editor_document_highlight_read_background,
22478 cx,
22479 );
22480 });
22481
22482 let mut prepare_rename_handler = cx
22483 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22484 move |_, _, _| async move {
22485 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22486 start: lsp::Position {
22487 line: 0,
22488 character: 7,
22489 },
22490 end: lsp::Position {
22491 line: 0,
22492 character: 10,
22493 },
22494 })))
22495 },
22496 );
22497 let prepare_rename_task = cx
22498 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22499 .expect("Prepare rename was not started");
22500 prepare_rename_handler.next().await.unwrap();
22501 prepare_rename_task.await.expect("Prepare rename failed");
22502
22503 let mut rename_handler =
22504 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22505 let edit = lsp::TextEdit {
22506 range: lsp::Range {
22507 start: lsp::Position {
22508 line: 0,
22509 character: 7,
22510 },
22511 end: lsp::Position {
22512 line: 0,
22513 character: 10,
22514 },
22515 },
22516 new_text: "FooRenamed".to_string(),
22517 };
22518 Ok(Some(lsp::WorkspaceEdit::new(
22519 // Specify the same edit twice
22520 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22521 )))
22522 });
22523 let rename_task = cx
22524 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22525 .expect("Confirm rename was not started");
22526 rename_handler.next().await.unwrap();
22527 rename_task.await.expect("Confirm rename failed");
22528 cx.run_until_parked();
22529
22530 // Despite two edits, only one is actually applied as those are identical
22531 cx.assert_editor_state(indoc! {"
22532 struct FooRenamedˇ {}
22533 "});
22534}
22535
22536#[gpui::test]
22537async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22538 init_test(cx, |_| {});
22539 // These capabilities indicate that the server does not support prepare rename.
22540 let capabilities = lsp::ServerCapabilities {
22541 rename_provider: Some(lsp::OneOf::Left(true)),
22542 ..Default::default()
22543 };
22544 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22545
22546 cx.set_state(indoc! {"
22547 struct Fˇoo {}
22548 "});
22549
22550 cx.update_editor(|editor, _window, cx| {
22551 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22552 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22553 editor.highlight_background::<DocumentHighlightRead>(
22554 &[highlight_range],
22555 |theme| theme.colors().editor_document_highlight_read_background,
22556 cx,
22557 );
22558 });
22559
22560 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22561 .expect("Prepare rename was not started")
22562 .await
22563 .expect("Prepare rename failed");
22564
22565 let mut rename_handler =
22566 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22567 let edit = lsp::TextEdit {
22568 range: lsp::Range {
22569 start: lsp::Position {
22570 line: 0,
22571 character: 7,
22572 },
22573 end: lsp::Position {
22574 line: 0,
22575 character: 10,
22576 },
22577 },
22578 new_text: "FooRenamed".to_string(),
22579 };
22580 Ok(Some(lsp::WorkspaceEdit::new(
22581 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
22582 )))
22583 });
22584 let rename_task = cx
22585 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22586 .expect("Confirm rename was not started");
22587 rename_handler.next().await.unwrap();
22588 rename_task.await.expect("Confirm rename failed");
22589 cx.run_until_parked();
22590
22591 // Correct range is renamed, as `surrounding_word` is used to find it.
22592 cx.assert_editor_state(indoc! {"
22593 struct FooRenamedˇ {}
22594 "});
22595}
22596
22597#[gpui::test]
22598async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
22599 init_test(cx, |_| {});
22600 let mut cx = EditorTestContext::new(cx).await;
22601
22602 let language = Arc::new(
22603 Language::new(
22604 LanguageConfig::default(),
22605 Some(tree_sitter_html::LANGUAGE.into()),
22606 )
22607 .with_brackets_query(
22608 r#"
22609 ("<" @open "/>" @close)
22610 ("</" @open ">" @close)
22611 ("<" @open ">" @close)
22612 ("\"" @open "\"" @close)
22613 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
22614 "#,
22615 )
22616 .unwrap(),
22617 );
22618 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22619
22620 cx.set_state(indoc! {"
22621 <span>ˇ</span>
22622 "});
22623 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22624 cx.assert_editor_state(indoc! {"
22625 <span>
22626 ˇ
22627 </span>
22628 "});
22629
22630 cx.set_state(indoc! {"
22631 <span><span></span>ˇ</span>
22632 "});
22633 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22634 cx.assert_editor_state(indoc! {"
22635 <span><span></span>
22636 ˇ</span>
22637 "});
22638
22639 cx.set_state(indoc! {"
22640 <span>ˇ
22641 </span>
22642 "});
22643 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
22644 cx.assert_editor_state(indoc! {"
22645 <span>
22646 ˇ
22647 </span>
22648 "});
22649}
22650
22651#[gpui::test(iterations = 10)]
22652async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
22653 init_test(cx, |_| {});
22654
22655 let fs = FakeFs::new(cx.executor());
22656 fs.insert_tree(
22657 path!("/dir"),
22658 json!({
22659 "a.ts": "a",
22660 }),
22661 )
22662 .await;
22663
22664 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
22665 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22666 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22667
22668 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22669 language_registry.add(Arc::new(Language::new(
22670 LanguageConfig {
22671 name: "TypeScript".into(),
22672 matcher: LanguageMatcher {
22673 path_suffixes: vec!["ts".to_string()],
22674 ..Default::default()
22675 },
22676 ..Default::default()
22677 },
22678 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
22679 )));
22680 let mut fake_language_servers = language_registry.register_fake_lsp(
22681 "TypeScript",
22682 FakeLspAdapter {
22683 capabilities: lsp::ServerCapabilities {
22684 code_lens_provider: Some(lsp::CodeLensOptions {
22685 resolve_provider: Some(true),
22686 }),
22687 execute_command_provider: Some(lsp::ExecuteCommandOptions {
22688 commands: vec!["_the/command".to_string()],
22689 ..lsp::ExecuteCommandOptions::default()
22690 }),
22691 ..lsp::ServerCapabilities::default()
22692 },
22693 ..FakeLspAdapter::default()
22694 },
22695 );
22696
22697 let editor = workspace
22698 .update(cx, |workspace, window, cx| {
22699 workspace.open_abs_path(
22700 PathBuf::from(path!("/dir/a.ts")),
22701 OpenOptions::default(),
22702 window,
22703 cx,
22704 )
22705 })
22706 .unwrap()
22707 .await
22708 .unwrap()
22709 .downcast::<Editor>()
22710 .unwrap();
22711 cx.executor().run_until_parked();
22712
22713 let fake_server = fake_language_servers.next().await.unwrap();
22714
22715 let buffer = editor.update(cx, |editor, cx| {
22716 editor
22717 .buffer()
22718 .read(cx)
22719 .as_singleton()
22720 .expect("have opened a single file by path")
22721 });
22722
22723 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
22724 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
22725 drop(buffer_snapshot);
22726 let actions = cx
22727 .update_window(*workspace, |_, window, cx| {
22728 project.code_actions(&buffer, anchor..anchor, window, cx)
22729 })
22730 .unwrap();
22731
22732 fake_server
22733 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22734 Ok(Some(vec![
22735 lsp::CodeLens {
22736 range: lsp::Range::default(),
22737 command: Some(lsp::Command {
22738 title: "Code lens command".to_owned(),
22739 command: "_the/command".to_owned(),
22740 arguments: None,
22741 }),
22742 data: None,
22743 },
22744 lsp::CodeLens {
22745 range: lsp::Range::default(),
22746 command: Some(lsp::Command {
22747 title: "Command not in capabilities".to_owned(),
22748 command: "not in capabilities".to_owned(),
22749 arguments: None,
22750 }),
22751 data: None,
22752 },
22753 lsp::CodeLens {
22754 range: lsp::Range {
22755 start: lsp::Position {
22756 line: 1,
22757 character: 1,
22758 },
22759 end: lsp::Position {
22760 line: 1,
22761 character: 1,
22762 },
22763 },
22764 command: Some(lsp::Command {
22765 title: "Command not in range".to_owned(),
22766 command: "_the/command".to_owned(),
22767 arguments: None,
22768 }),
22769 data: None,
22770 },
22771 ]))
22772 })
22773 .next()
22774 .await;
22775
22776 let actions = actions.await.unwrap();
22777 assert_eq!(
22778 actions.len(),
22779 1,
22780 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
22781 );
22782 let action = actions[0].clone();
22783 let apply = project.update(cx, |project, cx| {
22784 project.apply_code_action(buffer.clone(), action, true, cx)
22785 });
22786
22787 // Resolving the code action does not populate its edits. In absence of
22788 // edits, we must execute the given command.
22789 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
22790 |mut lens, _| async move {
22791 let lens_command = lens.command.as_mut().expect("should have a command");
22792 assert_eq!(lens_command.title, "Code lens command");
22793 lens_command.arguments = Some(vec![json!("the-argument")]);
22794 Ok(lens)
22795 },
22796 );
22797
22798 // While executing the command, the language server sends the editor
22799 // a `workspaceEdit` request.
22800 fake_server
22801 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
22802 let fake = fake_server.clone();
22803 move |params, _| {
22804 assert_eq!(params.command, "_the/command");
22805 let fake = fake.clone();
22806 async move {
22807 fake.server
22808 .request::<lsp::request::ApplyWorkspaceEdit>(
22809 lsp::ApplyWorkspaceEditParams {
22810 label: None,
22811 edit: lsp::WorkspaceEdit {
22812 changes: Some(
22813 [(
22814 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
22815 vec![lsp::TextEdit {
22816 range: lsp::Range::new(
22817 lsp::Position::new(0, 0),
22818 lsp::Position::new(0, 0),
22819 ),
22820 new_text: "X".into(),
22821 }],
22822 )]
22823 .into_iter()
22824 .collect(),
22825 ),
22826 ..lsp::WorkspaceEdit::default()
22827 },
22828 },
22829 )
22830 .await
22831 .into_response()
22832 .unwrap();
22833 Ok(Some(json!(null)))
22834 }
22835 }
22836 })
22837 .next()
22838 .await;
22839
22840 // Applying the code lens command returns a project transaction containing the edits
22841 // sent by the language server in its `workspaceEdit` request.
22842 let transaction = apply.await.unwrap();
22843 assert!(transaction.0.contains_key(&buffer));
22844 buffer.update(cx, |buffer, cx| {
22845 assert_eq!(buffer.text(), "Xa");
22846 buffer.undo(cx);
22847 assert_eq!(buffer.text(), "a");
22848 });
22849
22850 let actions_after_edits = cx
22851 .update_window(*workspace, |_, window, cx| {
22852 project.code_actions(&buffer, anchor..anchor, window, cx)
22853 })
22854 .unwrap()
22855 .await
22856 .unwrap();
22857 assert_eq!(
22858 actions, actions_after_edits,
22859 "For the same selection, same code lens actions should be returned"
22860 );
22861
22862 let _responses =
22863 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
22864 panic!("No more code lens requests are expected");
22865 });
22866 editor.update_in(cx, |editor, window, cx| {
22867 editor.select_all(&SelectAll, window, cx);
22868 });
22869 cx.executor().run_until_parked();
22870 let new_actions = cx
22871 .update_window(*workspace, |_, window, cx| {
22872 project.code_actions(&buffer, anchor..anchor, window, cx)
22873 })
22874 .unwrap()
22875 .await
22876 .unwrap();
22877 assert_eq!(
22878 actions, new_actions,
22879 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
22880 );
22881}
22882
22883#[gpui::test]
22884async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
22885 init_test(cx, |_| {});
22886
22887 let fs = FakeFs::new(cx.executor());
22888 let main_text = r#"fn main() {
22889println!("1");
22890println!("2");
22891println!("3");
22892println!("4");
22893println!("5");
22894}"#;
22895 let lib_text = "mod foo {}";
22896 fs.insert_tree(
22897 path!("/a"),
22898 json!({
22899 "lib.rs": lib_text,
22900 "main.rs": main_text,
22901 }),
22902 )
22903 .await;
22904
22905 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22906 let (workspace, cx) =
22907 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22908 let worktree_id = workspace.update(cx, |workspace, cx| {
22909 workspace.project().update(cx, |project, cx| {
22910 project.worktrees(cx).next().unwrap().read(cx).id()
22911 })
22912 });
22913
22914 let expected_ranges = vec![
22915 Point::new(0, 0)..Point::new(0, 0),
22916 Point::new(1, 0)..Point::new(1, 1),
22917 Point::new(2, 0)..Point::new(2, 2),
22918 Point::new(3, 0)..Point::new(3, 3),
22919 ];
22920
22921 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22922 let editor_1 = workspace
22923 .update_in(cx, |workspace, window, cx| {
22924 workspace.open_path(
22925 (worktree_id, "main.rs"),
22926 Some(pane_1.downgrade()),
22927 true,
22928 window,
22929 cx,
22930 )
22931 })
22932 .unwrap()
22933 .await
22934 .downcast::<Editor>()
22935 .unwrap();
22936 pane_1.update(cx, |pane, cx| {
22937 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22938 open_editor.update(cx, |editor, cx| {
22939 assert_eq!(
22940 editor.display_text(cx),
22941 main_text,
22942 "Original main.rs text on initial open",
22943 );
22944 assert_eq!(
22945 editor
22946 .selections
22947 .all::<Point>(cx)
22948 .into_iter()
22949 .map(|s| s.range())
22950 .collect::<Vec<_>>(),
22951 vec![Point::zero()..Point::zero()],
22952 "Default selections on initial open",
22953 );
22954 })
22955 });
22956 editor_1.update_in(cx, |editor, window, cx| {
22957 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22958 s.select_ranges(expected_ranges.clone());
22959 });
22960 });
22961
22962 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22963 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22964 });
22965 let editor_2 = workspace
22966 .update_in(cx, |workspace, window, cx| {
22967 workspace.open_path(
22968 (worktree_id, "main.rs"),
22969 Some(pane_2.downgrade()),
22970 true,
22971 window,
22972 cx,
22973 )
22974 })
22975 .unwrap()
22976 .await
22977 .downcast::<Editor>()
22978 .unwrap();
22979 pane_2.update(cx, |pane, cx| {
22980 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22981 open_editor.update(cx, |editor, cx| {
22982 assert_eq!(
22983 editor.display_text(cx),
22984 main_text,
22985 "Original main.rs text on initial open in another panel",
22986 );
22987 assert_eq!(
22988 editor
22989 .selections
22990 .all::<Point>(cx)
22991 .into_iter()
22992 .map(|s| s.range())
22993 .collect::<Vec<_>>(),
22994 vec![Point::zero()..Point::zero()],
22995 "Default selections on initial open in another panel",
22996 );
22997 })
22998 });
22999
23000 editor_2.update_in(cx, |editor, window, cx| {
23001 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23002 });
23003
23004 let _other_editor_1 = workspace
23005 .update_in(cx, |workspace, window, cx| {
23006 workspace.open_path(
23007 (worktree_id, "lib.rs"),
23008 Some(pane_1.downgrade()),
23009 true,
23010 window,
23011 cx,
23012 )
23013 })
23014 .unwrap()
23015 .await
23016 .downcast::<Editor>()
23017 .unwrap();
23018 pane_1
23019 .update_in(cx, |pane, window, cx| {
23020 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23021 })
23022 .await
23023 .unwrap();
23024 drop(editor_1);
23025 pane_1.update(cx, |pane, cx| {
23026 pane.active_item()
23027 .unwrap()
23028 .downcast::<Editor>()
23029 .unwrap()
23030 .update(cx, |editor, cx| {
23031 assert_eq!(
23032 editor.display_text(cx),
23033 lib_text,
23034 "Other file should be open and active",
23035 );
23036 });
23037 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23038 });
23039
23040 let _other_editor_2 = workspace
23041 .update_in(cx, |workspace, window, cx| {
23042 workspace.open_path(
23043 (worktree_id, "lib.rs"),
23044 Some(pane_2.downgrade()),
23045 true,
23046 window,
23047 cx,
23048 )
23049 })
23050 .unwrap()
23051 .await
23052 .downcast::<Editor>()
23053 .unwrap();
23054 pane_2
23055 .update_in(cx, |pane, window, cx| {
23056 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23057 })
23058 .await
23059 .unwrap();
23060 drop(editor_2);
23061 pane_2.update(cx, |pane, cx| {
23062 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23063 open_editor.update(cx, |editor, cx| {
23064 assert_eq!(
23065 editor.display_text(cx),
23066 lib_text,
23067 "Other file should be open and active in another panel too",
23068 );
23069 });
23070 assert_eq!(
23071 pane.items().count(),
23072 1,
23073 "No other editors should be open in another pane",
23074 );
23075 });
23076
23077 let _editor_1_reopened = workspace
23078 .update_in(cx, |workspace, window, cx| {
23079 workspace.open_path(
23080 (worktree_id, "main.rs"),
23081 Some(pane_1.downgrade()),
23082 true,
23083 window,
23084 cx,
23085 )
23086 })
23087 .unwrap()
23088 .await
23089 .downcast::<Editor>()
23090 .unwrap();
23091 let _editor_2_reopened = workspace
23092 .update_in(cx, |workspace, window, cx| {
23093 workspace.open_path(
23094 (worktree_id, "main.rs"),
23095 Some(pane_2.downgrade()),
23096 true,
23097 window,
23098 cx,
23099 )
23100 })
23101 .unwrap()
23102 .await
23103 .downcast::<Editor>()
23104 .unwrap();
23105 pane_1.update(cx, |pane, cx| {
23106 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23107 open_editor.update(cx, |editor, cx| {
23108 assert_eq!(
23109 editor.display_text(cx),
23110 main_text,
23111 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23112 );
23113 assert_eq!(
23114 editor
23115 .selections
23116 .all::<Point>(cx)
23117 .into_iter()
23118 .map(|s| s.range())
23119 .collect::<Vec<_>>(),
23120 expected_ranges,
23121 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23122 );
23123 })
23124 });
23125 pane_2.update(cx, |pane, cx| {
23126 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23127 open_editor.update(cx, |editor, cx| {
23128 assert_eq!(
23129 editor.display_text(cx),
23130 r#"fn main() {
23131⋯rintln!("1");
23132⋯intln!("2");
23133⋯ntln!("3");
23134println!("4");
23135println!("5");
23136}"#,
23137 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23138 );
23139 assert_eq!(
23140 editor
23141 .selections
23142 .all::<Point>(cx)
23143 .into_iter()
23144 .map(|s| s.range())
23145 .collect::<Vec<_>>(),
23146 vec![Point::zero()..Point::zero()],
23147 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23148 );
23149 })
23150 });
23151}
23152
23153#[gpui::test]
23154async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23155 init_test(cx, |_| {});
23156
23157 let fs = FakeFs::new(cx.executor());
23158 let main_text = r#"fn main() {
23159println!("1");
23160println!("2");
23161println!("3");
23162println!("4");
23163println!("5");
23164}"#;
23165 let lib_text = "mod foo {}";
23166 fs.insert_tree(
23167 path!("/a"),
23168 json!({
23169 "lib.rs": lib_text,
23170 "main.rs": main_text,
23171 }),
23172 )
23173 .await;
23174
23175 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23176 let (workspace, cx) =
23177 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23178 let worktree_id = workspace.update(cx, |workspace, cx| {
23179 workspace.project().update(cx, |project, cx| {
23180 project.worktrees(cx).next().unwrap().read(cx).id()
23181 })
23182 });
23183
23184 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23185 let editor = workspace
23186 .update_in(cx, |workspace, window, cx| {
23187 workspace.open_path(
23188 (worktree_id, "main.rs"),
23189 Some(pane.downgrade()),
23190 true,
23191 window,
23192 cx,
23193 )
23194 })
23195 .unwrap()
23196 .await
23197 .downcast::<Editor>()
23198 .unwrap();
23199 pane.update(cx, |pane, cx| {
23200 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23201 open_editor.update(cx, |editor, cx| {
23202 assert_eq!(
23203 editor.display_text(cx),
23204 main_text,
23205 "Original main.rs text on initial open",
23206 );
23207 })
23208 });
23209 editor.update_in(cx, |editor, window, cx| {
23210 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23211 });
23212
23213 cx.update_global(|store: &mut SettingsStore, cx| {
23214 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23215 s.restore_on_file_reopen = Some(false);
23216 });
23217 });
23218 editor.update_in(cx, |editor, window, cx| {
23219 editor.fold_ranges(
23220 vec![
23221 Point::new(1, 0)..Point::new(1, 1),
23222 Point::new(2, 0)..Point::new(2, 2),
23223 Point::new(3, 0)..Point::new(3, 3),
23224 ],
23225 false,
23226 window,
23227 cx,
23228 );
23229 });
23230 pane.update_in(cx, |pane, window, cx| {
23231 pane.close_all_items(&CloseAllItems::default(), window, cx)
23232 })
23233 .await
23234 .unwrap();
23235 pane.update(cx, |pane, _| {
23236 assert!(pane.active_item().is_none());
23237 });
23238 cx.update_global(|store: &mut SettingsStore, cx| {
23239 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
23240 s.restore_on_file_reopen = Some(true);
23241 });
23242 });
23243
23244 let _editor_reopened = workspace
23245 .update_in(cx, |workspace, window, cx| {
23246 workspace.open_path(
23247 (worktree_id, "main.rs"),
23248 Some(pane.downgrade()),
23249 true,
23250 window,
23251 cx,
23252 )
23253 })
23254 .unwrap()
23255 .await
23256 .downcast::<Editor>()
23257 .unwrap();
23258 pane.update(cx, |pane, cx| {
23259 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23260 open_editor.update(cx, |editor, cx| {
23261 assert_eq!(
23262 editor.display_text(cx),
23263 main_text,
23264 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23265 );
23266 })
23267 });
23268}
23269
23270#[gpui::test]
23271async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23272 struct EmptyModalView {
23273 focus_handle: gpui::FocusHandle,
23274 }
23275 impl EventEmitter<DismissEvent> for EmptyModalView {}
23276 impl Render for EmptyModalView {
23277 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23278 div()
23279 }
23280 }
23281 impl Focusable for EmptyModalView {
23282 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23283 self.focus_handle.clone()
23284 }
23285 }
23286 impl workspace::ModalView for EmptyModalView {}
23287 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23288 EmptyModalView {
23289 focus_handle: cx.focus_handle(),
23290 }
23291 }
23292
23293 init_test(cx, |_| {});
23294
23295 let fs = FakeFs::new(cx.executor());
23296 let project = Project::test(fs, [], cx).await;
23297 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23298 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23299 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23300 let editor = cx.new_window_entity(|window, cx| {
23301 Editor::new(
23302 EditorMode::full(),
23303 buffer,
23304 Some(project.clone()),
23305 window,
23306 cx,
23307 )
23308 });
23309 workspace
23310 .update(cx, |workspace, window, cx| {
23311 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23312 })
23313 .unwrap();
23314 editor.update_in(cx, |editor, window, cx| {
23315 editor.open_context_menu(&OpenContextMenu, window, cx);
23316 assert!(editor.mouse_context_menu.is_some());
23317 });
23318 workspace
23319 .update(cx, |workspace, window, cx| {
23320 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23321 })
23322 .unwrap();
23323 cx.read(|cx| {
23324 assert!(editor.read(cx).mouse_context_menu.is_none());
23325 });
23326}
23327
23328#[gpui::test]
23329async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23330 init_test(cx, |_| {});
23331
23332 let fs = FakeFs::new(cx.executor());
23333 fs.insert_file(path!("/file.html"), Default::default())
23334 .await;
23335
23336 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23337
23338 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23339 let html_language = Arc::new(Language::new(
23340 LanguageConfig {
23341 name: "HTML".into(),
23342 matcher: LanguageMatcher {
23343 path_suffixes: vec!["html".to_string()],
23344 ..LanguageMatcher::default()
23345 },
23346 brackets: BracketPairConfig {
23347 pairs: vec![BracketPair {
23348 start: "<".into(),
23349 end: ">".into(),
23350 close: true,
23351 ..Default::default()
23352 }],
23353 ..Default::default()
23354 },
23355 ..Default::default()
23356 },
23357 Some(tree_sitter_html::LANGUAGE.into()),
23358 ));
23359 language_registry.add(html_language);
23360 let mut fake_servers = language_registry.register_fake_lsp(
23361 "HTML",
23362 FakeLspAdapter {
23363 capabilities: lsp::ServerCapabilities {
23364 completion_provider: Some(lsp::CompletionOptions {
23365 resolve_provider: Some(true),
23366 ..Default::default()
23367 }),
23368 ..Default::default()
23369 },
23370 ..Default::default()
23371 },
23372 );
23373
23374 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23375 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23376
23377 let worktree_id = workspace
23378 .update(cx, |workspace, _window, cx| {
23379 workspace.project().update(cx, |project, cx| {
23380 project.worktrees(cx).next().unwrap().read(cx).id()
23381 })
23382 })
23383 .unwrap();
23384 project
23385 .update(cx, |project, cx| {
23386 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23387 })
23388 .await
23389 .unwrap();
23390 let editor = workspace
23391 .update(cx, |workspace, window, cx| {
23392 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23393 })
23394 .unwrap()
23395 .await
23396 .unwrap()
23397 .downcast::<Editor>()
23398 .unwrap();
23399
23400 let fake_server = fake_servers.next().await.unwrap();
23401 editor.update_in(cx, |editor, window, cx| {
23402 editor.set_text("<ad></ad>", window, cx);
23403 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23404 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23405 });
23406 let Some((buffer, _)) = editor
23407 .buffer
23408 .read(cx)
23409 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23410 else {
23411 panic!("Failed to get buffer for selection position");
23412 };
23413 let buffer = buffer.read(cx);
23414 let buffer_id = buffer.remote_id();
23415 let opening_range =
23416 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23417 let closing_range =
23418 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23419 let mut linked_ranges = HashMap::default();
23420 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23421 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23422 });
23423 let mut completion_handle =
23424 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23425 Ok(Some(lsp::CompletionResponse::Array(vec![
23426 lsp::CompletionItem {
23427 label: "head".to_string(),
23428 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23429 lsp::InsertReplaceEdit {
23430 new_text: "head".to_string(),
23431 insert: lsp::Range::new(
23432 lsp::Position::new(0, 1),
23433 lsp::Position::new(0, 3),
23434 ),
23435 replace: lsp::Range::new(
23436 lsp::Position::new(0, 1),
23437 lsp::Position::new(0, 3),
23438 ),
23439 },
23440 )),
23441 ..Default::default()
23442 },
23443 ])))
23444 });
23445 editor.update_in(cx, |editor, window, cx| {
23446 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23447 });
23448 cx.run_until_parked();
23449 completion_handle.next().await.unwrap();
23450 editor.update(cx, |editor, _| {
23451 assert!(
23452 editor.context_menu_visible(),
23453 "Completion menu should be visible"
23454 );
23455 });
23456 editor.update_in(cx, |editor, window, cx| {
23457 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23458 });
23459 cx.executor().run_until_parked();
23460 editor.update(cx, |editor, cx| {
23461 assert_eq!(editor.text(cx), "<head></head>");
23462 });
23463}
23464
23465#[gpui::test]
23466async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23467 init_test(cx, |_| {});
23468
23469 let fs = FakeFs::new(cx.executor());
23470 fs.insert_tree(
23471 path!("/root"),
23472 json!({
23473 "a": {
23474 "main.rs": "fn main() {}",
23475 },
23476 "foo": {
23477 "bar": {
23478 "external_file.rs": "pub mod external {}",
23479 }
23480 }
23481 }),
23482 )
23483 .await;
23484
23485 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23486 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23487 language_registry.add(rust_lang());
23488 let _fake_servers = language_registry.register_fake_lsp(
23489 "Rust",
23490 FakeLspAdapter {
23491 ..FakeLspAdapter::default()
23492 },
23493 );
23494 let (workspace, cx) =
23495 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23496 let worktree_id = workspace.update(cx, |workspace, cx| {
23497 workspace.project().update(cx, |project, cx| {
23498 project.worktrees(cx).next().unwrap().read(cx).id()
23499 })
23500 });
23501
23502 let assert_language_servers_count =
23503 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23504 project.update(cx, |project, cx| {
23505 let current = project
23506 .lsp_store()
23507 .read(cx)
23508 .as_local()
23509 .unwrap()
23510 .language_servers
23511 .len();
23512 assert_eq!(expected, current, "{context}");
23513 });
23514 };
23515
23516 assert_language_servers_count(
23517 0,
23518 "No servers should be running before any file is open",
23519 cx,
23520 );
23521 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23522 let main_editor = workspace
23523 .update_in(cx, |workspace, window, cx| {
23524 workspace.open_path(
23525 (worktree_id, "main.rs"),
23526 Some(pane.downgrade()),
23527 true,
23528 window,
23529 cx,
23530 )
23531 })
23532 .unwrap()
23533 .await
23534 .downcast::<Editor>()
23535 .unwrap();
23536 pane.update(cx, |pane, cx| {
23537 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23538 open_editor.update(cx, |editor, cx| {
23539 assert_eq!(
23540 editor.display_text(cx),
23541 "fn main() {}",
23542 "Original main.rs text on initial open",
23543 );
23544 });
23545 assert_eq!(open_editor, main_editor);
23546 });
23547 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
23548
23549 let external_editor = workspace
23550 .update_in(cx, |workspace, window, cx| {
23551 workspace.open_abs_path(
23552 PathBuf::from("/root/foo/bar/external_file.rs"),
23553 OpenOptions::default(),
23554 window,
23555 cx,
23556 )
23557 })
23558 .await
23559 .expect("opening external file")
23560 .downcast::<Editor>()
23561 .expect("downcasted external file's open element to editor");
23562 pane.update(cx, |pane, cx| {
23563 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23564 open_editor.update(cx, |editor, cx| {
23565 assert_eq!(
23566 editor.display_text(cx),
23567 "pub mod external {}",
23568 "External file is open now",
23569 );
23570 });
23571 assert_eq!(open_editor, external_editor);
23572 });
23573 assert_language_servers_count(
23574 1,
23575 "Second, external, *.rs file should join the existing server",
23576 cx,
23577 );
23578
23579 pane.update_in(cx, |pane, window, cx| {
23580 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23581 })
23582 .await
23583 .unwrap();
23584 pane.update_in(cx, |pane, window, cx| {
23585 pane.navigate_backward(&Default::default(), window, cx);
23586 });
23587 cx.run_until_parked();
23588 pane.update(cx, |pane, cx| {
23589 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23590 open_editor.update(cx, |editor, cx| {
23591 assert_eq!(
23592 editor.display_text(cx),
23593 "pub mod external {}",
23594 "External file is open now",
23595 );
23596 });
23597 });
23598 assert_language_servers_count(
23599 1,
23600 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
23601 cx,
23602 );
23603
23604 cx.update(|_, cx| {
23605 workspace::reload(cx);
23606 });
23607 assert_language_servers_count(
23608 1,
23609 "After reloading the worktree with local and external files opened, only one project should be started",
23610 cx,
23611 );
23612}
23613
23614#[gpui::test]
23615async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
23616 init_test(cx, |_| {});
23617
23618 let mut cx = EditorTestContext::new(cx).await;
23619 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23620 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23621
23622 // test cursor move to start of each line on tab
23623 // for `if`, `elif`, `else`, `while`, `with` and `for`
23624 cx.set_state(indoc! {"
23625 def main():
23626 ˇ for item in items:
23627 ˇ while item.active:
23628 ˇ if item.value > 10:
23629 ˇ continue
23630 ˇ elif item.value < 0:
23631 ˇ break
23632 ˇ else:
23633 ˇ with item.context() as ctx:
23634 ˇ yield count
23635 ˇ else:
23636 ˇ log('while else')
23637 ˇ else:
23638 ˇ log('for else')
23639 "});
23640 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23641 cx.assert_editor_state(indoc! {"
23642 def main():
23643 ˇfor item in items:
23644 ˇwhile item.active:
23645 ˇif item.value > 10:
23646 ˇcontinue
23647 ˇelif item.value < 0:
23648 ˇbreak
23649 ˇelse:
23650 ˇwith item.context() as ctx:
23651 ˇyield count
23652 ˇelse:
23653 ˇlog('while else')
23654 ˇelse:
23655 ˇlog('for else')
23656 "});
23657 // test relative indent is preserved when tab
23658 // for `if`, `elif`, `else`, `while`, `with` and `for`
23659 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23660 cx.assert_editor_state(indoc! {"
23661 def main():
23662 ˇfor item in items:
23663 ˇwhile item.active:
23664 ˇif item.value > 10:
23665 ˇcontinue
23666 ˇelif item.value < 0:
23667 ˇbreak
23668 ˇelse:
23669 ˇwith item.context() as ctx:
23670 ˇyield count
23671 ˇelse:
23672 ˇlog('while else')
23673 ˇelse:
23674 ˇlog('for else')
23675 "});
23676
23677 // test cursor move to start of each line on tab
23678 // for `try`, `except`, `else`, `finally`, `match` and `def`
23679 cx.set_state(indoc! {"
23680 def main():
23681 ˇ try:
23682 ˇ fetch()
23683 ˇ except ValueError:
23684 ˇ handle_error()
23685 ˇ else:
23686 ˇ match value:
23687 ˇ case _:
23688 ˇ finally:
23689 ˇ def status():
23690 ˇ return 0
23691 "});
23692 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23693 cx.assert_editor_state(indoc! {"
23694 def main():
23695 ˇtry:
23696 ˇfetch()
23697 ˇexcept ValueError:
23698 ˇhandle_error()
23699 ˇelse:
23700 ˇmatch value:
23701 ˇcase _:
23702 ˇfinally:
23703 ˇdef status():
23704 ˇreturn 0
23705 "});
23706 // test relative indent is preserved when tab
23707 // for `try`, `except`, `else`, `finally`, `match` and `def`
23708 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23709 cx.assert_editor_state(indoc! {"
23710 def main():
23711 ˇtry:
23712 ˇfetch()
23713 ˇexcept ValueError:
23714 ˇhandle_error()
23715 ˇelse:
23716 ˇmatch value:
23717 ˇcase _:
23718 ˇfinally:
23719 ˇdef status():
23720 ˇreturn 0
23721 "});
23722}
23723
23724#[gpui::test]
23725async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
23726 init_test(cx, |_| {});
23727
23728 let mut cx = EditorTestContext::new(cx).await;
23729 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23730 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23731
23732 // test `else` auto outdents when typed inside `if` block
23733 cx.set_state(indoc! {"
23734 def main():
23735 if i == 2:
23736 return
23737 ˇ
23738 "});
23739 cx.update_editor(|editor, window, cx| {
23740 editor.handle_input("else:", window, cx);
23741 });
23742 cx.assert_editor_state(indoc! {"
23743 def main():
23744 if i == 2:
23745 return
23746 else:ˇ
23747 "});
23748
23749 // test `except` auto outdents when typed inside `try` block
23750 cx.set_state(indoc! {"
23751 def main():
23752 try:
23753 i = 2
23754 ˇ
23755 "});
23756 cx.update_editor(|editor, window, cx| {
23757 editor.handle_input("except:", window, cx);
23758 });
23759 cx.assert_editor_state(indoc! {"
23760 def main():
23761 try:
23762 i = 2
23763 except:ˇ
23764 "});
23765
23766 // test `else` auto outdents when typed inside `except` block
23767 cx.set_state(indoc! {"
23768 def main():
23769 try:
23770 i = 2
23771 except:
23772 j = 2
23773 ˇ
23774 "});
23775 cx.update_editor(|editor, window, cx| {
23776 editor.handle_input("else:", window, cx);
23777 });
23778 cx.assert_editor_state(indoc! {"
23779 def main():
23780 try:
23781 i = 2
23782 except:
23783 j = 2
23784 else:ˇ
23785 "});
23786
23787 // test `finally` auto outdents when typed inside `else` block
23788 cx.set_state(indoc! {"
23789 def main():
23790 try:
23791 i = 2
23792 except:
23793 j = 2
23794 else:
23795 k = 2
23796 ˇ
23797 "});
23798 cx.update_editor(|editor, window, cx| {
23799 editor.handle_input("finally:", window, cx);
23800 });
23801 cx.assert_editor_state(indoc! {"
23802 def main():
23803 try:
23804 i = 2
23805 except:
23806 j = 2
23807 else:
23808 k = 2
23809 finally:ˇ
23810 "});
23811
23812 // test `else` does not outdents when typed inside `except` block right after for block
23813 cx.set_state(indoc! {"
23814 def main():
23815 try:
23816 i = 2
23817 except:
23818 for i in range(n):
23819 pass
23820 ˇ
23821 "});
23822 cx.update_editor(|editor, window, cx| {
23823 editor.handle_input("else:", window, cx);
23824 });
23825 cx.assert_editor_state(indoc! {"
23826 def main():
23827 try:
23828 i = 2
23829 except:
23830 for i in range(n):
23831 pass
23832 else:ˇ
23833 "});
23834
23835 // test `finally` auto outdents when typed inside `else` block right after for block
23836 cx.set_state(indoc! {"
23837 def main():
23838 try:
23839 i = 2
23840 except:
23841 j = 2
23842 else:
23843 for i in range(n):
23844 pass
23845 ˇ
23846 "});
23847 cx.update_editor(|editor, window, cx| {
23848 editor.handle_input("finally:", window, cx);
23849 });
23850 cx.assert_editor_state(indoc! {"
23851 def main():
23852 try:
23853 i = 2
23854 except:
23855 j = 2
23856 else:
23857 for i in range(n):
23858 pass
23859 finally:ˇ
23860 "});
23861
23862 // test `except` outdents to inner "try" block
23863 cx.set_state(indoc! {"
23864 def main():
23865 try:
23866 i = 2
23867 if i == 2:
23868 try:
23869 i = 3
23870 ˇ
23871 "});
23872 cx.update_editor(|editor, window, cx| {
23873 editor.handle_input("except:", window, cx);
23874 });
23875 cx.assert_editor_state(indoc! {"
23876 def main():
23877 try:
23878 i = 2
23879 if i == 2:
23880 try:
23881 i = 3
23882 except:ˇ
23883 "});
23884
23885 // test `except` outdents to outer "try" block
23886 cx.set_state(indoc! {"
23887 def main():
23888 try:
23889 i = 2
23890 if i == 2:
23891 try:
23892 i = 3
23893 ˇ
23894 "});
23895 cx.update_editor(|editor, window, cx| {
23896 editor.handle_input("except:", window, cx);
23897 });
23898 cx.assert_editor_state(indoc! {"
23899 def main():
23900 try:
23901 i = 2
23902 if i == 2:
23903 try:
23904 i = 3
23905 except:ˇ
23906 "});
23907
23908 // test `else` stays at correct indent when typed after `for` block
23909 cx.set_state(indoc! {"
23910 def main():
23911 for i in range(10):
23912 if i == 3:
23913 break
23914 ˇ
23915 "});
23916 cx.update_editor(|editor, window, cx| {
23917 editor.handle_input("else:", window, cx);
23918 });
23919 cx.assert_editor_state(indoc! {"
23920 def main():
23921 for i in range(10):
23922 if i == 3:
23923 break
23924 else:ˇ
23925 "});
23926
23927 // test does not outdent on typing after line with square brackets
23928 cx.set_state(indoc! {"
23929 def f() -> list[str]:
23930 ˇ
23931 "});
23932 cx.update_editor(|editor, window, cx| {
23933 editor.handle_input("a", window, cx);
23934 });
23935 cx.assert_editor_state(indoc! {"
23936 def f() -> list[str]:
23937 aˇ
23938 "});
23939
23940 // test does not outdent on typing : after case keyword
23941 cx.set_state(indoc! {"
23942 match 1:
23943 caseˇ
23944 "});
23945 cx.update_editor(|editor, window, cx| {
23946 editor.handle_input(":", window, cx);
23947 });
23948 cx.assert_editor_state(indoc! {"
23949 match 1:
23950 case:ˇ
23951 "});
23952}
23953
23954#[gpui::test]
23955async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23956 init_test(cx, |_| {});
23957 update_test_language_settings(cx, |settings| {
23958 settings.defaults.extend_comment_on_newline = Some(false);
23959 });
23960 let mut cx = EditorTestContext::new(cx).await;
23961 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23962 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23963
23964 // test correct indent after newline on comment
23965 cx.set_state(indoc! {"
23966 # COMMENT:ˇ
23967 "});
23968 cx.update_editor(|editor, window, cx| {
23969 editor.newline(&Newline, window, cx);
23970 });
23971 cx.assert_editor_state(indoc! {"
23972 # COMMENT:
23973 ˇ
23974 "});
23975
23976 // test correct indent after newline in brackets
23977 cx.set_state(indoc! {"
23978 {ˇ}
23979 "});
23980 cx.update_editor(|editor, window, cx| {
23981 editor.newline(&Newline, window, cx);
23982 });
23983 cx.run_until_parked();
23984 cx.assert_editor_state(indoc! {"
23985 {
23986 ˇ
23987 }
23988 "});
23989
23990 cx.set_state(indoc! {"
23991 (ˇ)
23992 "});
23993 cx.update_editor(|editor, window, cx| {
23994 editor.newline(&Newline, window, cx);
23995 });
23996 cx.run_until_parked();
23997 cx.assert_editor_state(indoc! {"
23998 (
23999 ˇ
24000 )
24001 "});
24002
24003 // do not indent after empty lists or dictionaries
24004 cx.set_state(indoc! {"
24005 a = []ˇ
24006 "});
24007 cx.update_editor(|editor, window, cx| {
24008 editor.newline(&Newline, window, cx);
24009 });
24010 cx.run_until_parked();
24011 cx.assert_editor_state(indoc! {"
24012 a = []
24013 ˇ
24014 "});
24015}
24016
24017#[gpui::test]
24018async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24019 init_test(cx, |_| {});
24020
24021 let mut cx = EditorTestContext::new(cx).await;
24022 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24023 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24024
24025 // test cursor move to start of each line on tab
24026 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24027 cx.set_state(indoc! {"
24028 function main() {
24029 ˇ for item in $items; do
24030 ˇ while [ -n \"$item\" ]; do
24031 ˇ if [ \"$value\" -gt 10 ]; then
24032 ˇ continue
24033 ˇ elif [ \"$value\" -lt 0 ]; then
24034 ˇ break
24035 ˇ else
24036 ˇ echo \"$item\"
24037 ˇ fi
24038 ˇ done
24039 ˇ done
24040 ˇ}
24041 "});
24042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24043 cx.assert_editor_state(indoc! {"
24044 function main() {
24045 ˇfor item in $items; do
24046 ˇwhile [ -n \"$item\" ]; do
24047 ˇif [ \"$value\" -gt 10 ]; then
24048 ˇcontinue
24049 ˇelif [ \"$value\" -lt 0 ]; then
24050 ˇbreak
24051 ˇelse
24052 ˇecho \"$item\"
24053 ˇfi
24054 ˇdone
24055 ˇdone
24056 ˇ}
24057 "});
24058 // test relative indent is preserved when tab
24059 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24060 cx.assert_editor_state(indoc! {"
24061 function main() {
24062 ˇfor item in $items; do
24063 ˇwhile [ -n \"$item\" ]; do
24064 ˇif [ \"$value\" -gt 10 ]; then
24065 ˇcontinue
24066 ˇelif [ \"$value\" -lt 0 ]; then
24067 ˇbreak
24068 ˇelse
24069 ˇecho \"$item\"
24070 ˇfi
24071 ˇdone
24072 ˇdone
24073 ˇ}
24074 "});
24075
24076 // test cursor move to start of each line on tab
24077 // for `case` statement with patterns
24078 cx.set_state(indoc! {"
24079 function handle() {
24080 ˇ case \"$1\" in
24081 ˇ start)
24082 ˇ echo \"a\"
24083 ˇ ;;
24084 ˇ stop)
24085 ˇ echo \"b\"
24086 ˇ ;;
24087 ˇ *)
24088 ˇ echo \"c\"
24089 ˇ ;;
24090 ˇ esac
24091 ˇ}
24092 "});
24093 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24094 cx.assert_editor_state(indoc! {"
24095 function handle() {
24096 ˇcase \"$1\" in
24097 ˇstart)
24098 ˇecho \"a\"
24099 ˇ;;
24100 ˇstop)
24101 ˇecho \"b\"
24102 ˇ;;
24103 ˇ*)
24104 ˇecho \"c\"
24105 ˇ;;
24106 ˇesac
24107 ˇ}
24108 "});
24109}
24110
24111#[gpui::test]
24112async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24113 init_test(cx, |_| {});
24114
24115 let mut cx = EditorTestContext::new(cx).await;
24116 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24117 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24118
24119 // test indents on comment insert
24120 cx.set_state(indoc! {"
24121 function main() {
24122 ˇ for item in $items; do
24123 ˇ while [ -n \"$item\" ]; do
24124 ˇ if [ \"$value\" -gt 10 ]; then
24125 ˇ continue
24126 ˇ elif [ \"$value\" -lt 0 ]; then
24127 ˇ break
24128 ˇ else
24129 ˇ echo \"$item\"
24130 ˇ fi
24131 ˇ done
24132 ˇ done
24133 ˇ}
24134 "});
24135 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24136 cx.assert_editor_state(indoc! {"
24137 function main() {
24138 #ˇ for item in $items; do
24139 #ˇ while [ -n \"$item\" ]; do
24140 #ˇ if [ \"$value\" -gt 10 ]; then
24141 #ˇ continue
24142 #ˇ elif [ \"$value\" -lt 0 ]; then
24143 #ˇ break
24144 #ˇ else
24145 #ˇ echo \"$item\"
24146 #ˇ fi
24147 #ˇ done
24148 #ˇ done
24149 #ˇ}
24150 "});
24151}
24152
24153#[gpui::test]
24154async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24155 init_test(cx, |_| {});
24156
24157 let mut cx = EditorTestContext::new(cx).await;
24158 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24159 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24160
24161 // test `else` auto outdents when typed inside `if` block
24162 cx.set_state(indoc! {"
24163 if [ \"$1\" = \"test\" ]; then
24164 echo \"foo bar\"
24165 ˇ
24166 "});
24167 cx.update_editor(|editor, window, cx| {
24168 editor.handle_input("else", window, cx);
24169 });
24170 cx.assert_editor_state(indoc! {"
24171 if [ \"$1\" = \"test\" ]; then
24172 echo \"foo bar\"
24173 elseˇ
24174 "});
24175
24176 // test `elif` auto outdents when typed inside `if` block
24177 cx.set_state(indoc! {"
24178 if [ \"$1\" = \"test\" ]; then
24179 echo \"foo bar\"
24180 ˇ
24181 "});
24182 cx.update_editor(|editor, window, cx| {
24183 editor.handle_input("elif", window, cx);
24184 });
24185 cx.assert_editor_state(indoc! {"
24186 if [ \"$1\" = \"test\" ]; then
24187 echo \"foo bar\"
24188 elifˇ
24189 "});
24190
24191 // test `fi` auto outdents when typed inside `else` block
24192 cx.set_state(indoc! {"
24193 if [ \"$1\" = \"test\" ]; then
24194 echo \"foo bar\"
24195 else
24196 echo \"bar baz\"
24197 ˇ
24198 "});
24199 cx.update_editor(|editor, window, cx| {
24200 editor.handle_input("fi", window, cx);
24201 });
24202 cx.assert_editor_state(indoc! {"
24203 if [ \"$1\" = \"test\" ]; then
24204 echo \"foo bar\"
24205 else
24206 echo \"bar baz\"
24207 fiˇ
24208 "});
24209
24210 // test `done` auto outdents when typed inside `while` block
24211 cx.set_state(indoc! {"
24212 while read line; do
24213 echo \"$line\"
24214 ˇ
24215 "});
24216 cx.update_editor(|editor, window, cx| {
24217 editor.handle_input("done", window, cx);
24218 });
24219 cx.assert_editor_state(indoc! {"
24220 while read line; do
24221 echo \"$line\"
24222 doneˇ
24223 "});
24224
24225 // test `done` auto outdents when typed inside `for` block
24226 cx.set_state(indoc! {"
24227 for file in *.txt; do
24228 cat \"$file\"
24229 ˇ
24230 "});
24231 cx.update_editor(|editor, window, cx| {
24232 editor.handle_input("done", window, cx);
24233 });
24234 cx.assert_editor_state(indoc! {"
24235 for file in *.txt; do
24236 cat \"$file\"
24237 doneˇ
24238 "});
24239
24240 // test `esac` auto outdents when typed inside `case` block
24241 cx.set_state(indoc! {"
24242 case \"$1\" in
24243 start)
24244 echo \"foo bar\"
24245 ;;
24246 stop)
24247 echo \"bar baz\"
24248 ;;
24249 ˇ
24250 "});
24251 cx.update_editor(|editor, window, cx| {
24252 editor.handle_input("esac", window, cx);
24253 });
24254 cx.assert_editor_state(indoc! {"
24255 case \"$1\" in
24256 start)
24257 echo \"foo bar\"
24258 ;;
24259 stop)
24260 echo \"bar baz\"
24261 ;;
24262 esacˇ
24263 "});
24264
24265 // test `*)` auto outdents when typed inside `case` block
24266 cx.set_state(indoc! {"
24267 case \"$1\" in
24268 start)
24269 echo \"foo bar\"
24270 ;;
24271 ˇ
24272 "});
24273 cx.update_editor(|editor, window, cx| {
24274 editor.handle_input("*)", window, cx);
24275 });
24276 cx.assert_editor_state(indoc! {"
24277 case \"$1\" in
24278 start)
24279 echo \"foo bar\"
24280 ;;
24281 *)ˇ
24282 "});
24283
24284 // test `fi` outdents to correct level with nested if blocks
24285 cx.set_state(indoc! {"
24286 if [ \"$1\" = \"test\" ]; then
24287 echo \"outer if\"
24288 if [ \"$2\" = \"debug\" ]; then
24289 echo \"inner if\"
24290 ˇ
24291 "});
24292 cx.update_editor(|editor, window, cx| {
24293 editor.handle_input("fi", window, cx);
24294 });
24295 cx.assert_editor_state(indoc! {"
24296 if [ \"$1\" = \"test\" ]; then
24297 echo \"outer if\"
24298 if [ \"$2\" = \"debug\" ]; then
24299 echo \"inner if\"
24300 fiˇ
24301 "});
24302}
24303
24304#[gpui::test]
24305async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24306 init_test(cx, |_| {});
24307 update_test_language_settings(cx, |settings| {
24308 settings.defaults.extend_comment_on_newline = Some(false);
24309 });
24310 let mut cx = EditorTestContext::new(cx).await;
24311 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24312 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24313
24314 // test correct indent after newline on comment
24315 cx.set_state(indoc! {"
24316 # COMMENT:ˇ
24317 "});
24318 cx.update_editor(|editor, window, cx| {
24319 editor.newline(&Newline, window, cx);
24320 });
24321 cx.assert_editor_state(indoc! {"
24322 # COMMENT:
24323 ˇ
24324 "});
24325
24326 // test correct indent after newline after `then`
24327 cx.set_state(indoc! {"
24328
24329 if [ \"$1\" = \"test\" ]; thenˇ
24330 "});
24331 cx.update_editor(|editor, window, cx| {
24332 editor.newline(&Newline, window, cx);
24333 });
24334 cx.run_until_parked();
24335 cx.assert_editor_state(indoc! {"
24336
24337 if [ \"$1\" = \"test\" ]; then
24338 ˇ
24339 "});
24340
24341 // test correct indent after newline after `else`
24342 cx.set_state(indoc! {"
24343 if [ \"$1\" = \"test\" ]; then
24344 elseˇ
24345 "});
24346 cx.update_editor(|editor, window, cx| {
24347 editor.newline(&Newline, window, cx);
24348 });
24349 cx.run_until_parked();
24350 cx.assert_editor_state(indoc! {"
24351 if [ \"$1\" = \"test\" ]; then
24352 else
24353 ˇ
24354 "});
24355
24356 // test correct indent after newline after `elif`
24357 cx.set_state(indoc! {"
24358 if [ \"$1\" = \"test\" ]; then
24359 elifˇ
24360 "});
24361 cx.update_editor(|editor, window, cx| {
24362 editor.newline(&Newline, window, cx);
24363 });
24364 cx.run_until_parked();
24365 cx.assert_editor_state(indoc! {"
24366 if [ \"$1\" = \"test\" ]; then
24367 elif
24368 ˇ
24369 "});
24370
24371 // test correct indent after newline after `do`
24372 cx.set_state(indoc! {"
24373 for file in *.txt; doˇ
24374 "});
24375 cx.update_editor(|editor, window, cx| {
24376 editor.newline(&Newline, window, cx);
24377 });
24378 cx.run_until_parked();
24379 cx.assert_editor_state(indoc! {"
24380 for file in *.txt; do
24381 ˇ
24382 "});
24383
24384 // test correct indent after newline after case pattern
24385 cx.set_state(indoc! {"
24386 case \"$1\" in
24387 start)ˇ
24388 "});
24389 cx.update_editor(|editor, window, cx| {
24390 editor.newline(&Newline, window, cx);
24391 });
24392 cx.run_until_parked();
24393 cx.assert_editor_state(indoc! {"
24394 case \"$1\" in
24395 start)
24396 ˇ
24397 "});
24398
24399 // test correct indent after newline after case pattern
24400 cx.set_state(indoc! {"
24401 case \"$1\" in
24402 start)
24403 ;;
24404 *)ˇ
24405 "});
24406 cx.update_editor(|editor, window, cx| {
24407 editor.newline(&Newline, window, cx);
24408 });
24409 cx.run_until_parked();
24410 cx.assert_editor_state(indoc! {"
24411 case \"$1\" in
24412 start)
24413 ;;
24414 *)
24415 ˇ
24416 "});
24417
24418 // test correct indent after newline after function opening brace
24419 cx.set_state(indoc! {"
24420 function test() {ˇ}
24421 "});
24422 cx.update_editor(|editor, window, cx| {
24423 editor.newline(&Newline, window, cx);
24424 });
24425 cx.run_until_parked();
24426 cx.assert_editor_state(indoc! {"
24427 function test() {
24428 ˇ
24429 }
24430 "});
24431
24432 // test no extra indent after semicolon on same line
24433 cx.set_state(indoc! {"
24434 echo \"test\";ˇ
24435 "});
24436 cx.update_editor(|editor, window, cx| {
24437 editor.newline(&Newline, window, cx);
24438 });
24439 cx.run_until_parked();
24440 cx.assert_editor_state(indoc! {"
24441 echo \"test\";
24442 ˇ
24443 "});
24444}
24445
24446fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24447 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24448 point..point
24449}
24450
24451#[track_caller]
24452fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24453 let (text, ranges) = marked_text_ranges(marked_text, true);
24454 assert_eq!(editor.text(cx), text);
24455 assert_eq!(
24456 editor.selections.ranges(cx),
24457 ranges,
24458 "Assert selections are {}",
24459 marked_text
24460 );
24461}
24462
24463pub fn handle_signature_help_request(
24464 cx: &mut EditorLspTestContext,
24465 mocked_response: lsp::SignatureHelp,
24466) -> impl Future<Output = ()> + use<> {
24467 let mut request =
24468 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24469 let mocked_response = mocked_response.clone();
24470 async move { Ok(Some(mocked_response)) }
24471 });
24472
24473 async move {
24474 request.next().await;
24475 }
24476}
24477
24478#[track_caller]
24479pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24480 cx.update_editor(|editor, _, _| {
24481 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24482 let entries = menu.entries.borrow();
24483 let entries = entries
24484 .iter()
24485 .map(|entry| entry.string.as_str())
24486 .collect::<Vec<_>>();
24487 assert_eq!(entries, expected);
24488 } else {
24489 panic!("Expected completions menu");
24490 }
24491 });
24492}
24493
24494/// Handle completion request passing a marked string specifying where the completion
24495/// should be triggered from using '|' character, what range should be replaced, and what completions
24496/// should be returned using '<' and '>' to delimit the range.
24497///
24498/// Also see `handle_completion_request_with_insert_and_replace`.
24499#[track_caller]
24500pub fn handle_completion_request(
24501 marked_string: &str,
24502 completions: Vec<&'static str>,
24503 is_incomplete: bool,
24504 counter: Arc<AtomicUsize>,
24505 cx: &mut EditorLspTestContext,
24506) -> impl Future<Output = ()> {
24507 let complete_from_marker: TextRangeMarker = '|'.into();
24508 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24509 let (_, mut marked_ranges) = marked_text_ranges_by(
24510 marked_string,
24511 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24512 );
24513
24514 let complete_from_position =
24515 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24516 let replace_range =
24517 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24518
24519 let mut request =
24520 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24521 let completions = completions.clone();
24522 counter.fetch_add(1, atomic::Ordering::Release);
24523 async move {
24524 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24525 assert_eq!(
24526 params.text_document_position.position,
24527 complete_from_position
24528 );
24529 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24530 is_incomplete,
24531 item_defaults: None,
24532 items: completions
24533 .iter()
24534 .map(|completion_text| lsp::CompletionItem {
24535 label: completion_text.to_string(),
24536 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24537 range: replace_range,
24538 new_text: completion_text.to_string(),
24539 })),
24540 ..Default::default()
24541 })
24542 .collect(),
24543 })))
24544 }
24545 });
24546
24547 async move {
24548 request.next().await;
24549 }
24550}
24551
24552/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
24553/// given instead, which also contains an `insert` range.
24554///
24555/// This function uses markers to define ranges:
24556/// - `|` marks the cursor position
24557/// - `<>` marks the replace range
24558/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
24559pub fn handle_completion_request_with_insert_and_replace(
24560 cx: &mut EditorLspTestContext,
24561 marked_string: &str,
24562 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
24563 counter: Arc<AtomicUsize>,
24564) -> impl Future<Output = ()> {
24565 let complete_from_marker: TextRangeMarker = '|'.into();
24566 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24567 let insert_range_marker: TextRangeMarker = ('{', '}').into();
24568
24569 let (_, mut marked_ranges) = marked_text_ranges_by(
24570 marked_string,
24571 vec![
24572 complete_from_marker.clone(),
24573 replace_range_marker.clone(),
24574 insert_range_marker.clone(),
24575 ],
24576 );
24577
24578 let complete_from_position =
24579 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24580 let replace_range =
24581 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24582
24583 let insert_range = match marked_ranges.remove(&insert_range_marker) {
24584 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
24585 _ => lsp::Range {
24586 start: replace_range.start,
24587 end: complete_from_position,
24588 },
24589 };
24590
24591 let mut request =
24592 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24593 let completions = completions.clone();
24594 counter.fetch_add(1, atomic::Ordering::Release);
24595 async move {
24596 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24597 assert_eq!(
24598 params.text_document_position.position, complete_from_position,
24599 "marker `|` position doesn't match",
24600 );
24601 Ok(Some(lsp::CompletionResponse::Array(
24602 completions
24603 .iter()
24604 .map(|(label, new_text)| lsp::CompletionItem {
24605 label: label.to_string(),
24606 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24607 lsp::InsertReplaceEdit {
24608 insert: insert_range,
24609 replace: replace_range,
24610 new_text: new_text.to_string(),
24611 },
24612 )),
24613 ..Default::default()
24614 })
24615 .collect(),
24616 )))
24617 }
24618 });
24619
24620 async move {
24621 request.next().await;
24622 }
24623}
24624
24625fn handle_resolve_completion_request(
24626 cx: &mut EditorLspTestContext,
24627 edits: Option<Vec<(&'static str, &'static str)>>,
24628) -> impl Future<Output = ()> {
24629 let edits = edits.map(|edits| {
24630 edits
24631 .iter()
24632 .map(|(marked_string, new_text)| {
24633 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
24634 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
24635 lsp::TextEdit::new(replace_range, new_text.to_string())
24636 })
24637 .collect::<Vec<_>>()
24638 });
24639
24640 let mut request =
24641 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
24642 let edits = edits.clone();
24643 async move {
24644 Ok(lsp::CompletionItem {
24645 additional_text_edits: edits,
24646 ..Default::default()
24647 })
24648 }
24649 });
24650
24651 async move {
24652 request.next().await;
24653 }
24654}
24655
24656pub(crate) fn update_test_language_settings(
24657 cx: &mut TestAppContext,
24658 f: impl Fn(&mut AllLanguageSettingsContent),
24659) {
24660 cx.update(|cx| {
24661 SettingsStore::update_global(cx, |store, cx| {
24662 store.update_user_settings::<AllLanguageSettings>(cx, f);
24663 });
24664 });
24665}
24666
24667pub(crate) fn update_test_project_settings(
24668 cx: &mut TestAppContext,
24669 f: impl Fn(&mut ProjectSettings),
24670) {
24671 cx.update(|cx| {
24672 SettingsStore::update_global(cx, |store, cx| {
24673 store.update_user_settings::<ProjectSettings>(cx, f);
24674 });
24675 });
24676}
24677
24678pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
24679 cx.update(|cx| {
24680 assets::Assets.load_test_fonts(cx);
24681 let store = SettingsStore::test(cx);
24682 cx.set_global(store);
24683 theme::init(theme::LoadThemes::JustBase, cx);
24684 release_channel::init(SemanticVersion::default(), cx);
24685 client::init_settings(cx);
24686 language::init(cx);
24687 Project::init_settings(cx);
24688 workspace::init_settings(cx);
24689 crate::init(cx);
24690 });
24691 zlog::init_test();
24692 update_test_language_settings(cx, f);
24693}
24694
24695#[track_caller]
24696fn assert_hunk_revert(
24697 not_reverted_text_with_selections: &str,
24698 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
24699 expected_reverted_text_with_selections: &str,
24700 base_text: &str,
24701 cx: &mut EditorLspTestContext,
24702) {
24703 cx.set_state(not_reverted_text_with_selections);
24704 cx.set_head_text(base_text);
24705 cx.executor().run_until_parked();
24706
24707 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
24708 let snapshot = editor.snapshot(window, cx);
24709 let reverted_hunk_statuses = snapshot
24710 .buffer_snapshot
24711 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
24712 .map(|hunk| hunk.status().kind)
24713 .collect::<Vec<_>>();
24714
24715 editor.git_restore(&Default::default(), window, cx);
24716 reverted_hunk_statuses
24717 });
24718 cx.executor().run_until_parked();
24719 cx.assert_editor_state(expected_reverted_text_with_selections);
24720 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
24721}
24722
24723#[gpui::test(iterations = 10)]
24724async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
24725 init_test(cx, |_| {});
24726
24727 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
24728 let counter = diagnostic_requests.clone();
24729
24730 let fs = FakeFs::new(cx.executor());
24731 fs.insert_tree(
24732 path!("/a"),
24733 json!({
24734 "first.rs": "fn main() { let a = 5; }",
24735 "second.rs": "// Test file",
24736 }),
24737 )
24738 .await;
24739
24740 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24741 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24742 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24743
24744 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24745 language_registry.add(rust_lang());
24746 let mut fake_servers = language_registry.register_fake_lsp(
24747 "Rust",
24748 FakeLspAdapter {
24749 capabilities: lsp::ServerCapabilities {
24750 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
24751 lsp::DiagnosticOptions {
24752 identifier: None,
24753 inter_file_dependencies: true,
24754 workspace_diagnostics: true,
24755 work_done_progress_options: Default::default(),
24756 },
24757 )),
24758 ..Default::default()
24759 },
24760 ..Default::default()
24761 },
24762 );
24763
24764 let editor = workspace
24765 .update(cx, |workspace, window, cx| {
24766 workspace.open_abs_path(
24767 PathBuf::from(path!("/a/first.rs")),
24768 OpenOptions::default(),
24769 window,
24770 cx,
24771 )
24772 })
24773 .unwrap()
24774 .await
24775 .unwrap()
24776 .downcast::<Editor>()
24777 .unwrap();
24778 let fake_server = fake_servers.next().await.unwrap();
24779 let server_id = fake_server.server.server_id();
24780 let mut first_request = fake_server
24781 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
24782 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
24783 let result_id = Some(new_result_id.to_string());
24784 assert_eq!(
24785 params.text_document.uri,
24786 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
24787 );
24788 async move {
24789 Ok(lsp::DocumentDiagnosticReportResult::Report(
24790 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
24791 related_documents: None,
24792 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
24793 items: Vec::new(),
24794 result_id,
24795 },
24796 }),
24797 ))
24798 }
24799 });
24800
24801 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
24802 project.update(cx, |project, cx| {
24803 let buffer_id = editor
24804 .read(cx)
24805 .buffer()
24806 .read(cx)
24807 .as_singleton()
24808 .expect("created a singleton buffer")
24809 .read(cx)
24810 .remote_id();
24811 let buffer_result_id = project
24812 .lsp_store()
24813 .read(cx)
24814 .result_id(server_id, buffer_id, cx);
24815 assert_eq!(expected, buffer_result_id);
24816 });
24817 };
24818
24819 ensure_result_id(None, cx);
24820 cx.executor().advance_clock(Duration::from_millis(60));
24821 cx.executor().run_until_parked();
24822 assert_eq!(
24823 diagnostic_requests.load(atomic::Ordering::Acquire),
24824 1,
24825 "Opening file should trigger diagnostic request"
24826 );
24827 first_request
24828 .next()
24829 .await
24830 .expect("should have sent the first diagnostics pull request");
24831 ensure_result_id(Some("1".to_string()), cx);
24832
24833 // Editing should trigger diagnostics
24834 editor.update_in(cx, |editor, window, cx| {
24835 editor.handle_input("2", window, cx)
24836 });
24837 cx.executor().advance_clock(Duration::from_millis(60));
24838 cx.executor().run_until_parked();
24839 assert_eq!(
24840 diagnostic_requests.load(atomic::Ordering::Acquire),
24841 2,
24842 "Editing should trigger diagnostic request"
24843 );
24844 ensure_result_id(Some("2".to_string()), cx);
24845
24846 // Moving cursor should not trigger diagnostic request
24847 editor.update_in(cx, |editor, window, cx| {
24848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24849 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
24850 });
24851 });
24852 cx.executor().advance_clock(Duration::from_millis(60));
24853 cx.executor().run_until_parked();
24854 assert_eq!(
24855 diagnostic_requests.load(atomic::Ordering::Acquire),
24856 2,
24857 "Cursor movement should not trigger diagnostic request"
24858 );
24859 ensure_result_id(Some("2".to_string()), cx);
24860 // Multiple rapid edits should be debounced
24861 for _ in 0..5 {
24862 editor.update_in(cx, |editor, window, cx| {
24863 editor.handle_input("x", window, cx)
24864 });
24865 }
24866 cx.executor().advance_clock(Duration::from_millis(60));
24867 cx.executor().run_until_parked();
24868
24869 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
24870 assert!(
24871 final_requests <= 4,
24872 "Multiple rapid edits should be debounced (got {final_requests} requests)",
24873 );
24874 ensure_result_id(Some(final_requests.to_string()), cx);
24875}
24876
24877#[gpui::test]
24878async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
24879 // Regression test for issue #11671
24880 // Previously, adding a cursor after moving multiple cursors would reset
24881 // the cursor count instead of adding to the existing cursors.
24882 init_test(cx, |_| {});
24883 let mut cx = EditorTestContext::new(cx).await;
24884
24885 // Create a simple buffer with cursor at start
24886 cx.set_state(indoc! {"
24887 ˇaaaa
24888 bbbb
24889 cccc
24890 dddd
24891 eeee
24892 ffff
24893 gggg
24894 hhhh"});
24895
24896 // Add 2 cursors below (so we have 3 total)
24897 cx.update_editor(|editor, window, cx| {
24898 editor.add_selection_below(&Default::default(), window, cx);
24899 editor.add_selection_below(&Default::default(), window, cx);
24900 });
24901
24902 // Verify we have 3 cursors
24903 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24904 assert_eq!(
24905 initial_count, 3,
24906 "Should have 3 cursors after adding 2 below"
24907 );
24908
24909 // Move down one line
24910 cx.update_editor(|editor, window, cx| {
24911 editor.move_down(&MoveDown, window, cx);
24912 });
24913
24914 // Add another cursor below
24915 cx.update_editor(|editor, window, cx| {
24916 editor.add_selection_below(&Default::default(), window, cx);
24917 });
24918
24919 // Should now have 4 cursors (3 original + 1 new)
24920 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24921 assert_eq!(
24922 final_count, 4,
24923 "Should have 4 cursors after moving and adding another"
24924 );
24925}
24926
24927#[gpui::test(iterations = 10)]
24928async fn test_document_colors(cx: &mut TestAppContext) {
24929 let expected_color = Rgba {
24930 r: 0.33,
24931 g: 0.33,
24932 b: 0.33,
24933 a: 0.33,
24934 };
24935
24936 init_test(cx, |_| {});
24937
24938 let fs = FakeFs::new(cx.executor());
24939 fs.insert_tree(
24940 path!("/a"),
24941 json!({
24942 "first.rs": "fn main() { let a = 5; }",
24943 }),
24944 )
24945 .await;
24946
24947 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24948 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24949 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24950
24951 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24952 language_registry.add(rust_lang());
24953 let mut fake_servers = language_registry.register_fake_lsp(
24954 "Rust",
24955 FakeLspAdapter {
24956 capabilities: lsp::ServerCapabilities {
24957 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24958 ..lsp::ServerCapabilities::default()
24959 },
24960 name: "rust-analyzer",
24961 ..FakeLspAdapter::default()
24962 },
24963 );
24964 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24965 "Rust",
24966 FakeLspAdapter {
24967 capabilities: lsp::ServerCapabilities {
24968 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24969 ..lsp::ServerCapabilities::default()
24970 },
24971 name: "not-rust-analyzer",
24972 ..FakeLspAdapter::default()
24973 },
24974 );
24975
24976 let editor = workspace
24977 .update(cx, |workspace, window, cx| {
24978 workspace.open_abs_path(
24979 PathBuf::from(path!("/a/first.rs")),
24980 OpenOptions::default(),
24981 window,
24982 cx,
24983 )
24984 })
24985 .unwrap()
24986 .await
24987 .unwrap()
24988 .downcast::<Editor>()
24989 .unwrap();
24990 let fake_language_server = fake_servers.next().await.unwrap();
24991 let fake_language_server_without_capabilities =
24992 fake_servers_without_capabilities.next().await.unwrap();
24993 let requests_made = Arc::new(AtomicUsize::new(0));
24994 let closure_requests_made = Arc::clone(&requests_made);
24995 let mut color_request_handle = fake_language_server
24996 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
24997 let requests_made = Arc::clone(&closure_requests_made);
24998 async move {
24999 assert_eq!(
25000 params.text_document.uri,
25001 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25002 );
25003 requests_made.fetch_add(1, atomic::Ordering::Release);
25004 Ok(vec![
25005 lsp::ColorInformation {
25006 range: lsp::Range {
25007 start: lsp::Position {
25008 line: 0,
25009 character: 0,
25010 },
25011 end: lsp::Position {
25012 line: 0,
25013 character: 1,
25014 },
25015 },
25016 color: lsp::Color {
25017 red: 0.33,
25018 green: 0.33,
25019 blue: 0.33,
25020 alpha: 0.33,
25021 },
25022 },
25023 lsp::ColorInformation {
25024 range: lsp::Range {
25025 start: lsp::Position {
25026 line: 0,
25027 character: 0,
25028 },
25029 end: lsp::Position {
25030 line: 0,
25031 character: 1,
25032 },
25033 },
25034 color: lsp::Color {
25035 red: 0.33,
25036 green: 0.33,
25037 blue: 0.33,
25038 alpha: 0.33,
25039 },
25040 },
25041 ])
25042 }
25043 });
25044
25045 let _handle = fake_language_server_without_capabilities
25046 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25047 panic!("Should not be called");
25048 });
25049 cx.executor().advance_clock(Duration::from_millis(100));
25050 color_request_handle.next().await.unwrap();
25051 cx.run_until_parked();
25052 assert_eq!(
25053 1,
25054 requests_made.load(atomic::Ordering::Acquire),
25055 "Should query for colors once per editor open"
25056 );
25057 editor.update_in(cx, |editor, _, cx| {
25058 assert_eq!(
25059 vec![expected_color],
25060 extract_color_inlays(editor, cx),
25061 "Should have an initial inlay"
25062 );
25063 });
25064
25065 // opening another file in a split should not influence the LSP query counter
25066 workspace
25067 .update(cx, |workspace, window, cx| {
25068 assert_eq!(
25069 workspace.panes().len(),
25070 1,
25071 "Should have one pane with one editor"
25072 );
25073 workspace.move_item_to_pane_in_direction(
25074 &MoveItemToPaneInDirection {
25075 direction: SplitDirection::Right,
25076 focus: false,
25077 clone: true,
25078 },
25079 window,
25080 cx,
25081 );
25082 })
25083 .unwrap();
25084 cx.run_until_parked();
25085 workspace
25086 .update(cx, |workspace, _, cx| {
25087 let panes = workspace.panes();
25088 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25089 for pane in panes {
25090 let editor = pane
25091 .read(cx)
25092 .active_item()
25093 .and_then(|item| item.downcast::<Editor>())
25094 .expect("Should have opened an editor in each split");
25095 let editor_file = editor
25096 .read(cx)
25097 .buffer()
25098 .read(cx)
25099 .as_singleton()
25100 .expect("test deals with singleton buffers")
25101 .read(cx)
25102 .file()
25103 .expect("test buffese should have a file")
25104 .path();
25105 assert_eq!(
25106 editor_file.as_ref(),
25107 Path::new("first.rs"),
25108 "Both editors should be opened for the same file"
25109 )
25110 }
25111 })
25112 .unwrap();
25113
25114 cx.executor().advance_clock(Duration::from_millis(500));
25115 let save = editor.update_in(cx, |editor, window, cx| {
25116 editor.move_to_end(&MoveToEnd, window, cx);
25117 editor.handle_input("dirty", window, cx);
25118 editor.save(
25119 SaveOptions {
25120 format: true,
25121 autosave: true,
25122 },
25123 project.clone(),
25124 window,
25125 cx,
25126 )
25127 });
25128 save.await.unwrap();
25129
25130 color_request_handle.next().await.unwrap();
25131 cx.run_until_parked();
25132 assert_eq!(
25133 3,
25134 requests_made.load(atomic::Ordering::Acquire),
25135 "Should query for colors once per save and once per formatting after save"
25136 );
25137
25138 drop(editor);
25139 let close = workspace
25140 .update(cx, |workspace, window, cx| {
25141 workspace.active_pane().update(cx, |pane, cx| {
25142 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25143 })
25144 })
25145 .unwrap();
25146 close.await.unwrap();
25147 let close = workspace
25148 .update(cx, |workspace, window, cx| {
25149 workspace.active_pane().update(cx, |pane, cx| {
25150 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25151 })
25152 })
25153 .unwrap();
25154 close.await.unwrap();
25155 assert_eq!(
25156 3,
25157 requests_made.load(atomic::Ordering::Acquire),
25158 "After saving and closing all editors, no extra requests should be made"
25159 );
25160 workspace
25161 .update(cx, |workspace, _, cx| {
25162 assert!(
25163 workspace.active_item(cx).is_none(),
25164 "Should close all editors"
25165 )
25166 })
25167 .unwrap();
25168
25169 workspace
25170 .update(cx, |workspace, window, cx| {
25171 workspace.active_pane().update(cx, |pane, cx| {
25172 pane.navigate_backward(&Default::default(), window, cx);
25173 })
25174 })
25175 .unwrap();
25176 cx.executor().advance_clock(Duration::from_millis(100));
25177 cx.run_until_parked();
25178 let editor = workspace
25179 .update(cx, |workspace, _, cx| {
25180 workspace
25181 .active_item(cx)
25182 .expect("Should have reopened the editor again after navigating back")
25183 .downcast::<Editor>()
25184 .expect("Should be an editor")
25185 })
25186 .unwrap();
25187 color_request_handle.next().await.unwrap();
25188 assert_eq!(
25189 3,
25190 requests_made.load(atomic::Ordering::Acquire),
25191 "Cache should be reused on buffer close and reopen"
25192 );
25193 editor.update(cx, |editor, cx| {
25194 assert_eq!(
25195 vec![expected_color],
25196 extract_color_inlays(editor, cx),
25197 "Should have an initial inlay"
25198 );
25199 });
25200}
25201
25202#[gpui::test]
25203async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25204 init_test(cx, |_| {});
25205 let (editor, cx) = cx.add_window_view(Editor::single_line);
25206 editor.update_in(cx, |editor, window, cx| {
25207 editor.set_text("oops\n\nwow\n", window, cx)
25208 });
25209 cx.run_until_parked();
25210 editor.update(cx, |editor, cx| {
25211 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25212 });
25213 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25214 cx.run_until_parked();
25215 editor.update(cx, |editor, cx| {
25216 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25217 });
25218}
25219
25220#[gpui::test]
25221async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25222 init_test(cx, |_| {});
25223
25224 cx.update(|cx| {
25225 register_project_item::<Editor>(cx);
25226 });
25227
25228 let fs = FakeFs::new(cx.executor());
25229 fs.insert_tree("/root1", json!({})).await;
25230 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25231 .await;
25232
25233 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25234 let (workspace, cx) =
25235 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25236
25237 let worktree_id = project.update(cx, |project, cx| {
25238 project.worktrees(cx).next().unwrap().read(cx).id()
25239 });
25240
25241 let handle = workspace
25242 .update_in(cx, |workspace, window, cx| {
25243 let project_path = (worktree_id, "one.pdf");
25244 workspace.open_path(project_path, None, true, window, cx)
25245 })
25246 .await
25247 .unwrap();
25248
25249 assert_eq!(
25250 handle.to_any().entity_type(),
25251 TypeId::of::<InvalidBufferView>()
25252 );
25253}
25254
25255#[track_caller]
25256fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25257 editor
25258 .all_inlays(cx)
25259 .into_iter()
25260 .filter_map(|inlay| inlay.get_color())
25261 .map(Rgba::from)
25262 .collect()
25263}